Merge "AuthManager phpdoc cleanup"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 26 Sep 2016 22:25:08 +0000 (22:25 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 26 Sep 2016 22:25:08 +0000 (22:25 +0000)
170 files changed:
autoload.php
docs/hooks.txt
includes/CategoryViewer.php
includes/DefaultSettings.php
includes/EditPage.php
includes/FileDeleteForm.php
includes/GlobalFunctions.php
includes/Linker.php
includes/Message.php
includes/OutputPage.php
includes/StreamFile.php
includes/Title.php
includes/api/ApiDelete.php
includes/api/i18n/ast.json
includes/api/i18n/bg.json
includes/api/i18n/hr.json [new file with mode: 0644]
includes/auth/LocalPasswordPrimaryAuthenticationProvider.php
includes/clientpool/RedisConnectionPool.php [deleted file]
includes/deferred/DeferredUpdates.php
includes/deferred/LinksUpdate.php
includes/filebackend/FSFileBackend.php [deleted file]
includes/filebackend/FileBackendGroup.php
includes/filebackend/FileBackendMultiWrite.php [deleted file]
includes/filebackend/FileBackendStore.php [deleted file]
includes/filebackend/FileOp.php [deleted file]
includes/filebackend/FileOpBatch.php [deleted file]
includes/filebackend/MemoryFileBackend.php [deleted file]
includes/filebackend/SwiftFileBackend.php [deleted file]
includes/filebackend/lockmanager/DBLockManager.php [deleted file]
includes/filebackend/lockmanager/LockManagerGroup.php
includes/filebackend/lockmanager/MySqlLockManager.php
includes/filebackend/lockmanager/PostgreSqlLockManager.php [deleted file]
includes/filebackend/lockmanager/RedisLockManager.php [deleted file]
includes/filerepo/ForeignAPIRepo.php
includes/htmlform/HTMLForm.php
includes/htmlform/HTMLFormField.php
includes/htmlform/fields/HTMLDateTimeField.php [new file with mode: 0644]
includes/htmlform/fields/HTMLRestrictionsField.php [new file with mode: 0644]
includes/installer/DatabaseInstaller.php
includes/installer/DatabaseUpdater.php
includes/installer/i18n/diq.json
includes/installer/i18n/nb.json
includes/jobqueue/JobQueueRedis.php
includes/jobqueue/aggregator/JobQueueAggregatorRedis.php
includes/libs/ScopedCallback.php
includes/libs/filebackend/FSFile.php
includes/libs/filebackend/FSFileBackend.php [new file with mode: 0644]
includes/libs/filebackend/FileBackend.php
includes/libs/filebackend/FileBackendMultiWrite.php [new file with mode: 0644]
includes/libs/filebackend/FileBackendStore.php [new file with mode: 0644]
includes/libs/filebackend/FileOpBatch.php [new file with mode: 0644]
includes/libs/filebackend/HTTPFileStreamer.php [new file with mode: 0644]
includes/libs/filebackend/MemoryFileBackend.php [new file with mode: 0644]
includes/libs/filebackend/SwiftFileBackend.php [new file with mode: 0644]
includes/libs/filebackend/fileop/CopyFileOp.php [new file with mode: 0644]
includes/libs/filebackend/fileop/CreateFileOp.php [new file with mode: 0644]
includes/libs/filebackend/fileop/DeleteFileOp.php [new file with mode: 0644]
includes/libs/filebackend/fileop/DescribeFileOp.php [new file with mode: 0644]
includes/libs/filebackend/fileop/FileOp.php [new file with mode: 0644]
includes/libs/filebackend/fileop/MoveFileOp.php [new file with mode: 0644]
includes/libs/filebackend/fileop/NullFileOp.php [new file with mode: 0644]
includes/libs/filebackend/fileop/StoreFileOp.php [new file with mode: 0644]
includes/libs/lockmanager/DBLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/LockManager.php
includes/libs/lockmanager/PostgreSqlLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/RedisLockManager.php [new file with mode: 0644]
includes/libs/objectcache/RedisBagOStuff.php [new file with mode: 0644]
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMysql.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabaseMysqli.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/database/IMaintainableDatabase.php [new file with mode: 0644]
includes/libs/rdbms/defines.php
includes/libs/rdbms/lbfactory/ILBFactory.php [new file with mode: 0644]
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/lbfactory/LBFactoryMulti.php
includes/libs/rdbms/lbfactory/LBFactorySimple.php
includes/libs/rdbms/lbfactory/LBFactorySingle.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/libs/redis/RedisConnRef.php [new file with mode: 0644]
includes/libs/redis/RedisConnectionPool.php [new file with mode: 0644]
includes/libs/time/ConvertibleTimestamp.php
includes/objectcache/RedisBagOStuff.php [deleted file]
includes/page/Article.php
includes/page/WikiPage.php
includes/poolcounter/PoolCounterRedis.php
includes/search/SearchIndexField.php
includes/specials/SpecialBotPasswords.php
includes/specials/SpecialChangeContentModel.php
includes/specials/SpecialRecentchanges.php
includes/utils/MWRestrictions.php
includes/widget/AUTHORS.txt
includes/widget/DateTimeInputWidget.php [new file with mode: 0644]
languages/i18n/ace.json
languages/i18n/ar.json
languages/i18n/ast.json
languages/i18n/ba.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bg.json
languages/i18n/bn.json
languages/i18n/bs.json
languages/i18n/ca.json
languages/i18n/cdo.json
languages/i18n/ce.json
languages/i18n/cs.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/dty.json
languages/i18n/egl.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gl.json
languages/i18n/got.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/hy.json
languages/i18n/id.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/ka.json
languages/i18n/kk-cyrl.json
languages/i18n/kn.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/lt.json
languages/i18n/lv.json
languages/i18n/mai.json
languages/i18n/mk.json
languages/i18n/nb.json
languages/i18n/nl.json
languages/i18n/or.json
languages/i18n/pl.json
languages/i18n/pnb.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/ro.json
languages/i18n/ru.json
languages/i18n/sah.json
languages/i18n/sk.json
languages/i18n/sl.json
languages/i18n/sv.json
languages/i18n/tr.json
languages/i18n/tt-cyrl.json
languages/i18n/uk.json
languages/i18n/ur.json
languages/i18n/yi.json
languages/i18n/zh-hans.json
resources/Resources.php
resources/src/mediawiki.widgets.datetime/DateTimeInputWidget.js
resources/src/mediawiki/htmlform/datetime.js [new file with mode: 0644]
resources/src/mediawiki/mediawiki.Upload.Dialog.js
resources/src/mediawiki/mediawiki.js
resources/src/mediawiki/mediawiki.log.js
tests/parser/parserTests.txt
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/db/DatabaseMysqlBaseTest.php
tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php [new file with mode: 0644]

index a352884..ec15c99 100644 (file)
@@ -286,13 +286,13 @@ $wgAutoloadLocalClasses = [
        'Cookie' => __DIR__ . '/includes/libs/Cookie.php',
        'CookieJar' => __DIR__ . '/includes/libs/CookieJar.php',
        'CopyFileBackend' => __DIR__ . '/maintenance/copyFileBackend.php',
-       'CopyFileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
+       'CopyFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/CopyFileOp.php',
        'CopyJobQueue' => __DIR__ . '/maintenance/copyJobQueue.php',
        'CoreParserFunctions' => __DIR__ . '/includes/parser/CoreParserFunctions.php',
        'CoreTagHooks' => __DIR__ . '/includes/parser/CoreTagHooks.php',
        'CoreVersionChecker' => __DIR__ . '/includes/registration/CoreVersionChecker.php',
        'CreateAndPromote' => __DIR__ . '/maintenance/createAndPromote.php',
-       'CreateFileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
+       'CreateFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/CreateFileOp.php',
        'CreditsAction' => __DIR__ . '/includes/actions/CreditsAction.php',
        'CssContent' => __DIR__ . '/includes/content/CssContent.php',
        'CssContentHandler' => __DIR__ . '/includes/content/CssContentHandler.php',
@@ -306,7 +306,7 @@ $wgAutoloadLocalClasses = [
        'DBError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
        'DBExpectedError' => __DIR__ . '/includes/libs/rdbms/exception/DBExpectedError.php',
        'DBFileJournal' => __DIR__ . '/includes/filebackend/filejournal/DBFileJournal.php',
-       'DBLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php',
+       'DBLockManager' => __DIR__ . '/includes/libs/lockmanager/DBLockManager.php',
        'DBMasterPos' => __DIR__ . '/includes/libs/rdbms/database/position/DBMasterPos.php',
        'DBQueryError' => __DIR__ . '/includes/libs/rdbms/exception/DBQueryError.php',
        'DBReadOnlyError' => __DIR__ . '/includes/libs/rdbms/exception/DBReadOnlyError.php',
@@ -343,7 +343,7 @@ $wgAutoloadLocalClasses = [
        'DeleteBatch' => __DIR__ . '/maintenance/deleteBatch.php',
        'DeleteDefaultMessages' => __DIR__ . '/maintenance/deleteDefaultMessages.php',
        'DeleteEqualMessages' => __DIR__ . '/maintenance/deleteEqualMessages.php',
-       'DeleteFileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
+       'DeleteFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/DeleteFileOp.php',
        'DeleteLinksJob' => __DIR__ . '/includes/jobqueue/jobs/DeleteLinksJob.php',
        'DeleteLogFormatter' => __DIR__ . '/includes/logging/DeleteLogFormatter.php',
        'DeleteOldRevisions' => __DIR__ . '/maintenance/deleteOldRevisions.php',
@@ -358,7 +358,7 @@ $wgAutoloadLocalClasses = [
        'DerivativeContext' => __DIR__ . '/includes/context/DerivativeContext.php',
        'DerivativeRequest' => __DIR__ . '/includes/DerivativeRequest.php',
        'DerivativeResourceLoaderContext' => __DIR__ . '/includes/resourceloader/DerivativeResourceLoaderContext.php',
-       'DescribeFileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
+       'DescribeFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/DescribeFileOp.php',
        'Diff' => __DIR__ . '/includes/diff/DairikiDiff.php',
        'DiffEngine' => __DIR__ . '/includes/diff/DiffEngine.php',
        'DiffFormatter' => __DIR__ . '/includes/diff/DiffFormatter.php',
@@ -433,11 +433,11 @@ $wgAutoloadLocalClasses = [
        'ExternalStoreMedium' => __DIR__ . '/includes/externalstore/ExternalStoreMedium.php',
        'ExternalStoreMwstore' => __DIR__ . '/includes/externalstore/ExternalStoreMwstore.php',
        'FSFile' => __DIR__ . '/includes/libs/filebackend/FSFile.php',
-       'FSFileBackend' => __DIR__ . '/includes/filebackend/FSFileBackend.php',
-       'FSFileBackendDirList' => __DIR__ . '/includes/filebackend/FSFileBackend.php',
-       'FSFileBackendFileList' => __DIR__ . '/includes/filebackend/FSFileBackend.php',
-       'FSFileBackendList' => __DIR__ . '/includes/filebackend/FSFileBackend.php',
-       'FSFileOpHandle' => __DIR__ . '/includes/filebackend/FSFileBackend.php',
+       'FSFileBackend' => __DIR__ . '/includes/libs/filebackend/FSFileBackend.php',
+       'FSFileBackendDirList' => __DIR__ . '/includes/libs/filebackend/FSFileBackend.php',
+       'FSFileBackendFileList' => __DIR__ . '/includes/libs/filebackend/FSFileBackend.php',
+       'FSFileBackendList' => __DIR__ . '/includes/libs/filebackend/FSFileBackend.php',
+       'FSFileOpHandle' => __DIR__ . '/includes/libs/filebackend/FSFileBackend.php',
        'FSLockManager' => __DIR__ . '/includes/libs/lockmanager/FSLockManager.php',
        'FSRepo' => __DIR__ . '/includes/filerepo/FSRepo.php',
        'FakeAuthTemplate' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php',
@@ -458,12 +458,12 @@ $wgAutoloadLocalClasses = [
        'FileBackendDBRepoWrapper' => __DIR__ . '/includes/filerepo/FileBackendDBRepoWrapper.php',
        'FileBackendError' => __DIR__ . '/includes/libs/filebackend/FileBackendError.php',
        'FileBackendGroup' => __DIR__ . '/includes/filebackend/FileBackendGroup.php',
-       'FileBackendMultiWrite' => __DIR__ . '/includes/filebackend/FileBackendMultiWrite.php',
-       'FileBackendStore' => __DIR__ . '/includes/filebackend/FileBackendStore.php',
-       'FileBackendStoreOpHandle' => __DIR__ . '/includes/filebackend/FileBackendStore.php',
-       'FileBackendStoreShardDirIterator' => __DIR__ . '/includes/filebackend/FileBackendStore.php',
-       'FileBackendStoreShardFileIterator' => __DIR__ . '/includes/filebackend/FileBackendStore.php',
-       'FileBackendStoreShardListIterator' => __DIR__ . '/includes/filebackend/FileBackendStore.php',
+       'FileBackendMultiWrite' => __DIR__ . '/includes/libs/filebackend/FileBackendMultiWrite.php',
+       'FileBackendStore' => __DIR__ . '/includes/libs/filebackend/FileBackendStore.php',
+       'FileBackendStoreOpHandle' => __DIR__ . '/includes/libs/filebackend/FileBackendStore.php',
+       'FileBackendStoreShardDirIterator' => __DIR__ . '/includes/libs/filebackend/FileBackendStore.php',
+       'FileBackendStoreShardFileIterator' => __DIR__ . '/includes/libs/filebackend/FileBackendStore.php',
+       'FileBackendStoreShardListIterator' => __DIR__ . '/includes/libs/filebackend/FileBackendStore.php',
        'FileBasedSiteLookup' => __DIR__ . '/includes/site/FileBasedSiteLookup.php',
        'FileCacheBase' => __DIR__ . '/includes/cache/FileCacheBase.php',
        'FileContentsHasher' => __DIR__ . '/includes/utils/FileContentsHasher.php',
@@ -471,8 +471,8 @@ $wgAutoloadLocalClasses = [
        'FileDependency' => __DIR__ . '/includes/cache/CacheDependency.php',
        'FileDuplicateSearchPage' => __DIR__ . '/includes/specials/SpecialFileDuplicateSearch.php',
        'FileJournal' => __DIR__ . '/includes/libs/filebackend/filejournal/FileJournal.php',
-       'FileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
-       'FileOpBatch' => __DIR__ . '/includes/filebackend/FileOpBatch.php',
+       'FileOp' => __DIR__ . '/includes/libs/filebackend/fileop/FileOp.php',
+       'FileOpBatch' => __DIR__ . '/includes/libs/filebackend/FileOpBatch.php',
        'FileRepo' => __DIR__ . '/includes/filerepo/FileRepo.php',
        'FileRepoStatus' => __DIR__ . '/includes/filerepo/FileRepoStatus.php',
        'FindDeprecated' => __DIR__ . '/maintenance/findDeprecated.php',
@@ -526,6 +526,7 @@ $wgAutoloadLocalClasses = [
        'HTMLCheckField' => __DIR__ . '/includes/htmlform/fields/HTMLCheckField.php',
        'HTMLCheckMatrix' => __DIR__ . '/includes/htmlform/fields/HTMLCheckMatrix.php',
        'HTMLComboboxField' => __DIR__ . '/includes/htmlform/fields/HTMLComboboxField.php',
+       'HTMLDateTimeField' => __DIR__ . '/includes/htmlform/fields/HTMLDateTimeField.php',
        'HTMLEditTools' => __DIR__ . '/includes/htmlform/fields/HTMLEditTools.php',
        'HTMLFileCache' => __DIR__ . '/includes/cache/HTMLFileCache.php',
        'HTMLFloatField' => __DIR__ . '/includes/htmlform/fields/HTMLFloatField.php',
@@ -543,6 +544,7 @@ $wgAutoloadLocalClasses = [
        'HTMLMultiSelectField' => __DIR__ . '/includes/htmlform/fields/HTMLMultiSelectField.php',
        'HTMLNestedFilterable' => __DIR__ . '/includes/htmlform/HTMLNestedFilterable.php',
        'HTMLRadioField' => __DIR__ . '/includes/htmlform/fields/HTMLRadioField.php',
+       'HTMLRestrictionsField' => __DIR__ . '/includes/htmlform/fields/HTMLRestrictionsField.php',
        'HTMLSelectAndOtherField' => __DIR__ . '/includes/htmlform/fields/HTMLSelectAndOtherField.php',
        'HTMLSelectField' => __DIR__ . '/includes/htmlform/fields/HTMLSelectField.php',
        'HTMLSelectLimitField' => __DIR__ . '/includes/htmlform/fields/HTMLSelectLimitField.php',
@@ -556,6 +558,7 @@ $wgAutoloadLocalClasses = [
        'HTMLTextFieldWithButton' => __DIR__ . '/includes/htmlform/fields/HTMLTextFieldWithButton.php',
        'HTMLTitleTextField' => __DIR__ . '/includes/htmlform/fields/HTMLTitleTextField.php',
        'HTMLUserTextField' => __DIR__ . '/includes/htmlform/fields/HTMLUserTextField.php',
+       'HTTPFileStreamer' => __DIR__ . '/includes/libs/filebackend/HTTPFileStreamer.php',
        'HWLDFWordAccumulator' => __DIR__ . '/includes/diff/DairikiDiff.php',
        'HashBagOStuff' => __DIR__ . '/includes/libs/objectcache/HashBagOStuff.php',
        'HashConfig' => __DIR__ . '/includes/config/HashConfig.php',
@@ -583,8 +586,10 @@ $wgAutoloadLocalClasses = [
        'IEUrlExtension' => __DIR__ . '/includes/libs/IEUrlExtension.php',
        'IExpiringStore' => __DIR__ . '/includes/libs/objectcache/IExpiringStore.php',
        'IJobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php',
+       'ILBFactory' => __DIR__ . '/includes/libs/rdbms/lbfactory/ILBFactory.php',
        'ILoadBalancer' => __DIR__ . '/includes/libs/rdbms/loadbalancer/ILoadBalancer.php',
        'ILoadMonitor' => __DIR__ . '/includes/libs/rdbms/loadmonitor/ILoadMonitor.php',
+       'IMaintainableDatabase' => __DIR__ . '/includes/libs/rdbms/database/IMaintainableDatabase.php',
        'IP' => __DIR__ . '/includes/libs/IP.php',
        'IPSet' => __DIR__ . '/includes/compat/IPSetCompat.php',
        'IPTC' => __DIR__ . '/includes/media/IPTC.php',
@@ -909,6 +914,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Tidy\\TidyDriverBase' => __DIR__ . '/includes/tidy/TidyDriverBase.php',
        'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php',
        'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php',
+       'MediaWiki\\Widget\\DateTimeInputWidget' => __DIR__ . '/includes/widget/DateTimeInputWidget.php',
        'MediaWiki\\Widget\\NamespaceInputWidget' => __DIR__ . '/includes/widget/NamespaceInputWidget.php',
        'MediaWiki\\Widget\\SearchInputWidget' => __DIR__ . '/includes/widget/SearchInputWidget.php',
        'MediaWiki\\Widget\\TitleInputWidget' => __DIR__ . '/includes/widget/TitleInputWidget.php',
@@ -920,7 +926,7 @@ $wgAutoloadLocalClasses = [
        'MemcachedPeclBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPeclBagOStuff.php',
        'MemcachedPhpBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPhpBagOStuff.php',
        'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
-       'MemoryFileBackend' => __DIR__ . '/includes/filebackend/MemoryFileBackend.php',
+       'MemoryFileBackend' => __DIR__ . '/includes/libs/filebackend/MemoryFileBackend.php',
        'MergeHistory' => __DIR__ . '/includes/MergeHistory.php',
        'MergeHistoryPager' => __DIR__ . '/includes/specials/pagers/MergeHistoryPager.php',
        'MergeLogFormatter' => __DIR__ . '/includes/logging/MergeLogFormatter.php',
@@ -943,7 +949,7 @@ $wgAutoloadLocalClasses = [
        'MostlinkedTemplatesPage' => __DIR__ . '/includes/specials/SpecialMostlinkedtemplates.php',
        'MostrevisionsPage' => __DIR__ . '/includes/specials/SpecialMostrevisions.php',
        'MoveBatch' => __DIR__ . '/maintenance/moveBatch.php',
-       'MoveFileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
+       'MoveFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/MoveFileOp.php',
        'MoveLogFormatter' => __DIR__ . '/includes/logging/MoveLogFormatter.php',
        'MovePage' => __DIR__ . '/includes/MovePage.php',
        'MovePageForm' => __DIR__ . '/includes/specials/SpecialMovepage.php',
@@ -976,7 +982,7 @@ $wgAutoloadLocalClasses = [
        'NukeNS' => __DIR__ . '/maintenance/nukeNS.php',
        'NukePage' => __DIR__ . '/maintenance/nukePage.php',
        'NullFileJournal' => __DIR__ . '/includes/libs/filebackend/filejournal/NullFileJournal.php',
-       'NullFileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
+       'NullFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/NullFileOp.php',
        'NullIndexField' => __DIR__ . '/includes/search/NullIndexField.php',
        'NullJob' => __DIR__ . '/includes/jobqueue/jobs/NullJob.php',
        'NullLockManager' => __DIR__ . '/includes/libs/lockmanager/NullLockManager.php',
@@ -1069,7 +1075,7 @@ $wgAutoloadLocalClasses = [
        'PopulateRecentChangesSource' => __DIR__ . '/maintenance/populateRecentChangesSource.php',
        'PopulateRevisionLength' => __DIR__ . '/maintenance/populateRevisionLength.php',
        'PopulateRevisionSha1' => __DIR__ . '/maintenance/populateRevisionSha1.php',
-       'PostgreSqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/PostgreSqlLockManager.php',
+       'PostgreSqlLockManager' => __DIR__ . '/includes/libs/lockmanager/PostgreSqlLockManager.php',
        'PostgresBlob' => __DIR__ . '/includes/libs/rdbms/encasing/PostgresBlob.php',
        'PostgresField' => __DIR__ . '/includes/libs/rdbms/field/PostgresField.php',
        'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php',
@@ -1137,10 +1143,10 @@ $wgAutoloadLocalClasses = [
        'RecompressTracked' => __DIR__ . '/maintenance/storage/recompressTracked.php',
        'RedirectSpecialArticle' => __DIR__ . '/includes/specialpage/RedirectSpecialPage.php',
        'RedirectSpecialPage' => __DIR__ . '/includes/specialpage/RedirectSpecialPage.php',
-       'RedisBagOStuff' => __DIR__ . '/includes/objectcache/RedisBagOStuff.php',
-       'RedisConnRef' => __DIR__ . '/includes/clientpool/RedisConnectionPool.php',
-       'RedisConnectionPool' => __DIR__ . '/includes/clientpool/RedisConnectionPool.php',
-       'RedisLockManager' => __DIR__ . '/includes/filebackend/lockmanager/RedisLockManager.php',
+       'RedisBagOStuff' => __DIR__ . '/includes/libs/objectcache/RedisBagOStuff.php',
+       'RedisConnRef' => __DIR__ . '/includes/libs/redis/RedisConnRef.php',
+       'RedisConnectionPool' => __DIR__ . '/includes/libs/redis/RedisConnectionPool.php',
+       'RedisLockManager' => __DIR__ . '/includes/libs/lockmanager/RedisLockManager.php',
        'RedisPubSubFeedEngine' => __DIR__ . '/includes/rcfeed/RedisPubSubFeedEngine.php',
        'RefreshFileHeaders' => __DIR__ . '/maintenance/refreshFileHeaders.php',
        'RefreshImageMetadata' => __DIR__ . '/maintenance/refreshImageMetadata.php',
@@ -1382,7 +1388,7 @@ $wgAutoloadLocalClasses = [
        'Status' => __DIR__ . '/includes/Status.php',
        'StatusValue' => __DIR__ . '/includes/libs/StatusValue.php',
        'StorageTypeStats' => __DIR__ . '/maintenance/storage/storageTypeStats.php',
-       'StoreFileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
+       'StoreFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/StoreFileOp.php',
        'StreamFile' => __DIR__ . '/includes/StreamFile.php',
        'StringPrefixSearch' => __DIR__ . '/includes/PrefixSearch.php',
        'StringUtils' => __DIR__ . '/includes/libs/StringUtils.php',
@@ -1392,11 +1398,11 @@ $wgAutoloadLocalClasses = [
        'SubmitAction' => __DIR__ . '/includes/actions/SubmitAction.php',
        'SubpageImportTitleFactory' => __DIR__ . '/includes/title/SubpageImportTitleFactory.php',
        'SvgHandler' => __DIR__ . '/includes/media/SVG.php',
-       'SwiftFileBackend' => __DIR__ . '/includes/filebackend/SwiftFileBackend.php',
-       'SwiftFileBackendDirList' => __DIR__ . '/includes/filebackend/SwiftFileBackend.php',
-       'SwiftFileBackendFileList' => __DIR__ . '/includes/filebackend/SwiftFileBackend.php',
-       'SwiftFileBackendList' => __DIR__ . '/includes/filebackend/SwiftFileBackend.php',
-       'SwiftFileOpHandle' => __DIR__ . '/includes/filebackend/SwiftFileBackend.php',
+       'SwiftFileBackend' => __DIR__ . '/includes/libs/filebackend/SwiftFileBackend.php',
+       'SwiftFileBackendDirList' => __DIR__ . '/includes/libs/filebackend/SwiftFileBackend.php',
+       'SwiftFileBackendFileList' => __DIR__ . '/includes/libs/filebackend/SwiftFileBackend.php',
+       'SwiftFileBackendList' => __DIR__ . '/includes/libs/filebackend/SwiftFileBackend.php',
+       'SwiftFileOpHandle' => __DIR__ . '/includes/libs/filebackend/SwiftFileBackend.php',
        'SwiftVirtualRESTService' => __DIR__ . '/includes/libs/virtualrest/SwiftVirtualRESTService.php',
        'SyncFileBackend' => __DIR__ . '/maintenance/syncFileBackend.php',
        'TableCleanup' => __DIR__ . '/maintenance/cleanupTable.inc',
index ae0770b..2bfeb66 100644 (file)
@@ -1974,6 +1974,7 @@ $insertions: an array of links to insert
 'LinksUpdateComplete': At the end of LinksUpdate::doUpdate() when updating,
 including delete and insert, has completed for all link tables
 &$linksUpdate: the LinksUpdate object
+$ticket: prior result of LBFactory::getEmptyTransactionTicket()
 
 'LinksUpdateConstructed': At the end of LinksUpdate() is construction.
 &$linksUpdate: the LinksUpdate object
index a8e988f..53e855b 100644 (file)
@@ -19,6 +19,7 @@
  *
  * @file
  */
+use MediaWiki\MediaWikiServices;
 
 class CategoryViewer extends ContextSource {
        /** @var int */
@@ -317,10 +318,19 @@ class CategoryViewer extends ContextSource {
 
                        $res = $dbr->select(
                                [ 'page', 'categorylinks', 'category' ],
-                               [ 'page_id', 'page_title', 'page_namespace', 'page_len',
-                                       'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title',
-                                       'cat_subcats', 'cat_pages', 'cat_files',
-                                       'cl_sortkey_prefix', 'cl_collation' ],
+                               array_merge(
+                                       LinkCache::getSelectFields(),
+                                       [
+                                               'cl_sortkey',
+                                               'cat_id',
+                                               'cat_title',
+                                               'cat_subcats',
+                                               'cat_pages',
+                                               'cat_files',
+                                               'cl_sortkey_prefix',
+                                               'cl_collation'
+                                       ]
+                               ),
                                array_merge( [ 'cl_to' => $this->title->getDBkey() ], $extraConds ),
                                __METHOD__,
                                [
@@ -338,10 +348,13 @@ class CategoryViewer extends ContextSource {
                        );
 
                        Hooks::run( 'CategoryViewer::doCategoryQuery', [ $type, $res ] );
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 
                        $count = 0;
                        foreach ( $res as $row ) {
                                $title = Title::newFromRow( $row );
+                               $linkCache->addGoodLinkObjFromRow( $title, $row );
+
                                if ( $row->cl_collation === '' ) {
                                        // Hack to make sure that while updating from 1.16 schema
                                        // and db is inconsistent, that the sky doesn't fall.
index aa54629..7facf2f 100644 (file)
@@ -616,6 +616,11 @@ $wgUploadDialog = [
  * Additional parameters are specific to the file backend class used.
  * These settings should be global to all wikis when possible.
  *
+ * FileBackendMultiWrite::__construct() is augmented with a 'template' option that
+ * can be used in any of the values of the 'backends' array. Its value is the name of
+ * another backend in $wgFileBackends. When set, it pre-fills the array with all of the
+ * configuration of the named backend. Explicitly set values in the array take precedence.
+ *
  * There are two particularly important aspects about each backend:
  *   - a) Whether it is fully qualified or wiki-relative.
  *        By default, the paths of files are relative to the current wiki,
@@ -644,6 +649,10 @@ $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 = [];
 
index 606b4cd..406673d 100644 (file)
@@ -1846,8 +1846,17 @@ class EditPage {
                        } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
                                $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
                                return $status;
-
                        }
+                       // Make sure the user can edit the page under the new content model too
+                       $titleWithNewContentModel = clone $this->mTitle;
+                       $titleWithNewContentModel->setContentModel( $this->contentModel );
+                       if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $wgUser )
+                               || !$titleWithNewContentModel->userCan( 'edit', $wgUser )
+                       ) {
+                               $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
+                               return $status;
+                       }
+
                        $changingContentModel = true;
                        $oldContentModel = $this->mTitle->getContentModel();
                }
index 47360df..e6223e8 100644 (file)
@@ -150,11 +150,12 @@ class FileDeleteForm {
         * @param string $reason Reason of the deletion
         * @param bool $suppress Whether to mark all deleted versions as restricted
         * @param User $user User object performing the request
+        * @param array $tags Tags to apply to the deletion action
         * @throws MWException
         * @return bool|Status
         */
        public static function doDelete( &$title, &$file, &$oldimage, $reason,
-               $suppress, User $user = null
+               $suppress, User $user = null, $tags = []
        ) {
                if ( $user === null ) {
                        global $wgUser;
@@ -178,6 +179,7 @@ class FileDeleteForm {
                                $logEntry->setPerformer( $user );
                                $logEntry->setTarget( $title );
                                $logEntry->setComment( $logComment );
+                               $logEntry->setTags( $tags );
                                $logid = $logEntry->insert();
                                $logEntry->publish( $logid );
 
@@ -192,7 +194,8 @@ class FileDeleteForm {
                        $dbw->startAtomic( __METHOD__ );
                        // delete the associated article first
                        $error = '';
-                       $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, $user );
+                       $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error,
+                               $user, $tags );
                        // doDeleteArticleReal() returns a non-fatal error status if the page
                        // or revision is missing, so check for isOK() rather than isGood()
                        if ( $deleteStatus->isOK() ) {
index 6e8ce8f..5fe4b4e 100644 (file)
@@ -2194,12 +2194,11 @@ function wfIniGetBool( $setting ) {
 }
 
 /**
- * Windows-compatible version of escapeshellarg()
- * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg()
- * function puts single quotes in regardless of OS.
+ * Version of escapeshellarg() that works better on Windows.
  *
- * Also fixes the locale problems on Linux in PHP 5.2.6+ (bug backported to
- * earlier distro releases of PHP)
+ * Originally, this fixed the incorrect use of single quotes on Windows
+ * (https://bugs.php.net/bug.php?id=26285) and the locale problems on Linux in
+ * PHP 5.2.6+ (bug backported to earlier distro releases of PHP).
  *
  * @param string ... strings to escape and glue together, or a single array of strings parameter
  * @return string
index 8682991..9011f17 100644 (file)
@@ -1311,10 +1311,10 @@ class Linker {
                                :? # ignore optional leading colon
                                ([^\]|]+) # 1. link target; page names cannot include ] or |
                                (?:\|
-                                       # 2. a pipe-separated substring; only the last is captured
-                                       # Stop matching at | and ]] without relying on backtracking.
-                                       ((?:]?[^\]|])*+)
-                               )*
+                                       # 2. link text
+                                       # Stop matching at ]] without relying on backtracking.
+                                       ((?:]?[^\]])*+)
+                               )?
                                \]\]
                                ([^[]*) # 3. link trail (the text up until the next link)
                        /x',
index c2c954a..c1a12aa 100644 (file)
@@ -852,6 +852,12 @@ class Message implements MessageSpecifier, Serializable {
         * @return string
         */
        public function __toString() {
+               if ( $this->format !== 'parse' ) {
+                       $ex = new LogicException( __METHOD__ . ' using implicit format: ' . $this->format );
+                       \MediaWiki\Logger\LoggerFactory::getInstance( 'message-format' )->warning(
+                               $ex->getMessage(), [ 'exception' => $ex, 'format' => $this->format, 'key' => $this->key ] );
+               }
+
                // PHP doesn't allow __toString to throw exceptions and will
                // trigger a fatal error if it does. So, catch any exceptions.
 
index c57e219..f5405d7 100644 (file)
@@ -3050,8 +3050,8 @@ class OutputPage extends ContextSource {
                if ( $user->isLoggedIn() ) {
                        $vars['wgUserId'] = $user->getId();
                        $vars['wgUserEditCount'] = $user->getEditCount();
-                       $userReg = wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
-                       $vars['wgUserRegistration'] = $userReg !== null ? ( $userReg * 1000 ) : null;
+                       $userReg = $user->getRegistration();
+                       $vars['wgUserRegistration'] = $userReg ? wfTimestamp( TS_UNIX, $userReg ) * 1000 : null;
                        // Get the revision ID of the oldest new message on the user's talk
                        // page. This can be used for constructing new message alerts on
                        // the client side.
index 0fc7980..cce3fc4 100644 (file)
@@ -25,9 +25,9 @@
  */
 class StreamFile {
        // Do not send any HTTP headers unless requested by caller (e.g. body only)
-       const STREAM_HEADLESS = 1;
+       const STREAM_HEADLESS = HTTPFileStreamer::STREAM_HEADLESS;
        // Do not try to tear down any PHP output buffers
-       const STREAM_ALLOW_OB = 2;
+       const STREAM_ALLOW_OB = HTTPFileStreamer::STREAM_ALLOW_OB;
 
        /**
         * Stream a file to the browser, adding all the headings and fun stuff.
@@ -45,115 +45,19 @@ class StreamFile {
        public static function stream(
                $fname, $headers = [], $sendErrors = true, $optHeaders = [], $flags = 0
        ) {
-               $section = new ProfileSection( __METHOD__ );
-
                if ( FileBackend::isStoragePath( $fname ) ) { // sanity
-                       throw new MWException( __FUNCTION__ . " given storage path '$fname'." );
-               }
-
-               // Don't stream it out as text/html if there was a PHP error
-               if ( ( ( $flags & self::STREAM_HEADLESS ) == 0 || $headers ) && headers_sent() ) {
-                       echo "Headers already sent, terminating.\n";
-                       return false;
-               }
-
-               $headerFunc = ( $flags & self::STREAM_HEADLESS )
-                       ? function ( $header ) {
-                                // no-op
-                       }
-                       : function ( $header ) {
-                               is_int( $header ) ? HttpStatus::header( $header ) : header( $header );
-                       };
-
-               MediaWiki\suppressWarnings();
-               $info = stat( $fname );
-               MediaWiki\restoreWarnings();
-
-               if ( !is_array( $info ) ) {
-                       if ( $sendErrors ) {
-                               self::send404Message( $fname, $flags );
-                       }
-                       return false;
-               }
-
-               // Send Last-Modified HTTP header for client-side caching
-               $headerFunc( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $info['mtime'] ) );
-
-               if ( ( $flags & self::STREAM_ALLOW_OB ) == 0 ) {
-                       // Cancel output buffering and gzipping if set
-                       wfResetOutputBuffers();
-               }
-
-               $type = self::contentTypeFromPath( $fname );
-               if ( $type && $type != 'unknown/unknown' ) {
-                       $headerFunc( "Content-type: $type" );
-               } else {
-                       // Send a content type which is not known to Internet Explorer, to
-                       // avoid triggering IE's content type detection. Sending a standard
-                       // unknown content type here essentially gives IE license to apply
-                       // whatever content type it likes.
-                       $headerFunc( 'Content-type: application/x-wiki' );
+                       throw new InvalidArgumentException( __FUNCTION__ . " given storage path '$fname'." );
                }
 
-               // Don't send if client has up to date cache
-               if ( isset( $optHeaders['if-modified-since'] ) ) {
-                       $modsince = preg_replace( '/;.*$/', '', $optHeaders['if-modified-since'] );
-                       if ( wfTimestamp( TS_UNIX, $info['mtime'] ) <= strtotime( $modsince ) ) {
-                               ini_set( 'zlib.output_compression', 0 );
-                               $headerFunc( 304 );
-                               return true; // ok
-                       }
-               }
-
-               // Send additional headers
-               foreach ( $headers as $header ) {
-                       header( $header ); // always use header(); specifically requested
-               }
-
-               if ( isset( $optHeaders['range'] ) ) {
-                       $range = self::parseRange( $optHeaders['range'], $info['size'] );
-                       if ( is_array( $range ) ) {
-                               $headerFunc( 206 );
-                               $headerFunc( 'Content-Length: ' . $range[2] );
-                               $headerFunc( "Content-Range: bytes {$range[0]}-{$range[1]}/{$info['size']}" );
-                       } elseif ( $range === 'invalid' ) {
-                               if ( $sendErrors ) {
-                                       $headerFunc( 416 );
-                                       $headerFunc( 'Cache-Control: no-cache' );
-                                       $headerFunc( 'Content-Type: text/html; charset=utf-8' );
-                                       $headerFunc( 'Content-Range: bytes */' . $info['size'] );
-                               }
-                               return false;
-                       } else { // unsupported Range request (e.g. multiple ranges)
-                               $range = null;
-                               $headerFunc( 'Content-Length: ' . $info['size'] );
-                       }
-               } else {
-                       $range = null;
-                       $headerFunc( 'Content-Length: ' . $info['size'] );
-               }
+               $streamer = new HTTPFileStreamer(
+                       $fname,
+                       [
+                               'obResetFunc' => 'wfResetOutputBuffers',
+                               'streamMimeFunc' => [ __CLASS__, 'contentTypeFromPath' ]
+                       ]
+               );
 
-               if ( is_array( $range ) ) {
-                       $handle = fopen( $fname, 'rb' );
-                       if ( $handle ) {
-                               $ok = true;
-                               fseek( $handle, $range[0] );
-                               $remaining = $range[2];
-                               while ( $remaining > 0 && $ok ) {
-                                       $bytes = min( $remaining, 8 * 1024 );
-                                       $data = fread( $handle, $bytes );
-                                       $remaining -= $bytes;
-                                       $ok = ( $data !== false );
-                                       print $data;
-                               }
-                       } else {
-                               return false;
-                       }
-               } else {
-                       return readfile( $fname ) !== false; // faster
-               }
-
-               return true;
+               return $streamer->stream( $headers, $sendErrors, $optHeaders, $flags );
        }
 
        /**
@@ -164,19 +68,7 @@ class StreamFile {
         * @since 1.24
         */
        public static function send404Message( $fname, $flags = 0 ) {
-               if ( ( $flags & self::STREAM_HEADLESS ) == 0 ) {
-                       HttpStatus::header( 404 );
-                       header( 'Cache-Control: no-cache' );
-                       header( 'Content-Type: text/html; charset=utf-8' );
-               }
-               $encFile = htmlspecialchars( $fname );
-               $encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] );
-               echo "<!DOCTYPE html><html><body>
-                       <h1>File not found</h1>
-                       <p>Although this PHP script ($encScript) exists, the file requested for output
-                       ($encFile) does not.</p>
-                       </body></html>
-                       ";
+               HTTPFileStreamer::send404Message( $fname, $flags );
        }
 
        /**
@@ -188,30 +80,7 @@ class StreamFile {
         * @since 1.24
         */
        public static function parseRange( $range, $size ) {
-               $m = [];
-               if ( preg_match( '#^bytes=(\d*)-(\d*)$#', $range, $m ) ) {
-                       list( , $start, $end ) = $m;
-                       if ( $start === '' && $end === '' ) {
-                               $absRange = [ 0, $size - 1 ];
-                       } elseif ( $start === '' ) {
-                               $absRange = [ $size - $end, $size - 1 ];
-                       } elseif ( $end === '' ) {
-                               $absRange = [ $start, $size - 1 ];
-                       } else {
-                               $absRange = [ $start, $end ];
-                       }
-                       if ( $absRange[0] >= 0 && $absRange[1] >= $absRange[0] ) {
-                               if ( $absRange[0] < $size ) {
-                                       $absRange[1] = min( $absRange[1], $size - 1 ); // stop at EOF
-                                       $absRange[2] = $absRange[1] - $absRange[0] + 1;
-                                       return $absRange;
-                               } elseif ( $absRange[0] == 0 && $size == 0 ) {
-                                       return 'unrecognized'; // the whole file should just be sent
-                               }
-                       }
-                       return 'invalid';
-               }
-               return 'unrecognized';
+               return HTTPFileStreamer::parseRange( $range, $size );
        }
 
        /**
index 27a334d..5e1e8c6 100644 (file)
@@ -91,7 +91,13 @@ class Title implements LinkTarget {
         * @var bool|string ID of the page's content model, i.e. one of the
         *   CONTENT_MODEL_XXX constants
         */
-       public $mContentModel = false;
+       private $mContentModel = false;
+
+       /**
+        * @var bool If a content model was forced via setContentModel()
+        *   this will be true to avoid having other code paths reset it
+        */
+       private $mForcedContentModel = false;
 
        /** @var int Estimated number of revisions; null of not loaded */
        private $mEstimateRevisions;
@@ -467,9 +473,9 @@ class Title implements LinkTarget {
                        if ( isset( $row->page_latest ) ) {
                                $this->mLatestID = (int)$row->page_latest;
                        }
-                       if ( isset( $row->page_content_model ) ) {
+                       if ( !$this->mForcedContentModel && isset( $row->page_content_model ) ) {
                                $this->mContentModel = strval( $row->page_content_model );
-                       } else {
+                       } elseif ( !$this->mForcedContentModel ) {
                                $this->mContentModel = false; # initialized lazily in getContentModel()
                        }
                        if ( isset( $row->page_lang ) ) {
@@ -483,7 +489,9 @@ class Title implements LinkTarget {
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
-                       $this->mContentModel = false; # initialized lazily in getContentModel()
+                       if ( !$this->mForcedContentModel ) {
+                               $this->mContentModel = false; # initialized lazily in getContentModel()
+                       }
                }
        }
 
@@ -921,8 +929,9 @@ class Title implements LinkTarget {
         * @return string Content model id
         */
        public function getContentModel( $flags = 0 ) {
-               if ( ( !$this->mContentModel || $flags === Title::GAID_FOR_UPDATE ) &&
-                       $this->getArticleID( $flags )
+               if ( !$this->mForcedContentModel
+                       && ( !$this->mContentModel || $flags === Title::GAID_FOR_UPDATE )
+                       && $this->getArticleID( $flags )
                ) {
                        $linkCache = LinkCache::singleton();
                        $linkCache->addLinkObj( $this ); # in case we already had an article ID
@@ -946,6 +955,22 @@ class Title implements LinkTarget {
                return $this->getContentModel() == $id;
        }
 
+       /**
+        * Set a proposed content model for the page for permissions
+        * checking. This does not actually change the content model
+        * of a title!
+        *
+        * Additionally, you should make sure you've checked
+        * ContentHandler::canBeUsedOn() first.
+        *
+        * @since 1.28
+        * @param string $model CONTENT_MODEL_XXX constant
+        */
+       public function setContentModel( $model ) {
+               $this->mContentModel = $model;
+               $this->mForcedContentModel = true;
+       }
+
        /**
         * Get the namespace text
         *
index 77911b0..993c23e 100644 (file)
@@ -73,10 +73,11 @@ class ApiDelete extends ApiBase {
                                $user,
                                $params['oldimage'],
                                $reason,
-                               false
+                               false,
+                               $params['tags']
                        );
                } else {
-                       $status = self::delete( $pageObj, $user, $reason );
+                       $status = self::delete( $pageObj, $user, $reason, $params['tags'] );
                }
 
                if ( is_array( $status ) ) {
@@ -96,11 +97,6 @@ class ApiDelete extends ApiBase {
                }
                $this->setWatch( $watch, $titleObj, 'watchdeletion' );
 
-               // Apply change tags to the log entry, if requested
-               if ( count( $params['tags'] ) ) {
-                       ChangeTags::addTags( $params['tags'], null, null, $status->value, null );
-               }
-
                $r = [
                        'title' => $titleObj->getPrefixedText(),
                        'reason' => $reason,
@@ -115,9 +111,10 @@ class ApiDelete extends ApiBase {
         * @param Page|WikiPage $page Page or WikiPage object to work on
         * @param User $user User doing the action
         * @param string|null $reason Reason for the deletion. Autogenerated if null
+        * @param array $tags Tags to tag the deletion with
         * @return Status|array
         */
-       protected static function delete( Page $page, User $user, &$reason = null ) {
+       protected static function delete( Page $page, User $user, &$reason = null, $tags = [] ) {
                $title = $page->getTitle();
 
                // Auto-generate a summary, if necessary
@@ -134,7 +131,7 @@ class ApiDelete extends ApiBase {
                $error = '';
 
                // Luckily, Article.php provides a reusable delete function that does the hard work for us
-               return $page->doDeleteArticleReal( $reason, false, 0, true, $error, $user );
+               return $page->doDeleteArticleReal( $reason, false, 0, true, $error, $user, $tags );
        }
 
        /**
@@ -143,16 +140,17 @@ class ApiDelete extends ApiBase {
         * @param string $oldimage Archive name
         * @param string $reason Reason for the deletion. Autogenerated if null.
         * @param bool $suppress Whether to mark all deleted versions as restricted
+        * @param array $tags Tags to tag the deletion with
         * @return Status|array
         */
        protected static function deleteFile( Page $page, User $user, $oldimage,
-               &$reason = null, $suppress = false
+               &$reason = null, $suppress = false, $tags = []
        ) {
                $title = $page->getTitle();
 
                $file = $page->getFile();
                if ( !$file->exists() || !$file->isLocal() || $file->getRedirected() ) {
-                       return self::delete( $page, $user, $reason );
+                       return self::delete( $page, $user, $reason, $tags );
                }
 
                if ( $oldimage ) {
@@ -169,7 +167,7 @@ class ApiDelete extends ApiBase {
                        $reason = '';
                }
 
-               return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress, $user );
+               return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress, $user, $tags );
        }
 
        public function mustBePosted() {
index 0d19545..d28165d 100644 (file)
@@ -8,6 +8,8 @@
        "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentación]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Llista d'alderique]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anuncios de la API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Fallos y solicitúes]\n</div>\n<strong>Estau:</strong> Toles carauterístiques qu'apaecen nesta páxina tendríen de funcionar, pero la API inda ta en desendolcu activu, y puede camudar en cualquier momentu. Suscríbete a la [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ llista de corréu mediawiki-api-announce] p'avisos sobro anovamientos.\n\n<strong>Solicitúes incorreutes:</strong> Cuando s'unvíen solicitúes incorreutes a la API, unvíase una cabecera HTTP cola clave \"MediaWiki-API-Error\" y, darréu, tanto'l valor de la cabecera como'l códigu d'error devueltu pondránse al mesmu valor. Pa más información, consulta [[mw:API:Errors_and_warnings|API: Errores y avisos]].\n\n<strong>Pruebes:</strong> Pa facilitar les pruebes de solicitúes API, consulta [[Special:ApiSandbox]].",
        "apihelp-main-param-action": "Qué aición facer.",
        "apihelp-main-param-format": "El formatu de la salida.",
+       "apihelp-main-param-servedby": "Incluyir el nome del host que sirvió la solicitú nes resultancies.",
+       "apihelp-main-param-curtimestamp": "Incluyir la marca de tiempu actual na resultancia.",
        "apihelp-block-description": "Bloquiar a un usuariu.",
        "apihelp-block-param-user": "El nome d'usuariu, dirección IP o intervalu d'IP que quies bloquiar.",
        "apihelp-block-param-expiry": "Fecha de caducidá. Puede ser relativa (por casu, <kbd>5 meses</kbd> o <kbd>2 selmanes</kbd>) o absoluta (por casu, 2016-01-16T12:34:56Z). Si s'establez a <kbd>infinitu</kbd>, <kbd>indefiníu</kbd>, o <kbd>nunca</kbd>, el bloquéu nun caducará nunca.",
        "apihelp-block-param-autoblock": "Bloquiar automáticamente la última dirección IP usada y les siguientes direcciones IP de les que traten d'aniciar sesión darréu.",
        "apihelp-block-param-noemail": "Torgar que l'usuariu unvie corréu al traviés de la wiki (Rique'l permisu <code>blockemail</code>).",
        "apihelp-block-param-hidename": "Despintar el nome d'usuariu del rexistru de bloquéu (Rique'l permisu <code>hideuser</code>).",
+       "apihelp-block-param-reblock": "Si la cuenta yá ta bloquiada, sobrescribir el bloquéu esistente.",
+       "apihelp-block-param-watchuser": "Vixilar les páxines d'usuariu y d'alderique del usuariu o de la dirección IP.",
+       "apihelp-block-example-ip-simple": "Bloquiar la dirección IP <kbd>192.0.2.5</kbd> mientres 3 díes col motivu <kbd>Primer avisu</kbd>.",
+       "apihelp-block-example-user-complex": "Bloquiar al usuariu <kbd>Vandal</kbd> indefinidamente col motivu <kbd>Vandalismu</kbd> y torgar que cree nueves cuentes o unvie correos.",
+       "apihelp-changeauthenticationdata-description": "Camudar los datos d'identificación del usuariu actual.",
+       "apihelp-changeauthenticationdata-example-password": "Intentar camudar la contraseña del usuariu actual a <kbd>ContraseñaExemplu</kbd>.",
        "apihelp-createaccount-param-name": "Nome d'usuariu.",
        "apihelp-createaccount-param-language": "Códigu de llingua p'afitar como predetermináu al usuariu (opcional, predetermina la llingua del conteníu).",
        "apihelp-disabled-description": "Esti módulu deshabilitóse."
index 4678504..4156395 100644 (file)
        "apihelp-emailuser-param-text": "Съдържание на писмото.",
        "apihelp-emailuser-param-ccme": "Изпращане на копие от това писмо до мен.",
        "apihelp-expandtemplates-param-title": "Заглавие на страница.",
+       "apihelp-feedcontributions-param-year": "От година (и по-рано).",
+       "apihelp-feedcontributions-param-month": "От месец (и по-рано).",
+       "apihelp-feedcontributions-param-tagfilter": "Филтриране на приноси, които имат тези етикети.",
+       "apihelp-feedcontributions-param-deletedonly": "Покажи само изтритите редакции.",
+       "apihelp-feedcontributions-param-newonly": "Показване само на редакции за създаване на страници.",
        "apihelp-feedcontributions-param-hideminor": "Скриване на малки промени.",
+       "apihelp-feedcontributions-param-showsizediff": "Показване на размера на разликите между версиите.",
        "apihelp-feedrecentchanges-param-hideminor": "Скриване на малки промени.",
        "apihelp-feedrecentchanges-param-hidebots": "Скриване на промени, направени от ботове.",
        "apihelp-feedrecentchanges-param-hideanons": "Скриване на промени, направени от анонимни потребители.",
@@ -39,6 +45,7 @@
        "apihelp-feedrecentchanges-example-30days": "Показване на последните промени в рамките на 30 дни.",
        "apihelp-login-param-name": "Потребителско име.",
        "apihelp-login-param-password": "Парола.",
+       "apihelp-login-param-domain": "Домейн (по избор).",
        "apihelp-move-description": "Преместване на страница.",
        "apihelp-move-param-reason": "Причина за преименуването.",
        "apihelp-move-param-movetalk": "Преименуване на беседата, ако има такава.",
        "apihelp-move-param-noredirect": "Не създавай пренасочване.",
        "apihelp-move-param-ignorewarnings": "Пренебрегване на всякакви предупреждения.",
        "apihelp-protect-example-protect": "Защита на страница.",
+       "apihelp-query+allusers-param-prefix": "Търсене за всички потребители, които започват с тази стойност.",
+       "apihelp-query+allusers-param-dir": "Посока на сортиране.",
+       "apihelp-query+allusers-param-group": "Включва само потребители от определените групи.",
+       "apihelp-query+allusers-param-excludegroup": "Изключване на потребители от определените групи.",
+       "apihelp-query+allusers-param-prop": "Каква информация да включва:",
+       "apihelp-query+allusers-paramvalue-prop-blockinfo": "Добавя информация за текущото блокиране на потребителя.",
        "apihelp-query+langlinks-paramvalue-prop-url": "Добавя пълният URL-адрес.",
        "apihelp-query+linkshere-paramvalue-prop-title": "Заглавие на всяка страница.",
        "apihelp-query+watchlist-paramvalue-type-log": "Записи в дневника."
diff --git a/includes/api/i18n/hr.json b/includes/api/i18n/hr.json
new file mode 100644 (file)
index 0000000..5f46e73
--- /dev/null
@@ -0,0 +1,9 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Ex13"
+               ]
+       },
+       "apihelp-block-description": "Blokiraj suradnika.",
+       "apihelp-block-param-user": "Suradničko ime, IP adresa ili opseg koje želite blokirati."
+}
index bbc6e8d..88df68d 100644 (file)
@@ -88,8 +88,8 @@ class LocalPasswordPrimaryAuthenticationProvider
                        'user_id', 'user_password', 'user_password_expires',
                ];
 
-               $dbw = wfGetDB( DB_MASTER );
-               $row = $dbw->selectRow(
+               $dbr = wfGetDB( DB_REPLICA );
+               $row = $dbr->selectRow(
                        'user',
                        $fields,
                        [ 'user_name' => $username ],
@@ -99,6 +99,7 @@ class LocalPasswordPrimaryAuthenticationProvider
                        return AuthenticationResponse::newAbstain();
                }
 
+               $oldRow = clone $row;
                // Check for *really* old password hashes that don't even have a type
                // The old hash format was just an md5 hex hash, with no type information
                if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
@@ -132,12 +133,18 @@ class LocalPasswordPrimaryAuthenticationProvider
                // @codeCoverageIgnoreStart
                if ( $this->getPasswordFactory()->needsUpdate( $pwhash ) ) {
                        $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
-                       $dbw->update(
-                               'user',
-                               [ 'user_password' => $pwhash->toString() ],
-                               [ 'user_id' => $row->user_id ],
-                               __METHOD__
-                       );
+                       \DeferredUpdates::addCallableUpdate( function () use ( $pwhash, $oldRow ) {
+                               $dbw = wfGetDB( DB_MASTER );
+                               $dbw->update(
+                                       'user',
+                                       [ 'user_password' => $pwhash->toString() ],
+                                       [
+                                               'user_id' => $oldRow->user_id,
+                                               'user_password' => $oldRow->user_password
+                                       ],
+                                       __METHOD__
+                               );
+                       } );
                }
                // @codeCoverageIgnoreEnd
 
@@ -152,8 +159,8 @@ class LocalPasswordPrimaryAuthenticationProvider
                        return false;
                }
 
-               $dbw = wfGetDB( DB_MASTER );
-               $row = $dbw->selectRow(
+               $dbr = wfGetDB( DB_REPLICA );
+               $row = $dbr->selectRow(
                        'user',
                        [ 'user_password' ],
                        [ 'user_name' => $username ],
diff --git a/includes/clientpool/RedisConnectionPool.php b/includes/clientpool/RedisConnectionPool.php
deleted file mode 100644 (file)
index a9bc593..0000000
+++ /dev/null
@@ -1,581 +0,0 @@
-<?php
-/**
- * Redis client connection pooling manager.
- *
- * 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
- * @defgroup Redis Redis
- * @author Aaron Schulz
- */
-
-use MediaWiki\Logger\LoggerFactory;
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-
-/**
- * Helper class to manage Redis connections.
- *
- * This can be used to get handle wrappers that free the handle when the wrapper
- * leaves scope. The maximum number of free handles (connections) is configurable.
- * This provides an easy way to cache connection handles that may also have state,
- * such as a handle does between multi() and exec(), and without hoarding connections.
- * The wrappers use PHP magic methods so that calling functions on them calls the
- * function of the actual Redis object handle.
- *
- * @ingroup Redis
- * @since 1.21
- */
-class RedisConnectionPool implements LoggerAwareInterface {
-       /**
-        * @name Pool settings.
-        * Settings there are shared for any connection made in this pool.
-        * See the singleton() method documentation for more details.
-        * @{
-        */
-       /** @var string Connection timeout in seconds */
-       protected $connectTimeout;
-       /** @var string Read timeout in seconds */
-       protected $readTimeout;
-       /** @var string Plaintext auth password */
-       protected $password;
-       /** @var bool Whether connections persist */
-       protected $persistent;
-       /** @var int Serializer to use (Redis::SERIALIZER_*) */
-       protected $serializer;
-       /** @} */
-
-       /** @var int Current idle pool size */
-       protected $idlePoolSize = 0;
-
-       /** @var array (server name => ((connection info array),...) */
-       protected $connections = [];
-       /** @var array (server name => UNIX timestamp) */
-       protected $downServers = [];
-
-       /** @var array (pool ID => RedisConnectionPool) */
-       protected static $instances = [];
-
-       /** integer; seconds to cache servers as "down". */
-       const SERVER_DOWN_TTL = 30;
-
-       /**
-        * @var LoggerInterface
-        */
-       protected $logger;
-
-       /**
-        * @param array $options
-        * @throws Exception
-        */
-       protected function __construct( array $options ) {
-               if ( !class_exists( 'Redis' ) ) {
-                       throw new Exception( __CLASS__ . ' requires a Redis client library. ' .
-                               'See https://www.mediawiki.org/wiki/Redis#Setup' );
-               }
-               if ( isset( $options['logger'] ) ) {
-                       $this->setLogger( $options['logger'] );
-               } else {
-                       $this->setLogger( LoggerFactory::getInstance( 'redis' ) );
-               }
-               $this->connectTimeout = $options['connectTimeout'];
-               $this->readTimeout = $options['readTimeout'];
-               $this->persistent = $options['persistent'];
-               $this->password = $options['password'];
-               if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) {
-                       $this->serializer = Redis::SERIALIZER_PHP;
-               } elseif ( $options['serializer'] === 'igbinary' ) {
-                       $this->serializer = Redis::SERIALIZER_IGBINARY;
-               } elseif ( $options['serializer'] === 'none' ) {
-                       $this->serializer = Redis::SERIALIZER_NONE;
-               } else {
-                       throw new InvalidArgumentException( "Invalid serializer specified." );
-               }
-       }
-
-       /**
-        * @param LoggerInterface $logger
-        * @return null
-        */
-       public function setLogger( LoggerInterface $logger ) {
-               $this->logger = $logger;
-       }
-
-       /**
-        * @param array $options
-        * @return array
-        */
-       protected static function applyDefaultConfig( array $options ) {
-               if ( !isset( $options['connectTimeout'] ) ) {
-                       $options['connectTimeout'] = 1;
-               }
-               if ( !isset( $options['readTimeout'] ) ) {
-                       $options['readTimeout'] = 1;
-               }
-               if ( !isset( $options['persistent'] ) ) {
-                       $options['persistent'] = false;
-               }
-               if ( !isset( $options['password'] ) ) {
-                       $options['password'] = null;
-               }
-
-               return $options;
-       }
-
-       /**
-        * @param array $options
-        * $options include:
-        *   - connectTimeout : The timeout for new connections, in seconds.
-        *                      Optional, default is 1 second.
-        *   - readTimeout    : The timeout for operation reads, in seconds.
-        *                      Commands like BLPOP can fail if told to wait longer than this.
-        *                      Optional, default is 1 second.
-        *   - persistent     : Set this to true to allow connections to persist across
-        *                      multiple web requests. False by default.
-        *   - password       : The authentication password, will be sent to Redis in clear text.
-        *                      Optional, if it is unspecified, no AUTH command will be sent.
-        *   - serializer     : Set to "php", "igbinary", or "none". Default is "php".
-        * @return RedisConnectionPool
-        */
-       public static function singleton( array $options ) {
-               $options = self::applyDefaultConfig( $options );
-               // Map the options to a unique hash...
-               ksort( $options ); // normalize to avoid pool fragmentation
-               $id = sha1( serialize( $options ) );
-               // Initialize the object at the hash as needed...
-               if ( !isset( self::$instances[$id] ) ) {
-                       self::$instances[$id] = new self( $options );
-                       LoggerFactory::getInstance( 'redis' )->debug(
-                               "Creating a new " . __CLASS__ . " instance with id $id."
-                       );
-               }
-
-               return self::$instances[$id];
-       }
-
-       /**
-        * Destroy all singleton() instances
-        * @since 1.27
-        */
-       public static function destroySingletons() {
-               self::$instances = [];
-       }
-
-       /**
-        * Get a connection to a redis server. Based on code in RedisBagOStuff.php.
-        *
-        * @param string $server A hostname/port combination or the absolute path of a UNIX socket.
-        *                       If a hostname is specified but no port, port 6379 will be used.
-        * @return RedisConnRef|bool Returns false on failure
-        * @throws MWException
-        */
-       public function getConnection( $server ) {
-               // Check the listing "dead" servers which have had a connection errors.
-               // Servers are marked dead for a limited period of time, to
-               // avoid excessive overhead from repeated connection timeouts.
-               if ( isset( $this->downServers[$server] ) ) {
-                       $now = time();
-                       if ( $now > $this->downServers[$server] ) {
-                               // Dead time expired
-                               unset( $this->downServers[$server] );
-                       } else {
-                               // Server is dead
-                               $this->logger->debug(
-                                       'Server "{redis_server}" is marked down for another ' .
-                                       ( $this->downServers[$server] - $now ) . 'seconds',
-                                       [ 'redis_server' => $server ]
-                               );
-
-                               return false;
-                       }
-               }
-
-               // Check if a connection is already free for use
-               if ( isset( $this->connections[$server] ) ) {
-                       foreach ( $this->connections[$server] as &$connection ) {
-                               if ( $connection['free'] ) {
-                                       $connection['free'] = false;
-                                       --$this->idlePoolSize;
-
-                                       return new RedisConnRef(
-                                               $this, $server, $connection['conn'], $this->logger
-                                       );
-                               }
-                       }
-               }
-
-               if ( substr( $server, 0, 1 ) === '/' ) {
-                       // UNIX domain socket
-                       // These are required by the redis extension to start with a slash, but
-                       // we still need to set the port to a special value to make it work.
-                       $host = $server;
-                       $port = 0;
-               } else {
-                       // TCP connection
-                       $hostPort = IP::splitHostAndPort( $server );
-                       if ( !$server || !$hostPort ) {
-                               throw new InvalidArgumentException(
-                                       __CLASS__ . ": invalid configured server \"$server\""
-                               );
-                       }
-                       list( $host, $port ) = $hostPort;
-                       if ( $port === false ) {
-                               $port = 6379;
-                       }
-               }
-
-               $conn = new Redis();
-               try {
-                       if ( $this->persistent ) {
-                               $result = $conn->pconnect( $host, $port, $this->connectTimeout );
-                       } else {
-                               $result = $conn->connect( $host, $port, $this->connectTimeout );
-                       }
-                       if ( !$result ) {
-                               $this->logger->error(
-                                       'Could not connect to server "{redis_server}"',
-                                       [ 'redis_server' => $server ]
-                               );
-                               // Mark server down for some time to avoid further timeouts
-                               $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
-
-                               return false;
-                       }
-                       if ( $this->password !== null ) {
-                               if ( !$conn->auth( $this->password ) ) {
-                                       $this->logger->error(
-                                               'Authentication error connecting to "{redis_server}"',
-                                               [ 'redis_server' => $server ]
-                                       );
-                               }
-                       }
-               } catch ( RedisException $e ) {
-                       $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
-                       $this->logger->error(
-                               'Redis exception connecting to "{redis_server}"',
-                               [
-                                       'redis_server' => $server,
-                                       'exception' => $e,
-                               ]
-                       );
-
-                       return false;
-               }
-
-               if ( $conn ) {
-                       $conn->setOption( Redis::OPT_READ_TIMEOUT, $this->readTimeout );
-                       $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
-                       $this->connections[$server][] = [ 'conn' => $conn, 'free' => false ];
-
-                       return new RedisConnRef( $this, $server, $conn, $this->logger );
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Mark a connection to a server as free to return to the pool
-        *
-        * @param string $server
-        * @param Redis $conn
-        * @return bool
-        */
-       public function freeConnection( $server, Redis $conn ) {
-               $found = false;
-
-               foreach ( $this->connections[$server] as &$connection ) {
-                       if ( $connection['conn'] === $conn && !$connection['free'] ) {
-                               $connection['free'] = true;
-                               ++$this->idlePoolSize;
-                               break;
-                       }
-               }
-
-               $this->closeExcessIdleConections();
-
-               return $found;
-       }
-
-       /**
-        * Close any extra idle connections if there are more than the limit
-        */
-       protected function closeExcessIdleConections() {
-               if ( $this->idlePoolSize <= count( $this->connections ) ) {
-                       return; // nothing to do (no more connections than servers)
-               }
-
-               foreach ( $this->connections as &$serverConnections ) {
-                       foreach ( $serverConnections as $key => &$connection ) {
-                               if ( $connection['free'] ) {
-                                       unset( $serverConnections[$key] );
-                                       if ( --$this->idlePoolSize <= count( $this->connections ) ) {
-                                               return; // done (no more connections than servers)
-                                       }
-                               }
-                       }
-               }
-       }
-
-       /**
-        * The redis extension throws an exception in response to various read, write
-        * and protocol errors. Sometimes it also closes the connection, sometimes
-        * not. The safest response for us is to explicitly destroy the connection
-        * object and let it be reopened during the next request.
-        *
-        * @param string $server
-        * @param RedisConnRef $cref
-        * @param RedisException $e
-        * @deprecated since 1.23
-        */
-       public function handleException( $server, RedisConnRef $cref, RedisException $e ) {
-               $this->handleError( $cref, $e );
-       }
-
-       /**
-        * The redis extension throws an exception in response to various read, write
-        * and protocol errors. Sometimes it also closes the connection, sometimes
-        * not. The safest response for us is to explicitly destroy the connection
-        * object and let it be reopened during the next request.
-        *
-        * @param RedisConnRef $cref
-        * @param RedisException $e
-        */
-       public function handleError( RedisConnRef $cref, RedisException $e ) {
-               $server = $cref->getServer();
-               $this->logger->error(
-                       'Redis exception on server "{redis_server}"',
-                       [
-                               'redis_server' => $server,
-                               'exception' => $e,
-                       ]
-               );
-               foreach ( $this->connections[$server] as $key => $connection ) {
-                       if ( $cref->isConnIdentical( $connection['conn'] ) ) {
-                               $this->idlePoolSize -= $connection['free'] ? 1 : 0;
-                               unset( $this->connections[$server][$key] );
-                               break;
-                       }
-               }
-       }
-
-       /**
-        * Re-send an AUTH request to the redis server (useful after disconnects).
-        *
-        * This works around an upstream bug in phpredis. phpredis hides disconnects by transparently
-        * reconnecting, but it neglects to re-authenticate the new connection. To the user of the
-        * phpredis client API this manifests as a seemingly random tendency of connections to lose
-        * their authentication status.
-        *
-        * This method is for internal use only.
-        *
-        * @see https://github.com/nicolasff/phpredis/issues/403
-        *
-        * @param string $server
-        * @param Redis $conn
-        * @return bool Success
-        */
-       public function reauthenticateConnection( $server, Redis $conn ) {
-               if ( $this->password !== null ) {
-                       if ( !$conn->auth( $this->password ) ) {
-                               $this->logger->error(
-                                       'Authentication error connecting to "{redis_server}"',
-                                       [ 'redis_server' => $server ]
-                               );
-
-                               return false;
-                       }
-               }
-
-               return true;
-       }
-
-       /**
-        * Adjust or reset the connection handle read timeout value
-        *
-        * @param Redis $conn
-        * @param int $timeout Optional
-        */
-       public function resetTimeout( Redis $conn, $timeout = null ) {
-               $conn->setOption( Redis::OPT_READ_TIMEOUT, $timeout ?: $this->readTimeout );
-       }
-
-       /**
-        * Make sure connections are closed for sanity
-        */
-       function __destruct() {
-               foreach ( $this->connections as $server => &$serverConnections ) {
-                       foreach ( $serverConnections as $key => &$connection ) {
-                               $connection['conn']->close();
-                       }
-               }
-       }
-}
-
-/**
- * Helper class to handle automatically marking connectons as reusable (via RAII pattern)
- *
- * This class simply wraps the Redis class and can be used the same way
- *
- * @ingroup Redis
- * @since 1.21
- */
-class RedisConnRef {
-       /** @var RedisConnectionPool */
-       protected $pool;
-       /** @var Redis */
-       protected $conn;
-
-       protected $server; // string
-       protected $lastError; // string
-
-       /**
-        * @var LoggerInterface
-        */
-       protected $logger;
-
-       /**
-        * @param RedisConnectionPool $pool
-        * @param string $server
-        * @param Redis $conn
-        * @param LoggerInterface $logger
-        */
-       public function __construct(
-               RedisConnectionPool $pool, $server, Redis $conn, LoggerInterface $logger
-       ) {
-               $this->pool = $pool;
-               $this->server = $server;
-               $this->conn = $conn;
-               $this->logger = $logger;
-       }
-
-       /**
-        * @return string
-        * @since 1.23
-        */
-       public function getServer() {
-               return $this->server;
-       }
-
-       public function getLastError() {
-               return $this->lastError;
-       }
-
-       public function clearLastError() {
-               $this->lastError = null;
-       }
-
-       public function __call( $name, $arguments ) {
-               $conn = $this->conn; // convenience
-
-               // Work around https://github.com/nicolasff/phpredis/issues/70
-               $lname = strtolower( $name );
-               if ( ( $lname === 'blpop' || $lname == 'brpop' )
-                       && is_array( $arguments[0] ) && isset( $arguments[1] )
-               ) {
-                       $this->pool->resetTimeout( $conn, $arguments[1] + 1 );
-               } elseif ( $lname === 'brpoplpush' && isset( $arguments[2] ) ) {
-                       $this->pool->resetTimeout( $conn, $arguments[2] + 1 );
-               }
-
-               $conn->clearLastError();
-               try {
-                       $res = call_user_func_array( [ $conn, $name ], $arguments );
-                       if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
-                               $this->pool->reauthenticateConnection( $this->server, $conn );
-                               $conn->clearLastError();
-                               $res = call_user_func_array( [ $conn, $name ], $arguments );
-                               $this->logger->info(
-                                       "Used automatic re-authentication for method '$name'.",
-                                       [ 'redis_server' => $this->server ]
-                               );
-                       }
-               } catch ( RedisException $e ) {
-                       $this->pool->resetTimeout( $conn ); // restore
-                       throw $e;
-               }
-
-               $this->lastError = $conn->getLastError() ?: $this->lastError;
-
-               $this->pool->resetTimeout( $conn ); // restore
-
-               return $res;
-       }
-
-       /**
-        * @param string $script
-        * @param array $params
-        * @param int $numKeys
-        * @return mixed
-        * @throws RedisException
-        */
-       public function luaEval( $script, array $params, $numKeys ) {
-               $sha1 = sha1( $script ); // 40 char hex
-               $conn = $this->conn; // convenience
-               $server = $this->server; // convenience
-
-               // Try to run the server-side cached copy of the script
-               $conn->clearLastError();
-               $res = $conn->evalSha( $sha1, $params, $numKeys );
-               // If we got a permission error reply that means that (a) we are not in
-               // multi()/pipeline() and (b) some connection problem likely occurred. If
-               // the password the client gave was just wrong, an exception should have
-               // been thrown back in getConnection() previously.
-               if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
-                       $this->pool->reauthenticateConnection( $server, $conn );
-                       $conn->clearLastError();
-                       $res = $conn->eval( $script, $params, $numKeys );
-                       $this->logger->info(
-                               "Used automatic re-authentication for Lua script '$sha1'.",
-                               [ 'redis_server' => $server ]
-                       );
-               }
-               // If the script is not in cache, use eval() to retry and cache it
-               if ( preg_match( '/^NOSCRIPT/', $conn->getLastError() ) ) {
-                       $conn->clearLastError();
-                       $res = $conn->eval( $script, $params, $numKeys );
-                       $this->logger->info(
-                               "Used eval() for Lua script '$sha1'.",
-                               [ 'redis_server' => $server ]
-                       );
-               }
-
-               if ( $conn->getLastError() ) { // script bug?
-                       $this->logger->error(
-                               'Lua script error on server "{redis_server}": {lua_error}',
-                               [
-                                       'redis_server' => $server,
-                                       'lua_error' => $conn->getLastError()
-                               ]
-                       );
-               }
-
-               $this->lastError = $conn->getLastError() ?: $this->lastError;
-
-               return $res;
-       }
-
-       /**
-        * @param Redis $conn
-        * @return bool
-        */
-       public function isConnIdentical( Redis $conn ) {
-               return $this->conn === $conn;
-       }
-
-       function __destruct() {
-               $this->pool->freeConnection( $this->server, $this->conn );
-       }
-}
index d24ebde..8a761f5 100644 (file)
@@ -214,6 +214,10 @@ class DeferredUpdates {
                                                $firstKey = key( self::$executeContext['subqueue'] );
                                                unset( self::$executeContext['subqueue'][$firstKey] );
 
+                                               if ( $subUpdate instanceof DataUpdate ) {
+                                                       $subUpdate->setTransactionTicket( $ticket );
+                                               }
+
                                                $guiError = self::runUpdate( $subUpdate, $lbFactory, $stage );
                                                $reportableError = $reportableError ?: $guiError;
                                        }
index d18349b..8954304 100644 (file)
@@ -176,7 +176,7 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
                // Run post-commit hooks without DBO_TRX
                $this->getDB()->onTransactionIdle(
                        function () {
-                               Hooks::run( 'LinksUpdateComplete', [ &$this ] );
+                               Hooks::run( 'LinksUpdateComplete', [ &$this, $this->ticket ] );
                        },
                        __METHOD__
                );
diff --git a/includes/filebackend/FSFileBackend.php b/includes/filebackend/FSFileBackend.php
deleted file mode 100644 (file)
index 45951ec..0000000
+++ /dev/null
@@ -1,975 +0,0 @@
-<?php
-/**
- * File system based backend.
- *
- * 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 FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * @brief Class for a file system (FS) based file backend.
- *
- * All "containers" each map to a directory under the backend's base directory.
- * For backwards-compatibility, some container paths can be set to custom paths.
- * The wiki ID will not be used in any custom paths, so this should be avoided.
- *
- * Having directories with thousands of files will diminish performance.
- * Sharding can be accomplished by using FileRepo-style hash paths.
- *
- * StatusValue messages should avoid mentioning the internal FS paths.
- * PHP warnings are assumed to be logged rather than output.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-class FSFileBackend extends FileBackendStore {
-       /** @var string Directory holding the container directories */
-       protected $basePath;
-
-       /** @var array Map of container names to root paths for custom container paths */
-       protected $containerPaths = [];
-
-       /** @var int File permission mode */
-       protected $fileMode;
-
-       /** @var string Required OS username to own files */
-       protected $fileOwner;
-
-       /** @var string OS username running this script */
-       protected $currentUser;
-
-       /** @var array */
-       protected $hadWarningErrors = [];
-
-       /**
-        * @see FileBackendStore::__construct()
-        * Additional $config params include:
-        *   - basePath       : File system directory that holds containers.
-        *   - containerPaths : Map of container names to custom file system directories.
-        *                      This should only be used for backwards-compatibility.
-        *   - fileMode       : Octal UNIX file permissions to use on files stored.
-        * @param array $config
-        */
-       public function __construct( array $config ) {
-               parent::__construct( $config );
-
-               // Remove any possible trailing slash from directories
-               if ( isset( $config['basePath'] ) ) {
-                       $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
-               } else {
-                       $this->basePath = null; // none; containers must have explicit paths
-               }
-
-               if ( isset( $config['containerPaths'] ) ) {
-                       $this->containerPaths = (array)$config['containerPaths'];
-                       foreach ( $this->containerPaths as &$path ) {
-                               $path = rtrim( $path, '/' ); // remove trailing slash
-                       }
-               }
-
-               $this->fileMode = isset( $config['fileMode'] ) ? $config['fileMode'] : 0644;
-               if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) {
-                       $this->fileOwner = $config['fileOwner'];
-                       // cache this, assuming it doesn't change
-                       $this->currentUser = posix_getpwuid( posix_getuid() )['name'];
-               }
-       }
-
-       public function getFeatures() {
-               return !wfIsWindows() ? FileBackend::ATTR_UNICODE_PATHS : 0;
-       }
-
-       protected function resolveContainerPath( $container, $relStoragePath ) {
-               // Check that container has a root directory
-               if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
-                       // Check for sane relative paths (assume the base paths are OK)
-                       if ( $this->isLegalRelPath( $relStoragePath ) ) {
-                               return $relStoragePath;
-                       }
-               }
-
-               return null;
-       }
-
-       /**
-        * Sanity check a relative file system path for validity
-        *
-        * @param string $path Normalized relative path
-        * @return bool
-        */
-       protected function isLegalRelPath( $path ) {
-               // Check for file names longer than 255 chars
-               if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS
-                       return false;
-               }
-               if ( wfIsWindows() ) { // NTFS
-                       return !preg_match( '![:*?"<>|]!', $path );
-               } else {
-                       return true;
-               }
-       }
-
-       /**
-        * Given the short (unresolved) and full (resolved) name of
-        * a container, return the file system path of the container.
-        *
-        * @param string $shortCont
-        * @param string $fullCont
-        * @return string|null
-        */
-       protected function containerFSRoot( $shortCont, $fullCont ) {
-               if ( isset( $this->containerPaths[$shortCont] ) ) {
-                       return $this->containerPaths[$shortCont];
-               } elseif ( isset( $this->basePath ) ) {
-                       return "{$this->basePath}/{$fullCont}";
-               }
-
-               return null; // no container base path defined
-       }
-
-       /**
-        * Get the absolute file system path for a storage path
-        *
-        * @param string $storagePath Storage path
-        * @return string|null
-        */
-       protected function resolveToFSPath( $storagePath ) {
-               list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
-               if ( $relPath === null ) {
-                       return null; // invalid
-               }
-               list( , $shortCont, ) = FileBackend::splitStoragePath( $storagePath );
-               $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
-               if ( $relPath != '' ) {
-                       $fsPath .= "/{$relPath}";
-               }
-
-               return $fsPath;
-       }
-
-       public function isPathUsableInternal( $storagePath ) {
-               $fsPath = $this->resolveToFSPath( $storagePath );
-               if ( $fsPath === null ) {
-                       return false; // invalid
-               }
-               $parentDir = dirname( $fsPath );
-
-               if ( file_exists( $fsPath ) ) {
-                       $ok = is_file( $fsPath ) && is_writable( $fsPath );
-               } else {
-                       $ok = is_dir( $parentDir ) && is_writable( $parentDir );
-               }
-
-               if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) {
-                       $ok = false;
-                       trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." );
-               }
-
-               return $ok;
-       }
-
-       protected function doCreateInternal( array $params ) {
-               $status = $this->newStatus();
-
-               $dest = $this->resolveToFSPath( $params['dst'] );
-               if ( $dest === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $tempFile = TempFSFile::factory( 'create_', 'tmp', $this->tmpDirectory );
-                       if ( !$tempFile ) {
-                               $status->fatal( 'backend-fail-create', $params['dst'] );
-
-                               return $status;
-                       }
-                       $this->trapWarnings();
-                       $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
-                       $this->untrapWarnings();
-                       if ( $bytes === false ) {
-                               $status->fatal( 'backend-fail-create', $params['dst'] );
-
-                               return $status;
-                       }
-                       $cmd = implode( ' ', [
-                               wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
-                               wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
-                               wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
-                       ] );
-                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
-                               if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
-                                       $status->fatal( 'backend-fail-create', $params['dst'] );
-                                       trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
-                               }
-                       };
-                       $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
-                       $tempFile->bind( $status->value );
-               } else { // immediate write
-                       $this->trapWarnings();
-                       $bytes = file_put_contents( $dest, $params['content'] );
-                       $this->untrapWarnings();
-                       if ( $bytes === false ) {
-                               $status->fatal( 'backend-fail-create', $params['dst'] );
-
-                               return $status;
-                       }
-                       $this->chmod( $dest );
-               }
-
-               return $status;
-       }
-
-       protected function doStoreInternal( array $params ) {
-               $status = $this->newStatus();
-
-               $dest = $this->resolveToFSPath( $params['dst'] );
-               if ( $dest === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $cmd = implode( ' ', [
-                               wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
-                               wfEscapeShellArg( $this->cleanPathSlashes( $params['src'] ) ),
-                               wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
-                       ] );
-                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
-                               if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
-                                       $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
-                                       trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
-                               }
-                       };
-                       $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
-               } else { // immediate write
-                       $this->trapWarnings();
-                       $ok = copy( $params['src'], $dest );
-                       $this->untrapWarnings();
-                       // In some cases (at least over NFS), copy() returns true when it fails
-                       if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
-                               if ( $ok ) { // PHP bug
-                                       unlink( $dest ); // remove broken file
-                                       trigger_error( __METHOD__ . ": copy() failed but returned true." );
-                               }
-                               $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
-
-                               return $status;
-                       }
-                       $this->chmod( $dest );
-               }
-
-               return $status;
-       }
-
-       protected function doCopyInternal( array $params ) {
-               $status = $this->newStatus();
-
-               $source = $this->resolveToFSPath( $params['src'] );
-               if ( $source === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
-                       return $status;
-               }
-
-               $dest = $this->resolveToFSPath( $params['dst'] );
-               if ( $dest === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               if ( !is_file( $source ) ) {
-                       if ( empty( $params['ignoreMissingSource'] ) ) {
-                               $status->fatal( 'backend-fail-copy', $params['src'] );
-                       }
-
-                       return $status; // do nothing; either OK or bad status
-               }
-
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $cmd = implode( ' ', [
-                               wfIsWindows() ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
-                               wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
-                               wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
-                       ] );
-                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
-                               if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
-                                       $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
-                                       trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
-                               }
-                       };
-                       $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
-               } else { // immediate write
-                       $this->trapWarnings();
-                       $ok = ( $source === $dest ) ? true : copy( $source, $dest );
-                       $this->untrapWarnings();
-                       // In some cases (at least over NFS), copy() returns true when it fails
-                       if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
-                               if ( $ok ) { // PHP bug
-                                       $this->trapWarnings();
-                                       unlink( $dest ); // remove broken file
-                                       $this->untrapWarnings();
-                                       trigger_error( __METHOD__ . ": copy() failed but returned true." );
-                               }
-                               $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
-
-                               return $status;
-                       }
-                       $this->chmod( $dest );
-               }
-
-               return $status;
-       }
-
-       protected function doMoveInternal( array $params ) {
-               $status = $this->newStatus();
-
-               $source = $this->resolveToFSPath( $params['src'] );
-               if ( $source === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
-                       return $status;
-               }
-
-               $dest = $this->resolveToFSPath( $params['dst'] );
-               if ( $dest === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               if ( !is_file( $source ) ) {
-                       if ( empty( $params['ignoreMissingSource'] ) ) {
-                               $status->fatal( 'backend-fail-move', $params['src'] );
-                       }
-
-                       return $status; // do nothing; either OK or bad status
-               }
-
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $cmd = implode( ' ', [
-                               wfIsWindows() ? 'MOVE /Y' : 'mv', // (overwrite)
-                               wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
-                               wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
-                       ] );
-                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
-                               if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
-                                       $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
-                                       trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
-                               }
-                       };
-                       $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
-               } else { // immediate write
-                       $this->trapWarnings();
-                       $ok = ( $source === $dest ) ? true : rename( $source, $dest );
-                       $this->untrapWarnings();
-                       clearstatcache(); // file no longer at source
-                       if ( !$ok ) {
-                               $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
-
-                               return $status;
-                       }
-               }
-
-               return $status;
-       }
-
-       protected function doDeleteInternal( array $params ) {
-               $status = $this->newStatus();
-
-               $source = $this->resolveToFSPath( $params['src'] );
-               if ( $source === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
-                       return $status;
-               }
-
-               if ( !is_file( $source ) ) {
-                       if ( empty( $params['ignoreMissingSource'] ) ) {
-                               $status->fatal( 'backend-fail-delete', $params['src'] );
-                       }
-
-                       return $status; // do nothing; either OK or bad status
-               }
-
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $cmd = implode( ' ', [
-                               wfIsWindows() ? 'DEL' : 'unlink',
-                               wfEscapeShellArg( $this->cleanPathSlashes( $source ) )
-                       ] );
-                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
-                               if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
-                                       $status->fatal( 'backend-fail-delete', $params['src'] );
-                                       trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
-                               }
-                       };
-                       $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
-               } else { // immediate write
-                       $this->trapWarnings();
-                       $ok = unlink( $source );
-                       $this->untrapWarnings();
-                       if ( !$ok ) {
-                               $status->fatal( 'backend-fail-delete', $params['src'] );
-
-                               return $status;
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @param string $fullCont
-        * @param string $dirRel
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
-               $status = $this->newStatus();
-               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
-               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
-               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
-               $existed = is_dir( $dir ); // already there?
-               // Create the directory and its parents as needed...
-               $this->trapWarnings();
-               if ( !wfMkdirParents( $dir ) ) {
-                       wfDebugLog( 'FSFileBackend', __METHOD__ . ": cannot create directory $dir" );
-                       $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
-               } elseif ( !is_writable( $dir ) ) {
-                       wfDebugLog( 'FSFileBackend', __METHOD__ . ": directory $dir is read-only" );
-                       $status->fatal( 'directoryreadonlyerror', $params['dir'] );
-               } elseif ( !is_readable( $dir ) ) {
-                       wfDebugLog( 'FSFileBackend', __METHOD__ . ": directory $dir is not readable" );
-                       $status->fatal( 'directorynotreadableerror', $params['dir'] );
-               }
-               $this->untrapWarnings();
-               // Respect any 'noAccess' or 'noListing' flags...
-               if ( is_dir( $dir ) && !$existed ) {
-                       $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
-               }
-
-               return $status;
-       }
-
-       protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
-               $status = $this->newStatus();
-               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
-               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
-               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
-               // Seed new directories with a blank index.html, to prevent crawling...
-               if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
-                       $this->trapWarnings();
-                       $bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() );
-                       $this->untrapWarnings();
-                       if ( $bytes === false ) {
-                               $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
-                       }
-               }
-               // Add a .htaccess file to the root of the container...
-               if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) {
-                       $this->trapWarnings();
-                       $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
-                       $this->untrapWarnings();
-                       if ( $bytes === false ) {
-                               $storeDir = "mwstore://{$this->name}/{$shortCont}";
-                               $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
-                       }
-               }
-
-               return $status;
-       }
-
-       protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
-               $status = $this->newStatus();
-               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
-               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
-               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
-               // Unseed new directories with a blank index.html, to allow crawling...
-               if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) {
-                       $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() );
-                       $this->trapWarnings();
-                       if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure()
-                               $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
-                       }
-                       $this->untrapWarnings();
-               }
-               // Remove the .htaccess file from the root of the container...
-               if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) {
-                       $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() );
-                       $this->trapWarnings();
-                       if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
-                               $storeDir = "mwstore://{$this->name}/{$shortCont}";
-                               $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
-                       }
-                       $this->untrapWarnings();
-               }
-
-               return $status;
-       }
-
-       protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
-               $status = $this->newStatus();
-               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
-               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
-               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
-               $this->trapWarnings();
-               if ( is_dir( $dir ) ) {
-                       rmdir( $dir ); // remove directory if empty
-               }
-               $this->untrapWarnings();
-
-               return $status;
-       }
-
-       protected function doGetFileStat( array $params ) {
-               $source = $this->resolveToFSPath( $params['src'] );
-               if ( $source === null ) {
-                       return false; // invalid storage path
-               }
-
-               $this->trapWarnings(); // don't trust 'false' if there were errors
-               $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
-               $hadError = $this->untrapWarnings();
-
-               if ( $stat ) {
-                       return [
-                               'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
-                               'size' => $stat['size']
-                       ];
-               } elseif ( !$hadError ) {
-                       return false; // file does not exist
-               } else {
-                       return null; // failure
-               }
-       }
-
-       protected function doClearCache( array $paths = null ) {
-               clearstatcache(); // clear the PHP file stat cache
-       }
-
-       protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
-               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
-               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
-               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
-
-               $this->trapWarnings(); // don't trust 'false' if there were errors
-               $exists = is_dir( $dir );
-               $hadError = $this->untrapWarnings();
-
-               return $hadError ? null : $exists;
-       }
-
-       /**
-        * @see FileBackendStore::getDirectoryListInternal()
-        * @param string $fullCont
-        * @param string $dirRel
-        * @param array $params
-        * @return array|null
-        */
-       public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
-               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
-               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
-               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
-               $exists = is_dir( $dir );
-               if ( !$exists ) {
-                       wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
-
-                       return []; // nothing under this dir
-               } elseif ( !is_readable( $dir ) ) {
-                       wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
-
-                       return null; // bad permissions?
-               }
-
-               return new FSFileBackendDirList( $dir, $params );
-       }
-
-       /**
-        * @see FileBackendStore::getFileListInternal()
-        * @param string $fullCont
-        * @param string $dirRel
-        * @param array $params
-        * @return array|FSFileBackendFileList|null
-        */
-       public function getFileListInternal( $fullCont, $dirRel, array $params ) {
-               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
-               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
-               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
-               $exists = is_dir( $dir );
-               if ( !$exists ) {
-                       wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
-
-                       return []; // nothing under this dir
-               } elseif ( !is_readable( $dir ) ) {
-                       wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
-
-                       return null; // bad permissions?
-               }
-
-               return new FSFileBackendFileList( $dir, $params );
-       }
-
-       protected function doGetLocalReferenceMulti( array $params ) {
-               $fsFiles = []; // (path => FSFile)
-
-               foreach ( $params['srcs'] as $src ) {
-                       $source = $this->resolveToFSPath( $src );
-                       if ( $source === null || !is_file( $source ) ) {
-                               $fsFiles[$src] = null; // invalid path or file does not exist
-                       } else {
-                               $fsFiles[$src] = new FSFile( $source );
-                       }
-               }
-
-               return $fsFiles;
-       }
-
-       protected function doGetLocalCopyMulti( array $params ) {
-               $tmpFiles = []; // (path => TempFSFile)
-
-               foreach ( $params['srcs'] as $src ) {
-                       $source = $this->resolveToFSPath( $src );
-                       if ( $source === null ) {
-                               $tmpFiles[$src] = null; // invalid path
-                       } else {
-                               // Create a new temporary file with the same extension...
-                               $ext = FileBackend::extensionFromPath( $src );
-                               $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
-                               if ( !$tmpFile ) {
-                                       $tmpFiles[$src] = null;
-                               } else {
-                                       $tmpPath = $tmpFile->getPath();
-                                       // Copy the source file over the temp file
-                                       $this->trapWarnings();
-                                       $ok = copy( $source, $tmpPath );
-                                       $this->untrapWarnings();
-                                       if ( !$ok ) {
-                                               $tmpFiles[$src] = null;
-                                       } else {
-                                               $this->chmod( $tmpPath );
-                                               $tmpFiles[$src] = $tmpFile;
-                                       }
-                               }
-                       }
-               }
-
-               return $tmpFiles;
-       }
-
-       protected function directoriesAreVirtual() {
-               return false;
-       }
-
-       /**
-        * @param FSFileOpHandle[] $fileOpHandles
-        *
-        * @return StatusValue[]
-        */
-       protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
-               $statuses = [];
-
-               $pipes = [];
-               foreach ( $fileOpHandles as $index => $fileOpHandle ) {
-                       $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' );
-               }
-
-               $errs = [];
-               foreach ( $pipes as $index => $pipe ) {
-                       // Result will be empty on success in *NIX. On Windows,
-                       // it may be something like "        1 file(s) [copied|moved].".
-                       $errs[$index] = stream_get_contents( $pipe );
-                       fclose( $pipe );
-               }
-
-               foreach ( $fileOpHandles as $index => $fileOpHandle ) {
-                       $status = $this->newStatus();
-                       $function = $fileOpHandle->call;
-                       $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
-                       $statuses[$index] = $status;
-                       if ( $status->isOK() && $fileOpHandle->chmodPath ) {
-                               $this->chmod( $fileOpHandle->chmodPath );
-                       }
-               }
-
-               clearstatcache(); // files changed
-               return $statuses;
-       }
-
-       /**
-        * Chmod a file, suppressing the warnings
-        *
-        * @param string $path Absolute file system path
-        * @return bool Success
-        */
-       protected function chmod( $path ) {
-               $this->trapWarnings();
-               $ok = chmod( $path, $this->fileMode );
-               $this->untrapWarnings();
-
-               return $ok;
-       }
-
-       /**
-        * Return the text of an index.html file to hide directory listings
-        *
-        * @return string
-        */
-       protected function indexHtmlPrivate() {
-               return '';
-       }
-
-       /**
-        * Return the text of a .htaccess file to make a directory private
-        *
-        * @return string
-        */
-       protected function htaccessPrivate() {
-               return "Deny from all\n";
-       }
-
-       /**
-        * Clean up directory separators for the given OS
-        *
-        * @param string $path FS path
-        * @return string
-        */
-       protected function cleanPathSlashes( $path ) {
-               return wfIsWindows() ? strtr( $path, '/', '\\' ) : $path;
-       }
-
-       /**
-        * 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 );
-       }
-
-       /**
-        * Stop listening for E_WARNING errors and return true if any happened
-        *
-        * @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
-        * @access private
-        */
-       public function handleWarning( $errno, $errstr ) {
-               wfDebugLog( 'FSFileBackend', $errstr ); // more detailed error logging
-               $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
-
-               return true; // suppress from PHP handler
-       }
-}
-
-/**
- * @see FileBackendStoreOpHandle
- */
-class FSFileOpHandle extends FileBackendStoreOpHandle {
-       public $cmd; // string; shell command
-       public $chmodPath; // string; file to chmod
-
-       /**
-        * @param FSFileBackend $backend
-        * @param array $params
-        * @param callable $call
-        * @param string $cmd
-        * @param int|null $chmodPath
-        */
-       public function __construct(
-               FSFileBackend $backend, array $params, $call, $cmd, $chmodPath = null
-       ) {
-               $this->backend = $backend;
-               $this->params = $params;
-               $this->call = $call;
-               $this->cmd = $cmd;
-               $this->chmodPath = $chmodPath;
-       }
-}
-
-/**
- * Wrapper around RecursiveDirectoryIterator/DirectoryIterator that
- * catches exception or does any custom behavoir that we may want.
- * Do not use this class from places outside FSFileBackend.
- *
- * @ingroup FileBackend
- */
-abstract class FSFileBackendList implements Iterator {
-       /** @var Iterator */
-       protected $iter;
-
-       /** @var int */
-       protected $suffixStart;
-
-       /** @var int */
-       protected $pos = 0;
-
-       /** @var array */
-       protected $params = [];
-
-       /**
-        * @param string $dir File system directory
-        * @param array $params
-        */
-       public function __construct( $dir, array $params ) {
-               $path = realpath( $dir ); // normalize
-               if ( $path === false ) {
-                       $path = $dir;
-               }
-               $this->suffixStart = strlen( $path ) + 1; // size of "path/to/dir/"
-               $this->params = $params;
-
-               try {
-                       $this->iter = $this->initIterator( $path );
-               } catch ( UnexpectedValueException $e ) {
-                       $this->iter = null; // bad permissions? deleted?
-               }
-       }
-
-       /**
-        * Return an appropriate iterator object to wrap
-        *
-        * @param string $dir File system directory
-        * @return Iterator
-        */
-       protected function initIterator( $dir ) {
-               if ( !empty( $this->params['topOnly'] ) ) { // non-recursive
-                       # Get an iterator that will get direct sub-nodes
-                       return new DirectoryIterator( $dir );
-               } else { // recursive
-                       # Get an iterator that will return leaf nodes (non-directories)
-                       # RecursiveDirectoryIterator extends FilesystemIterator.
-                       # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
-                       $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
-
-                       return new RecursiveIteratorIterator(
-                               new RecursiveDirectoryIterator( $dir, $flags ),
-                               RecursiveIteratorIterator::CHILD_FIRST // include dirs
-                       );
-               }
-       }
-
-       /**
-        * @see Iterator::key()
-        * @return int
-        */
-       public function key() {
-               return $this->pos;
-       }
-
-       /**
-        * @see Iterator::current()
-        * @return string|bool String or false
-        */
-       public function current() {
-               return $this->getRelPath( $this->iter->current()->getPathname() );
-       }
-
-       /**
-        * @see Iterator::next()
-        * @throws FileBackendError
-        */
-       public function next() {
-               try {
-                       $this->iter->next();
-                       $this->filterViaNext();
-               } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
-                       throw new FileBackendError( "File iterator gave UnexpectedValueException." );
-               }
-               ++$this->pos;
-       }
-
-       /**
-        * @see Iterator::rewind()
-        * @throws FileBackendError
-        */
-       public function rewind() {
-               $this->pos = 0;
-               try {
-                       $this->iter->rewind();
-                       $this->filterViaNext();
-               } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
-                       throw new FileBackendError( "File iterator gave UnexpectedValueException." );
-               }
-       }
-
-       /**
-        * @see Iterator::valid()
-        * @return bool
-        */
-       public function valid() {
-               return $this->iter && $this->iter->valid();
-       }
-
-       /**
-        * Filter out items by advancing to the next ones
-        */
-       protected function filterViaNext() {
-       }
-
-       /**
-        * Return only the relative path and normalize slashes to FileBackend-style.
-        * Uses the "real path" since the suffix is based upon that.
-        *
-        * @param string $dir
-        * @return string
-        */
-       protected function getRelPath( $dir ) {
-               $path = realpath( $dir );
-               if ( $path === false ) {
-                       $path = $dir;
-               }
-
-               return strtr( substr( $path, $this->suffixStart ), '\\', '/' );
-       }
-}
-
-class FSFileBackendDirList extends FSFileBackendList {
-       protected function filterViaNext() {
-               while ( $this->iter->valid() ) {
-                       if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
-                               $this->iter->next(); // skip non-directories and dot files
-                       } else {
-                               break;
-                       }
-               }
-       }
-}
-
-class FSFileBackendFileList extends FSFileBackendList {
-       protected function filterViaNext() {
-               while ( $this->iter->valid() ) {
-                       if ( !$this->iter->current()->isFile() ) {
-                               $this->iter->next(); // skip non-files and dot files
-                       } else {
-                               break;
-                       }
-               }
-       }
-}
index d0a99d4..ede73aa 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup FileBackend
  * @author Aaron Schulz
  */
+use \MediaWiki\Logger\LoggerFactory;
 
 /**
  * Class to handle file backend registration
@@ -61,7 +62,7 @@ class FileBackendGroup {
         * Register file backends from the global variables
         */
        protected function initFromGlobals() {
-               global $wgLocalFileRepo, $wgForeignFileRepos, $wgFileBackends;
+               global $wgLocalFileRepo, $wgForeignFileRepos, $wgFileBackends, $wgDirectoryMode;
 
                // Register explicitly defined backends
                $this->register( $wgFileBackends, wfConfiguredReadOnlyReason() );
@@ -86,9 +87,6 @@ class FileBackendGroup {
                        $transcodedDir = isset( $info['transcodedDir'] )
                                ? $info['transcodedDir']
                                : "{$directory}/transcoded";
-                       $fileMode = isset( $info['fileMode'] )
-                               ? $info['fileMode']
-                               : 0644;
                        // Get the FS backend configuration
                        $autoBackends[] = [
                                'name' => $backendName,
@@ -101,7 +99,8 @@ class FileBackendGroup {
                                        "{$repoName}-deleted" => $deletedDir,
                                        "{$repoName}-temp" => "{$directory}/temp"
                                ],
-                               'fileMode' => $fileMode,
+                               'fileMode' => isset( $info['fileMode'] ) ? $info['fileMode'] : 0644,
+                               'directoryMode' => $wgDirectoryMode,
                        ];
                }
 
@@ -157,18 +156,32 @@ class FileBackendGroup {
                if ( !isset( $this->backends[$name]['instance'] ) ) {
                        $class = $this->backends[$name]['class'];
                        $config = $this->backends[$name]['config'];
-                       $config['wikiId'] = isset( $config['wikiId'] )
-                               ? $config['wikiId']
-                               : wfWikiID(); // e.g. "my_wiki-en_"
+                       $config += [
+                               'wikiId' => wfWikiID(), // e.g. "my_wiki-en_"
+                               'mimeCallback' => [ $this, 'guessMimeInternal' ],
+                               'obResetFunc' => 'wfResetOutputBuffers',
+                               'streamMimeFunc' => [ 'StreamFile', 'contentTypeFromPath' ]
+                       ];
                        $config['lockManager'] =
                                LockManagerGroup::singleton( $config['wikiId'] )->get( $config['lockManager'] );
                        $config['fileJournal'] = isset( $config['fileJournal'] )
                                ? FileJournal::factory( $config['fileJournal'], $name )
                                : FileJournal::factory( [ 'class' => 'NullFileJournal' ], $name );
                        $config['wanCache'] = ObjectCache::getMainWANInstance();
-                       $config['mimeCallback'] = [ $this, 'guessMimeInternal' ];
+                       $config['srvCache'] = ObjectCache::getLocalServerInstance( 'hash' );
                        $config['statusWrapper'] = [ 'Status', 'wrap' ];
                        $config['tmpDirectory'] = wfTempDir();
+                       $config['logger'] = LoggerFactory::getInstance( 'FileOperation' );
+                       $config['profiler'] = Profiler::instance();
+                       if ( $class === 'FileBackendMultiWrite' ) {
+                               foreach ( $config['backends'] as $index => $beConfig ) {
+                                       if ( isset( $beConfig['template'] ) ) {
+                                               // Config is just a modified version of a registered backend's.
+                                               // This should only be used when that config is used only by this backend.
+                                               $config['backends'][$index] += $this->config( $beConfig['template'] );
+                                       }
+                               }
+                       }
 
                        $this->backends[$name]['instance'] = new $class( $config );
                }
diff --git a/includes/filebackend/FileBackendMultiWrite.php b/includes/filebackend/FileBackendMultiWrite.php
deleted file mode 100644 (file)
index 52b84d4..0000000
+++ /dev/null
@@ -1,761 +0,0 @@
-<?php
-/**
- * Proxy backend that mirrors writes to several internal backends.
- *
- * 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 FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * @brief Proxy backend that mirrors writes to several internal backends.
- *
- * This class defines a multi-write backend. Multiple backends can be
- * registered to this proxy backend and it will act as a single backend.
- * Use this when all access to those backends is through this proxy backend.
- * At least one of the backends must be declared the "master" backend.
- *
- * Only use this class when transitioning from one storage system to another.
- *
- * Read operations are only done on the 'master' backend for consistency.
- * Write operations are performed on all backends, starting with the master.
- * This makes a best-effort to have transactional semantics, but since requests
- * may sometimes fail, the use of "autoResync" or background scripts to fix
- * inconsistencies is important.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-class FileBackendMultiWrite extends FileBackend {
-       /** @var FileBackendStore[] Prioritized list of FileBackendStore objects */
-       protected $backends = [];
-
-       /** @var int Index of master backend */
-       protected $masterIndex = -1;
-       /** @var int Index of read affinity backend */
-       protected $readIndex = -1;
-
-       /** @var int Bitfield */
-       protected $syncChecks = 0;
-       /** @var string|bool */
-       protected $autoResync = false;
-
-       /** @var bool */
-       protected $asyncWrites = false;
-
-       /* Possible internal backend consistency checks */
-       const CHECK_SIZE = 1;
-       const CHECK_TIME = 2;
-       const CHECK_SHA1 = 4;
-
-       /**
-        * Construct a proxy backend that consists of several internal backends.
-        * Locking, journaling, and read-only checks are handled by the proxy backend.
-        *
-        * Additional $config params include:
-        *   - backends       : Array of backend config and multi-backend settings.
-        *                      Each value is the config used in the constructor of a
-        *                      FileBackendStore class, but with these additional settings:
-        *                        - class         : The name of the backend class
-        *                        - isMultiMaster : This must be set for one backend.
-        *                        - readAffinity  : Use this for reads without 'latest' set.
-        *                        - template:     : If given a backend name, this will use
-        *                                          the config of that backend as a template.
-        *                                          Values specified here take precedence.
-        *   - syncChecks     : Integer bitfield of internal backend sync checks to perform.
-        *                      Possible bits include the FileBackendMultiWrite::CHECK_* constants.
-        *                      There are constants for SIZE, TIME, and SHA1.
-        *                      The checks are done before allowing any file operations.
-        *   - autoResync     : Automatically resync the clone backends to the master backend
-        *                      when pre-operation sync checks fail. This should only be used
-        *                      if the master backend is stable and not missing any files.
-        *                      Use "conservative" to limit resyncing to copying newer master
-        *                      backend files over older (or non-existing) clone backend files.
-        *                      Cases that cannot be handled will result in operation abortion.
-        *   - replication    : Set to 'async' to defer file operations on the non-master backends.
-        *                      This will apply such updates post-send for web requests. Note that
-        *                      any checks from "syncChecks" are still synchronous.
-        *
-        * @param array $config
-        * @throws FileBackendError
-        */
-       public function __construct( array $config ) {
-               parent::__construct( $config );
-               $this->syncChecks = isset( $config['syncChecks'] )
-                       ? $config['syncChecks']
-                       : self::CHECK_SIZE;
-               $this->autoResync = isset( $config['autoResync'] )
-                       ? $config['autoResync']
-                       : false;
-               $this->asyncWrites = isset( $config['replication'] ) && $config['replication'] === 'async';
-               // Construct backends here rather than via registration
-               // to keep these backends hidden from outside the proxy.
-               $namesUsed = [];
-               foreach ( $config['backends'] as $index => $config ) {
-                       if ( isset( $config['template'] ) ) {
-                               // Config is just a modified version of a registered backend's.
-                               // This should only be used when that config is used only by this backend.
-                               $config = $config + FileBackendGroup::singleton()->config( $config['template'] );
-                       }
-                       $name = $config['name'];
-                       if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
-                               throw new LogicException( "Two or more backends defined with the name $name." );
-                       }
-                       $namesUsed[$name] = 1;
-                       // Alter certain sub-backend settings for sanity
-                       unset( $config['readOnly'] ); // use proxy backend setting
-                       unset( $config['fileJournal'] ); // use proxy backend journal
-                       unset( $config['lockManager'] ); // lock under proxy backend
-                       $config['wikiId'] = $this->wikiId; // use the proxy backend wiki ID
-                       if ( !empty( $config['isMultiMaster'] ) ) {
-                               if ( $this->masterIndex >= 0 ) {
-                                       throw new LogicException( 'More than one master backend defined.' );
-                               }
-                               $this->masterIndex = $index; // this is the "master"
-                               $config['fileJournal'] = $this->fileJournal; // log under proxy backend
-                       }
-                       if ( !empty( $config['readAffinity'] ) ) {
-                               $this->readIndex = $index; // prefer this for reads
-                       }
-                       // Create sub-backend object
-                       if ( !isset( $config['class'] ) ) {
-                               throw new InvalidArgumentException( 'No class given for a backend config.' );
-                       }
-                       $class = $config['class'];
-                       $this->backends[$index] = new $class( $config );
-               }
-               if ( $this->masterIndex < 0 ) { // need backends and must have a master
-                       throw new LogicException( 'No master backend defined.' );
-               }
-               if ( $this->readIndex < 0 ) {
-                       $this->readIndex = $this->masterIndex; // default
-               }
-       }
-
-       final protected function doOperationsInternal( array $ops, array $opts ) {
-               $status = $this->newStatus();
-
-               $mbe = $this->backends[$this->masterIndex]; // convenience
-
-               // Try to lock those files for the scope of this function...
-               $scopeLock = null;
-               if ( empty( $opts['nonLocking'] ) ) {
-                       // Try to lock those files for the scope of this function...
-                       /** @noinspection PhpUnusedLocalVariableInspection */
-                       $scopeLock = $this->getScopedLocksForOps( $ops, $status );
-                       if ( !$status->isOK() ) {
-                               return $status; // abort
-                       }
-               }
-               // Clear any cache entries (after locks acquired)
-               $this->clearCache();
-               $opts['preserveCache'] = true; // only locked files are cached
-               // Get the list of paths to read/write...
-               $relevantPaths = $this->fileStoragePathsForOps( $ops );
-               // Check if the paths are valid and accessible on all backends...
-               $status->merge( $this->accessibilityCheck( $relevantPaths ) );
-               if ( !$status->isOK() ) {
-                       return $status; // abort
-               }
-               // Do a consistency check to see if the backends are consistent...
-               $syncStatus = $this->consistencyCheck( $relevantPaths );
-               if ( !$syncStatus->isOK() ) {
-                       wfDebugLog( 'FileOperation', get_class( $this ) .
-                               " failed sync check: " . FormatJson::encode( $relevantPaths ) );
-                       // Try to resync the clone backends to the master on the spot...
-                       if ( $this->autoResync === false
-                               || !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK()
-                       ) {
-                               $status->merge( $syncStatus );
-
-                               return $status; // abort
-                       }
-               }
-               // Actually attempt the operation batch on the master backend...
-               $realOps = $this->substOpBatchPaths( $ops, $mbe );
-               $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.
-               if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
-                       foreach ( $this->backends as $index => $backend ) {
-                               if ( $index === $this->masterIndex ) {
-                                       continue; // done already
-                               }
-
-                               $realOps = $this->substOpBatchPaths( $ops, $backend );
-                               if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
-                                       // Bind $scopeLock to the callback to preserve locks
-                                       DeferredUpdates::addCallableUpdate(
-                                               function() use ( $backend, $realOps, $opts, $scopeLock, $relevantPaths ) {
-                                                       wfDebugLog( 'FileOperationReplication',
-                                                               "'{$backend->getName()}' async replication; paths: " .
-                                                               FormatJson::encode( $relevantPaths ) );
-                                                       $backend->doOperations( $realOps, $opts );
-                                               }
-                                       );
-                               } else {
-                                       wfDebugLog( 'FileOperationReplication',
-                                               "'{$backend->getName()}' sync replication; paths: " .
-                                               FormatJson::encode( $relevantPaths ) );
-                                       $status->merge( $backend->doOperations( $realOps, $opts ) );
-                               }
-                       }
-               }
-               // Make 'success', 'successCount', and 'failCount' fields reflect
-               // the overall operation, rather than all the batches for each backend.
-               // Do this by only using success values from the master backend's batch.
-               $status->success = $masterStatus->success;
-               $status->successCount = $masterStatus->successCount;
-               $status->failCount = $masterStatus->failCount;
-
-               return $status;
-       }
-
-       /**
-        * Check that a set of files are consistent across all internal backends
-        *
-        * @param array $paths List of storage paths
-        * @return StatusValue
-        */
-       public function consistencyCheck( array $paths ) {
-               $status = $this->newStatus();
-               if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
-                       return $status; // skip checks
-               }
-
-               // Preload all of the stat info in as few round trips as possible...
-               foreach ( $this->backends as $backend ) {
-                       $realPaths = $this->substPaths( $paths, $backend );
-                       $backend->preloadFileStat( [ 'srcs' => $realPaths, 'latest' => true ] );
-               }
-
-               $mBackend = $this->backends[$this->masterIndex];
-               foreach ( $paths as $path ) {
-                       $params = [ 'src' => $path, 'latest' => true ];
-                       $mParams = $this->substOpPaths( $params, $mBackend );
-                       // Stat the file on the 'master' backend
-                       $mStat = $mBackend->getFileStat( $mParams );
-                       if ( $this->syncChecks & self::CHECK_SHA1 ) {
-                               $mSha1 = $mBackend->getFileSha1Base36( $mParams );
-                       } else {
-                               $mSha1 = false;
-                       }
-                       // Check if all clone backends agree with the master...
-                       foreach ( $this->backends as $index => $cBackend ) {
-                               if ( $index === $this->masterIndex ) {
-                                       continue; // master
-                               }
-                               $cParams = $this->substOpPaths( $params, $cBackend );
-                               $cStat = $cBackend->getFileStat( $cParams );
-                               if ( $mStat ) { // file is in master
-                                       if ( !$cStat ) { // file should exist
-                                               $status->fatal( 'backend-fail-synced', $path );
-                                               continue;
-                                       }
-                                       if ( $this->syncChecks & self::CHECK_SIZE ) {
-                                               if ( $cStat['size'] != $mStat['size'] ) { // wrong size
-                                                       $status->fatal( 'backend-fail-synced', $path );
-                                                       continue;
-                                               }
-                                       }
-                                       if ( $this->syncChecks & self::CHECK_TIME ) {
-                                               $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
-                                               $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
-                                               if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
-                                                       $status->fatal( 'backend-fail-synced', $path );
-                                                       continue;
-                                               }
-                                       }
-                                       if ( $this->syncChecks & self::CHECK_SHA1 ) {
-                                               if ( $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1
-                                                       $status->fatal( 'backend-fail-synced', $path );
-                                                       continue;
-                                               }
-                                       }
-                               } else { // file is not in master
-                                       if ( $cStat ) { // file should not exist
-                                               $status->fatal( 'backend-fail-synced', $path );
-                                       }
-                               }
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * Check that a set of file paths are usable across all internal backends
-        *
-        * @param array $paths List of storage paths
-        * @return StatusValue
-        */
-       public function accessibilityCheck( array $paths ) {
-               $status = $this->newStatus();
-               if ( count( $this->backends ) <= 1 ) {
-                       return $status; // skip checks
-               }
-
-               foreach ( $paths as $path ) {
-                       foreach ( $this->backends as $backend ) {
-                               $realPath = $this->substPaths( $path, $backend );
-                               if ( !$backend->isPathUsableInternal( $realPath ) ) {
-                                       $status->fatal( 'backend-fail-usable', $path );
-                               }
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * Check that a set of files are consistent across all internal backends
-        * and re-synchronize those files against the "multi master" if needed.
-        *
-        * @param array $paths List of storage paths
-        * @param string|bool $resyncMode False, True, or "conservative"; see __construct()
-        * @return StatusValue
-        */
-       public function resyncFiles( array $paths, $resyncMode = true ) {
-               $status = $this->newStatus();
-
-               $mBackend = $this->backends[$this->masterIndex];
-               foreach ( $paths as $path ) {
-                       $mPath = $this->substPaths( $path, $mBackend );
-                       $mSha1 = $mBackend->getFileSha1Base36( [ 'src' => $mPath, 'latest' => true ] );
-                       $mStat = $mBackend->getFileStat( [ 'src' => $mPath, 'latest' => true ] );
-                       if ( $mStat === null || ( $mSha1 !== false && !$mStat ) ) { // sanity
-                               $status->fatal( 'backend-fail-internal', $this->name );
-                               wfDebugLog( 'FileOperation', __METHOD__
-                                       . ': File is not available on the master backend' );
-                               continue; // file is not available on the master backend...
-                       }
-                       // Check of all clone backends agree with the master...
-                       foreach ( $this->backends as $index => $cBackend ) {
-                               if ( $index === $this->masterIndex ) {
-                                       continue; // master
-                               }
-                               $cPath = $this->substPaths( $path, $cBackend );
-                               $cSha1 = $cBackend->getFileSha1Base36( [ 'src' => $cPath, 'latest' => true ] );
-                               $cStat = $cBackend->getFileStat( [ 'src' => $cPath, 'latest' => true ] );
-                               if ( $cStat === null || ( $cSha1 !== false && !$cStat ) ) { // sanity
-                                       $status->fatal( 'backend-fail-internal', $cBackend->getName() );
-                                       wfDebugLog( 'FileOperation', __METHOD__ .
-                                               ': File is not available on the clone backend' );
-                                       continue; // file is not available on the clone backend...
-                               }
-                               if ( $mSha1 === $cSha1 ) {
-                                       // already synced; nothing to do
-                               } elseif ( $mSha1 !== false ) { // file is in master
-                                       if ( $resyncMode === 'conservative'
-                                               && $cStat && $cStat['mtime'] > $mStat['mtime']
-                                       ) {
-                                               $status->fatal( 'backend-fail-synced', $path );
-                                               continue; // don't rollback data
-                                       }
-                                       $fsFile = $mBackend->getLocalReference(
-                                               [ 'src' => $mPath, 'latest' => true ] );
-                                       $status->merge( $cBackend->quickStore(
-                                               [ 'src' => $fsFile->getPath(), 'dst' => $cPath ]
-                                       ) );
-                               } elseif ( $mStat === false ) { // file is not in master
-                                       if ( $resyncMode === 'conservative' ) {
-                                               $status->fatal( 'backend-fail-synced', $path );
-                                               continue; // don't delete data
-                                       }
-                                       $status->merge( $cBackend->quickDelete( [ 'src' => $cPath ] ) );
-                               }
-                       }
-               }
-
-               if ( !$status->isOK() ) {
-                       wfDebugLog( 'FileOperation', get_class( $this ) .
-                               " failed to resync: " . FormatJson::encode( $paths ) );
-               }
-
-               return $status;
-       }
-
-       /**
-        * Get a list of file storage paths to read or write for a list of operations
-        *
-        * @param array $ops Same format as doOperations()
-        * @return array List of storage paths to files (does not include directories)
-        */
-       protected function fileStoragePathsForOps( array $ops ) {
-               $paths = [];
-               foreach ( $ops as $op ) {
-                       if ( isset( $op['src'] ) ) {
-                               // For things like copy/move/delete with "ignoreMissingSource" and there
-                               // is no source file, nothing should happen and there should be no errors.
-                               if ( empty( $op['ignoreMissingSource'] )
-                                       || $this->fileExists( [ 'src' => $op['src'] ] )
-                               ) {
-                                       $paths[] = $op['src'];
-                               }
-                       }
-                       if ( isset( $op['srcs'] ) ) {
-                               $paths = array_merge( $paths, $op['srcs'] );
-                       }
-                       if ( isset( $op['dst'] ) ) {
-                               $paths[] = $op['dst'];
-                       }
-               }
-
-               return array_values( array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) ) );
-       }
-
-       /**
-        * Substitute the backend name in storage path parameters
-        * for a set of operations with that of a given internal backend.
-        *
-        * @param array $ops List of file operation arrays
-        * @param FileBackendStore $backend
-        * @return array
-        */
-       protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
-               $newOps = []; // operations
-               foreach ( $ops as $op ) {
-                       $newOp = $op; // operation
-                       foreach ( [ 'src', 'srcs', 'dst', 'dir' ] as $par ) {
-                               if ( isset( $newOp[$par] ) ) { // string or array
-                                       $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
-                               }
-                       }
-                       $newOps[] = $newOp;
-               }
-
-               return $newOps;
-       }
-
-       /**
-        * Same as substOpBatchPaths() but for a single operation
-        *
-        * @param array $ops File operation array
-        * @param FileBackendStore $backend
-        * @return array
-        */
-       protected function substOpPaths( array $ops, FileBackendStore $backend ) {
-               $newOps = $this->substOpBatchPaths( [ $ops ], $backend );
-
-               return $newOps[0];
-       }
-
-       /**
-        * Substitute the backend of storage paths with an internal backend's name
-        *
-        * @param array|string $paths List of paths or single string path
-        * @param FileBackendStore $backend
-        * @return array|string
-        */
-       protected function substPaths( $paths, FileBackendStore $backend ) {
-               return preg_replace(
-                       '!^mwstore://' . preg_quote( $this->name, '!' ) . '/!',
-                       StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
-                       $paths // string or array
-               );
-       }
-
-       /**
-        * Substitute the backend of internal storage paths with the proxy backend's name
-        *
-        * @param array|string $paths List of paths or single string path
-        * @return array|string
-        */
-       protected function unsubstPaths( $paths ) {
-               return preg_replace(
-                       '!^mwstore://([^/]+)!',
-                       StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
-                       $paths // string or array
-               );
-       }
-
-       /**
-        * @param array $ops File operations for FileBackend::doOperations()
-        * @return bool Whether there are file path sources with outside lifetime/ownership
-        */
-       protected function hasVolatileSources( array $ops ) {
-               foreach ( $ops as $op ) {
-                       if ( $op['op'] === 'store' && !isset( $op['srcRef'] ) ) {
-                               return true; // source file might be deleted anytime after do*Operations()
-                       }
-               }
-
-               return false;
-       }
-
-       protected function doQuickOperationsInternal( array $ops ) {
-               $status = $this->newStatus();
-               // Do the operations on the master backend; setting StatusValue fields...
-               $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
-               $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
-               $status->merge( $masterStatus );
-               // Propagate the operations to the clone backends...
-               foreach ( $this->backends as $index => $backend ) {
-                       if ( $index === $this->masterIndex ) {
-                               continue; // done already
-                       }
-
-                       $realOps = $this->substOpBatchPaths( $ops, $backend );
-                       if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
-                               DeferredUpdates::addCallableUpdate(
-                                       function() use ( $backend, $realOps ) {
-                                               $backend->doQuickOperations( $realOps );
-                                       }
-                               );
-                       } else {
-                               $status->merge( $backend->doQuickOperations( $realOps ) );
-                       }
-               }
-               // Make 'success', 'successCount', and 'failCount' fields reflect
-               // the overall operation, rather than all the batches for each backend.
-               // Do this by only using success values from the master backend's batch.
-               $status->success = $masterStatus->success;
-               $status->successCount = $masterStatus->successCount;
-               $status->failCount = $masterStatus->failCount;
-
-               return $status;
-       }
-
-       protected function doPrepare( array $params ) {
-               return $this->doDirectoryOp( 'prepare', $params );
-       }
-
-       protected function doSecure( array $params ) {
-               return $this->doDirectoryOp( 'secure', $params );
-       }
-
-       protected function doPublish( array $params ) {
-               return $this->doDirectoryOp( 'publish', $params );
-       }
-
-       protected function doClean( array $params ) {
-               return $this->doDirectoryOp( 'clean', $params );
-       }
-
-       /**
-        * @param string $method One of (doPrepare,doSecure,doPublish,doClean)
-        * @param array $params Method arguments
-        * @return StatusValue
-        */
-       protected function doDirectoryOp( $method, array $params ) {
-               $status = $this->newStatus();
-
-               $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
-               $masterStatus = $this->backends[$this->masterIndex]->$method( $realParams );
-               $status->merge( $masterStatus );
-
-               foreach ( $this->backends as $index => $backend ) {
-                       if ( $index === $this->masterIndex ) {
-                               continue; // already done
-                       }
-
-                       $realParams = $this->substOpPaths( $params, $backend );
-                       if ( $this->asyncWrites ) {
-                               DeferredUpdates::addCallableUpdate(
-                                       function() use ( $backend, $method, $realParams ) {
-                                               $backend->$method( $realParams );
-                                       }
-                               );
-                       } else {
-                               $status->merge( $backend->$method( $realParams ) );
-                       }
-               }
-
-               return $status;
-       }
-
-       public function concatenate( array $params ) {
-               // We are writing to an FS file, so we don't need to do this per-backend
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->concatenate( $realParams );
-       }
-
-       public function fileExists( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->fileExists( $realParams );
-       }
-
-       public function getFileTimestamp( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->getFileTimestamp( $realParams );
-       }
-
-       public function getFileSize( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->getFileSize( $realParams );
-       }
-
-       public function getFileStat( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->getFileStat( $realParams );
-       }
-
-       public function getFileXAttributes( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->getFileXAttributes( $realParams );
-       }
-
-       public function getFileContentsMulti( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               $contentsM = $this->backends[$index]->getFileContentsMulti( $realParams );
-
-               $contents = []; // (path => FSFile) mapping using the proxy backend's name
-               foreach ( $contentsM as $path => $data ) {
-                       $contents[$this->unsubstPaths( $path )] = $data;
-               }
-
-               return $contents;
-       }
-
-       public function getFileSha1Base36( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->getFileSha1Base36( $realParams );
-       }
-
-       public function getFileProps( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->getFileProps( $realParams );
-       }
-
-       public function streamFile( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->streamFile( $realParams );
-       }
-
-       public function getLocalReferenceMulti( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               $fsFilesM = $this->backends[$index]->getLocalReferenceMulti( $realParams );
-
-               $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name
-               foreach ( $fsFilesM as $path => $fsFile ) {
-                       $fsFiles[$this->unsubstPaths( $path )] = $fsFile;
-               }
-
-               return $fsFiles;
-       }
-
-       public function getLocalCopyMulti( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               $tempFilesM = $this->backends[$index]->getLocalCopyMulti( $realParams );
-
-               $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name
-               foreach ( $tempFilesM as $path => $tempFile ) {
-                       $tempFiles[$this->unsubstPaths( $path )] = $tempFile;
-               }
-
-               return $tempFiles;
-       }
-
-       public function getFileHttpUrl( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->getFileHttpUrl( $realParams );
-       }
-
-       public function directoryExists( array $params ) {
-               $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
-
-               return $this->backends[$this->masterIndex]->directoryExists( $realParams );
-       }
-
-       public function getDirectoryList( array $params ) {
-               $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
-
-               return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
-       }
-
-       public function getFileList( array $params ) {
-               $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
-
-               return $this->backends[$this->masterIndex]->getFileList( $realParams );
-       }
-
-       public function getFeatures() {
-               return $this->backends[$this->masterIndex]->getFeatures();
-       }
-
-       public function clearCache( array $paths = null ) {
-               foreach ( $this->backends as $backend ) {
-                       $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
-                       $backend->clearCache( $realPaths );
-               }
-       }
-
-       public function preloadCache( array $paths ) {
-               $realPaths = $this->substPaths( $paths, $this->backends[$this->readIndex] );
-               $this->backends[$this->readIndex]->preloadCache( $realPaths );
-       }
-
-       public function preloadFileStat( array $params ) {
-               $index = $this->getReadIndexFromParams( $params );
-               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
-
-               return $this->backends[$index]->preloadFileStat( $realParams );
-       }
-
-       public function getScopedLocksForOps( array $ops, StatusValue $status ) {
-               $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
-               $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $realOps );
-               // Get the paths to lock from the master backend
-               $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
-               // Get the paths under the proxy backend's name
-               $pbPaths = [
-                       LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ),
-                       LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] )
-               ];
-
-               // Actually acquire the locks
-               return $this->getScopedFileLocks( $pbPaths, 'mixed', $status );
-       }
-
-       /**
-        * @param array $params
-        * @return int The master or read affinity backend index, based on $params['latest']
-        */
-       protected function getReadIndexFromParams( array $params ) {
-               return !empty( $params['latest'] ) ? $this->masterIndex : $this->readIndex;
-       }
-}
diff --git a/includes/filebackend/FileBackendStore.php b/includes/filebackend/FileBackendStore.php
deleted file mode 100644 (file)
index 9efec36..0000000
+++ /dev/null
@@ -1,1972 +0,0 @@
-<?php
-/**
- * Base class for all backends using particular storage medium.
- *
- * 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 FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * @brief Base class for all backends using particular storage medium.
- *
- * This class defines the methods as abstract that subclasses must implement.
- * Outside callers should *not* use functions with "Internal" in the name.
- *
- * The FileBackend operations are implemented using basic functions
- * such as storeInternal(), copyInternal(), deleteInternal() and the like.
- * This class is also responsible for path resolution and sanitization.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-abstract class FileBackendStore extends FileBackend {
-       /** @var WANObjectCache */
-       protected $memCache;
-       /** @var ProcessCacheLRU Map of paths to small (RAM/disk) cache items */
-       protected $cheapCache;
-       /** @var ProcessCacheLRU Map of paths to large (RAM/disk) cache items */
-       protected $expensiveCache;
-
-       /** @var array Map of container names to sharding config */
-       protected $shardViaHashLevels = [];
-
-       /** @var callable Method to get the MIME type of files */
-       protected $mimeCallback;
-
-       protected $maxFileSize = 4294967296; // integer bytes (4GiB)
-
-       const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
-       const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache"
-       const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
-
-       /**
-        * @see FileBackend::__construct()
-        * Additional $config params include:
-        *   - wanCache     : WANObjectCache object to use for persistent caching.
-        *   - mimeCallback : Callback that takes (storage path, content, file system path) and
-        *                    returns the MIME type of the file or 'unknown/unknown'. The file
-        *                    system path parameter should be used if the content one is null.
-        *
-        * @param array $config
-        */
-       public function __construct( array $config ) {
-               parent::__construct( $config );
-               $this->mimeCallback = isset( $config['mimeCallback'] )
-                       ? $config['mimeCallback']
-                       : null;
-               $this->memCache = WANObjectCache::newEmpty(); // disabled by default
-               $this->cheapCache = new ProcessCacheLRU( self::CACHE_CHEAP_SIZE );
-               $this->expensiveCache = new ProcessCacheLRU( self::CACHE_EXPENSIVE_SIZE );
-       }
-
-       /**
-        * Get the maximum allowable file size given backend
-        * medium restrictions and basic performance constraints.
-        * Do not call this function from places outside FileBackend and FileOp.
-        *
-        * @return int Bytes
-        */
-       final public function maxFileSizeInternal() {
-               return $this->maxFileSize;
-       }
-
-       /**
-        * Check if a file can be created or changed at a given storage path.
-        * FS backends should check if the parent directory exists, files can be
-        * written under it, and that any file already there is writable.
-        * Backends using key/value stores should check if the container exists.
-        *
-        * @param string $storagePath
-        * @return bool
-        */
-       abstract public function isPathUsableInternal( $storagePath );
-
-       /**
-        * Create a file in the backend with the given contents.
-        * This will overwrite any file that exists at the destination.
-        * Do not call this function from places outside FileBackend and FileOp.
-        *
-        * $params include:
-        *   - content     : the raw file contents
-        *   - dst         : destination storage path
-        *   - headers     : HTTP header name/value map
-        *   - async       : StatusValue will be returned immediately if supported.
-        *                   If the StatusValue is OK, then its value field will be
-        *                   set to a FileBackendStoreOpHandle object.
-        *   - dstExists   : Whether a file exists at the destination (optimization).
-        *                   Callers can use "false" if no existing file is being changed.
-        *
-        * @param array $params
-        * @return StatusValue
-        */
-       final public function createInternal( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
-                       $status = $this->newStatus( 'backend-fail-maxsize',
-                               $params['dst'], $this->maxFileSizeInternal() );
-               } else {
-                       $status = $this->doCreateInternal( $params );
-                       $this->clearCache( [ $params['dst'] ] );
-                       if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
-                               $this->deleteFileCache( $params['dst'] ); // persistent cache
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::createInternal()
-        * @param array $params
-        * @return StatusValue
-        */
-       abstract protected function doCreateInternal( array $params );
-
-       /**
-        * Store a file into the backend from a file on disk.
-        * This will overwrite any file that exists at the destination.
-        * Do not call this function from places outside FileBackend and FileOp.
-        *
-        * $params include:
-        *   - src         : source path on disk
-        *   - dst         : destination storage path
-        *   - headers     : HTTP header name/value map
-        *   - async       : StatusValue will be returned immediately if supported.
-        *                   If the StatusValue is OK, then its value field will be
-        *                   set to a FileBackendStoreOpHandle object.
-        *   - dstExists   : Whether a file exists at the destination (optimization).
-        *                   Callers can use "false" if no existing file is being changed.
-        *
-        * @param array $params
-        * @return StatusValue
-        */
-       final public function storeInternal( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
-                       $status = $this->newStatus( 'backend-fail-maxsize',
-                               $params['dst'], $this->maxFileSizeInternal() );
-               } else {
-                       $status = $this->doStoreInternal( $params );
-                       $this->clearCache( [ $params['dst'] ] );
-                       if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
-                               $this->deleteFileCache( $params['dst'] ); // persistent cache
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::storeInternal()
-        * @param array $params
-        * @return StatusValue
-        */
-       abstract protected function doStoreInternal( array $params );
-
-       /**
-        * Copy a file from one storage path to another in the backend.
-        * This will overwrite any file that exists at the destination.
-        * Do not call this function from places outside FileBackend and FileOp.
-        *
-        * $params include:
-        *   - src                 : source storage path
-        *   - dst                 : destination storage path
-        *   - ignoreMissingSource : do nothing if the source file does not exist
-        *   - headers             : HTTP header name/value map
-        *   - async               : StatusValue will be returned immediately if supported.
-        *                           If the StatusValue is OK, then its value field will be
-        *                           set to a FileBackendStoreOpHandle object.
-        *   - dstExists           : Whether a file exists at the destination (optimization).
-        *                           Callers can use "false" if no existing file is being changed.
-        *
-        * @param array $params
-        * @return StatusValue
-        */
-       final public function copyInternal( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->doCopyInternal( $params );
-               $this->clearCache( [ $params['dst'] ] );
-               if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
-                       $this->deleteFileCache( $params['dst'] ); // persistent cache
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::copyInternal()
-        * @param array $params
-        * @return StatusValue
-        */
-       abstract protected function doCopyInternal( array $params );
-
-       /**
-        * Delete a file at the storage path.
-        * Do not call this function from places outside FileBackend and FileOp.
-        *
-        * $params include:
-        *   - src                 : source storage path
-        *   - ignoreMissingSource : do nothing if the source file does not exist
-        *   - async               : StatusValue will be returned immediately if supported.
-        *                           If the StatusValue is OK, then its value field will be
-        *                           set to a FileBackendStoreOpHandle object.
-        *
-        * @param array $params
-        * @return StatusValue
-        */
-       final public function deleteInternal( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->doDeleteInternal( $params );
-               $this->clearCache( [ $params['src'] ] );
-               $this->deleteFileCache( $params['src'] ); // persistent cache
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::deleteInternal()
-        * @param array $params
-        * @return StatusValue
-        */
-       abstract protected function doDeleteInternal( array $params );
-
-       /**
-        * Move a file from one storage path to another in the backend.
-        * This will overwrite any file that exists at the destination.
-        * Do not call this function from places outside FileBackend and FileOp.
-        *
-        * $params include:
-        *   - src                 : source storage path
-        *   - dst                 : destination storage path
-        *   - ignoreMissingSource : do nothing if the source file does not exist
-        *   - headers             : HTTP header name/value map
-        *   - async               : StatusValue will be returned immediately if supported.
-        *                           If the StatusValue is OK, then its value field will be
-        *                           set to a FileBackendStoreOpHandle object.
-        *   - dstExists           : Whether a file exists at the destination (optimization).
-        *                           Callers can use "false" if no existing file is being changed.
-        *
-        * @param array $params
-        * @return StatusValue
-        */
-       final public function moveInternal( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->doMoveInternal( $params );
-               $this->clearCache( [ $params['src'], $params['dst'] ] );
-               $this->deleteFileCache( $params['src'] ); // persistent cache
-               if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
-                       $this->deleteFileCache( $params['dst'] ); // persistent cache
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::moveInternal()
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function doMoveInternal( array $params ) {
-               unset( $params['async'] ); // two steps, won't work here :)
-               $nsrc = FileBackend::normalizeStoragePath( $params['src'] );
-               $ndst = FileBackend::normalizeStoragePath( $params['dst'] );
-               // Copy source to dest
-               $status = $this->copyInternal( $params );
-               if ( $nsrc !== $ndst && $status->isOK() ) {
-                       // Delete source (only fails due to races or network problems)
-                       $status->merge( $this->deleteInternal( [ 'src' => $params['src'] ] ) );
-                       $status->setResult( true, $status->value ); // ignore delete() errors
-               }
-
-               return $status;
-       }
-
-       /**
-        * Alter metadata for a file at the storage path.
-        * Do not call this function from places outside FileBackend and FileOp.
-        *
-        * $params include:
-        *   - src           : source storage path
-        *   - headers       : HTTP header name/value map
-        *   - async         : StatusValue will be returned immediately if supported.
-        *                     If the StatusValue is OK, then its value field will be
-        *                     set to a FileBackendStoreOpHandle object.
-        *
-        * @param array $params
-        * @return StatusValue
-        */
-       final public function describeInternal( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               if ( count( $params['headers'] ) ) {
-                       $status = $this->doDescribeInternal( $params );
-                       $this->clearCache( [ $params['src'] ] );
-                       $this->deleteFileCache( $params['src'] ); // persistent cache
-               } else {
-                       $status = $this->newStatus(); // nothing to do
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::describeInternal()
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function doDescribeInternal( array $params ) {
-               return $this->newStatus();
-       }
-
-       /**
-        * No-op file operation that does nothing.
-        * Do not call this function from places outside FileBackend and FileOp.
-        *
-        * @param array $params
-        * @return StatusValue
-        */
-       final public function nullInternal( array $params ) {
-               return $this->newStatus();
-       }
-
-       final public function concatenate( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->newStatus();
-
-               // Try to lock the source files for the scope of this function
-               $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
-               if ( $status->isOK() ) {
-                       // Actually do the file concatenation...
-                       $start_time = microtime( true );
-                       $status->merge( $this->doConcatenate( $params ) );
-                       $sec = microtime( true ) - $start_time;
-                       if ( !$status->isOK() ) {
-                               wfDebugLog( 'FileOperation', get_class( $this ) . "-{$this->name}" .
-                                       " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" );
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::concatenate()
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function doConcatenate( array $params ) {
-               $status = $this->newStatus();
-               $tmpPath = $params['dst']; // convenience
-               unset( $params['latest'] ); // sanity
-
-               // Check that the specified temp file is valid...
-               MediaWiki\suppressWarnings();
-               $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
-               MediaWiki\restoreWarnings();
-               if ( !$ok ) { // not present or not empty
-                       $status->fatal( 'backend-fail-opentemp', $tmpPath );
-
-                       return $status;
-               }
-
-               // Get local FS versions of the chunks needed for the concatenation...
-               $fsFiles = $this->getLocalReferenceMulti( $params );
-               foreach ( $fsFiles as $path => &$fsFile ) {
-                       if ( !$fsFile ) { // chunk failed to download?
-                               $fsFile = $this->getLocalReference( [ 'src' => $path ] );
-                               if ( !$fsFile ) { // retry failed?
-                                       $status->fatal( 'backend-fail-read', $path );
-
-                                       return $status;
-                               }
-                       }
-               }
-               unset( $fsFile ); // unset reference so we can reuse $fsFile
-
-               // Get a handle for the destination temp file
-               $tmpHandle = fopen( $tmpPath, 'ab' );
-               if ( $tmpHandle === false ) {
-                       $status->fatal( 'backend-fail-opentemp', $tmpPath );
-
-                       return $status;
-               }
-
-               // Build up the temp file using the source chunks (in order)...
-               foreach ( $fsFiles as $virtualSource => $fsFile ) {
-                       // Get a handle to the local FS version
-                       $sourceHandle = fopen( $fsFile->getPath(), 'rb' );
-                       if ( $sourceHandle === false ) {
-                               fclose( $tmpHandle );
-                               $status->fatal( 'backend-fail-read', $virtualSource );
-
-                               return $status;
-                       }
-                       // Append chunk to file (pass chunk size to avoid magic quotes)
-                       if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
-                               fclose( $sourceHandle );
-                               fclose( $tmpHandle );
-                               $status->fatal( 'backend-fail-writetemp', $tmpPath );
-
-                               return $status;
-                       }
-                       fclose( $sourceHandle );
-               }
-               if ( !fclose( $tmpHandle ) ) {
-                       $status->fatal( 'backend-fail-closetemp', $tmpPath );
-
-                       return $status;
-               }
-
-               clearstatcache(); // temp file changed
-
-               return $status;
-       }
-
-       final protected function doPrepare( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->newStatus();
-
-               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
-
-                       return $status; // invalid storage path
-               }
-
-               if ( $shard !== null ) { // confined to a single container/shard
-                       $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
-               } else { // directory is on several shards
-                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
-                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
-                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
-                               $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::doPrepare()
-        * @param string $container
-        * @param string $dir
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function doPrepareInternal( $container, $dir, array $params ) {
-               return $this->newStatus();
-       }
-
-       final protected function doSecure( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->newStatus();
-
-               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
-
-                       return $status; // invalid storage path
-               }
-
-               if ( $shard !== null ) { // confined to a single container/shard
-                       $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
-               } else { // directory is on several shards
-                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
-                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
-                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
-                               $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::doSecure()
-        * @param string $container
-        * @param string $dir
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function doSecureInternal( $container, $dir, array $params ) {
-               return $this->newStatus();
-       }
-
-       final protected function doPublish( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->newStatus();
-
-               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
-
-                       return $status; // invalid storage path
-               }
-
-               if ( $shard !== null ) { // confined to a single container/shard
-                       $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
-               } else { // directory is on several shards
-                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
-                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
-                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
-                               $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::doPublish()
-        * @param string $container
-        * @param string $dir
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function doPublishInternal( $container, $dir, array $params ) {
-               return $this->newStatus();
-       }
-
-       final protected function doClean( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->newStatus();
-
-               // Recursive: first delete all empty subdirs recursively
-               if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
-                       $subDirsRel = $this->getTopDirectoryList( [ 'dir' => $params['dir'] ] );
-                       if ( $subDirsRel !== null ) { // no errors
-                               foreach ( $subDirsRel as $subDirRel ) {
-                                       $subDir = $params['dir'] . "/{$subDirRel}"; // full path
-                                       $status->merge( $this->doClean( [ 'dir' => $subDir ] + $params ) );
-                               }
-                               unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends)
-                       }
-               }
-
-               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
-
-                       return $status; // invalid storage path
-               }
-
-               // Attempt to lock this directory...
-               $filesLockEx = [ $params['dir'] ];
-               $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
-               if ( !$status->isOK() ) {
-                       return $status; // abort
-               }
-
-               if ( $shard !== null ) { // confined to a single container/shard
-                       $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
-                       $this->deleteContainerCache( $fullCont ); // purge cache
-               } else { // directory is on several shards
-                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
-                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
-                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
-                               $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
-                               $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::doClean()
-        * @param string $container
-        * @param string $dir
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function doCleanInternal( $container, $dir, array $params ) {
-               return $this->newStatus();
-       }
-
-       final public function fileExists( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $stat = $this->getFileStat( $params );
-
-               return ( $stat === null ) ? null : (bool)$stat; // null => failure
-       }
-
-       final public function getFileTimestamp( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $stat = $this->getFileStat( $params );
-
-               return $stat ? $stat['mtime'] : false;
-       }
-
-       final public function getFileSize( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $stat = $this->getFileStat( $params );
-
-               return $stat ? $stat['size'] : false;
-       }
-
-       final public function getFileStat( array $params ) {
-               $path = self::normalizeStoragePath( $params['src'] );
-               if ( $path === null ) {
-                       return false; // invalid storage path
-               }
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $latest = !empty( $params['latest'] ); // use latest data?
-               if ( !$latest && !$this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
-                       $this->primeFileCache( [ $path ] ); // check persistent cache
-               }
-               if ( $this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
-                       $stat = $this->cheapCache->get( $path, 'stat' );
-                       // If we want the latest data, check that this cached
-                       // value was in fact fetched with the latest available data.
-                       if ( is_array( $stat ) ) {
-                               if ( !$latest || $stat['latest'] ) {
-                                       return $stat;
-                               }
-                       } elseif ( in_array( $stat, [ 'NOT_EXIST', 'NOT_EXIST_LATEST' ] ) ) {
-                               if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) {
-                                       return false;
-                               }
-                       }
-               }
-               $stat = $this->doGetFileStat( $params );
-               if ( is_array( $stat ) ) { // file exists
-                       // Strongly consistent backends can automatically set "latest"
-                       $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
-                       $this->cheapCache->set( $path, 'stat', $stat );
-                       $this->setFileCache( $path, $stat ); // update persistent cache
-                       if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
-                               $this->cheapCache->set( $path, 'sha1',
-                                       [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
-                       }
-                       if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
-                               $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
-                               $this->cheapCache->set( $path, 'xattr',
-                                       [ 'map' => $stat['xattr'], 'latest' => $latest ] );
-                       }
-               } elseif ( $stat === false ) { // file does not exist
-                       $this->cheapCache->set( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
-                       $this->cheapCache->set( $path, 'xattr', [ 'map' => false, 'latest' => $latest ] );
-                       $this->cheapCache->set( $path, 'sha1', [ 'hash' => false, 'latest' => $latest ] );
-                       wfDebug( __METHOD__ . ": File $path does not exist.\n" );
-               } else { // an error occurred
-                       wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
-               }
-
-               return $stat;
-       }
-
-       /**
-        * @see FileBackendStore::getFileStat()
-        */
-       abstract protected function doGetFileStat( array $params );
-
-       public function getFileContentsMulti( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
-               $params = $this->setConcurrencyFlags( $params );
-               $contents = $this->doGetFileContentsMulti( $params );
-
-               return $contents;
-       }
-
-       /**
-        * @see FileBackendStore::getFileContentsMulti()
-        * @param array $params
-        * @return array
-        */
-       protected function doGetFileContentsMulti( array $params ) {
-               $contents = [];
-               foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
-                       MediaWiki\suppressWarnings();
-                       $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
-                       MediaWiki\restoreWarnings();
-               }
-
-               return $contents;
-       }
-
-       final public function getFileXAttributes( array $params ) {
-               $path = self::normalizeStoragePath( $params['src'] );
-               if ( $path === null ) {
-                       return false; // invalid storage path
-               }
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $latest = !empty( $params['latest'] ); // use latest data?
-               if ( $this->cheapCache->has( $path, 'xattr', self::CACHE_TTL ) ) {
-                       $stat = $this->cheapCache->get( $path, 'xattr' );
-                       // If we want the latest data, check that this cached
-                       // value was in fact fetched with the latest available data.
-                       if ( !$latest || $stat['latest'] ) {
-                               return $stat['map'];
-                       }
-               }
-               $fields = $this->doGetFileXAttributes( $params );
-               $fields = is_array( $fields ) ? self::normalizeXAttributes( $fields ) : false;
-               $this->cheapCache->set( $path, 'xattr', [ 'map' => $fields, 'latest' => $latest ] );
-
-               return $fields;
-       }
-
-       /**
-        * @see FileBackendStore::getFileXAttributes()
-        * @return bool|string
-        */
-       protected function doGetFileXAttributes( array $params ) {
-               return [ 'headers' => [], 'metadata' => [] ]; // not supported
-       }
-
-       final public function getFileSha1Base36( array $params ) {
-               $path = self::normalizeStoragePath( $params['src'] );
-               if ( $path === null ) {
-                       return false; // invalid storage path
-               }
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $latest = !empty( $params['latest'] ); // use latest data?
-               if ( $this->cheapCache->has( $path, 'sha1', self::CACHE_TTL ) ) {
-                       $stat = $this->cheapCache->get( $path, 'sha1' );
-                       // If we want the latest data, check that this cached
-                       // value was in fact fetched with the latest available data.
-                       if ( !$latest || $stat['latest'] ) {
-                               return $stat['hash'];
-                       }
-               }
-               $hash = $this->doGetFileSha1Base36( $params );
-               $this->cheapCache->set( $path, 'sha1', [ 'hash' => $hash, 'latest' => $latest ] );
-
-               return $hash;
-       }
-
-       /**
-        * @see FileBackendStore::getFileSha1Base36()
-        * @param array $params
-        * @return bool|string
-        */
-       protected function doGetFileSha1Base36( array $params ) {
-               $fsFile = $this->getLocalReference( $params );
-               if ( !$fsFile ) {
-                       return false;
-               } else {
-                       return $fsFile->getSha1Base36();
-               }
-       }
-
-       final public function getFileProps( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $fsFile = $this->getLocalReference( $params );
-               $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
-
-               return $props;
-       }
-
-       final public function getLocalReferenceMulti( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
-               $params = $this->setConcurrencyFlags( $params );
-
-               $fsFiles = []; // (path => FSFile)
-               $latest = !empty( $params['latest'] ); // use latest data?
-               // Reuse any files already in process cache...
-               foreach ( $params['srcs'] as $src ) {
-                       $path = self::normalizeStoragePath( $src );
-                       if ( $path === null ) {
-                               $fsFiles[$src] = null; // invalid storage path
-                       } elseif ( $this->expensiveCache->has( $path, 'localRef' ) ) {
-                               $val = $this->expensiveCache->get( $path, 'localRef' );
-                               // If we want the latest data, check that this cached
-                               // value was in fact fetched with the latest available data.
-                               if ( !$latest || $val['latest'] ) {
-                                       $fsFiles[$src] = $val['object'];
-                               }
-                       }
-               }
-               // Fetch local references of any remaning files...
-               $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
-               foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
-                       $fsFiles[$path] = $fsFile;
-                       if ( $fsFile ) { // update the process cache...
-                               $this->expensiveCache->set( $path, 'localRef',
-                                       [ 'object' => $fsFile, 'latest' => $latest ] );
-                       }
-               }
-
-               return $fsFiles;
-       }
-
-       /**
-        * @see FileBackendStore::getLocalReferenceMulti()
-        * @param array $params
-        * @return array
-        */
-       protected function doGetLocalReferenceMulti( array $params ) {
-               return $this->doGetLocalCopyMulti( $params );
-       }
-
-       final public function getLocalCopyMulti( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
-               $params = $this->setConcurrencyFlags( $params );
-               $tmpFiles = $this->doGetLocalCopyMulti( $params );
-
-               return $tmpFiles;
-       }
-
-       /**
-        * @see FileBackendStore::getLocalCopyMulti()
-        * @param array $params
-        * @return array
-        */
-       abstract protected function doGetLocalCopyMulti( array $params );
-
-       /**
-        * @see FileBackend::getFileHttpUrl()
-        * @param array $params
-        * @return string|null
-        */
-       public function getFileHttpUrl( array $params ) {
-               return null; // not supported
-       }
-
-       final public function streamFile( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->newStatus();
-
-               // Always set some fields for subclass convenience
-               $params['options'] = isset( $params['options'] ) ? $params['options'] : [];
-               $params['headers'] = isset( $params['headers'] ) ? $params['headers'] : [];
-
-               // Don't stream it out as text/html if there was a PHP error
-               if ( ( empty( $params['headless'] ) || $params['headers'] ) && headers_sent() ) {
-                       print "Headers already sent, terminating.\n";
-                       $status->fatal( 'backend-fail-stream', $params['src'] );
-                       return $status;
-               }
-
-               $status->merge( $this->doStreamFile( $params ) );
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::streamFile()
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function doStreamFile( array $params ) {
-               $status = $this->newStatus();
-
-               $flags = 0;
-               $flags |= !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
-               $flags |= !empty( $params['allowOB'] ) ? StreamFile::STREAM_ALLOW_OB : 0;
-
-               $fsFile = $this->getLocalReference( $params );
-
-               if ( $fsFile ) {
-                       $res = StreamFile::stream( $fsFile->getPath(),
-                               $params['headers'], true, $params['options'], $flags );
-               } else {
-                       $res = false;
-                       StreamFile::send404Message( $params['src'], $flags );
-               }
-
-               if ( !$res ) {
-                       $status->fatal( 'backend-fail-stream', $params['src'] );
-               }
-
-               return $status;
-       }
-
-       final public function directoryExists( array $params ) {
-               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) {
-                       return false; // invalid storage path
-               }
-               if ( $shard !== null ) { // confined to a single container/shard
-                       return $this->doDirectoryExists( $fullCont, $dir, $params );
-               } else { // directory is on several shards
-                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
-                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
-                       $res = false; // response
-                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
-                               $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
-                               if ( $exists ) {
-                                       $res = true;
-                                       break; // found one!
-                               } elseif ( $exists === null ) { // error?
-                                       $res = null; // if we don't find anything, it is indeterminate
-                               }
-                       }
-
-                       return $res;
-               }
-       }
-
-       /**
-        * @see FileBackendStore::directoryExists()
-        *
-        * @param string $container Resolved container name
-        * @param string $dir Resolved path relative to container
-        * @param array $params
-        * @return bool|null
-        */
-       abstract protected function doDirectoryExists( $container, $dir, array $params );
-
-       final public function getDirectoryList( array $params ) {
-               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) { // invalid storage path
-                       return null;
-               }
-               if ( $shard !== null ) {
-                       // File listing is confined to a single container/shard
-                       return $this->getDirectoryListInternal( $fullCont, $dir, $params );
-               } else {
-                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
-                       // File listing spans multiple containers/shards
-                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
-
-                       return new FileBackendStoreShardDirIterator( $this,
-                               $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
-               }
-       }
-
-       /**
-        * Do not call this function from places outside FileBackend
-        *
-        * @see FileBackendStore::getDirectoryList()
-        *
-        * @param string $container Resolved container name
-        * @param string $dir Resolved path relative to container
-        * @param array $params
-        * @return Traversable|array|null Returns null on failure
-        */
-       abstract public function getDirectoryListInternal( $container, $dir, array $params );
-
-       final public function getFileList( array $params ) {
-               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
-               if ( $dir === null ) { // invalid storage path
-                       return null;
-               }
-               if ( $shard !== null ) {
-                       // File listing is confined to a single container/shard
-                       return $this->getFileListInternal( $fullCont, $dir, $params );
-               } else {
-                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
-                       // File listing spans multiple containers/shards
-                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
-
-                       return new FileBackendStoreShardFileIterator( $this,
-                               $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
-               }
-       }
-
-       /**
-        * Do not call this function from places outside FileBackend
-        *
-        * @see FileBackendStore::getFileList()
-        *
-        * @param string $container Resolved container name
-        * @param string $dir Resolved path relative to container
-        * @param array $params
-        * @return Traversable|array|null Returns null on failure
-        */
-       abstract public function getFileListInternal( $container, $dir, array $params );
-
-       /**
-        * Return a list of FileOp objects from a list of operations.
-        * Do not call this function from places outside FileBackend.
-        *
-        * The result must have the same number of items as the input.
-        * An exception is thrown if an unsupported operation is requested.
-        *
-        * @param array $ops Same format as doOperations()
-        * @return FileOp[] List of FileOp objects
-        * @throws FileBackendError
-        */
-       final public function getOperationsInternal( array $ops ) {
-               $supportedOps = [
-                       'store' => 'StoreFileOp',
-                       'copy' => 'CopyFileOp',
-                       'move' => 'MoveFileOp',
-                       'delete' => 'DeleteFileOp',
-                       'create' => 'CreateFileOp',
-                       'describe' => 'DescribeFileOp',
-                       'null' => 'NullFileOp'
-               ];
-
-               $performOps = []; // array of FileOp objects
-               // Build up ordered array of FileOps...
-               foreach ( $ops as $operation ) {
-                       $opName = $operation['op'];
-                       if ( isset( $supportedOps[$opName] ) ) {
-                               $class = $supportedOps[$opName];
-                               // Get params for this operation
-                               $params = $operation;
-                               // Append the FileOp class
-                               $performOps[] = new $class( $this, $params );
-                       } else {
-                               throw new FileBackendError( "Operation '$opName' is not supported." );
-                       }
-               }
-
-               return $performOps;
-       }
-
-       /**
-        * Get a list of storage paths to lock for a list of operations
-        * Returns an array with LockManager::LOCK_UW (shared locks) and
-        * LockManager::LOCK_EX (exclusive locks) keys, each corresponding
-        * to a list of storage paths to be locked. All returned paths are
-        * normalized.
-        *
-        * @param array $performOps List of FileOp objects
-        * @return array (LockManager::LOCK_UW => path list, LockManager::LOCK_EX => path list)
-        */
-       final public function getPathsToLockForOpsInternal( array $performOps ) {
-               // Build up a list of files to lock...
-               $paths = [ 'sh' => [], 'ex' => [] ];
-               foreach ( $performOps as $fileOp ) {
-                       $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
-                       $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
-               }
-               // Optimization: if doing an EX lock anyway, don't also set an SH one
-               $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
-               // Get a shared lock on the parent directory of each path changed
-               $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
-
-               return [
-                       LockManager::LOCK_UW => $paths['sh'],
-                       LockManager::LOCK_EX => $paths['ex']
-               ];
-       }
-
-       public function getScopedLocksForOps( array $ops, StatusValue $status ) {
-               $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
-
-               return $this->getScopedFileLocks( $paths, 'mixed', $status );
-       }
-
-       final protected function doOperationsInternal( array $ops, array $opts ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->newStatus();
-
-               // Fix up custom header name/value pairs...
-               $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
-
-               // Build up a list of FileOps...
-               $performOps = $this->getOperationsInternal( $ops );
-
-               // Acquire any locks as needed...
-               if ( empty( $opts['nonLocking'] ) ) {
-                       // Build up a list of files to lock...
-                       $paths = $this->getPathsToLockForOpsInternal( $performOps );
-                       // Try to lock those files for the scope of this function...
-
-                       $scopeLock = $this->getScopedFileLocks( $paths, 'mixed', $status );
-                       if ( !$status->isOK() ) {
-                               return $status; // abort
-                       }
-               }
-
-               // Clear any file cache entries (after locks acquired)
-               if ( empty( $opts['preserveCache'] ) ) {
-                       $this->clearCache();
-               }
-
-               // Build the list of paths involved
-               $paths = [];
-               foreach ( $performOps as $op ) {
-                       $paths = array_merge( $paths, $op->storagePathsRead() );
-                       $paths = array_merge( $paths, $op->storagePathsChanged() );
-               }
-
-               // Enlarge the cache to fit the stat entries of these files
-               $this->cheapCache->resize( max( 2 * count( $paths ), self::CACHE_CHEAP_SIZE ) );
-
-               // Load from the persistent container caches
-               $this->primeContainerCache( $paths );
-               // Get the latest stat info for all the files (having locked them)
-               $ok = $this->preloadFileStat( [ 'srcs' => $paths, 'latest' => true ] );
-
-               if ( $ok ) {
-                       // Actually attempt the operation batch...
-                       $opts = $this->setConcurrencyFlags( $opts );
-                       $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
-               } else {
-                       // If we could not even stat some files, then bail out...
-                       $subStatus = $this->newStatus( 'backend-fail-internal', $this->name );
-                       foreach ( $ops as $i => $op ) { // mark each op as failed
-                               $subStatus->success[$i] = false;
-                               ++$subStatus->failCount;
-                       }
-                       wfDebugLog( 'FileOperation', get_class( $this ) . "-{$this->name} " .
-                               " stat failure; aborted operations: " . FormatJson::encode( $ops ) );
-               }
-
-               // Merge errors into StatusValue fields
-               $status->merge( $subStatus );
-               $status->success = $subStatus->success; // not done in merge()
-
-               // Shrink the stat cache back to normal size
-               $this->cheapCache->resize( self::CACHE_CHEAP_SIZE );
-
-               return $status;
-       }
-
-       final protected function doQuickOperationsInternal( array $ops ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = $this->newStatus();
-
-               // Fix up custom header name/value pairs...
-               $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
-
-               // Clear any file cache entries
-               $this->clearCache();
-
-               $supportedOps = [ 'create', 'store', 'copy', 'move', 'delete', 'describe', 'null' ];
-               // Parallel ops may be disabled in config due to dependencies (e.g. needing popen())
-               $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
-               $maxConcurrency = $this->concurrency; // throttle
-               /** @var StatusValue[] $statuses */
-               $statuses = []; // array of (index => StatusValue)
-               $fileOpHandles = []; // list of (index => handle) arrays
-               $curFileOpHandles = []; // current handle batch
-               // Perform the sync-only ops and build up op handles for the async ops...
-               foreach ( $ops as $index => $params ) {
-                       if ( !in_array( $params['op'], $supportedOps ) ) {
-                               throw new FileBackendError( "Operation '{$params['op']}' is not supported." );
-                       }
-                       $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
-                       $subStatus = $this->$method( [ 'async' => $async ] + $params );
-                       if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
-                               if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
-                                       $fileOpHandles[] = $curFileOpHandles; // push this batch
-                                       $curFileOpHandles = [];
-                               }
-                               $curFileOpHandles[$index] = $subStatus->value; // keep index
-                       } else { // error or completed
-                               $statuses[$index] = $subStatus; // keep index
-                       }
-               }
-               if ( count( $curFileOpHandles ) ) {
-                       $fileOpHandles[] = $curFileOpHandles; // last batch
-               }
-               // Do all the async ops that can be done concurrently...
-               foreach ( $fileOpHandles as $fileHandleBatch ) {
-                       $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
-               }
-               // Marshall and merge all the responses...
-               foreach ( $statuses as $index => $subStatus ) {
-                       $status->merge( $subStatus );
-                       if ( $subStatus->isOK() ) {
-                               $status->success[$index] = true;
-                               ++$status->successCount;
-                       } else {
-                               $status->success[$index] = false;
-                               ++$status->failCount;
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * Execute a list of FileBackendStoreOpHandle handles in parallel.
-        * The resulting StatusValue object fields will correspond
-        * to the order in which the handles where given.
-        *
-        * @param FileBackendStoreOpHandle[] $fileOpHandles
-        *
-        * @throws FileBackendError
-        * @return StatusValue[] Map of StatusValue objects
-        */
-       final public function executeOpHandlesInternal( array $fileOpHandles ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
-               foreach ( $fileOpHandles as $fileOpHandle ) {
-                       if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
-                               throw new InvalidArgumentException( "Got a non-FileBackendStoreOpHandle object." );
-                       } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
-                               throw new InvalidArgumentException(
-                                       "Got a FileBackendStoreOpHandle for the wrong backend." );
-                       }
-               }
-               $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
-               foreach ( $fileOpHandles as $fileOpHandle ) {
-                       $fileOpHandle->closeResources();
-               }
-
-               return $res;
-       }
-
-       /**
-        * @see FileBackendStore::executeOpHandlesInternal()
-        *
-        * @param FileBackendStoreOpHandle[] $fileOpHandles
-        *
-        * @throws FileBackendError
-        * @return StatusValue[] List of corresponding StatusValue objects
-        */
-       protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
-               if ( count( $fileOpHandles ) ) {
-                       throw new LogicException( "Backend does not support asynchronous operations." );
-               }
-
-               return [];
-       }
-
-       /**
-        * Normalize and filter HTTP headers from a file operation
-        *
-        * This normalizes and strips long HTTP headers from a file operation.
-        * Most headers are just numbers, but some are allowed to be long.
-        * This function is useful for cleaning up headers and avoiding backend
-        * specific errors, especially in the middle of batch file operations.
-        *
-        * @param array $op Same format as doOperation()
-        * @return array
-        */
-       protected function sanitizeOpHeaders( array $op ) {
-               static $longs = [ 'content-disposition' ];
-
-               if ( isset( $op['headers'] ) ) { // op sets HTTP headers
-                       $newHeaders = [];
-                       foreach ( $op['headers'] as $name => $value ) {
-                               $name = strtolower( $name );
-                               $maxHVLen = in_array( $name, $longs ) ? INF : 255;
-                               if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
-                                       trigger_error( "Header '$name: $value' is too long." );
-                               } else {
-                                       $newHeaders[$name] = strlen( $value ) ? $value : ''; // null/false => ""
-                               }
-                       }
-                       $op['headers'] = $newHeaders;
-               }
-
-               return $op;
-       }
-
-       final public function preloadCache( array $paths ) {
-               $fullConts = []; // full container names
-               foreach ( $paths as $path ) {
-                       list( $fullCont, , ) = $this->resolveStoragePath( $path );
-                       $fullConts[] = $fullCont;
-               }
-               // Load from the persistent file and container caches
-               $this->primeContainerCache( $fullConts );
-               $this->primeFileCache( $paths );
-       }
-
-       final public function clearCache( array $paths = null ) {
-               if ( is_array( $paths ) ) {
-                       $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
-                       $paths = array_filter( $paths, 'strlen' ); // remove nulls
-               }
-               if ( $paths === null ) {
-                       $this->cheapCache->clear();
-                       $this->expensiveCache->clear();
-               } else {
-                       foreach ( $paths as $path ) {
-                               $this->cheapCache->clear( $path );
-                               $this->expensiveCache->clear( $path );
-                       }
-               }
-               $this->doClearCache( $paths );
-       }
-
-       /**
-        * Clears any additional stat caches for storage paths
-        *
-        * @see FileBackend::clearCache()
-        *
-        * @param array $paths Storage paths (optional)
-        */
-       protected function doClearCache( array $paths = null ) {
-       }
-
-       final public function preloadFileStat( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $success = true; // no network errors
-
-               $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
-               $stats = $this->doGetFileStatMulti( $params );
-               if ( $stats === null ) {
-                       return true; // not supported
-               }
-
-               $latest = !empty( $params['latest'] ); // use latest data?
-               foreach ( $stats as $path => $stat ) {
-                       $path = FileBackend::normalizeStoragePath( $path );
-                       if ( $path === null ) {
-                               continue; // this shouldn't happen
-                       }
-                       if ( is_array( $stat ) ) { // file exists
-                               // Strongly consistent backends can automatically set "latest"
-                               $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
-                               $this->cheapCache->set( $path, 'stat', $stat );
-                               $this->setFileCache( $path, $stat ); // update persistent cache
-                               if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
-                                       $this->cheapCache->set( $path, 'sha1',
-                                               [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
-                               }
-                               if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
-                                       $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
-                                       $this->cheapCache->set( $path, 'xattr',
-                                               [ 'map' => $stat['xattr'], 'latest' => $latest ] );
-                               }
-                       } elseif ( $stat === false ) { // file does not exist
-                               $this->cheapCache->set( $path, 'stat',
-                                       $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
-                               $this->cheapCache->set( $path, 'xattr',
-                                       [ 'map' => false, 'latest' => $latest ] );
-                               $this->cheapCache->set( $path, 'sha1',
-                                       [ 'hash' => false, 'latest' => $latest ] );
-                               wfDebug( __METHOD__ . ": File $path does not exist.\n" );
-                       } else { // an error occurred
-                               $success = false;
-                               wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
-                       }
-               }
-
-               return $success;
-       }
-
-       /**
-        * Get file stat information (concurrently if possible) for several files
-        *
-        * @see FileBackend::getFileStat()
-        *
-        * @param array $params Parameters include:
-        *   - srcs        : list of source storage paths
-        *   - latest      : use the latest available data
-        * @return array|null Map of storage paths to array|bool|null (returns null if not supported)
-        * @since 1.23
-        */
-       protected function doGetFileStatMulti( array $params ) {
-               return null; // not supported
-       }
-
-       /**
-        * Is this a key/value store where directories are just virtual?
-        * Virtual directories exists in so much as files exists that are
-        * prefixed with the directory path followed by a forward slash.
-        *
-        * @return bool
-        */
-       abstract protected function directoriesAreVirtual();
-
-       /**
-        * Check if a short container name is valid
-        *
-        * This checks for length and illegal characters.
-        * This may disallow certain characters that can appear
-        * in the prefix used to make the full container name.
-        *
-        * @param string $container
-        * @return bool
-        */
-       final protected static function isValidShortContainerName( $container ) {
-               // Suffixes like '.xxx' (hex shard chars) or '.seg' (file segments)
-               // might be used by subclasses. Reserve the dot character for sanity.
-               // The only way dots end up in containers (e.g. resolveStoragePath)
-               // is due to the wikiId container prefix or the above suffixes.
-               return self::isValidContainerName( $container ) && !preg_match( '/[.]/', $container );
-       }
-
-       /**
-        * Check if a full container name is valid
-        *
-        * This checks for length and illegal characters.
-        * Limiting the characters makes migrations to other stores easier.
-        *
-        * @param string $container
-        * @return bool
-        */
-       final protected static function isValidContainerName( $container ) {
-               // This accounts for NTFS, Swift, and Ceph restrictions
-               // and disallows directory separators or traversal characters.
-               // Note that matching strings URL encode to the same string;
-               // in Swift/Ceph, the length restriction is *after* URL encoding.
-               return (bool)preg_match( '/^[a-z0-9][a-z0-9-_.]{0,199}$/i', $container );
-       }
-
-       /**
-        * Splits a storage path into an internal container name,
-        * an internal relative file name, and a container shard suffix.
-        * Any shard suffix is already appended to the internal container name.
-        * This also checks that the storage path is valid and within this backend.
-        *
-        * If the container is sharded but a suffix could not be determined,
-        * this means that the path can only refer to a directory and can only
-        * be scanned by looking in all the container shards.
-        *
-        * @param string $storagePath
-        * @return array (container, path, container suffix) or (null, null, null) if invalid
-        */
-       final protected function resolveStoragePath( $storagePath ) {
-               list( $backend, $shortCont, $relPath ) = self::splitStoragePath( $storagePath );
-               if ( $backend === $this->name ) { // must be for this backend
-                       $relPath = self::normalizeContainerPath( $relPath );
-                       if ( $relPath !== null && self::isValidShortContainerName( $shortCont ) ) {
-                               // Get shard for the normalized path if this container is sharded
-                               $cShard = $this->getContainerShard( $shortCont, $relPath );
-                               // Validate and sanitize the relative path (backend-specific)
-                               $relPath = $this->resolveContainerPath( $shortCont, $relPath );
-                               if ( $relPath !== null ) {
-                                       // Prepend any wiki ID prefix to the container name
-                                       $container = $this->fullContainerName( $shortCont );
-                                       if ( self::isValidContainerName( $container ) ) {
-                                               // Validate and sanitize the container name (backend-specific)
-                                               $container = $this->resolveContainerName( "{$container}{$cShard}" );
-                                               if ( $container !== null ) {
-                                                       return [ $container, $relPath, $cShard ];
-                                               }
-                                       }
-                               }
-                       }
-               }
-
-               return [ null, null, null ];
-       }
-
-       /**
-        * Like resolveStoragePath() except null values are returned if
-        * the container is sharded and the shard could not be determined
-        * or if the path ends with '/'. The latter case is illegal for FS
-        * backends and can confuse listings for object store backends.
-        *
-        * This function is used when resolving paths that must be valid
-        * locations for files. Directory and listing functions should
-        * generally just use resolveStoragePath() instead.
-        *
-        * @see FileBackendStore::resolveStoragePath()
-        *
-        * @param string $storagePath
-        * @return array (container, path) or (null, null) if invalid
-        */
-       final protected function resolveStoragePathReal( $storagePath ) {
-               list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
-               if ( $cShard !== null && substr( $relPath, -1 ) !== '/' ) {
-                       return [ $container, $relPath ];
-               }
-
-               return [ null, null ];
-       }
-
-       /**
-        * Get the container name shard suffix for a given path.
-        * Any empty suffix means the container is not sharded.
-        *
-        * @param string $container Container name
-        * @param string $relPath Storage path relative to the container
-        * @return string|null Returns null if shard could not be determined
-        */
-       final protected function getContainerShard( $container, $relPath ) {
-               list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
-               if ( $levels == 1 || $levels == 2 ) {
-                       // Hash characters are either base 16 or 36
-                       $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
-                       // Get a regex that represents the shard portion of paths.
-                       // The concatenation of the captures gives us the shard.
-                       if ( $levels === 1 ) { // 16 or 36 shards per container
-                               $hashDirRegex = '(' . $char . ')';
-                       } else { // 256 or 1296 shards per container
-                               if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
-                                       $hashDirRegex = $char . '/(' . $char . '{2})';
-                               } else { // short hash dir format (e.g. "a/b/c")
-                                       $hashDirRegex = '(' . $char . ')/(' . $char . ')';
-                               }
-                       }
-                       // Allow certain directories to be above the hash dirs so as
-                       // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
-                       // They must be 2+ chars to avoid any hash directory ambiguity.
-                       $m = [];
-                       if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
-                               return '.' . implode( '', array_slice( $m, 1 ) );
-                       }
-
-                       return null; // failed to match
-               }
-
-               return ''; // no sharding
-       }
-
-       /**
-        * Check if a storage path maps to a single shard.
-        * Container dirs like "a", where the container shards on "x/xy",
-        * can reside on several shards. Such paths are tricky to handle.
-        *
-        * @param string $storagePath Storage path
-        * @return bool
-        */
-       final public function isSingleShardPathInternal( $storagePath ) {
-               list( , , $shard ) = $this->resolveStoragePath( $storagePath );
-
-               return ( $shard !== null );
-       }
-
-       /**
-        * Get the sharding config for a container.
-        * If greater than 0, then all file storage paths within
-        * the container are required to be hashed accordingly.
-        *
-        * @param string $container
-        * @return array (integer levels, integer base, repeat flag) or (0, 0, false)
-        */
-       final protected function getContainerHashLevels( $container ) {
-               if ( isset( $this->shardViaHashLevels[$container] ) ) {
-                       $config = $this->shardViaHashLevels[$container];
-                       $hashLevels = (int)$config['levels'];
-                       if ( $hashLevels == 1 || $hashLevels == 2 ) {
-                               $hashBase = (int)$config['base'];
-                               if ( $hashBase == 16 || $hashBase == 36 ) {
-                                       return [ $hashLevels, $hashBase, $config['repeat'] ];
-                               }
-                       }
-               }
-
-               return [ 0, 0, false ]; // no sharding
-       }
-
-       /**
-        * Get a list of full container shard suffixes for a container
-        *
-        * @param string $container
-        * @return array
-        */
-       final protected function getContainerSuffixes( $container ) {
-               $shards = [];
-               list( $digits, $base ) = $this->getContainerHashLevels( $container );
-               if ( $digits > 0 ) {
-                       $numShards = pow( $base, $digits );
-                       for ( $index = 0; $index < $numShards; $index++ ) {
-                               $shards[] = '.' . Wikimedia\base_convert( $index, 10, $base, $digits );
-                       }
-               }
-
-               return $shards;
-       }
-
-       /**
-        * Get the full container name, including the wiki ID prefix
-        *
-        * @param string $container
-        * @return string
-        */
-       final protected function fullContainerName( $container ) {
-               if ( $this->wikiId != '' ) {
-                       return "{$this->wikiId}-$container";
-               } else {
-                       return $container;
-               }
-       }
-
-       /**
-        * Resolve a container name, checking if it's allowed by the backend.
-        * This is intended for internal use, such as encoding illegal chars.
-        * Subclasses can override this to be more restrictive.
-        *
-        * @param string $container
-        * @return string|null
-        */
-       protected function resolveContainerName( $container ) {
-               return $container;
-       }
-
-       /**
-        * Resolve a relative storage path, checking if it's allowed by the backend.
-        * This is intended for internal use, such as encoding illegal chars or perhaps
-        * getting absolute paths (e.g. FS based backends). Note that the relative path
-        * may be the empty string (e.g. the path is simply to the container).
-        *
-        * @param string $container Container name
-        * @param string $relStoragePath Storage path relative to the container
-        * @return string|null Path or null if not valid
-        */
-       protected function resolveContainerPath( $container, $relStoragePath ) {
-               return $relStoragePath;
-       }
-
-       /**
-        * Get the cache key for a container
-        *
-        * @param string $container Resolved container name
-        * @return string
-        */
-       private function containerCacheKey( $container ) {
-               return "filebackend:{$this->name}:{$this->wikiId}:container:{$container}";
-       }
-
-       /**
-        * Set the cached info for a container
-        *
-        * @param string $container Resolved container name
-        * @param array $val Information to cache
-        */
-       final protected function setContainerCache( $container, array $val ) {
-               $this->memCache->set( $this->containerCacheKey( $container ), $val, 14 * 86400 );
-       }
-
-       /**
-        * Delete the cached info for a container.
-        * The cache key is salted for a while to prevent race conditions.
-        *
-        * @param string $container Resolved container name
-        */
-       final protected function deleteContainerCache( $container ) {
-               if ( !$this->memCache->delete( $this->containerCacheKey( $container ), 300 ) ) {
-                       trigger_error( "Unable to delete stat cache for container $container." );
-               }
-       }
-
-       /**
-        * Do a batch lookup from cache for container stats for all containers
-        * used in a list of container names or storage paths objects.
-        * This loads the persistent cache values into the process cache.
-        *
-        * @param array $items
-        */
-       final protected function primeContainerCache( array $items ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
-               $paths = []; // list of storage paths
-               $contNames = []; // (cache key => resolved container name)
-               // Get all the paths/containers from the items...
-               foreach ( $items as $item ) {
-                       if ( self::isStoragePath( $item ) ) {
-                               $paths[] = $item;
-                       } elseif ( is_string( $item ) ) { // full container name
-                               $contNames[$this->containerCacheKey( $item )] = $item;
-                       }
-               }
-               // Get all the corresponding cache keys for paths...
-               foreach ( $paths as $path ) {
-                       list( $fullCont, , ) = $this->resolveStoragePath( $path );
-                       if ( $fullCont !== null ) { // valid path for this backend
-                               $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
-                       }
-               }
-
-               $contInfo = []; // (resolved container name => cache value)
-               // Get all cache entries for these container cache keys...
-               $values = $this->memCache->getMulti( array_keys( $contNames ) );
-               foreach ( $values as $cacheKey => $val ) {
-                       $contInfo[$contNames[$cacheKey]] = $val;
-               }
-
-               // Populate the container process cache for the backend...
-               $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
-       }
-
-       /**
-        * Fill the backend-specific process cache given an array of
-        * resolved container names and their corresponding cached info.
-        * Only containers that actually exist should appear in the map.
-        *
-        * @param array $containerInfo Map of resolved container names to cached info
-        */
-       protected function doPrimeContainerCache( array $containerInfo ) {
-       }
-
-       /**
-        * Get the cache key for a file path
-        *
-        * @param string $path Normalized storage path
-        * @return string
-        */
-       private function fileCacheKey( $path ) {
-               return "filebackend:{$this->name}:{$this->wikiId}:file:" . sha1( $path );
-       }
-
-       /**
-        * Set the cached stat info for a file path.
-        * Negatives (404s) are not cached. By not caching negatives, we can skip cache
-        * salting for the case when a file is created at a path were there was none before.
-        *
-        * @param string $path Storage path
-        * @param array $val Stat information to cache
-        */
-       final protected function setFileCache( $path, array $val ) {
-               $path = FileBackend::normalizeStoragePath( $path );
-               if ( $path === null ) {
-                       return; // invalid storage path
-               }
-               $mtime = wfTimestamp( TS_UNIX, $val['mtime'] );
-               $ttl = $this->memCache->adaptiveTTL( $mtime, 7 * 86400, 300, .1 );
-               $key = $this->fileCacheKey( $path );
-               // Set the cache unless it is currently salted.
-               $this->memCache->set( $key, $val, $ttl );
-       }
-
-       /**
-        * Delete the cached stat info for a file path.
-        * The cache key is salted for a while to prevent race conditions.
-        * Since negatives (404s) are not cached, this does not need to be called when
-        * a file is created at a path were there was none before.
-        *
-        * @param string $path Storage path
-        */
-       final protected function deleteFileCache( $path ) {
-               $path = FileBackend::normalizeStoragePath( $path );
-               if ( $path === null ) {
-                       return; // invalid storage path
-               }
-               if ( !$this->memCache->delete( $this->fileCacheKey( $path ), 300 ) ) {
-                       trigger_error( "Unable to delete stat cache for file $path." );
-               }
-       }
-
-       /**
-        * Do a batch lookup from cache for file stats for all paths
-        * used in a list of storage paths or FileOp objects.
-        * This loads the persistent cache values into the process cache.
-        *
-        * @param array $items List of storage paths
-        */
-       final protected function primeFileCache( array $items ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
-               $paths = []; // list of storage paths
-               $pathNames = []; // (cache key => storage path)
-               // Get all the paths/containers from the items...
-               foreach ( $items as $item ) {
-                       if ( self::isStoragePath( $item ) ) {
-                               $paths[] = FileBackend::normalizeStoragePath( $item );
-                       }
-               }
-               // Get rid of any paths that failed normalization...
-               $paths = array_filter( $paths, 'strlen' ); // remove nulls
-               // Get all the corresponding cache keys for paths...
-               foreach ( $paths as $path ) {
-                       list( , $rel, ) = $this->resolveStoragePath( $path );
-                       if ( $rel !== null ) { // valid path for this backend
-                               $pathNames[$this->fileCacheKey( $path )] = $path;
-                       }
-               }
-               // Get all cache entries for these file cache keys...
-               $values = $this->memCache->getMulti( array_keys( $pathNames ) );
-               foreach ( $values as $cacheKey => $val ) {
-                       $path = $pathNames[$cacheKey];
-                       if ( is_array( $val ) ) {
-                               $val['latest'] = false; // never completely trust cache
-                               $this->cheapCache->set( $path, 'stat', $val );
-                               if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
-                                       $this->cheapCache->set( $path, 'sha1',
-                                               [ 'hash' => $val['sha1'], 'latest' => false ] );
-                               }
-                               if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata
-                                       $val['xattr'] = self::normalizeXAttributes( $val['xattr'] );
-                                       $this->cheapCache->set( $path, 'xattr',
-                                               [ 'map' => $val['xattr'], 'latest' => false ] );
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Normalize file headers/metadata to the FileBackend::getFileXAttributes() format
-        *
-        * @param array $xattr
-        * @return array
-        * @since 1.22
-        */
-       final protected static function normalizeXAttributes( array $xattr ) {
-               $newXAttr = [ 'headers' => [], 'metadata' => [] ];
-
-               foreach ( $xattr['headers'] as $name => $value ) {
-                       $newXAttr['headers'][strtolower( $name )] = $value;
-               }
-
-               foreach ( $xattr['metadata'] as $name => $value ) {
-                       $newXAttr['metadata'][strtolower( $name )] = $value;
-               }
-
-               return $newXAttr;
-       }
-
-       /**
-        * Set the 'concurrency' option from a list of operation options
-        *
-        * @param array $opts Map of operation options
-        * @return array
-        */
-       final protected function setConcurrencyFlags( array $opts ) {
-               $opts['concurrency'] = 1; // off
-               if ( $this->parallelize === 'implicit' ) {
-                       if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
-                               $opts['concurrency'] = $this->concurrency;
-                       }
-               } elseif ( $this->parallelize === 'explicit' ) {
-                       if ( !empty( $opts['parallelize'] ) ) {
-                               $opts['concurrency'] = $this->concurrency;
-                       }
-               }
-
-               return $opts;
-       }
-
-       /**
-        * Get the content type to use in HEAD/GET requests for a file
-        *
-        * @param string $storagePath
-        * @param string|null $content File data
-        * @param string|null $fsPath File system path
-        * @return string MIME type
-        */
-       protected function getContentType( $storagePath, $content, $fsPath ) {
-               if ( $this->mimeCallback ) {
-                       return call_user_func_array( $this->mimeCallback, func_get_args() );
-               }
-
-               $mime = null;
-               if ( $fsPath !== null && function_exists( 'finfo_file' ) ) {
-                       $finfo = finfo_open( FILEINFO_MIME_TYPE );
-                       $mime = finfo_file( $finfo, $fsPath );
-                       finfo_close( $finfo );
-               }
-
-               return is_string( $mime ) ? $mime : 'unknown/unknown';
-       }
-}
-
-/**
- * FileBackendStore helper class for performing asynchronous file operations.
- *
- * For example, calling FileBackendStore::createInternal() with the "async"
- * param flag may result in a StatusValue that contains this object as a value.
- * This class is largely backend-specific and is mostly just "magic" to be
- * passed to FileBackendStore::executeOpHandlesInternal().
- */
-abstract class FileBackendStoreOpHandle {
-       /** @var array */
-       public $params = []; // params to caller functions
-       /** @var FileBackendStore */
-       public $backend;
-       /** @var array */
-       public $resourcesToClose = [];
-
-       public $call; // string; name that identifies the function called
-
-       /**
-        * Close all open file handles
-        */
-       public function closeResources() {
-               array_map( 'fclose', $this->resourcesToClose );
-       }
-}
-
-/**
- * FileBackendStore helper function to handle listings that span container shards.
- * Do not use this class from places outside of FileBackendStore.
- *
- * @ingroup FileBackend
- */
-abstract class FileBackendStoreShardListIterator extends FilterIterator {
-       /** @var FileBackendStore */
-       protected $backend;
-
-       /** @var array */
-       protected $params;
-
-       /** @var string Full container name */
-       protected $container;
-
-       /** @var string Resolved relative path */
-       protected $directory;
-
-       /** @var array */
-       protected $multiShardPaths = []; // (rel path => 1)
-
-       /**
-        * @param FileBackendStore $backend
-        * @param string $container Full storage container name
-        * @param string $dir Storage directory relative to container
-        * @param array $suffixes List of container shard suffixes
-        * @param array $params
-        */
-       public function __construct(
-               FileBackendStore $backend, $container, $dir, array $suffixes, array $params
-       ) {
-               $this->backend = $backend;
-               $this->container = $container;
-               $this->directory = $dir;
-               $this->params = $params;
-
-               $iter = new AppendIterator();
-               foreach ( $suffixes as $suffix ) {
-                       $iter->append( $this->listFromShard( $this->container . $suffix ) );
-               }
-
-               parent::__construct( $iter );
-       }
-
-       public function accept() {
-               $rel = $this->getInnerIterator()->current(); // path relative to given directory
-               $path = $this->params['dir'] . "/{$rel}"; // full storage path
-               if ( $this->backend->isSingleShardPathInternal( $path ) ) {
-                       return true; // path is only on one shard; no issue with duplicates
-               } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
-                       // Don't keep listing paths that are on multiple shards
-                       return false;
-               } else {
-                       $this->multiShardPaths[$rel] = 1;
-
-                       return true;
-               }
-       }
-
-       public function rewind() {
-               parent::rewind();
-               $this->multiShardPaths = [];
-       }
-
-       /**
-        * Get the list for a given container shard
-        *
-        * @param string $container Resolved container name
-        * @return Iterator
-        */
-       abstract protected function listFromShard( $container );
-}
-
-/**
- * Iterator for listing directories
- */
-class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
-       protected function listFromShard( $container ) {
-               $list = $this->backend->getDirectoryListInternal(
-                       $container, $this->directory, $this->params );
-               if ( $list === null ) {
-                       return new ArrayIterator( [] );
-               } else {
-                       return is_array( $list ) ? new ArrayIterator( $list ) : $list;
-               }
-       }
-}
-
-/**
- * Iterator for listing regular files
- */
-class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
-       protected function listFromShard( $container ) {
-               $list = $this->backend->getFileListInternal(
-                       $container, $this->directory, $this->params );
-               if ( $list === null ) {
-                       return new ArrayIterator( [] );
-               } else {
-                       return is_array( $list ) ? new ArrayIterator( $list ) : $list;
-               }
-       }
-}
diff --git a/includes/filebackend/FileOp.php b/includes/filebackend/FileOp.php
deleted file mode 100644 (file)
index 480ebdf..0000000
+++ /dev/null
@@ -1,848 +0,0 @@
-<?php
-/**
- * Helper class for representing operations with transaction support.
- *
- * 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 FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * FileBackend helper class for representing operations.
- * Do not use this class from places outside FileBackend.
- *
- * Methods called from FileOpBatch::attempt() should avoid throwing
- * exceptions at all costs. FileOp objects should be lightweight in order
- * to support large arrays in memory and serialization.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-abstract class FileOp {
-       /** @var array */
-       protected $params = [];
-
-       /** @var FileBackendStore */
-       protected $backend;
-
-       /** @var int */
-       protected $state = self::STATE_NEW;
-
-       /** @var bool */
-       protected $failed = false;
-
-       /** @var bool */
-       protected $async = false;
-
-       /** @var string */
-       protected $batchId;
-
-       /** @var bool Operation is not a no-op */
-       protected $doOperation = true;
-
-       /** @var string */
-       protected $sourceSha1;
-
-       /** @var bool */
-       protected $overwriteSameCase;
-
-       /** @var bool */
-       protected $destExists;
-
-       /* Object life-cycle */
-       const STATE_NEW = 1;
-       const STATE_CHECKED = 2;
-       const STATE_ATTEMPTED = 3;
-
-       /**
-        * Build a new batch file operation transaction
-        *
-        * @param FileBackendStore $backend
-        * @param array $params
-        * @throws FileBackendError
-        */
-       final public function __construct( FileBackendStore $backend, array $params ) {
-               $this->backend = $backend;
-               list( $required, $optional, $paths ) = $this->allowedParams();
-               foreach ( $required as $name ) {
-                       if ( isset( $params[$name] ) ) {
-                               $this->params[$name] = $params[$name];
-                       } else {
-                               throw new InvalidArgumentException( "File operation missing parameter '$name'." );
-                       }
-               }
-               foreach ( $optional as $name ) {
-                       if ( isset( $params[$name] ) ) {
-                               $this->params[$name] = $params[$name];
-                       }
-               }
-               foreach ( $paths as $name ) {
-                       if ( isset( $this->params[$name] ) ) {
-                               // Normalize paths so the paths to the same file have the same string
-                               $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
-                       }
-               }
-       }
-
-       /**
-        * Normalize a string if it is a valid storage path
-        *
-        * @param string $path
-        * @return string
-        */
-       protected static function normalizeIfValidStoragePath( $path ) {
-               if ( FileBackend::isStoragePath( $path ) ) {
-                       $res = FileBackend::normalizeStoragePath( $path );
-
-                       return ( $res !== null ) ? $res : $path;
-               }
-
-               return $path;
-       }
-
-       /**
-        * Set the batch UUID this operation belongs to
-        *
-        * @param string $batchId
-        */
-       final public function setBatchId( $batchId ) {
-               $this->batchId = $batchId;
-       }
-
-       /**
-        * Get the value of the parameter with the given name
-        *
-        * @param string $name
-        * @return mixed Returns null if the parameter is not set
-        */
-       final public function getParam( $name ) {
-               return isset( $this->params[$name] ) ? $this->params[$name] : null;
-       }
-
-       /**
-        * Check if this operation failed precheck() or attempt()
-        *
-        * @return bool
-        */
-       final public function failed() {
-               return $this->failed;
-       }
-
-       /**
-        * Get a new empty predicates array for precheck()
-        *
-        * @return array
-        */
-       final public static function newPredicates() {
-               return [ 'exists' => [], 'sha1' => [] ];
-       }
-
-       /**
-        * Get a new empty dependency tracking array for paths read/written to
-        *
-        * @return array
-        */
-       final public static function newDependencies() {
-               return [ 'read' => [], 'write' => [] ];
-       }
-
-       /**
-        * Update a dependency tracking array to account for this operation
-        *
-        * @param array $deps Prior path reads/writes; format of FileOp::newPredicates()
-        * @return array
-        */
-       final public function applyDependencies( array $deps ) {
-               $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
-               $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
-
-               return $deps;
-       }
-
-       /**
-        * Check if this operation changes files listed in $paths
-        *
-        * @param array $deps Prior path reads/writes; format of FileOp::newPredicates()
-        * @return bool
-        */
-       final public function dependsOn( array $deps ) {
-               foreach ( $this->storagePathsChanged() as $path ) {
-                       if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
-                               return true; // "output" or "anti" dependency
-                       }
-               }
-               foreach ( $this->storagePathsRead() as $path ) {
-                       if ( isset( $deps['write'][$path] ) ) {
-                               return true; // "flow" dependency
-                       }
-               }
-
-               return false;
-       }
-
-       /**
-        * Get the file journal entries for this file operation
-        *
-        * @param array $oPredicates Pre-op info about files (format of FileOp::newPredicates)
-        * @param array $nPredicates Post-op info about files (format of FileOp::newPredicates)
-        * @return array
-        */
-       final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
-               if ( !$this->doOperation ) {
-                       return []; // this is a no-op
-               }
-               $nullEntries = [];
-               $updateEntries = [];
-               $deleteEntries = [];
-               $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
-               foreach ( array_unique( $pathsUsed ) as $path ) {
-                       $nullEntries[] = [ // assertion for recovery
-                               'op' => 'null',
-                               'path' => $path,
-                               'newSha1' => $this->fileSha1( $path, $oPredicates )
-                       ];
-               }
-               foreach ( $this->storagePathsChanged() as $path ) {
-                       if ( $nPredicates['sha1'][$path] === false ) { // deleted
-                               $deleteEntries[] = [
-                                       'op' => 'delete',
-                                       'path' => $path,
-                                       'newSha1' => ''
-                               ];
-                       } else { // created/updated
-                               $updateEntries[] = [
-                                       'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
-                                       'path' => $path,
-                                       'newSha1' => $nPredicates['sha1'][$path]
-                               ];
-                       }
-               }
-
-               return array_merge( $nullEntries, $updateEntries, $deleteEntries );
-       }
-
-       /**
-        * Check preconditions of the operation without writing anything.
-        * This must update $predicates for each path that the op can change
-        * except when a failing StatusValue object is returned.
-        *
-        * @param array $predicates
-        * @return StatusValue
-        */
-       final public function precheck( array &$predicates ) {
-               if ( $this->state !== self::STATE_NEW ) {
-                       return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
-               }
-               $this->state = self::STATE_CHECKED;
-               $status = $this->doPrecheck( $predicates );
-               if ( !$status->isOK() ) {
-                       $this->failed = true;
-               }
-
-               return $status;
-       }
-
-       /**
-        * @param array $predicates
-        * @return StatusValue
-        */
-       protected function doPrecheck( array &$predicates ) {
-               return StatusValue::newGood();
-       }
-
-       /**
-        * Attempt the operation
-        *
-        * @return StatusValue
-        */
-       final public function attempt() {
-               if ( $this->state !== self::STATE_CHECKED ) {
-                       return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
-               } elseif ( $this->failed ) { // failed precheck
-                       return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
-               }
-               $this->state = self::STATE_ATTEMPTED;
-               if ( $this->doOperation ) {
-                       $status = $this->doAttempt();
-                       if ( !$status->isOK() ) {
-                               $this->failed = true;
-                               $this->logFailure( 'attempt' );
-                       }
-               } else { // no-op
-                       $status = StatusValue::newGood();
-               }
-
-               return $status;
-       }
-
-       /**
-        * @return StatusValue
-        */
-       protected function doAttempt() {
-               return StatusValue::newGood();
-       }
-
-       /**
-        * Attempt the operation in the background
-        *
-        * @return StatusValue
-        */
-       final public function attemptAsync() {
-               $this->async = true;
-               $result = $this->attempt();
-               $this->async = false;
-
-               return $result;
-       }
-
-       /**
-        * Get the file operation parameters
-        *
-        * @return array (required params list, optional params list, list of params that are paths)
-        */
-       protected function allowedParams() {
-               return [ [], [], [] ];
-       }
-
-       /**
-        * Adjust params to FileBackendStore internal file calls
-        *
-        * @param array $params
-        * @return array (required params list, optional params list)
-        */
-       protected function setFlags( array $params ) {
-               return [ 'async' => $this->async ] + $params;
-       }
-
-       /**
-        * Get a list of storage paths read from for this operation
-        *
-        * @return array
-        */
-       public function storagePathsRead() {
-               return [];
-       }
-
-       /**
-        * Get a list of storage paths written to for this operation
-        *
-        * @return array
-        */
-       public function storagePathsChanged() {
-               return [];
-       }
-
-       /**
-        * Check for errors with regards to the destination file already existing.
-        * Also set the destExists, overwriteSameCase and sourceSha1 member variables.
-        * A bad StatusValue will be returned if there is no chance it can be overwritten.
-        *
-        * @param array $predicates
-        * @return StatusValue
-        */
-       protected function precheckDestExistence( array $predicates ) {
-               $status = StatusValue::newGood();
-               // Get hash of source file/string and the destination file
-               $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
-               if ( $this->sourceSha1 === null ) { // file in storage?
-                       $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
-               }
-               $this->overwriteSameCase = false;
-               $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
-               if ( $this->destExists ) {
-                       if ( $this->getParam( 'overwrite' ) ) {
-                               return $status; // OK
-                       } elseif ( $this->getParam( 'overwriteSame' ) ) {
-                               $dhash = $this->fileSha1( $this->params['dst'], $predicates );
-                               // Check if hashes are valid and match each other...
-                               if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
-                                       $status->fatal( 'backend-fail-hashes' );
-                               } elseif ( $this->sourceSha1 !== $dhash ) {
-                                       // Give an error if the files are not identical
-                                       $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
-                               } else {
-                                       $this->overwriteSameCase = true; // OK
-                               }
-
-                               return $status; // do nothing; either OK or bad status
-                       } else {
-                               $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
-
-                               return $status;
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * precheckDestExistence() helper function to get the source file SHA-1.
-        * Subclasses should overwride this if the source is not in storage.
-        *
-        * @return string|bool Returns false on failure
-        */
-       protected function getSourceSha1Base36() {
-               return null; // N/A
-       }
-
-       /**
-        * Check if a file will exist in storage when this operation is attempted
-        *
-        * @param string $source Storage path
-        * @param array $predicates
-        * @return bool
-        */
-       final protected function fileExists( $source, array $predicates ) {
-               if ( isset( $predicates['exists'][$source] ) ) {
-                       return $predicates['exists'][$source]; // previous op assures this
-               } else {
-                       $params = [ 'src' => $source, 'latest' => true ];
-
-                       return $this->backend->fileExists( $params );
-               }
-       }
-
-       /**
-        * Get the SHA-1 of a file in storage when this operation is attempted
-        *
-        * @param string $source Storage path
-        * @param array $predicates
-        * @return string|bool False on failure
-        */
-       final protected function fileSha1( $source, array $predicates ) {
-               if ( isset( $predicates['sha1'][$source] ) ) {
-                       return $predicates['sha1'][$source]; // previous op assures this
-               } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
-                       return false; // previous op assures this
-               } else {
-                       $params = [ 'src' => $source, 'latest' => true ];
-
-                       return $this->backend->getFileSha1Base36( $params );
-               }
-       }
-
-       /**
-        * Get the backend this operation is for
-        *
-        * @return FileBackendStore
-        */
-       public function getBackend() {
-               return $this->backend;
-       }
-
-       /**
-        * Log a file operation failure and preserve any temp files
-        *
-        * @param string $action
-        */
-       final public function logFailure( $action ) {
-               $params = $this->params;
-               $params['failedAction'] = $action;
-               try {
-                       wfDebugLog( 'FileOperation', get_class( $this ) .
-                               " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
-               } catch ( Exception $e ) {
-                       // bad config? debug log error?
-               }
-       }
-}
-
-/**
- * Create a file in the backend with the given content.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class CreateFileOp extends FileOp {
-       protected function allowedParams() {
-               return [
-                       [ 'content', 'dst' ],
-                       [ 'overwrite', 'overwriteSame', 'headers' ],
-                       [ 'dst' ]
-               ];
-       }
-
-       protected function doPrecheck( array &$predicates ) {
-               $status = StatusValue::newGood();
-               // Check if the source data is too big
-               if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
-                       $status->fatal( 'backend-fail-maxsize',
-                               $this->params['dst'], $this->backend->maxFileSizeInternal() );
-                       $status->fatal( 'backend-fail-create', $this->params['dst'] );
-
-                       return $status;
-               // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-create', $this->params['dst'] );
-
-                       return $status;
-               }
-               // Check if destination file exists
-               $status->merge( $this->precheckDestExistence( $predicates ) );
-               $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
-               if ( $status->isOK() ) {
-                       // Update file existence predicates
-                       $predicates['exists'][$this->params['dst']] = true;
-                       $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
-               }
-
-               return $status; // safe to call attempt()
-       }
-
-       protected function doAttempt() {
-               if ( !$this->overwriteSameCase ) {
-                       // Create the file at the destination
-                       return $this->backend->createInternal( $this->setFlags( $this->params ) );
-               }
-
-               return StatusValue::newGood();
-       }
-
-       protected function getSourceSha1Base36() {
-               return Wikimedia\base_convert( sha1( $this->params['content'] ), 16, 36, 31 );
-       }
-
-       public function storagePathsChanged() {
-               return [ $this->params['dst'] ];
-       }
-}
-
-/**
- * Store a file into the backend from a file on the file system.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class StoreFileOp extends FileOp {
-       protected function allowedParams() {
-               return [
-                       [ 'src', 'dst' ],
-                       [ 'overwrite', 'overwriteSame', 'headers' ],
-                       [ 'src', 'dst' ]
-               ];
-       }
-
-       protected function doPrecheck( array &$predicates ) {
-               $status = StatusValue::newGood();
-               // Check if the source file exists on the file system
-               if ( !is_file( $this->params['src'] ) ) {
-                       $status->fatal( 'backend-fail-notexists', $this->params['src'] );
-
-                       return $status;
-               // Check if the source file is too big
-               } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
-                       $status->fatal( 'backend-fail-maxsize',
-                               $this->params['dst'], $this->backend->maxFileSizeInternal() );
-                       $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
-
-                       return $status;
-               // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
-
-                       return $status;
-               }
-               // Check if destination file exists
-               $status->merge( $this->precheckDestExistence( $predicates ) );
-               $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
-               if ( $status->isOK() ) {
-                       // Update file existence predicates
-                       $predicates['exists'][$this->params['dst']] = true;
-                       $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
-               }
-
-               return $status; // safe to call attempt()
-       }
-
-       protected function doAttempt() {
-               if ( !$this->overwriteSameCase ) {
-                       // Store the file at the destination
-                       return $this->backend->storeInternal( $this->setFlags( $this->params ) );
-               }
-
-               return StatusValue::newGood();
-       }
-
-       protected function getSourceSha1Base36() {
-               MediaWiki\suppressWarnings();
-               $hash = sha1_file( $this->params['src'] );
-               MediaWiki\restoreWarnings();
-               if ( $hash !== false ) {
-                       $hash = Wikimedia\base_convert( $hash, 16, 36, 31 );
-               }
-
-               return $hash;
-       }
-
-       public function storagePathsChanged() {
-               return [ $this->params['dst'] ];
-       }
-}
-
-/**
- * Copy a file from one storage path to another in the backend.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class CopyFileOp extends FileOp {
-       protected function allowedParams() {
-               return [
-                       [ 'src', 'dst' ],
-                       [ 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ],
-                       [ 'src', 'dst' ]
-               ];
-       }
-
-       protected function doPrecheck( array &$predicates ) {
-               $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
-                       if ( $this->getParam( 'ignoreMissingSource' ) ) {
-                               $this->doOperation = false; // no-op
-                               // Update file existence predicates (cache 404s)
-                               $predicates['exists'][$this->params['src']] = false;
-                               $predicates['sha1'][$this->params['src']] = false;
-
-                               return $status; // nothing to do
-                       } else {
-                               $status->fatal( 'backend-fail-notexists', $this->params['src'] );
-
-                               return $status;
-                       }
-                       // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
-
-                       return $status;
-               }
-               // Check if destination file exists
-               $status->merge( $this->precheckDestExistence( $predicates ) );
-               $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
-               if ( $status->isOK() ) {
-                       // Update file existence predicates
-                       $predicates['exists'][$this->params['dst']] = true;
-                       $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
-               }
-
-               return $status; // safe to call attempt()
-       }
-
-       protected function doAttempt() {
-               if ( $this->overwriteSameCase ) {
-                       $status = StatusValue::newGood(); // nothing to do
-               } elseif ( $this->params['src'] === $this->params['dst'] ) {
-                       // Just update the destination file headers
-                       $headers = $this->getParam( 'headers' ) ?: [];
-                       $status = $this->backend->describeInternal( $this->setFlags( [
-                               'src' => $this->params['dst'], 'headers' => $headers
-                       ] ) );
-               } else {
-                       // Copy the file to the destination
-                       $status = $this->backend->copyInternal( $this->setFlags( $this->params ) );
-               }
-
-               return $status;
-       }
-
-       public function storagePathsRead() {
-               return [ $this->params['src'] ];
-       }
-
-       public function storagePathsChanged() {
-               return [ $this->params['dst'] ];
-       }
-}
-
-/**
- * Move a file from one storage path to another in the backend.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class MoveFileOp extends FileOp {
-       protected function allowedParams() {
-               return [
-                       [ 'src', 'dst' ],
-                       [ 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ],
-                       [ 'src', 'dst' ]
-               ];
-       }
-
-       protected function doPrecheck( array &$predicates ) {
-               $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
-                       if ( $this->getParam( 'ignoreMissingSource' ) ) {
-                               $this->doOperation = false; // no-op
-                               // Update file existence predicates (cache 404s)
-                               $predicates['exists'][$this->params['src']] = false;
-                               $predicates['sha1'][$this->params['src']] = false;
-
-                               return $status; // nothing to do
-                       } else {
-                               $status->fatal( 'backend-fail-notexists', $this->params['src'] );
-
-                               return $status;
-                       }
-               // Check if a file can be placed/changed at the destination
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
-                       $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
-
-                       return $status;
-               }
-               // Check if destination file exists
-               $status->merge( $this->precheckDestExistence( $predicates ) );
-               $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
-               if ( $status->isOK() ) {
-                       // Update file existence predicates
-                       $predicates['exists'][$this->params['src']] = false;
-                       $predicates['sha1'][$this->params['src']] = false;
-                       $predicates['exists'][$this->params['dst']] = true;
-                       $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
-               }
-
-               return $status; // safe to call attempt()
-       }
-
-       protected function doAttempt() {
-               if ( $this->overwriteSameCase ) {
-                       if ( $this->params['src'] === $this->params['dst'] ) {
-                               // Do nothing to the destination (which is also the source)
-                               $status = StatusValue::newGood();
-                       } else {
-                               // Just delete the source as the destination file needs no changes
-                               $status = $this->backend->deleteInternal( $this->setFlags(
-                                       [ 'src' => $this->params['src'] ]
-                               ) );
-                       }
-               } elseif ( $this->params['src'] === $this->params['dst'] ) {
-                       // Just update the destination file headers
-                       $headers = $this->getParam( 'headers' ) ?: [];
-                       $status = $this->backend->describeInternal( $this->setFlags(
-                               [ 'src' => $this->params['dst'], 'headers' => $headers ]
-                       ) );
-               } else {
-                       // Move the file to the destination
-                       $status = $this->backend->moveInternal( $this->setFlags( $this->params ) );
-               }
-
-               return $status;
-       }
-
-       public function storagePathsRead() {
-               return [ $this->params['src'] ];
-       }
-
-       public function storagePathsChanged() {
-               return [ $this->params['src'], $this->params['dst'] ];
-       }
-}
-
-/**
- * Delete a file at the given storage path from the backend.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class DeleteFileOp extends FileOp {
-       protected function allowedParams() {
-               return [ [ 'src' ], [ 'ignoreMissingSource' ], [ 'src' ] ];
-       }
-
-       protected function doPrecheck( array &$predicates ) {
-               $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
-                       if ( $this->getParam( 'ignoreMissingSource' ) ) {
-                               $this->doOperation = false; // no-op
-                               // Update file existence predicates (cache 404s)
-                               $predicates['exists'][$this->params['src']] = false;
-                               $predicates['sha1'][$this->params['src']] = false;
-
-                               return $status; // nothing to do
-                       } else {
-                               $status->fatal( 'backend-fail-notexists', $this->params['src'] );
-
-                               return $status;
-                       }
-               // Check if a file can be placed/changed at the source
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['src'] );
-                       $status->fatal( 'backend-fail-delete', $this->params['src'] );
-
-                       return $status;
-               }
-               // Update file existence predicates
-               $predicates['exists'][$this->params['src']] = false;
-               $predicates['sha1'][$this->params['src']] = false;
-
-               return $status; // safe to call attempt()
-       }
-
-       protected function doAttempt() {
-               // Delete the source file
-               return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
-       }
-
-       public function storagePathsChanged() {
-               return [ $this->params['src'] ];
-       }
-}
-
-/**
- * Change metadata for a file at the given storage path in the backend.
- * Parameters for this operation are outlined in FileBackend::doOperations().
- */
-class DescribeFileOp extends FileOp {
-       protected function allowedParams() {
-               return [ [ 'src' ], [ 'headers' ], [ 'src' ] ];
-       }
-
-       protected function doPrecheck( array &$predicates ) {
-               $status = StatusValue::newGood();
-               // Check if the source file exists
-               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
-                       $status->fatal( 'backend-fail-notexists', $this->params['src'] );
-
-                       return $status;
-               // Check if a file can be placed/changed at the source
-               } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
-                       $status->fatal( 'backend-fail-usable', $this->params['src'] );
-                       $status->fatal( 'backend-fail-describe', $this->params['src'] );
-
-                       return $status;
-               }
-               // Update file existence predicates
-               $predicates['exists'][$this->params['src']] =
-                       $this->fileExists( $this->params['src'], $predicates );
-               $predicates['sha1'][$this->params['src']] =
-                       $this->fileSha1( $this->params['src'], $predicates );
-
-               return $status; // safe to call attempt()
-       }
-
-       protected function doAttempt() {
-               // Update the source file's metadata
-               return $this->backend->describeInternal( $this->setFlags( $this->params ) );
-       }
-
-       public function storagePathsChanged() {
-               return [ $this->params['src'] ];
-       }
-}
-
-/**
- * Placeholder operation that has no params and does nothing
- */
-class NullFileOp extends FileOp {
-}
diff --git a/includes/filebackend/FileOpBatch.php b/includes/filebackend/FileOpBatch.php
deleted file mode 100644 (file)
index e34ad8c..0000000
+++ /dev/null
@@ -1,204 +0,0 @@
-<?php
-/**
- * Helper class for representing batch file operations.
- *
- * 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 FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * Helper class for representing batch file operations.
- * Do not use this class from places outside FileBackend.
- *
- * Methods should avoid throwing exceptions at all costs.
- *
- * @ingroup FileBackend
- * @since 1.20
- */
-class FileOpBatch {
-       /* Timeout related parameters */
-       const MAX_BATCH_SIZE = 1000; // integer
-
-       /**
-        * Attempt to perform a series of file operations.
-        * Callers are responsible for handling file locking.
-        *
-        * $opts is an array of options, including:
-        *   - force        : Errors that would normally cause a rollback do not.
-        *                    The remaining operations are still attempted if any fail.
-        *   - nonJournaled : Don't log this operation batch in the file journal.
-        *   - concurrency  : Try to do this many operations in parallel when possible.
-        *
-        * 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
-        *
-        * @param FileOp[] $performOps List of FileOp operations
-        * @param array $opts Batch operation options
-        * @param FileJournal $journal Journal to log operations to
-        * @return StatusValue
-        */
-       public static function attempt( array $performOps, array $opts, FileJournal $journal ) {
-               $status = StatusValue::newGood();
-
-               $n = count( $performOps );
-               if ( $n > self::MAX_BATCH_SIZE ) {
-                       $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE );
-
-                       return $status;
-               }
-
-               $batchId = $journal->getTimestampedUUID();
-               $ignoreErrors = !empty( $opts['force'] );
-               $journaled = empty( $opts['nonJournaled'] );
-               $maxConcurrency = isset( $opts['concurrency'] ) ? $opts['concurrency'] : 1;
-
-               $entries = []; // file journal entry list
-               $predicates = FileOp::newPredicates(); // account for previous ops in prechecks
-               $curBatch = []; // concurrent FileOp sub-batch accumulation
-               $curBatchDeps = FileOp::newDependencies(); // paths used in FileOp sub-batch
-               $pPerformOps = []; // ordered list of concurrent FileOp sub-batches
-               $lastBackend = null; // last op backend name
-               // Do pre-checks for each operation; abort on failure...
-               foreach ( $performOps as $index => $fileOp ) {
-                       $backendName = $fileOp->getBackend()->getName();
-                       $fileOp->setBatchId( $batchId ); // transaction ID
-                       // Decide if this op can be done concurrently within this sub-batch
-                       // or if a new concurrent sub-batch must be started after this one...
-                       if ( $fileOp->dependsOn( $curBatchDeps )
-                               || count( $curBatch ) >= $maxConcurrency
-                               || ( $backendName !== $lastBackend && count( $curBatch ) )
-                       ) {
-                               $pPerformOps[] = $curBatch; // push this batch
-                               $curBatch = []; // start a new sub-batch
-                               $curBatchDeps = FileOp::newDependencies();
-                       }
-                       $lastBackend = $backendName;
-                       $curBatch[$index] = $fileOp; // keep index
-                       // Update list of affected paths in this batch
-                       $curBatchDeps = $fileOp->applyDependencies( $curBatchDeps );
-                       // Simulate performing the operation...
-                       $oldPredicates = $predicates;
-                       $subStatus = $fileOp->precheck( $predicates ); // updates $predicates
-                       $status->merge( $subStatus );
-                       if ( $subStatus->isOK() ) {
-                               if ( $journaled ) { // journal log entries
-                                       $entries = array_merge( $entries,
-                                               $fileOp->getJournalEntries( $oldPredicates, $predicates ) );
-                               }
-                       } else { // operation failed?
-                               $status->success[$index] = false;
-                               ++$status->failCount;
-                               if ( !$ignoreErrors ) {
-                                       return $status; // abort
-                               }
-                       }
-               }
-               // Push the last sub-batch
-               if ( count( $curBatch ) ) {
-                       $pPerformOps[] = $curBatch;
-               }
-
-               // Log the operations in the file journal...
-               if ( count( $entries ) ) {
-                       $subStatus = $journal->logChangeBatch( $entries, $batchId );
-                       if ( !$subStatus->isOK() ) {
-                               $status->merge( $subStatus );
-
-                               return $status; // abort
-                       }
-               }
-
-               if ( $ignoreErrors ) { // treat precheck() fatals as mere warnings
-                       $status->setResult( true, $status->value );
-               }
-
-               // Attempt each operation (in parallel if allowed and possible)...
-               self::runParallelBatches( $pPerformOps, $status );
-
-               return $status;
-       }
-
-       /**
-        * Attempt a list of file operations sub-batches in series.
-        *
-        * The operations *in* each sub-batch will be done in parallel.
-        * The caller is responsible for making sure the operations
-        * within any given sub-batch do not depend on each other.
-        * This will abort remaining ops on failure.
-        *
-        * @param array $pPerformOps Batches of file ops (batches use original indexes)
-        * @param StatusValue $status
-        */
-       protected static function runParallelBatches( array $pPerformOps, StatusValue $status ) {
-               $aborted = false; // set to true on unexpected errors
-               foreach ( $pPerformOps as $performOpsBatch ) {
-                       /** @var FileOp[] $performOpsBatch */
-                       if ( $aborted ) { // check batch op abort flag...
-                               // We can't continue (even with $ignoreErrors) as $predicates is wrong.
-                               // Log the remaining ops as failed for recovery...
-                               foreach ( $performOpsBatch as $i => $fileOp ) {
-                                       $status->success[$i] = false;
-                                       ++$status->failCount;
-                                       $performOpsBatch[$i]->logFailure( 'attempt_aborted' );
-                               }
-                               continue;
-                       }
-                       /** @var StatusValue[] $statuses */
-                       $statuses = [];
-                       $opHandles = [];
-                       // Get the backend; all sub-batch ops belong to a single backend
-                       $backend = reset( $performOpsBatch )->getBackend();
-                       // Get the operation handles or actually do it if there is just one.
-                       // If attemptAsync() returns a StatusValue, it was either due to an error
-                       // or the backend does not support async ops and did it synchronously.
-                       foreach ( $performOpsBatch as $i => $fileOp ) {
-                               if ( !isset( $status->success[$i] ) ) { // didn't already fail in precheck()
-                                       // Parallel ops may be disabled in config due to missing dependencies,
-                                       // (e.g. needing popen()). When they are, $performOpsBatch has size 1.
-                                       $subStatus = ( count( $performOpsBatch ) > 1 )
-                                               ? $fileOp->attemptAsync()
-                                               : $fileOp->attempt();
-                                       if ( $subStatus->value instanceof FileBackendStoreOpHandle ) {
-                                               $opHandles[$i] = $subStatus->value; // deferred
-                                       } else {
-                                               $statuses[$i] = $subStatus; // done already
-                                       }
-                               }
-                       }
-                       // Try to do all the operations concurrently...
-                       $statuses = $statuses + $backend->executeOpHandlesInternal( $opHandles );
-                       // Marshall and merge all the responses (blocking)...
-                       foreach ( $performOpsBatch as $i => $fileOp ) {
-                               if ( !isset( $status->success[$i] ) ) { // didn't already fail in precheck()
-                                       $subStatus = $statuses[$i];
-                                       $status->merge( $subStatus );
-                                       if ( $subStatus->isOK() ) {
-                                               $status->success[$i] = true;
-                                               ++$status->successCount;
-                                       } else {
-                                               $status->success[$i] = false;
-                                               ++$status->failCount;
-                                               $aborted = true; // set abort flag; we can't continue
-                                       }
-                               }
-                       }
-               }
-       }
-}
diff --git a/includes/filebackend/MemoryFileBackend.php b/includes/filebackend/MemoryFileBackend.php
deleted file mode 100644 (file)
index 44fe2cb..0000000
+++ /dev/null
@@ -1,263 +0,0 @@
-<?php
-/**
- * Simulation of a backend storage in memory.
- *
- * 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 FileBackend
- * @author Aaron Schulz
- */
-
-/**
- * Simulation of a backend storage in memory.
- *
- * All data in the backend is automatically deleted at the end of PHP execution.
- * Since the data stored here is volatile, this is only useful for staging or testing.
- *
- * @ingroup FileBackend
- * @since 1.23
- */
-class MemoryFileBackend extends FileBackendStore {
-       /** @var array Map of (file path => (data,mtime) */
-       protected $files = [];
-
-       public function getFeatures() {
-               return self::ATTR_UNICODE_PATHS;
-       }
-
-       public function isPathUsableInternal( $storagePath ) {
-               return true;
-       }
-
-       protected function doCreateInternal( array $params ) {
-               $status = $this->newStatus();
-
-               $dst = $this->resolveHashKey( $params['dst'] );
-               if ( $dst === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               $this->files[$dst] = [
-                       'data' => $params['content'],
-                       'mtime' => wfTimestamp( TS_MW, time() )
-               ];
-
-               return $status;
-       }
-
-       protected function doStoreInternal( array $params ) {
-               $status = $this->newStatus();
-
-               $dst = $this->resolveHashKey( $params['dst'] );
-               if ( $dst === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               MediaWiki\suppressWarnings();
-               $data = file_get_contents( $params['src'] );
-               MediaWiki\restoreWarnings();
-               if ( $data === false ) { // source doesn't exist?
-                       $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
-
-                       return $status;
-               }
-
-               $this->files[$dst] = [
-                       'data' => $data,
-                       'mtime' => wfTimestamp( TS_MW, time() )
-               ];
-
-               return $status;
-       }
-
-       protected function doCopyInternal( array $params ) {
-               $status = $this->newStatus();
-
-               $src = $this->resolveHashKey( $params['src'] );
-               if ( $src === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
-                       return $status;
-               }
-
-               $dst = $this->resolveHashKey( $params['dst'] );
-               if ( $dst === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               if ( !isset( $this->files[$src] ) ) {
-                       if ( empty( $params['ignoreMissingSource'] ) ) {
-                               $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
-                       }
-
-                       return $status;
-               }
-
-               $this->files[$dst] = [
-                       'data' => $this->files[$src]['data'],
-                       'mtime' => wfTimestamp( TS_MW, time() )
-               ];
-
-               return $status;
-       }
-
-       protected function doDeleteInternal( array $params ) {
-               $status = $this->newStatus();
-
-               $src = $this->resolveHashKey( $params['src'] );
-               if ( $src === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
-                       return $status;
-               }
-
-               if ( !isset( $this->files[$src] ) ) {
-                       if ( empty( $params['ignoreMissingSource'] ) ) {
-                               $status->fatal( 'backend-fail-delete', $params['src'] );
-                       }
-
-                       return $status;
-               }
-
-               unset( $this->files[$src] );
-
-               return $status;
-       }
-
-       protected function doGetFileStat( array $params ) {
-               $src = $this->resolveHashKey( $params['src'] );
-               if ( $src === null ) {
-                       return null;
-               }
-
-               if ( isset( $this->files[$src] ) ) {
-                       return [
-                               'mtime' => $this->files[$src]['mtime'],
-                               'size' => strlen( $this->files[$src]['data'] ),
-                       ];
-               }
-
-               return false;
-       }
-
-       protected function doGetLocalCopyMulti( array $params ) {
-               $tmpFiles = []; // (path => TempFSFile)
-               foreach ( $params['srcs'] as $srcPath ) {
-                       $src = $this->resolveHashKey( $srcPath );
-                       if ( $src === null || !isset( $this->files[$src] ) ) {
-                               $fsFile = null;
-                       } else {
-                               // Create a new temporary file with the same extension...
-                               $ext = FileBackend::extensionFromPath( $src );
-                               $fsFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
-                               if ( $fsFile ) {
-                                       $bytes = file_put_contents( $fsFile->getPath(), $this->files[$src]['data'] );
-                                       if ( $bytes !== strlen( $this->files[$src]['data'] ) ) {
-                                               $fsFile = null;
-                                       }
-                               }
-                       }
-                       $tmpFiles[$srcPath] = $fsFile;
-               }
-
-               return $tmpFiles;
-       }
-
-       protected function doDirectoryExists( $container, $dir, array $params ) {
-               $prefix = rtrim( "$container/$dir", '/' ) . '/';
-               foreach ( $this->files as $path => $data ) {
-                       if ( strpos( $path, $prefix ) === 0 ) {
-                               return true;
-                       }
-               }
-
-               return false;
-       }
-
-       public function getDirectoryListInternal( $container, $dir, array $params ) {
-               $dirs = [];
-               $prefix = rtrim( "$container/$dir", '/' ) . '/';
-               $prefixLen = strlen( $prefix );
-               foreach ( $this->files as $path => $data ) {
-                       if ( strpos( $path, $prefix ) === 0 ) {
-                               $relPath = substr( $path, $prefixLen );
-                               if ( $relPath === false ) {
-                                       continue;
-                               } elseif ( strpos( $relPath, '/' ) === false ) {
-                                       continue; // just a file
-                               }
-                               $parts = array_slice( explode( '/', $relPath ), 0, -1 ); // last part is file name
-                               if ( !empty( $params['topOnly'] ) ) {
-                                       $dirs[$parts[0]] = 1; // top directory
-                               } else {
-                                       $current = '';
-                                       foreach ( $parts as $part ) { // all directories
-                                               $dir = ( $current === '' ) ? $part : "$current/$part";
-                                               $dirs[$dir] = 1;
-                                               $current = $dir;
-                                       }
-                               }
-                       }
-               }
-
-               return array_keys( $dirs );
-       }
-
-       public function getFileListInternal( $container, $dir, array $params ) {
-               $files = [];
-               $prefix = rtrim( "$container/$dir", '/' ) . '/';
-               $prefixLen = strlen( $prefix );
-               foreach ( $this->files as $path => $data ) {
-                       if ( strpos( $path, $prefix ) === 0 ) {
-                               $relPath = substr( $path, $prefixLen );
-                               if ( $relPath === false ) {
-                                       continue;
-                               } elseif ( !empty( $params['topOnly'] ) && strpos( $relPath, '/' ) !== false ) {
-                                       continue;
-                               }
-                               $files[] = $relPath;
-                       }
-               }
-
-               return $files;
-       }
-
-       protected function directoriesAreVirtual() {
-               return true;
-       }
-
-       /**
-        * Get the absolute file system path for a storage path
-        *
-        * @param string $storagePath Storage path
-        * @return string|null
-        */
-       protected function resolveHashKey( $storagePath ) {
-               list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
-               if ( $relPath === null ) {
-                       return null; // invalid
-               }
-
-               return ( $relPath !== '' ) ? "$fullCont/$relPath" : $fullCont;
-       }
-}
diff --git a/includes/filebackend/SwiftFileBackend.php b/includes/filebackend/SwiftFileBackend.php
deleted file mode 100644 (file)
index 0a0e9f5..0000000
+++ /dev/null
@@ -1,1940 +0,0 @@
-<?php
-/**
- * OpenStack Swift based file backend.
- *
- * 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 FileBackend
- * @author Russ Nelson
- * @author Aaron Schulz
- */
-
-/**
- * @brief Class for an OpenStack Swift (or Ceph RGW) based file backend.
- *
- * StatusValue messages should avoid mentioning the Swift account name.
- * Likewise, error suppression should be used to avoid path disclosure.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-class SwiftFileBackend extends FileBackendStore {
-       /** @var MultiHttpClient */
-       protected $http;
-
-       /** @var int TTL in seconds */
-       protected $authTTL;
-
-       /** @var string Authentication base URL (without version) */
-       protected $swiftAuthUrl;
-
-       /** @var string Swift user (account:user) to authenticate as */
-       protected $swiftUser;
-
-       /** @var string Secret key for user */
-       protected $swiftKey;
-
-       /** @var string Shared secret value for making temp URLs */
-       protected $swiftTempUrlKey;
-
-       /** @var string S3 access key (RADOS Gateway) */
-       protected $rgwS3AccessKey;
-
-       /** @var string S3 authentication key (RADOS Gateway) */
-       protected $rgwS3SecretKey;
-
-       /** @var BagOStuff */
-       protected $srvCache;
-
-       /** @var ProcessCacheLRU Container stat cache */
-       protected $containerStatCache;
-
-       /** @var array */
-       protected $authCreds;
-
-       /** @var int UNIX timestamp */
-       protected $authSessionTimestamp = 0;
-
-       /** @var int UNIX timestamp */
-       protected $authErrorTimestamp = null;
-
-       /** @var bool Whether the server is an Ceph RGW */
-       protected $isRGW = false;
-
-       /**
-        * @see FileBackendStore::__construct()
-        * Additional $config params include:
-        *   - swiftAuthUrl       : Swift authentication server URL
-        *   - swiftUser          : Swift user used by MediaWiki (account:username)
-        *   - swiftKey           : Swift authentication key for the above user
-        *   - swiftAuthTTL       : Swift authentication TTL (seconds)
-        *   - swiftTempUrlKey    : Swift "X-Account-Meta-Temp-URL-Key" value on the account.
-        *                          Do not set this until it has been set in the backend.
-        *   - shardViaHashLevels : Map of container names to sharding config with:
-        *                             - base   : base of hash characters, 16 or 36
-        *                             - levels : the number of hash levels (and digits)
-        *                             - repeat : hash subdirectories are prefixed with all the
-        *                                        parent hash directory names (e.g. "a/ab/abc")
-        *   - cacheAuthInfo      : Whether to cache authentication tokens in APC, XCache, ect.
-        *                          If those are not available, then the main cache will be used.
-        *                          This is probably insecure in shared hosting environments.
-        *   - rgwS3AccessKey     : Rados Gateway S3 "access key" value on the account.
-        *                          Do not set this until it has been set in the backend.
-        *                          This is used for generating expiring pre-authenticated URLs.
-        *                          Only use this when using rgw and to work around
-        *                          http://tracker.newdream.net/issues/3454.
-        *   - rgwS3SecretKey     : Rados Gateway S3 "secret key" value on the account.
-        *                          Do not set this until it has been set in the backend.
-        *                          This is used for generating expiring pre-authenticated URLs.
-        *                          Only use this when using rgw and to work around
-        *                          http://tracker.newdream.net/issues/3454.
-        */
-       public function __construct( array $config ) {
-               parent::__construct( $config );
-               // Required settings
-               $this->swiftAuthUrl = $config['swiftAuthUrl'];
-               $this->swiftUser = $config['swiftUser'];
-               $this->swiftKey = $config['swiftKey'];
-               // Optional settings
-               $this->authTTL = isset( $config['swiftAuthTTL'] )
-                       ? $config['swiftAuthTTL']
-                       : 15 * 60; // some sane number
-               $this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] )
-                       ? $config['swiftTempUrlKey']
-                       : '';
-               $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
-                       ? $config['shardViaHashLevels']
-                       : '';
-               $this->rgwS3AccessKey = isset( $config['rgwS3AccessKey'] )
-                       ? $config['rgwS3AccessKey']
-                       : '';
-               $this->rgwS3SecretKey = isset( $config['rgwS3SecretKey'] )
-                       ? $config['rgwS3SecretKey']
-                       : '';
-               // HTTP helper client
-               $this->http = new MultiHttpClient( [] );
-               // Cache container information to mask latency
-               if ( isset( $config['wanCache'] ) && $config['wanCache'] instanceof WANObjectCache ) {
-                       $this->memCache = $config['wanCache'];
-               }
-               // Process cache for container info
-               $this->containerStatCache = new ProcessCacheLRU( 300 );
-               // Cache auth token information to avoid RTTs
-               if ( !empty( $config['cacheAuthInfo'] ) ) {
-                       if ( PHP_SAPI === 'cli' ) {
-                               // Preferrably memcached
-                               $this->srvCache = ObjectCache::getLocalClusterInstance();
-                       } else {
-                               // Look for APC, XCache, WinCache, ect...
-                               $this->srvCache = ObjectCache::getLocalServerInstance( CACHE_NONE );
-                       }
-               } else {
-                       $this->srvCache = new EmptyBagOStuff();
-               }
-       }
-
-       public function getFeatures() {
-               return ( FileBackend::ATTR_UNICODE_PATHS |
-                       FileBackend::ATTR_HEADERS | FileBackend::ATTR_METADATA );
-       }
-
-       protected function resolveContainerPath( $container, $relStoragePath ) {
-               if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) {
-                       return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
-               } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
-                       return null; // too long for Swift
-               }
-
-               return $relStoragePath;
-       }
-
-       public function isPathUsableInternal( $storagePath ) {
-               list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
-               if ( $rel === null ) {
-                       return false; // invalid
-               }
-
-               return is_array( $this->getContainerStat( $container ) );
-       }
-
-       /**
-        * Sanitize and filter the custom headers from a $params array.
-        * Only allows certain "standard" Content- and X-Content- headers.
-        *
-        * @param array $params
-        * @return array Sanitized value of 'headers' field in $params
-        */
-       protected function sanitizeHdrs( array $params ) {
-               return isset( $params['headers'] )
-                       ? $this->getCustomHeaders( $params['headers'] )
-                       : [];
-
-       }
-
-       /**
-        * @param array $rawHeaders
-        * @return array Custom non-metadata HTTP headers
-        */
-       protected function getCustomHeaders( array $rawHeaders ) {
-               $headers = [];
-
-               // Normalize casing, and strip out illegal headers
-               foreach ( $rawHeaders as $name => $value ) {
-                       $name = strtolower( $name );
-                       if ( preg_match( '/^content-(type|length)$/', $name ) ) {
-                               continue; // blacklisted
-                       } elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
-                               $headers[$name] = $value; // allowed
-                       } elseif ( preg_match( '/^content-(disposition)/', $name ) ) {
-                               $headers[$name] = $value; // allowed
-                       }
-               }
-               // By default, Swift has annoyingly low maximum header value limits
-               if ( isset( $headers['content-disposition'] ) ) {
-                       $disposition = '';
-                       // @note: assume FileBackend::makeContentDisposition() already used
-                       foreach ( explode( ';', $headers['content-disposition'] ) as $part ) {
-                               $part = trim( $part );
-                               $new = ( $disposition === '' ) ? $part : "{$disposition};{$part}";
-                               if ( strlen( $new ) <= 255 ) {
-                                       $disposition = $new;
-                               } else {
-                                       break; // too long; sigh
-                               }
-                       }
-                       $headers['content-disposition'] = $disposition;
-               }
-
-               return $headers;
-       }
-
-       /**
-        * @param array $rawHeaders
-        * @return array Custom metadata headers
-        */
-       protected function getMetadataHeaders( array $rawHeaders ) {
-               $headers = [];
-               foreach ( $rawHeaders as $name => $value ) {
-                       $name = strtolower( $name );
-                       if ( strpos( $name, 'x-object-meta-' ) === 0 ) {
-                               $headers[$name] = $value;
-                       }
-               }
-
-               return $headers;
-       }
-
-       /**
-        * @param array $rawHeaders
-        * @return array Custom metadata headers with prefix removed
-        */
-       protected function getMetadata( array $rawHeaders ) {
-               $metadata = [];
-               foreach ( $this->getMetadataHeaders( $rawHeaders ) as $name => $value ) {
-                       $metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value;
-               }
-
-               return $metadata;
-       }
-
-       protected function doCreateInternal( array $params ) {
-               $status = $this->newStatus();
-
-               list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
-               if ( $dstRel === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               $sha1Hash = Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 );
-               $contentType = isset( $params['headers']['content-type'] )
-                       ? $params['headers']['content-type']
-                       : $this->getContentType( $params['dst'], $params['content'], null );
-
-               $reqs = [ [
-                       'method' => 'PUT',
-                       'url' => [ $dstCont, $dstRel ],
-                       'headers' => [
-                               'content-length' => strlen( $params['content'] ),
-                               'etag' => md5( $params['content'] ),
-                               'content-type' => $contentType,
-                               'x-object-meta-sha1base36' => $sha1Hash
-                       ] + $this->sanitizeHdrs( $params ),
-                       'body' => $params['content']
-               ] ];
-
-               $method = __METHOD__;
-               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
-                       if ( $rcode === 201 ) {
-                               // good
-                       } elseif ( $rcode === 412 ) {
-                               $status->fatal( 'backend-fail-contenttype', $params['dst'] );
-                       } else {
-                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
-                       }
-               };
-
-               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $status->value = $opHandle;
-               } else { // actually write the object in Swift
-                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
-               }
-
-               return $status;
-       }
-
-       protected function doStoreInternal( array $params ) {
-               $status = $this->newStatus();
-
-               list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
-               if ( $dstRel === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               MediaWiki\suppressWarnings();
-               $sha1Hash = sha1_file( $params['src'] );
-               MediaWiki\restoreWarnings();
-               if ( $sha1Hash === false ) { // source doesn't exist?
-                       $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
-
-                       return $status;
-               }
-               $sha1Hash = Wikimedia\base_convert( $sha1Hash, 16, 36, 31 );
-               $contentType = isset( $params['headers']['content-type'] )
-                       ? $params['headers']['content-type']
-                       : $this->getContentType( $params['dst'], null, $params['src'] );
-
-               $handle = fopen( $params['src'], 'rb' );
-               if ( $handle === false ) { // source doesn't exist?
-                       $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
-
-                       return $status;
-               }
-
-               $reqs = [ [
-                       'method' => 'PUT',
-                       'url' => [ $dstCont, $dstRel ],
-                       'headers' => [
-                               'content-length' => filesize( $params['src'] ),
-                               'etag' => md5_file( $params['src'] ),
-                               'content-type' => $contentType,
-                               'x-object-meta-sha1base36' => $sha1Hash
-                       ] + $this->sanitizeHdrs( $params ),
-                       'body' => $handle // resource
-               ] ];
-
-               $method = __METHOD__;
-               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
-                       if ( $rcode === 201 ) {
-                               // good
-                       } elseif ( $rcode === 412 ) {
-                               $status->fatal( 'backend-fail-contenttype', $params['dst'] );
-                       } else {
-                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
-                       }
-               };
-
-               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $status->value = $opHandle;
-               } else { // actually write the object in Swift
-                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
-               }
-
-               return $status;
-       }
-
-       protected function doCopyInternal( array $params ) {
-               $status = $this->newStatus();
-
-               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
-               if ( $srcRel === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
-                       return $status;
-               }
-
-               list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
-               if ( $dstRel === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               $reqs = [ [
-                       'method' => 'PUT',
-                       'url' => [ $dstCont, $dstRel ],
-                       'headers' => [
-                               'x-copy-from' => '/' . rawurlencode( $srcCont ) .
-                                       '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
-                       ] + $this->sanitizeHdrs( $params ), // extra headers merged into object
-               ] ];
-
-               $method = __METHOD__;
-               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
-                       if ( $rcode === 201 ) {
-                               // good
-                       } elseif ( $rcode === 404 ) {
-                               $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
-                       } else {
-                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
-                       }
-               };
-
-               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $status->value = $opHandle;
-               } else { // actually write the object in Swift
-                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
-               }
-
-               return $status;
-       }
-
-       protected function doMoveInternal( array $params ) {
-               $status = $this->newStatus();
-
-               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
-               if ( $srcRel === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
-                       return $status;
-               }
-
-               list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
-               if ( $dstRel === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
-
-                       return $status;
-               }
-
-               $reqs = [
-                       [
-                               'method' => 'PUT',
-                               'url' => [ $dstCont, $dstRel ],
-                               'headers' => [
-                                       'x-copy-from' => '/' . rawurlencode( $srcCont ) .
-                                               '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
-                               ] + $this->sanitizeHdrs( $params ) // extra headers merged into object
-                       ]
-               ];
-               if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
-                       $reqs[] = [
-                               'method' => 'DELETE',
-                               'url' => [ $srcCont, $srcRel ],
-                               'headers' => []
-                       ];
-               }
-
-               $method = __METHOD__;
-               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
-                       if ( $request['method'] === 'PUT' && $rcode === 201 ) {
-                               // good
-                       } elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) {
-                               // good
-                       } elseif ( $rcode === 404 ) {
-                               $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
-                       } else {
-                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
-                       }
-               };
-
-               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $status->value = $opHandle;
-               } else { // actually move the object in Swift
-                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
-               }
-
-               return $status;
-       }
-
-       protected function doDeleteInternal( array $params ) {
-               $status = $this->newStatus();
-
-               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
-               if ( $srcRel === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
-                       return $status;
-               }
-
-               $reqs = [ [
-                       'method' => 'DELETE',
-                       'url' => [ $srcCont, $srcRel ],
-                       'headers' => []
-               ] ];
-
-               $method = __METHOD__;
-               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
-                       if ( $rcode === 204 ) {
-                               // good
-                       } elseif ( $rcode === 404 ) {
-                               if ( empty( $params['ignoreMissingSource'] ) ) {
-                                       $status->fatal( 'backend-fail-delete', $params['src'] );
-                               }
-                       } else {
-                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
-                       }
-               };
-
-               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $status->value = $opHandle;
-               } else { // actually delete the object in Swift
-                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
-               }
-
-               return $status;
-       }
-
-       protected function doDescribeInternal( array $params ) {
-               $status = $this->newStatus();
-
-               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
-               if ( $srcRel === null ) {
-                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
-                       return $status;
-               }
-
-               // Fetch the old object headers/metadata...this should be in stat cache by now
-               $stat = $this->getFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
-               if ( $stat && !isset( $stat['xattr'] ) ) { // older cache entry
-                       $stat = $this->doGetFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
-               }
-               if ( !$stat ) {
-                       $status->fatal( 'backend-fail-describe', $params['src'] );
-
-                       return $status;
-               }
-
-               // POST clears prior headers, so we need to merge the changes in to the old ones
-               $metaHdrs = [];
-               foreach ( $stat['xattr']['metadata'] as $name => $value ) {
-                       $metaHdrs["x-object-meta-$name"] = $value;
-               }
-               $customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers'];
-
-               $reqs = [ [
-                       'method' => 'POST',
-                       'url' => [ $srcCont, $srcRel ],
-                       'headers' => $metaHdrs + $customHdrs
-               ] ];
-
-               $method = __METHOD__;
-               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
-                       if ( $rcode === 202 ) {
-                               // good
-                       } elseif ( $rcode === 404 ) {
-                               $status->fatal( 'backend-fail-describe', $params['src'] );
-                       } else {
-                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
-                       }
-               };
-
-               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
-               if ( !empty( $params['async'] ) ) { // deferred
-                       $status->value = $opHandle;
-               } else { // actually change the object in Swift
-                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
-               }
-
-               return $status;
-       }
-
-       protected function doPrepareInternal( $fullCont, $dir, array $params ) {
-               $status = $this->newStatus();
-
-               // (a) Check if container already exists
-               $stat = $this->getContainerStat( $fullCont );
-               if ( is_array( $stat ) ) {
-                       return $status; // already there
-               } elseif ( $stat === null ) {
-                       $status->fatal( 'backend-fail-internal', $this->name );
-                       wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
-
-                       return $status;
-               }
-
-               // (b) Create container as needed with proper ACLs
-               if ( $stat === false ) {
-                       $params['op'] = 'prepare';
-                       $status->merge( $this->createContainer( $fullCont, $params ) );
-               }
-
-               return $status;
-       }
-
-       protected function doSecureInternal( $fullCont, $dir, array $params ) {
-               $status = $this->newStatus();
-               if ( empty( $params['noAccess'] ) ) {
-                       return $status; // nothing to do
-               }
-
-               $stat = $this->getContainerStat( $fullCont );
-               if ( is_array( $stat ) ) {
-                       // Make container private to end-users...
-                       $status->merge( $this->setContainerAccess(
-                               $fullCont,
-                               [ $this->swiftUser ], // read
-                               [ $this->swiftUser ] // write
-                       ) );
-               } elseif ( $stat === false ) {
-                       $status->fatal( 'backend-fail-usable', $params['dir'] );
-               } else {
-                       $status->fatal( 'backend-fail-internal', $this->name );
-                       wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
-               }
-
-               return $status;
-       }
-
-       protected function doPublishInternal( $fullCont, $dir, array $params ) {
-               $status = $this->newStatus();
-
-               $stat = $this->getContainerStat( $fullCont );
-               if ( is_array( $stat ) ) {
-                       // Make container public to end-users...
-                       $status->merge( $this->setContainerAccess(
-                               $fullCont,
-                               [ $this->swiftUser, '.r:*' ], // read
-                               [ $this->swiftUser ] // write
-                       ) );
-               } elseif ( $stat === false ) {
-                       $status->fatal( 'backend-fail-usable', $params['dir'] );
-               } else {
-                       $status->fatal( 'backend-fail-internal', $this->name );
-                       wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
-               }
-
-               return $status;
-       }
-
-       protected function doCleanInternal( $fullCont, $dir, array $params ) {
-               $status = $this->newStatus();
-
-               // Only containers themselves can be removed, all else is virtual
-               if ( $dir != '' ) {
-                       return $status; // nothing to do
-               }
-
-               // (a) Check the container
-               $stat = $this->getContainerStat( $fullCont, true );
-               if ( $stat === false ) {
-                       return $status; // ok, nothing to do
-               } elseif ( !is_array( $stat ) ) {
-                       $status->fatal( 'backend-fail-internal', $this->name );
-                       wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
-
-                       return $status;
-               }
-
-               // (b) Delete the container if empty
-               if ( $stat['count'] == 0 ) {
-                       $params['op'] = 'clean';
-                       $status->merge( $this->deleteContainer( $fullCont, $params ) );
-               }
-
-               return $status;
-       }
-
-       protected function doGetFileStat( array $params ) {
-               $params = [ 'srcs' => [ $params['src'] ], 'concurrency' => 1 ] + $params;
-               unset( $params['src'] );
-               $stats = $this->doGetFileStatMulti( $params );
-
-               return reset( $stats );
-       }
-
-       /**
-        * Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT"/"2013-05-11T07:37:27.678360Z".
-        * Dates might also come in like "2013-05-11T07:37:27.678360" from Swift listings,
-        * missing the timezone suffix (though Ceph RGW does not appear to have this bug).
-        *
-        * @param string $ts
-        * @param int $format Output format (TS_* constant)
-        * @return string
-        * @throws FileBackendError
-        */
-       protected function convertSwiftDate( $ts, $format = TS_MW ) {
-               try {
-                       $timestamp = new MWTimestamp( $ts );
-
-                       return $timestamp->getTimestamp( $format );
-               } catch ( Exception $e ) {
-                       throw new FileBackendError( $e->getMessage() );
-               }
-       }
-
-       /**
-        * Fill in any missing object metadata and save it to Swift
-        *
-        * @param array $objHdrs Object response headers
-        * @param string $path Storage path to object
-        * @return array New headers
-        */
-       protected function addMissingMetadata( array $objHdrs, $path ) {
-               if ( isset( $objHdrs['x-object-meta-sha1base36'] ) ) {
-                       return $objHdrs; // nothing to do
-               }
-
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               wfDebugLog( 'SwiftBackend', __METHOD__ . ": $path was not stored with SHA-1 metadata." );
-
-               $objHdrs['x-object-meta-sha1base36'] = false;
-
-               $auth = $this->getAuthentication();
-               if ( !$auth ) {
-                       return $objHdrs; // failed
-               }
-
-               // Find prior custom HTTP headers
-               $postHeaders = $this->getCustomHeaders( $objHdrs );
-               // Find prior metadata headers
-               $postHeaders += $this->getMetadataHeaders( $objHdrs );
-
-               $status = $this->newStatus();
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $scopeLockS = $this->getScopedFileLocks( [ $path ], LockManager::LOCK_UW, $status );
-               if ( $status->isOK() ) {
-                       $tmpFile = $this->getLocalCopy( [ 'src' => $path, 'latest' => 1 ] );
-                       if ( $tmpFile ) {
-                               $hash = $tmpFile->getSha1Base36();
-                               if ( $hash !== false ) {
-                                       $objHdrs['x-object-meta-sha1base36'] = $hash;
-                                       // Merge new SHA1 header into the old ones
-                                       $postHeaders['x-object-meta-sha1base36'] = $hash;
-                                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
-                                       list( $rcode ) = $this->http->run( [
-                                               'method' => 'POST',
-                                               'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
-                                               'headers' => $this->authTokenHeaders( $auth ) + $postHeaders
-                                       ] );
-                                       if ( $rcode >= 200 && $rcode <= 299 ) {
-                                               $this->deleteFileCache( $path );
-
-                                               return $objHdrs; // success
-                                       }
-                               }
-                       }
-               }
-
-               wfDebugLog( 'SwiftBackend', __METHOD__ . ": unable to set SHA-1 metadata for $path" );
-
-               return $objHdrs; // failed
-       }
-
-       protected function doGetFileContentsMulti( array $params ) {
-               $contents = [];
-
-               $auth = $this->getAuthentication();
-
-               $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
-               // Blindly create tmp files and stream to them, catching any exception if the file does
-               // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
-               $reqs = []; // (path => op)
-
-               foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
-                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
-                       if ( $srcRel === null || !$auth ) {
-                               $contents[$path] = false;
-                               continue;
-                       }
-                       // Create a new temporary memory file...
-                       $handle = fopen( 'php://temp', 'wb' );
-                       if ( $handle ) {
-                               $reqs[$path] = [
-                                       'method'  => 'GET',
-                                       'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
-                                       'headers' => $this->authTokenHeaders( $auth )
-                                               + $this->headersFromParams( $params ),
-                                       'stream'  => $handle,
-                               ];
-                       }
-                       $contents[$path] = false;
-               }
-
-               $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
-               $reqs = $this->http->runMulti( $reqs, $opts );
-               foreach ( $reqs as $path => $op ) {
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
-                       if ( $rcode >= 200 && $rcode <= 299 ) {
-                               rewind( $op['stream'] ); // start from the beginning
-                               $contents[$path] = stream_get_contents( $op['stream'] );
-                       } elseif ( $rcode === 404 ) {
-                               $contents[$path] = false;
-                       } else {
-                               $this->onError( null, __METHOD__,
-                                       [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
-                       }
-                       fclose( $op['stream'] ); // close open handle
-               }
-
-               return $contents;
-       }
-
-       protected function doDirectoryExists( $fullCont, $dir, array $params ) {
-               $prefix = ( $dir == '' ) ? null : "{$dir}/";
-               $status = $this->objectListing( $fullCont, 'names', 1, null, $prefix );
-               if ( $status->isOK() ) {
-                       return ( count( $status->value ) ) > 0;
-               }
-
-               return null; // error
-       }
-
-       /**
-        * @see FileBackendStore::getDirectoryListInternal()
-        * @param string $fullCont
-        * @param string $dir
-        * @param array $params
-        * @return SwiftFileBackendDirList
-        */
-       public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
-               return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
-       }
-
-       /**
-        * @see FileBackendStore::getFileListInternal()
-        * @param string $fullCont
-        * @param string $dir
-        * @param array $params
-        * @return SwiftFileBackendFileList
-        */
-       public function getFileListInternal( $fullCont, $dir, array $params ) {
-               return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
-       }
-
-       /**
-        * Do not call this function outside of SwiftFileBackendFileList
-        *
-        * @param string $fullCont Resolved container name
-        * @param string $dir Resolved storage directory with no trailing slash
-        * @param string|null $after Resolved container relative path to list items after
-        * @param int $limit Max number of items to list
-        * @param array $params Parameters for getDirectoryList()
-        * @return array List of container relative resolved paths of directories directly under $dir
-        * @throws FileBackendError
-        */
-       public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
-               $dirs = [];
-               if ( $after === INF ) {
-                       return $dirs; // nothing more
-               }
-
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
-               $prefix = ( $dir == '' ) ? null : "{$dir}/";
-               // Non-recursive: only list dirs right under $dir
-               if ( !empty( $params['topOnly'] ) ) {
-                       $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
-                       if ( !$status->isOK() ) {
-                               throw new FileBackendError( "Iterator page I/O error: {$status->getMessage()}" );
-                       }
-                       $objects = $status->value;
-                       foreach ( $objects as $object ) { // files and directories
-                               if ( substr( $object, -1 ) === '/' ) {
-                                       $dirs[] = $object; // directories end in '/'
-                               }
-                       }
-               } else {
-                       // Recursive: list all dirs under $dir and its subdirs
-                       $getParentDir = function ( $path ) {
-                               return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
-                       };
-
-                       // Get directory from last item of prior page
-                       $lastDir = $getParentDir( $after ); // must be first page
-                       $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
-
-                       if ( !$status->isOK() ) {
-                               throw new FileBackendError( "Iterator page I/O error: {$status->getMessage()}" );
-                       }
-
-                       $objects = $status->value;
-
-                       foreach ( $objects as $object ) { // files
-                               $objectDir = $getParentDir( $object ); // directory of object
-
-                               if ( $objectDir !== false && $objectDir !== $dir ) {
-                                       // Swift stores paths in UTF-8, using binary sorting.
-                                       // See function "create_container_table" in common/db.py.
-                                       // If a directory is not "greater" than the last one,
-                                       // then it was already listed by the calling iterator.
-                                       if ( strcmp( $objectDir, $lastDir ) > 0 ) {
-                                               $pDir = $objectDir;
-                                               do { // add dir and all its parent dirs
-                                                       $dirs[] = "{$pDir}/";
-                                                       $pDir = $getParentDir( $pDir );
-                                               } while ( $pDir !== false // sanity
-                                                       && strcmp( $pDir, $lastDir ) > 0 // not done already
-                                                       && strlen( $pDir ) > strlen( $dir ) // within $dir
-                                               );
-                                       }
-                                       $lastDir = $objectDir;
-                               }
-                       }
-               }
-               // Page on the unfiltered directory listing (what is returned may be filtered)
-               if ( count( $objects ) < $limit ) {
-                       $after = INF; // avoid a second RTT
-               } else {
-                       $after = end( $objects ); // update last item
-               }
-
-               return $dirs;
-       }
-
-       /**
-        * Do not call this function outside of SwiftFileBackendFileList
-        *
-        * @param string $fullCont Resolved container name
-        * @param string $dir Resolved storage directory with no trailing slash
-        * @param string|null $after Resolved container relative path of file to list items after
-        * @param int $limit Max number of items to list
-        * @param array $params Parameters for getDirectoryList()
-        * @return array List of resolved container relative paths of files under $dir
-        * @throws FileBackendError
-        */
-       public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
-               $files = []; // list of (path, stat array or null) entries
-               if ( $after === INF ) {
-                       return $files; // nothing more
-               }
-
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
-               $prefix = ( $dir == '' ) ? null : "{$dir}/";
-               // $objects will contain a list of unfiltered names or CF_Object items
-               // Non-recursive: only list files right under $dir
-               if ( !empty( $params['topOnly'] ) ) {
-                       if ( !empty( $params['adviseStat'] ) ) {
-                               $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix, '/' );
-                       } else {
-                               $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
-                       }
-               } else {
-                       // Recursive: list all files under $dir and its subdirs
-                       if ( !empty( $params['adviseStat'] ) ) {
-                               $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix );
-                       } else {
-                               $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
-                       }
-               }
-
-               // Reformat this list into a list of (name, stat array or null) entries
-               if ( !$status->isOK() ) {
-                       throw new FileBackendError( "Iterator page I/O error: {$status->getMessage()}" );
-               }
-
-               $objects = $status->value;
-               $files = $this->buildFileObjectListing( $params, $dir, $objects );
-
-               // Page on the unfiltered object listing (what is returned may be filtered)
-               if ( count( $objects ) < $limit ) {
-                       $after = INF; // avoid a second RTT
-               } else {
-                       $after = end( $objects ); // update last item
-                       $after = is_object( $after ) ? $after->name : $after;
-               }
-
-               return $files;
-       }
-
-       /**
-        * Build a list of file objects, filtering out any directories
-        * and extracting any stat info if provided in $objects (for CF_Objects)
-        *
-        * @param array $params Parameters for getDirectoryList()
-        * @param string $dir Resolved container directory path
-        * @param array $objects List of CF_Object items or object names
-        * @return array List of (names,stat array or null) entries
-        */
-       private function buildFileObjectListing( array $params, $dir, array $objects ) {
-               $names = [];
-               foreach ( $objects as $object ) {
-                       if ( is_object( $object ) ) {
-                               if ( isset( $object->subdir ) || !isset( $object->name ) ) {
-                                       continue; // virtual directory entry; ignore
-                               }
-                               $stat = [
-                                       // Convert various random Swift dates to TS_MW
-                                       'mtime'  => $this->convertSwiftDate( $object->last_modified, TS_MW ),
-                                       'size'   => (int)$object->bytes,
-                                       'sha1'   => null,
-                                       // Note: manifiest ETags are not an MD5 of the file
-                                       'md5'    => ctype_xdigit( $object->hash ) ? $object->hash : null,
-                                       'latest' => false // eventually consistent
-                               ];
-                               $names[] = [ $object->name, $stat ];
-                       } elseif ( substr( $object, -1 ) !== '/' ) {
-                               // Omit directories, which end in '/' in listings
-                               $names[] = [ $object, null ];
-                       }
-               }
-
-               return $names;
-       }
-
-       /**
-        * Do not call this function outside of SwiftFileBackendFileList
-        *
-        * @param string $path Storage path
-        * @param array $val Stat value
-        */
-       public function loadListingStatInternal( $path, array $val ) {
-               $this->cheapCache->set( $path, 'stat', $val );
-       }
-
-       protected function doGetFileXAttributes( array $params ) {
-               $stat = $this->getFileStat( $params );
-               if ( $stat ) {
-                       if ( !isset( $stat['xattr'] ) ) {
-                               // Stat entries filled by file listings don't include metadata/headers
-                               $this->clearCache( [ $params['src'] ] );
-                               $stat = $this->getFileStat( $params );
-                       }
-
-                       return $stat['xattr'];
-               } else {
-                       return false;
-               }
-       }
-
-       protected function doGetFileSha1base36( array $params ) {
-               $stat = $this->getFileStat( $params );
-               if ( $stat ) {
-                       if ( !isset( $stat['sha1'] ) ) {
-                               // Stat entries filled by file listings don't include SHA1
-                               $this->clearCache( [ $params['src'] ] );
-                               $stat = $this->getFileStat( $params );
-                       }
-
-                       return $stat['sha1'];
-               } else {
-                       return false;
-               }
-       }
-
-       protected function doStreamFile( array $params ) {
-               $status = $this->newStatus();
-
-               $flags = !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
-
-               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
-               if ( $srcRel === null ) {
-                       StreamFile::send404Message( $params['src'], $flags );
-                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
-
-                       return $status;
-               }
-
-               $auth = $this->getAuthentication();
-               if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) {
-                       StreamFile::send404Message( $params['src'], $flags );
-                       $status->fatal( 'backend-fail-stream', $params['src'] );
-
-                       return $status;
-               }
-
-               // If "headers" is set, we only want to send them if the file is there.
-               // Do not bother checking if the file exists if headers are not set though.
-               if ( $params['headers'] && !$this->fileExists( $params ) ) {
-                       StreamFile::send404Message( $params['src'], $flags );
-                       $status->fatal( 'backend-fail-stream', $params['src'] );
-
-                       return $status;
-               }
-
-               // Send the requested additional headers
-               foreach ( $params['headers'] as $header ) {
-                       header( $header ); // aways send
-               }
-
-               if ( empty( $params['allowOB'] ) ) {
-                       // Cancel output buffering and gzipping if set
-                       wfResetOutputBuffers();
-               }
-
-               $handle = fopen( 'php://output', 'wb' );
-               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
-                       'method' => 'GET',
-                       'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
-                       'headers' => $this->authTokenHeaders( $auth )
-                               + $this->headersFromParams( $params ) + $params['options'],
-                       'stream' => $handle,
-                       'flags'  => [ 'relayResponseHeaders' => empty( $params['headless'] ) ]
-               ] );
-
-               if ( $rcode >= 200 && $rcode <= 299 ) {
-                       // good
-               } elseif ( $rcode === 404 ) {
-                       $status->fatal( 'backend-fail-stream', $params['src'] );
-                       // Per bug 41113, nasty things can happen if bad cache entries get
-                       // stuck in cache. It's also possible that this error can come up
-                       // with simple race conditions. Clear out the stat cache to be safe.
-                       $this->clearCache( [ $params['src'] ] );
-                       $this->deleteFileCache( $params['src'] );
-               } else {
-                       $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
-               }
-
-               return $status;
-       }
-
-       protected function doGetLocalCopyMulti( array $params ) {
-               $tmpFiles = [];
-
-               $auth = $this->getAuthentication();
-
-               $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
-               // Blindly create tmp files and stream to them, catching any exception if the file does
-               // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
-               $reqs = []; // (path => op)
-
-               foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
-                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
-                       if ( $srcRel === null || !$auth ) {
-                               $tmpFiles[$path] = null;
-                               continue;
-                       }
-                       // Get source file extension
-                       $ext = FileBackend::extensionFromPath( $path );
-                       // Create a new temporary file...
-                       $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
-                       if ( $tmpFile ) {
-                               $handle = fopen( $tmpFile->getPath(), 'wb' );
-                               if ( $handle ) {
-                                       $reqs[$path] = [
-                                               'method'  => 'GET',
-                                               'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
-                                               'headers' => $this->authTokenHeaders( $auth )
-                                                       + $this->headersFromParams( $params ),
-                                               'stream'  => $handle,
-                                       ];
-                               } else {
-                                       $tmpFile = null;
-                               }
-                       }
-                       $tmpFiles[$path] = $tmpFile;
-               }
-
-               $isLatest = ( $this->isRGW || !empty( $params['latest'] ) );
-               $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
-               $reqs = $this->http->runMulti( $reqs, $opts );
-               foreach ( $reqs as $path => $op ) {
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
-                       fclose( $op['stream'] ); // close open handle
-                       if ( $rcode >= 200 && $rcode <= 299 ) {
-                               $size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0;
-                               // Double check that the disk is not full/broken
-                               if ( $size != $rhdrs['content-length'] ) {
-                                       $tmpFiles[$path] = null;
-                                       $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
-                                       $this->onError( null, __METHOD__,
-                                               [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
-                               }
-                               // Set the file stat process cache in passing
-                               $stat = $this->getStatFromHeaders( $rhdrs );
-                               $stat['latest'] = $isLatest;
-                               $this->cheapCache->set( $path, 'stat', $stat );
-                       } elseif ( $rcode === 404 ) {
-                               $tmpFiles[$path] = false;
-                       } else {
-                               $tmpFiles[$path] = null;
-                               $this->onError( null, __METHOD__,
-                                       [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
-                       }
-               }
-
-               return $tmpFiles;
-       }
-
-       public function getFileHttpUrl( array $params ) {
-               if ( $this->swiftTempUrlKey != '' ||
-                       ( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' )
-               ) {
-                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
-                       if ( $srcRel === null ) {
-                               return null; // invalid path
-                       }
-
-                       $auth = $this->getAuthentication();
-                       if ( !$auth ) {
-                               return null;
-                       }
-
-                       $ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400;
-                       $expires = time() + $ttl;
-
-                       if ( $this->swiftTempUrlKey != '' ) {
-                               $url = $this->storageUrl( $auth, $srcCont, $srcRel );
-                               // Swift wants the signature based on the unencoded object name
-                               $contPath = parse_url( $this->storageUrl( $auth, $srcCont ), PHP_URL_PATH );
-                               $signature = hash_hmac( 'sha1',
-                                       "GET\n{$expires}\n{$contPath}/{$srcRel}",
-                                       $this->swiftTempUrlKey
-                               );
-
-                               return "{$url}?temp_url_sig={$signature}&temp_url_expires={$expires}";
-                       } else { // give S3 API URL for rgw
-                               // Path for signature starts with the bucket
-                               $spath = '/' . rawurlencode( $srcCont ) . '/' .
-                                       str_replace( '%2F', '/', rawurlencode( $srcRel ) );
-                               // Calculate the hash
-                               $signature = base64_encode( hash_hmac(
-                                       'sha1',
-                                       "GET\n\n\n{$expires}\n{$spath}",
-                                       $this->rgwS3SecretKey,
-                                       true // raw
-                               ) );
-                               // See http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
-                               // Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
-                               return wfAppendQuery(
-                                       str_replace( '/swift/v1', '', // S3 API is the rgw default
-                                               $this->storageUrl( $auth ) . $spath ),
-                                       [
-                                               'Signature' => $signature,
-                                               'Expires' => $expires,
-                                               'AWSAccessKeyId' => $this->rgwS3AccessKey ]
-                               );
-                       }
-               }
-
-               return null;
-       }
-
-       protected function directoriesAreVirtual() {
-               return true;
-       }
-
-       /**
-        * Get headers to send to Swift when reading a file based
-        * on a FileBackend params array, e.g. that of getLocalCopy().
-        * $params is currently only checked for a 'latest' flag.
-        *
-        * @param array $params
-        * @return array
-        */
-       protected function headersFromParams( array $params ) {
-               $hdrs = [];
-               if ( !empty( $params['latest'] ) ) {
-                       $hdrs['x-newest'] = 'true';
-               }
-
-               return $hdrs;
-       }
-
-       /**
-        * @param FileBackendStoreOpHandle[] $fileOpHandles
-        *
-        * @return StatusValue[]
-        */
-       protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
-               $statuses = [];
-
-               $auth = $this->getAuthentication();
-               if ( !$auth ) {
-                       foreach ( $fileOpHandles as $index => $fileOpHandle ) {
-                               $statuses[$index] = $this->newStatus( 'backend-fail-connect', $this->name );
-                       }
-
-                       return $statuses;
-               }
-
-               // Split the HTTP requests into stages that can be done concurrently
-               $httpReqsByStage = []; // map of (stage => index => HTTP request)
-               foreach ( $fileOpHandles as $index => $fileOpHandle ) {
-                       $reqs = $fileOpHandle->httpOp;
-                       // Convert the 'url' parameter to an actual URL using $auth
-                       foreach ( $reqs as $stage => &$req ) {
-                               list( $container, $relPath ) = $req['url'];
-                               $req['url'] = $this->storageUrl( $auth, $container, $relPath );
-                               $req['headers'] = isset( $req['headers'] ) ? $req['headers'] : [];
-                               $req['headers'] = $this->authTokenHeaders( $auth ) + $req['headers'];
-                               $httpReqsByStage[$stage][$index] = $req;
-                       }
-                       $statuses[$index] = $this->newStatus();
-               }
-
-               // Run all requests for the first stage, then the next, and so on
-               $reqCount = count( $httpReqsByStage );
-               for ( $stage = 0; $stage < $reqCount; ++$stage ) {
-                       $httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] );
-                       foreach ( $httpReqs as $index => $httpReq ) {
-                               // Run the callback for each request of this operation
-                               $callback = $fileOpHandles[$index]->callback;
-                               call_user_func_array( $callback, [ $httpReq, $statuses[$index] ] );
-                               // On failure, abort all remaining requests for this operation
-                               // (e.g. abort the DELETE request if the COPY request fails for a move)
-                               if ( !$statuses[$index]->isOK() ) {
-                                       $stages = count( $fileOpHandles[$index]->httpOp );
-                                       for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
-                                               unset( $httpReqsByStage[$s][$index] );
-                                       }
-                               }
-                       }
-               }
-
-               return $statuses;
-       }
-
-       /**
-        * Set read/write permissions for a Swift container.
-        *
-        * @see http://swift.openstack.org/misc.html#acls
-        *
-        * In general, we don't allow listings to end-users. It's not useful, isn't well-defined
-        * (lists are truncated to 10000 item with no way to page), and is just a performance risk.
-        *
-        * @param string $container Resolved Swift container
-        * @param array $readGrps List of the possible criteria for a request to have
-        * access to read a container. Each item is one of the following formats:
-        *   - account:user        : Grants access if the request is by the given user
-        *   - ".r:<regex>"        : Grants access if the request is from a referrer host that
-        *                           matches the expression and the request is not for a listing.
-        *                           Setting this to '*' effectively makes a container public.
-        *   -".rlistings:<regex>" : Grants access if the request is from a referrer host that
-        *                           matches the expression and the request is for a listing.
-        * @param array $writeGrps A list of the possible criteria for a request to have
-        * access to write to a container. Each item is of the following format:
-        *   - account:user       : Grants access if the request is by the given user
-        * @return StatusValue
-        */
-       protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
-               $status = $this->newStatus();
-               $auth = $this->getAuthentication();
-
-               if ( !$auth ) {
-                       $status->fatal( 'backend-fail-connect', $this->name );
-
-                       return $status;
-               }
-
-               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
-                       'method' => 'POST',
-                       'url' => $this->storageUrl( $auth, $container ),
-                       'headers' => $this->authTokenHeaders( $auth ) + [
-                               'x-container-read' => implode( ',', $readGrps ),
-                               'x-container-write' => implode( ',', $writeGrps )
-                       ]
-               ] );
-
-               if ( $rcode != 204 && $rcode !== 202 ) {
-                       $status->fatal( 'backend-fail-internal', $this->name );
-                       wfDebugLog( 'SwiftBackend', __METHOD__ . ': unexpected rcode value (' . $rcode . ')' );
-               }
-
-               return $status;
-       }
-
-       /**
-        * Get a Swift container stat array, possibly from process cache.
-        * Use $reCache if the file count or byte count is needed.
-        *
-        * @param string $container Container name
-        * @param bool $bypassCache Bypass all caches and load from Swift
-        * @return array|bool|null False on 404, null on failure
-        */
-       protected function getContainerStat( $container, $bypassCache = false ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
-               if ( $bypassCache ) { // purge cache
-                       $this->containerStatCache->clear( $container );
-               } elseif ( !$this->containerStatCache->has( $container, 'stat' ) ) {
-                       $this->primeContainerCache( [ $container ] ); // check persistent cache
-               }
-               if ( !$this->containerStatCache->has( $container, 'stat' ) ) {
-                       $auth = $this->getAuthentication();
-                       if ( !$auth ) {
-                               return null;
-                       }
-
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
-                               'method' => 'HEAD',
-                               'url' => $this->storageUrl( $auth, $container ),
-                               'headers' => $this->authTokenHeaders( $auth )
-                       ] );
-
-                       if ( $rcode === 204 ) {
-                               $stat = [
-                                       'count' => $rhdrs['x-container-object-count'],
-                                       'bytes' => $rhdrs['x-container-bytes-used']
-                               ];
-                               if ( $bypassCache ) {
-                                       return $stat;
-                               } else {
-                                       $this->containerStatCache->set( $container, 'stat', $stat ); // cache it
-                                       $this->setContainerCache( $container, $stat ); // update persistent cache
-                               }
-                       } elseif ( $rcode === 404 ) {
-                               return false;
-                       } else {
-                               $this->onError( null, __METHOD__,
-                                       [ 'cont' => $container ], $rerr, $rcode, $rdesc );
-
-                               return null;
-                       }
-               }
-
-               return $this->containerStatCache->get( $container, 'stat' );
-       }
-
-       /**
-        * Create a Swift container
-        *
-        * @param string $container Container name
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function createContainer( $container, array $params ) {
-               $status = $this->newStatus();
-
-               $auth = $this->getAuthentication();
-               if ( !$auth ) {
-                       $status->fatal( 'backend-fail-connect', $this->name );
-
-                       return $status;
-               }
-
-               // @see SwiftFileBackend::setContainerAccess()
-               if ( empty( $params['noAccess'] ) ) {
-                       $readGrps = [ '.r:*', $this->swiftUser ]; // public
-               } else {
-                       $readGrps = [ $this->swiftUser ]; // private
-               }
-               $writeGrps = [ $this->swiftUser ]; // sanity
-
-               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
-                       'method' => 'PUT',
-                       'url' => $this->storageUrl( $auth, $container ),
-                       'headers' => $this->authTokenHeaders( $auth ) + [
-                               'x-container-read' => implode( ',', $readGrps ),
-                               'x-container-write' => implode( ',', $writeGrps )
-                       ]
-               ] );
-
-               if ( $rcode === 201 ) { // new
-                       // good
-               } elseif ( $rcode === 202 ) { // already there
-                       // this shouldn't really happen, but is OK
-               } else {
-                       $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
-               }
-
-               return $status;
-       }
-
-       /**
-        * Delete a Swift container
-        *
-        * @param string $container Container name
-        * @param array $params
-        * @return StatusValue
-        */
-       protected function deleteContainer( $container, array $params ) {
-               $status = $this->newStatus();
-
-               $auth = $this->getAuthentication();
-               if ( !$auth ) {
-                       $status->fatal( 'backend-fail-connect', $this->name );
-
-                       return $status;
-               }
-
-               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
-                       'method' => 'DELETE',
-                       'url' => $this->storageUrl( $auth, $container ),
-                       'headers' => $this->authTokenHeaders( $auth )
-               ] );
-
-               if ( $rcode >= 200 && $rcode <= 299 ) { // deleted
-                       $this->containerStatCache->clear( $container ); // purge
-               } elseif ( $rcode === 404 ) { // not there
-                       // this shouldn't really happen, but is OK
-               } elseif ( $rcode === 409 ) { // not empty
-                       $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc ); // race?
-               } else {
-                       $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
-               }
-
-               return $status;
-       }
-
-       /**
-        * Get a list of objects under a container.
-        * Either just the names or a list of stdClass objects with details can be returned.
-        *
-        * @param string $fullCont
-        * @param string $type ('info' for a list of object detail maps, 'names' for names only)
-        * @param int $limit
-        * @param string|null $after
-        * @param string|null $prefix
-        * @param string|null $delim
-        * @return StatusValue With the list as value
-        */
-       private function objectListing(
-               $fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
-       ) {
-               $status = $this->newStatus();
-
-               $auth = $this->getAuthentication();
-               if ( !$auth ) {
-                       $status->fatal( 'backend-fail-connect', $this->name );
-
-                       return $status;
-               }
-
-               $query = [ 'limit' => $limit ];
-               if ( $type === 'info' ) {
-                       $query['format'] = 'json';
-               }
-               if ( $after !== null ) {
-                       $query['marker'] = $after;
-               }
-               if ( $prefix !== null ) {
-                       $query['prefix'] = $prefix;
-               }
-               if ( $delim !== null ) {
-                       $query['delimiter'] = $delim;
-               }
-
-               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
-                       'method' => 'GET',
-                       'url' => $this->storageUrl( $auth, $fullCont ),
-                       'query' => $query,
-                       'headers' => $this->authTokenHeaders( $auth )
-               ] );
-
-               $params = [ 'cont' => $fullCont, 'prefix' => $prefix, 'delim' => $delim ];
-               if ( $rcode === 200 ) { // good
-                       if ( $type === 'info' ) {
-                               $status->value = FormatJson::decode( trim( $rbody ) );
-                       } else {
-                               $status->value = explode( "\n", trim( $rbody ) );
-                       }
-               } elseif ( $rcode === 204 ) {
-                       $status->value = []; // empty container
-               } elseif ( $rcode === 404 ) {
-                       $status->value = []; // no container
-               } else {
-                       $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
-               }
-
-               return $status;
-       }
-
-       protected function doPrimeContainerCache( array $containerInfo ) {
-               foreach ( $containerInfo as $container => $info ) {
-                       $this->containerStatCache->set( $container, 'stat', $info );
-               }
-       }
-
-       protected function doGetFileStatMulti( array $params ) {
-               $stats = [];
-
-               $auth = $this->getAuthentication();
-
-               $reqs = [];
-               foreach ( $params['srcs'] as $path ) {
-                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
-                       if ( $srcRel === null ) {
-                               $stats[$path] = false;
-                               continue; // invalid storage path
-                       } elseif ( !$auth ) {
-                               $stats[$path] = null;
-                               continue;
-                       }
-
-                       // (a) Check the container
-                       $cstat = $this->getContainerStat( $srcCont );
-                       if ( $cstat === false ) {
-                               $stats[$path] = false;
-                               continue; // ok, nothing to do
-                       } elseif ( !is_array( $cstat ) ) {
-                               $stats[$path] = null;
-                               continue;
-                       }
-
-                       $reqs[$path] = [
-                               'method'  => 'HEAD',
-                               'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
-                               'headers' => $this->authTokenHeaders( $auth ) + $this->headersFromParams( $params )
-                       ];
-               }
-
-               $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
-               $reqs = $this->http->runMulti( $reqs, $opts );
-
-               foreach ( $params['srcs'] as $path ) {
-                       if ( array_key_exists( $path, $stats ) ) {
-                               continue; // some sort of failure above
-                       }
-                       // (b) Check the file
-                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
-                       if ( $rcode === 200 || $rcode === 204 ) {
-                               // Update the object if it is missing some headers
-                               $rhdrs = $this->addMissingMetadata( $rhdrs, $path );
-                               // Load the stat array from the headers
-                               $stat = $this->getStatFromHeaders( $rhdrs );
-                               if ( $this->isRGW ) {
-                                       $stat['latest'] = true; // strong consistency
-                               }
-                       } elseif ( $rcode === 404 ) {
-                               $stat = false;
-                       } else {
-                               $stat = null;
-                               $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
-                       }
-                       $stats[$path] = $stat;
-               }
-
-               return $stats;
-       }
-
-       /**
-        * @param array $rhdrs
-        * @return array
-        */
-       protected function getStatFromHeaders( array $rhdrs ) {
-               // Fetch all of the custom metadata headers
-               $metadata = $this->getMetadata( $rhdrs );
-               // Fetch all of the custom raw HTTP headers
-               $headers = $this->sanitizeHdrs( [ 'headers' => $rhdrs ] );
-
-               return [
-                       // Convert various random Swift dates to TS_MW
-                       'mtime' => $this->convertSwiftDate( $rhdrs['last-modified'], TS_MW ),
-                       // Empty objects actually return no content-length header in Ceph
-                       'size'  => isset( $rhdrs['content-length'] ) ? (int)$rhdrs['content-length'] : 0,
-                       'sha1'  => isset( $metadata['sha1base36'] ) ? $metadata['sha1base36'] : null,
-                       // Note: manifiest ETags are not an MD5 of the file
-                       'md5'   => ctype_xdigit( $rhdrs['etag'] ) ? $rhdrs['etag'] : null,
-                       'xattr' => [ 'metadata' => $metadata, 'headers' => $headers ]
-               ];
-       }
-
-       /**
-        * @return array|null Credential map
-        */
-       protected function getAuthentication() {
-               if ( $this->authErrorTimestamp !== null ) {
-                       if ( ( time() - $this->authErrorTimestamp ) < 60 ) {
-                               return null; // failed last attempt; don't bother
-                       } else { // actually retry this time
-                               $this->authErrorTimestamp = null;
-                       }
-               }
-               // Session keys expire after a while, so we renew them periodically
-               $reAuth = ( ( time() - $this->authSessionTimestamp ) > $this->authTTL );
-               // Authenticate with proxy and get a session key...
-               if ( !$this->authCreds || $reAuth ) {
-                       $this->authSessionTimestamp = 0;
-                       $cacheKey = $this->getCredsCacheKey( $this->swiftUser );
-                       $creds = $this->srvCache->get( $cacheKey ); // credentials
-                       // Try to use the credential cache
-                       if ( isset( $creds['auth_token'] ) && isset( $creds['storage_url'] ) ) {
-                               $this->authCreds = $creds;
-                               // Skew the timestamp for worst case to avoid using stale credentials
-                               $this->authSessionTimestamp = time() - ceil( $this->authTTL / 2 );
-                       } else { // cache miss
-                               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
-                                       'method' => 'GET',
-                                       'url' => "{$this->swiftAuthUrl}/v1.0",
-                                       'headers' => [
-                                               'x-auth-user' => $this->swiftUser,
-                                               'x-auth-key' => $this->swiftKey
-                                       ]
-                               ] );
-
-                               if ( $rcode >= 200 && $rcode <= 299 ) { // OK
-                                       $this->authCreds = [
-                                               'auth_token' => $rhdrs['x-auth-token'],
-                                               'storage_url' => $rhdrs['x-storage-url']
-                                       ];
-                                       $this->srvCache->set( $cacheKey, $this->authCreds, ceil( $this->authTTL / 2 ) );
-                                       $this->authSessionTimestamp = time();
-                               } elseif ( $rcode === 401 ) {
-                                       $this->onError( null, __METHOD__, [], "Authentication failed.", $rcode );
-                                       $this->authErrorTimestamp = time();
-
-                                       return null;
-                               } else {
-                                       $this->onError( null, __METHOD__, [], "HTTP return code: $rcode", $rcode );
-                                       $this->authErrorTimestamp = time();
-
-                                       return null;
-                               }
-                       }
-                       // Ceph RGW does not use <account> in URLs (OpenStack Swift uses "/v1/<account>")
-                       if ( substr( $this->authCreds['storage_url'], -3 ) === '/v1' ) {
-                               $this->isRGW = true; // take advantage of strong consistency in Ceph
-                       }
-               }
-
-               return $this->authCreds;
-       }
-
-       /**
-        * @param array $creds From getAuthentication()
-        * @param string $container
-        * @param string $object
-        * @return array
-        */
-       protected function storageUrl( array $creds, $container = null, $object = null ) {
-               $parts = [ $creds['storage_url'] ];
-               if ( strlen( $container ) ) {
-                       $parts[] = rawurlencode( $container );
-               }
-               if ( strlen( $object ) ) {
-                       $parts[] = str_replace( "%2F", "/", rawurlencode( $object ) );
-               }
-
-               return implode( '/', $parts );
-       }
-
-       /**
-        * @param array $creds From getAuthentication()
-        * @return array
-        */
-       protected function authTokenHeaders( array $creds ) {
-               return [ 'x-auth-token' => $creds['auth_token'] ];
-       }
-
-       /**
-        * Get the cache key for a container
-        *
-        * @param string $username
-        * @return string
-        */
-       private function getCredsCacheKey( $username ) {
-               return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl );
-       }
-
-       /**
-        * Log an unexpected exception for this backend.
-        * This also sets the StatusValue object to have a fatal error.
-        *
-        * @param StatusValue|null $status
-        * @param string $func
-        * @param array $params
-        * @param string $err Error string
-        * @param int $code HTTP status
-        * @param string $desc HTTP StatusValue description
-        */
-       public function onError( $status, $func, array $params, $err = '', $code = 0, $desc = '' ) {
-               if ( $status instanceof StatusValue ) {
-                       $status->fatal( 'backend-fail-internal', $this->name );
-               }
-               if ( $code == 401 ) { // possibly a stale token
-                       $this->srvCache->delete( $this->getCredsCacheKey( $this->swiftUser ) );
-               }
-               wfDebugLog( 'SwiftBackend',
-                       "HTTP $code ($desc) in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
-                       ( $err ? ": $err" : "" )
-               );
-       }
-}
-
-/**
- * @see FileBackendStoreOpHandle
- */
-class SwiftFileOpHandle extends FileBackendStoreOpHandle {
-       /** @var array List of Requests for MultiHttpClient */
-       public $httpOp;
-       /** @var Closure */
-       public $callback;
-
-       /**
-        * @param SwiftFileBackend $backend
-        * @param Closure $callback Function that takes (HTTP request array, status)
-        * @param array $httpOp MultiHttpClient op
-        */
-       public function __construct( SwiftFileBackend $backend, Closure $callback, array $httpOp ) {
-               $this->backend = $backend;
-               $this->callback = $callback;
-               $this->httpOp = $httpOp;
-       }
-}
-
-/**
- * SwiftFileBackend helper class to page through listings.
- * Swift also has a listing limit of 10,000 objects for sanity.
- * Do not use this class from places outside SwiftFileBackend.
- *
- * @ingroup FileBackend
- */
-abstract class SwiftFileBackendList implements Iterator {
-       /** @var array List of path or (path,stat array) entries */
-       protected $bufferIter = [];
-
-       /** @var string List items *after* this path */
-       protected $bufferAfter = null;
-
-       /** @var int */
-       protected $pos = 0;
-
-       /** @var array */
-       protected $params = [];
-
-       /** @var SwiftFileBackend */
-       protected $backend;
-
-       /** @var string Container name */
-       protected $container;
-
-       /** @var string Storage directory */
-       protected $dir;
-
-       /** @var int */
-       protected $suffixStart;
-
-       const PAGE_SIZE = 9000; // file listing buffer size
-
-       /**
-        * @param SwiftFileBackend $backend
-        * @param string $fullCont Resolved container name
-        * @param string $dir Resolved directory relative to container
-        * @param array $params
-        */
-       public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
-               $this->backend = $backend;
-               $this->container = $fullCont;
-               $this->dir = $dir;
-               if ( substr( $this->dir, -1 ) === '/' ) {
-                       $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
-               }
-               if ( $this->dir == '' ) { // whole container
-                       $this->suffixStart = 0;
-               } else { // dir within container
-                       $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
-               }
-               $this->params = $params;
-       }
-
-       /**
-        * @see Iterator::key()
-        * @return int
-        */
-       public function key() {
-               return $this->pos;
-       }
-
-       /**
-        * @see Iterator::next()
-        */
-       public function next() {
-               // Advance to the next file in the page
-               next( $this->bufferIter );
-               ++$this->pos;
-               // Check if there are no files left in this page and
-               // advance to the next page if this page was not empty.
-               if ( !$this->valid() && count( $this->bufferIter ) ) {
-                       $this->bufferIter = $this->pageFromList(
-                               $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
-                       ); // updates $this->bufferAfter
-               }
-       }
-
-       /**
-        * @see Iterator::rewind()
-        */
-       public function rewind() {
-               $this->pos = 0;
-               $this->bufferAfter = null;
-               $this->bufferIter = $this->pageFromList(
-                       $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
-               ); // updates $this->bufferAfter
-       }
-
-       /**
-        * @see Iterator::valid()
-        * @return bool
-        */
-       public function valid() {
-               if ( $this->bufferIter === null ) {
-                       return false; // some failure?
-               } else {
-                       return ( current( $this->bufferIter ) !== false ); // no paths can have this value
-               }
-       }
-
-       /**
-        * Get the given list portion (page)
-        *
-        * @param string $container Resolved container name
-        * @param string $dir Resolved path relative to container
-        * @param string $after
-        * @param int $limit
-        * @param array $params
-        * @return Traversable|array
-        */
-       abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
-}
-
-/**
- * Iterator for listing directories
- */
-class SwiftFileBackendDirList extends SwiftFileBackendList {
-       /**
-        * @see Iterator::current()
-        * @return string|bool String (relative path) or false
-        */
-       public function current() {
-               return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
-       }
-
-       protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
-               return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
-       }
-}
-
-/**
- * Iterator for listing regular files
- */
-class SwiftFileBackendFileList extends SwiftFileBackendList {
-       /**
-        * @see Iterator::current()
-        * @return string|bool String (relative path) or false
-        */
-       public function current() {
-               list( $path, $stat ) = current( $this->bufferIter );
-               $relPath = substr( $path, $this->suffixStart );
-               if ( is_array( $stat ) ) {
-                       $storageDir = rtrim( $this->params['dir'], '/' );
-                       $this->backend->loadListingStatInternal( "$storageDir/$relPath", $stat );
-               }
-
-               return $relPath;
-       }
-
-       protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
-               return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
-       }
-}
diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php
deleted file mode 100644 (file)
index 4667dde..0000000
+++ /dev/null
@@ -1,246 +0,0 @@
-<?php
-/**
- * Version of LockManager based on using DB table locks.
- *
- * 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 LockManager
- */
-
-/**
- * Version of LockManager based on using named/row DB locks.
- *
- * This is meant for multi-wiki systems that may share files.
- *
- * All lock requests for a resource, identified by a hash string, will map to one bucket.
- * Each bucket maps to one or several peer DBs, each on their own server.
- * A majority of peer DBs must agree for a lock to be acquired.
- *
- * Caching is used to avoid hitting servers that are down.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-abstract class DBLockManager extends QuorumLockManager {
-       /** @var array[] Map of DB names to server config */
-       protected $dbServers; // (DB name => server config array)
-       /** @var BagOStuff */
-       protected $statusCache;
-
-       protected $lockExpiry; // integer number of seconds
-       protected $safeDelay; // integer number of seconds
-
-       protected $session = 0; // random integer
-       /** @var IDatabase[] Map Database connections (DB name => Database) */
-       protected $conns = [];
-
-       /**
-        * Construct a new instance from configuration.
-        *
-        * @param array $config Parameters include:
-        *   - dbServers   : Associative array of DB names to server configuration.
-        *                   Configuration is an associative array that includes:
-        *                     - host        : DB server name
-        *                     - dbname      : DB name
-        *                     - type        : DB type (mysql,postgres,...)
-        *                     - user        : DB user
-        *                     - password    : DB user password
-        *                     - tablePrefix : DB table prefix
-        *                     - flags       : DB flags (see DatabaseBase)
-        *   - dbsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
-        *                   each having an odd-numbered list of DB names (peers) as values.
-        *                   Any DB named 'localDBMaster' will automatically use the DB master
-        *                   settings for this wiki (without the need for a dbServers entry).
-        *                   Only use 'localDBMaster' if the domain is a valid wiki ID.
-        *   - lockExpiry  : Lock timeout (seconds) for dropped connections. [optional]
-        *                   This tells the DB server how long to wait before assuming
-        *                   connection failure and releasing all the locks for a session.
-        */
-       public function __construct( array $config ) {
-               parent::__construct( $config );
-
-               $this->dbServers = isset( $config['dbServers'] )
-                       ? $config['dbServers']
-                       : []; // likely just using 'localDBMaster'
-               // Sanitize srvsByBucket config to prevent PHP errors
-               $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' );
-               $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
-
-               if ( isset( $config['lockExpiry'] ) ) {
-                       $this->lockExpiry = $config['lockExpiry'];
-               } else {
-                       $met = ini_get( 'max_execution_time' );
-                       $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0
-               }
-               $this->safeDelay = ( $this->lockExpiry <= 0 )
-                       ? 60 // pick a safe-ish number to match DB timeout default
-                       : $this->lockExpiry; // cover worst case
-
-               foreach ( $this->srvsByBucket as $bucket ) {
-                       if ( count( $bucket ) > 1 ) { // multiple peers
-                               // Tracks peers that couldn't be queried recently to avoid lengthy
-                               // connection timeouts. This is useless if each bucket has one peer.
-                               $this->statusCache = ObjectCache::getLocalServerInstance();
-                               break;
-                       }
-               }
-
-               $this->session = wfRandomString( 31 );
-       }
-
-       // @todo change this code to work in one batch
-       protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = StatusValue::newGood();
-               foreach ( $pathsByType as $type => $paths ) {
-                       $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
-               }
-
-               return $status;
-       }
-
-       abstract protected function doGetLocksOnServer( $lockSrv, array $paths, $type );
-
-       protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
-               return StatusValue::newGood();
-       }
-
-       /**
-        * @see QuorumLockManager::isServerUp()
-        * @param string $lockSrv
-        * @return bool
-        */
-       protected function isServerUp( $lockSrv ) {
-               if ( !$this->cacheCheckFailures( $lockSrv ) ) {
-                       return false; // recent failure to connect
-               }
-               try {
-                       $this->getConnection( $lockSrv );
-               } catch ( DBError $e ) {
-                       $this->cacheRecordFailure( $lockSrv );
-
-                       return false; // failed to connect
-               }
-
-               return true;
-       }
-
-       /**
-        * Get (or reuse) a connection to a lock DB
-        *
-        * @param string $lockDb
-        * @return IDatabase
-        * @throws DBError
-        * @throws UnexpectedValueException
-        */
-       protected function getConnection( $lockDb ) {
-               if ( !isset( $this->conns[$lockDb] ) ) {
-                       if ( $lockDb === 'localDBMaster' ) {
-                               $lb = $this->getLocalLB();
-                               $db = $lb->getConnection( DB_MASTER, [], $this->domain );
-                               # Do not mess with settings if the LoadBalancer is the main singleton
-                               # to avoid clobbering the settings of handles from wfGetDB( DB_MASTER ).
-                               $init = ( wfGetLB() !== $lb );
-                       } elseif ( isset( $this->dbServers[$lockDb] ) ) {
-                               $config = $this->dbServers[$lockDb];
-                               $db = DatabaseBase::factory( $config['type'], $config );
-                               $init = true;
-                       } else {
-                               throw new UnexpectedValueException( "No server called '$lockDb'." );
-                       }
-
-                       if ( $init ) {
-                               $db->clearFlag( DBO_TRX );
-                               # If the connection drops, try to avoid letting the DB rollback
-                               # and release the locks before the file operations are finished.
-                               # This won't handle the case of DB server restarts however.
-                               $options = [];
-                               if ( $this->lockExpiry > 0 ) {
-                                       $options['connTimeout'] = $this->lockExpiry;
-                               }
-                               $db->setSessionOptions( $options );
-                               $this->initConnection( $lockDb, $db );
-                       }
-
-                       $this->conns[$lockDb] = $db;
-               }
-
-               return $this->conns[$lockDb];
-       }
-
-       /**
-        * @return LoadBalancer
-        */
-       protected function getLocalLB() {
-               return wfGetLBFactory()->getMainLB( $this->domain );
-       }
-
-       /**
-        * Do additional initialization for new lock DB connection
-        *
-        * @param string $lockDb
-        * @param IDatabase $db
-        * @throws DBError
-        */
-       protected function initConnection( $lockDb, IDatabase $db ) {
-       }
-
-       /**
-        * Checks if the DB has not recently had connection/query errors.
-        * This just avoids wasting time on doomed connection attempts.
-        *
-        * @param string $lockDb
-        * @return bool
-        */
-       protected function cacheCheckFailures( $lockDb ) {
-               return ( $this->statusCache && $this->safeDelay > 0 )
-                       ? !$this->statusCache->get( $this->getMissKey( $lockDb ) )
-                       : true;
-       }
-
-       /**
-        * Log a lock request failure to the cache
-        *
-        * @param string $lockDb
-        * @return bool Success
-        */
-       protected function cacheRecordFailure( $lockDb ) {
-               return ( $this->statusCache && $this->safeDelay > 0 )
-                       ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay )
-                       : true;
-       }
-
-       /**
-        * Get a cache key for recent query misses for a DB
-        *
-        * @param string $lockDb
-        * @return string
-        */
-       protected function getMissKey( $lockDb ) {
-               $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative
-               return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb );
-       }
-
-       /**
-        * Make sure remaining locks get cleared for sanity
-        */
-       function __destruct() {
-               $this->releaseAllLocks();
-               foreach ( $this->conns as $db ) {
-                       $db->close();
-               }
-       }
-}
index 602b876..1e66e6e 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  * @ingroup LockManager
  */
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Logger\LoggerFactory;
 
 /**
  * Class to handle file lock manager registration
@@ -29,7 +31,7 @@
  * @since 1.19
  */
 class LockManagerGroup {
-       /** @var array (domain => LockManager) */
+       /** @var LockManagerGroup[] (domain => LockManagerGroup) */
        protected static $instances = [];
 
        protected $domain; // string; domain (usually wiki ID)
@@ -115,6 +117,16 @@ class LockManagerGroup {
                if ( !isset( $this->managers[$name]['instance'] ) ) {
                        $class = $this->managers[$name]['class'];
                        $config = $this->managers[$name]['config'];
+                       if ( $class === 'DBLockManager' ) {
+                               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+                               $lb = $lbFactory->newMainLB( $config['domain'] );
+                               $dbw = $lb->getLazyConnectionRef( DB_MASTER, [], $config['domain'] );
+
+                               $config['dbServers']['localDBMaster'] = $dbw;
+                               $config['srvCache'] = ObjectCache::getLocalServerInstance( 'hash' );
+                       }
+                       $config['logger'] = LoggerFactory::getInstance( 'LockManager' );
+
                        $this->managers[$name]['instance'] = new $class( $config );
                }
 
index 124d410..fc23f76 100644 (file)
@@ -2,7 +2,11 @@
 /**
  * MySQL version of DBLockManager that supports shared locks.
  *
- * All lock servers must have the innodb table defined in locking/filelocks.sql.
+ * Do NOT use this on connection handles that are also being used for anything
+ * else as the transaction isolation will be wrong and all the other changes will
+ * get rolled back when the locks release!
+ *
+ * All lock servers must have the innodb table defined in maintenance/locking/filelocks.sql.
  * All locks are non-blocking, which avoids deadlocks.
  *
  * @ingroup LockManager
@@ -15,9 +19,10 @@ class MySqlLockManager extends DBLockManager {
                self::LOCK_EX => self::LOCK_EX
        ];
 
-       protected function getLocalLB() {
-               // Use a separate connection so releaseAllLocks() doesn't rollback the main trx
-               return wfGetLBFactory()->newMainLB( $this->domain );
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+
+               $this->session = substr( $this->session, 0, 31 ); // fit to field
        }
 
        protected function initConnection( $lockDb, IDatabase $db ) {
@@ -51,7 +56,7 @@ class MySqlLockManager extends DBLockManager {
                        $keys[] = $key;
                        $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ];
                        if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
-                               $checkEXKeys[] = $key;
+                               $checkEXKeys[] = $key; // this has no EX lock on $key itself
                        }
                }
 
@@ -59,13 +64,16 @@ class MySqlLockManager extends DBLockManager {
                $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] );
                # Actually do the locking queries...
                if ( $type == self::LOCK_SH ) { // reader locks
-                       $blocked = false;
                        # Bail if there are any existing writers...
                        if ( count( $checkEXKeys ) ) {
-                               $blocked = $db->selectField( 'filelocks_exclusive', '1',
+                               $blocked = $db->selectField(
+                                       'filelocks_exclusive',
+                                       '1',
                                        [ 'fle_key' => $checkEXKeys ],
                                        __METHOD__
                                );
+                       } else {
+                               $blocked = false;
                        }
                        # Other prospective writers that haven't yet updated filelocks_exclusive
                        # will recheck filelocks_shared after doing so and bail due to this entry.
@@ -74,7 +82,9 @@ class MySqlLockManager extends DBLockManager {
                        # Bail if there are any existing writers...
                        # This may detect readers, but the safe check for them is below.
                        # Note: if two writers come at the same time, both bail :)
-                       $blocked = $db->selectField( 'filelocks_shared', '1',
+                       $blocked = $db->selectField(
+                               'filelocks_shared',
+                               '1',
                                [ 'fls_key' => $keys, "fls_session != $encSession" ],
                                __METHOD__
                        );
@@ -87,7 +97,9 @@ class MySqlLockManager extends DBLockManager {
                                # Block new readers/writers...
                                $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
                                # Bail if there are any existing readers...
-                               $blocked = $db->selectField( 'filelocks_shared', '1',
+                               $blocked = $db->selectField(
+                                       'filelocks_shared',
+                                       '1',
                                        [ 'fls_key' => $keys, "fls_session != $encSession" ],
                                        __METHOD__
                                );
diff --git a/includes/filebackend/lockmanager/PostgreSqlLockManager.php b/includes/filebackend/lockmanager/PostgreSqlLockManager.php
deleted file mode 100644 (file)
index d6b1ce8..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-/**
- * PostgreSQL version of DBLockManager that supports shared locks.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * @ingroup LockManager
- */
-class PostgreSqlLockManager extends DBLockManager {
-       /** @var array Mapping of lock types to the type actually used */
-       protected $lockTypeMap = [
-               self::LOCK_SH => self::LOCK_SH,
-               self::LOCK_UW => self::LOCK_SH,
-               self::LOCK_EX => self::LOCK_EX
-       ];
-
-       protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
-               $status = StatusValue::newGood();
-               if ( !count( $paths ) ) {
-                       return $status; // nothing to lock
-               }
-
-               $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
-               $bigints = array_unique( array_map(
-                       function ( $key ) {
-                               return Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 );
-                       },
-                       array_map( [ $this, 'sha1Base16Absolute' ], $paths )
-               ) );
-
-               // Try to acquire all the locks...
-               $fields = [];
-               foreach ( $bigints as $bigint ) {
-                       $fields[] = ( $type == self::LOCK_SH )
-                               ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
-                               : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
-               }
-               $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
-               $row = $res->fetchRow();
-
-               if ( in_array( 'f', $row ) ) {
-                       // Release any acquired locks if some could not be acquired...
-                       $fields = [];
-                       foreach ( $row as $kbigint => $ok ) {
-                               if ( $ok === 't' ) { // locked
-                                       $bigint = substr( $kbigint, 1 ); // strip off the "K"
-                                       $fields[] = ( $type == self::LOCK_SH )
-                                               ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
-                                               : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
-                               }
-                       }
-                       if ( count( $fields ) ) {
-                               $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
-                       }
-                       foreach ( $paths as $path ) {
-                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see QuorumLockManager::releaseAllLocks()
-        * @return StatusValue
-        */
-       protected function releaseAllLocks() {
-               $status = StatusValue::newGood();
-
-               foreach ( $this->conns as $lockDb => $db ) {
-                       try {
-                               $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
-                       } catch ( DBError $e ) {
-                               $status->fatal( 'lockmanager-fail-db-release', $lockDb );
-                       }
-               }
-
-               return $status;
-       }
-}
diff --git a/includes/filebackend/lockmanager/RedisLockManager.php b/includes/filebackend/lockmanager/RedisLockManager.php
deleted file mode 100644 (file)
index 6fd819d..0000000
+++ /dev/null
@@ -1,276 +0,0 @@
-<?php
-/**
- * Version of LockManager based on using redis servers.
- *
- * 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 LockManager
- */
-
-/**
- * Manage locks using redis servers.
- *
- * Version of LockManager based on using redis servers.
- * This is meant for multi-wiki systems that may share files.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * All lock requests for a resource, identified by a hash string, will map to one
- * bucket. Each bucket maps to one or several peer servers, each running redis.
- * A majority of peers must agree for a lock to be acquired.
- *
- * This class requires Redis 2.6 as it makes use Lua scripts for fast atomic operations.
- *
- * @ingroup LockManager
- * @since 1.22
- */
-class RedisLockManager extends QuorumLockManager {
-       /** @var array Mapping of lock types to the type actually used */
-       protected $lockTypeMap = [
-               self::LOCK_SH => self::LOCK_SH,
-               self::LOCK_UW => self::LOCK_SH,
-               self::LOCK_EX => self::LOCK_EX
-       ];
-
-       /** @var RedisConnectionPool */
-       protected $redisPool;
-
-       /** @var array Map server names to hostname/IP and port numbers */
-       protected $lockServers = [];
-
-       /** @var string Random UUID */
-       protected $session = '';
-
-       /**
-        * Construct a new instance from configuration.
-        *
-        * @param array $config Parameters include:
-        *   - lockServers  : Associative array of server names to "<IP>:<port>" strings.
-        *   - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
-        *                    each having an odd-numbered list of server names (peers) as values.
-        *   - redisConfig  : Configuration for RedisConnectionPool::__construct().
-        * @throws Exception
-        */
-       public function __construct( array $config ) {
-               parent::__construct( $config );
-
-               $this->lockServers = $config['lockServers'];
-               // Sanitize srvsByBucket config to prevent PHP errors
-               $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
-               $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
-
-               $config['redisConfig']['serializer'] = 'none';
-               $this->redisPool = RedisConnectionPool::singleton( $config['redisConfig'] );
-
-               $this->session = wfRandomString( 32 );
-       }
-
-       protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = StatusValue::newGood();
-
-               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
-
-               $server = $this->lockServers[$lockSrv];
-               $conn = $this->redisPool->getConnection( $server );
-               if ( !$conn ) {
-                       foreach ( $pathList as $path ) {
-                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                       }
-
-                       return $status;
-               }
-
-               $pathsByKey = []; // (type:hash => path) map
-               foreach ( $pathsByType as $type => $paths ) {
-                       $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
-                       foreach ( $paths as $path ) {
-                               $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
-                       }
-               }
-
-               try {
-                       static $script =
-<<<LUA
-                       local failed = {}
-                       -- Load input params (e.g. session, ttl, time of request)
-                       local rSession, rTTL, rTime = unpack(ARGV)
-                       -- Check that all the locks can be acquired
-                       for i,requestKey in ipairs(KEYS) do
-                               local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
-                               local keyIsFree = true
-                               local currentLocks = redis.call('hKeys',resourceKey)
-                               for i,lockKey in ipairs(currentLocks) do
-                                       -- Get the type and session of this lock
-                                       local _, _, type, session = string.find(lockKey,"(%w+):(%w+)")
-                                       -- Check any locks that are not owned by this session
-                                       if session ~= rSession then
-                                               local lockExpiry = redis.call('hGet',resourceKey,lockKey)
-                                               if 1*lockExpiry < 1*rTime then
-                                                       -- Lock is stale, so just prune it out
-                                                       redis.call('hDel',resourceKey,lockKey)
-                                               elseif rType == 'EX' or type == 'EX' then
-                                                       keyIsFree = false
-                                                       break
-                                               end
-                                       end
-                               end
-                               if not keyIsFree then
-                                       failed[#failed+1] = requestKey
-                               end
-                       end
-                       -- If all locks could be acquired, then do so
-                       if #failed == 0 then
-                               for i,requestKey in ipairs(KEYS) do
-                                       local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
-                                       redis.call('hSet',resourceKey,rType .. ':' .. rSession,rTime + rTTL)
-                                       -- In addition to invalidation logic, be sure to garbage collect
-                                       redis.call('expire',resourceKey,rTTL)
-                               end
-                       end
-                       return failed
-LUA;
-                       $res = $conn->luaEval( $script,
-                               array_merge(
-                                       array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
-                                       [
-                                               $this->session, // ARGV[1]
-                                               $this->lockTTL, // ARGV[2]
-                                               time() // ARGV[3]
-                                       ]
-                               ),
-                               count( $pathsByKey ) # number of first argument(s) that are keys
-                       );
-               } catch ( RedisException $e ) {
-                       $res = false;
-                       $this->redisPool->handleError( $conn, $e );
-               }
-
-               if ( $res === false ) {
-                       foreach ( $pathList as $path ) {
-                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                       }
-               } else {
-                       foreach ( $res as $key ) {
-                               $status->fatal( 'lockmanager-fail-acquirelock', $pathsByKey[$key] );
-                       }
-               }
-
-               return $status;
-       }
-
-       protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = StatusValue::newGood();
-
-               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
-
-               $server = $this->lockServers[$lockSrv];
-               $conn = $this->redisPool->getConnection( $server );
-               if ( !$conn ) {
-                       foreach ( $pathList as $path ) {
-                               $status->fatal( 'lockmanager-fail-releaselock', $path );
-                       }
-
-                       return $status;
-               }
-
-               $pathsByKey = []; // (type:hash => path) map
-               foreach ( $pathsByType as $type => $paths ) {
-                       $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
-                       foreach ( $paths as $path ) {
-                               $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
-                       }
-               }
-
-               try {
-                       static $script =
-<<<LUA
-                       local failed = {}
-                       -- Load input params (e.g. session)
-                       local rSession = unpack(ARGV)
-                       for i,requestKey in ipairs(KEYS) do
-                               local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
-                               local released = redis.call('hDel',resourceKey,rType .. ':' .. rSession)
-                               if released > 0 then
-                                       -- Remove the whole structure if it is now empty
-                                       if redis.call('hLen',resourceKey) == 0 then
-                                               redis.call('del',resourceKey)
-                                       end
-                               else
-                                       failed[#failed+1] = requestKey
-                               end
-                       end
-                       return failed
-LUA;
-                       $res = $conn->luaEval( $script,
-                               array_merge(
-                                       array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
-                                       [
-                                               $this->session, // ARGV[1]
-                                       ]
-                               ),
-                               count( $pathsByKey ) # number of first argument(s) that are keys
-                       );
-               } catch ( RedisException $e ) {
-                       $res = false;
-                       $this->redisPool->handleError( $conn, $e );
-               }
-
-               if ( $res === false ) {
-                       foreach ( $pathList as $path ) {
-                               $status->fatal( 'lockmanager-fail-releaselock', $path );
-                       }
-               } else {
-                       foreach ( $res as $key ) {
-                               $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] );
-                       }
-               }
-
-               return $status;
-       }
-
-       protected function releaseAllLocks() {
-               return StatusValue::newGood(); // not supported
-       }
-
-       protected function isServerUp( $lockSrv ) {
-               return (bool)$this->redisPool->getConnection( $this->lockServers[$lockSrv] );
-       }
-
-       /**
-        * @param string $path
-        * @param string $type One of (EX,SH)
-        * @return string
-        */
-       protected function recordKeyForPath( $path, $type ) {
-               return implode( ':',
-                       [ __CLASS__, 'locks', "$type:" . $this->sha1Base36Absolute( $path ) ] );
-       }
-
-       /**
-        * Make sure remaining locks get cleared for sanity
-        */
-       function __destruct() {
-               while ( count( $this->locksHeld ) ) {
-                       $pathsByType = [];
-                       foreach ( $this->locksHeld as $path => $locks ) {
-                               foreach ( $locks as $type => $count ) {
-                                       $pathsByType[$type][] = $path;
-                               }
-                       }
-                       $this->unlockByType( $pathsByType );
-               }
-       }
-}
index 645a59b..4176c82 100644 (file)
@@ -532,8 +532,8 @@ class ForeignAPIRepo extends FileRepo {
                $status = $req->execute();
 
                if ( $status->isOK() ) {
-                       $mtime = wfTimestampOrNull( TS_UNIX, $req->getResponseHeader( 'Last-Modified' ) );
-                       $mtime = $mtime ?: false;
+                       $lmod = $req->getResponseHeader( 'Last-Modified' );
+                       $mtime = $lmod ? wfTimestamp( TS_UNIX, $lmod ) : false;
 
                        return $req->getContent();
                } else {
index 567e692..c65d97f 100644 (file)
@@ -153,6 +153,9 @@ class HTMLForm extends ContextSource {
                'checkmatrix' => 'HTMLCheckMatrix',
                'cloner' => 'HTMLFormFieldCloner',
                'autocompleteselect' => 'HTMLAutoCompleteSelectField',
+               'date' => 'HTMLDateTimeField',
+               'time' => 'HTMLDateTimeField',
+               'datetime' => 'HTMLDateTimeField',
                // HTMLTextField will output the correct type="" attribute automagically.
                // There are about four zillion other HTML5 input types, like range, but
                // we don't use those at the moment, so no point in adding all of them.
index 8604ba2..4afdea7 100644 (file)
@@ -821,7 +821,7 @@ abstract class HTMLFormField {
        /**
         * Determine the help text to display
         * @since 1.20
-        * @return string HTML
+        * @return string|null HTML
         */
        public function getHelpText() {
                $helptext = null;
diff --git a/includes/htmlform/fields/HTMLDateTimeField.php b/includes/htmlform/fields/HTMLDateTimeField.php
new file mode 100644 (file)
index 0000000..66f89f9
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+
+/**
+ * A field that will contain a date and/or time
+ *
+ * Currently recognizes only {YYYY}-{MM}-{DD}T{HH}:{MM}:{SS.S*}Z formatted dates.
+ *
+ * Besides the parameters recognized by HTMLTextField, additional recognized
+ * parameters in the field descriptor array include:
+ *  type - 'date', 'time', or 'datetime'
+ *  min - The minimum date to allow, in any recognized format.
+ *  max - The maximum date to allow, in any recognized format.
+ *  placeholder - The default comes from the htmlform-(date|time|datetime)-placeholder message.
+ *
+ * The result is a formatted date.
+ *
+ * @note This widget is not likely to work well in non-OOUI forms.
+ */
+class HTMLDateTimeField extends HTMLTextField {
+       protected static $patterns = [
+               'date' => '[0-9]{4}-[01][0-9]-[0-3][0-9]',
+               'time' => '[0-2][0-9]:[0-5][0-9]:[0-5][0-9](?:\.[0-9]+)?',
+               'datetime' => '[0-9]{4}-[01][0-9]-[0-3][0-9][T ][0-2][0-9]:[0-5][0-9]:[0-5][0-9](?:\.[0-9]+)?Z?',
+       ];
+
+       protected $mType = 'datetime';
+
+       public function __construct( $params ) {
+               parent::__construct( $params );
+
+               $this->mType = array_key_exists( 'type', $params )
+                       ? $params['type']
+                       : 'datetime';
+
+               if ( !in_array( $this->mType, [ 'date', 'time', 'datetime' ] ) ) {
+                       throw new InvalidArgumentException( "Invalid type '$this->mType'" );
+               }
+
+               $this->mClass .= ' mw-htmlform-datetime-field';
+       }
+
+       public function getAttributes( array $list ) {
+               $parentList = array_diff( $list, [ 'min', 'max' ] );
+               $ret = parent::getAttributes( $parentList );
+
+               if ( in_array( 'placeholder', $list ) && !isset( $ret['placeholder'] ) ) {
+                       // Messages: htmlform-date-placeholder htmlform-time-placeholder htmlform-datetime-placeholder
+                       $ret['placeholder'] = $this->msg( "htmlform-{$this->mType}-placeholder" )->text();
+               }
+
+               if ( in_array( 'min', $list ) && isset( $this->mParams['min'] ) ) {
+                       $min = $this->parseDate( $this->mParams['min'] );
+                       if ( $min ) {
+                               $ret['min'] = $this->formatDate( $min );
+                               // Because Html::expandAttributes filters it out
+                               $ret['data-min'] = $ret['min'];
+                       }
+               }
+               if ( in_array( 'max', $list ) && isset( $this->mParams['max'] ) ) {
+                       $max = $this->parseDate( $this->mParams['max'] );
+                       if ( $max ) {
+                               $ret['max'] = $this->formatDate( $max );
+                               // Because Html::expandAttributes filters it out
+                               $ret['data-max'] = $ret['max'];
+                       }
+               }
+
+               $ret['step'] = 1;
+               // Because Html::expandAttributes filters it out
+               $ret['data-step'] = 1;
+
+               $ret['type'] = $this->mType;
+               $ret['pattern'] = static::$patterns[$this->mType];
+
+               return $ret;
+       }
+
+       function loadDataFromRequest( $request ) {
+               if ( !$request->getCheck( $this->mName ) ) {
+                       return $this->getDefault();
+               }
+
+               $value = $request->getText( $this->mName );
+               $date = $this->parseDate( $value );
+               return $date ? $this->formatDate( $date ) : $value;
+       }
+
+       function validate( $value, $alldata ) {
+               $p = parent::validate( $value, $alldata );
+
+               if ( $p !== true ) {
+                       return $p;
+               }
+
+               if ( $value === '' ) {
+                       // required was already checked by parent::validate
+                       return true;
+               }
+
+               $date = $this->parseDate( $value );
+               if ( !$date ) {
+                       // Messages: htmlform-date-invalid htmlform-time-invalid htmlform-datetime-invalid
+                       return $this->msg( "htmlform-{$this->mType}-invalid" )->parseAsBlock();
+               }
+
+               if ( isset( $this->mParams['min'] ) ) {
+                       $min = $this->parseDate( $this->mParams['min'] );
+                       if ( $min && $date < $min ) {
+                               // Messages: htmlform-date-toolow htmlform-time-toolow htmlform-datetime-toolow
+                               return $this->msg( "htmlform-{$this->mType}-toolow", $this->formatDate( $min ) )
+                                       ->parseAsBlock();
+                       }
+               }
+
+               if ( isset( $this->mParams['max'] ) ) {
+                       $max = $this->parseDate( $this->mParams['max'] );
+                       if ( $max && $date > $max ) {
+                               // Messages: htmlform-date-toohigh htmlform-time-toohigh htmlform-datetime-toohigh
+                               return $this->msg( "htmlform-{$this->mType}-toohigh", $this->formatDate( $max ) )
+                                       ->parseAsBlock();
+                       }
+               }
+
+               return true;
+       }
+
+       protected function parseDate( $value ) {
+               $value = trim( $value );
+
+               if ( $this->mType === 'date' ) {
+                       $value .= ' T00:00:00+0000';
+               }
+               if ( $this->mType === 'time' ) {
+                       $value = '1970-01-01 ' . $value . '+0000';
+               }
+
+               try {
+                       $date = new DateTime( $value, new DateTimeZone( 'GMT' ) );
+                       return $date->getTimestamp();
+               } catch ( Exception $ex ) {
+                       return 0;
+               }
+       }
+
+       protected function formatDate( $value ) {
+               switch ( $this->mType ) {
+                       case 'date':
+                               return gmdate( 'Y-m-d', $value );
+
+                       case 'time':
+                               return gmdate( 'H:i:s', $value );
+
+                       case 'datetime':
+                               return gmdate( 'Y-m-d\\TH:i:s\\Z', $value );
+               }
+       }
+
+       public function getInputOOUI( $value ) {
+               $params = [
+                       'type' => $this->mType,
+                       'value' => $value,
+                       'name' => $this->mName,
+                       'id' => $this->mID,
+               ];
+
+               if ( isset( $this->mParams['min'] ) ) {
+                       $min = $this->parseDate( $this->mParams['min'] );
+                       if ( $min ) {
+                               $params['min'] = $this->formatDate( $min );
+                       }
+               }
+               if ( isset( $this->mParams['max'] ) ) {
+                       $max = $this->parseDate( $this->mParams['max'] );
+                       if ( $max ) {
+                               $params['max'] = $this->formatDate( $max );
+                       }
+               }
+
+               return new MediaWiki\Widget\DateTimeInputWidget( $params );
+       }
+
+       protected function getOOUIModules() {
+               return [ 'mediawiki.widgets.datetime' ];
+       }
+
+       protected function shouldInfuseOOUI() {
+               return true;
+       }
+
+}
diff --git a/includes/htmlform/fields/HTMLRestrictionsField.php b/includes/htmlform/fields/HTMLRestrictionsField.php
new file mode 100644 (file)
index 0000000..8dc16bf
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * Class for updating an MWRestrictions value (which is, currently, basically just an IP address
+ * list).
+ *
+ * Will be represented as a textarea with one address per line, with intelligent defaults for
+ * label, help text and row count.
+ *
+ * The value returned will be an MWRestrictions or the input string if it was not a list of
+ * valid IP ranges.
+ */
+class HTMLRestrictionsField extends HTMLTextAreaField {
+       const DEFAULT_ROWS = 5;
+
+       public function __construct( array $params ) {
+               parent::__construct( $params );
+               if ( !$this->mLabel ) {
+                       $this->mLabel = $this->msg( 'restrictionsfield-label' )->parse();
+               }
+       }
+
+       public function getHelpText() {
+               $helpText = parent::getHelpText();
+               if ( $helpText === null ) {
+                       $helpText = $this->msg( 'restrictionsfield-help' )->parse();
+               }
+               return $helpText;
+       }
+
+       /**
+        * @param WebRequest $request
+        * @return string|MWRestrictions Restrictions object or original string if invalid
+        */
+       function loadDataFromRequest( $request ) {
+               if ( !$request->getCheck( $this->mName ) ) {
+                       return $this->getDefault();
+               }
+
+               $value = rtrim( $request->getText( $this->mName ), "\r\n" );
+               $ips = $value === '' ? [] : explode( PHP_EOL, $value );
+               try {
+                       return MWRestrictions::newFromArray( [ 'IPAddresses' => $ips ] );
+               } catch ( InvalidArgumentException $e ) {
+                       return $value;
+               }
+       }
+
+       /**
+        * @return MWRestrictions
+        */
+       public function getDefault() {
+               $default = parent::getDefault();
+               if ( $default === null ) {
+                       $default = MWRestrictions::newDefault();
+               }
+               return $default;
+       }
+
+       /**
+        * @param string|MWRestrictions $value The value the field was submitted with
+        * @param array $alldata The data collected from the form
+        *
+        * @return bool|string True on success, or String error to display, or
+        *   false to fail validation without displaying an error.
+        */
+       public function validate( $value, $alldata ) {
+               if ( $this->isHidden( $alldata ) ) {
+                       return true;
+               }
+
+               if (
+                       isset( $this->mParams['required'] ) && $this->mParams['required'] !== false
+                       && $value instanceof MWRestrictions && !$value->toArray()['IPAddresses']
+               ) {
+                       return $this->msg( 'htmlform-required' )->parse();
+               }
+
+               if ( is_string( $value ) ) {
+                       // MWRestrictions::newFromArray failed; one of the IP ranges must be invalid
+                       $status = Status::newGood();
+                       foreach ( explode( PHP_EOL,  $value ) as $range ) {
+                               if ( !\IP::isIPAddress( $range ) ) {
+                                       $status->fatal( 'restrictionsfield-badip', $range );
+                               }
+                       }
+                       if ( $status->isOK() ) {
+                               $status->fatal( 'unknown-error' );
+                       }
+                       return $status->getMessage()->parse();
+               }
+
+               if ( isset( $this->mValidationCallback ) ) {
+                       return call_user_func( $this->mValidationCallback, $value, $alldata, $this->mParent );
+               }
+
+               return true;
+       }
+
+       /**
+        * @param string|MWRestrictions $value
+        * @return string
+        */
+       public function getInputHTML( $value ) {
+               if ( $value instanceof MWRestrictions ) {
+                       $value = implode( PHP_EOL, $value->toArray()['IPAddresses'] );
+               }
+               return parent::getInputHTML( $value );
+       }
+
+       /**
+        * @param MWRestrictions $value
+        * @return string
+        */
+       public function getInputOOUI( $value ) {
+               if ( $value instanceof MWRestrictions ) {
+                       $value = implode( PHP_EOL, $value->toArray()['IPAddresses'] );
+               }
+               return parent::getInputOOUI( $value );
+       }
+}
index 4f10367..2b84144 100644 (file)
@@ -167,7 +167,7 @@ abstract class DatabaseInstaller {
         *
         * @param string $sourceFileMethod
         * @param string $stepName
-        * @param string $archiveTableMustNotExist
+        * @param bool $archiveTableMustNotExist
         * @return Status
         */
        private function stepApplySourceFile(
@@ -353,10 +353,14 @@ abstract class DatabaseInstaller {
                $up = DatabaseUpdater::newForDB( $this->db );
                try {
                        $up->doUpdates();
-               } catch ( Exception $e ) {
+               } catch ( MWException $e ) {
                        echo "\nAn error occurred:\n";
                        echo $e->getText();
                        $ret = false;
+               } catch ( Exception $e ) {
+                       echo "\nAn error occurred:\n";
+                       echo $e->getMessage();
+                       $ret = false;
                }
                $up->purgeCache();
                ob_end_flush();
index 0d0da08..2425005 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  * @ingroup Deployment
  */
+use MediaWiki\MediaWikiServices;
 
 require_once __DIR__ . '/../../maintenance/Maintenance.php';
 
@@ -456,6 +457,8 @@ abstract class DatabaseUpdater {
         * @param bool $passSelf Whether to pass this object we calling external functions
         */
        private function runUpdates( array $updates, $passSelf ) {
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+
                $updatesDone = [];
                $updatesSkipped = [];
                foreach ( $updates as $params ) {
@@ -470,7 +473,7 @@ abstract class DatabaseUpdater {
                        flush();
                        if ( $ret !== false ) {
                                $updatesDone[] = $origParams;
-                               wfGetLBFactory()->waitForReplication();
+                               $lbFactory->waitForReplication();
                        } else {
                                $updatesSkipped[] = [ $func, $params, $origParams ];
                        }
index 6fa270a..2890e3c 100644 (file)
@@ -13,6 +13,7 @@
        "config-localsettings-key": "Kesay berzkerdin:",
        "config-your-language": "Zıwanê şıma:",
        "config-wiki-language": "Wiki zıwan:",
+       "config-wiki-language-help": "Degmesi zıwanê kı wiki do tey bınusi yo",
        "config-back": "← Peyser",
        "config-continue": "Dewam ke",
        "config-page-language": "Zıwan",
index 29bbb71..a8bc219 100644 (file)
@@ -7,7 +7,8 @@
                        "Danmichaelo",
                        "Jeblad",
                        "Macofe",
-                       "SuperPotato"
+                       "SuperPotato",
+                       "Jon Harald Søby"
                ]
        },
        "config-desc": "Installasjonsprogrammet for MediaWiki",
@@ -55,7 +56,7 @@
        "config-env-hhvm": "HHVM $1 er installert.",
        "config-unicode-using-intl": "Bruker [http://pecl.php.net/intl intl PECL-utvidelsen] for Unicode-normalisering.",
        "config-unicode-pure-php-warning": "'''Advarsel''': [http://pecl.php.net/intl intl PECL-utvidelsen] er ikke tilgjengelig for å håndtere Unicode-normaliseringen, faller tilbake til en langsommere ren-PHP-implementasjon.\nOm du kjører et nettsted med høy trafikk bør du lese litt om [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode-normalisering].",
-       "config-unicode-update-warning": "'''Advarsel''': Den installerte versjonen av Unicode-normalisereren bruker en eldre versjon av [http://site.icu-project.org/ ICU-prosjektets] bibliotek.\nDu bør [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations oppgradere] om du er bekymret for å bruke Unicode.",
+       "config-unicode-update-warning": "<strong>Advarsel:</strong> Den installerte versjonen av Unicode-normalisereren bruker en eldre versjon av [http://site.icu-project.org/ ICU-prosjektets] bibliotek.\nDu bør [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations oppgradere] om du er bekymret for å bruke Unicode.",
        "config-no-db": "Fant ingen passende databasedriver! Du må installere en databasedriver for PHP.\nFølgende {{PLURAL:$2|databasetype|databasetyper}} støttes: $1\n\nOm du kompilerte PHP selv, rekonfigurer den med en aktivert databaseklient, for eksempel ved å bruke <code>./configure --with-mysql</code>.\nOm du installerte PHP fra en Debian- eller Ubuntu-pakke, må du også installere for eksempel <code>php5-mysql</code>-pakken.",
        "config-outdated-sqlite": "'''Advarsel''': Du har SQLite $1, som er en eldre versjon enn minimumskravet SQLite $2. SQLite vil ikke være tilgjengelig.",
        "config-no-fts3": "'''Advarsel''': SQLite er kompilert uten [//sqlite.org/fts3.html FTS3-modulen], søkefunksjoner vil ikke være tilgjengelig på dette bakstykket.",
        "config-ns-site-name": "Samme som wikinavnet: $1",
        "config-ns-other": "Annet (spesifiser)",
        "config-ns-other-default": "MyWiki",
-       "config-project-namespace-help": "Etter Wikipedias eksempel holder mange wikier deres sider med retningslinjer atskilt fra sine innholdssider, i et «'''prosjektnavnerom'''».\nAlle sidetitler i dette navnerommet starter med et gitt prefiks som du kan angi her.\nTradisjonelt er dette prefikset avledet fra navnet på wikien, men det kan ikke innholde punkttegn som «#» eller «:».",
+       "config-project-namespace-help": "Etter Wikipedias eksempel holder mange wikier deres sider med retningslinjer atskilt fra sine innholdssider, i et «'''prosjektnavnerom'''».\nAlle sidetitler i dette navnerommet starter med et gitt prefiks som du kan angi her.\nVanligvis er dette prefikset avledet fra navnet på wikien, men det kan ikke innholde punkttegn som «#» eller «:».",
        "config-ns-invalid": "Det angitte navnerommet «<nowiki>$1</nowiki>» er ugyldig.\nAngi et annet prosjektnavnerom.",
        "config-ns-conflict": "Det angitte navnerommet «<nowiki>$1</nowiki>» er i konflikt med et standard MediaWiki-navnerom.\nAngi et annet prosjekt-navnerom.",
        "config-admin-box": "Administratorkonto",
        "config-subscribe": "Abonner på [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce e-postlisten for utgivelsesannonseringer].",
        "config-subscribe-help": "Dette er en lav-volums e-postliste brukt til utgivelsesannonseringer, herunder viktige sikkerhetsannonseringer.\nDu bør abonnere på den og oppdatere MediaWikiinstallasjonen din når nye versjoner kommer ut.",
        "config-subscribe-noemail": "Du prøvde å abonnere på epost-meldinger om nye versjoner uten å oppgi en epost-adresse. Vær vennlig å oppgi en epost-adresse om du ønsker dette abonnementet.",
+       "config-pingback": "Del data om denne installasjonen med MediaWiki-utviklerne.",
+       "config-pingback-help": "Om du velger denne vil MediaWiki periodisk pinge https://www.mediawiki.org med grunnleggende data om denne MediaWiki-instansen. Disse dataene inkluderer for eksempel systemtypen, PHP-versjonen og hvilket databasebakstykke som er valgt. Wikimedia Foundation deler disse dataene med MediaWiki-utviklerne for å bestemme framtidige utviklingstiltak. Følgende data vil bli sendt for ditt system:\n<pre>$1</pre>",
        "config-almost-done": "Du er nesten ferdig!\nDu kan velge å hoppe over de siste konfigurasjonstrinnene og installere wikien med en gang.",
        "config-optional-continue": "Still meg flere spørsmål.",
        "config-optional-skip": "Jeg er lei, bare installer wikien.",
        "config-install-extension-tables": "Oppretter tabeller for aktiviserte utvidelser",
        "config-install-mainpage-failed": "Kunne ikke sette inn hovedside: $1",
        "config-install-done": "<strong>Gratulrerer!</strong>\nDu har lykkes i å installere MediaWiki.\n\nInstallasjonsprogrammet har generert en <code>LocalSettings.php</code>-fil.\nDen inneholder alle dine konfigureringer.\n\nDu må laste den ned og legge den på hovedfolderen for din wiki-installasjon (der index.php ligger). Nedlastingen skulle ha startet automatisk.\n\nHvis ingen nedlasting ble tilbudt, eller du avbrøt den, kan du få den i gang ved å klikke på lenken under:\n\n$3\n\n<strong>OBS:</strong> Hvis du ikke gjør dette nå, vil den genererte konfigurasjonsfilen ikke være tilgjengelig for deg senere.\n\nNår dette er gjort, kan du <strong>[$2 gå inn i wikien]</strong>.",
+       "config-install-done-path": "<strong>Gratulerer!</strong>\nDu har installert MediaWiki.\n\nInstallereren har generert en <code>LocalSettings.php</code>-fil.\nDen inneholder all konfigurasjonen for wikien.\n\nDu må laste den ned og legge den i <code>$4</code>. Nedlastingen skal ha startet automatisk.\n\nOm nedlastingen ikke ble startet, eller om du avbrøt den, kan du starte på nytt ved å klikke lenken nedenfor:\n\n$3\n\n<strong>Merk:</strong> Om du ikke gjør dette nå vil den genererte konfigurasjonen ikke være tilgjengelig senere.\n\nNår dette er gjort kan du <strong>[$2 gå til wikien din]</strong>.",
        "config-download-localsettings": "Last ned <code>LocalSettings.php</code>",
        "config-help": "hjelp",
        "config-help-tooltip": "klikk for å utvide",
        "config-nofile": "Filen \"$1\" ble ikke funnet. Kan den være blitt slettet?",
        "config-extension-link": "Visste du at wikien din kan brukes sammen med en mengde [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions utvidelser]?\n\nDu kan sjekke gjennom [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category utvidelser per kategori] eller [https://www.mediawiki.org/wiki/Extension_Matrix utvidelsesmatrisen] for å se den komplette listen av utvidelser.",
-       "mainpagetext": "'''MediaWiki-programvaren er nå installert.'''",
+       "mainpagetext": "<strong>MediaWiki har blitt installert.</strong>",
        "mainpagedocfooter": "Sjekk [https://meta.wikimedia.org/wiki/Help:Contents brukerveiledningen] for å få informasjon om hvordan du bruker wiki-programvaren.\n\n==Hvordan komme igang==\n*[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Innstillingsliste]\n*[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Ofte stilte spørsmål om MediaWiki]\n*[https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-postliste]\n*[https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Tilpass MediaWiki for ditt språk]\n*[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Lær deg å beskytte deg mot spam på wikien din]"
 }
index a356e84..25a271c 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  * @author Aaron Schulz
  */
+use Psr\Log\LoggerInterface;
 
 /**
  * Class to handle job queues stored in Redis
@@ -66,6 +67,8 @@
 class JobQueueRedis extends JobQueue {
        /** @var RedisConnectionPool */
        protected $redisPool;
+       /** @var LoggerInterface */
+       protected $logger;
 
        /** @var string Server address */
        protected $server;
@@ -96,6 +99,7 @@ class JobQueueRedis extends JobQueue {
                                "Non-daemonized mode is no longer supported. Please install the " .
                                "mediawiki/services/jobrunner service and update \$wgJobTypeConf as needed." );
                }
+               $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'redis' );
        }
 
        protected function supportedOrders() {
@@ -250,6 +254,7 @@ class JobQueueRedis extends JobQueue {
                        $args[] = (string)$this->serialize( $item );
                }
                static $script =
+               /** @lang Lua */
 <<<LUA
                local kUnclaimed, kSha1ById, kIdBySha1, kDelayed, kData, kQwJobs = unpack(KEYS)
                -- First argument is the queue ID
@@ -339,6 +344,7 @@ LUA;
         */
        protected function popAndAcquireBlob( RedisConnRef $conn ) {
                static $script =
+               /** @lang Lua */
 <<<LUA
                local kUnclaimed, kSha1ById, kIdBySha1, kClaimed, kAttempts, kData = unpack(KEYS)
                local rTime = unpack(ARGV)
@@ -386,6 +392,7 @@ LUA;
                $conn = $this->getConnection();
                try {
                        static $script =
+                       /** @lang Lua */
 <<<LUA
                        local kClaimed, kAttempts, kData = unpack(KEYS)
                        local id = unpack(ARGV)
@@ -745,7 +752,7 @@ LUA;
         * @throws JobQueueConnectionError
         */
        protected function getConnection() {
-               $conn = $this->redisPool->getConnection( $this->server );
+               $conn = $this->redisPool->getConnection( $this->server, $this->logger );
                if ( !$conn ) {
                        throw new JobQueueConnectionError(
                                "Unable to connect to redis server {$this->server}." );
index 906a48e..6ae8837 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  * @author Aaron Schulz
  */
+use Psr\Log\LoggerInterface;
 
 /**
  * Class to handle tracking information about all queues using PhpRedis
@@ -33,6 +34,8 @@
 class JobQueueAggregatorRedis extends JobQueueAggregator {
        /** @var RedisConnectionPool */
        protected $redisPool;
+       /** @var LoggerInterface */
+       protected $logger;
        /** @var array List of Redis server addresses */
        protected $servers;
 
@@ -52,6 +55,7 @@ class JobQueueAggregatorRedis extends JobQueueAggregator {
                        : [ $params['redisServer'] ]; // b/c
                $params['redisConfig']['serializer'] = 'none';
                $this->redisPool = RedisConnectionPool::singleton( $params['redisConfig'] );
+               $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'redis' );
        }
 
        protected function doNotifyQueueEmpty( $wiki, $type ) {
@@ -104,7 +108,7 @@ class JobQueueAggregatorRedis extends JobQueueAggregator {
        protected function getConnection() {
                $conn = false;
                foreach ( $this->servers as $server ) {
-                       $conn = $this->redisPool->getConnection( $server );
+                       $conn = $this->redisPool->getConnection( $server, $this->logger );
                        if ( $conn ) {
                                break;
                        }
index 96075aa..2fd60ea 100644 (file)
@@ -21,7 +21,7 @@
  */
 
 /**
- * Class for asserting that a callback happens when an dummy object leaves scope
+ * Class for asserting that a callback happens when a dummy object leaves scope
  *
  * @since 1.21
  */
index d0e93da..dacad1c 100644 (file)
@@ -153,26 +153,6 @@ class FSFile {
                return $info;
        }
 
-       /**
-        * Exract image size information
-        *
-        * @param array $gis
-        * @return array
-        */
-       protected function extractImageSizeInfo( array $gis ) {
-               $info = [];
-               # NOTE: $gis[2] contains a code for the image type. This is no longer used.
-               $info['width'] = $gis[0];
-               $info['height'] = $gis[1];
-               if ( isset( $gis['bits'] ) ) {
-                       $info['bits'] = $gis['bits'];
-               } else {
-                       $info['bits'] = 0;
-               }
-
-               return $info;
-       }
-
        /**
         * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
         * encoding, zero padded to 31 digits.
diff --git a/includes/libs/filebackend/FSFileBackend.php b/includes/libs/filebackend/FSFileBackend.php
new file mode 100644 (file)
index 0000000..8afdce4
--- /dev/null
@@ -0,0 +1,984 @@
+<?php
+/**
+ * File system based backend.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Class for a file system (FS) based file backend.
+ *
+ * All "containers" each map to a directory under the backend's base directory.
+ * For backwards-compatibility, some container paths can be set to custom paths.
+ * The domain ID will not be used in any custom paths, so this should be avoided.
+ *
+ * Having directories with thousands of files will diminish performance.
+ * Sharding can be accomplished by using FileRepo-style hash paths.
+ *
+ * StatusValue messages should avoid mentioning the internal FS paths.
+ * PHP warnings are assumed to be logged rather than output.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+class FSFileBackend extends FileBackendStore {
+       /** @var string Directory holding the container directories */
+       protected $basePath;
+
+       /** @var array Map of container names to root paths for custom container paths */
+       protected $containerPaths = [];
+
+       /** @var int File permission mode */
+       protected $fileMode;
+       /** @var int File permission mode */
+       protected $dirMode;
+
+       /** @var string Required OS username to own files */
+       protected $fileOwner;
+
+       /** @var bool */
+       protected $isWindows;
+       /** @var string OS username running this script */
+       protected $currentUser;
+
+       /** @var array */
+       protected $hadWarningErrors = [];
+
+       /**
+        * @see FileBackendStore::__construct()
+        * Additional $config params include:
+        *   - basePath       : File system directory that holds containers.
+        *   - containerPaths : Map of container names to custom file system directories.
+        *                      This should only be used for backwards-compatibility.
+        *   - fileMode       : Octal UNIX file permissions to use on files stored.
+        *   - directoryMode  : Octal UNIX file permissions to use on directories created.
+        * @param array $config
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+
+               $this->isWindows = ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' );
+               // Remove any possible trailing slash from directories
+               if ( isset( $config['basePath'] ) ) {
+                       $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
+               } else {
+                       $this->basePath = null; // none; containers must have explicit paths
+               }
+
+               if ( isset( $config['containerPaths'] ) ) {
+                       $this->containerPaths = (array)$config['containerPaths'];
+                       foreach ( $this->containerPaths as &$path ) {
+                               $path = rtrim( $path, '/' ); // remove trailing slash
+                       }
+               }
+
+               $this->fileMode = isset( $config['fileMode'] ) ? $config['fileMode'] : 0644;
+               $this->dirMode = isset( $config['directoryMode'] ) ? $config['directoryMode'] : 0777;
+               if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) {
+                       $this->fileOwner = $config['fileOwner'];
+                       // cache this, assuming it doesn't change
+                       $this->currentUser = posix_getpwuid( posix_getuid() )['name'];
+               }
+       }
+
+       public function getFeatures() {
+               return !$this->isWindows ? FileBackend::ATTR_UNICODE_PATHS : 0;
+       }
+
+       protected function resolveContainerPath( $container, $relStoragePath ) {
+               // Check that container has a root directory
+               if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
+                       // Check for sane relative paths (assume the base paths are OK)
+                       if ( $this->isLegalRelPath( $relStoragePath ) ) {
+                               return $relStoragePath;
+                       }
+               }
+
+               return null;
+       }
+
+       /**
+        * Sanity check a relative file system path for validity
+        *
+        * @param string $path Normalized relative path
+        * @return bool
+        */
+       protected function isLegalRelPath( $path ) {
+               // Check for file names longer than 255 chars
+               if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS
+                       return false;
+               }
+               if ( $this->isWindows ) { // NTFS
+                       return !preg_match( '![:*?"<>|]!', $path );
+               } else {
+                       return true;
+               }
+       }
+
+       /**
+        * Given the short (unresolved) and full (resolved) name of
+        * a container, return the file system path of the container.
+        *
+        * @param string $shortCont
+        * @param string $fullCont
+        * @return string|null
+        */
+       protected function containerFSRoot( $shortCont, $fullCont ) {
+               if ( isset( $this->containerPaths[$shortCont] ) ) {
+                       return $this->containerPaths[$shortCont];
+               } elseif ( isset( $this->basePath ) ) {
+                       return "{$this->basePath}/{$fullCont}";
+               }
+
+               return null; // no container base path defined
+       }
+
+       /**
+        * Get the absolute file system path for a storage path
+        *
+        * @param string $storagePath Storage path
+        * @return string|null
+        */
+       protected function resolveToFSPath( $storagePath ) {
+               list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
+               if ( $relPath === null ) {
+                       return null; // invalid
+               }
+               list( , $shortCont, ) = FileBackend::splitStoragePath( $storagePath );
+               $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               if ( $relPath != '' ) {
+                       $fsPath .= "/{$relPath}";
+               }
+
+               return $fsPath;
+       }
+
+       public function isPathUsableInternal( $storagePath ) {
+               $fsPath = $this->resolveToFSPath( $storagePath );
+               if ( $fsPath === null ) {
+                       return false; // invalid
+               }
+               $parentDir = dirname( $fsPath );
+
+               if ( file_exists( $fsPath ) ) {
+                       $ok = is_file( $fsPath ) && is_writable( $fsPath );
+               } else {
+                       $ok = is_dir( $parentDir ) && is_writable( $parentDir );
+               }
+
+               if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) {
+                       $ok = false;
+                       trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." );
+               }
+
+               return $ok;
+       }
+
+       protected function doCreateInternal( array $params ) {
+               $status = $this->newStatus();
+
+               $dest = $this->resolveToFSPath( $params['dst'] );
+               if ( $dest === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $tempFile = TempFSFile::factory( 'create_', 'tmp', $this->tmpDirectory );
+                       if ( !$tempFile ) {
+                               $status->fatal( 'backend-fail-create', $params['dst'] );
+
+                               return $status;
+                       }
+                       $this->trapWarnings();
+                       $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
+                       $this->untrapWarnings();
+                       if ( $bytes === false ) {
+                               $status->fatal( 'backend-fail-create', $params['dst'] );
+
+                               return $status;
+                       }
+                       $cmd = implode( ' ', [
+                               $this->isWindows ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
+                               escapeshellarg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
+                               escapeshellarg( $this->cleanPathSlashes( $dest ) )
+                       ] );
+                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
+                               if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
+                                       $status->fatal( 'backend-fail-create', $params['dst'] );
+                                       trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+                               }
+                       };
+                       $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
+                       $tempFile->bind( $status->value );
+               } else { // immediate write
+                       $this->trapWarnings();
+                       $bytes = file_put_contents( $dest, $params['content'] );
+                       $this->untrapWarnings();
+                       if ( $bytes === false ) {
+                               $status->fatal( 'backend-fail-create', $params['dst'] );
+
+                               return $status;
+                       }
+                       $this->chmod( $dest );
+               }
+
+               return $status;
+       }
+
+       protected function doStoreInternal( array $params ) {
+               $status = $this->newStatus();
+
+               $dest = $this->resolveToFSPath( $params['dst'] );
+               if ( $dest === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $cmd = implode( ' ', [
+                               $this->isWindows ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
+                               escapeshellarg( $this->cleanPathSlashes( $params['src'] ) ),
+                               escapeshellarg( $this->cleanPathSlashes( $dest ) )
+                       ] );
+                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
+                               if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
+                                       $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
+                                       trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+                               }
+                       };
+                       $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
+               } else { // immediate write
+                       $this->trapWarnings();
+                       $ok = copy( $params['src'], $dest );
+                       $this->untrapWarnings();
+                       // In some cases (at least over NFS), copy() returns true when it fails
+                       if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
+                               if ( $ok ) { // PHP bug
+                                       unlink( $dest ); // remove broken file
+                                       trigger_error( __METHOD__ . ": copy() failed but returned true." );
+                               }
+                               $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
+
+                               return $status;
+                       }
+                       $this->chmod( $dest );
+               }
+
+               return $status;
+       }
+
+       protected function doCopyInternal( array $params ) {
+               $status = $this->newStatus();
+
+               $source = $this->resolveToFSPath( $params['src'] );
+               if ( $source === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
+               }
+
+               $dest = $this->resolveToFSPath( $params['dst'] );
+               if ( $dest === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               if ( !is_file( $source ) ) {
+                       if ( empty( $params['ignoreMissingSource'] ) ) {
+                               $status->fatal( 'backend-fail-copy', $params['src'] );
+                       }
+
+                       return $status; // do nothing; either OK or bad status
+               }
+
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $cmd = implode( ' ', [
+                               $this->isWindows ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
+                               escapeshellarg( $this->cleanPathSlashes( $source ) ),
+                               escapeshellarg( $this->cleanPathSlashes( $dest ) )
+                       ] );
+                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
+                               if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
+                                       $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+                                       trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+                               }
+                       };
+                       $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
+               } else { // immediate write
+                       $this->trapWarnings();
+                       $ok = ( $source === $dest ) ? true : copy( $source, $dest );
+                       $this->untrapWarnings();
+                       // In some cases (at least over NFS), copy() returns true when it fails
+                       if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
+                               if ( $ok ) { // PHP bug
+                                       $this->trapWarnings();
+                                       unlink( $dest ); // remove broken file
+                                       $this->untrapWarnings();
+                                       trigger_error( __METHOD__ . ": copy() failed but returned true." );
+                               }
+                               $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+
+                               return $status;
+                       }
+                       $this->chmod( $dest );
+               }
+
+               return $status;
+       }
+
+       protected function doMoveInternal( array $params ) {
+               $status = $this->newStatus();
+
+               $source = $this->resolveToFSPath( $params['src'] );
+               if ( $source === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
+               }
+
+               $dest = $this->resolveToFSPath( $params['dst'] );
+               if ( $dest === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               if ( !is_file( $source ) ) {
+                       if ( empty( $params['ignoreMissingSource'] ) ) {
+                               $status->fatal( 'backend-fail-move', $params['src'] );
+                       }
+
+                       return $status; // do nothing; either OK or bad status
+               }
+
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $cmd = implode( ' ', [
+                               $this->isWindows ? 'MOVE /Y' : 'mv', // (overwrite)
+                               escapeshellarg( $this->cleanPathSlashes( $source ) ),
+                               escapeshellarg( $this->cleanPathSlashes( $dest ) )
+                       ] );
+                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
+                               if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
+                                       $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+                                       trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+                               }
+                       };
+                       $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
+               } else { // immediate write
+                       $this->trapWarnings();
+                       $ok = ( $source === $dest ) ? true : rename( $source, $dest );
+                       $this->untrapWarnings();
+                       clearstatcache(); // file no longer at source
+                       if ( !$ok ) {
+                               $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+
+                               return $status;
+                       }
+               }
+
+               return $status;
+       }
+
+       protected function doDeleteInternal( array $params ) {
+               $status = $this->newStatus();
+
+               $source = $this->resolveToFSPath( $params['src'] );
+               if ( $source === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
+               }
+
+               if ( !is_file( $source ) ) {
+                       if ( empty( $params['ignoreMissingSource'] ) ) {
+                               $status->fatal( 'backend-fail-delete', $params['src'] );
+                       }
+
+                       return $status; // do nothing; either OK or bad status
+               }
+
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $cmd = implode( ' ', [
+                               $this->isWindows ? 'DEL' : 'unlink',
+                               escapeshellarg( $this->cleanPathSlashes( $source ) )
+                       ] );
+                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
+                               if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
+                                       $status->fatal( 'backend-fail-delete', $params['src'] );
+                                       trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
+                               }
+                       };
+                       $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
+               } else { // immediate write
+                       $this->trapWarnings();
+                       $ok = unlink( $source );
+                       $this->untrapWarnings();
+                       if ( !$ok ) {
+                               $status->fatal( 'backend-fail-delete', $params['src'] );
+
+                               return $status;
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @param string $fullCont
+        * @param string $dirRel
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
+               $status = $this->newStatus();
+               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+               $existed = is_dir( $dir ); // already there?
+               // Create the directory and its parents as needed...
+               $this->trapWarnings();
+               if ( !$existed && !mkdir( $dir, $this->dirMode, true ) && !is_dir( $dir ) ) {
+                       $this->logger->error( __METHOD__ . ": cannot create directory $dir" );
+                       $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
+               } elseif ( !is_writable( $dir ) ) {
+                       $this->logger->error( __METHOD__ . ": directory $dir is read-only" );
+                       $status->fatal( 'directoryreadonlyerror', $params['dir'] );
+               } elseif ( !is_readable( $dir ) ) {
+                       $this->logger->error( __METHOD__ . ": directory $dir is not readable" );
+                       $status->fatal( 'directorynotreadableerror', $params['dir'] );
+               }
+               $this->untrapWarnings();
+               // Respect any 'noAccess' or 'noListing' flags...
+               if ( is_dir( $dir ) && !$existed ) {
+                       $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
+               }
+
+               return $status;
+       }
+
+       protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
+               $status = $this->newStatus();
+               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+               // Seed new directories with a blank index.html, to prevent crawling...
+               if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
+                       $this->trapWarnings();
+                       $bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() );
+                       $this->untrapWarnings();
+                       if ( $bytes === false ) {
+                               $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
+                       }
+               }
+               // Add a .htaccess file to the root of the container...
+               if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) {
+                       $this->trapWarnings();
+                       $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
+                       $this->untrapWarnings();
+                       if ( $bytes === false ) {
+                               $storeDir = "mwstore://{$this->name}/{$shortCont}";
+                               $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
+                       }
+               }
+
+               return $status;
+       }
+
+       protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
+               $status = $this->newStatus();
+               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+               // Unseed new directories with a blank index.html, to allow crawling...
+               if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) {
+                       $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() );
+                       $this->trapWarnings();
+                       if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure()
+                               $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
+                       }
+                       $this->untrapWarnings();
+               }
+               // Remove the .htaccess file from the root of the container...
+               if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) {
+                       $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() );
+                       $this->trapWarnings();
+                       if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
+                               $storeDir = "mwstore://{$this->name}/{$shortCont}";
+                               $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
+                       }
+                       $this->untrapWarnings();
+               }
+
+               return $status;
+       }
+
+       protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
+               $status = $this->newStatus();
+               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+               $this->trapWarnings();
+               if ( is_dir( $dir ) ) {
+                       rmdir( $dir ); // remove directory if empty
+               }
+               $this->untrapWarnings();
+
+               return $status;
+       }
+
+       protected function doGetFileStat( array $params ) {
+               $source = $this->resolveToFSPath( $params['src'] );
+               if ( $source === null ) {
+                       return false; // invalid storage path
+               }
+
+               $this->trapWarnings(); // don't trust 'false' if there were errors
+               $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
+               $hadError = $this->untrapWarnings();
+
+               if ( $stat ) {
+                       $ct = new ConvertibleTimestamp( $stat['mtime'] );
+
+                       return [
+                               'mtime' => $ct->getTimestamp( TS_MW ),
+                               'size' => $stat['size']
+                       ];
+               } elseif ( !$hadError ) {
+                       return false; // file does not exist
+               } else {
+                       return null; // failure
+               }
+       }
+
+       protected function doClearCache( array $paths = null ) {
+               clearstatcache(); // clear the PHP file stat cache
+       }
+
+       protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
+               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+
+               $this->trapWarnings(); // don't trust 'false' if there were errors
+               $exists = is_dir( $dir );
+               $hadError = $this->untrapWarnings();
+
+               return $hadError ? null : $exists;
+       }
+
+       /**
+        * @see FileBackendStore::getDirectoryListInternal()
+        * @param string $fullCont
+        * @param string $dirRel
+        * @param array $params
+        * @return array|FSFileBackendDirList|null
+        */
+       public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
+               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+               $exists = is_dir( $dir );
+               if ( !$exists ) {
+                       $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+
+                       return []; // nothing under this dir
+               } elseif ( !is_readable( $dir ) ) {
+                       $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+
+                       return null; // bad permissions?
+               }
+
+               return new FSFileBackendDirList( $dir, $params );
+       }
+
+       /**
+        * @see FileBackendStore::getFileListInternal()
+        * @param string $fullCont
+        * @param string $dirRel
+        * @param array $params
+        * @return array|FSFileBackendFileList|null
+        */
+       public function getFileListInternal( $fullCont, $dirRel, array $params ) {
+               list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+               $exists = is_dir( $dir );
+               if ( !$exists ) {
+                       $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+
+                       return []; // nothing under this dir
+               } elseif ( !is_readable( $dir ) ) {
+                       $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+
+                       return null; // bad permissions?
+               }
+
+               return new FSFileBackendFileList( $dir, $params );
+       }
+
+       protected function doGetLocalReferenceMulti( array $params ) {
+               $fsFiles = []; // (path => FSFile)
+
+               foreach ( $params['srcs'] as $src ) {
+                       $source = $this->resolveToFSPath( $src );
+                       if ( $source === null || !is_file( $source ) ) {
+                               $fsFiles[$src] = null; // invalid path or file does not exist
+                       } else {
+                               $fsFiles[$src] = new FSFile( $source );
+                       }
+               }
+
+               return $fsFiles;
+       }
+
+       protected function doGetLocalCopyMulti( array $params ) {
+               $tmpFiles = []; // (path => TempFSFile)
+
+               foreach ( $params['srcs'] as $src ) {
+                       $source = $this->resolveToFSPath( $src );
+                       if ( $source === null ) {
+                               $tmpFiles[$src] = null; // invalid path
+                       } else {
+                               // Create a new temporary file with the same extension...
+                               $ext = FileBackend::extensionFromPath( $src );
+                               $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
+                               if ( !$tmpFile ) {
+                                       $tmpFiles[$src] = null;
+                               } else {
+                                       $tmpPath = $tmpFile->getPath();
+                                       // Copy the source file over the temp file
+                                       $this->trapWarnings();
+                                       $ok = copy( $source, $tmpPath );
+                                       $this->untrapWarnings();
+                                       if ( !$ok ) {
+                                               $tmpFiles[$src] = null;
+                                       } else {
+                                               $this->chmod( $tmpPath );
+                                               $tmpFiles[$src] = $tmpFile;
+                                       }
+                               }
+                       }
+               }
+
+               return $tmpFiles;
+       }
+
+       protected function directoriesAreVirtual() {
+               return false;
+       }
+
+       /**
+        * @param FSFileOpHandle[] $fileOpHandles
+        *
+        * @return StatusValue[]
+        */
+       protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
+               $statuses = [];
+
+               $pipes = [];
+               foreach ( $fileOpHandles as $index => $fileOpHandle ) {
+                       $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' );
+               }
+
+               $errs = [];
+               foreach ( $pipes as $index => $pipe ) {
+                       // Result will be empty on success in *NIX. On Windows,
+                       // it may be something like "        1 file(s) [copied|moved].".
+                       $errs[$index] = stream_get_contents( $pipe );
+                       fclose( $pipe );
+               }
+
+               foreach ( $fileOpHandles as $index => $fileOpHandle ) {
+                       $status = $this->newStatus();
+                       $function = $fileOpHandle->call;
+                       $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
+                       $statuses[$index] = $status;
+                       if ( $status->isOK() && $fileOpHandle->chmodPath ) {
+                               $this->chmod( $fileOpHandle->chmodPath );
+                       }
+               }
+
+               clearstatcache(); // files changed
+               return $statuses;
+       }
+
+       /**
+        * Chmod a file, suppressing the warnings
+        *
+        * @param string $path Absolute file system path
+        * @return bool Success
+        */
+       protected function chmod( $path ) {
+               $this->trapWarnings();
+               $ok = chmod( $path, $this->fileMode );
+               $this->untrapWarnings();
+
+               return $ok;
+       }
+
+       /**
+        * Return the text of an index.html file to hide directory listings
+        *
+        * @return string
+        */
+       protected function indexHtmlPrivate() {
+               return '';
+       }
+
+       /**
+        * Return the text of a .htaccess file to make a directory private
+        *
+        * @return string
+        */
+       protected function htaccessPrivate() {
+               return "Deny from all\n";
+       }
+
+       /**
+        * Clean up directory separators for the given OS
+        *
+        * @param string $path FS path
+        * @return string
+        */
+       protected function cleanPathSlashes( $path ) {
+               return $this->isWindows ? strtr( $path, '/', '\\' ) : $path;
+       }
+
+       /**
+        * 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 );
+       }
+
+       /**
+        * Stop listening for E_WARNING errors and return true if any happened
+        *
+        * @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
+        * @access 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
+       }
+}
+
+/**
+ * @see FileBackendStoreOpHandle
+ */
+class FSFileOpHandle extends FileBackendStoreOpHandle {
+       public $cmd; // string; shell command
+       public $chmodPath; // string; file to chmod
+
+       /**
+        * @param FSFileBackend $backend
+        * @param array $params
+        * @param callable $call
+        * @param string $cmd
+        * @param int|null $chmodPath
+        */
+       public function __construct(
+               FSFileBackend $backend, array $params, $call, $cmd, $chmodPath = null
+       ) {
+               $this->backend = $backend;
+               $this->params = $params;
+               $this->call = $call;
+               $this->cmd = $cmd;
+               $this->chmodPath = $chmodPath;
+       }
+}
+
+/**
+ * Wrapper around RecursiveDirectoryIterator/DirectoryIterator that
+ * catches exception or does any custom behavoir that we may want.
+ * Do not use this class from places outside FSFileBackend.
+ *
+ * @ingroup FileBackend
+ */
+abstract class FSFileBackendList implements Iterator {
+       /** @var Iterator */
+       protected $iter;
+
+       /** @var int */
+       protected $suffixStart;
+
+       /** @var int */
+       protected $pos = 0;
+
+       /** @var array */
+       protected $params = [];
+
+       /**
+        * @param string $dir File system directory
+        * @param array $params
+        */
+       public function __construct( $dir, array $params ) {
+               $path = realpath( $dir ); // normalize
+               if ( $path === false ) {
+                       $path = $dir;
+               }
+               $this->suffixStart = strlen( $path ) + 1; // size of "path/to/dir/"
+               $this->params = $params;
+
+               try {
+                       $this->iter = $this->initIterator( $path );
+               } catch ( UnexpectedValueException $e ) {
+                       $this->iter = null; // bad permissions? deleted?
+               }
+       }
+
+       /**
+        * Return an appropriate iterator object to wrap
+        *
+        * @param string $dir File system directory
+        * @return Iterator
+        */
+       protected function initIterator( $dir ) {
+               if ( !empty( $this->params['topOnly'] ) ) { // non-recursive
+                       # Get an iterator that will get direct sub-nodes
+                       return new DirectoryIterator( $dir );
+               } else { // recursive
+                       # Get an iterator that will return leaf nodes (non-directories)
+                       # RecursiveDirectoryIterator extends FilesystemIterator.
+                       # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
+                       $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
+
+                       return new RecursiveIteratorIterator(
+                               new RecursiveDirectoryIterator( $dir, $flags ),
+                               RecursiveIteratorIterator::CHILD_FIRST // include dirs
+                       );
+               }
+       }
+
+       /**
+        * @see Iterator::key()
+        * @return int
+        */
+       public function key() {
+               return $this->pos;
+       }
+
+       /**
+        * @see Iterator::current()
+        * @return string|bool String or false
+        */
+       public function current() {
+               return $this->getRelPath( $this->iter->current()->getPathname() );
+       }
+
+       /**
+        * @see Iterator::next()
+        * @throws FileBackendError
+        */
+       public function next() {
+               try {
+                       $this->iter->next();
+                       $this->filterViaNext();
+               } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
+                       throw new FileBackendError( "File iterator gave UnexpectedValueException." );
+               }
+               ++$this->pos;
+       }
+
+       /**
+        * @see Iterator::rewind()
+        * @throws FileBackendError
+        */
+       public function rewind() {
+               $this->pos = 0;
+               try {
+                       $this->iter->rewind();
+                       $this->filterViaNext();
+               } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
+                       throw new FileBackendError( "File iterator gave UnexpectedValueException." );
+               }
+       }
+
+       /**
+        * @see Iterator::valid()
+        * @return bool
+        */
+       public function valid() {
+               return $this->iter && $this->iter->valid();
+       }
+
+       /**
+        * Filter out items by advancing to the next ones
+        */
+       protected function filterViaNext() {
+       }
+
+       /**
+        * Return only the relative path and normalize slashes to FileBackend-style.
+        * Uses the "real path" since the suffix is based upon that.
+        *
+        * @param string $dir
+        * @return string
+        */
+       protected function getRelPath( $dir ) {
+               $path = realpath( $dir );
+               if ( $path === false ) {
+                       $path = $dir;
+               }
+
+               return strtr( substr( $path, $this->suffixStart ), '\\', '/' );
+       }
+}
+
+class FSFileBackendDirList extends FSFileBackendList {
+       protected function filterViaNext() {
+               while ( $this->iter->valid() ) {
+                       if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
+                               $this->iter->next(); // skip non-directories and dot files
+                       } else {
+                               break;
+                       }
+               }
+       }
+}
+
+class FSFileBackendFileList extends FSFileBackendList {
+       protected function filterViaNext() {
+               while ( $this->iter->valid() ) {
+                       if ( !$this->iter->current()->isFile() ) {
+                               $this->iter->next(); // skip non-files and dot files
+                       } else {
+                               break;
+                       }
+               }
+       }
+}
index 0b9eee0..0ef9f63 100644 (file)
@@ -28,6 +28,8 @@
  * @ingroup FileBackend
  * @author Aaron Schulz
  */
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
 
 /**
  * @brief Base class for all file backend classes (including multi-write backends).
  * Outside callers can assume that all backends will have these functions.
  *
  * All "storage paths" are of the format "mwstore://<backend>/<container>/<path>".
- * The "backend" portion is unique name for MediaWiki to refer to a backend, while
+ * The "backend" portion is unique name for the application to refer to a backend, while
  * the "container" portion is a top-level directory of the backend. The "path" portion
  * is a relative path that uses UNIX file system (FS) notation, though any particular
  * backend may not actually be using a local filesystem. Therefore, the relative paths
  * are only virtual.
  *
- * Backend contents are stored under wiki-specific container names by default.
- * Global (qualified) backends are achieved by configuring the "wiki ID" to a constant.
+ * Backend contents are stored under "domain"-specific container names by default.
+ * A domain is simply a logical umbrella for entities, such as those belonging to a certain
+ * application or portion of a website, for example. A domain can be local or global.
+ * Global (qualified) backends are achieved by configuring the "domain ID" to a constant.
+ * Global domains are simpler, but local domains can be used by choosing a domain ID based on
+ * the current context, such as which language of a website is being used.
+ *
  * For legacy reasons, the FSFileBackend class allows manually setting the paths of
- * containers to ones that do not respect the "wiki ID".
+ * containers to ones that do not respect the "domain ID".
  *
  * In key/value (object) stores, containers are the only hierarchy (the rest is emulated).
  * FS-based backends are somewhat more restrictive due to the existence of real
  * @ingroup FileBackend
  * @since 1.19
  */
-abstract class FileBackend {
+abstract class FileBackend implements LoggerAwareInterface {
        /** @var string Unique backend name */
        protected $name;
 
-       /** @var string Unique wiki name */
-       protected $wikiId;
+       /** @var string Unique domain name */
+       protected $domainId;
 
        /** @var string Read-only explanation message */
        protected $readOnly;
@@ -103,10 +110,17 @@ abstract class FileBackend {
 
        /** @var LockManager */
        protected $lockManager;
-
        /** @var FileJournal */
        protected $fileJournal;
+       /** @var LoggerInterface */
+       protected $logger;
+       /** @var object|string Class name or object With profileIn/profileOut methods */
+       protected $profiler;
 
+       /** @var callable */
+       protected $obResetFunc;
+       /** @var callable */
+       protected $streamMimeFunc;
        /** @var callable */
        protected $statusWrapper;
 
@@ -120,34 +134,41 @@ abstract class FileBackend {
         * This should only be called from within FileBackendGroup.
         *
         * @param array $config Parameters include:
-        *   - name        : The unique name of this backend.
-        *                   This should consist of alphanumberic, '-', and '_' characters.
-        *                   This name should not be changed after use (e.g. with journaling).
-        *                   Note that the name is *not* used in actual container names.
-        *   - wikiId      : Prefix to container names that is unique to this backend.
-        *                   It should only consist of alphanumberic, '-', and '_' characters.
-        *                   This ID is what avoids collisions if multiple logical backends
-        *                   use the same storage system, so this should be set carefully.
+        *   - name : The unique name of this backend.
+        *      This should consist of alphanumberic, '-', and '_' characters.
+        *      This name should not be changed after use (e.g. with journaling).
+        *      Note that the name is *not* used in actual container names.
+        *   - domainId : Prefix to container names that is unique to this backend.
+        *      It should only consist of alphanumberic, '-', and '_' characters.
+        *      This ID is what avoids collisions if multiple logical backends
+        *      use the same storage system, so this should be set carefully.
         *   - lockManager : LockManager object to use for any file locking.
-        *                   If not provided, then no file locking will be enforced.
+        *      If not provided, then no file locking will be enforced.
         *   - fileJournal : FileJournal object to use for logging changes to files.
-        *                   If not provided, then change journaling will be disabled.
-        *   - readOnly    : Write operations are disallowed if this is a non-empty string.
-        *                   It should be an explanation for the backend being read-only.
+        *      If not provided, then change journaling will be disabled.
+        *   - readOnly : Write operations are disallowed if this is a non-empty string.
+        *      It should be an explanation for the backend being read-only.
         *   - parallelize : When to do file operations in parallel (when possible).
-        *                   Allowed values are "implicit", "explicit" and "off".
+        *      Allowed values are "implicit", "explicit" and "off".
         *   - concurrency : How many file operations can be done in parallel.
         *   - tmpDirectory : Directory to use for temporary files. If this is not set or null,
-        *                    then the backend will try to discover a usable temporary directory.
+        *      then the backend will try to discover a usable temporary directory.
+        *   - obResetFunc : alternative callback to clear the output buffer
+        *   - streamMimeFunc : alternative method to determine the content type from the path
+        *   - logger : Optional PSR logger object.
+        *   - profiler : Optional class name or object With profileIn/profileOut methods.
         * @throws InvalidArgumentException
         */
        public function __construct( array $config ) {
                $this->name = $config['name'];
-               $this->wikiId = $config['wikiId']; // e.g. "my_wiki-en_"
+               $this->domainId = isset( $config['domainId'] )
+                       ? $config['domainId'] // e.g. "my_wiki-en_"
+                       : $config['wikiId']; // b/c alias
                if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
                        throw new InvalidArgumentException( "Backend name '{$this->name}' is invalid." );
-               } elseif ( !is_string( $this->wikiId ) ) {
-                       throw new InvalidArgumentException( "Backend wiki ID not provided for '{$this->name}'." );
+               } elseif ( !is_string( $this->domainId ) ) {
+                       throw new InvalidArgumentException(
+                               "Backend domain ID not provided for '{$this->name}'." );
                }
                $this->lockManager = isset( $config['lockManager'] )
                        ? $config['lockManager']
@@ -164,10 +185,24 @@ abstract class FileBackend {
                $this->concurrency = isset( $config['concurrency'] )
                        ? (int)$config['concurrency']
                        : 50;
+               $this->obResetFunc = isset( $params['obResetFunc'] )
+                       ? $params['obResetFunc']
+                       : [ $this, 'resetOutputBuffer' ];
+               $this->streamMimeFunc = isset( $params['streamMimeFunc'] )
+                       ? $params['streamMimeFunc']
+                       : null;
+               $this->statusWrapper = isset( $config['statusWrapper'] ) ? $config['statusWrapper'] : null;
+
+               $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
+               $this->logger = isset( $config['logger'] ) ? $config['logger'] : new \Psr\Log\NullLogger();
                $this->statusWrapper = isset( $config['statusWrapper'] ) ? $config['statusWrapper'] : null;
                $this->tmpDirectory = isset( $config['tmpDirectory'] ) ? $config['tmpDirectory'] : null;
        }
 
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
        /**
         * Get the unique backend name.
         * We may have multiple different backends of the same type.
@@ -180,14 +215,22 @@ abstract class FileBackend {
        }
 
        /**
-        * Get the wiki identifier used for this backend (possibly empty).
-        * Note that this might *not* be in the same format as wfWikiID().
+        * Get the domain identifier used for this backend (possibly empty).
         *
         * @return string
+        * @since 1.28
+        */
+       final public function getDomainId() {
+               return $this->domainId;
+       }
+
+       /**
+        * Alias to getDomainId()
+        * @return string
         * @since 1.20
         */
        final public function getWikiId() {
-               return $this->wikiId;
+               return $this->getDomainId();
        }
 
        /**
@@ -1569,4 +1612,27 @@ abstract class FileBackend {
        final protected function wrapStatus( StatusValue $sv ) {
                return $this->statusWrapper ? call_user_func( $this->statusWrapper, $sv ) : $sv;
        }
+
+       /**
+        * @param string $section
+        * @return ScopedCallback|null
+        */
+       protected function scopedProfileSection( $section ) {
+               if ( $this->profiler ) {
+                       call_user_func( [ $this->profiler, 'profileIn' ], $section );
+                       return new ScopedCallback( [ $this->profiler, 'profileOut' ] );
+               }
+
+               return null;
+       }
+
+       protected function resetOutputBuffer() {
+               while ( ob_get_status() ) {
+                       if ( !ob_end_clean() ) {
+                               // Could not remove output buffer handler; abort now
+                               // to avoid getting in some kind of infinite loop.
+                               break;
+                       }
+               }
+       }
 }
diff --git a/includes/libs/filebackend/FileBackendMultiWrite.php b/includes/libs/filebackend/FileBackendMultiWrite.php
new file mode 100644 (file)
index 0000000..7c32d02
--- /dev/null
@@ -0,0 +1,753 @@
+<?php
+/**
+ * Proxy backend that mirrors writes to several internal backends.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Proxy backend that mirrors writes to several internal backends.
+ *
+ * This class defines a multi-write backend. Multiple backends can be
+ * registered to this proxy backend and it will act as a single backend.
+ * Use this when all access to those backends is through this proxy backend.
+ * At least one of the backends must be declared the "master" backend.
+ *
+ * Only use this class when transitioning from one storage system to another.
+ *
+ * Read operations are only done on the 'master' backend for consistency.
+ * Write operations are performed on all backends, starting with the master.
+ * This makes a best-effort to have transactional semantics, but since requests
+ * may sometimes fail, the use of "autoResync" or background scripts to fix
+ * inconsistencies is important.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+class FileBackendMultiWrite extends FileBackend {
+       /** @var FileBackendStore[] Prioritized list of FileBackendStore objects */
+       protected $backends = [];
+
+       /** @var int Index of master backend */
+       protected $masterIndex = -1;
+       /** @var int Index of read affinity backend */
+       protected $readIndex = -1;
+
+       /** @var int Bitfield */
+       protected $syncChecks = 0;
+       /** @var string|bool */
+       protected $autoResync = false;
+
+       /** @var bool */
+       protected $asyncWrites = false;
+
+       /* Possible internal backend consistency checks */
+       const CHECK_SIZE = 1;
+       const CHECK_TIME = 2;
+       const CHECK_SHA1 = 4;
+
+       /**
+        * Construct a proxy backend that consists of several internal backends.
+        * Locking, journaling, and read-only checks are handled by the proxy backend.
+        *
+        * Additional $config params include:
+        *   - backends       : Array of backend config and multi-backend settings.
+        *                      Each value is the config used in the constructor of a
+        *                      FileBackendStore class, but with these additional settings:
+        *                        - class         : The name of the backend class
+        *                        - isMultiMaster : This must be set for one backend.
+        *                        - readAffinity  : Use this for reads without 'latest' set.
+        *   - syncChecks     : Integer bitfield of internal backend sync checks to perform.
+        *                      Possible bits include the FileBackendMultiWrite::CHECK_* constants.
+        *                      There are constants for SIZE, TIME, and SHA1.
+        *                      The checks are done before allowing any file operations.
+        *   - autoResync     : Automatically resync the clone backends to the master backend
+        *                      when pre-operation sync checks fail. This should only be used
+        *                      if the master backend is stable and not missing any files.
+        *                      Use "conservative" to limit resyncing to copying newer master
+        *                      backend files over older (or non-existing) clone backend files.
+        *                      Cases that cannot be handled will result in operation abortion.
+        *   - replication    : Set to 'async' to defer file operations on the non-master backends.
+        *                      This will apply such updates post-send for web requests. Note that
+        *                      any checks from "syncChecks" are still synchronous.
+        *
+        * @param array $config
+        * @throws FileBackendError
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+               $this->syncChecks = isset( $config['syncChecks'] )
+                       ? $config['syncChecks']
+                       : self::CHECK_SIZE;
+               $this->autoResync = isset( $config['autoResync'] )
+                       ? $config['autoResync']
+                       : false;
+               $this->asyncWrites = isset( $config['replication'] ) && $config['replication'] === 'async';
+               // Construct backends here rather than via registration
+               // to keep these backends hidden from outside the proxy.
+               $namesUsed = [];
+               foreach ( $config['backends'] as $index => $config ) {
+                       $name = $config['name'];
+                       if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
+                               throw new LogicException( "Two or more backends defined with the name $name." );
+                       }
+                       $namesUsed[$name] = 1;
+                       // Alter certain sub-backend settings for sanity
+                       unset( $config['readOnly'] ); // use proxy backend setting
+                       unset( $config['fileJournal'] ); // use proxy backend journal
+                       unset( $config['lockManager'] ); // lock under proxy backend
+                       $config['domainId'] = $this->domainId; // use the proxy backend wiki ID
+                       if ( !empty( $config['isMultiMaster'] ) ) {
+                               if ( $this->masterIndex >= 0 ) {
+                                       throw new LogicException( 'More than one master backend defined.' );
+                               }
+                               $this->masterIndex = $index; // this is the "master"
+                               $config['fileJournal'] = $this->fileJournal; // log under proxy backend
+                       }
+                       if ( !empty( $config['readAffinity'] ) ) {
+                               $this->readIndex = $index; // prefer this for reads
+                       }
+                       // Create sub-backend object
+                       if ( !isset( $config['class'] ) ) {
+                               throw new InvalidArgumentException( 'No class given for a backend config.' );
+                       }
+                       $class = $config['class'];
+                       $this->backends[$index] = new $class( $config );
+               }
+               if ( $this->masterIndex < 0 ) { // need backends and must have a master
+                       throw new LogicException( 'No master backend defined.' );
+               }
+               if ( $this->readIndex < 0 ) {
+                       $this->readIndex = $this->masterIndex; // default
+               }
+       }
+
+       final protected function doOperationsInternal( array $ops, array $opts ) {
+               $status = $this->newStatus();
+
+               $mbe = $this->backends[$this->masterIndex]; // convenience
+
+               // Try to lock those files for the scope of this function...
+               $scopeLock = null;
+               if ( empty( $opts['nonLocking'] ) ) {
+                       // Try to lock those files for the scope of this function...
+                       /** @noinspection PhpUnusedLocalVariableInspection */
+                       $scopeLock = $this->getScopedLocksForOps( $ops, $status );
+                       if ( !$status->isOK() ) {
+                               return $status; // abort
+                       }
+               }
+               // Clear any cache entries (after locks acquired)
+               $this->clearCache();
+               $opts['preserveCache'] = true; // only locked files are cached
+               // Get the list of paths to read/write...
+               $relevantPaths = $this->fileStoragePathsForOps( $ops );
+               // Check if the paths are valid and accessible on all backends...
+               $status->merge( $this->accessibilityCheck( $relevantPaths ) );
+               if ( !$status->isOK() ) {
+                       return $status; // abort
+               }
+               // Do a consistency check to see if the backends are consistent...
+               $syncStatus = $this->consistencyCheck( $relevantPaths );
+               if ( !$syncStatus->isOK() ) {
+                       wfDebugLog( 'FileOperation', get_class( $this ) .
+                               " failed sync check: " . FormatJson::encode( $relevantPaths ) );
+                       // Try to resync the clone backends to the master on the spot...
+                       if ( $this->autoResync === false
+                               || !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK()
+                       ) {
+                               $status->merge( $syncStatus );
+
+                               return $status; // abort
+                       }
+               }
+               // Actually attempt the operation batch on the master backend...
+               $realOps = $this->substOpBatchPaths( $ops, $mbe );
+               $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.
+               if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
+                       foreach ( $this->backends as $index => $backend ) {
+                               if ( $index === $this->masterIndex ) {
+                                       continue; // done already
+                               }
+
+                               $realOps = $this->substOpBatchPaths( $ops, $backend );
+                               if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
+                                       // Bind $scopeLock to the callback to preserve locks
+                                       DeferredUpdates::addCallableUpdate(
+                                               function() use ( $backend, $realOps, $opts, $scopeLock, $relevantPaths ) {
+                                                       wfDebugLog( 'FileOperationReplication',
+                                                               "'{$backend->getName()}' async replication; paths: " .
+                                                               FormatJson::encode( $relevantPaths ) );
+                                                       $backend->doOperations( $realOps, $opts );
+                                               }
+                                       );
+                               } else {
+                                       wfDebugLog( 'FileOperationReplication',
+                                               "'{$backend->getName()}' sync replication; paths: " .
+                                               FormatJson::encode( $relevantPaths ) );
+                                       $status->merge( $backend->doOperations( $realOps, $opts ) );
+                               }
+                       }
+               }
+               // Make 'success', 'successCount', and 'failCount' fields reflect
+               // the overall operation, rather than all the batches for each backend.
+               // Do this by only using success values from the master backend's batch.
+               $status->success = $masterStatus->success;
+               $status->successCount = $masterStatus->successCount;
+               $status->failCount = $masterStatus->failCount;
+
+               return $status;
+       }
+
+       /**
+        * Check that a set of files are consistent across all internal backends
+        *
+        * @param array $paths List of storage paths
+        * @return StatusValue
+        */
+       public function consistencyCheck( array $paths ) {
+               $status = $this->newStatus();
+               if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
+                       return $status; // skip checks
+               }
+
+               // Preload all of the stat info in as few round trips as possible...
+               foreach ( $this->backends as $backend ) {
+                       $realPaths = $this->substPaths( $paths, $backend );
+                       $backend->preloadFileStat( [ 'srcs' => $realPaths, 'latest' => true ] );
+               }
+
+               $mBackend = $this->backends[$this->masterIndex];
+               foreach ( $paths as $path ) {
+                       $params = [ 'src' => $path, 'latest' => true ];
+                       $mParams = $this->substOpPaths( $params, $mBackend );
+                       // Stat the file on the 'master' backend
+                       $mStat = $mBackend->getFileStat( $mParams );
+                       if ( $this->syncChecks & self::CHECK_SHA1 ) {
+                               $mSha1 = $mBackend->getFileSha1Base36( $mParams );
+                       } else {
+                               $mSha1 = false;
+                       }
+                       // Check if all clone backends agree with the master...
+                       foreach ( $this->backends as $index => $cBackend ) {
+                               if ( $index === $this->masterIndex ) {
+                                       continue; // master
+                               }
+                               $cParams = $this->substOpPaths( $params, $cBackend );
+                               $cStat = $cBackend->getFileStat( $cParams );
+                               if ( $mStat ) { // file is in master
+                                       if ( !$cStat ) { // file should exist
+                                               $status->fatal( 'backend-fail-synced', $path );
+                                               continue;
+                                       }
+                                       if ( $this->syncChecks & self::CHECK_SIZE ) {
+                                               if ( $cStat['size'] != $mStat['size'] ) { // wrong size
+                                                       $status->fatal( 'backend-fail-synced', $path );
+                                                       continue;
+                                               }
+                                       }
+                                       if ( $this->syncChecks & self::CHECK_TIME ) {
+                                               $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
+                                               $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
+                                               if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
+                                                       $status->fatal( 'backend-fail-synced', $path );
+                                                       continue;
+                                               }
+                                       }
+                                       if ( $this->syncChecks & self::CHECK_SHA1 ) {
+                                               if ( $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1
+                                                       $status->fatal( 'backend-fail-synced', $path );
+                                                       continue;
+                                               }
+                                       }
+                               } else { // file is not in master
+                                       if ( $cStat ) { // file should not exist
+                                               $status->fatal( 'backend-fail-synced', $path );
+                                       }
+                               }
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * Check that a set of file paths are usable across all internal backends
+        *
+        * @param array $paths List of storage paths
+        * @return StatusValue
+        */
+       public function accessibilityCheck( array $paths ) {
+               $status = $this->newStatus();
+               if ( count( $this->backends ) <= 1 ) {
+                       return $status; // skip checks
+               }
+
+               foreach ( $paths as $path ) {
+                       foreach ( $this->backends as $backend ) {
+                               $realPath = $this->substPaths( $path, $backend );
+                               if ( !$backend->isPathUsableInternal( $realPath ) ) {
+                                       $status->fatal( 'backend-fail-usable', $path );
+                               }
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * Check that a set of files are consistent across all internal backends
+        * and re-synchronize those files against the "multi master" if needed.
+        *
+        * @param array $paths List of storage paths
+        * @param string|bool $resyncMode False, True, or "conservative"; see __construct()
+        * @return StatusValue
+        */
+       public function resyncFiles( array $paths, $resyncMode = true ) {
+               $status = $this->newStatus();
+
+               $mBackend = $this->backends[$this->masterIndex];
+               foreach ( $paths as $path ) {
+                       $mPath = $this->substPaths( $path, $mBackend );
+                       $mSha1 = $mBackend->getFileSha1Base36( [ 'src' => $mPath, 'latest' => true ] );
+                       $mStat = $mBackend->getFileStat( [ 'src' => $mPath, 'latest' => true ] );
+                       if ( $mStat === null || ( $mSha1 !== false && !$mStat ) ) { // sanity
+                               $status->fatal( 'backend-fail-internal', $this->name );
+                               wfDebugLog( 'FileOperation', __METHOD__
+                                       . ': File is not available on the master backend' );
+                               continue; // file is not available on the master backend...
+                       }
+                       // Check of all clone backends agree with the master...
+                       foreach ( $this->backends as $index => $cBackend ) {
+                               if ( $index === $this->masterIndex ) {
+                                       continue; // master
+                               }
+                               $cPath = $this->substPaths( $path, $cBackend );
+                               $cSha1 = $cBackend->getFileSha1Base36( [ 'src' => $cPath, 'latest' => true ] );
+                               $cStat = $cBackend->getFileStat( [ 'src' => $cPath, 'latest' => true ] );
+                               if ( $cStat === null || ( $cSha1 !== false && !$cStat ) ) { // sanity
+                                       $status->fatal( 'backend-fail-internal', $cBackend->getName() );
+                                       wfDebugLog( 'FileOperation', __METHOD__ .
+                                               ': File is not available on the clone backend' );
+                                       continue; // file is not available on the clone backend...
+                               }
+                               if ( $mSha1 === $cSha1 ) {
+                                       // already synced; nothing to do
+                               } elseif ( $mSha1 !== false ) { // file is in master
+                                       if ( $resyncMode === 'conservative'
+                                               && $cStat && $cStat['mtime'] > $mStat['mtime']
+                                       ) {
+                                               $status->fatal( 'backend-fail-synced', $path );
+                                               continue; // don't rollback data
+                                       }
+                                       $fsFile = $mBackend->getLocalReference(
+                                               [ 'src' => $mPath, 'latest' => true ] );
+                                       $status->merge( $cBackend->quickStore(
+                                               [ 'src' => $fsFile->getPath(), 'dst' => $cPath ]
+                                       ) );
+                               } elseif ( $mStat === false ) { // file is not in master
+                                       if ( $resyncMode === 'conservative' ) {
+                                               $status->fatal( 'backend-fail-synced', $path );
+                                               continue; // don't delete data
+                                       }
+                                       $status->merge( $cBackend->quickDelete( [ 'src' => $cPath ] ) );
+                               }
+                       }
+               }
+
+               if ( !$status->isOK() ) {
+                       wfDebugLog( 'FileOperation', get_class( $this ) .
+                               " failed to resync: " . FormatJson::encode( $paths ) );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Get a list of file storage paths to read or write for a list of operations
+        *
+        * @param array $ops Same format as doOperations()
+        * @return array List of storage paths to files (does not include directories)
+        */
+       protected function fileStoragePathsForOps( array $ops ) {
+               $paths = [];
+               foreach ( $ops as $op ) {
+                       if ( isset( $op['src'] ) ) {
+                               // For things like copy/move/delete with "ignoreMissingSource" and there
+                               // is no source file, nothing should happen and there should be no errors.
+                               if ( empty( $op['ignoreMissingSource'] )
+                                       || $this->fileExists( [ 'src' => $op['src'] ] )
+                               ) {
+                                       $paths[] = $op['src'];
+                               }
+                       }
+                       if ( isset( $op['srcs'] ) ) {
+                               $paths = array_merge( $paths, $op['srcs'] );
+                       }
+                       if ( isset( $op['dst'] ) ) {
+                               $paths[] = $op['dst'];
+                       }
+               }
+
+               return array_values( array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) ) );
+       }
+
+       /**
+        * Substitute the backend name in storage path parameters
+        * for a set of operations with that of a given internal backend.
+        *
+        * @param array $ops List of file operation arrays
+        * @param FileBackendStore $backend
+        * @return array
+        */
+       protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
+               $newOps = []; // operations
+               foreach ( $ops as $op ) {
+                       $newOp = $op; // operation
+                       foreach ( [ 'src', 'srcs', 'dst', 'dir' ] as $par ) {
+                               if ( isset( $newOp[$par] ) ) { // string or array
+                                       $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
+                               }
+                       }
+                       $newOps[] = $newOp;
+               }
+
+               return $newOps;
+       }
+
+       /**
+        * Same as substOpBatchPaths() but for a single operation
+        *
+        * @param array $ops File operation array
+        * @param FileBackendStore $backend
+        * @return array
+        */
+       protected function substOpPaths( array $ops, FileBackendStore $backend ) {
+               $newOps = $this->substOpBatchPaths( [ $ops ], $backend );
+
+               return $newOps[0];
+       }
+
+       /**
+        * Substitute the backend of storage paths with an internal backend's name
+        *
+        * @param array|string $paths List of paths or single string path
+        * @param FileBackendStore $backend
+        * @return array|string
+        */
+       protected function substPaths( $paths, FileBackendStore $backend ) {
+               return preg_replace(
+                       '!^mwstore://' . preg_quote( $this->name, '!' ) . '/!',
+                       StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
+                       $paths // string or array
+               );
+       }
+
+       /**
+        * Substitute the backend of internal storage paths with the proxy backend's name
+        *
+        * @param array|string $paths List of paths or single string path
+        * @return array|string
+        */
+       protected function unsubstPaths( $paths ) {
+               return preg_replace(
+                       '!^mwstore://([^/]+)!',
+                       StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
+                       $paths // string or array
+               );
+       }
+
+       /**
+        * @param array $ops File operations for FileBackend::doOperations()
+        * @return bool Whether there are file path sources with outside lifetime/ownership
+        */
+       protected function hasVolatileSources( array $ops ) {
+               foreach ( $ops as $op ) {
+                       if ( $op['op'] === 'store' && !isset( $op['srcRef'] ) ) {
+                               return true; // source file might be deleted anytime after do*Operations()
+                       }
+               }
+
+               return false;
+       }
+
+       protected function doQuickOperationsInternal( array $ops ) {
+               $status = $this->newStatus();
+               // Do the operations on the master backend; setting StatusValue fields...
+               $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
+               $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
+               $status->merge( $masterStatus );
+               // Propagate the operations to the clone backends...
+               foreach ( $this->backends as $index => $backend ) {
+                       if ( $index === $this->masterIndex ) {
+                               continue; // done already
+                       }
+
+                       $realOps = $this->substOpBatchPaths( $ops, $backend );
+                       if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
+                               DeferredUpdates::addCallableUpdate(
+                                       function() use ( $backend, $realOps ) {
+                                               $backend->doQuickOperations( $realOps );
+                                       }
+                               );
+                       } else {
+                               $status->merge( $backend->doQuickOperations( $realOps ) );
+                       }
+               }
+               // Make 'success', 'successCount', and 'failCount' fields reflect
+               // the overall operation, rather than all the batches for each backend.
+               // Do this by only using success values from the master backend's batch.
+               $status->success = $masterStatus->success;
+               $status->successCount = $masterStatus->successCount;
+               $status->failCount = $masterStatus->failCount;
+
+               return $status;
+       }
+
+       protected function doPrepare( array $params ) {
+               return $this->doDirectoryOp( 'prepare', $params );
+       }
+
+       protected function doSecure( array $params ) {
+               return $this->doDirectoryOp( 'secure', $params );
+       }
+
+       protected function doPublish( array $params ) {
+               return $this->doDirectoryOp( 'publish', $params );
+       }
+
+       protected function doClean( array $params ) {
+               return $this->doDirectoryOp( 'clean', $params );
+       }
+
+       /**
+        * @param string $method One of (doPrepare,doSecure,doPublish,doClean)
+        * @param array $params Method arguments
+        * @return StatusValue
+        */
+       protected function doDirectoryOp( $method, array $params ) {
+               $status = $this->newStatus();
+
+               $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+               $masterStatus = $this->backends[$this->masterIndex]->$method( $realParams );
+               $status->merge( $masterStatus );
+
+               foreach ( $this->backends as $index => $backend ) {
+                       if ( $index === $this->masterIndex ) {
+                               continue; // already done
+                       }
+
+                       $realParams = $this->substOpPaths( $params, $backend );
+                       if ( $this->asyncWrites ) {
+                               DeferredUpdates::addCallableUpdate(
+                                       function() use ( $backend, $method, $realParams ) {
+                                               $backend->$method( $realParams );
+                                       }
+                               );
+                       } else {
+                               $status->merge( $backend->$method( $realParams ) );
+                       }
+               }
+
+               return $status;
+       }
+
+       public function concatenate( array $params ) {
+               // We are writing to an FS file, so we don't need to do this per-backend
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->concatenate( $realParams );
+       }
+
+       public function fileExists( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->fileExists( $realParams );
+       }
+
+       public function getFileTimestamp( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->getFileTimestamp( $realParams );
+       }
+
+       public function getFileSize( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->getFileSize( $realParams );
+       }
+
+       public function getFileStat( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->getFileStat( $realParams );
+       }
+
+       public function getFileXAttributes( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->getFileXAttributes( $realParams );
+       }
+
+       public function getFileContentsMulti( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               $contentsM = $this->backends[$index]->getFileContentsMulti( $realParams );
+
+               $contents = []; // (path => FSFile) mapping using the proxy backend's name
+               foreach ( $contentsM as $path => $data ) {
+                       $contents[$this->unsubstPaths( $path )] = $data;
+               }
+
+               return $contents;
+       }
+
+       public function getFileSha1Base36( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->getFileSha1Base36( $realParams );
+       }
+
+       public function getFileProps( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->getFileProps( $realParams );
+       }
+
+       public function streamFile( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->streamFile( $realParams );
+       }
+
+       public function getLocalReferenceMulti( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               $fsFilesM = $this->backends[$index]->getLocalReferenceMulti( $realParams );
+
+               $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name
+               foreach ( $fsFilesM as $path => $fsFile ) {
+                       $fsFiles[$this->unsubstPaths( $path )] = $fsFile;
+               }
+
+               return $fsFiles;
+       }
+
+       public function getLocalCopyMulti( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               $tempFilesM = $this->backends[$index]->getLocalCopyMulti( $realParams );
+
+               $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name
+               foreach ( $tempFilesM as $path => $tempFile ) {
+                       $tempFiles[$this->unsubstPaths( $path )] = $tempFile;
+               }
+
+               return $tempFiles;
+       }
+
+       public function getFileHttpUrl( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->getFileHttpUrl( $realParams );
+       }
+
+       public function directoryExists( array $params ) {
+               $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+
+               return $this->backends[$this->masterIndex]->directoryExists( $realParams );
+       }
+
+       public function getDirectoryList( array $params ) {
+               $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+
+               return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
+       }
+
+       public function getFileList( array $params ) {
+               $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+
+               return $this->backends[$this->masterIndex]->getFileList( $realParams );
+       }
+
+       public function getFeatures() {
+               return $this->backends[$this->masterIndex]->getFeatures();
+       }
+
+       public function clearCache( array $paths = null ) {
+               foreach ( $this->backends as $backend ) {
+                       $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
+                       $backend->clearCache( $realPaths );
+               }
+       }
+
+       public function preloadCache( array $paths ) {
+               $realPaths = $this->substPaths( $paths, $this->backends[$this->readIndex] );
+               $this->backends[$this->readIndex]->preloadCache( $realPaths );
+       }
+
+       public function preloadFileStat( array $params ) {
+               $index = $this->getReadIndexFromParams( $params );
+               $realParams = $this->substOpPaths( $params, $this->backends[$index] );
+
+               return $this->backends[$index]->preloadFileStat( $realParams );
+       }
+
+       public function getScopedLocksForOps( array $ops, StatusValue $status ) {
+               $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
+               $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $realOps );
+               // Get the paths to lock from the master backend
+               $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
+               // Get the paths under the proxy backend's name
+               $pbPaths = [
+                       LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ),
+                       LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] )
+               ];
+
+               // Actually acquire the locks
+               return $this->getScopedFileLocks( $pbPaths, 'mixed', $status );
+       }
+
+       /**
+        * @param array $params
+        * @return int The master or read affinity backend index, based on $params['latest']
+        */
+       protected function getReadIndexFromParams( array $params ) {
+               return !empty( $params['latest'] ) ? $this->masterIndex : $this->readIndex;
+       }
+}
diff --git a/includes/libs/filebackend/FileBackendStore.php b/includes/libs/filebackend/FileBackendStore.php
new file mode 100644 (file)
index 0000000..b1b7652
--- /dev/null
@@ -0,0 +1,1983 @@
+<?php
+/**
+ * Base class for all backends using particular storage medium.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Base class for all backends using particular storage medium.
+ *
+ * This class defines the methods as abstract that subclasses must implement.
+ * Outside callers should *not* use functions with "Internal" in the name.
+ *
+ * The FileBackend operations are implemented using basic functions
+ * such as storeInternal(), copyInternal(), deleteInternal() and the like.
+ * This class is also responsible for path resolution and sanitization.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+abstract class FileBackendStore extends FileBackend {
+       /** @var WANObjectCache */
+       protected $memCache;
+       /** @var BagOStuff */
+       protected $srvCache;
+       /** @var ProcessCacheLRU Map of paths to small (RAM/disk) cache items */
+       protected $cheapCache;
+       /** @var ProcessCacheLRU Map of paths to large (RAM/disk) cache items */
+       protected $expensiveCache;
+
+       /** @var array Map of container names to sharding config */
+       protected $shardViaHashLevels = [];
+
+       /** @var callable Method to get the MIME type of files */
+       protected $mimeCallback;
+
+       protected $maxFileSize = 4294967296; // integer bytes (4GiB)
+
+       const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
+       const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache"
+       const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
+
+       /**
+        * @see FileBackend::__construct()
+        * Additional $config params include:
+        *   - srvCache     : BagOStuff cache to APC/XCache or the like.
+        *   - wanCache     : WANObjectCache object to use for persistent caching.
+        *   - mimeCallback : Callback that takes (storage path, content, file system path) and
+        *                    returns the MIME type of the file or 'unknown/unknown'. The file
+        *                    system path parameter should be used if the content one is null.
+        *
+        * @param array $config
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+               $this->mimeCallback = isset( $config['mimeCallback'] )
+                       ? $config['mimeCallback']
+                       : null;
+               $this->srvCache = new EmptyBagOStuff(); // disabled by default
+               $this->memCache = WANObjectCache::newEmpty(); // disabled by default
+               $this->cheapCache = new ProcessCacheLRU( self::CACHE_CHEAP_SIZE );
+               $this->expensiveCache = new ProcessCacheLRU( self::CACHE_EXPENSIVE_SIZE );
+       }
+
+       /**
+        * Get the maximum allowable file size given backend
+        * medium restrictions and basic performance constraints.
+        * Do not call this function from places outside FileBackend and FileOp.
+        *
+        * @return int Bytes
+        */
+       final public function maxFileSizeInternal() {
+               return $this->maxFileSize;
+       }
+
+       /**
+        * Check if a file can be created or changed at a given storage path.
+        * FS backends should check if the parent directory exists, files can be
+        * written under it, and that any file already there is writable.
+        * Backends using key/value stores should check if the container exists.
+        *
+        * @param string $storagePath
+        * @return bool
+        */
+       abstract public function isPathUsableInternal( $storagePath );
+
+       /**
+        * Create a file in the backend with the given contents.
+        * This will overwrite any file that exists at the destination.
+        * Do not call this function from places outside FileBackend and FileOp.
+        *
+        * $params include:
+        *   - content     : the raw file contents
+        *   - dst         : destination storage path
+        *   - headers     : HTTP header name/value map
+        *   - async       : StatusValue will be returned immediately if supported.
+        *                   If the StatusValue is OK, then its value field will be
+        *                   set to a FileBackendStoreOpHandle object.
+        *   - dstExists   : Whether a file exists at the destination (optimization).
+        *                   Callers can use "false" if no existing file is being changed.
+        *
+        * @param array $params
+        * @return StatusValue
+        */
+       final public function createInternal( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
+                       $status = $this->newStatus( 'backend-fail-maxsize',
+                               $params['dst'], $this->maxFileSizeInternal() );
+               } else {
+                       $status = $this->doCreateInternal( $params );
+                       $this->clearCache( [ $params['dst'] ] );
+                       if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
+                               $this->deleteFileCache( $params['dst'] ); // persistent cache
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::createInternal()
+        * @param array $params
+        * @return StatusValue
+        */
+       abstract protected function doCreateInternal( array $params );
+
+       /**
+        * Store a file into the backend from a file on disk.
+        * This will overwrite any file that exists at the destination.
+        * Do not call this function from places outside FileBackend and FileOp.
+        *
+        * $params include:
+        *   - src         : source path on disk
+        *   - dst         : destination storage path
+        *   - headers     : HTTP header name/value map
+        *   - async       : StatusValue will be returned immediately if supported.
+        *                   If the StatusValue is OK, then its value field will be
+        *                   set to a FileBackendStoreOpHandle object.
+        *   - dstExists   : Whether a file exists at the destination (optimization).
+        *                   Callers can use "false" if no existing file is being changed.
+        *
+        * @param array $params
+        * @return StatusValue
+        */
+       final public function storeInternal( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
+                       $status = $this->newStatus( 'backend-fail-maxsize',
+                               $params['dst'], $this->maxFileSizeInternal() );
+               } else {
+                       $status = $this->doStoreInternal( $params );
+                       $this->clearCache( [ $params['dst'] ] );
+                       if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
+                               $this->deleteFileCache( $params['dst'] ); // persistent cache
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::storeInternal()
+        * @param array $params
+        * @return StatusValue
+        */
+       abstract protected function doStoreInternal( array $params );
+
+       /**
+        * Copy a file from one storage path to another in the backend.
+        * This will overwrite any file that exists at the destination.
+        * Do not call this function from places outside FileBackend and FileOp.
+        *
+        * $params include:
+        *   - src                 : source storage path
+        *   - dst                 : destination storage path
+        *   - ignoreMissingSource : do nothing if the source file does not exist
+        *   - headers             : HTTP header name/value map
+        *   - async               : StatusValue will be returned immediately if supported.
+        *                           If the StatusValue is OK, then its value field will be
+        *                           set to a FileBackendStoreOpHandle object.
+        *   - dstExists           : Whether a file exists at the destination (optimization).
+        *                           Callers can use "false" if no existing file is being changed.
+        *
+        * @param array $params
+        * @return StatusValue
+        */
+       final public function copyInternal( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->doCopyInternal( $params );
+               $this->clearCache( [ $params['dst'] ] );
+               if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
+                       $this->deleteFileCache( $params['dst'] ); // persistent cache
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::copyInternal()
+        * @param array $params
+        * @return StatusValue
+        */
+       abstract protected function doCopyInternal( array $params );
+
+       /**
+        * Delete a file at the storage path.
+        * Do not call this function from places outside FileBackend and FileOp.
+        *
+        * $params include:
+        *   - src                 : source storage path
+        *   - ignoreMissingSource : do nothing if the source file does not exist
+        *   - async               : StatusValue will be returned immediately if supported.
+        *                           If the StatusValue is OK, then its value field will be
+        *                           set to a FileBackendStoreOpHandle object.
+        *
+        * @param array $params
+        * @return StatusValue
+        */
+       final public function deleteInternal( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->doDeleteInternal( $params );
+               $this->clearCache( [ $params['src'] ] );
+               $this->deleteFileCache( $params['src'] ); // persistent cache
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::deleteInternal()
+        * @param array $params
+        * @return StatusValue
+        */
+       abstract protected function doDeleteInternal( array $params );
+
+       /**
+        * Move a file from one storage path to another in the backend.
+        * This will overwrite any file that exists at the destination.
+        * Do not call this function from places outside FileBackend and FileOp.
+        *
+        * $params include:
+        *   - src                 : source storage path
+        *   - dst                 : destination storage path
+        *   - ignoreMissingSource : do nothing if the source file does not exist
+        *   - headers             : HTTP header name/value map
+        *   - async               : StatusValue will be returned immediately if supported.
+        *                           If the StatusValue is OK, then its value field will be
+        *                           set to a FileBackendStoreOpHandle object.
+        *   - dstExists           : Whether a file exists at the destination (optimization).
+        *                           Callers can use "false" if no existing file is being changed.
+        *
+        * @param array $params
+        * @return StatusValue
+        */
+       final public function moveInternal( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->doMoveInternal( $params );
+               $this->clearCache( [ $params['src'], $params['dst'] ] );
+               $this->deleteFileCache( $params['src'] ); // persistent cache
+               if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
+                       $this->deleteFileCache( $params['dst'] ); // persistent cache
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::moveInternal()
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function doMoveInternal( array $params ) {
+               unset( $params['async'] ); // two steps, won't work here :)
+               $nsrc = FileBackend::normalizeStoragePath( $params['src'] );
+               $ndst = FileBackend::normalizeStoragePath( $params['dst'] );
+               // Copy source to dest
+               $status = $this->copyInternal( $params );
+               if ( $nsrc !== $ndst && $status->isOK() ) {
+                       // Delete source (only fails due to races or network problems)
+                       $status->merge( $this->deleteInternal( [ 'src' => $params['src'] ] ) );
+                       $status->setResult( true, $status->value ); // ignore delete() errors
+               }
+
+               return $status;
+       }
+
+       /**
+        * Alter metadata for a file at the storage path.
+        * Do not call this function from places outside FileBackend and FileOp.
+        *
+        * $params include:
+        *   - src           : source storage path
+        *   - headers       : HTTP header name/value map
+        *   - async         : StatusValue will be returned immediately if supported.
+        *                     If the StatusValue is OK, then its value field will be
+        *                     set to a FileBackendStoreOpHandle object.
+        *
+        * @param array $params
+        * @return StatusValue
+        */
+       final public function describeInternal( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               if ( count( $params['headers'] ) ) {
+                       $status = $this->doDescribeInternal( $params );
+                       $this->clearCache( [ $params['src'] ] );
+                       $this->deleteFileCache( $params['src'] ); // persistent cache
+               } else {
+                       $status = $this->newStatus(); // nothing to do
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::describeInternal()
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function doDescribeInternal( array $params ) {
+               return $this->newStatus();
+       }
+
+       /**
+        * No-op file operation that does nothing.
+        * Do not call this function from places outside FileBackend and FileOp.
+        *
+        * @param array $params
+        * @return StatusValue
+        */
+       final public function nullInternal( array $params ) {
+               return $this->newStatus();
+       }
+
+       final public function concatenate( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->newStatus();
+
+               // Try to lock the source files for the scope of this function
+               $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
+               if ( $status->isOK() ) {
+                       // Actually do the file concatenation...
+                       $start_time = microtime( true );
+                       $status->merge( $this->doConcatenate( $params ) );
+                       $sec = microtime( true ) - $start_time;
+                       if ( !$status->isOK() ) {
+                               $this->logger->error( get_class( $this ) . "-{$this->name}" .
+                                       " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" );
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::concatenate()
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function doConcatenate( array $params ) {
+               $status = $this->newStatus();
+               $tmpPath = $params['dst']; // convenience
+               unset( $params['latest'] ); // sanity
+
+               // Check that the specified temp file is valid...
+               MediaWiki\suppressWarnings();
+               $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
+               MediaWiki\restoreWarnings();
+               if ( !$ok ) { // not present or not empty
+                       $status->fatal( 'backend-fail-opentemp', $tmpPath );
+
+                       return $status;
+               }
+
+               // Get local FS versions of the chunks needed for the concatenation...
+               $fsFiles = $this->getLocalReferenceMulti( $params );
+               foreach ( $fsFiles as $path => &$fsFile ) {
+                       if ( !$fsFile ) { // chunk failed to download?
+                               $fsFile = $this->getLocalReference( [ 'src' => $path ] );
+                               if ( !$fsFile ) { // retry failed?
+                                       $status->fatal( 'backend-fail-read', $path );
+
+                                       return $status;
+                               }
+                       }
+               }
+               unset( $fsFile ); // unset reference so we can reuse $fsFile
+
+               // Get a handle for the destination temp file
+               $tmpHandle = fopen( $tmpPath, 'ab' );
+               if ( $tmpHandle === false ) {
+                       $status->fatal( 'backend-fail-opentemp', $tmpPath );
+
+                       return $status;
+               }
+
+               // Build up the temp file using the source chunks (in order)...
+               foreach ( $fsFiles as $virtualSource => $fsFile ) {
+                       // Get a handle to the local FS version
+                       $sourceHandle = fopen( $fsFile->getPath(), 'rb' );
+                       if ( $sourceHandle === false ) {
+                               fclose( $tmpHandle );
+                               $status->fatal( 'backend-fail-read', $virtualSource );
+
+                               return $status;
+                       }
+                       // Append chunk to file (pass chunk size to avoid magic quotes)
+                       if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
+                               fclose( $sourceHandle );
+                               fclose( $tmpHandle );
+                               $status->fatal( 'backend-fail-writetemp', $tmpPath );
+
+                               return $status;
+                       }
+                       fclose( $sourceHandle );
+               }
+               if ( !fclose( $tmpHandle ) ) {
+                       $status->fatal( 'backend-fail-closetemp', $tmpPath );
+
+                       return $status;
+               }
+
+               clearstatcache(); // temp file changed
+
+               return $status;
+       }
+
+       final protected function doPrepare( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->newStatus();
+
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
+
+                       return $status; // invalid storage path
+               }
+
+               if ( $shard !== null ) { // confined to a single container/shard
+                       $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
+               } else { // directory is on several shards
+                       $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
+                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
+                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+                               $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::doPrepare()
+        * @param string $container
+        * @param string $dir
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function doPrepareInternal( $container, $dir, array $params ) {
+               return $this->newStatus();
+       }
+
+       final protected function doSecure( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->newStatus();
+
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
+
+                       return $status; // invalid storage path
+               }
+
+               if ( $shard !== null ) { // confined to a single container/shard
+                       $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
+               } else { // directory is on several shards
+                       $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
+                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
+                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+                               $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::doSecure()
+        * @param string $container
+        * @param string $dir
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function doSecureInternal( $container, $dir, array $params ) {
+               return $this->newStatus();
+       }
+
+       final protected function doPublish( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->newStatus();
+
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
+
+                       return $status; // invalid storage path
+               }
+
+               if ( $shard !== null ) { // confined to a single container/shard
+                       $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
+               } else { // directory is on several shards
+                       $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
+                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
+                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+                               $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::doPublish()
+        * @param string $container
+        * @param string $dir
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function doPublishInternal( $container, $dir, array $params ) {
+               return $this->newStatus();
+       }
+
+       final protected function doClean( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->newStatus();
+
+               // Recursive: first delete all empty subdirs recursively
+               if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
+                       $subDirsRel = $this->getTopDirectoryList( [ 'dir' => $params['dir'] ] );
+                       if ( $subDirsRel !== null ) { // no errors
+                               foreach ( $subDirsRel as $subDirRel ) {
+                                       $subDir = $params['dir'] . "/{$subDirRel}"; // full path
+                                       $status->merge( $this->doClean( [ 'dir' => $subDir ] + $params ) );
+                               }
+                               unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends)
+                       }
+               }
+
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
+
+                       return $status; // invalid storage path
+               }
+
+               // Attempt to lock this directory...
+               $filesLockEx = [ $params['dir'] ];
+               $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
+               if ( !$status->isOK() ) {
+                       return $status; // abort
+               }
+
+               if ( $shard !== null ) { // confined to a single container/shard
+                       $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
+                       $this->deleteContainerCache( $fullCont ); // purge cache
+               } else { // directory is on several shards
+                       $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
+                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
+                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+                               $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+                               $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::doClean()
+        * @param string $container
+        * @param string $dir
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function doCleanInternal( $container, $dir, array $params ) {
+               return $this->newStatus();
+       }
+
+       final public function fileExists( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $stat = $this->getFileStat( $params );
+
+               return ( $stat === null ) ? null : (bool)$stat; // null => failure
+       }
+
+       final public function getFileTimestamp( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $stat = $this->getFileStat( $params );
+
+               return $stat ? $stat['mtime'] : false;
+       }
+
+       final public function getFileSize( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $stat = $this->getFileStat( $params );
+
+               return $stat ? $stat['size'] : false;
+       }
+
+       final public function getFileStat( array $params ) {
+               $path = self::normalizeStoragePath( $params['src'] );
+               if ( $path === null ) {
+                       return false; // invalid storage path
+               }
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $latest = !empty( $params['latest'] ); // use latest data?
+               if ( !$latest && !$this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
+                       $this->primeFileCache( [ $path ] ); // check persistent cache
+               }
+               if ( $this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
+                       $stat = $this->cheapCache->get( $path, 'stat' );
+                       // If we want the latest data, check that this cached
+                       // value was in fact fetched with the latest available data.
+                       if ( is_array( $stat ) ) {
+                               if ( !$latest || $stat['latest'] ) {
+                                       return $stat;
+                               }
+                       } elseif ( in_array( $stat, [ 'NOT_EXIST', 'NOT_EXIST_LATEST' ] ) ) {
+                               if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) {
+                                       return false;
+                               }
+                       }
+               }
+               $stat = $this->doGetFileStat( $params );
+               if ( is_array( $stat ) ) { // file exists
+                       // Strongly consistent backends can automatically set "latest"
+                       $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
+                       $this->cheapCache->set( $path, 'stat', $stat );
+                       $this->setFileCache( $path, $stat ); // update persistent cache
+                       if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
+                               $this->cheapCache->set( $path, 'sha1',
+                                       [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
+                       }
+                       if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
+                               $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
+                               $this->cheapCache->set( $path, 'xattr',
+                                       [ 'map' => $stat['xattr'], 'latest' => $latest ] );
+                       }
+               } elseif ( $stat === false ) { // file does not exist
+                       $this->cheapCache->set( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
+                       $this->cheapCache->set( $path, 'xattr', [ 'map' => false, 'latest' => $latest ] );
+                       $this->cheapCache->set( $path, 'sha1', [ 'hash' => false, 'latest' => $latest ] );
+                       $this->logger->debug( __METHOD__ . ": File $path does not exist.\n" );
+               } else { // an error occurred
+                       $this->logger->warning( __METHOD__ . ": Could not stat file $path.\n" );
+               }
+
+               return $stat;
+       }
+
+       /**
+        * @see FileBackendStore::getFileStat()
+        * @param array $params
+        */
+       abstract protected function doGetFileStat( array $params );
+
+       public function getFileContentsMulti( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
+               $params = $this->setConcurrencyFlags( $params );
+               $contents = $this->doGetFileContentsMulti( $params );
+
+               return $contents;
+       }
+
+       /**
+        * @see FileBackendStore::getFileContentsMulti()
+        * @param array $params
+        * @return array
+        */
+       protected function doGetFileContentsMulti( array $params ) {
+               $contents = [];
+               foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
+                       MediaWiki\suppressWarnings();
+                       $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
+                       MediaWiki\restoreWarnings();
+               }
+
+               return $contents;
+       }
+
+       final public function getFileXAttributes( array $params ) {
+               $path = self::normalizeStoragePath( $params['src'] );
+               if ( $path === null ) {
+                       return false; // invalid storage path
+               }
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $latest = !empty( $params['latest'] ); // use latest data?
+               if ( $this->cheapCache->has( $path, 'xattr', self::CACHE_TTL ) ) {
+                       $stat = $this->cheapCache->get( $path, 'xattr' );
+                       // If we want the latest data, check that this cached
+                       // value was in fact fetched with the latest available data.
+                       if ( !$latest || $stat['latest'] ) {
+                               return $stat['map'];
+                       }
+               }
+               $fields = $this->doGetFileXAttributes( $params );
+               $fields = is_array( $fields ) ? self::normalizeXAttributes( $fields ) : false;
+               $this->cheapCache->set( $path, 'xattr', [ 'map' => $fields, 'latest' => $latest ] );
+
+               return $fields;
+       }
+
+       /**
+        * @see FileBackendStore::getFileXAttributes()
+        * @param array $params
+        * @return bool|string
+        */
+       protected function doGetFileXAttributes( array $params ) {
+               return [ 'headers' => [], 'metadata' => [] ]; // not supported
+       }
+
+       final public function getFileSha1Base36( array $params ) {
+               $path = self::normalizeStoragePath( $params['src'] );
+               if ( $path === null ) {
+                       return false; // invalid storage path
+               }
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $latest = !empty( $params['latest'] ); // use latest data?
+               if ( $this->cheapCache->has( $path, 'sha1', self::CACHE_TTL ) ) {
+                       $stat = $this->cheapCache->get( $path, 'sha1' );
+                       // If we want the latest data, check that this cached
+                       // value was in fact fetched with the latest available data.
+                       if ( !$latest || $stat['latest'] ) {
+                               return $stat['hash'];
+                       }
+               }
+               $hash = $this->doGetFileSha1Base36( $params );
+               $this->cheapCache->set( $path, 'sha1', [ 'hash' => $hash, 'latest' => $latest ] );
+
+               return $hash;
+       }
+
+       /**
+        * @see FileBackendStore::getFileSha1Base36()
+        * @param array $params
+        * @return bool|string
+        */
+       protected function doGetFileSha1Base36( array $params ) {
+               $fsFile = $this->getLocalReference( $params );
+               if ( !$fsFile ) {
+                       return false;
+               } else {
+                       return $fsFile->getSha1Base36();
+               }
+       }
+
+       final public function getFileProps( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $fsFile = $this->getLocalReference( $params );
+               $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
+
+               return $props;
+       }
+
+       final public function getLocalReferenceMulti( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
+               $params = $this->setConcurrencyFlags( $params );
+
+               $fsFiles = []; // (path => FSFile)
+               $latest = !empty( $params['latest'] ); // use latest data?
+               // Reuse any files already in process cache...
+               foreach ( $params['srcs'] as $src ) {
+                       $path = self::normalizeStoragePath( $src );
+                       if ( $path === null ) {
+                               $fsFiles[$src] = null; // invalid storage path
+                       } elseif ( $this->expensiveCache->has( $path, 'localRef' ) ) {
+                               $val = $this->expensiveCache->get( $path, 'localRef' );
+                               // If we want the latest data, check that this cached
+                               // value was in fact fetched with the latest available data.
+                               if ( !$latest || $val['latest'] ) {
+                                       $fsFiles[$src] = $val['object'];
+                               }
+                       }
+               }
+               // Fetch local references of any remaning files...
+               $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
+               foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
+                       $fsFiles[$path] = $fsFile;
+                       if ( $fsFile ) { // update the process cache...
+                               $this->expensiveCache->set( $path, 'localRef',
+                                       [ 'object' => $fsFile, 'latest' => $latest ] );
+                       }
+               }
+
+               return $fsFiles;
+       }
+
+       /**
+        * @see FileBackendStore::getLocalReferenceMulti()
+        * @param array $params
+        * @return array
+        */
+       protected function doGetLocalReferenceMulti( array $params ) {
+               return $this->doGetLocalCopyMulti( $params );
+       }
+
+       final public function getLocalCopyMulti( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
+               $params = $this->setConcurrencyFlags( $params );
+               $tmpFiles = $this->doGetLocalCopyMulti( $params );
+
+               return $tmpFiles;
+       }
+
+       /**
+        * @see FileBackendStore::getLocalCopyMulti()
+        * @param array $params
+        * @return array
+        */
+       abstract protected function doGetLocalCopyMulti( array $params );
+
+       /**
+        * @see FileBackend::getFileHttpUrl()
+        * @param array $params
+        * @return string|null
+        */
+       public function getFileHttpUrl( array $params ) {
+               return null; // not supported
+       }
+
+       final public function streamFile( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->newStatus();
+
+               // Always set some fields for subclass convenience
+               $params['options'] = isset( $params['options'] ) ? $params['options'] : [];
+               $params['headers'] = isset( $params['headers'] ) ? $params['headers'] : [];
+
+               // Don't stream it out as text/html if there was a PHP error
+               if ( ( empty( $params['headless'] ) || $params['headers'] ) && headers_sent() ) {
+                       print "Headers already sent, terminating.\n";
+                       $status->fatal( 'backend-fail-stream', $params['src'] );
+                       return $status;
+               }
+
+               $status->merge( $this->doStreamFile( $params ) );
+
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::streamFile()
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function doStreamFile( array $params ) {
+               $status = $this->newStatus();
+
+               $flags = 0;
+               $flags |= !empty( $params['headless'] ) ? HTTPFileStreamer::STREAM_HEADLESS : 0;
+               $flags |= !empty( $params['allowOB'] ) ? HTTPFileStreamer::STREAM_ALLOW_OB : 0;
+
+               $fsFile = $this->getLocalReference( $params );
+               if ( $fsFile ) {
+                       $streamer = new HTTPFileStreamer(
+                               $fsFile->getPath(),
+                               [
+                                       'obResetFunc' => $this->obResetFunc,
+                                       'streamMimeFunc' => $this->streamMimeFunc
+                               ]
+                       );
+                       $res = $streamer->stream( $params['headers'], true, $params['options'], $flags );
+               } else {
+                       $res = false;
+                       HTTPFileStreamer::send404Message( $params['src'], $flags );
+               }
+
+               if ( !$res ) {
+                       $status->fatal( 'backend-fail-stream', $params['src'] );
+               }
+
+               return $status;
+       }
+
+       final public function directoryExists( array $params ) {
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) {
+                       return false; // invalid storage path
+               }
+               if ( $shard !== null ) { // confined to a single container/shard
+                       return $this->doDirectoryExists( $fullCont, $dir, $params );
+               } else { // directory is on several shards
+                       $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
+                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
+                       $res = false; // response
+                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+                               $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
+                               if ( $exists ) {
+                                       $res = true;
+                                       break; // found one!
+                               } elseif ( $exists === null ) { // error?
+                                       $res = null; // if we don't find anything, it is indeterminate
+                               }
+                       }
+
+                       return $res;
+               }
+       }
+
+       /**
+        * @see FileBackendStore::directoryExists()
+        *
+        * @param string $container Resolved container name
+        * @param string $dir Resolved path relative to container
+        * @param array $params
+        * @return bool|null
+        */
+       abstract protected function doDirectoryExists( $container, $dir, array $params );
+
+       final public function getDirectoryList( array $params ) {
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) { // invalid storage path
+                       return null;
+               }
+               if ( $shard !== null ) {
+                       // File listing is confined to a single container/shard
+                       return $this->getDirectoryListInternal( $fullCont, $dir, $params );
+               } else {
+                       $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
+                       // File listing spans multiple containers/shards
+                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
+
+                       return new FileBackendStoreShardDirIterator( $this,
+                               $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
+               }
+       }
+
+       /**
+        * Do not call this function from places outside FileBackend
+        *
+        * @see FileBackendStore::getDirectoryList()
+        *
+        * @param string $container Resolved container name
+        * @param string $dir Resolved path relative to container
+        * @param array $params
+        * @return Traversable|array|null Returns null on failure
+        */
+       abstract public function getDirectoryListInternal( $container, $dir, array $params );
+
+       final public function getFileList( array $params ) {
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) { // invalid storage path
+                       return null;
+               }
+               if ( $shard !== null ) {
+                       // File listing is confined to a single container/shard
+                       return $this->getFileListInternal( $fullCont, $dir, $params );
+               } else {
+                       $this->logger->debug( __METHOD__ . ": iterating over all container shards.\n" );
+                       // File listing spans multiple containers/shards
+                       list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
+
+                       return new FileBackendStoreShardFileIterator( $this,
+                               $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
+               }
+       }
+
+       /**
+        * Do not call this function from places outside FileBackend
+        *
+        * @see FileBackendStore::getFileList()
+        *
+        * @param string $container Resolved container name
+        * @param string $dir Resolved path relative to container
+        * @param array $params
+        * @return Traversable|array|null Returns null on failure
+        */
+       abstract public function getFileListInternal( $container, $dir, array $params );
+
+       /**
+        * Return a list of FileOp objects from a list of operations.
+        * Do not call this function from places outside FileBackend.
+        *
+        * The result must have the same number of items as the input.
+        * An exception is thrown if an unsupported operation is requested.
+        *
+        * @param array $ops Same format as doOperations()
+        * @return FileOp[] List of FileOp objects
+        * @throws FileBackendError
+        */
+       final public function getOperationsInternal( array $ops ) {
+               $supportedOps = [
+                       'store' => 'StoreFileOp',
+                       'copy' => 'CopyFileOp',
+                       'move' => 'MoveFileOp',
+                       'delete' => 'DeleteFileOp',
+                       'create' => 'CreateFileOp',
+                       'describe' => 'DescribeFileOp',
+                       'null' => 'NullFileOp'
+               ];
+
+               $performOps = []; // array of FileOp objects
+               // Build up ordered array of FileOps...
+               foreach ( $ops as $operation ) {
+                       $opName = $operation['op'];
+                       if ( isset( $supportedOps[$opName] ) ) {
+                               $class = $supportedOps[$opName];
+                               // Get params for this operation
+                               $params = $operation;
+                               // Append the FileOp class
+                               $performOps[] = new $class( $this, $params, $this->logger );
+                       } else {
+                               throw new FileBackendError( "Operation '$opName' is not supported." );
+                       }
+               }
+
+               return $performOps;
+       }
+
+       /**
+        * Get a list of storage paths to lock for a list of operations
+        * Returns an array with LockManager::LOCK_UW (shared locks) and
+        * LockManager::LOCK_EX (exclusive locks) keys, each corresponding
+        * to a list of storage paths to be locked. All returned paths are
+        * normalized.
+        *
+        * @param array $performOps List of FileOp objects
+        * @return array (LockManager::LOCK_UW => path list, LockManager::LOCK_EX => path list)
+        */
+       final public function getPathsToLockForOpsInternal( array $performOps ) {
+               // Build up a list of files to lock...
+               $paths = [ 'sh' => [], 'ex' => [] ];
+               foreach ( $performOps as $fileOp ) {
+                       $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
+                       $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
+               }
+               // Optimization: if doing an EX lock anyway, don't also set an SH one
+               $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
+               // Get a shared lock on the parent directory of each path changed
+               $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
+
+               return [
+                       LockManager::LOCK_UW => $paths['sh'],
+                       LockManager::LOCK_EX => $paths['ex']
+               ];
+       }
+
+       public function getScopedLocksForOps( array $ops, StatusValue $status ) {
+               $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
+
+               return $this->getScopedFileLocks( $paths, 'mixed', $status );
+       }
+
+       final protected function doOperationsInternal( array $ops, array $opts ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->newStatus();
+
+               // Fix up custom header name/value pairs...
+               $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
+
+               // Build up a list of FileOps...
+               $performOps = $this->getOperationsInternal( $ops );
+
+               // Acquire any locks as needed...
+               if ( empty( $opts['nonLocking'] ) ) {
+                       // Build up a list of files to lock...
+                       $paths = $this->getPathsToLockForOpsInternal( $performOps );
+                       // Try to lock those files for the scope of this function...
+
+                       $scopeLock = $this->getScopedFileLocks( $paths, 'mixed', $status );
+                       if ( !$status->isOK() ) {
+                               return $status; // abort
+                       }
+               }
+
+               // Clear any file cache entries (after locks acquired)
+               if ( empty( $opts['preserveCache'] ) ) {
+                       $this->clearCache();
+               }
+
+               // Build the list of paths involved
+               $paths = [];
+               foreach ( $performOps as $op ) {
+                       $paths = array_merge( $paths, $op->storagePathsRead() );
+                       $paths = array_merge( $paths, $op->storagePathsChanged() );
+               }
+
+               // Enlarge the cache to fit the stat entries of these files
+               $this->cheapCache->resize( max( 2 * count( $paths ), self::CACHE_CHEAP_SIZE ) );
+
+               // Load from the persistent container caches
+               $this->primeContainerCache( $paths );
+               // Get the latest stat info for all the files (having locked them)
+               $ok = $this->preloadFileStat( [ 'srcs' => $paths, 'latest' => true ] );
+
+               if ( $ok ) {
+                       // Actually attempt the operation batch...
+                       $opts = $this->setConcurrencyFlags( $opts );
+                       $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
+               } else {
+                       // If we could not even stat some files, then bail out...
+                       $subStatus = $this->newStatus( 'backend-fail-internal', $this->name );
+                       foreach ( $ops as $i => $op ) { // mark each op as failed
+                               $subStatus->success[$i] = false;
+                               ++$subStatus->failCount;
+                       }
+                       $this->logger->error( get_class( $this ) . "-{$this->name} " .
+                               " stat failure; aborted operations: " . FormatJson::encode( $ops ) );
+               }
+
+               // Merge errors into StatusValue fields
+               $status->merge( $subStatus );
+               $status->success = $subStatus->success; // not done in merge()
+
+               // Shrink the stat cache back to normal size
+               $this->cheapCache->resize( self::CACHE_CHEAP_SIZE );
+
+               return $status;
+       }
+
+       final protected function doQuickOperationsInternal( array $ops ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $status = $this->newStatus();
+
+               // Fix up custom header name/value pairs...
+               $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
+
+               // Clear any file cache entries
+               $this->clearCache();
+
+               $supportedOps = [ 'create', 'store', 'copy', 'move', 'delete', 'describe', 'null' ];
+               // Parallel ops may be disabled in config due to dependencies (e.g. needing popen())
+               $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
+               $maxConcurrency = $this->concurrency; // throttle
+               /** @var StatusValue[] $statuses */
+               $statuses = []; // array of (index => StatusValue)
+               $fileOpHandles = []; // list of (index => handle) arrays
+               $curFileOpHandles = []; // current handle batch
+               // Perform the sync-only ops and build up op handles for the async ops...
+               foreach ( $ops as $index => $params ) {
+                       if ( !in_array( $params['op'], $supportedOps ) ) {
+                               throw new FileBackendError( "Operation '{$params['op']}' is not supported." );
+                       }
+                       $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
+                       $subStatus = $this->$method( [ 'async' => $async ] + $params );
+                       if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
+                               if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
+                                       $fileOpHandles[] = $curFileOpHandles; // push this batch
+                                       $curFileOpHandles = [];
+                               }
+                               $curFileOpHandles[$index] = $subStatus->value; // keep index
+                       } else { // error or completed
+                               $statuses[$index] = $subStatus; // keep index
+                       }
+               }
+               if ( count( $curFileOpHandles ) ) {
+                       $fileOpHandles[] = $curFileOpHandles; // last batch
+               }
+               // Do all the async ops that can be done concurrently...
+               foreach ( $fileOpHandles as $fileHandleBatch ) {
+                       $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
+               }
+               // Marshall and merge all the responses...
+               foreach ( $statuses as $index => $subStatus ) {
+                       $status->merge( $subStatus );
+                       if ( $subStatus->isOK() ) {
+                               $status->success[$index] = true;
+                               ++$status->successCount;
+                       } else {
+                               $status->success[$index] = false;
+                               ++$status->failCount;
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * Execute a list of FileBackendStoreOpHandle handles in parallel.
+        * The resulting StatusValue object fields will correspond
+        * to the order in which the handles where given.
+        *
+        * @param FileBackendStoreOpHandle[] $fileOpHandles
+        *
+        * @throws FileBackendError
+        * @return StatusValue[] Map of StatusValue objects
+        */
+       final public function executeOpHandlesInternal( array $fileOpHandles ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
+               foreach ( $fileOpHandles as $fileOpHandle ) {
+                       if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
+                               throw new InvalidArgumentException( "Got a non-FileBackendStoreOpHandle object." );
+                       } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
+                               throw new InvalidArgumentException(
+                                       "Got a FileBackendStoreOpHandle for the wrong backend." );
+                       }
+               }
+               $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
+               foreach ( $fileOpHandles as $fileOpHandle ) {
+                       $fileOpHandle->closeResources();
+               }
+
+               return $res;
+       }
+
+       /**
+        * @see FileBackendStore::executeOpHandlesInternal()
+        *
+        * @param FileBackendStoreOpHandle[] $fileOpHandles
+        *
+        * @throws FileBackendError
+        * @return StatusValue[] List of corresponding StatusValue objects
+        */
+       protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
+               if ( count( $fileOpHandles ) ) {
+                       throw new LogicException( "Backend does not support asynchronous operations." );
+               }
+
+               return [];
+       }
+
+       /**
+        * Normalize and filter HTTP headers from a file operation
+        *
+        * This normalizes and strips long HTTP headers from a file operation.
+        * Most headers are just numbers, but some are allowed to be long.
+        * This function is useful for cleaning up headers and avoiding backend
+        * specific errors, especially in the middle of batch file operations.
+        *
+        * @param array $op Same format as doOperation()
+        * @return array
+        */
+       protected function sanitizeOpHeaders( array $op ) {
+               static $longs = [ 'content-disposition' ];
+
+               if ( isset( $op['headers'] ) ) { // op sets HTTP headers
+                       $newHeaders = [];
+                       foreach ( $op['headers'] as $name => $value ) {
+                               $name = strtolower( $name );
+                               $maxHVLen = in_array( $name, $longs ) ? INF : 255;
+                               if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
+                                       trigger_error( "Header '$name: $value' is too long." );
+                               } else {
+                                       $newHeaders[$name] = strlen( $value ) ? $value : ''; // null/false => ""
+                               }
+                       }
+                       $op['headers'] = $newHeaders;
+               }
+
+               return $op;
+       }
+
+       final public function preloadCache( array $paths ) {
+               $fullConts = []; // full container names
+               foreach ( $paths as $path ) {
+                       list( $fullCont, , ) = $this->resolveStoragePath( $path );
+                       $fullConts[] = $fullCont;
+               }
+               // Load from the persistent file and container caches
+               $this->primeContainerCache( $fullConts );
+               $this->primeFileCache( $paths );
+       }
+
+       final public function clearCache( array $paths = null ) {
+               if ( is_array( $paths ) ) {
+                       $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
+                       $paths = array_filter( $paths, 'strlen' ); // remove nulls
+               }
+               if ( $paths === null ) {
+                       $this->cheapCache->clear();
+                       $this->expensiveCache->clear();
+               } else {
+                       foreach ( $paths as $path ) {
+                               $this->cheapCache->clear( $path );
+                               $this->expensiveCache->clear( $path );
+                       }
+               }
+               $this->doClearCache( $paths );
+       }
+
+       /**
+        * Clears any additional stat caches for storage paths
+        *
+        * @see FileBackend::clearCache()
+        *
+        * @param array $paths Storage paths (optional)
+        */
+       protected function doClearCache( array $paths = null ) {
+       }
+
+       final public function preloadFileStat( array $params ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $success = true; // no network errors
+
+               $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
+               $stats = $this->doGetFileStatMulti( $params );
+               if ( $stats === null ) {
+                       return true; // not supported
+               }
+
+               $latest = !empty( $params['latest'] ); // use latest data?
+               foreach ( $stats as $path => $stat ) {
+                       $path = FileBackend::normalizeStoragePath( $path );
+                       if ( $path === null ) {
+                               continue; // this shouldn't happen
+                       }
+                       if ( is_array( $stat ) ) { // file exists
+                               // Strongly consistent backends can automatically set "latest"
+                               $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
+                               $this->cheapCache->set( $path, 'stat', $stat );
+                               $this->setFileCache( $path, $stat ); // update persistent cache
+                               if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
+                                       $this->cheapCache->set( $path, 'sha1',
+                                               [ 'hash' => $stat['sha1'], 'latest' => $latest ] );
+                               }
+                               if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
+                                       $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
+                                       $this->cheapCache->set( $path, 'xattr',
+                                               [ 'map' => $stat['xattr'], 'latest' => $latest ] );
+                               }
+                       } elseif ( $stat === false ) { // file does not exist
+                               $this->cheapCache->set( $path, 'stat',
+                                       $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
+                               $this->cheapCache->set( $path, 'xattr',
+                                       [ 'map' => false, 'latest' => $latest ] );
+                               $this->cheapCache->set( $path, 'sha1',
+                                       [ 'hash' => false, 'latest' => $latest ] );
+                               $this->logger->debug( __METHOD__ . ": File $path does not exist.\n" );
+                       } else { // an error occurred
+                               $success = false;
+                               $this->logger->warning( __METHOD__ . ": Could not stat file $path.\n" );
+                       }
+               }
+
+               return $success;
+       }
+
+       /**
+        * Get file stat information (concurrently if possible) for several files
+        *
+        * @see FileBackend::getFileStat()
+        *
+        * @param array $params Parameters include:
+        *   - srcs        : list of source storage paths
+        *   - latest      : use the latest available data
+        * @return array|null Map of storage paths to array|bool|null (returns null if not supported)
+        * @since 1.23
+        */
+       protected function doGetFileStatMulti( array $params ) {
+               return null; // not supported
+       }
+
+       /**
+        * Is this a key/value store where directories are just virtual?
+        * Virtual directories exists in so much as files exists that are
+        * prefixed with the directory path followed by a forward slash.
+        *
+        * @return bool
+        */
+       abstract protected function directoriesAreVirtual();
+
+       /**
+        * Check if a short container name is valid
+        *
+        * This checks for length and illegal characters.
+        * This may disallow certain characters that can appear
+        * in the prefix used to make the full container name.
+        *
+        * @param string $container
+        * @return bool
+        */
+       final protected static function isValidShortContainerName( $container ) {
+               // Suffixes like '.xxx' (hex shard chars) or '.seg' (file segments)
+               // might be used by subclasses. Reserve the dot character for sanity.
+               // The only way dots end up in containers (e.g. resolveStoragePath)
+               // is due to the wikiId container prefix or the above suffixes.
+               return self::isValidContainerName( $container ) && !preg_match( '/[.]/', $container );
+       }
+
+       /**
+        * Check if a full container name is valid
+        *
+        * This checks for length and illegal characters.
+        * Limiting the characters makes migrations to other stores easier.
+        *
+        * @param string $container
+        * @return bool
+        */
+       final protected static function isValidContainerName( $container ) {
+               // This accounts for NTFS, Swift, and Ceph restrictions
+               // and disallows directory separators or traversal characters.
+               // Note that matching strings URL encode to the same string;
+               // in Swift/Ceph, the length restriction is *after* URL encoding.
+               return (bool)preg_match( '/^[a-z0-9][a-z0-9-_.]{0,199}$/i', $container );
+       }
+
+       /**
+        * Splits a storage path into an internal container name,
+        * an internal relative file name, and a container shard suffix.
+        * Any shard suffix is already appended to the internal container name.
+        * This also checks that the storage path is valid and within this backend.
+        *
+        * If the container is sharded but a suffix could not be determined,
+        * this means that the path can only refer to a directory and can only
+        * be scanned by looking in all the container shards.
+        *
+        * @param string $storagePath
+        * @return array (container, path, container suffix) or (null, null, null) if invalid
+        */
+       final protected function resolveStoragePath( $storagePath ) {
+               list( $backend, $shortCont, $relPath ) = self::splitStoragePath( $storagePath );
+               if ( $backend === $this->name ) { // must be for this backend
+                       $relPath = self::normalizeContainerPath( $relPath );
+                       if ( $relPath !== null && self::isValidShortContainerName( $shortCont ) ) {
+                               // Get shard for the normalized path if this container is sharded
+                               $cShard = $this->getContainerShard( $shortCont, $relPath );
+                               // Validate and sanitize the relative path (backend-specific)
+                               $relPath = $this->resolveContainerPath( $shortCont, $relPath );
+                               if ( $relPath !== null ) {
+                                       // Prepend any wiki ID prefix to the container name
+                                       $container = $this->fullContainerName( $shortCont );
+                                       if ( self::isValidContainerName( $container ) ) {
+                                               // Validate and sanitize the container name (backend-specific)
+                                               $container = $this->resolveContainerName( "{$container}{$cShard}" );
+                                               if ( $container !== null ) {
+                                                       return [ $container, $relPath, $cShard ];
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               return [ null, null, null ];
+       }
+
+       /**
+        * Like resolveStoragePath() except null values are returned if
+        * the container is sharded and the shard could not be determined
+        * or if the path ends with '/'. The latter case is illegal for FS
+        * backends and can confuse listings for object store backends.
+        *
+        * This function is used when resolving paths that must be valid
+        * locations for files. Directory and listing functions should
+        * generally just use resolveStoragePath() instead.
+        *
+        * @see FileBackendStore::resolveStoragePath()
+        *
+        * @param string $storagePath
+        * @return array (container, path) or (null, null) if invalid
+        */
+       final protected function resolveStoragePathReal( $storagePath ) {
+               list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
+               if ( $cShard !== null && substr( $relPath, -1 ) !== '/' ) {
+                       return [ $container, $relPath ];
+               }
+
+               return [ null, null ];
+       }
+
+       /**
+        * Get the container name shard suffix for a given path.
+        * Any empty suffix means the container is not sharded.
+        *
+        * @param string $container Container name
+        * @param string $relPath Storage path relative to the container
+        * @return string|null Returns null if shard could not be determined
+        */
+       final protected function getContainerShard( $container, $relPath ) {
+               list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
+               if ( $levels == 1 || $levels == 2 ) {
+                       // Hash characters are either base 16 or 36
+                       $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
+                       // Get a regex that represents the shard portion of paths.
+                       // The concatenation of the captures gives us the shard.
+                       if ( $levels === 1 ) { // 16 or 36 shards per container
+                               $hashDirRegex = '(' . $char . ')';
+                       } else { // 256 or 1296 shards per container
+                               if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
+                                       $hashDirRegex = $char . '/(' . $char . '{2})';
+                               } else { // short hash dir format (e.g. "a/b/c")
+                                       $hashDirRegex = '(' . $char . ')/(' . $char . ')';
+                               }
+                       }
+                       // Allow certain directories to be above the hash dirs so as
+                       // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
+                       // They must be 2+ chars to avoid any hash directory ambiguity.
+                       $m = [];
+                       if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
+                               return '.' . implode( '', array_slice( $m, 1 ) );
+                       }
+
+                       return null; // failed to match
+               }
+
+               return ''; // no sharding
+       }
+
+       /**
+        * Check if a storage path maps to a single shard.
+        * Container dirs like "a", where the container shards on "x/xy",
+        * can reside on several shards. Such paths are tricky to handle.
+        *
+        * @param string $storagePath Storage path
+        * @return bool
+        */
+       final public function isSingleShardPathInternal( $storagePath ) {
+               list( , , $shard ) = $this->resolveStoragePath( $storagePath );
+
+               return ( $shard !== null );
+       }
+
+       /**
+        * Get the sharding config for a container.
+        * If greater than 0, then all file storage paths within
+        * the container are required to be hashed accordingly.
+        *
+        * @param string $container
+        * @return array (integer levels, integer base, repeat flag) or (0, 0, false)
+        */
+       final protected function getContainerHashLevels( $container ) {
+               if ( isset( $this->shardViaHashLevels[$container] ) ) {
+                       $config = $this->shardViaHashLevels[$container];
+                       $hashLevels = (int)$config['levels'];
+                       if ( $hashLevels == 1 || $hashLevels == 2 ) {
+                               $hashBase = (int)$config['base'];
+                               if ( $hashBase == 16 || $hashBase == 36 ) {
+                                       return [ $hashLevels, $hashBase, $config['repeat'] ];
+                               }
+                       }
+               }
+
+               return [ 0, 0, false ]; // no sharding
+       }
+
+       /**
+        * Get a list of full container shard suffixes for a container
+        *
+        * @param string $container
+        * @return array
+        */
+       final protected function getContainerSuffixes( $container ) {
+               $shards = [];
+               list( $digits, $base ) = $this->getContainerHashLevels( $container );
+               if ( $digits > 0 ) {
+                       $numShards = pow( $base, $digits );
+                       for ( $index = 0; $index < $numShards; $index++ ) {
+                               $shards[] = '.' . Wikimedia\base_convert( $index, 10, $base, $digits );
+                       }
+               }
+
+               return $shards;
+       }
+
+       /**
+        * Get the full container name, including the wiki ID prefix
+        *
+        * @param string $container
+        * @return string
+        */
+       final protected function fullContainerName( $container ) {
+               if ( $this->domainId != '' ) {
+                       return "{$this->domainId}-$container";
+               } else {
+                       return $container;
+               }
+       }
+
+       /**
+        * Resolve a container name, checking if it's allowed by the backend.
+        * This is intended for internal use, such as encoding illegal chars.
+        * Subclasses can override this to be more restrictive.
+        *
+        * @param string $container
+        * @return string|null
+        */
+       protected function resolveContainerName( $container ) {
+               return $container;
+       }
+
+       /**
+        * Resolve a relative storage path, checking if it's allowed by the backend.
+        * This is intended for internal use, such as encoding illegal chars or perhaps
+        * getting absolute paths (e.g. FS based backends). Note that the relative path
+        * may be the empty string (e.g. the path is simply to the container).
+        *
+        * @param string $container Container name
+        * @param string $relStoragePath Storage path relative to the container
+        * @return string|null Path or null if not valid
+        */
+       protected function resolveContainerPath( $container, $relStoragePath ) {
+               return $relStoragePath;
+       }
+
+       /**
+        * Get the cache key for a container
+        *
+        * @param string $container Resolved container name
+        * @return string
+        */
+       private function containerCacheKey( $container ) {
+               return "filebackend:{$this->name}:{$this->domainId}:container:{$container}";
+       }
+
+       /**
+        * Set the cached info for a container
+        *
+        * @param string $container Resolved container name
+        * @param array $val Information to cache
+        */
+       final protected function setContainerCache( $container, array $val ) {
+               $this->memCache->set( $this->containerCacheKey( $container ), $val, 14 * 86400 );
+       }
+
+       /**
+        * Delete the cached info for a container.
+        * The cache key is salted for a while to prevent race conditions.
+        *
+        * @param string $container Resolved container name
+        */
+       final protected function deleteContainerCache( $container ) {
+               if ( !$this->memCache->delete( $this->containerCacheKey( $container ), 300 ) ) {
+                       trigger_error( "Unable to delete stat cache for container $container." );
+               }
+       }
+
+       /**
+        * Do a batch lookup from cache for container stats for all containers
+        * used in a list of container names or storage paths objects.
+        * This loads the persistent cache values into the process cache.
+        *
+        * @param array $items
+        */
+       final protected function primeContainerCache( array $items ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
+               $paths = []; // list of storage paths
+               $contNames = []; // (cache key => resolved container name)
+               // Get all the paths/containers from the items...
+               foreach ( $items as $item ) {
+                       if ( self::isStoragePath( $item ) ) {
+                               $paths[] = $item;
+                       } elseif ( is_string( $item ) ) { // full container name
+                               $contNames[$this->containerCacheKey( $item )] = $item;
+                       }
+               }
+               // Get all the corresponding cache keys for paths...
+               foreach ( $paths as $path ) {
+                       list( $fullCont, , ) = $this->resolveStoragePath( $path );
+                       if ( $fullCont !== null ) { // valid path for this backend
+                               $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
+                       }
+               }
+
+               $contInfo = []; // (resolved container name => cache value)
+               // Get all cache entries for these container cache keys...
+               $values = $this->memCache->getMulti( array_keys( $contNames ) );
+               foreach ( $values as $cacheKey => $val ) {
+                       $contInfo[$contNames[$cacheKey]] = $val;
+               }
+
+               // Populate the container process cache for the backend...
+               $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
+       }
+
+       /**
+        * Fill the backend-specific process cache given an array of
+        * resolved container names and their corresponding cached info.
+        * Only containers that actually exist should appear in the map.
+        *
+        * @param array $containerInfo Map of resolved container names to cached info
+        */
+       protected function doPrimeContainerCache( array $containerInfo ) {
+       }
+
+       /**
+        * Get the cache key for a file path
+        *
+        * @param string $path Normalized storage path
+        * @return string
+        */
+       private function fileCacheKey( $path ) {
+               return "filebackend:{$this->name}:{$this->domainId}:file:" . sha1( $path );
+       }
+
+       /**
+        * Set the cached stat info for a file path.
+        * Negatives (404s) are not cached. By not caching negatives, we can skip cache
+        * salting for the case when a file is created at a path were there was none before.
+        *
+        * @param string $path Storage path
+        * @param array $val Stat information to cache
+        */
+       final protected function setFileCache( $path, array $val ) {
+               $path = FileBackend::normalizeStoragePath( $path );
+               if ( $path === null ) {
+                       return; // invalid storage path
+               }
+               $mtime = ConvertibleTimestamp::convert( TS_UNIX, $val['mtime'] );
+               $ttl = $this->memCache->adaptiveTTL( $mtime, 7 * 86400, 300, .1 );
+               $key = $this->fileCacheKey( $path );
+               // Set the cache unless it is currently salted.
+               $this->memCache->set( $key, $val, $ttl );
+       }
+
+       /**
+        * Delete the cached stat info for a file path.
+        * The cache key is salted for a while to prevent race conditions.
+        * Since negatives (404s) are not cached, this does not need to be called when
+        * a file is created at a path were there was none before.
+        *
+        * @param string $path Storage path
+        */
+       final protected function deleteFileCache( $path ) {
+               $path = FileBackend::normalizeStoragePath( $path );
+               if ( $path === null ) {
+                       return; // invalid storage path
+               }
+               if ( !$this->memCache->delete( $this->fileCacheKey( $path ), 300 ) ) {
+                       trigger_error( "Unable to delete stat cache for file $path." );
+               }
+       }
+
+       /**
+        * Do a batch lookup from cache for file stats for all paths
+        * used in a list of storage paths or FileOp objects.
+        * This loads the persistent cache values into the process cache.
+        *
+        * @param array $items List of storage paths
+        */
+       final protected function primeFileCache( array $items ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
+               $paths = []; // list of storage paths
+               $pathNames = []; // (cache key => storage path)
+               // Get all the paths/containers from the items...
+               foreach ( $items as $item ) {
+                       if ( self::isStoragePath( $item ) ) {
+                               $paths[] = FileBackend::normalizeStoragePath( $item );
+                       }
+               }
+               // Get rid of any paths that failed normalization...
+               $paths = array_filter( $paths, 'strlen' ); // remove nulls
+               // Get all the corresponding cache keys for paths...
+               foreach ( $paths as $path ) {
+                       list( , $rel, ) = $this->resolveStoragePath( $path );
+                       if ( $rel !== null ) { // valid path for this backend
+                               $pathNames[$this->fileCacheKey( $path )] = $path;
+                       }
+               }
+               // Get all cache entries for these file cache keys...
+               $values = $this->memCache->getMulti( array_keys( $pathNames ) );
+               foreach ( $values as $cacheKey => $val ) {
+                       $path = $pathNames[$cacheKey];
+                       if ( is_array( $val ) ) {
+                               $val['latest'] = false; // never completely trust cache
+                               $this->cheapCache->set( $path, 'stat', $val );
+                               if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
+                                       $this->cheapCache->set( $path, 'sha1',
+                                               [ 'hash' => $val['sha1'], 'latest' => false ] );
+                               }
+                               if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata
+                                       $val['xattr'] = self::normalizeXAttributes( $val['xattr'] );
+                                       $this->cheapCache->set( $path, 'xattr',
+                                               [ 'map' => $val['xattr'], 'latest' => false ] );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Normalize file headers/metadata to the FileBackend::getFileXAttributes() format
+        *
+        * @param array $xattr
+        * @return array
+        * @since 1.22
+        */
+       final protected static function normalizeXAttributes( array $xattr ) {
+               $newXAttr = [ 'headers' => [], 'metadata' => [] ];
+
+               foreach ( $xattr['headers'] as $name => $value ) {
+                       $newXAttr['headers'][strtolower( $name )] = $value;
+               }
+
+               foreach ( $xattr['metadata'] as $name => $value ) {
+                       $newXAttr['metadata'][strtolower( $name )] = $value;
+               }
+
+               return $newXAttr;
+       }
+
+       /**
+        * Set the 'concurrency' option from a list of operation options
+        *
+        * @param array $opts Map of operation options
+        * @return array
+        */
+       final protected function setConcurrencyFlags( array $opts ) {
+               $opts['concurrency'] = 1; // off
+               if ( $this->parallelize === 'implicit' ) {
+                       if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
+                               $opts['concurrency'] = $this->concurrency;
+                       }
+               } elseif ( $this->parallelize === 'explicit' ) {
+                       if ( !empty( $opts['parallelize'] ) ) {
+                               $opts['concurrency'] = $this->concurrency;
+                       }
+               }
+
+               return $opts;
+       }
+
+       /**
+        * Get the content type to use in HEAD/GET requests for a file
+        *
+        * @param string $storagePath
+        * @param string|null $content File data
+        * @param string|null $fsPath File system path
+        * @return string MIME type
+        */
+       protected function getContentType( $storagePath, $content, $fsPath ) {
+               if ( $this->mimeCallback ) {
+                       return call_user_func_array( $this->mimeCallback, func_get_args() );
+               }
+
+               $mime = null;
+               if ( $fsPath !== null && function_exists( 'finfo_file' ) ) {
+                       $finfo = finfo_open( FILEINFO_MIME_TYPE );
+                       $mime = finfo_file( $finfo, $fsPath );
+                       finfo_close( $finfo );
+               }
+
+               return is_string( $mime ) ? $mime : 'unknown/unknown';
+       }
+}
+
+/**
+ * FileBackendStore helper class for performing asynchronous file operations.
+ *
+ * For example, calling FileBackendStore::createInternal() with the "async"
+ * param flag may result in a StatusValue that contains this object as a value.
+ * This class is largely backend-specific and is mostly just "magic" to be
+ * passed to FileBackendStore::executeOpHandlesInternal().
+ */
+abstract class FileBackendStoreOpHandle {
+       /** @var array */
+       public $params = []; // params to caller functions
+       /** @var FileBackendStore */
+       public $backend;
+       /** @var array */
+       public $resourcesToClose = [];
+
+       public $call; // string; name that identifies the function called
+
+       /**
+        * Close all open file handles
+        */
+       public function closeResources() {
+               array_map( 'fclose', $this->resourcesToClose );
+       }
+}
+
+/**
+ * FileBackendStore helper function to handle listings that span container shards.
+ * Do not use this class from places outside of FileBackendStore.
+ *
+ * @ingroup FileBackend
+ */
+abstract class FileBackendStoreShardListIterator extends FilterIterator {
+       /** @var FileBackendStore */
+       protected $backend;
+
+       /** @var array */
+       protected $params;
+
+       /** @var string Full container name */
+       protected $container;
+
+       /** @var string Resolved relative path */
+       protected $directory;
+
+       /** @var array */
+       protected $multiShardPaths = []; // (rel path => 1)
+
+       /**
+        * @param FileBackendStore $backend
+        * @param string $container Full storage container name
+        * @param string $dir Storage directory relative to container
+        * @param array $suffixes List of container shard suffixes
+        * @param array $params
+        */
+       public function __construct(
+               FileBackendStore $backend, $container, $dir, array $suffixes, array $params
+       ) {
+               $this->backend = $backend;
+               $this->container = $container;
+               $this->directory = $dir;
+               $this->params = $params;
+
+               $iter = new AppendIterator();
+               foreach ( $suffixes as $suffix ) {
+                       $iter->append( $this->listFromShard( $this->container . $suffix ) );
+               }
+
+               parent::__construct( $iter );
+       }
+
+       public function accept() {
+               $rel = $this->getInnerIterator()->current(); // path relative to given directory
+               $path = $this->params['dir'] . "/{$rel}"; // full storage path
+               if ( $this->backend->isSingleShardPathInternal( $path ) ) {
+                       return true; // path is only on one shard; no issue with duplicates
+               } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
+                       // Don't keep listing paths that are on multiple shards
+                       return false;
+               } else {
+                       $this->multiShardPaths[$rel] = 1;
+
+                       return true;
+               }
+       }
+
+       public function rewind() {
+               parent::rewind();
+               $this->multiShardPaths = [];
+       }
+
+       /**
+        * Get the list for a given container shard
+        *
+        * @param string $container Resolved container name
+        * @return Iterator
+        */
+       abstract protected function listFromShard( $container );
+}
+
+/**
+ * Iterator for listing directories
+ */
+class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
+       protected function listFromShard( $container ) {
+               $list = $this->backend->getDirectoryListInternal(
+                       $container, $this->directory, $this->params );
+               if ( $list === null ) {
+                       return new ArrayIterator( [] );
+               } else {
+                       return is_array( $list ) ? new ArrayIterator( $list ) : $list;
+               }
+       }
+}
+
+/**
+ * Iterator for listing regular files
+ */
+class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
+       protected function listFromShard( $container ) {
+               $list = $this->backend->getFileListInternal(
+                       $container, $this->directory, $this->params );
+               if ( $list === null ) {
+                       return new ArrayIterator( [] );
+               } else {
+                       return is_array( $list ) ? new ArrayIterator( $list ) : $list;
+               }
+       }
+}
diff --git a/includes/libs/filebackend/FileOpBatch.php b/includes/libs/filebackend/FileOpBatch.php
new file mode 100644 (file)
index 0000000..71b5c7d
--- /dev/null
@@ -0,0 +1,205 @@
+<?php
+/**
+ * Helper class for representing batch file operations.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * Helper class for representing batch file operations.
+ * Do not use this class from places outside FileBackend.
+ *
+ * Methods should avoid throwing exceptions at all costs.
+ *
+ * @ingroup FileBackend
+ * @since 1.20
+ */
+class FileOpBatch {
+       /* Timeout related parameters */
+       const MAX_BATCH_SIZE = 1000; // integer
+
+       /**
+        * Attempt to perform a series of file operations.
+        * Callers are responsible for handling file locking.
+        *
+        * $opts is an array of options, including:
+        *   - force        : Errors that would normally cause a rollback do not.
+        *                    The remaining operations are still attempted if any fail.
+        *   - nonJournaled : Don't log this operation batch in the file journal.
+        *   - concurrency  : Try to do this many operations in parallel when possible.
+        *
+        * 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
+        *
+        * @param FileOp[] $performOps List of FileOp operations
+        * @param array $opts Batch operation options
+        * @param FileJournal $journal Journal to log operations to
+        * @return StatusValue
+        */
+       public static function attempt( array $performOps, array $opts, FileJournal $journal ) {
+               $status = StatusValue::newGood();
+
+               $n = count( $performOps );
+               if ( $n > self::MAX_BATCH_SIZE ) {
+                       $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE );
+
+                       return $status;
+               }
+
+               $batchId = $journal->getTimestampedUUID();
+               $ignoreErrors = !empty( $opts['force'] );
+               $journaled = empty( $opts['nonJournaled'] );
+               $maxConcurrency = isset( $opts['concurrency'] ) ? $opts['concurrency'] : 1;
+
+               $entries = []; // file journal entry list
+               $predicates = FileOp::newPredicates(); // account for previous ops in prechecks
+               $curBatch = []; // concurrent FileOp sub-batch accumulation
+               $curBatchDeps = FileOp::newDependencies(); // paths used in FileOp sub-batch
+               $pPerformOps = []; // ordered list of concurrent FileOp sub-batches
+               $lastBackend = null; // last op backend name
+               // Do pre-checks for each operation; abort on failure...
+               foreach ( $performOps as $index => $fileOp ) {
+                       $backendName = $fileOp->getBackend()->getName();
+                       $fileOp->setBatchId( $batchId ); // transaction ID
+                       // Decide if this op can be done concurrently within this sub-batch
+                       // or if a new concurrent sub-batch must be started after this one...
+                       if ( $fileOp->dependsOn( $curBatchDeps )
+                               || count( $curBatch ) >= $maxConcurrency
+                               || ( $backendName !== $lastBackend && count( $curBatch ) )
+                       ) {
+                               $pPerformOps[] = $curBatch; // push this batch
+                               $curBatch = []; // start a new sub-batch
+                               $curBatchDeps = FileOp::newDependencies();
+                       }
+                       $lastBackend = $backendName;
+                       $curBatch[$index] = $fileOp; // keep index
+                       // Update list of affected paths in this batch
+                       $curBatchDeps = $fileOp->applyDependencies( $curBatchDeps );
+                       // Simulate performing the operation...
+                       $oldPredicates = $predicates;
+                       $subStatus = $fileOp->precheck( $predicates ); // updates $predicates
+                       $status->merge( $subStatus );
+                       if ( $subStatus->isOK() ) {
+                               if ( $journaled ) { // journal log entries
+                                       $entries = array_merge( $entries,
+                                               $fileOp->getJournalEntries( $oldPredicates, $predicates ) );
+                               }
+                       } else { // operation failed?
+                               $status->success[$index] = false;
+                               ++$status->failCount;
+                               if ( !$ignoreErrors ) {
+                                       return $status; // abort
+                               }
+                       }
+               }
+               // Push the last sub-batch
+               if ( count( $curBatch ) ) {
+                       $pPerformOps[] = $curBatch;
+               }
+
+               // Log the operations in the file journal...
+               if ( count( $entries ) ) {
+                       $subStatus = $journal->logChangeBatch( $entries, $batchId );
+                       if ( !$subStatus->isOK() ) {
+                               $status->merge( $subStatus );
+
+                               return $status; // abort
+                       }
+               }
+
+               if ( $ignoreErrors ) { // treat precheck() fatals as mere warnings
+                       $status->setResult( true, $status->value );
+               }
+
+               // Attempt each operation (in parallel if allowed and possible)...
+               self::runParallelBatches( $pPerformOps, $status );
+
+               return $status;
+       }
+
+       /**
+        * Attempt a list of file operations sub-batches in series.
+        *
+        * The operations *in* each sub-batch will be done in parallel.
+        * The caller is responsible for making sure the operations
+        * within any given sub-batch do not depend on each other.
+        * This will abort remaining ops on failure.
+        *
+        * @param array $pPerformOps Batches of file ops (batches use original indexes)
+        * @param StatusValue $status
+        */
+       protected static function runParallelBatches( array $pPerformOps, StatusValue $status ) {
+               $aborted = false; // set to true on unexpected errors
+               foreach ( $pPerformOps as $performOpsBatch ) {
+                       /** @var FileOp[] $performOpsBatch */
+                       if ( $aborted ) { // check batch op abort flag...
+                               // We can't continue (even with $ignoreErrors) as $predicates is wrong.
+                               // Log the remaining ops as failed for recovery...
+                               foreach ( $performOpsBatch as $i => $fileOp ) {
+                                       $status->success[$i] = false;
+                                       ++$status->failCount;
+                                       $performOpsBatch[$i]->logFailure( 'attempt_aborted' );
+                               }
+                               continue;
+                       }
+                       /** @var StatusValue[] $statuses */
+                       $statuses = [];
+                       $opHandles = [];
+                       // Get the backend; all sub-batch ops belong to a single backend
+                       /** @var FileBackendStore $backend */
+                       $backend = reset( $performOpsBatch )->getBackend();
+                       // Get the operation handles or actually do it if there is just one.
+                       // If attemptAsync() returns a StatusValue, it was either due to an error
+                       // or the backend does not support async ops and did it synchronously.
+                       foreach ( $performOpsBatch as $i => $fileOp ) {
+                               if ( !isset( $status->success[$i] ) ) { // didn't already fail in precheck()
+                                       // Parallel ops may be disabled in config due to missing dependencies,
+                                       // (e.g. needing popen()). When they are, $performOpsBatch has size 1.
+                                       $subStatus = ( count( $performOpsBatch ) > 1 )
+                                               ? $fileOp->attemptAsync()
+                                               : $fileOp->attempt();
+                                       if ( $subStatus->value instanceof FileBackendStoreOpHandle ) {
+                                               $opHandles[$i] = $subStatus->value; // deferred
+                                       } else {
+                                               $statuses[$i] = $subStatus; // done already
+                                       }
+                               }
+                       }
+                       // Try to do all the operations concurrently...
+                       $statuses = $statuses + $backend->executeOpHandlesInternal( $opHandles );
+                       // Marshall and merge all the responses (blocking)...
+                       foreach ( $performOpsBatch as $i => $fileOp ) {
+                               if ( !isset( $status->success[$i] ) ) { // didn't already fail in precheck()
+                                       $subStatus = $statuses[$i];
+                                       $status->merge( $subStatus );
+                                       if ( $subStatus->isOK() ) {
+                                               $status->success[$i] = true;
+                                               ++$status->successCount;
+                                       } else {
+                                               $status->success[$i] = false;
+                                               ++$status->failCount;
+                                               $aborted = true; // set abort flag; we can't continue
+                                       }
+                               }
+                       }
+               }
+       }
+}
diff --git a/includes/libs/filebackend/HTTPFileStreamer.php b/includes/libs/filebackend/HTTPFileStreamer.php
new file mode 100644 (file)
index 0000000..800fdfa
--- /dev/null
@@ -0,0 +1,268 @@
+<?php
+/**
+ * Functions related to the output of file content.
+ *
+ * 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
+ */
+
+/**
+ * Functions related to the output of file content
+ *
+ * @since 1.28
+ */
+class HTTPFileStreamer {
+       /** @var string */
+       protected $path;
+       /** @var callable */
+       protected $obResetFunc;
+       /** @var callable */
+       protected $streamMimeFunc;
+
+       // Do not send any HTTP headers unless requested by caller (e.g. body only)
+       const STREAM_HEADLESS = 1;
+       // Do not try to tear down any PHP output buffers
+       const STREAM_ALLOW_OB = 2;
+
+       /**
+        * @param string $path Local filesystem path to a file
+        * @param array $params Options map, which includes:
+        *   - obResetFunc : alternative callback to clear the output buffer
+        *   - streamMimeFunc : alternative method to determine the content type from the path
+        */
+       public function __construct( $path, array $params = [] ) {
+               $this->path = $path;
+               $this->obResetFunc = isset( $params['obResetFunc'] )
+                       ? $params['obResetFunc']
+                       : [ __CLASS__, 'resetOutputBuffers' ];
+               $this->streamMimeFunc = isset( $params['streamMimeFunc'] )
+                       ? $params['streamMimeFunc']
+                       : [ __CLASS__, 'contentTypeFromPath' ];
+       }
+
+       /**
+        * Stream a file to the browser, adding all the headings and fun stuff.
+        * Headers sent include: Content-type, Content-Length, Last-Modified,
+        * and Content-Disposition.
+        *
+        * @param array $headers Any additional headers to send if the file exists
+        * @param bool $sendErrors Send error messages if errors occur (like 404)
+        * @param array $optHeaders HTTP request header map (e.g. "range") (use lowercase keys)
+        * @param integer $flags Bitfield of STREAM_* constants
+        * @throws MWException
+        * @return bool Success
+        */
+       public function stream(
+               $headers = [], $sendErrors = true, $optHeaders = [], $flags = 0
+       ) {
+               // Don't stream it out as text/html if there was a PHP error
+               if ( ( ( $flags & self::STREAM_HEADLESS ) == 0 || $headers ) && headers_sent() ) {
+                       echo "Headers already sent, terminating.\n";
+                       return false;
+               }
+
+               $headerFunc = ( $flags & self::STREAM_HEADLESS )
+                       ? function ( $header ) {
+                               // no-op
+                       }
+                       : function ( $header ) {
+                               is_int( $header ) ? HttpStatus::header( $header ) : header( $header );
+                       };
+
+               MediaWiki\suppressWarnings();
+               $info = stat( $this->path );
+               MediaWiki\restoreWarnings();
+
+               if ( !is_array( $info ) ) {
+                       if ( $sendErrors ) {
+                               self::send404Message( $this->path, $flags );
+                       }
+                       return false;
+               }
+
+               // Send Last-Modified HTTP header for client-side caching
+               $mtimeCT = new ConvertibleTimestamp( $info['mtime'] );
+               $headerFunc( 'Last-Modified: ' . $mtimeCT->getTimestamp( TS_RFC2822 ) );
+
+               if ( ( $flags & self::STREAM_ALLOW_OB ) == 0 ) {
+                       call_user_func( $this->obResetFunc );
+               }
+
+               $type = call_user_func( $this->streamMimeFunc, $this->path );
+               if ( $type && $type != 'unknown/unknown' ) {
+                       $headerFunc( "Content-type: $type" );
+               } else {
+                       // Send a content type which is not known to Internet Explorer, to
+                       // avoid triggering IE's content type detection. Sending a standard
+                       // unknown content type here essentially gives IE license to apply
+                       // whatever content type it likes.
+                       $headerFunc( 'Content-type: application/x-wiki' );
+               }
+
+               // Don't send if client has up to date cache
+               if ( isset( $optHeaders['if-modified-since'] ) ) {
+                       $modsince = preg_replace( '/;.*$/', '', $optHeaders['if-modified-since'] );
+                       if ( $mtimeCT->getTimestamp( TS_UNIX ) <= strtotime( $modsince ) ) {
+                               ini_set( 'zlib.output_compression', 0 );
+                               $headerFunc( 304 );
+                               return true; // ok
+                       }
+               }
+
+               // Send additional headers
+               foreach ( $headers as $header ) {
+                       header( $header ); // always use header(); specifically requested
+               }
+
+               if ( isset( $optHeaders['range'] ) ) {
+                       $range = self::parseRange( $optHeaders['range'], $info['size'] );
+                       if ( is_array( $range ) ) {
+                               $headerFunc( 206 );
+                               $headerFunc( 'Content-Length: ' . $range[2] );
+                               $headerFunc( "Content-Range: bytes {$range[0]}-{$range[1]}/{$info['size']}" );
+                       } elseif ( $range === 'invalid' ) {
+                               if ( $sendErrors ) {
+                                       $headerFunc( 416 );
+                                       $headerFunc( 'Cache-Control: no-cache' );
+                                       $headerFunc( 'Content-Type: text/html; charset=utf-8' );
+                                       $headerFunc( 'Content-Range: bytes */' . $info['size'] );
+                               }
+                               return false;
+                       } else { // unsupported Range request (e.g. multiple ranges)
+                               $range = null;
+                               $headerFunc( 'Content-Length: ' . $info['size'] );
+                       }
+               } else {
+                       $range = null;
+                       $headerFunc( 'Content-Length: ' . $info['size'] );
+               }
+
+               if ( is_array( $range ) ) {
+                       $handle = fopen( $this->path, 'rb' );
+                       if ( $handle ) {
+                               $ok = true;
+                               fseek( $handle, $range[0] );
+                               $remaining = $range[2];
+                               while ( $remaining > 0 && $ok ) {
+                                       $bytes = min( $remaining, 8 * 1024 );
+                                       $data = fread( $handle, $bytes );
+                                       $remaining -= $bytes;
+                                       $ok = ( $data !== false );
+                                       print $data;
+                               }
+                       } else {
+                               return false;
+                       }
+               } else {
+                       return readfile( $this->path ) !== false; // faster
+               }
+
+               return true;
+       }
+
+       /**
+        * Send out a standard 404 message for a file
+        *
+        * @param string $fname Full name and path of the file to stream
+        * @param integer $flags Bitfield of STREAM_* constants
+        * @since 1.24
+        */
+       public static function send404Message( $fname, $flags = 0 ) {
+               if ( ( $flags & self::STREAM_HEADLESS ) == 0 ) {
+                       HttpStatus::header( 404 );
+                       header( 'Cache-Control: no-cache' );
+                       header( 'Content-Type: text/html; charset=utf-8' );
+               }
+               $encFile = htmlspecialchars( $fname );
+               $encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] );
+               echo "<!DOCTYPE html><html><body>
+                       <h1>File not found</h1>
+                       <p>Although this PHP script ($encScript) exists, the file requested for output
+                       ($encFile) does not.</p>
+                       </body></html>
+                       ";
+       }
+
+       /**
+        * Convert a Range header value to an absolute (start, end) range tuple
+        *
+        * @param string $range Range header value
+        * @param integer $size File size
+        * @return array|string Returns error string on failure (start, end, length)
+        * @since 1.24
+        */
+       public static function parseRange( $range, $size ) {
+               $m = [];
+               if ( preg_match( '#^bytes=(\d*)-(\d*)$#', $range, $m ) ) {
+                       list( , $start, $end ) = $m;
+                       if ( $start === '' && $end === '' ) {
+                               $absRange = [ 0, $size - 1 ];
+                       } elseif ( $start === '' ) {
+                               $absRange = [ $size - $end, $size - 1 ];
+                       } elseif ( $end === '' ) {
+                               $absRange = [ $start, $size - 1 ];
+                       } else {
+                               $absRange = [ $start, $end ];
+                       }
+                       if ( $absRange[0] >= 0 && $absRange[1] >= $absRange[0] ) {
+                               if ( $absRange[0] < $size ) {
+                                       $absRange[1] = min( $absRange[1], $size - 1 ); // stop at EOF
+                                       $absRange[2] = $absRange[1] - $absRange[0] + 1;
+                                       return $absRange;
+                               } elseif ( $absRange[0] == 0 && $size == 0 ) {
+                                       return 'unrecognized'; // the whole file should just be sent
+                               }
+                       }
+                       return 'invalid';
+               }
+               return 'unrecognized';
+       }
+
+       protected static function resetOutputBuffers() {
+               while ( ob_get_status() ) {
+                       if ( !ob_end_clean() ) {
+                               // Could not remove output buffer handler; abort now
+                               // to avoid getting in some kind of infinite loop.
+                               break;
+                       }
+               }
+       }
+
+       /**
+        * Determine the file type of a file based on the path
+        *
+        * @param string $filename Storage path or file system path
+        * @return null|string
+        */
+       protected static function contentTypeFromPath( $filename ) {
+               $ext = strrchr( $filename, '.' );
+               $ext = $ext === false ? '' : strtolower( substr( $ext, 1 ) );
+
+               switch ( $ext ) {
+                       case 'gif':
+                               return 'image/gif';
+                       case 'png':
+                               return 'image/png';
+                       case 'jpg':
+                               return 'image/jpeg';
+                       case 'jpeg':
+                               return 'image/jpeg';
+               }
+
+               return 'unknown/unknown';
+       }
+}
diff --git a/includes/libs/filebackend/MemoryFileBackend.php b/includes/libs/filebackend/MemoryFileBackend.php
new file mode 100644 (file)
index 0000000..44fe2cb
--- /dev/null
@@ -0,0 +1,263 @@
+<?php
+/**
+ * Simulation of a backend storage in memory.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * Simulation of a backend storage in memory.
+ *
+ * All data in the backend is automatically deleted at the end of PHP execution.
+ * Since the data stored here is volatile, this is only useful for staging or testing.
+ *
+ * @ingroup FileBackend
+ * @since 1.23
+ */
+class MemoryFileBackend extends FileBackendStore {
+       /** @var array Map of (file path => (data,mtime) */
+       protected $files = [];
+
+       public function getFeatures() {
+               return self::ATTR_UNICODE_PATHS;
+       }
+
+       public function isPathUsableInternal( $storagePath ) {
+               return true;
+       }
+
+       protected function doCreateInternal( array $params ) {
+               $status = $this->newStatus();
+
+               $dst = $this->resolveHashKey( $params['dst'] );
+               if ( $dst === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               $this->files[$dst] = [
+                       'data' => $params['content'],
+                       'mtime' => wfTimestamp( TS_MW, time() )
+               ];
+
+               return $status;
+       }
+
+       protected function doStoreInternal( array $params ) {
+               $status = $this->newStatus();
+
+               $dst = $this->resolveHashKey( $params['dst'] );
+               if ( $dst === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               MediaWiki\suppressWarnings();
+               $data = file_get_contents( $params['src'] );
+               MediaWiki\restoreWarnings();
+               if ( $data === false ) { // source doesn't exist?
+                       $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
+
+                       return $status;
+               }
+
+               $this->files[$dst] = [
+                       'data' => $data,
+                       'mtime' => wfTimestamp( TS_MW, time() )
+               ];
+
+               return $status;
+       }
+
+       protected function doCopyInternal( array $params ) {
+               $status = $this->newStatus();
+
+               $src = $this->resolveHashKey( $params['src'] );
+               if ( $src === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
+               }
+
+               $dst = $this->resolveHashKey( $params['dst'] );
+               if ( $dst === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               if ( !isset( $this->files[$src] ) ) {
+                       if ( empty( $params['ignoreMissingSource'] ) ) {
+                               $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+                       }
+
+                       return $status;
+               }
+
+               $this->files[$dst] = [
+                       'data' => $this->files[$src]['data'],
+                       'mtime' => wfTimestamp( TS_MW, time() )
+               ];
+
+               return $status;
+       }
+
+       protected function doDeleteInternal( array $params ) {
+               $status = $this->newStatus();
+
+               $src = $this->resolveHashKey( $params['src'] );
+               if ( $src === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
+               }
+
+               if ( !isset( $this->files[$src] ) ) {
+                       if ( empty( $params['ignoreMissingSource'] ) ) {
+                               $status->fatal( 'backend-fail-delete', $params['src'] );
+                       }
+
+                       return $status;
+               }
+
+               unset( $this->files[$src] );
+
+               return $status;
+       }
+
+       protected function doGetFileStat( array $params ) {
+               $src = $this->resolveHashKey( $params['src'] );
+               if ( $src === null ) {
+                       return null;
+               }
+
+               if ( isset( $this->files[$src] ) ) {
+                       return [
+                               'mtime' => $this->files[$src]['mtime'],
+                               'size' => strlen( $this->files[$src]['data'] ),
+                       ];
+               }
+
+               return false;
+       }
+
+       protected function doGetLocalCopyMulti( array $params ) {
+               $tmpFiles = []; // (path => TempFSFile)
+               foreach ( $params['srcs'] as $srcPath ) {
+                       $src = $this->resolveHashKey( $srcPath );
+                       if ( $src === null || !isset( $this->files[$src] ) ) {
+                               $fsFile = null;
+                       } else {
+                               // Create a new temporary file with the same extension...
+                               $ext = FileBackend::extensionFromPath( $src );
+                               $fsFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
+                               if ( $fsFile ) {
+                                       $bytes = file_put_contents( $fsFile->getPath(), $this->files[$src]['data'] );
+                                       if ( $bytes !== strlen( $this->files[$src]['data'] ) ) {
+                                               $fsFile = null;
+                                       }
+                               }
+                       }
+                       $tmpFiles[$srcPath] = $fsFile;
+               }
+
+               return $tmpFiles;
+       }
+
+       protected function doDirectoryExists( $container, $dir, array $params ) {
+               $prefix = rtrim( "$container/$dir", '/' ) . '/';
+               foreach ( $this->files as $path => $data ) {
+                       if ( strpos( $path, $prefix ) === 0 ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+       public function getDirectoryListInternal( $container, $dir, array $params ) {
+               $dirs = [];
+               $prefix = rtrim( "$container/$dir", '/' ) . '/';
+               $prefixLen = strlen( $prefix );
+               foreach ( $this->files as $path => $data ) {
+                       if ( strpos( $path, $prefix ) === 0 ) {
+                               $relPath = substr( $path, $prefixLen );
+                               if ( $relPath === false ) {
+                                       continue;
+                               } elseif ( strpos( $relPath, '/' ) === false ) {
+                                       continue; // just a file
+                               }
+                               $parts = array_slice( explode( '/', $relPath ), 0, -1 ); // last part is file name
+                               if ( !empty( $params['topOnly'] ) ) {
+                                       $dirs[$parts[0]] = 1; // top directory
+                               } else {
+                                       $current = '';
+                                       foreach ( $parts as $part ) { // all directories
+                                               $dir = ( $current === '' ) ? $part : "$current/$part";
+                                               $dirs[$dir] = 1;
+                                               $current = $dir;
+                                       }
+                               }
+                       }
+               }
+
+               return array_keys( $dirs );
+       }
+
+       public function getFileListInternal( $container, $dir, array $params ) {
+               $files = [];
+               $prefix = rtrim( "$container/$dir", '/' ) . '/';
+               $prefixLen = strlen( $prefix );
+               foreach ( $this->files as $path => $data ) {
+                       if ( strpos( $path, $prefix ) === 0 ) {
+                               $relPath = substr( $path, $prefixLen );
+                               if ( $relPath === false ) {
+                                       continue;
+                               } elseif ( !empty( $params['topOnly'] ) && strpos( $relPath, '/' ) !== false ) {
+                                       continue;
+                               }
+                               $files[] = $relPath;
+                       }
+               }
+
+               return $files;
+       }
+
+       protected function directoriesAreVirtual() {
+               return true;
+       }
+
+       /**
+        * Get the absolute file system path for a storage path
+        *
+        * @param string $storagePath Storage path
+        * @return string|null
+        */
+       protected function resolveHashKey( $storagePath ) {
+               list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
+               if ( $relPath === null ) {
+                       return null; // invalid
+               }
+
+               return ( $relPath !== '' ) ? "$fullCont/$relPath" : $fullCont;
+       }
+}
diff --git a/includes/libs/filebackend/SwiftFileBackend.php b/includes/libs/filebackend/SwiftFileBackend.php
new file mode 100644 (file)
index 0000000..4bc0ce6
--- /dev/null
@@ -0,0 +1,1937 @@
+<?php
+/**
+ * OpenStack Swift based file backend.
+ *
+ * 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 FileBackend
+ * @author Russ Nelson
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Class for an OpenStack Swift (or Ceph RGW) based file backend.
+ *
+ * StatusValue messages should avoid mentioning the Swift account name.
+ * Likewise, error suppression should be used to avoid path disclosure.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+class SwiftFileBackend extends FileBackendStore {
+       /** @var MultiHttpClient */
+       protected $http;
+
+       /** @var int TTL in seconds */
+       protected $authTTL;
+
+       /** @var string Authentication base URL (without version) */
+       protected $swiftAuthUrl;
+
+       /** @var string Swift user (account:user) to authenticate as */
+       protected $swiftUser;
+
+       /** @var string Secret key for user */
+       protected $swiftKey;
+
+       /** @var string Shared secret value for making temp URLs */
+       protected $swiftTempUrlKey;
+
+       /** @var string S3 access key (RADOS Gateway) */
+       protected $rgwS3AccessKey;
+
+       /** @var string S3 authentication key (RADOS Gateway) */
+       protected $rgwS3SecretKey;
+
+       /** @var BagOStuff */
+       protected $srvCache;
+
+       /** @var ProcessCacheLRU Container stat cache */
+       protected $containerStatCache;
+
+       /** @var array */
+       protected $authCreds;
+
+       /** @var int UNIX timestamp */
+       protected $authSessionTimestamp = 0;
+
+       /** @var int UNIX timestamp */
+       protected $authErrorTimestamp = null;
+
+       /** @var bool Whether the server is an Ceph RGW */
+       protected $isRGW = false;
+
+       /**
+        * @see FileBackendStore::__construct()
+        * Additional $config params include:
+        *   - swiftAuthUrl       : Swift authentication server URL
+        *   - swiftUser          : Swift user used by MediaWiki (account:username)
+        *   - swiftKey           : Swift authentication key for the above user
+        *   - swiftAuthTTL       : Swift authentication TTL (seconds)
+        *   - swiftTempUrlKey    : Swift "X-Account-Meta-Temp-URL-Key" value on the account.
+        *                          Do not set this until it has been set in the backend.
+        *   - shardViaHashLevels : Map of container names to sharding config with:
+        *                             - base   : base of hash characters, 16 or 36
+        *                             - levels : the number of hash levels (and digits)
+        *                             - repeat : hash subdirectories are prefixed with all the
+        *                                        parent hash directory names (e.g. "a/ab/abc")
+        *   - cacheAuthInfo      : Whether to cache authentication tokens in APC, XCache, ect.
+        *                          If those are not available, then the main cache will be used.
+        *                          This is probably insecure in shared hosting environments.
+        *   - rgwS3AccessKey     : Rados Gateway S3 "access key" value on the account.
+        *                          Do not set this until it has been set in the backend.
+        *                          This is used for generating expiring pre-authenticated URLs.
+        *                          Only use this when using rgw and to work around
+        *                          http://tracker.newdream.net/issues/3454.
+        *   - rgwS3SecretKey     : Rados Gateway S3 "secret key" value on the account.
+        *                          Do not set this until it has been set in the backend.
+        *                          This is used for generating expiring pre-authenticated URLs.
+        *                          Only use this when using rgw and to work around
+        *                          http://tracker.newdream.net/issues/3454.
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+               // Required settings
+               $this->swiftAuthUrl = $config['swiftAuthUrl'];
+               $this->swiftUser = $config['swiftUser'];
+               $this->swiftKey = $config['swiftKey'];
+               // Optional settings
+               $this->authTTL = isset( $config['swiftAuthTTL'] )
+                       ? $config['swiftAuthTTL']
+                       : 15 * 60; // some sane number
+               $this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] )
+                       ? $config['swiftTempUrlKey']
+                       : '';
+               $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
+                       ? $config['shardViaHashLevels']
+                       : '';
+               $this->rgwS3AccessKey = isset( $config['rgwS3AccessKey'] )
+                       ? $config['rgwS3AccessKey']
+                       : '';
+               $this->rgwS3SecretKey = isset( $config['rgwS3SecretKey'] )
+                       ? $config['rgwS3SecretKey']
+                       : '';
+               // HTTP helper client
+               $this->http = new MultiHttpClient( [] );
+               // Cache container information to mask latency
+               if ( isset( $config['wanCache'] ) && $config['wanCache'] instanceof WANObjectCache ) {
+                       $this->memCache = $config['wanCache'];
+               }
+               // Process cache for container info
+               $this->containerStatCache = new ProcessCacheLRU( 300 );
+               // Cache auth token information to avoid RTTs
+               if ( !empty( $config['cacheAuthInfo'] ) && isset( $config['srvCache'] ) ) {
+                       $this->srvCache = $config['srvCache'];
+               } else {
+                       $this->srvCache = new EmptyBagOStuff();
+               }
+       }
+
+       public function getFeatures() {
+               return ( FileBackend::ATTR_UNICODE_PATHS |
+                       FileBackend::ATTR_HEADERS | FileBackend::ATTR_METADATA );
+       }
+
+       protected function resolveContainerPath( $container, $relStoragePath ) {
+               if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) {
+                       return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
+               } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
+                       return null; // too long for Swift
+               }
+
+               return $relStoragePath;
+       }
+
+       public function isPathUsableInternal( $storagePath ) {
+               list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
+               if ( $rel === null ) {
+                       return false; // invalid
+               }
+
+               return is_array( $this->getContainerStat( $container ) );
+       }
+
+       /**
+        * Sanitize and filter the custom headers from a $params array.
+        * Only allows certain "standard" Content- and X-Content- headers.
+        *
+        * @param array $params
+        * @return array Sanitized value of 'headers' field in $params
+        */
+       protected function sanitizeHdrs( array $params ) {
+               return isset( $params['headers'] )
+                       ? $this->getCustomHeaders( $params['headers'] )
+                       : [];
+
+       }
+
+       /**
+        * @param array $rawHeaders
+        * @return array Custom non-metadata HTTP headers
+        */
+       protected function getCustomHeaders( array $rawHeaders ) {
+               $headers = [];
+
+               // Normalize casing, and strip out illegal headers
+               foreach ( $rawHeaders as $name => $value ) {
+                       $name = strtolower( $name );
+                       if ( preg_match( '/^content-(type|length)$/', $name ) ) {
+                               continue; // blacklisted
+                       } elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
+                               $headers[$name] = $value; // allowed
+                       } elseif ( preg_match( '/^content-(disposition)/', $name ) ) {
+                               $headers[$name] = $value; // allowed
+                       }
+               }
+               // By default, Swift has annoyingly low maximum header value limits
+               if ( isset( $headers['content-disposition'] ) ) {
+                       $disposition = '';
+                       // @note: assume FileBackend::makeContentDisposition() already used
+                       foreach ( explode( ';', $headers['content-disposition'] ) as $part ) {
+                               $part = trim( $part );
+                               $new = ( $disposition === '' ) ? $part : "{$disposition};{$part}";
+                               if ( strlen( $new ) <= 255 ) {
+                                       $disposition = $new;
+                               } else {
+                                       break; // too long; sigh
+                               }
+                       }
+                       $headers['content-disposition'] = $disposition;
+               }
+
+               return $headers;
+       }
+
+       /**
+        * @param array $rawHeaders
+        * @return array Custom metadata headers
+        */
+       protected function getMetadataHeaders( array $rawHeaders ) {
+               $headers = [];
+               foreach ( $rawHeaders as $name => $value ) {
+                       $name = strtolower( $name );
+                       if ( strpos( $name, 'x-object-meta-' ) === 0 ) {
+                               $headers[$name] = $value;
+                       }
+               }
+
+               return $headers;
+       }
+
+       /**
+        * @param array $rawHeaders
+        * @return array Custom metadata headers with prefix removed
+        */
+       protected function getMetadata( array $rawHeaders ) {
+               $metadata = [];
+               foreach ( $this->getMetadataHeaders( $rawHeaders ) as $name => $value ) {
+                       $metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value;
+               }
+
+               return $metadata;
+       }
+
+       protected function doCreateInternal( array $params ) {
+               $status = $this->newStatus();
+
+               list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
+               if ( $dstRel === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               $sha1Hash = Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 );
+               $contentType = isset( $params['headers']['content-type'] )
+                       ? $params['headers']['content-type']
+                       : $this->getContentType( $params['dst'], $params['content'], null );
+
+               $reqs = [ [
+                       'method' => 'PUT',
+                       'url' => [ $dstCont, $dstRel ],
+                       'headers' => [
+                               'content-length' => strlen( $params['content'] ),
+                               'etag' => md5( $params['content'] ),
+                               'content-type' => $contentType,
+                               'x-object-meta-sha1base36' => $sha1Hash
+                       ] + $this->sanitizeHdrs( $params ),
+                       'body' => $params['content']
+               ] ];
+
+               $method = __METHOD__;
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
+                       if ( $rcode === 201 ) {
+                               // good
+                       } elseif ( $rcode === 412 ) {
+                               $status->fatal( 'backend-fail-contenttype', $params['dst'] );
+                       } else {
+                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
+                       }
+               };
+
+               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $status->value = $opHandle;
+               } else { // actually write the object in Swift
+                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
+               }
+
+               return $status;
+       }
+
+       protected function doStoreInternal( array $params ) {
+               $status = $this->newStatus();
+
+               list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
+               if ( $dstRel === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               MediaWiki\suppressWarnings();
+               $sha1Hash = sha1_file( $params['src'] );
+               MediaWiki\restoreWarnings();
+               if ( $sha1Hash === false ) { // source doesn't exist?
+                       $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
+
+                       return $status;
+               }
+               $sha1Hash = Wikimedia\base_convert( $sha1Hash, 16, 36, 31 );
+               $contentType = isset( $params['headers']['content-type'] )
+                       ? $params['headers']['content-type']
+                       : $this->getContentType( $params['dst'], null, $params['src'] );
+
+               $handle = fopen( $params['src'], 'rb' );
+               if ( $handle === false ) { // source doesn't exist?
+                       $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
+
+                       return $status;
+               }
+
+               $reqs = [ [
+                       'method' => 'PUT',
+                       'url' => [ $dstCont, $dstRel ],
+                       'headers' => [
+                               'content-length' => filesize( $params['src'] ),
+                               'etag' => md5_file( $params['src'] ),
+                               'content-type' => $contentType,
+                               'x-object-meta-sha1base36' => $sha1Hash
+                       ] + $this->sanitizeHdrs( $params ),
+                       'body' => $handle // resource
+               ] ];
+
+               $method = __METHOD__;
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
+                       if ( $rcode === 201 ) {
+                               // good
+                       } elseif ( $rcode === 412 ) {
+                               $status->fatal( 'backend-fail-contenttype', $params['dst'] );
+                       } else {
+                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
+                       }
+               };
+
+               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $status->value = $opHandle;
+               } else { // actually write the object in Swift
+                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
+               }
+
+               return $status;
+       }
+
+       protected function doCopyInternal( array $params ) {
+               $status = $this->newStatus();
+
+               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+               if ( $srcRel === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
+               }
+
+               list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
+               if ( $dstRel === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               $reqs = [ [
+                       'method' => 'PUT',
+                       'url' => [ $dstCont, $dstRel ],
+                       'headers' => [
+                               'x-copy-from' => '/' . rawurlencode( $srcCont ) .
+                                       '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
+                       ] + $this->sanitizeHdrs( $params ), // extra headers merged into object
+               ] ];
+
+               $method = __METHOD__;
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
+                       if ( $rcode === 201 ) {
+                               // good
+                       } elseif ( $rcode === 404 ) {
+                               $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+                       } else {
+                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
+                       }
+               };
+
+               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $status->value = $opHandle;
+               } else { // actually write the object in Swift
+                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
+               }
+
+               return $status;
+       }
+
+       protected function doMoveInternal( array $params ) {
+               $status = $this->newStatus();
+
+               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+               if ( $srcRel === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
+               }
+
+               list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
+               if ( $dstRel === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
+
+                       return $status;
+               }
+
+               $reqs = [
+                       [
+                               'method' => 'PUT',
+                               'url' => [ $dstCont, $dstRel ],
+                               'headers' => [
+                                       'x-copy-from' => '/' . rawurlencode( $srcCont ) .
+                                               '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
+                               ] + $this->sanitizeHdrs( $params ) // extra headers merged into object
+                       ]
+               ];
+               if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
+                       $reqs[] = [
+                               'method' => 'DELETE',
+                               'url' => [ $srcCont, $srcRel ],
+                               'headers' => []
+                       ];
+               }
+
+               $method = __METHOD__;
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
+                       if ( $request['method'] === 'PUT' && $rcode === 201 ) {
+                               // good
+                       } elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) {
+                               // good
+                       } elseif ( $rcode === 404 ) {
+                               $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
+                       } else {
+                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
+                       }
+               };
+
+               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $status->value = $opHandle;
+               } else { // actually move the object in Swift
+                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
+               }
+
+               return $status;
+       }
+
+       protected function doDeleteInternal( array $params ) {
+               $status = $this->newStatus();
+
+               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+               if ( $srcRel === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
+               }
+
+               $reqs = [ [
+                       'method' => 'DELETE',
+                       'url' => [ $srcCont, $srcRel ],
+                       'headers' => []
+               ] ];
+
+               $method = __METHOD__;
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
+                       if ( $rcode === 204 ) {
+                               // good
+                       } elseif ( $rcode === 404 ) {
+                               if ( empty( $params['ignoreMissingSource'] ) ) {
+                                       $status->fatal( 'backend-fail-delete', $params['src'] );
+                               }
+                       } else {
+                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
+                       }
+               };
+
+               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $status->value = $opHandle;
+               } else { // actually delete the object in Swift
+                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
+               }
+
+               return $status;
+       }
+
+       protected function doDescribeInternal( array $params ) {
+               $status = $this->newStatus();
+
+               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+               if ( $srcRel === null ) {
+                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
+               }
+
+               // Fetch the old object headers/metadata...this should be in stat cache by now
+               $stat = $this->getFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
+               if ( $stat && !isset( $stat['xattr'] ) ) { // older cache entry
+                       $stat = $this->doGetFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
+               }
+               if ( !$stat ) {
+                       $status->fatal( 'backend-fail-describe', $params['src'] );
+
+                       return $status;
+               }
+
+               // POST clears prior headers, so we need to merge the changes in to the old ones
+               $metaHdrs = [];
+               foreach ( $stat['xattr']['metadata'] as $name => $value ) {
+                       $metaHdrs["x-object-meta-$name"] = $value;
+               }
+               $customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers'];
+
+               $reqs = [ [
+                       'method' => 'POST',
+                       'url' => [ $srcCont, $srcRel ],
+                       'headers' => $metaHdrs + $customHdrs
+               ] ];
+
+               $method = __METHOD__;
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
+                       if ( $rcode === 202 ) {
+                               // good
+                       } elseif ( $rcode === 404 ) {
+                               $status->fatal( 'backend-fail-describe', $params['src'] );
+                       } else {
+                               $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
+                       }
+               };
+
+               $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
+               if ( !empty( $params['async'] ) ) { // deferred
+                       $status->value = $opHandle;
+               } else { // actually change the object in Swift
+                       $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
+               }
+
+               return $status;
+       }
+
+       protected function doPrepareInternal( $fullCont, $dir, array $params ) {
+               $status = $this->newStatus();
+
+               // (a) Check if container already exists
+               $stat = $this->getContainerStat( $fullCont );
+               if ( is_array( $stat ) ) {
+                       return $status; // already there
+               } elseif ( $stat === null ) {
+                       $status->fatal( 'backend-fail-internal', $this->name );
+                       $this->logger->error( __METHOD__ . ': cannot get container stat' );
+
+                       return $status;
+               }
+
+               // (b) Create container as needed with proper ACLs
+               if ( $stat === false ) {
+                       $params['op'] = 'prepare';
+                       $status->merge( $this->createContainer( $fullCont, $params ) );
+               }
+
+               return $status;
+       }
+
+       protected function doSecureInternal( $fullCont, $dir, array $params ) {
+               $status = $this->newStatus();
+               if ( empty( $params['noAccess'] ) ) {
+                       return $status; // nothing to do
+               }
+
+               $stat = $this->getContainerStat( $fullCont );
+               if ( is_array( $stat ) ) {
+                       // Make container private to end-users...
+                       $status->merge( $this->setContainerAccess(
+                               $fullCont,
+                               [ $this->swiftUser ], // read
+                               [ $this->swiftUser ] // write
+                       ) );
+               } elseif ( $stat === false ) {
+                       $status->fatal( 'backend-fail-usable', $params['dir'] );
+               } else {
+                       $status->fatal( 'backend-fail-internal', $this->name );
+                       $this->logger->error( __METHOD__ . ': cannot get container stat' );
+               }
+
+               return $status;
+       }
+
+       protected function doPublishInternal( $fullCont, $dir, array $params ) {
+               $status = $this->newStatus();
+
+               $stat = $this->getContainerStat( $fullCont );
+               if ( is_array( $stat ) ) {
+                       // Make container public to end-users...
+                       $status->merge( $this->setContainerAccess(
+                               $fullCont,
+                               [ $this->swiftUser, '.r:*' ], // read
+                               [ $this->swiftUser ] // write
+                       ) );
+               } elseif ( $stat === false ) {
+                       $status->fatal( 'backend-fail-usable', $params['dir'] );
+               } else {
+                       $status->fatal( 'backend-fail-internal', $this->name );
+                       $this->logger->error( __METHOD__ . ': cannot get container stat' );
+               }
+
+               return $status;
+       }
+
+       protected function doCleanInternal( $fullCont, $dir, array $params ) {
+               $status = $this->newStatus();
+
+               // Only containers themselves can be removed, all else is virtual
+               if ( $dir != '' ) {
+                       return $status; // nothing to do
+               }
+
+               // (a) Check the container
+               $stat = $this->getContainerStat( $fullCont, true );
+               if ( $stat === false ) {
+                       return $status; // ok, nothing to do
+               } elseif ( !is_array( $stat ) ) {
+                       $status->fatal( 'backend-fail-internal', $this->name );
+                       $this->logger->error( __METHOD__ . ': cannot get container stat' );
+
+                       return $status;
+               }
+
+               // (b) Delete the container if empty
+               if ( $stat['count'] == 0 ) {
+                       $params['op'] = 'clean';
+                       $status->merge( $this->deleteContainer( $fullCont, $params ) );
+               }
+
+               return $status;
+       }
+
+       protected function doGetFileStat( array $params ) {
+               $params = [ 'srcs' => [ $params['src'] ], 'concurrency' => 1 ] + $params;
+               unset( $params['src'] );
+               $stats = $this->doGetFileStatMulti( $params );
+
+               return reset( $stats );
+       }
+
+       /**
+        * Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT"/"2013-05-11T07:37:27.678360Z".
+        * Dates might also come in like "2013-05-11T07:37:27.678360" from Swift listings,
+        * missing the timezone suffix (though Ceph RGW does not appear to have this bug).
+        *
+        * @param string $ts
+        * @param int $format Output format (TS_* constant)
+        * @return string
+        * @throws FileBackendError
+        */
+       protected function convertSwiftDate( $ts, $format = TS_MW ) {
+               try {
+                       $timestamp = new MWTimestamp( $ts );
+
+                       return $timestamp->getTimestamp( $format );
+               } catch ( Exception $e ) {
+                       throw new FileBackendError( $e->getMessage() );
+               }
+       }
+
+       /**
+        * Fill in any missing object metadata and save it to Swift
+        *
+        * @param array $objHdrs Object response headers
+        * @param string $path Storage path to object
+        * @return array New headers
+        */
+       protected function addMissingMetadata( array $objHdrs, $path ) {
+               if ( isset( $objHdrs['x-object-meta-sha1base36'] ) ) {
+                       return $objHdrs; // nothing to do
+               }
+
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+               $this->logger->error( __METHOD__ . ": $path was not stored with SHA-1 metadata." );
+
+               $objHdrs['x-object-meta-sha1base36'] = false;
+
+               $auth = $this->getAuthentication();
+               if ( !$auth ) {
+                       return $objHdrs; // failed
+               }
+
+               // Find prior custom HTTP headers
+               $postHeaders = $this->getCustomHeaders( $objHdrs );
+               // Find prior metadata headers
+               $postHeaders += $this->getMetadataHeaders( $objHdrs );
+
+               $status = $this->newStatus();
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $scopeLockS = $this->getScopedFileLocks( [ $path ], LockManager::LOCK_UW, $status );
+               if ( $status->isOK() ) {
+                       $tmpFile = $this->getLocalCopy( [ 'src' => $path, 'latest' => 1 ] );
+                       if ( $tmpFile ) {
+                               $hash = $tmpFile->getSha1Base36();
+                               if ( $hash !== false ) {
+                                       $objHdrs['x-object-meta-sha1base36'] = $hash;
+                                       // Merge new SHA1 header into the old ones
+                                       $postHeaders['x-object-meta-sha1base36'] = $hash;
+                                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
+                                       list( $rcode ) = $this->http->run( [
+                                               'method' => 'POST',
+                                               'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
+                                               'headers' => $this->authTokenHeaders( $auth ) + $postHeaders
+                                       ] );
+                                       if ( $rcode >= 200 && $rcode <= 299 ) {
+                                               $this->deleteFileCache( $path );
+
+                                               return $objHdrs; // success
+                                       }
+                               }
+                       }
+               }
+
+               $this->logger->error( __METHOD__ . ": unable to set SHA-1 metadata for $path" );
+
+               return $objHdrs; // failed
+       }
+
+       protected function doGetFileContentsMulti( array $params ) {
+               $contents = [];
+
+               $auth = $this->getAuthentication();
+
+               $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
+               // Blindly create tmp files and stream to them, catching any exception if the file does
+               // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
+               $reqs = []; // (path => op)
+
+               foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
+                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
+                       if ( $srcRel === null || !$auth ) {
+                               $contents[$path] = false;
+                               continue;
+                       }
+                       // Create a new temporary memory file...
+                       $handle = fopen( 'php://temp', 'wb' );
+                       if ( $handle ) {
+                               $reqs[$path] = [
+                                       'method'  => 'GET',
+                                       'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
+                                       'headers' => $this->authTokenHeaders( $auth )
+                                               + $this->headersFromParams( $params ),
+                                       'stream'  => $handle,
+                               ];
+                       }
+                       $contents[$path] = false;
+               }
+
+               $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
+               $reqs = $this->http->runMulti( $reqs, $opts );
+               foreach ( $reqs as $path => $op ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
+                       if ( $rcode >= 200 && $rcode <= 299 ) {
+                               rewind( $op['stream'] ); // start from the beginning
+                               $contents[$path] = stream_get_contents( $op['stream'] );
+                       } elseif ( $rcode === 404 ) {
+                               $contents[$path] = false;
+                       } else {
+                               $this->onError( null, __METHOD__,
+                                       [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
+                       }
+                       fclose( $op['stream'] ); // close open handle
+               }
+
+               return $contents;
+       }
+
+       protected function doDirectoryExists( $fullCont, $dir, array $params ) {
+               $prefix = ( $dir == '' ) ? null : "{$dir}/";
+               $status = $this->objectListing( $fullCont, 'names', 1, null, $prefix );
+               if ( $status->isOK() ) {
+                       return ( count( $status->value ) ) > 0;
+               }
+
+               return null; // error
+       }
+
+       /**
+        * @see FileBackendStore::getDirectoryListInternal()
+        * @param string $fullCont
+        * @param string $dir
+        * @param array $params
+        * @return SwiftFileBackendDirList
+        */
+       public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
+               return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
+       }
+
+       /**
+        * @see FileBackendStore::getFileListInternal()
+        * @param string $fullCont
+        * @param string $dir
+        * @param array $params
+        * @return SwiftFileBackendFileList
+        */
+       public function getFileListInternal( $fullCont, $dir, array $params ) {
+               return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
+       }
+
+       /**
+        * Do not call this function outside of SwiftFileBackendFileList
+        *
+        * @param string $fullCont Resolved container name
+        * @param string $dir Resolved storage directory with no trailing slash
+        * @param string|null $after Resolved container relative path to list items after
+        * @param int $limit Max number of items to list
+        * @param array $params Parameters for getDirectoryList()
+        * @return array List of container relative resolved paths of directories directly under $dir
+        * @throws FileBackendError
+        */
+       public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
+               $dirs = [];
+               if ( $after === INF ) {
+                       return $dirs; // nothing more
+               }
+
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
+               $prefix = ( $dir == '' ) ? null : "{$dir}/";
+               // Non-recursive: only list dirs right under $dir
+               if ( !empty( $params['topOnly'] ) ) {
+                       $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
+                       if ( !$status->isOK() ) {
+                               throw new FileBackendError( "Iterator page I/O error." );
+                       }
+                       $objects = $status->value;
+                       foreach ( $objects as $object ) { // files and directories
+                               if ( substr( $object, -1 ) === '/' ) {
+                                       $dirs[] = $object; // directories end in '/'
+                               }
+                       }
+               } else {
+                       // Recursive: list all dirs under $dir and its subdirs
+                       $getParentDir = function ( $path ) {
+                               return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
+                       };
+
+                       // Get directory from last item of prior page
+                       $lastDir = $getParentDir( $after ); // must be first page
+                       $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
+
+                       if ( !$status->isOK() ) {
+                               throw new FileBackendError( "Iterator page I/O error." );
+                       }
+
+                       $objects = $status->value;
+
+                       foreach ( $objects as $object ) { // files
+                               $objectDir = $getParentDir( $object ); // directory of object
+
+                               if ( $objectDir !== false && $objectDir !== $dir ) {
+                                       // Swift stores paths in UTF-8, using binary sorting.
+                                       // See function "create_container_table" in common/db.py.
+                                       // If a directory is not "greater" than the last one,
+                                       // then it was already listed by the calling iterator.
+                                       if ( strcmp( $objectDir, $lastDir ) > 0 ) {
+                                               $pDir = $objectDir;
+                                               do { // add dir and all its parent dirs
+                                                       $dirs[] = "{$pDir}/";
+                                                       $pDir = $getParentDir( $pDir );
+                                               } while ( $pDir !== false // sanity
+                                                       && strcmp( $pDir, $lastDir ) > 0 // not done already
+                                                       && strlen( $pDir ) > strlen( $dir ) // within $dir
+                                               );
+                                       }
+                                       $lastDir = $objectDir;
+                               }
+                       }
+               }
+               // Page on the unfiltered directory listing (what is returned may be filtered)
+               if ( count( $objects ) < $limit ) {
+                       $after = INF; // avoid a second RTT
+               } else {
+                       $after = end( $objects ); // update last item
+               }
+
+               return $dirs;
+       }
+
+       /**
+        * Do not call this function outside of SwiftFileBackendFileList
+        *
+        * @param string $fullCont Resolved container name
+        * @param string $dir Resolved storage directory with no trailing slash
+        * @param string|null $after Resolved container relative path of file to list items after
+        * @param int $limit Max number of items to list
+        * @param array $params Parameters for getDirectoryList()
+        * @return array List of resolved container relative paths of files under $dir
+        * @throws FileBackendError
+        */
+       public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
+               $files = []; // list of (path, stat array or null) entries
+               if ( $after === INF ) {
+                       return $files; // nothing more
+               }
+
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
+               $prefix = ( $dir == '' ) ? null : "{$dir}/";
+               // $objects will contain a list of unfiltered names or CF_Object items
+               // Non-recursive: only list files right under $dir
+               if ( !empty( $params['topOnly'] ) ) {
+                       if ( !empty( $params['adviseStat'] ) ) {
+                               $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix, '/' );
+                       } else {
+                               $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
+                       }
+               } else {
+                       // Recursive: list all files under $dir and its subdirs
+                       if ( !empty( $params['adviseStat'] ) ) {
+                               $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix );
+                       } else {
+                               $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
+                       }
+               }
+
+               // Reformat this list into a list of (name, stat array or null) entries
+               if ( !$status->isOK() ) {
+                       throw new FileBackendError( "Iterator page I/O error." );
+               }
+
+               $objects = $status->value;
+               $files = $this->buildFileObjectListing( $params, $dir, $objects );
+
+               // Page on the unfiltered object listing (what is returned may be filtered)
+               if ( count( $objects ) < $limit ) {
+                       $after = INF; // avoid a second RTT
+               } else {
+                       $after = end( $objects ); // update last item
+                       $after = is_object( $after ) ? $after->name : $after;
+               }
+
+               return $files;
+       }
+
+       /**
+        * Build a list of file objects, filtering out any directories
+        * and extracting any stat info if provided in $objects (for CF_Objects)
+        *
+        * @param array $params Parameters for getDirectoryList()
+        * @param string $dir Resolved container directory path
+        * @param array $objects List of CF_Object items or object names
+        * @return array List of (names,stat array or null) entries
+        */
+       private function buildFileObjectListing( array $params, $dir, array $objects ) {
+               $names = [];
+               foreach ( $objects as $object ) {
+                       if ( is_object( $object ) ) {
+                               if ( isset( $object->subdir ) || !isset( $object->name ) ) {
+                                       continue; // virtual directory entry; ignore
+                               }
+                               $stat = [
+                                       // Convert various random Swift dates to TS_MW
+                                       'mtime'  => $this->convertSwiftDate( $object->last_modified, TS_MW ),
+                                       'size'   => (int)$object->bytes,
+                                       'sha1'   => null,
+                                       // Note: manifiest ETags are not an MD5 of the file
+                                       'md5'    => ctype_xdigit( $object->hash ) ? $object->hash : null,
+                                       'latest' => false // eventually consistent
+                               ];
+                               $names[] = [ $object->name, $stat ];
+                       } elseif ( substr( $object, -1 ) !== '/' ) {
+                               // Omit directories, which end in '/' in listings
+                               $names[] = [ $object, null ];
+                       }
+               }
+
+               return $names;
+       }
+
+       /**
+        * Do not call this function outside of SwiftFileBackendFileList
+        *
+        * @param string $path Storage path
+        * @param array $val Stat value
+        */
+       public function loadListingStatInternal( $path, array $val ) {
+               $this->cheapCache->set( $path, 'stat', $val );
+       }
+
+       protected function doGetFileXAttributes( array $params ) {
+               $stat = $this->getFileStat( $params );
+               if ( $stat ) {
+                       if ( !isset( $stat['xattr'] ) ) {
+                               // Stat entries filled by file listings don't include metadata/headers
+                               $this->clearCache( [ $params['src'] ] );
+                               $stat = $this->getFileStat( $params );
+                       }
+
+                       return $stat['xattr'];
+               } else {
+                       return false;
+               }
+       }
+
+       protected function doGetFileSha1base36( array $params ) {
+               $stat = $this->getFileStat( $params );
+               if ( $stat ) {
+                       if ( !isset( $stat['sha1'] ) ) {
+                               // Stat entries filled by file listings don't include SHA1
+                               $this->clearCache( [ $params['src'] ] );
+                               $stat = $this->getFileStat( $params );
+                       }
+
+                       return $stat['sha1'];
+               } else {
+                       return false;
+               }
+       }
+
+       protected function doStreamFile( array $params ) {
+               $status = $this->newStatus();
+
+               $flags = !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
+
+               list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+               if ( $srcRel === null ) {
+                       StreamFile::send404Message( $params['src'], $flags );
+                       $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
+               }
+
+               $auth = $this->getAuthentication();
+               if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) {
+                       StreamFile::send404Message( $params['src'], $flags );
+                       $status->fatal( 'backend-fail-stream', $params['src'] );
+
+                       return $status;
+               }
+
+               // If "headers" is set, we only want to send them if the file is there.
+               // Do not bother checking if the file exists if headers are not set though.
+               if ( $params['headers'] && !$this->fileExists( $params ) ) {
+                       StreamFile::send404Message( $params['src'], $flags );
+                       $status->fatal( 'backend-fail-stream', $params['src'] );
+
+                       return $status;
+               }
+
+               // Send the requested additional headers
+               foreach ( $params['headers'] as $header ) {
+                       header( $header ); // aways send
+               }
+
+               if ( empty( $params['allowOB'] ) ) {
+                       // Cancel output buffering and gzipping if set
+                       call_user_func( $this->obResetFunc );
+               }
+
+               $handle = fopen( 'php://output', 'wb' );
+               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
+                       'method' => 'GET',
+                       'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
+                       'headers' => $this->authTokenHeaders( $auth )
+                               + $this->headersFromParams( $params ) + $params['options'],
+                       'stream' => $handle,
+                       'flags'  => [ 'relayResponseHeaders' => empty( $params['headless'] ) ]
+               ] );
+
+               if ( $rcode >= 200 && $rcode <= 299 ) {
+                       // good
+               } elseif ( $rcode === 404 ) {
+                       $status->fatal( 'backend-fail-stream', $params['src'] );
+                       // Per bug 41113, nasty things can happen if bad cache entries get
+                       // stuck in cache. It's also possible that this error can come up
+                       // with simple race conditions. Clear out the stat cache to be safe.
+                       $this->clearCache( [ $params['src'] ] );
+                       $this->deleteFileCache( $params['src'] );
+               } else {
+                       $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
+               }
+
+               return $status;
+       }
+
+       protected function doGetLocalCopyMulti( array $params ) {
+               /** @var TempFSFile[] $tmpFiles */
+               $tmpFiles = [];
+
+               $auth = $this->getAuthentication();
+
+               $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
+               // Blindly create tmp files and stream to them, catching any exception if the file does
+               // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
+               $reqs = []; // (path => op)
+
+               foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
+                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
+                       if ( $srcRel === null || !$auth ) {
+                               $tmpFiles[$path] = null;
+                               continue;
+                       }
+                       // Get source file extension
+                       $ext = FileBackend::extensionFromPath( $path );
+                       // Create a new temporary file...
+                       $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
+                       if ( $tmpFile ) {
+                               $handle = fopen( $tmpFile->getPath(), 'wb' );
+                               if ( $handle ) {
+                                       $reqs[$path] = [
+                                               'method'  => 'GET',
+                                               'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
+                                               'headers' => $this->authTokenHeaders( $auth )
+                                                       + $this->headersFromParams( $params ),
+                                               'stream'  => $handle,
+                                       ];
+                               } else {
+                                       $tmpFile = null;
+                               }
+                       }
+                       $tmpFiles[$path] = $tmpFile;
+               }
+
+               $isLatest = ( $this->isRGW || !empty( $params['latest'] ) );
+               $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
+               $reqs = $this->http->runMulti( $reqs, $opts );
+               foreach ( $reqs as $path => $op ) {
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
+                       fclose( $op['stream'] ); // close open handle
+                       if ( $rcode >= 200 && $rcode <= 299 ) {
+                               $size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0;
+                               // Double check that the disk is not full/broken
+                               if ( $size != $rhdrs['content-length'] ) {
+                                       $tmpFiles[$path] = null;
+                                       $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
+                                       $this->onError( null, __METHOD__,
+                                               [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
+                               }
+                               // Set the file stat process cache in passing
+                               $stat = $this->getStatFromHeaders( $rhdrs );
+                               $stat['latest'] = $isLatest;
+                               $this->cheapCache->set( $path, 'stat', $stat );
+                       } elseif ( $rcode === 404 ) {
+                               $tmpFiles[$path] = false;
+                       } else {
+                               $tmpFiles[$path] = null;
+                               $this->onError( null, __METHOD__,
+                                       [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
+                       }
+               }
+
+               return $tmpFiles;
+       }
+
+       public function getFileHttpUrl( array $params ) {
+               if ( $this->swiftTempUrlKey != '' ||
+                       ( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' )
+               ) {
+                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
+                       if ( $srcRel === null ) {
+                               return null; // invalid path
+                       }
+
+                       $auth = $this->getAuthentication();
+                       if ( !$auth ) {
+                               return null;
+                       }
+
+                       $ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400;
+                       $expires = time() + $ttl;
+
+                       if ( $this->swiftTempUrlKey != '' ) {
+                               $url = $this->storageUrl( $auth, $srcCont, $srcRel );
+                               // Swift wants the signature based on the unencoded object name
+                               $contPath = parse_url( $this->storageUrl( $auth, $srcCont ), PHP_URL_PATH );
+                               $signature = hash_hmac( 'sha1',
+                                       "GET\n{$expires}\n{$contPath}/{$srcRel}",
+                                       $this->swiftTempUrlKey
+                               );
+
+                               return "{$url}?temp_url_sig={$signature}&temp_url_expires={$expires}";
+                       } else { // give S3 API URL for rgw
+                               // Path for signature starts with the bucket
+                               $spath = '/' . rawurlencode( $srcCont ) . '/' .
+                                       str_replace( '%2F', '/', rawurlencode( $srcRel ) );
+                               // Calculate the hash
+                               $signature = base64_encode( hash_hmac(
+                                       'sha1',
+                                       "GET\n\n\n{$expires}\n{$spath}",
+                                       $this->rgwS3SecretKey,
+                                       true // raw
+                               ) );
+                               // See http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
+                               // Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
+                               // Note: S3 API is the rgw default; remove the /swift/ URL bit.
+                               return str_replace( '/swift/v1', '', $this->storageUrl( $auth ) . $spath ) .
+                                       '?' .
+                                       http_build_query( [
+                                               'Signature' => $signature,
+                                               'Expires' => $expires,
+                                               'AWSAccessKeyId' => $this->rgwS3AccessKey
+                                       ] );
+                       }
+               }
+
+               return null;
+       }
+
+       protected function directoriesAreVirtual() {
+               return true;
+       }
+
+       /**
+        * Get headers to send to Swift when reading a file based
+        * on a FileBackend params array, e.g. that of getLocalCopy().
+        * $params is currently only checked for a 'latest' flag.
+        *
+        * @param array $params
+        * @return array
+        */
+       protected function headersFromParams( array $params ) {
+               $hdrs = [];
+               if ( !empty( $params['latest'] ) ) {
+                       $hdrs['x-newest'] = 'true';
+               }
+
+               return $hdrs;
+       }
+
+       /**
+        * @param FileBackendStoreOpHandle[] $fileOpHandles
+        *
+        * @return StatusValue[]
+        */
+       protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
+               /** @var $statuses StatusValue[] */
+               $statuses = [];
+
+               $auth = $this->getAuthentication();
+               if ( !$auth ) {
+                       foreach ( $fileOpHandles as $index => $fileOpHandle ) {
+                               $statuses[$index] = $this->newStatus( 'backend-fail-connect', $this->name );
+                       }
+
+                       return $statuses;
+               }
+
+               // Split the HTTP requests into stages that can be done concurrently
+               $httpReqsByStage = []; // map of (stage => index => HTTP request)
+               foreach ( $fileOpHandles as $index => $fileOpHandle ) {
+                       /** @var SwiftFileOpHandle $fileOpHandle */
+                       $reqs = $fileOpHandle->httpOp;
+                       // Convert the 'url' parameter to an actual URL using $auth
+                       foreach ( $reqs as $stage => &$req ) {
+                               list( $container, $relPath ) = $req['url'];
+                               $req['url'] = $this->storageUrl( $auth, $container, $relPath );
+                               $req['headers'] = isset( $req['headers'] ) ? $req['headers'] : [];
+                               $req['headers'] = $this->authTokenHeaders( $auth ) + $req['headers'];
+                               $httpReqsByStage[$stage][$index] = $req;
+                       }
+                       $statuses[$index] = $this->newStatus();
+               }
+
+               // Run all requests for the first stage, then the next, and so on
+               $reqCount = count( $httpReqsByStage );
+               for ( $stage = 0; $stage < $reqCount; ++$stage ) {
+                       $httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] );
+                       foreach ( $httpReqs as $index => $httpReq ) {
+                               // Run the callback for each request of this operation
+                               $callback = $fileOpHandles[$index]->callback;
+                               call_user_func_array( $callback, [ $httpReq, $statuses[$index] ] );
+                               // On failure, abort all remaining requests for this operation
+                               // (e.g. abort the DELETE request if the COPY request fails for a move)
+                               if ( !$statuses[$index]->isOK() ) {
+                                       $stages = count( $fileOpHandles[$index]->httpOp );
+                                       for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
+                                               unset( $httpReqsByStage[$s][$index] );
+                                       }
+                               }
+                       }
+               }
+
+               return $statuses;
+       }
+
+       /**
+        * Set read/write permissions for a Swift container.
+        *
+        * @see http://swift.openstack.org/misc.html#acls
+        *
+        * In general, we don't allow listings to end-users. It's not useful, isn't well-defined
+        * (lists are truncated to 10000 item with no way to page), and is just a performance risk.
+        *
+        * @param string $container Resolved Swift container
+        * @param array $readGrps List of the possible criteria for a request to have
+        * access to read a container. Each item is one of the following formats:
+        *   - account:user        : Grants access if the request is by the given user
+        *   - ".r:<regex>"        : Grants access if the request is from a referrer host that
+        *                           matches the expression and the request is not for a listing.
+        *                           Setting this to '*' effectively makes a container public.
+        *   -".rlistings:<regex>" : Grants access if the request is from a referrer host that
+        *                           matches the expression and the request is for a listing.
+        * @param array $writeGrps A list of the possible criteria for a request to have
+        * access to write to a container. Each item is of the following format:
+        *   - account:user       : Grants access if the request is by the given user
+        * @return StatusValue
+        */
+       protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
+               $status = $this->newStatus();
+               $auth = $this->getAuthentication();
+
+               if ( !$auth ) {
+                       $status->fatal( 'backend-fail-connect', $this->name );
+
+                       return $status;
+               }
+
+               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
+                       'method' => 'POST',
+                       'url' => $this->storageUrl( $auth, $container ),
+                       'headers' => $this->authTokenHeaders( $auth ) + [
+                               'x-container-read' => implode( ',', $readGrps ),
+                               'x-container-write' => implode( ',', $writeGrps )
+                       ]
+               ] );
+
+               if ( $rcode != 204 && $rcode !== 202 ) {
+                       $status->fatal( 'backend-fail-internal', $this->name );
+                       $this->logger->error( __METHOD__ . ': unexpected rcode value (' . $rcode . ')' );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Get a Swift container stat array, possibly from process cache.
+        * Use $reCache if the file count or byte count is needed.
+        *
+        * @param string $container Container name
+        * @param bool $bypassCache Bypass all caches and load from Swift
+        * @return array|bool|null False on 404, null on failure
+        */
+       protected function getContainerStat( $container, $bypassCache = false ) {
+               $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
+
+               if ( $bypassCache ) { // purge cache
+                       $this->containerStatCache->clear( $container );
+               } elseif ( !$this->containerStatCache->has( $container, 'stat' ) ) {
+                       $this->primeContainerCache( [ $container ] ); // check persistent cache
+               }
+               if ( !$this->containerStatCache->has( $container, 'stat' ) ) {
+                       $auth = $this->getAuthentication();
+                       if ( !$auth ) {
+                               return null;
+                       }
+
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
+                               'method' => 'HEAD',
+                               'url' => $this->storageUrl( $auth, $container ),
+                               'headers' => $this->authTokenHeaders( $auth )
+                       ] );
+
+                       if ( $rcode === 204 ) {
+                               $stat = [
+                                       'count' => $rhdrs['x-container-object-count'],
+                                       'bytes' => $rhdrs['x-container-bytes-used']
+                               ];
+                               if ( $bypassCache ) {
+                                       return $stat;
+                               } else {
+                                       $this->containerStatCache->set( $container, 'stat', $stat ); // cache it
+                                       $this->setContainerCache( $container, $stat ); // update persistent cache
+                               }
+                       } elseif ( $rcode === 404 ) {
+                               return false;
+                       } else {
+                               $this->onError( null, __METHOD__,
+                                       [ 'cont' => $container ], $rerr, $rcode, $rdesc );
+
+                               return null;
+                       }
+               }
+
+               return $this->containerStatCache->get( $container, 'stat' );
+       }
+
+       /**
+        * Create a Swift container
+        *
+        * @param string $container Container name
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function createContainer( $container, array $params ) {
+               $status = $this->newStatus();
+
+               $auth = $this->getAuthentication();
+               if ( !$auth ) {
+                       $status->fatal( 'backend-fail-connect', $this->name );
+
+                       return $status;
+               }
+
+               // @see SwiftFileBackend::setContainerAccess()
+               if ( empty( $params['noAccess'] ) ) {
+                       $readGrps = [ '.r:*', $this->swiftUser ]; // public
+               } else {
+                       $readGrps = [ $this->swiftUser ]; // private
+               }
+               $writeGrps = [ $this->swiftUser ]; // sanity
+
+               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
+                       'method' => 'PUT',
+                       'url' => $this->storageUrl( $auth, $container ),
+                       'headers' => $this->authTokenHeaders( $auth ) + [
+                               'x-container-read' => implode( ',', $readGrps ),
+                               'x-container-write' => implode( ',', $writeGrps )
+                       ]
+               ] );
+
+               if ( $rcode === 201 ) { // new
+                       // good
+               } elseif ( $rcode === 202 ) { // already there
+                       // this shouldn't really happen, but is OK
+               } else {
+                       $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Delete a Swift container
+        *
+        * @param string $container Container name
+        * @param array $params
+        * @return StatusValue
+        */
+       protected function deleteContainer( $container, array $params ) {
+               $status = $this->newStatus();
+
+               $auth = $this->getAuthentication();
+               if ( !$auth ) {
+                       $status->fatal( 'backend-fail-connect', $this->name );
+
+                       return $status;
+               }
+
+               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
+                       'method' => 'DELETE',
+                       'url' => $this->storageUrl( $auth, $container ),
+                       'headers' => $this->authTokenHeaders( $auth )
+               ] );
+
+               if ( $rcode >= 200 && $rcode <= 299 ) { // deleted
+                       $this->containerStatCache->clear( $container ); // purge
+               } elseif ( $rcode === 404 ) { // not there
+                       // this shouldn't really happen, but is OK
+               } elseif ( $rcode === 409 ) { // not empty
+                       $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc ); // race?
+               } else {
+                       $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Get a list of objects under a container.
+        * Either just the names or a list of stdClass objects with details can be returned.
+        *
+        * @param string $fullCont
+        * @param string $type ('info' for a list of object detail maps, 'names' for names only)
+        * @param int $limit
+        * @param string|null $after
+        * @param string|null $prefix
+        * @param string|null $delim
+        * @return StatusValue With the list as value
+        */
+       private function objectListing(
+               $fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
+       ) {
+               $status = $this->newStatus();
+
+               $auth = $this->getAuthentication();
+               if ( !$auth ) {
+                       $status->fatal( 'backend-fail-connect', $this->name );
+
+                       return $status;
+               }
+
+               $query = [ 'limit' => $limit ];
+               if ( $type === 'info' ) {
+                       $query['format'] = 'json';
+               }
+               if ( $after !== null ) {
+                       $query['marker'] = $after;
+               }
+               if ( $prefix !== null ) {
+                       $query['prefix'] = $prefix;
+               }
+               if ( $delim !== null ) {
+                       $query['delimiter'] = $delim;
+               }
+
+               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
+                       'method' => 'GET',
+                       'url' => $this->storageUrl( $auth, $fullCont ),
+                       'query' => $query,
+                       'headers' => $this->authTokenHeaders( $auth )
+               ] );
+
+               $params = [ 'cont' => $fullCont, 'prefix' => $prefix, 'delim' => $delim ];
+               if ( $rcode === 200 ) { // good
+                       if ( $type === 'info' ) {
+                               $status->value = FormatJson::decode( trim( $rbody ) );
+                       } else {
+                               $status->value = explode( "\n", trim( $rbody ) );
+                       }
+               } elseif ( $rcode === 204 ) {
+                       $status->value = []; // empty container
+               } elseif ( $rcode === 404 ) {
+                       $status->value = []; // no container
+               } else {
+                       $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
+               }
+
+               return $status;
+       }
+
+       protected function doPrimeContainerCache( array $containerInfo ) {
+               foreach ( $containerInfo as $container => $info ) {
+                       $this->containerStatCache->set( $container, 'stat', $info );
+               }
+       }
+
+       protected function doGetFileStatMulti( array $params ) {
+               $stats = [];
+
+               $auth = $this->getAuthentication();
+
+               $reqs = [];
+               foreach ( $params['srcs'] as $path ) {
+                       list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
+                       if ( $srcRel === null ) {
+                               $stats[$path] = false;
+                               continue; // invalid storage path
+                       } elseif ( !$auth ) {
+                               $stats[$path] = null;
+                               continue;
+                       }
+
+                       // (a) Check the container
+                       $cstat = $this->getContainerStat( $srcCont );
+                       if ( $cstat === false ) {
+                               $stats[$path] = false;
+                               continue; // ok, nothing to do
+                       } elseif ( !is_array( $cstat ) ) {
+                               $stats[$path] = null;
+                               continue;
+                       }
+
+                       $reqs[$path] = [
+                               'method'  => 'HEAD',
+                               'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
+                               'headers' => $this->authTokenHeaders( $auth ) + $this->headersFromParams( $params )
+                       ];
+               }
+
+               $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
+               $reqs = $this->http->runMulti( $reqs, $opts );
+
+               foreach ( $params['srcs'] as $path ) {
+                       if ( array_key_exists( $path, $stats ) ) {
+                               continue; // some sort of failure above
+                       }
+                       // (b) Check the file
+                       list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
+                       if ( $rcode === 200 || $rcode === 204 ) {
+                               // Update the object if it is missing some headers
+                               $rhdrs = $this->addMissingMetadata( $rhdrs, $path );
+                               // Load the stat array from the headers
+                               $stat = $this->getStatFromHeaders( $rhdrs );
+                               if ( $this->isRGW ) {
+                                       $stat['latest'] = true; // strong consistency
+                               }
+                       } elseif ( $rcode === 404 ) {
+                               $stat = false;
+                       } else {
+                               $stat = null;
+                               $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
+                       }
+                       $stats[$path] = $stat;
+               }
+
+               return $stats;
+       }
+
+       /**
+        * @param array $rhdrs
+        * @return array
+        */
+       protected function getStatFromHeaders( array $rhdrs ) {
+               // Fetch all of the custom metadata headers
+               $metadata = $this->getMetadata( $rhdrs );
+               // Fetch all of the custom raw HTTP headers
+               $headers = $this->sanitizeHdrs( [ 'headers' => $rhdrs ] );
+
+               return [
+                       // Convert various random Swift dates to TS_MW
+                       'mtime' => $this->convertSwiftDate( $rhdrs['last-modified'], TS_MW ),
+                       // Empty objects actually return no content-length header in Ceph
+                       'size'  => isset( $rhdrs['content-length'] ) ? (int)$rhdrs['content-length'] : 0,
+                       'sha1'  => isset( $metadata['sha1base36'] ) ? $metadata['sha1base36'] : null,
+                       // Note: manifiest ETags are not an MD5 of the file
+                       'md5'   => ctype_xdigit( $rhdrs['etag'] ) ? $rhdrs['etag'] : null,
+                       'xattr' => [ 'metadata' => $metadata, 'headers' => $headers ]
+               ];
+       }
+
+       /**
+        * @return array|null Credential map
+        */
+       protected function getAuthentication() {
+               if ( $this->authErrorTimestamp !== null ) {
+                       if ( ( time() - $this->authErrorTimestamp ) < 60 ) {
+                               return null; // failed last attempt; don't bother
+                       } else { // actually retry this time
+                               $this->authErrorTimestamp = null;
+                       }
+               }
+               // Session keys expire after a while, so we renew them periodically
+               $reAuth = ( ( time() - $this->authSessionTimestamp ) > $this->authTTL );
+               // Authenticate with proxy and get a session key...
+               if ( !$this->authCreds || $reAuth ) {
+                       $this->authSessionTimestamp = 0;
+                       $cacheKey = $this->getCredsCacheKey( $this->swiftUser );
+                       $creds = $this->srvCache->get( $cacheKey ); // credentials
+                       // Try to use the credential cache
+                       if ( isset( $creds['auth_token'] ) && isset( $creds['storage_url'] ) ) {
+                               $this->authCreds = $creds;
+                               // Skew the timestamp for worst case to avoid using stale credentials
+                               $this->authSessionTimestamp = time() - ceil( $this->authTTL / 2 );
+                       } else { // cache miss
+                               list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
+                                       'method' => 'GET',
+                                       'url' => "{$this->swiftAuthUrl}/v1.0",
+                                       'headers' => [
+                                               'x-auth-user' => $this->swiftUser,
+                                               'x-auth-key' => $this->swiftKey
+                                       ]
+                               ] );
+
+                               if ( $rcode >= 200 && $rcode <= 299 ) { // OK
+                                       $this->authCreds = [
+                                               'auth_token' => $rhdrs['x-auth-token'],
+                                               'storage_url' => $rhdrs['x-storage-url']
+                                       ];
+                                       $this->srvCache->set( $cacheKey, $this->authCreds, ceil( $this->authTTL / 2 ) );
+                                       $this->authSessionTimestamp = time();
+                               } elseif ( $rcode === 401 ) {
+                                       $this->onError( null, __METHOD__, [], "Authentication failed.", $rcode );
+                                       $this->authErrorTimestamp = time();
+
+                                       return null;
+                               } else {
+                                       $this->onError( null, __METHOD__, [], "HTTP return code: $rcode", $rcode );
+                                       $this->authErrorTimestamp = time();
+
+                                       return null;
+                               }
+                       }
+                       // Ceph RGW does not use <account> in URLs (OpenStack Swift uses "/v1/<account>")
+                       if ( substr( $this->authCreds['storage_url'], -3 ) === '/v1' ) {
+                               $this->isRGW = true; // take advantage of strong consistency in Ceph
+                       }
+               }
+
+               return $this->authCreds;
+       }
+
+       /**
+        * @param array $creds From getAuthentication()
+        * @param string $container
+        * @param string $object
+        * @return array
+        */
+       protected function storageUrl( array $creds, $container = null, $object = null ) {
+               $parts = [ $creds['storage_url'] ];
+               if ( strlen( $container ) ) {
+                       $parts[] = rawurlencode( $container );
+               }
+               if ( strlen( $object ) ) {
+                       $parts[] = str_replace( "%2F", "/", rawurlencode( $object ) );
+               }
+
+               return implode( '/', $parts );
+       }
+
+       /**
+        * @param array $creds From getAuthentication()
+        * @return array
+        */
+       protected function authTokenHeaders( array $creds ) {
+               return [ 'x-auth-token' => $creds['auth_token'] ];
+       }
+
+       /**
+        * Get the cache key for a container
+        *
+        * @param string $username
+        * @return string
+        */
+       private function getCredsCacheKey( $username ) {
+               return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl );
+       }
+
+       /**
+        * Log an unexpected exception for this backend.
+        * This also sets the StatusValue object to have a fatal error.
+        *
+        * @param StatusValue|null $status
+        * @param string $func
+        * @param array $params
+        * @param string $err Error string
+        * @param int $code HTTP status
+        * @param string $desc HTTP StatusValue description
+        */
+       public function onError( $status, $func, array $params, $err = '', $code = 0, $desc = '' ) {
+               if ( $status instanceof StatusValue ) {
+                       $status->fatal( 'backend-fail-internal', $this->name );
+               }
+               if ( $code == 401 ) { // possibly a stale token
+                       $this->srvCache->delete( $this->getCredsCacheKey( $this->swiftUser ) );
+               }
+               $this->logger->error(
+                       "HTTP $code ($desc) in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
+                       ( $err ? ": $err" : "" )
+               );
+       }
+}
+
+/**
+ * @see FileBackendStoreOpHandle
+ */
+class SwiftFileOpHandle extends FileBackendStoreOpHandle {
+       /** @var array List of Requests for MultiHttpClient */
+       public $httpOp;
+       /** @var Closure */
+       public $callback;
+
+       /**
+        * @param SwiftFileBackend $backend
+        * @param Closure $callback Function that takes (HTTP request array, status)
+        * @param array $httpOp MultiHttpClient op
+        */
+       public function __construct( SwiftFileBackend $backend, Closure $callback, array $httpOp ) {
+               $this->backend = $backend;
+               $this->callback = $callback;
+               $this->httpOp = $httpOp;
+       }
+}
+
+/**
+ * SwiftFileBackend helper class to page through listings.
+ * Swift also has a listing limit of 10,000 objects for sanity.
+ * Do not use this class from places outside SwiftFileBackend.
+ *
+ * @ingroup FileBackend
+ */
+abstract class SwiftFileBackendList implements Iterator {
+       /** @var array List of path or (path,stat array) entries */
+       protected $bufferIter = [];
+
+       /** @var string List items *after* this path */
+       protected $bufferAfter = null;
+
+       /** @var int */
+       protected $pos = 0;
+
+       /** @var array */
+       protected $params = [];
+
+       /** @var SwiftFileBackend */
+       protected $backend;
+
+       /** @var string Container name */
+       protected $container;
+
+       /** @var string Storage directory */
+       protected $dir;
+
+       /** @var int */
+       protected $suffixStart;
+
+       const PAGE_SIZE = 9000; // file listing buffer size
+
+       /**
+        * @param SwiftFileBackend $backend
+        * @param string $fullCont Resolved container name
+        * @param string $dir Resolved directory relative to container
+        * @param array $params
+        */
+       public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
+               $this->backend = $backend;
+               $this->container = $fullCont;
+               $this->dir = $dir;
+               if ( substr( $this->dir, -1 ) === '/' ) {
+                       $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
+               }
+               if ( $this->dir == '' ) { // whole container
+                       $this->suffixStart = 0;
+               } else { // dir within container
+                       $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
+               }
+               $this->params = $params;
+       }
+
+       /**
+        * @see Iterator::key()
+        * @return int
+        */
+       public function key() {
+               return $this->pos;
+       }
+
+       /**
+        * @see Iterator::next()
+        */
+       public function next() {
+               // Advance to the next file in the page
+               next( $this->bufferIter );
+               ++$this->pos;
+               // Check if there are no files left in this page and
+               // advance to the next page if this page was not empty.
+               if ( !$this->valid() && count( $this->bufferIter ) ) {
+                       $this->bufferIter = $this->pageFromList(
+                               $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
+                       ); // updates $this->bufferAfter
+               }
+       }
+
+       /**
+        * @see Iterator::rewind()
+        */
+       public function rewind() {
+               $this->pos = 0;
+               $this->bufferAfter = null;
+               $this->bufferIter = $this->pageFromList(
+                       $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
+               ); // updates $this->bufferAfter
+       }
+
+       /**
+        * @see Iterator::valid()
+        * @return bool
+        */
+       public function valid() {
+               if ( $this->bufferIter === null ) {
+                       return false; // some failure?
+               } else {
+                       return ( current( $this->bufferIter ) !== false ); // no paths can have this value
+               }
+       }
+
+       /**
+        * Get the given list portion (page)
+        *
+        * @param string $container Resolved container name
+        * @param string $dir Resolved path relative to container
+        * @param string $after
+        * @param int $limit
+        * @param array $params
+        * @return Traversable|array
+        */
+       abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
+}
+
+/**
+ * Iterator for listing directories
+ */
+class SwiftFileBackendDirList extends SwiftFileBackendList {
+       /**
+        * @see Iterator::current()
+        * @return string|bool String (relative path) or false
+        */
+       public function current() {
+               return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
+       }
+
+       protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
+               return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
+       }
+}
+
+/**
+ * Iterator for listing regular files
+ */
+class SwiftFileBackendFileList extends SwiftFileBackendList {
+       /**
+        * @see Iterator::current()
+        * @return string|bool String (relative path) or false
+        */
+       public function current() {
+               list( $path, $stat ) = current( $this->bufferIter );
+               $relPath = substr( $path, $this->suffixStart );
+               if ( is_array( $stat ) ) {
+                       $storageDir = rtrim( $this->params['dir'], '/' );
+                       $this->backend->loadListingStatInternal( "$storageDir/$relPath", $stat );
+               }
+
+               return $relPath;
+       }
+
+       protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
+               return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
+       }
+}
diff --git a/includes/libs/filebackend/fileop/CopyFileOp.php b/includes/libs/filebackend/fileop/CopyFileOp.php
new file mode 100644 (file)
index 0000000..e3b8c51
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Helper class for representing operations with transaction support.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * Copy a file from one storage path to another in the backend.
+ * Parameters for this operation are outlined in FileBackend::doOperations().
+ */
+class CopyFileOp extends FileOp {
+       protected function allowedParams() {
+               return [
+                       [ 'src', 'dst' ],
+                       [ 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ],
+                       [ 'src', 'dst' ]
+               ];
+       }
+
+       protected function doPrecheck( array &$predicates ) {
+               $status = StatusValue::newGood();
+               // Check if the source file exists
+               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+                       if ( $this->getParam( 'ignoreMissingSource' ) ) {
+                               $this->doOperation = false; // no-op
+                               // Update file existence predicates (cache 404s)
+                               $predicates['exists'][$this->params['src']] = false;
+                               $predicates['sha1'][$this->params['src']] = false;
+
+                               return $status; // nothing to do
+                       } else {
+                               $status->fatal( 'backend-fail-notexists', $this->params['src'] );
+
+                               return $status;
+                       }
+                       // Check if a file can be placed/changed at the destination
+               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
+                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
+                       $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
+
+                       return $status;
+               }
+               // Check if destination file exists
+               $status->merge( $this->precheckDestExistence( $predicates ) );
+               $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
+               if ( $status->isOK() ) {
+                       // Update file existence predicates
+                       $predicates['exists'][$this->params['dst']] = true;
+                       $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
+               }
+
+               return $status; // safe to call attempt()
+       }
+
+       protected function doAttempt() {
+               if ( $this->overwriteSameCase ) {
+                       $status = StatusValue::newGood(); // nothing to do
+               } elseif ( $this->params['src'] === $this->params['dst'] ) {
+                       // Just update the destination file headers
+                       $headers = $this->getParam( 'headers' ) ?: [];
+                       $status = $this->backend->describeInternal( $this->setFlags( [
+                               'src' => $this->params['dst'], 'headers' => $headers
+                       ] ) );
+               } else {
+                       // Copy the file to the destination
+                       $status = $this->backend->copyInternal( $this->setFlags( $this->params ) );
+               }
+
+               return $status;
+       }
+
+       public function storagePathsRead() {
+               return [ $this->params['src'] ];
+       }
+
+       public function storagePathsChanged() {
+               return [ $this->params['dst'] ];
+       }
+}
diff --git a/includes/libs/filebackend/fileop/CreateFileOp.php b/includes/libs/filebackend/fileop/CreateFileOp.php
new file mode 100644 (file)
index 0000000..120ca2b
--- /dev/null
@@ -0,0 +1,80 @@
+<?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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * Create a file in the backend with the given content.
+ * Parameters for this operation are outlined in FileBackend::doOperations().
+ */
+class CreateFileOp extends FileOp {
+       protected function allowedParams() {
+               return [
+                       [ 'content', 'dst' ],
+                       [ 'overwrite', 'overwriteSame', 'headers' ],
+                       [ 'dst' ]
+               ];
+       }
+
+       protected function doPrecheck( array &$predicates ) {
+               $status = StatusValue::newGood();
+               // Check if the source data is too big
+               if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
+                       $status->fatal( 'backend-fail-maxsize',
+                               $this->params['dst'], $this->backend->maxFileSizeInternal() );
+                       $status->fatal( 'backend-fail-create', $this->params['dst'] );
+
+                       return $status;
+                       // Check if a file can be placed/changed at the destination
+               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
+                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
+                       $status->fatal( 'backend-fail-create', $this->params['dst'] );
+
+                       return $status;
+               }
+               // Check if destination file exists
+               $status->merge( $this->precheckDestExistence( $predicates ) );
+               $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
+               if ( $status->isOK() ) {
+                       // Update file existence predicates
+                       $predicates['exists'][$this->params['dst']] = true;
+                       $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
+               }
+
+               return $status; // safe to call attempt()
+       }
+
+       protected function doAttempt() {
+               if ( !$this->overwriteSameCase ) {
+                       // Create the file at the destination
+                       return $this->backend->createInternal( $this->setFlags( $this->params ) );
+               }
+
+               return StatusValue::newGood();
+       }
+
+       protected function getSourceSha1Base36() {
+               return Wikimedia\base_convert( sha1( $this->params['content'] ), 16, 36, 31 );
+       }
+
+       public function storagePathsChanged() {
+               return [ $this->params['dst'] ];
+       }
+}
diff --git a/includes/libs/filebackend/fileop/DeleteFileOp.php b/includes/libs/filebackend/fileop/DeleteFileOp.php
new file mode 100644 (file)
index 0000000..0ccb1e3
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+/**
+* Helper class for representing operations with transaction support.
+*
+* 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 FileBackend
+* @author Aaron Schulz
+*/
+
+/**
+ * Delete a file at the given storage path from the backend.
+ * Parameters for this operation are outlined in FileBackend::doOperations().
+ */
+class DeleteFileOp extends FileOp {
+       protected function allowedParams() {
+               return [ [ 'src' ], [ 'ignoreMissingSource' ], [ 'src' ] ];
+       }
+
+       protected function doPrecheck( array &$predicates ) {
+               $status = StatusValue::newGood();
+               // Check if the source file exists
+               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+                       if ( $this->getParam( 'ignoreMissingSource' ) ) {
+                               $this->doOperation = false; // no-op
+                               // Update file existence predicates (cache 404s)
+                               $predicates['exists'][$this->params['src']] = false;
+                               $predicates['sha1'][$this->params['src']] = false;
+
+                               return $status; // nothing to do
+                       } else {
+                               $status->fatal( 'backend-fail-notexists', $this->params['src'] );
+
+                               return $status;
+                       }
+                       // Check if a file can be placed/changed at the source
+               } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
+                       $status->fatal( 'backend-fail-usable', $this->params['src'] );
+                       $status->fatal( 'backend-fail-delete', $this->params['src'] );
+
+                       return $status;
+               }
+               // Update file existence predicates
+               $predicates['exists'][$this->params['src']] = false;
+               $predicates['sha1'][$this->params['src']] = false;
+
+               return $status; // safe to call attempt()
+       }
+
+       protected function doAttempt() {
+               // Delete the source file
+               return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
+       }
+
+       public function storagePathsChanged() {
+               return [ $this->params['src'] ];
+       }
+}
diff --git a/includes/libs/filebackend/fileop/DescribeFileOp.php b/includes/libs/filebackend/fileop/DescribeFileOp.php
new file mode 100644 (file)
index 0000000..9b53222
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Helper class for representing operations with transaction support.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * Change metadata for a file at the given storage path in the backend.
+ * Parameters for this operation are outlined in FileBackend::doOperations().
+ */
+class DescribeFileOp extends FileOp {
+       protected function allowedParams() {
+               return [ [ 'src' ], [ 'headers' ], [ 'src' ] ];
+       }
+
+       protected function doPrecheck( array &$predicates ) {
+               $status = StatusValue::newGood();
+               // Check if the source file exists
+               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+                       $status->fatal( 'backend-fail-notexists', $this->params['src'] );
+
+                       return $status;
+                       // Check if a file can be placed/changed at the source
+               } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
+                       $status->fatal( 'backend-fail-usable', $this->params['src'] );
+                       $status->fatal( 'backend-fail-describe', $this->params['src'] );
+
+                       return $status;
+               }
+               // Update file existence predicates
+               $predicates['exists'][$this->params['src']] =
+                       $this->fileExists( $this->params['src'], $predicates );
+               $predicates['sha1'][$this->params['src']] =
+                       $this->fileSha1( $this->params['src'], $predicates );
+
+               return $status; // safe to call attempt()
+       }
+
+       protected function doAttempt() {
+               // Update the source file's metadata
+               return $this->backend->describeInternal( $this->setFlags( $this->params ) );
+       }
+
+       public function storagePathsChanged() {
+               return [ $this->params['src'] ];
+       }
+}
diff --git a/includes/libs/filebackend/fileop/FileOp.php b/includes/libs/filebackend/fileop/FileOp.php
new file mode 100644 (file)
index 0000000..fab5a37
--- /dev/null
@@ -0,0 +1,470 @@
+<?php
+/**
+ * Helper class for representing operations with transaction support.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+use Psr\Log\LoggerInterface;
+
+/**
+ * FileBackend helper class for representing operations.
+ * Do not use this class from places outside FileBackend.
+ *
+ * Methods called from FileOpBatch::attempt() should avoid throwing
+ * exceptions at all costs. FileOp objects should be lightweight in order
+ * to support large arrays in memory and serialization.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+abstract class FileOp {
+       /** @var array */
+       protected $params = [];
+
+       /** @var FileBackendStore */
+       protected $backend;
+       /** @var LoggerInterface */
+       protected $logger;
+
+       /** @var int */
+       protected $state = self::STATE_NEW;
+
+       /** @var bool */
+       protected $failed = false;
+
+       /** @var bool */
+       protected $async = false;
+
+       /** @var string */
+       protected $batchId;
+
+       /** @var bool Operation is not a no-op */
+       protected $doOperation = true;
+
+       /** @var string */
+       protected $sourceSha1;
+
+       /** @var bool */
+       protected $overwriteSameCase;
+
+       /** @var bool */
+       protected $destExists;
+
+       /* Object life-cycle */
+       const STATE_NEW = 1;
+       const STATE_CHECKED = 2;
+       const STATE_ATTEMPTED = 3;
+
+       /**
+        * Build a new batch file operation transaction
+        *
+        * @param FileBackendStore $backend
+        * @param array $params
+        * @param LoggerInterface $logger PSR logger instance
+        * @throws FileBackendError
+        */
+       final public function __construct(
+               FileBackendStore $backend, array $params, LoggerInterface $logger
+       ) {
+               $this->backend = $backend;
+               $this->logger = $logger;
+               list( $required, $optional, $paths ) = $this->allowedParams();
+               foreach ( $required as $name ) {
+                       if ( isset( $params[$name] ) ) {
+                               $this->params[$name] = $params[$name];
+                       } else {
+                               throw new InvalidArgumentException( "File operation missing parameter '$name'." );
+                       }
+               }
+               foreach ( $optional as $name ) {
+                       if ( isset( $params[$name] ) ) {
+                               $this->params[$name] = $params[$name];
+                       }
+               }
+               foreach ( $paths as $name ) {
+                       if ( isset( $this->params[$name] ) ) {
+                               // Normalize paths so the paths to the same file have the same string
+                               $this->params[$name] = self::normalizeIfValidStoragePath( $this->params[$name] );
+                       }
+               }
+       }
+
+       /**
+        * Normalize a string if it is a valid storage path
+        *
+        * @param string $path
+        * @return string
+        */
+       protected static function normalizeIfValidStoragePath( $path ) {
+               if ( FileBackend::isStoragePath( $path ) ) {
+                       $res = FileBackend::normalizeStoragePath( $path );
+
+                       return ( $res !== null ) ? $res : $path;
+               }
+
+               return $path;
+       }
+
+       /**
+        * Set the batch UUID this operation belongs to
+        *
+        * @param string $batchId
+        */
+       final public function setBatchId( $batchId ) {
+               $this->batchId = $batchId;
+       }
+
+       /**
+        * Get the value of the parameter with the given name
+        *
+        * @param string $name
+        * @return mixed Returns null if the parameter is not set
+        */
+       final public function getParam( $name ) {
+               return isset( $this->params[$name] ) ? $this->params[$name] : null;
+       }
+
+       /**
+        * Check if this operation failed precheck() or attempt()
+        *
+        * @return bool
+        */
+       final public function failed() {
+               return $this->failed;
+       }
+
+       /**
+        * Get a new empty predicates array for precheck()
+        *
+        * @return array
+        */
+       final public static function newPredicates() {
+               return [ 'exists' => [], 'sha1' => [] ];
+       }
+
+       /**
+        * Get a new empty dependency tracking array for paths read/written to
+        *
+        * @return array
+        */
+       final public static function newDependencies() {
+               return [ 'read' => [], 'write' => [] ];
+       }
+
+       /**
+        * Update a dependency tracking array to account for this operation
+        *
+        * @param array $deps Prior path reads/writes; format of FileOp::newPredicates()
+        * @return array
+        */
+       final public function applyDependencies( array $deps ) {
+               $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
+               $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
+
+               return $deps;
+       }
+
+       /**
+        * Check if this operation changes files listed in $paths
+        *
+        * @param array $deps Prior path reads/writes; format of FileOp::newPredicates()
+        * @return bool
+        */
+       final public function dependsOn( array $deps ) {
+               foreach ( $this->storagePathsChanged() as $path ) {
+                       if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
+                               return true; // "output" or "anti" dependency
+                       }
+               }
+               foreach ( $this->storagePathsRead() as $path ) {
+                       if ( isset( $deps['write'][$path] ) ) {
+                               return true; // "flow" dependency
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Get the file journal entries for this file operation
+        *
+        * @param array $oPredicates Pre-op info about files (format of FileOp::newPredicates)
+        * @param array $nPredicates Post-op info about files (format of FileOp::newPredicates)
+        * @return array
+        */
+       final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
+               if ( !$this->doOperation ) {
+                       return []; // this is a no-op
+               }
+               $nullEntries = [];
+               $updateEntries = [];
+               $deleteEntries = [];
+               $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
+               foreach ( array_unique( $pathsUsed ) as $path ) {
+                       $nullEntries[] = [ // assertion for recovery
+                               'op' => 'null',
+                               'path' => $path,
+                               'newSha1' => $this->fileSha1( $path, $oPredicates )
+                       ];
+               }
+               foreach ( $this->storagePathsChanged() as $path ) {
+                       if ( $nPredicates['sha1'][$path] === false ) { // deleted
+                               $deleteEntries[] = [
+                                       'op' => 'delete',
+                                       'path' => $path,
+                                       'newSha1' => ''
+                               ];
+                       } else { // created/updated
+                               $updateEntries[] = [
+                                       'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
+                                       'path' => $path,
+                                       'newSha1' => $nPredicates['sha1'][$path]
+                               ];
+                       }
+               }
+
+               return array_merge( $nullEntries, $updateEntries, $deleteEntries );
+       }
+
+       /**
+        * Check preconditions of the operation without writing anything.
+        * This must update $predicates for each path that the op can change
+        * except when a failing StatusValue object is returned.
+        *
+        * @param array $predicates
+        * @return StatusValue
+        */
+       final public function precheck( array &$predicates ) {
+               if ( $this->state !== self::STATE_NEW ) {
+                       return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
+               }
+               $this->state = self::STATE_CHECKED;
+               $status = $this->doPrecheck( $predicates );
+               if ( !$status->isOK() ) {
+                       $this->failed = true;
+               }
+
+               return $status;
+       }
+
+       /**
+        * @param array $predicates
+        * @return StatusValue
+        */
+       protected function doPrecheck( array &$predicates ) {
+               return StatusValue::newGood();
+       }
+
+       /**
+        * Attempt the operation
+        *
+        * @return StatusValue
+        */
+       final public function attempt() {
+               if ( $this->state !== self::STATE_CHECKED ) {
+                       return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
+               } elseif ( $this->failed ) { // failed precheck
+                       return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
+               }
+               $this->state = self::STATE_ATTEMPTED;
+               if ( $this->doOperation ) {
+                       $status = $this->doAttempt();
+                       if ( !$status->isOK() ) {
+                               $this->failed = true;
+                               $this->logFailure( 'attempt' );
+                       }
+               } else { // no-op
+                       $status = StatusValue::newGood();
+               }
+
+               return $status;
+       }
+
+       /**
+        * @return StatusValue
+        */
+       protected function doAttempt() {
+               return StatusValue::newGood();
+       }
+
+       /**
+        * Attempt the operation in the background
+        *
+        * @return StatusValue
+        */
+       final public function attemptAsync() {
+               $this->async = true;
+               $result = $this->attempt();
+               $this->async = false;
+
+               return $result;
+       }
+
+       /**
+        * Get the file operation parameters
+        *
+        * @return array (required params list, optional params list, list of params that are paths)
+        */
+       protected function allowedParams() {
+               return [ [], [], [] ];
+       }
+
+       /**
+        * Adjust params to FileBackendStore internal file calls
+        *
+        * @param array $params
+        * @return array (required params list, optional params list)
+        */
+       protected function setFlags( array $params ) {
+               return [ 'async' => $this->async ] + $params;
+       }
+
+       /**
+        * Get a list of storage paths read from for this operation
+        *
+        * @return array
+        */
+       public function storagePathsRead() {
+               return [];
+       }
+
+       /**
+        * Get a list of storage paths written to for this operation
+        *
+        * @return array
+        */
+       public function storagePathsChanged() {
+               return [];
+       }
+
+       /**
+        * Check for errors with regards to the destination file already existing.
+        * Also set the destExists, overwriteSameCase and sourceSha1 member variables.
+        * A bad StatusValue will be returned if there is no chance it can be overwritten.
+        *
+        * @param array $predicates
+        * @return StatusValue
+        */
+       protected function precheckDestExistence( array $predicates ) {
+               $status = StatusValue::newGood();
+               // Get hash of source file/string and the destination file
+               $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
+               if ( $this->sourceSha1 === null ) { // file in storage?
+                       $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
+               }
+               $this->overwriteSameCase = false;
+               $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
+               if ( $this->destExists ) {
+                       if ( $this->getParam( 'overwrite' ) ) {
+                               return $status; // OK
+                       } elseif ( $this->getParam( 'overwriteSame' ) ) {
+                               $dhash = $this->fileSha1( $this->params['dst'], $predicates );
+                               // Check if hashes are valid and match each other...
+                               if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
+                                       $status->fatal( 'backend-fail-hashes' );
+                               } elseif ( $this->sourceSha1 !== $dhash ) {
+                                       // Give an error if the files are not identical
+                                       $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
+                               } else {
+                                       $this->overwriteSameCase = true; // OK
+                               }
+
+                               return $status; // do nothing; either OK or bad status
+                       } else {
+                               $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
+
+                               return $status;
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * precheckDestExistence() helper function to get the source file SHA-1.
+        * Subclasses should overwride this if the source is not in storage.
+        *
+        * @return string|bool Returns false on failure
+        */
+       protected function getSourceSha1Base36() {
+               return null; // N/A
+       }
+
+       /**
+        * Check if a file will exist in storage when this operation is attempted
+        *
+        * @param string $source Storage path
+        * @param array $predicates
+        * @return bool
+        */
+       final protected function fileExists( $source, array $predicates ) {
+               if ( isset( $predicates['exists'][$source] ) ) {
+                       return $predicates['exists'][$source]; // previous op assures this
+               } else {
+                       $params = [ 'src' => $source, 'latest' => true ];
+
+                       return $this->backend->fileExists( $params );
+               }
+       }
+
+       /**
+        * Get the SHA-1 of a file in storage when this operation is attempted
+        *
+        * @param string $source Storage path
+        * @param array $predicates
+        * @return string|bool False on failure
+        */
+       final protected function fileSha1( $source, array $predicates ) {
+               if ( isset( $predicates['sha1'][$source] ) ) {
+                       return $predicates['sha1'][$source]; // previous op assures this
+               } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
+                       return false; // previous op assures this
+               } else {
+                       $params = [ 'src' => $source, 'latest' => true ];
+
+                       return $this->backend->getFileSha1Base36( $params );
+               }
+       }
+
+       /**
+        * Get the backend this operation is for
+        *
+        * @return FileBackendStore
+        */
+       public function getBackend() {
+               return $this->backend;
+       }
+
+       /**
+        * Log a file operation failure and preserve any temp files
+        *
+        * @param string $action
+        */
+       final public function logFailure( $action ) {
+               $params = $this->params;
+               $params['failedAction'] = $action;
+               try {
+                       $this->logger->error( get_class( $this ) .
+                               " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
+               } catch ( Exception $e ) {
+                       // bad config? debug log error?
+               }
+       }
+}
diff --git a/includes/libs/filebackend/fileop/MoveFileOp.php b/includes/libs/filebackend/fileop/MoveFileOp.php
new file mode 100644 (file)
index 0000000..fee3f4a
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+/**
+ * Helper class for representing operations with transaction support.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * Move a file from one storage path to another in the backend.
+ * Parameters for this operation are outlined in FileBackend::doOperations().
+ */
+class MoveFileOp extends FileOp {
+       protected function allowedParams() {
+               return [
+                       [ 'src', 'dst' ],
+                       [ 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'headers' ],
+                       [ 'src', 'dst' ]
+               ];
+       }
+
+       protected function doPrecheck( array &$predicates ) {
+               $status = StatusValue::newGood();
+               // Check if the source file exists
+               if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
+                       if ( $this->getParam( 'ignoreMissingSource' ) ) {
+                               $this->doOperation = false; // no-op
+                               // Update file existence predicates (cache 404s)
+                               $predicates['exists'][$this->params['src']] = false;
+                               $predicates['sha1'][$this->params['src']] = false;
+
+                               return $status; // nothing to do
+                       } else {
+                               $status->fatal( 'backend-fail-notexists', $this->params['src'] );
+
+                               return $status;
+                       }
+                       // Check if a file can be placed/changed at the destination
+               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
+                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
+                       $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
+
+                       return $status;
+               }
+               // Check if destination file exists
+               $status->merge( $this->precheckDestExistence( $predicates ) );
+               $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
+               if ( $status->isOK() ) {
+                       // Update file existence predicates
+                       $predicates['exists'][$this->params['src']] = false;
+                       $predicates['sha1'][$this->params['src']] = false;
+                       $predicates['exists'][$this->params['dst']] = true;
+                       $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
+               }
+
+               return $status; // safe to call attempt()
+       }
+
+       protected function doAttempt() {
+               if ( $this->overwriteSameCase ) {
+                       if ( $this->params['src'] === $this->params['dst'] ) {
+                               // Do nothing to the destination (which is also the source)
+                               $status = StatusValue::newGood();
+                       } else {
+                               // Just delete the source as the destination file needs no changes
+                               $status = $this->backend->deleteInternal( $this->setFlags(
+                                       [ 'src' => $this->params['src'] ]
+                               ) );
+                       }
+               } elseif ( $this->params['src'] === $this->params['dst'] ) {
+                       // Just update the destination file headers
+                       $headers = $this->getParam( 'headers' ) ?: [];
+                       $status = $this->backend->describeInternal( $this->setFlags(
+                               [ 'src' => $this->params['dst'], 'headers' => $headers ]
+                       ) );
+               } else {
+                       // Move the file to the destination
+                       $status = $this->backend->moveInternal( $this->setFlags( $this->params ) );
+               }
+
+               return $status;
+       }
+
+       public function storagePathsRead() {
+               return [ $this->params['src'] ];
+       }
+
+       public function storagePathsChanged() {
+               return [ $this->params['src'], $this->params['dst'] ];
+       }
+}
diff --git a/includes/libs/filebackend/fileop/NullFileOp.php b/includes/libs/filebackend/fileop/NullFileOp.php
new file mode 100644 (file)
index 0000000..ed23e81
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Helper class for representing operations with transaction support.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * Placeholder operation that has no params and does nothing
+ */
+class NullFileOp extends FileOp {
+}
diff --git a/includes/libs/filebackend/fileop/StoreFileOp.php b/includes/libs/filebackend/fileop/StoreFileOp.php
new file mode 100644 (file)
index 0000000..b97b410
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+/**
+ * Helper class for representing operations with transaction support.
+ *
+ * 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 FileBackend
+ * @author Aaron Schulz
+ */
+
+/**
+ * Store a file into the backend from a file on the file system.
+ * Parameters for this operation are outlined in FileBackend::doOperations().
+ */
+class StoreFileOp extends FileOp {
+       protected function allowedParams() {
+               return [
+                       [ 'src', 'dst' ],
+                       [ 'overwrite', 'overwriteSame', 'headers' ],
+                       [ 'src', 'dst' ]
+               ];
+       }
+
+       protected function doPrecheck( array &$predicates ) {
+               $status = StatusValue::newGood();
+               // Check if the source file exists on the file system
+               if ( !is_file( $this->params['src'] ) ) {
+                       $status->fatal( 'backend-fail-notexists', $this->params['src'] );
+
+                       return $status;
+                       // Check if the source file is too big
+               } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
+                       $status->fatal( 'backend-fail-maxsize',
+                               $this->params['dst'], $this->backend->maxFileSizeInternal() );
+                       $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
+
+                       return $status;
+                       // Check if a file can be placed/changed at the destination
+               } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
+                       $status->fatal( 'backend-fail-usable', $this->params['dst'] );
+                       $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
+
+                       return $status;
+               }
+               // Check if destination file exists
+               $status->merge( $this->precheckDestExistence( $predicates ) );
+               $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
+               if ( $status->isOK() ) {
+                       // Update file existence predicates
+                       $predicates['exists'][$this->params['dst']] = true;
+                       $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
+               }
+
+               return $status; // safe to call attempt()
+       }
+
+       protected function doAttempt() {
+               if ( !$this->overwriteSameCase ) {
+                       // Store the file at the destination
+                       return $this->backend->storeInternal( $this->setFlags( $this->params ) );
+               }
+
+               return StatusValue::newGood();
+       }
+
+       protected function getSourceSha1Base36() {
+               MediaWiki\suppressWarnings();
+               $hash = sha1_file( $this->params['src'] );
+               MediaWiki\restoreWarnings();
+               if ( $hash !== false ) {
+                       $hash = Wikimedia\base_convert( $hash, 16, 36, 31 );
+               }
+
+               return $hash;
+       }
+
+       public function storagePathsChanged() {
+               return [ $this->params['dst'] ];
+       }
+}
diff --git a/includes/libs/lockmanager/DBLockManager.php b/includes/libs/lockmanager/DBLockManager.php
new file mode 100644 (file)
index 0000000..b058146
--- /dev/null
@@ -0,0 +1,227 @@
+<?php
+/**
+ * Version of LockManager based on using DB table locks.
+ *
+ * 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 LockManager
+ */
+
+/**
+ * Version of LockManager based on using named/row DB locks.
+ *
+ * This is meant for multi-wiki systems that may share files.
+ *
+ * All lock requests for a resource, identified by a hash string, will map to one bucket.
+ * Each bucket maps to one or several peer DBs, each on their own server.
+ * A majority of peer DBs must agree for a lock to be acquired.
+ *
+ * Caching is used to avoid hitting servers that are down.
+ *
+ * @ingroup LockManager
+ * @since 1.19
+ */
+abstract class DBLockManager extends QuorumLockManager {
+       /** @var array[]|IDatabase[] Map of (DB names => server config or IDatabase) */
+       protected $dbServers; // (DB name => server config array)
+       /** @var BagOStuff */
+       protected $statusCache;
+
+       protected $lockExpiry; // integer number of seconds
+       protected $safeDelay; // integer number of seconds
+       /** @var IDatabase[] Map Database connections (DB name => Database) */
+       protected $conns = [];
+
+       /**
+        * Construct a new instance from configuration.
+        *
+        * @param array $config Parameters include:
+        *   - dbServers   : Associative array of DB names to server configuration.
+        *                   Configuration is an associative array that includes:
+        *                     - host        : DB server name
+        *                     - dbname      : DB name
+        *                     - type        : DB type (mysql,postgres,...)
+        *                     - user        : DB user
+        *                     - password    : DB user password
+        *                     - tablePrefix : DB table prefix
+        *                     - flags       : DB flags (see DatabaseBase)
+        *   - dbsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
+        *                   each having an odd-numbered list of DB names (peers) as values.
+        *   - lockExpiry  : Lock timeout (seconds) for dropped connections. [optional]
+        *                   This tells the DB server how long to wait before assuming
+        *                   connection failure and releasing all the locks for a session.
+        *   - srvCache    : A BagOStuff instance using APC or the like.
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+
+               $this->dbServers = $config['dbServers'];
+               // Sanitize srvsByBucket config to prevent PHP errors
+               $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' );
+               $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
+
+               if ( isset( $config['lockExpiry'] ) ) {
+                       $this->lockExpiry = $config['lockExpiry'];
+               } else {
+                       $met = ini_get( 'max_execution_time' );
+                       $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0
+               }
+               $this->safeDelay = ( $this->lockExpiry <= 0 )
+                       ? 60 // pick a safe-ish number to match DB timeout default
+                       : $this->lockExpiry; // cover worst case
+
+               // Tracks peers that couldn't be queried recently to avoid lengthy
+               // connection timeouts. This is useless if each bucket has one peer.
+               $this->statusCache = isset( $config['srvCache'] )
+                       ? $config['srvCache']
+                       : new HashBagOStuff();
+       }
+
+       /**
+        * @TODO change this code to work in one batch
+        * @param string $lockSrv
+        * @param array $pathsByType
+        * @return StatusValue
+        */
+       protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
+               $status = StatusValue::newGood();
+               foreach ( $pathsByType as $type => $paths ) {
+                       $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
+               }
+
+               return $status;
+       }
+
+       abstract protected function doGetLocksOnServer( $lockSrv, array $paths, $type );
+
+       protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
+               return StatusValue::newGood();
+       }
+
+       /**
+        * @see QuorumLockManager::isServerUp()
+        * @param string $lockSrv
+        * @return bool
+        */
+       protected function isServerUp( $lockSrv ) {
+               if ( !$this->cacheCheckFailures( $lockSrv ) ) {
+                       return false; // recent failure to connect
+               }
+               try {
+                       $this->getConnection( $lockSrv );
+               } catch ( DBError $e ) {
+                       $this->cacheRecordFailure( $lockSrv );
+
+                       return false; // failed to connect
+               }
+
+               return true;
+       }
+
+       /**
+        * Get (or reuse) a connection to a lock DB
+        *
+        * @param string $lockDb
+        * @return IDatabase
+        * @throws DBError
+        * @throws UnexpectedValueException
+        */
+       protected function getConnection( $lockDb ) {
+               if ( !isset( $this->conns[$lockDb] ) ) {
+                       if ( $this->dbServers[$lockDb] instanceof IDatabase ) {
+                               // Direct injected connection hande for $lockDB
+                               $db = $this->dbServers[$lockDb];
+                       } elseif ( is_array( $this->dbServers[$lockDb] ) ) {
+                               // Parameters to construct a new database connection
+                               $config = $this->dbServers[$lockDb];
+                               $db = DatabaseBase::factory( $config['type'], $config );
+                       } else {
+                               throw new UnexpectedValueException( "No server called '$lockDb'." );
+                       }
+
+                       $db->clearFlag( DBO_TRX );
+                       # If the connection drops, try to avoid letting the DB rollback
+                       # and release the locks before the file operations are finished.
+                       # This won't handle the case of DB server restarts however.
+                       $options = [];
+                       if ( $this->lockExpiry > 0 ) {
+                               $options['connTimeout'] = $this->lockExpiry;
+                       }
+                       $db->setSessionOptions( $options );
+                       $this->initConnection( $lockDb, $db );
+
+                       $this->conns[$lockDb] = $db;
+               }
+
+               return $this->conns[$lockDb];
+       }
+
+       /**
+        * Do additional initialization for new lock DB connection
+        *
+        * @param string $lockDb
+        * @param IDatabase $db
+        * @throws DBError
+        */
+       protected function initConnection( $lockDb, IDatabase $db ) {
+       }
+
+       /**
+        * Checks if the DB has not recently had connection/query errors.
+        * This just avoids wasting time on doomed connection attempts.
+        *
+        * @param string $lockDb
+        * @return bool
+        */
+       protected function cacheCheckFailures( $lockDb ) {
+               return ( $this->safeDelay > 0 )
+                       ? !$this->statusCache->get( $this->getMissKey( $lockDb ) )
+                       : true;
+       }
+
+       /**
+        * Log a lock request failure to the cache
+        *
+        * @param string $lockDb
+        * @return bool Success
+        */
+       protected function cacheRecordFailure( $lockDb ) {
+               return ( $this->safeDelay > 0 )
+                       ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay )
+                       : true;
+       }
+
+       /**
+        * Get a cache key for recent query misses for a DB
+        *
+        * @param string $lockDb
+        * @return string
+        */
+       protected function getMissKey( $lockDb ) {
+               return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb );
+       }
+
+       /**
+        * Make sure remaining locks get cleared for sanity
+        */
+       function __destruct() {
+               $this->releaseAllLocks();
+               foreach ( $this->conns as $db ) {
+                       $db->close();
+               }
+       }
+}
index 80add5b..42391a0 100644 (file)
@@ -3,6 +3,7 @@
  * @defgroup LockManager Lock management
  * @ingroup FileBackend
  */
+use Psr\Log\LoggerInterface;
 
 /**
  * Resource locking handling.
@@ -43,6 +44,9 @@
  * @since 1.19
  */
 abstract class LockManager {
+       /** @var LoggerInterface */
+       protected $logger;
+
        /** @var array Mapping of lock types to the type actually used */
        protected $lockTypeMap = [
                self::LOCK_SH => self::LOCK_SH,
@@ -56,6 +60,9 @@ abstract class LockManager {
        protected $domain; // string; domain (usually wiki ID)
        protected $lockTTL; // integer; maximum time locks can be held
 
+       /** @var string Random 32-char hex number */
+       protected $session;
+
        /** Lock types; stronger locks have higher values */
        const LOCK_SH = 1; // shared lock (for reads)
        const LOCK_UW = 2; // shared lock (for reads used to write elsewhere)
@@ -79,6 +86,14 @@ abstract class LockManager {
                        $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
                        $this->lockTTL = max( 5 * 60, 2 * (int)$met );
                }
+
+               $random = [];
+               for ( $i = 1; $i <= 5; ++$i ) {
+                       $random[] = mt_rand( 0, 0xFFFFFFF );
+               }
+               $this->session = md5( implode( '-', $random ) );
+
+               $this->logger = isset( $config['logger'] ) ? $config['logger'] : new \Psr\Log\NullLogger();
        }
 
        /**
diff --git a/includes/libs/lockmanager/PostgreSqlLockManager.php b/includes/libs/lockmanager/PostgreSqlLockManager.php
new file mode 100644 (file)
index 0000000..d6b1ce8
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+/**
+ * PostgreSQL version of DBLockManager that supports shared locks.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * @ingroup LockManager
+ */
+class PostgreSqlLockManager extends DBLockManager {
+       /** @var array Mapping of lock types to the type actually used */
+       protected $lockTypeMap = [
+               self::LOCK_SH => self::LOCK_SH,
+               self::LOCK_UW => self::LOCK_SH,
+               self::LOCK_EX => self::LOCK_EX
+       ];
+
+       protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
+               $status = StatusValue::newGood();
+               if ( !count( $paths ) ) {
+                       return $status; // nothing to lock
+               }
+
+               $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
+               $bigints = array_unique( array_map(
+                       function ( $key ) {
+                               return Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 );
+                       },
+                       array_map( [ $this, 'sha1Base16Absolute' ], $paths )
+               ) );
+
+               // Try to acquire all the locks...
+               $fields = [];
+               foreach ( $bigints as $bigint ) {
+                       $fields[] = ( $type == self::LOCK_SH )
+                               ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
+                               : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
+               }
+               $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
+               $row = $res->fetchRow();
+
+               if ( in_array( 'f', $row ) ) {
+                       // Release any acquired locks if some could not be acquired...
+                       $fields = [];
+                       foreach ( $row as $kbigint => $ok ) {
+                               if ( $ok === 't' ) { // locked
+                                       $bigint = substr( $kbigint, 1 ); // strip off the "K"
+                                       $fields[] = ( $type == self::LOCK_SH )
+                                               ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
+                                               : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
+                               }
+                       }
+                       if ( count( $fields ) ) {
+                               $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
+                       }
+                       foreach ( $paths as $path ) {
+                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see QuorumLockManager::releaseAllLocks()
+        * @return StatusValue
+        */
+       protected function releaseAllLocks() {
+               $status = StatusValue::newGood();
+
+               foreach ( $this->conns as $lockDb => $db ) {
+                       try {
+                               $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
+                       } catch ( DBError $e ) {
+                               $status->fatal( 'lockmanager-fail-db-release', $lockDb );
+                       }
+               }
+
+               return $status;
+       }
+}
diff --git a/includes/libs/lockmanager/RedisLockManager.php b/includes/libs/lockmanager/RedisLockManager.php
new file mode 100644 (file)
index 0000000..6001705
--- /dev/null
@@ -0,0 +1,275 @@
+<?php
+/**
+ * Version of LockManager based on using redis servers.
+ *
+ * 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 LockManager
+ */
+
+/**
+ * Manage locks using redis servers.
+ *
+ * Version of LockManager based on using redis servers.
+ * This is meant for multi-wiki systems that may share files.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * All lock requests for a resource, identified by a hash string, will map to one
+ * bucket. Each bucket maps to one or several peer servers, each running redis.
+ * A majority of peers must agree for a lock to be acquired.
+ *
+ * This class requires Redis 2.6 as it makes use Lua scripts for fast atomic operations.
+ *
+ * @ingroup LockManager
+ * @since 1.22
+ */
+class RedisLockManager extends QuorumLockManager {
+       /** @var array Mapping of lock types to the type actually used */
+       protected $lockTypeMap = [
+               self::LOCK_SH => self::LOCK_SH,
+               self::LOCK_UW => self::LOCK_SH,
+               self::LOCK_EX => self::LOCK_EX
+       ];
+
+       /** @var RedisConnectionPool */
+       protected $redisPool;
+
+       /** @var array Map server names to hostname/IP and port numbers */
+       protected $lockServers = [];
+
+       /**
+        * Construct a new instance from configuration.
+        *
+        * @param array $config Parameters include:
+        *   - lockServers  : Associative array of server names to "<IP>:<port>" strings.
+        *   - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
+        *                    each having an odd-numbered list of server names (peers) as values.
+        *   - redisConfig  : Configuration for RedisConnectionPool::__construct().
+        * @throws Exception
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+
+               $this->lockServers = $config['lockServers'];
+               // Sanitize srvsByBucket config to prevent PHP errors
+               $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
+               $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
+
+               $config['redisConfig']['serializer'] = 'none';
+               $this->redisPool = RedisConnectionPool::singleton( $config['redisConfig'] );
+       }
+
+       protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
+               $status = StatusValue::newGood();
+
+               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+
+               $server = $this->lockServers[$lockSrv];
+               $conn = $this->redisPool->getConnection( $server, $this->logger );
+               if ( !$conn ) {
+                       foreach ( $pathList as $path ) {
+                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                       }
+
+                       return $status;
+               }
+
+               $pathsByKey = []; // (type:hash => path) map
+               foreach ( $pathsByType as $type => $paths ) {
+                       $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
+                       foreach ( $paths as $path ) {
+                               $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
+                       }
+               }
+
+               try {
+                       static $script =
+                       /** @lang Lua */
+<<<LUA
+                       local failed = {}
+                       -- Load input params (e.g. session, ttl, time of request)
+                       local rSession, rTTL, rTime = unpack(ARGV)
+                       -- Check that all the locks can be acquired
+                       for i,requestKey in ipairs(KEYS) do
+                               local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
+                               local keyIsFree = true
+                               local currentLocks = redis.call('hKeys',resourceKey)
+                               for i,lockKey in ipairs(currentLocks) do
+                                       -- Get the type and session of this lock
+                                       local _, _, type, session = string.find(lockKey,"(%w+):(%w+)")
+                                       -- Check any locks that are not owned by this session
+                                       if session ~= rSession then
+                                               local lockExpiry = redis.call('hGet',resourceKey,lockKey)
+                                               if 1*lockExpiry < 1*rTime then
+                                                       -- Lock is stale, so just prune it out
+                                                       redis.call('hDel',resourceKey,lockKey)
+                                               elseif rType == 'EX' or type == 'EX' then
+                                                       keyIsFree = false
+                                                       break
+                                               end
+                                       end
+                               end
+                               if not keyIsFree then
+                                       failed[#failed+1] = requestKey
+                               end
+                       end
+                       -- If all locks could be acquired, then do so
+                       if #failed == 0 then
+                               for i,requestKey in ipairs(KEYS) do
+                                       local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
+                                       redis.call('hSet',resourceKey,rType .. ':' .. rSession,rTime + rTTL)
+                                       -- In addition to invalidation logic, be sure to garbage collect
+                                       redis.call('expire',resourceKey,rTTL)
+                               end
+                       end
+                       return failed
+LUA;
+                       $res = $conn->luaEval( $script,
+                               array_merge(
+                                       array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
+                                       [
+                                               $this->session, // ARGV[1]
+                                               $this->lockTTL, // ARGV[2]
+                                               time() // ARGV[3]
+                                       ]
+                               ),
+                               count( $pathsByKey ) # number of first argument(s) that are keys
+                       );
+               } catch ( RedisException $e ) {
+                       $res = false;
+                       $this->redisPool->handleError( $conn, $e );
+               }
+
+               if ( $res === false ) {
+                       foreach ( $pathList as $path ) {
+                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                       }
+               } else {
+                       foreach ( $res as $key ) {
+                               $status->fatal( 'lockmanager-fail-acquirelock', $pathsByKey[$key] );
+                       }
+               }
+
+               return $status;
+       }
+
+       protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
+               $status = StatusValue::newGood();
+
+               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+
+               $server = $this->lockServers[$lockSrv];
+               $conn = $this->redisPool->getConnection( $server, $this->logger );
+               if ( !$conn ) {
+                       foreach ( $pathList as $path ) {
+                               $status->fatal( 'lockmanager-fail-releaselock', $path );
+                       }
+
+                       return $status;
+               }
+
+               $pathsByKey = []; // (type:hash => path) map
+               foreach ( $pathsByType as $type => $paths ) {
+                       $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
+                       foreach ( $paths as $path ) {
+                               $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
+                       }
+               }
+
+               try {
+                       static $script =
+                       /** @lang Lua */
+<<<LUA
+                       local failed = {}
+                       -- Load input params (e.g. session)
+                       local rSession = unpack(ARGV)
+                       for i,requestKey in ipairs(KEYS) do
+                               local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
+                               local released = redis.call('hDel',resourceKey,rType .. ':' .. rSession)
+                               if released > 0 then
+                                       -- Remove the whole structure if it is now empty
+                                       if redis.call('hLen',resourceKey) == 0 then
+                                               redis.call('del',resourceKey)
+                                       end
+                               else
+                                       failed[#failed+1] = requestKey
+                               end
+                       end
+                       return failed
+LUA;
+                       $res = $conn->luaEval( $script,
+                               array_merge(
+                                       array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
+                                       [
+                                               $this->session, // ARGV[1]
+                                       ]
+                               ),
+                               count( $pathsByKey ) # number of first argument(s) that are keys
+                       );
+               } catch ( RedisException $e ) {
+                       $res = false;
+                       $this->redisPool->handleError( $conn, $e );
+               }
+
+               if ( $res === false ) {
+                       foreach ( $pathList as $path ) {
+                               $status->fatal( 'lockmanager-fail-releaselock', $path );
+                       }
+               } else {
+                       foreach ( $res as $key ) {
+                               $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] );
+                       }
+               }
+
+               return $status;
+       }
+
+       protected function releaseAllLocks() {
+               return StatusValue::newGood(); // not supported
+       }
+
+       protected function isServerUp( $lockSrv ) {
+               $conn = $this->redisPool->getConnection( $this->lockServers[$lockSrv], $this->logger );
+
+               return (bool)$conn;
+       }
+
+       /**
+        * @param string $path
+        * @param string $type One of (EX,SH)
+        * @return string
+        */
+       protected function recordKeyForPath( $path, $type ) {
+               return implode( ':',
+                       [ __CLASS__, 'locks', "$type:" . $this->sha1Base36Absolute( $path ) ] );
+       }
+
+       /**
+        * Make sure remaining locks get cleared for sanity
+        */
+       function __destruct() {
+               while ( count( $this->locksHeld ) ) {
+                       $pathsByType = [];
+                       foreach ( $this->locksHeld as $path => $locks ) {
+                               foreach ( $locks as $type => $count ) {
+                                       $pathsByType[$type][] = $path;
+                               }
+                       }
+                       $this->unlockByType( $pathsByType );
+               }
+       }
+}
diff --git a/includes/libs/objectcache/RedisBagOStuff.php b/includes/libs/objectcache/RedisBagOStuff.php
new file mode 100644 (file)
index 0000000..d852f82
--- /dev/null
@@ -0,0 +1,433 @@
+<?php
+/**
+ * Object caching using Redis (http://redis.io/).
+ *
+ * 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
+ */
+
+/**
+ * Redis-based caching module for redis server >= 2.6.12
+ *
+ * @note: avoid use of Redis::MULTI transactions for twemproxy support
+ */
+class RedisBagOStuff extends BagOStuff {
+       /** @var RedisConnectionPool */
+       protected $redisPool;
+       /** @var array List of server names */
+       protected $servers;
+       /** @var array Map of (tag => server name) */
+       protected $serverTagMap;
+       /** @var bool */
+       protected $automaticFailover;
+
+       /**
+        * Construct a RedisBagOStuff object. Parameters are:
+        *
+        *   - servers: An array of server names. A server name may be a hostname,
+        *     a hostname/port combination or the absolute path of a UNIX socket.
+        *     If a hostname is specified but no port, the standard port number
+        *     6379 will be used. Arrays keys can be used to specify the tag to
+        *     hash on in place of the host/port. Required.
+        *
+        *   - connectTimeout: The timeout for new connections, in seconds. Optional,
+        *     default is 1 second.
+        *
+        *   - persistent: Set this to true to allow connections to persist across
+        *     multiple web requests. False by default.
+        *
+        *   - password: The authentication password, will be sent to Redis in
+        *     clear text. Optional, if it is unspecified, no AUTH command will be
+        *     sent.
+        *
+        *   - automaticFailover: If this is false, then each key will be mapped to
+        *     a single server, and if that server is down, any requests for that key
+        *     will fail. If this is true, a connection failure will cause the client
+        *     to immediately try the next server in the list (as determined by a
+        *     consistent hashing algorithm). True by default. This has the
+        *     potential to create consistency issues if a server is slow enough to
+        *     flap, for example if it is in swap death.
+        * @param array $params
+        */
+       function __construct( $params ) {
+               parent::__construct( $params );
+               $redisConf = [ 'serializer' => 'none' ]; // manage that in this class
+               foreach ( [ 'connectTimeout', 'persistent', 'password' ] as $opt ) {
+                       if ( isset( $params[$opt] ) ) {
+                               $redisConf[$opt] = $params[$opt];
+                       }
+               }
+               $this->redisPool = RedisConnectionPool::singleton( $redisConf );
+
+               $this->servers = $params['servers'];
+               foreach ( $this->servers as $key => $server ) {
+                       $this->serverTagMap[is_int( $key ) ? $server : $key] = $server;
+               }
+
+               if ( isset( $params['automaticFailover'] ) ) {
+                       $this->automaticFailover = $params['automaticFailover'];
+               } else {
+                       $this->automaticFailover = true;
+               }
+
+               $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_NONE;
+       }
+
+       protected function doGet( $key, $flags = 0 ) {
+               list( $server, $conn ) = $this->getConnection( $key );
+               if ( !$conn ) {
+                       return false;
+               }
+               try {
+                       $value = $conn->get( $key );
+                       $result = $this->unserialize( $value );
+               } catch ( RedisException $e ) {
+                       $result = false;
+                       $this->handleException( $conn, $e );
+               }
+
+               $this->logRequest( 'get', $key, $server, $result );
+               return $result;
+       }
+
+       public function set( $key, $value, $expiry = 0, $flags = 0 ) {
+               list( $server, $conn ) = $this->getConnection( $key );
+               if ( !$conn ) {
+                       return false;
+               }
+               $expiry = $this->convertToRelative( $expiry );
+               try {
+                       if ( $expiry ) {
+                               $result = $conn->setex( $key, $expiry, $this->serialize( $value ) );
+                       } else {
+                               // No expiry, that is very different from zero expiry in Redis
+                               $result = $conn->set( $key, $this->serialize( $value ) );
+                       }
+               } catch ( RedisException $e ) {
+                       $result = false;
+                       $this->handleException( $conn, $e );
+               }
+
+               $this->logRequest( 'set', $key, $server, $result );
+               return $result;
+       }
+
+       public function delete( $key ) {
+               list( $server, $conn ) = $this->getConnection( $key );
+               if ( !$conn ) {
+                       return false;
+               }
+               try {
+                       $conn->delete( $key );
+                       // Return true even if the key didn't exist
+                       $result = true;
+               } catch ( RedisException $e ) {
+                       $result = false;
+                       $this->handleException( $conn, $e );
+               }
+
+               $this->logRequest( 'delete', $key, $server, $result );
+               return $result;
+       }
+
+       public function getMulti( array $keys, $flags = 0 ) {
+               $batches = [];
+               $conns = [];
+               foreach ( $keys as $key ) {
+                       list( $server, $conn ) = $this->getConnection( $key );
+                       if ( !$conn ) {
+                               continue;
+                       }
+                       $conns[$server] = $conn;
+                       $batches[$server][] = $key;
+               }
+               $result = [];
+               foreach ( $batches as $server => $batchKeys ) {
+                       $conn = $conns[$server];
+                       try {
+                               $conn->multi( Redis::PIPELINE );
+                               foreach ( $batchKeys as $key ) {
+                                       $conn->get( $key );
+                               }
+                               $batchResult = $conn->exec();
+                               if ( $batchResult === false ) {
+                                       $this->debug( "multi request to $server failed" );
+                                       continue;
+                               }
+                               foreach ( $batchResult as $i => $value ) {
+                                       if ( $value !== false ) {
+                                               $result[$batchKeys[$i]] = $this->unserialize( $value );
+                                       }
+                               }
+                       } catch ( RedisException $e ) {
+                               $this->handleException( $conn, $e );
+                       }
+               }
+
+               $this->debug( "getMulti for " . count( $keys ) . " keys " .
+                       "returned " . count( $result ) . " results" );
+               return $result;
+       }
+
+       /**
+        * @param array $data
+        * @param int $expiry
+        * @return bool
+        */
+       public function setMulti( array $data, $expiry = 0 ) {
+               $batches = [];
+               $conns = [];
+               foreach ( $data as $key => $value ) {
+                       list( $server, $conn ) = $this->getConnection( $key );
+                       if ( !$conn ) {
+                               continue;
+                       }
+                       $conns[$server] = $conn;
+                       $batches[$server][] = $key;
+               }
+
+               $expiry = $this->convertToRelative( $expiry );
+               $result = true;
+               foreach ( $batches as $server => $batchKeys ) {
+                       $conn = $conns[$server];
+                       try {
+                               $conn->multi( Redis::PIPELINE );
+                               foreach ( $batchKeys as $key ) {
+                                       if ( $expiry ) {
+                                               $conn->setex( $key, $expiry, $this->serialize( $data[$key] ) );
+                                       } else {
+                                               $conn->set( $key, $this->serialize( $data[$key] ) );
+                                       }
+                               }
+                               $batchResult = $conn->exec();
+                               if ( $batchResult === false ) {
+                                       $this->debug( "setMulti request to $server failed" );
+                                       continue;
+                               }
+                               foreach ( $batchResult as $value ) {
+                                       if ( $value === false ) {
+                                               $result = false;
+                                       }
+                               }
+                       } catch ( RedisException $e ) {
+                               $this->handleException( $server, $conn, $e );
+                               $result = false;
+                       }
+               }
+
+               return $result;
+       }
+
+       public function add( $key, $value, $expiry = 0 ) {
+               list( $server, $conn ) = $this->getConnection( $key );
+               if ( !$conn ) {
+                       return false;
+               }
+               $expiry = $this->convertToRelative( $expiry );
+               try {
+                       if ( $expiry ) {
+                               $result = $conn->set(
+                                       $key,
+                                       $this->serialize( $value ),
+                                       [ 'nx', 'ex' => $expiry ]
+                               );
+                       } else {
+                               $result = $conn->setnx( $key, $this->serialize( $value ) );
+                       }
+               } catch ( RedisException $e ) {
+                       $result = false;
+                       $this->handleException( $conn, $e );
+               }
+
+               $this->logRequest( 'add', $key, $server, $result );
+               return $result;
+       }
+
+       /**
+        * Non-atomic implementation of incr().
+        *
+        * Probably all callers actually want incr() to atomically initialise
+        * values to zero if they don't exist, as provided by the Redis INCR
+        * command. But we are constrained by the memcached-like interface to
+        * return null in that case. Once the key exists, further increments are
+        * atomic.
+        * @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 ) {
+               list( $server, $conn ) = $this->getConnection( $key );
+               if ( !$conn ) {
+                       return false;
+               }
+               try {
+                       if ( !$conn->exists( $key ) ) {
+                               return null;
+                       }
+                       // @FIXME: on races, the key may have a 0 TTL
+                       $result = $conn->incrBy( $key, $value );
+               } catch ( RedisException $e ) {
+                       $result = false;
+                       $this->handleException( $conn, $e );
+               }
+
+               $this->logRequest( 'incr', $key, $server, $result );
+               return $result;
+       }
+
+       public function changeTTL( $key, $expiry = 0 ) {
+               list( $server, $conn ) = $this->getConnection( $key );
+               if ( !$conn ) {
+                       return false;
+               }
+
+               $expiry = $this->convertToRelative( $expiry );
+               try {
+                       $result = $conn->expire( $key, $expiry );
+               } catch ( RedisException $e ) {
+                       $result = false;
+                       $this->handleException( $conn, $e );
+               }
+
+               $this->logRequest( 'expire', $key, $server, $result );
+               return $result;
+       }
+
+       public function modifySimpleRelayEvent( array $event ) {
+               if ( array_key_exists( 'val', $event ) ) {
+                       $event['val'] = serialize( $event['val'] ); // this class uses PHP serialization
+               }
+
+               return $event;
+       }
+
+       /**
+        * @param mixed $data
+        * @return string
+        */
+       protected function serialize( $data ) {
+               // Serialize anything but integers so INCR/DECR work
+               // Do not store integer-like strings as integers to avoid type confusion (bug 60563)
+               return is_int( $data ) ? $data : serialize( $data );
+       }
+
+       /**
+        * @param string $data
+        * @return mixed
+        */
+       protected function unserialize( $data ) {
+               $int = intval( $data );
+               return $data === (string)$int ? $int : unserialize( $data );
+       }
+
+       /**
+        * Get a Redis object with a connection suitable for fetching the specified key
+        * @param string $key
+        * @return array (server, RedisConnRef) or (false, false)
+        */
+       protected function getConnection( $key ) {
+               $candidates = array_keys( $this->serverTagMap );
+
+               if ( count( $this->servers ) > 1 ) {
+                       ArrayUtils::consistentHashSort( $candidates, $key, '/' );
+                       if ( !$this->automaticFailover ) {
+                               $candidates = array_slice( $candidates, 0, 1 );
+                       }
+               }
+
+               while ( ( $tag = array_shift( $candidates ) ) !== null ) {
+                       $server = $this->serverTagMap[$tag];
+                       $conn = $this->redisPool->getConnection( $server, $this->logger );
+                       if ( !$conn ) {
+                               continue;
+                       }
+
+                       // If automatic failover is enabled, check that the server's link
+                       // to its master (if any) is up -- but only if there are other
+                       // viable candidates left to consider. Also, getMasterLinkStatus()
+                       // does not work with twemproxy, though $candidates will be empty
+                       // by now in such cases.
+                       if ( $this->automaticFailover && $candidates ) {
+                               try {
+                                       if ( $this->getMasterLinkStatus( $conn ) === 'down' ) {
+                                               // If the master cannot be reached, fail-over to the next server.
+                                               // If masters are in data-center A, and replica DBs in data-center B,
+                                               // this helps avoid the case were fail-over happens in A but not
+                                               // to the corresponding server in B (e.g. read/write mismatch).
+                                               continue;
+                                       }
+                               } catch ( RedisException $e ) {
+                                       // Server is not accepting commands
+                                       $this->handleException( $conn, $e );
+                                       continue;
+                               }
+                       }
+
+                       return [ $server, $conn ];
+               }
+
+               $this->setLastError( BagOStuff::ERR_UNREACHABLE );
+
+               return [ false, false ];
+       }
+
+       /**
+        * Check the master link status of a Redis server that is configured as a replica DB.
+        * @param RedisConnRef $conn
+        * @return string|null Master link status (either 'up' or 'down'), or null
+        *  if the server is not a replica DB.
+        */
+       protected function getMasterLinkStatus( RedisConnRef $conn ) {
+               $info = $conn->info();
+               return isset( $info['master_link_status'] )
+                       ? $info['master_link_status']
+                       : null;
+       }
+
+       /**
+        * Log a fatal error
+        * @param string $msg
+        */
+       protected function logError( $msg ) {
+               $this->logger->error( "Redis error: $msg" );
+       }
+
+       /**
+        * The redis extension throws an exception in response to various read, write
+        * and protocol errors. Sometimes it also closes the connection, sometimes
+        * not. The safest response for us is to explicitly destroy the connection
+        * object and let it be reopened during the next request.
+        * @param RedisConnRef $conn
+        * @param Exception $e
+        */
+       protected function handleException( RedisConnRef $conn, $e ) {
+               $this->setLastError( BagOStuff::ERR_UNEXPECTED );
+               $this->redisPool->handleError( $conn, $e );
+       }
+
+       /**
+        * Send information about a single request to the debug log
+        * @param string $method
+        * @param string $key
+        * @param string $server
+        * @param bool $result
+        */
+       public function logRequest( $method, $key, $server, $result ) {
+               $this->debug( "$method $key on $server: " .
+                       ( $result === false ? "failure" : "success" ) );
+       }
+}
index 1cb8906..20198bf 100644 (file)
@@ -445,7 +445,7 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function getSlavePos() {
+       public function getReplicaPos() {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
index 66f7046..897e55f 100644 (file)
@@ -27,10 +27,12 @@ use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
 
 /**
- * Database abstraction object
+ * Relational database abstraction object
+ *
  * @ingroup Database
+ * @since 1.28
  */
-abstract class Database implements IDatabase, LoggerAwareInterface {
+abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
        /** Number of times to re-try an operation in case of deadlock */
        const DEADLOCK_TRIES = 4;
        /** Minimum time to wait before retry, in microseconds */
@@ -209,12 +211,6 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
        private $lazyMasterHandle;
 
-       /**
-        * @since 1.22
-        * @var string[] Process cache of VIEWs names in the database
-        */
-       protected $allViews = null;
-
        /** @var float UNIX timestamp */
        protected $lastPing = 0.0;
 
@@ -248,11 +244,11 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                $this->agent = str_replace( '/', '-', $params['agent'] );
 
                $this->mFlags = $params['flags'];
-               if ( $this->mFlags & DBO_DEFAULT ) {
+               if ( $this->mFlags & self::DBO_DEFAULT ) {
                        if ( $this->cliMode ) {
-                               $this->mFlags &= ~DBO_TRX;
+                               $this->mFlags &= ~self::DBO_TRX;
                        } else {
-                               $this->mFlags |= DBO_TRX;
+                               $this->mFlags |= self::DBO_TRX;
                        }
                }
 
@@ -407,9 +403,11 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        }
 
        public function bufferResults( $buffer = null ) {
-               $res = !$this->getFlag( DBO_NOBUFFER );
+               $res = !$this->getFlag( self::DBO_NOBUFFER );
                if ( $buffer !== null ) {
-                       $buffer ? $this->clearFlag( DBO_NOBUFFER ) : $this->setFlag( DBO_NOBUFFER );
+                       $buffer
+                               ? $this->clearFlag( self::DBO_NOBUFFER )
+                               : $this->setFlag( self::DBO_NOBUFFER );
                }
 
                return $res;
@@ -428,9 +426,11 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
         * @return bool The previous value of the flag.
         */
        protected function ignoreErrors( $ignoreErrors = null ) {
-               $res = $this->getFlag( DBO_IGNORE );
+               $res = $this->getFlag( self::DBO_IGNORE );
                if ( $ignoreErrors !== null ) {
-                       $ignoreErrors ? $this->setFlag( DBO_IGNORE ) : $this->clearFlag( DBO_IGNORE );
+                       $ignoreErrors
+                               ? $this->setFlag( self::DBO_IGNORE )
+                               : $this->clearFlag( self::DBO_IGNORE );
                }
 
                return $res;
@@ -494,7 +494,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
         * @see setLazyMasterHandle()
         * @since 1.27
         */
-       public function getLazyMasterHandle() {
+       protected function getLazyMasterHandle() {
                return $this->lazyMasterHandle;
        }
 
@@ -828,7 +828,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
 
                # Start implicit transactions that wrap the request if DBO_TRX is enabled
-               if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
+               if ( !$this->mTrxLevel && $this->getFlag( self::DBO_TRX )
                        && $this->isTransactableQuery( $sql )
                ) {
                        $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
@@ -842,7 +842,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                                $this->mServer, $this->mDBname, $this->mTrxShortId );
                }
 
-               if ( $this->getFlag( DBO_DEBUG ) ) {
+               if ( $this->getFlag( self::DBO_DEBUG ) ) {
                        $this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
                }
 
@@ -1230,7 +1230,6 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                return '';
        }
 
-       // See IDatabase::select for the docs for this function
        public function select( $table, $vars, $conds = '', $fname = __METHOD__,
                $options = [], $join_conds = [] ) {
                $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
@@ -1674,25 +1673,6 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                return $this->mServer;
        }
 
-       /**
-        * Format a table name ready for use in constructing an SQL query
-        *
-        * This does two important things: it quotes the table names to clean them up,
-        * and it adds a table prefix if only given a table name with no quotes.
-        *
-        * All functions of this object which require a table name call this function
-        * themselves. Pass the canonical name to such functions. This is only needed
-        * when calling query() directly.
-        *
-        * @note This function does not sanitize user input. It is not safe to use
-        *   this function to escape user input.
-        * @param string $name Database table name
-        * @param string $format One of:
-        *   quoted - Automatically pass the table name through addIdentifierQuotes()
-        *            so that it can be used in a query.
-        *   raw - Do not add identifier quotes to the table name
-        * @return string Full database name
-        */
        public function tableName( $name, $format = 'quoted' ) {
                # Skip the entire process when we have a string quoted on both ends.
                # Note that we check the end so that we will still quote any use of
@@ -1773,17 +1753,6 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                return $tableName;
        }
 
-       /**
-        * Fetch a number of table names into an array
-        * This is handy when you need to construct SQL for joins
-        *
-        * Example:
-        * extract( $dbr->tableNames( 'user', 'watchlist' ) );
-        * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
-        *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
-        *
-        * @return array
-        */
        public function tableNames() {
                $inArray = func_get_args();
                $retVal = [];
@@ -1795,17 +1764,6 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                return $retVal;
        }
 
-       /**
-        * Fetch a number of table names into an zero-indexed numerical array
-        * This is handy when you need to construct SQL for joins
-        *
-        * Example:
-        * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
-        * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
-        *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
-        *
-        * @return array
-        */
        public function tableNamesN() {
                $inArray = func_get_args();
                $retVal = [];
@@ -2239,13 +2197,6 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                $this->query( $sql, $fname );
        }
 
-       /**
-        * Returns the size of a text field, or -1 for "unlimited"
-        *
-        * @param string $table
-        * @param string $field
-        * @return int
-        */
        public function textFieldSize( $table, $field ) {
                $table = $this->tableName( $table );
                $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
@@ -2444,30 +2395,6 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                return false;
        }
 
-       /**
-        * Perform a deadlock-prone transaction.
-        *
-        * This function invokes a callback function to perform a set of write
-        * queries. If a deadlock occurs during the processing, the transaction
-        * will be rolled back and the callback function will be called again.
-        *
-        * Avoid using this method outside of Job or Maintenance classes.
-        *
-        * Usage:
-        *   $dbw->deadlockLoop( callback, ... );
-        *
-        * Extra arguments are passed through to the specified callback function.
-        * This method requires that no transactions are already active to avoid
-        * causing premature commits or exceptions.
-        *
-        * Returns whatever the callback function returned on its successful,
-        * iteration, or false on error, for example if the retry limit was
-        * reached.
-        *
-        * @return mixed
-        * @throws DBUnexpectedError
-        * @throws Exception
-        */
        public function deadlockLoop() {
                $args = func_get_args();
                $function = array_shift( $args );
@@ -2509,7 +2436,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                return 0;
        }
 
-       public function getSlavePos() {
+       public function getReplicaPos() {
                # Stub
                return false;
        }
@@ -2587,7 +2514,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                        return;
                }
 
-               $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
+               $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
                /** @var Exception $e */
                $e = null; // first exception
                do { // callbacks may add callbacks :)
@@ -2600,12 +2527,12 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                        foreach ( $callbacks as $callback ) {
                                try {
                                        list( $phpCallback ) = $callback;
-                                       $this->clearFlag( DBO_TRX ); // make each query its own transaction
+                                       $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
                                        call_user_func_array( $phpCallback, [ $trigger ] );
                                        if ( $autoTrx ) {
-                                               $this->setFlag( DBO_TRX ); // restore automatic begin()
+                                               $this->setFlag( self::DBO_TRX ); // restore automatic begin()
                                        } else {
-                                               $this->clearFlag( DBO_TRX ); // restore auto-commit
+                                               $this->clearFlag( self::DBO_TRX ); // restore auto-commit
                                        }
                                } catch ( Exception $ex ) {
                                        call_user_func( $this->errorLogger, $ex );
@@ -2689,7 +2616,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                        $this->begin( $fname, self::TRANSACTION_INTERNAL );
                        // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
                        // in all changes being in one transaction to keep requests transactional.
-                       if ( !$this->getFlag( DBO_TRX ) ) {
+                       if ( !$this->getFlag( self::DBO_TRX ) ) {
                                $this->mTrxAutomaticAtomic = true;
                        }
                }
@@ -2741,7 +2668,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                                $this->queryLogger->error( $msg );
                                return; // join the main transaction set
                        }
-               } elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
+               } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
                        // @TODO: make this an exception at some point
                        $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
                        $this->queryLogger->error( $msg );
@@ -2853,7 +2780,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                                $this->queryLogger->error(
                                        "$fname: No transaction to rollback, something got out of sync." );
                                return; // nothing to do
-                       } elseif ( $this->getFlag( DBO_TRX ) ) {
+                       } elseif ( $this->getFlag( self::DBO_TRX ) ) {
                                throw new DBUnexpectedError(
                                        $this,
                                        "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
@@ -2934,42 +2861,10 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
        }
 
-       /**
-        * Reset the views process cache set by listViews()
-        * @since 1.22
-        */
-       final public function clearViewsCache() {
-               $this->allViews = null;
-       }
-
-       /**
-        * Lists all the VIEWs in the database
-        *
-        * For caching purposes the list of all views should be stored in
-        * $this->allViews. The process cache can be cleared with clearViewsCache()
-        *
-        * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
-        * @param string $fname Name of calling function
-        * @throws RuntimeException
-        * @return array
-        * @since 1.22
-        */
        public function listViews( $prefix = null, $fname = __METHOD__ ) {
                throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
        }
 
-       /**
-        * Differentiates between a TABLE and a VIEW
-        *
-        * @param string $name Name of the database-structure to test.
-        * @throws RuntimeException
-        * @return bool
-        * @since 1.22
-        */
-       public function isView( $name ) {
-               throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
-       }
-
        public function timestamp( $ts = 0 ) {
                $t = new ConvertibleTimestamp( $ts );
                // Let errors bubble up to avoid putting garbage in the DB
@@ -3020,7 +2915,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                }
 
                // This will reconnect if possible or return false if not
-               $this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
+               $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
                $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
                $this->restoreFlags( self::RESTORE_PRIOR );
 
@@ -3140,22 +3035,6 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        public function setSessionOptions( array $options ) {
        }
 
-       /**
-        * Read and execute SQL commands from a file.
-        *
-        * Returns true on success, error string or exception on failure (depending
-        * on object's error ignore settings).
-        *
-        * @param string $filename File name to open
-        * @param bool|callable $lineCallback Optional function called before reading each line
-        * @param bool|callable $resultCallback Optional function called for each MySQL result
-        * @param bool|string $fname Calling function name or false if name should be
-        *   generated dynamically using $filename
-        * @param bool|callable $inputCallback Optional function called for each
-        *   complete line sent
-        * @return bool|string
-        * @throws Exception
-        */
        public function sourceFile(
                $filename,
                $lineCallback = false,
@@ -3192,19 +3071,6 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                $this->mSchemaVars = $vars;
        }
 
-       /**
-        * Read and execute commands from an open file handle.
-        *
-        * Returns true on success, error string or exception on failure (depending
-        * on object's error ignore settings).
-        *
-        * @param resource $fp File handle
-        * @param bool|callable $lineCallback Optional function called before reading each query
-        * @param bool|callable $resultCallback Optional function called for each MySQL result
-        * @param string $fname Calling function name
-        * @param bool|callable $inputCallback Optional function called for each complete query sent
-        * @return bool|string
-        */
        public function sourceStream(
                $fp,
                $lineCallback = false,
index 87330b0..9ab7c64 100644 (file)
@@ -59,10 +59,10 @@ class DatabaseMysql extends DatabaseMysqlBase {
                }
 
                $connFlags = 0;
-               if ( $this->mFlags & DBO_SSL ) {
+               if ( $this->mFlags & self::DBO_SSL ) {
                        $connFlags |= MYSQL_CLIENT_SSL;
                }
-               if ( $this->mFlags & DBO_COMPRESS ) {
+               if ( $this->mFlags & self::DBO_COMPRESS ) {
                        $connFlags |= MYSQL_CLIENT_COMPRESS;
                }
 
@@ -81,7 +81,7 @@ class DatabaseMysql extends DatabaseMysqlBase {
                        if ( $i > 1 ) {
                                usleep( 1000 );
                        }
-                       if ( $this->mFlags & DBO_PERSISTENT ) {
+                       if ( $this->mFlags & self::DBO_PERSISTENT ) {
                                $conn = mysql_pconnect( $realServer, $this->mUser, $this->mPassword, $connFlags );
                        } else {
                                # Create a new connection...
index 675bc87..c31b9f9 100644 (file)
@@ -811,7 +811,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase {
                        // with an old master hostname. Such calls make MASTER_POS_WAIT() return null. Try
                        // to detect this and treat the replica DB as having reached the position; a proper master
                        // switchover already requires that the new master be caught up before the switch.
-                       $replicationPos = $this->getSlavePos();
+                       $replicationPos = $this->getReplicaPos();
                        if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) {
                                $this->lastKnownReplicaPos = $replicationPos;
                                $status = 0;
@@ -829,7 +829,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase {
         *
         * @return MySQLMasterPos|bool
         */
-       function getSlavePos() {
+       function getReplicaPos() {
                $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
                $row = $this->fetchObject( $res );
 
@@ -1296,26 +1296,22 @@ abstract class DatabaseMysqlBase extends DatabaseBase {
         * @since 1.22
         */
        public function listViews( $prefix = null, $fname = __METHOD__ ) {
+               // The name of the column containing the name of the VIEW
+               $propertyName = 'Tables_in_' . $this->mDBname;
 
-               if ( !isset( $this->allViews ) ) {
-
-                       // The name of the column containing the name of the VIEW
-                       $propertyName = 'Tables_in_' . $this->mDBname;
-
-                       // Query for the VIEWS
-                       $result = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
-                       $this->allViews = [];
-                       while ( ( $row = $this->fetchRow( $result ) ) !== false ) {
-                               array_push( $this->allViews, $row[$propertyName] );
-                       }
+               // Query for the VIEWS
+               $res = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
+               $allViews = [];
+               foreach ( $res as $row ) {
+                       array_push( $allViews, $row->$propertyName );
                }
 
                if ( is_null( $prefix ) || $prefix === '' ) {
-                       return $this->allViews;
+                       return $allViews;
                }
 
                $filteredViews = [];
-               foreach ( $this->allViews as $viewName ) {
+               foreach ( $allViews as $viewName ) {
                        // Does the name of this VIEW start with the table-prefix?
                        if ( strpos( $viewName, $prefix ) === 0 ) {
                                array_push( $filteredViews, $viewName );
index fb983bd..c34f901 100644 (file)
@@ -82,7 +82,7 @@ class DatabaseMysqli extends DatabaseMysqlBase {
                $mysqli = mysqli_init();
 
                $connFlags = 0;
-               if ( $this->mFlags & DBO_SSL ) {
+               if ( $this->mFlags & self::DBO_SSL ) {
                        $connFlags |= MYSQLI_CLIENT_SSL;
                        $mysqli->ssl_set(
                                $this->sslKeyPath,
@@ -92,10 +92,10 @@ class DatabaseMysqli extends DatabaseMysqlBase {
                                $this->sslCiphers
                        );
                }
-               if ( $this->mFlags & DBO_COMPRESS ) {
+               if ( $this->mFlags & self::DBO_COMPRESS ) {
                        $connFlags |= MYSQLI_CLIENT_COMPRESS;
                }
-               if ( $this->mFlags & DBO_PERSISTENT ) {
+               if ( $this->mFlags & self::DBO_PERSISTENT ) {
                        $realServer = 'p:' . $realServer;
                }
 
index 84439f4..a69a5fa 100644 (file)
@@ -109,7 +109,7 @@ class DatabasePostgres extends DatabaseBase {
                if ( (int)$this->port > 0 ) {
                        $connectVars['port'] = (int)$this->port;
                }
-               if ( $this->mFlags & DBO_SSL ) {
+               if ( $this->mFlags & self::DBO_SSL ) {
                        $connectVars['sslmode'] = 1;
                }
 
index f201dad..c33d3b3 100644 (file)
@@ -171,7 +171,7 @@ class DatabaseSqlite extends DatabaseBase {
 
                $this->dbPath = $fileName;
                try {
-                       if ( $this->mFlags & DBO_PERSISTENT ) {
+                       if ( $this->mFlags & self::DBO_PERSISTENT ) {
                                $this->mConn = new PDO( "sqlite:$fileName", '', '',
                                        [ PDO::ATTR_PERSISTENT => true ] );
                        } else {
index 56eb002..0396ec8 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 /**
  * @defgroup Database Database
  *
@@ -26,7 +25,7 @@
  */
 
 /**
- * Basic database interface for live and lazy-loaded DB handles
+ * Basic database interface for live and lazy-loaded relation database handles
  *
  * @note: IDatabase and DBConnRef should be updated to reflect any changes
  * @ingroup Database
@@ -74,6 +73,27 @@ interface IDatabase {
        /** @var int Combine list with OR clauses */
        const LIST_OR = 4;
 
+       /** @var int Enable debug logging */
+       const DBO_DEBUG = 1;
+       /** @var int Disable query buffering (only one result set can be iterated at a time) */
+       const DBO_NOBUFFER = 2;
+       /** @var int Ignore query errors (internal use only!) */
+       const DBO_IGNORE = 4;
+       /** @var int Autoatically start transaction on first query (work with ILoadBalancer rounds) */
+       const DBO_TRX = 8;
+       /** @var int Use DBO_TRX in non-CLI mode */
+       const DBO_DEFAULT = 16;
+       /** @var int Use DB persistent connections if possible */
+       const DBO_PERSISTENT = 32;
+       /** @var int DBA session mode; mostly for Oracle */
+       const DBO_SYSDBA = 64;
+       /** @var int Schema file mode; mostly for Oracle */
+       const DBO_DDLMODE = 128;
+       /** @var int Enable SSL/TLS in connection protocol */
+       const DBO_SSL = 256;
+       /** @var int Enable compression in connection protocol */
+       const DBO_COMPRESS = 512;
+
        /**
         * A string describing the current software version, and possibly
         * other details in a user-friendly way. Will be listed on Special:Version, etc.
@@ -1302,7 +1322,7 @@ interface IDatabase {
         *
         * @return DBMasterPos|bool False if this is not a replica DB.
         */
-       public function getSlavePos();
+       public function getReplicaPos();
 
        /**
         * Get the position of this master
diff --git a/includes/libs/rdbms/database/IMaintainableDatabase.php b/includes/libs/rdbms/database/IMaintainableDatabase.php
new file mode 100644 (file)
index 0000000..f65e0dd
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+
+/**
+ * This file deals with database interface functions
+ * and query specifics/optimisations.
+ *
+ * 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 Database
+ */
+
+/**
+ * Advanced database interface for IDatabase handles that include maintenance methods
+ *
+ * This is useful for type-hints used by installer, upgrader, and background scripts
+ * that will make use of lower-level and longer-running queries, including schema changes.
+ *
+ * @ingroup Database
+ * @since 1.28
+ */
+interface IMaintainableDatabase extends IDatabase {
+       /**
+        * Format a table name ready for use in constructing an SQL query
+        *
+        * This does two important things: it quotes the table names to clean them up,
+        * and it adds a table prefix if only given a table name with no quotes.
+        *
+        * All functions of this object which require a table name call this function
+        * themselves. Pass the canonical name to such functions. This is only needed
+        * when calling query() directly.
+        *
+        * @note This function does not sanitize user input. It is not safe to use
+        *   this function to escape user input.
+        * @param string $name Database table name
+        * @param string $format One of:
+        *   quoted - Automatically pass the table name through addIdentifierQuotes()
+        *            so that it can be used in a query.
+        *   raw - Do not add identifier quotes to the table name
+        * @return string Full database name
+        */
+       public function tableName( $name, $format = 'quoted' );
+
+       /**
+        * Fetch a number of table names into an array
+        * This is handy when you need to construct SQL for joins
+        *
+        * Example:
+        * extract( $dbr->tableNames( 'user', 'watchlist' ) );
+        * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+        *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+        *
+        * @return array
+        */
+       public function tableNames();
+
+       /**
+        * Fetch a number of table names into an zero-indexed numerical array
+        * This is handy when you need to construct SQL for joins
+        *
+        * Example:
+        * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
+        * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+        *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+        *
+        * @return array
+        */
+       public function tableNamesN();
+
+       /**
+        * Returns the size of a text field, or -1 for "unlimited"
+        *
+        * @param string $table
+        * @param string $field
+        * @return int
+        */
+       public function textFieldSize( $table, $field );
+
+       /**
+        * Read and execute SQL commands from a file.
+        *
+        * Returns true on success, error string or exception on failure (depending
+        * on object's error ignore settings).
+        *
+        * @param string $filename File name to open
+        * @param bool|callable $lineCallback Optional function called before reading each line
+        * @param bool|callable $resultCallback Optional function called for each MySQL result
+        * @param bool|string $fname Calling function name or false if name should be
+        *   generated dynamically using $filename
+        * @param bool|callable $inputCallback Optional function called for each
+        *   complete line sent
+        * @return bool|string
+        * @throws Exception
+        */
+       public function sourceFile(
+               $filename,
+               $lineCallback = false,
+               $resultCallback = false,
+               $fname = false,
+               $inputCallback = false
+       );
+
+       /**
+        * Read and execute commands from an open file handle.
+        *
+        * Returns true on success, error string or exception on failure (depending
+        * on object's error ignore settings).
+        *
+        * @param resource $fp File handle
+        * @param bool|callable $lineCallback Optional function called before reading each query
+        * @param bool|callable $resultCallback Optional function called for each MySQL result
+        * @param string $fname Calling function name
+        * @param bool|callable $inputCallback Optional function called for each complete query sent
+        * @return bool|string
+        */
+       public function sourceStream(
+               $fp,
+               $lineCallback = false,
+               $resultCallback = false,
+               $fname = __METHOD__,
+               $inputCallback = false
+       );
+
+       /**
+        * Called by sourceStream() to check if we've reached a statement end
+        *
+        * @param string &$sql SQL assembled so far
+        * @param string &$newLine New line about to be added to $sql
+        * @return bool Whether $newLine contains end of the statement
+        */
+       public function streamStatementEnd( &$sql, &$newLine );
+
+       /**
+        * Delete a table
+        * @param string $tableName
+        * @param string $fName
+        * @return bool|ResultWrapper
+        */
+       public function dropTable( $tableName, $fName = __METHOD__ );
+
+       /**
+        * Perform a deadlock-prone transaction.
+        *
+        * This function invokes a callback function to perform a set of write
+        * queries. If a deadlock occurs during the processing, the transaction
+        * will be rolled back and the callback function will be called again.
+        *
+        * Avoid using this method outside of Job or Maintenance classes.
+        *
+        * Usage:
+        *   $dbw->deadlockLoop( callback, ... );
+        *
+        * Extra arguments are passed through to the specified callback function.
+        * This method requires that no transactions are already active to avoid
+        * causing premature commits or exceptions.
+        *
+        * Returns whatever the callback function returned on its successful,
+        * iteration, or false on error, for example if the retry limit was
+        * reached.
+        *
+        * @return mixed
+        * @throws DBUnexpectedError
+        * @throws Exception
+        */
+       public function deadlockLoop();
+
+       /**
+        * Lists all the VIEWs in the database
+        *
+        * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
+        * @param string $fname Name of calling function
+        * @throws RuntimeException
+        * @return array
+        */
+       public function listViews( $prefix = null, $fname = __METHOD__ );
+}
index b420ca1..692a704 100644 (file)
@@ -3,22 +3,22 @@
 /**@{
  * Database related constants
  */
-define( 'DBO_DEBUG', 1 );
-define( 'DBO_NOBUFFER', 2 );
-define( 'DBO_IGNORE', 4 );
-define( 'DBO_TRX', 8 ); // automatically start transaction on first query
-define( 'DBO_DEFAULT', 16 );
-define( 'DBO_PERSISTENT', 32 );
-define( 'DBO_SYSDBA', 64 ); // for oracle maintenance
-define( 'DBO_DDLMODE', 128 ); // when using schema files: mostly for Oracle
-define( 'DBO_SSL', 256 );
-define( 'DBO_COMPRESS', 512 );
+define( 'DBO_DEBUG', IDatabase::DBO_DEBUG );
+define( 'DBO_NOBUFFER', IDatabase::DBO_NOBUFFER );
+define( 'DBO_IGNORE', IDatabase::DBO_IGNORE );
+define( 'DBO_TRX', IDatabase::DBO_TRX );
+define( 'DBO_DEFAULT', IDatabase::DBO_DEFAULT );
+define( 'DBO_PERSISTENT', IDatabase::DBO_PERSISTENT );
+define( 'DBO_SYSDBA', IDatabase::DBO_SYSDBA );
+define( 'DBO_DDLMODE', IDatabase::DBO_DDLMODE );
+define( 'DBO_SSL', IDatabase::DBO_SSL );
+define( 'DBO_COMPRESS', IDatabase::DBO_COMPRESS );
 /**@}*/
 
 /**@{
  * Valid database indexes
  * Operation-based indexes
  */
-define( 'DB_REPLICA', -1 );     # Read from a replica (or only server)
-define( 'DB_MASTER', -2 );    # Write to master (or only server)
+define( 'DB_REPLICA', ILoadBalancer::DB_REPLICA );
+define( 'DB_MASTER', ILoadBalancer::DB_MASTER );
 /**@}*/
diff --git a/includes/libs/rdbms/lbfactory/ILBFactory.php b/includes/libs/rdbms/lbfactory/ILBFactory.php
new file mode 100644 (file)
index 0000000..d7ca7cd
--- /dev/null
@@ -0,0 +1,300 @@
+<?php
+/**
+ * Generator and manager of database load balancing objects
+ *
+ * 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 Database
+ */
+
+/**
+ * An interface for generating database load balancers
+ * @ingroup Database
+ * @since 1.28
+ */
+interface ILBFactory {
+       const SHUTDOWN_NO_CHRONPROT = 0; // don't save DB positions at all
+       const SHUTDOWN_CHRONPROT_ASYNC = 1; // save DB positions, but don't wait on remote DCs
+       const SHUTDOWN_CHRONPROT_SYNC = 2; // save DB positions, waiting on all DCs
+
+       /**
+        * Construct a manager of ILoadBalancer objects
+        *
+        * Sub-classes will extend the required keys in $conf with additional parameters
+        *
+        * @param $conf $params Array with keys:
+        *  - localDomain: A DatabaseDomain or domain ID string.
+        *  - readOnlyReason : Reason the master DB is read-only if so [optional]
+        *  - srvCache : BagOStuff object for server cache [optional]
+        *  - memCache : BagOStuff object for cluster memory cache [optional]
+        *  - wanCache : WANObjectCache object [optional]
+        *  - hostname : The name of the current server [optional]
+        *  - cliMode: Whether the execution context is a CLI script. [optional]
+        *  - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
+        *  - trxProfiler: TransactionProfiler instance. [optional]
+        *  - replLogger: PSR-3 logger instance. [optional]
+        *  - connLogger: PSR-3 logger instance. [optional]
+        *  - queryLogger: PSR-3 logger instance. [optional]
+        *  - perfLogger: PSR-3 logger instance. [optional]
+        *  - errorLogger : Callback that takes an Exception and logs it. [optional]
+        * @throws InvalidArgumentException
+        */
+       public function __construct( array $conf );
+
+       /**
+        * Disables all load balancers. All connections are closed, and any attempt to
+        * open a new connection will result in a DBAccessError.
+        * @see ILoadBalancer::disable()
+        */
+       public function destroy();
+
+       /**
+        * Create a new load balancer object. The resulting object will be untracked,
+        * not chronology-protected, and the caller is responsible for cleaning it up.
+        *
+        * This method is for only advanced usage and callers should almost always use
+        * getMainLB() instead. This method can be useful when a table is used as a key/value
+        * store. In that cases, one might want to query it in autocommit mode (DBO_TRX off)
+        * but still use DBO_TRX transaction rounds on other tables.
+        *
+        * @param bool|string $domain Domain ID, or false for the current domain
+        * @return ILoadBalancer
+        */
+       public function newMainLB( $domain = false );
+
+       /**
+        * Get a cached (tracked) load balancer object.
+        *
+        * @param bool|string $domain Domain ID, or false for the current domain
+        * @return ILoadBalancer
+        */
+       public function getMainLB( $domain = false );
+
+       /**
+        * Create a new load balancer for external storage. The resulting object will be
+        * untracked, not chronology-protected, and the caller is responsible for
+        * cleaning it up.
+        *
+        * This method is for only advanced usage and callers should almost always use
+        * getExternalLB() instead. This method can be useful when a table is used as a
+        * key/value store. In that cases, one might want to query it in autocommit mode
+        * (DBO_TRX off) but still use DBO_TRX transaction rounds on other tables.
+        *
+        * @param string $cluster External storage cluster, or false for core
+        * @param bool|string $domain Domain ID, or false for the current domain
+        * @return ILoadBalancer
+        */
+       public function newExternalLB( $cluster, $domain = false );
+
+       /**
+        * Get a cached (tracked) load balancer for external storage
+        *
+        * @param string $cluster External storage cluster, or false for core
+        * @param bool|string $domain Domain ID, or false for the current domain
+        * @return ILoadBalancer
+        */
+       public function getExternalLB( $cluster, $domain = false );
+
+       /**
+        * Execute a function for each tracked load balancer
+        * The callback is called with the load balancer as the first parameter,
+        * and $params passed as the subsequent parameters.
+        *
+        * @param callable $callback
+        * @param array $params
+        */
+       public function forEachLB( $callback, array $params = [] );
+
+       /**
+        * Prepare all tracked load balancers for shutdown
+        * @param integer $mode One of the class SHUTDOWN_* constants
+        * @param callable|null $workCallback Work to mask ChronologyProtector writes
+        */
+       public function shutdown(
+               $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
+       );
+
+       /**
+        * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshot
+        *
+        * @param string $fname Caller name
+        */
+       public function flushReplicaSnapshots( $fname = __METHOD__ );
+
+       /**
+        * Commit on all connections. Done for two reasons:
+        * 1. To commit changes to the masters.
+        * 2. To release the snapshot on all connections, master and replica DB.
+        * @param string $fname Caller name
+        * @param array $options Options map:
+        *   - maxWriteDuration: abort if more than this much time was spent in write queries
+        */
+       public function commitAll( $fname = __METHOD__, array $options = [] );
+
+       /**
+        * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set)
+        *
+        * The DBO_TRX setting will be reverted to the default in each of these methods:
+        *   - commitMasterChanges()
+        *   - rollbackMasterChanges()
+        *   - commitAll()
+        *
+        * This allows for custom transaction rounds from any outer transaction scope.
+        *
+        * @param string $fname
+        * @throws DBTransactionError
+        */
+       public function beginMasterChanges( $fname = __METHOD__ );
+
+       /**
+        * Commit changes on all master connections
+        * @param string $fname Caller name
+        * @param array $options Options map:
+        *   - maxWriteDuration: abort if more than this much time was spent in write queries
+        * @throws Exception
+        */
+       public function commitMasterChanges( $fname = __METHOD__, array $options = [] );
+
+       /**
+        * Rollback changes on all master connections
+        * @param string $fname Caller name
+        */
+       public function rollbackMasterChanges( $fname = __METHOD__ );
+
+       /**
+        * Determine if any master connection has pending changes
+        * @return bool
+        */
+       public function hasMasterChanges();
+
+       /**
+        * Detemine if any lagged replica DB connection was used
+        * @return bool
+        */
+       public function laggedReplicaUsed();
+
+       /**
+        * Determine if any master connection has pending/written changes from this request
+        * @param float $age How many seconds ago is "recent" [defaults to LB lag wait timeout]
+        * @return bool
+        */
+       public function hasOrMadeRecentMasterChanges( $age = null );
+
+       /**
+        * Waits for the replica DBs to catch up to the current master position
+        *
+        * Use this when updating very large numbers of rows, as in maintenance scripts,
+        * to avoid causing too much lag. Of course, this is a no-op if there are no replica DBs.
+        *
+        * By default this waits on all DB clusters actually used in this request.
+        * This makes sense when lag being waiting on is caused by the code that does this check.
+        * In that case, setting "ifWritesSince" can avoid the overhead of waiting for clusters
+        * that were not changed since the last wait check. To forcefully wait on a specific cluster
+        * for a given domain, use the 'domain' parameter. To forcefully wait on an "external" cluster,
+        * use the "cluster" parameter.
+        *
+        * Never call this function after a large DB write that is *still* in a transaction.
+        * It only makes sense to call this after the possible lag inducing changes were committed.
+        *
+        * @param array $opts Optional fields that include:
+        *   - domain : wait on the load balancer DBs that handles the given domain ID
+        *   - cluster : wait on the given external load balancer DBs
+        *   - timeout : Max wait time. Default: ~60 seconds
+        *   - ifWritesSince: Only wait if writes were done since this UNIX timestamp
+        * @throws DBReplicationWaitError If a timeout or error occured waiting on a DB cluster
+        */
+       public function waitForReplication( array $opts = [] );
+
+       /**
+        * Add a callback to be run in every call to waitForReplication() before waiting
+        *
+        * Callbacks must clear any transactions that they start
+        *
+        * @param string $name Callback name
+        * @param callable|null $callback Use null to unset a callback
+        */
+       public function setWaitForReplicationListener( $name, callable $callback = null );
+
+       /**
+        * Get a token asserting that no transaction writes are active
+        *
+        * @param string $fname Caller name (e.g. __METHOD__)
+        * @return mixed A value to pass to commitAndWaitForReplication()
+        */
+       public function getEmptyTransactionTicket( $fname );
+
+       /**
+        * Convenience method for safely running commitMasterChanges()/waitForReplication()
+        *
+        * This will commit and wait unless $ticket indicates it is unsafe to do so
+        *
+        * @param string $fname Caller name (e.g. __METHOD__)
+        * @param mixed $ticket Result of getEmptyTransactionTicket()
+        * @param array $opts Options to waitForReplication()
+        * @throws DBReplicationWaitError
+        */
+       public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] );
+
+       /**
+        * @param string $dbName DB master name (e.g. "db1052")
+        * @return float|bool UNIX timestamp when client last touched the DB or false if not recent
+        */
+       public function getChronologyProtectorTouched( $dbName );
+
+       /**
+        * Disable the ChronologyProtector for all load balancers
+        *
+        * This can be called at the start of special API entry points
+        */
+       public function disableChronologyProtection();
+
+       /**
+        * Set a new table prefix for the existing local domain ID for testing
+        *
+        * @param string $prefix
+        */
+       public function setDomainPrefix( $prefix );
+
+       /**
+        * Close all open database connections on all open load balancers.
+        */
+       public function closeAll();
+
+       /**
+        * @param string $agent Agent name for query profiling
+        */
+       public function setAgentName( $agent );
+
+       /**
+        * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed
+        *
+        * Note that unlike cookies, this works accross domains
+        *
+        * @param string $url
+        * @param float $time UNIX timestamp just before shutdown() was called
+        * @return string
+        */
+       public function appendPreShutdownTimeAsQuery( $url, $time );
+
+       /**
+        * @param array $info Map of fields, including:
+        *   - IPAddress : IP address
+        *   - UserAgent : User-Agent HTTP header
+        *   - ChronologyProtection : cookie/header value specifying ChronologyProtector usage
+        */
+       public function setRequestInfo( array $info );
+}
index d75ba93..85194bc 100644 (file)
@@ -27,7 +27,7 @@ use Psr\Log\LoggerInterface;
  * An interface for generating database load balancers
  * @ingroup Database
  */
-abstract class LBFactory {
+abstract class LBFactory implements ILBFactory {
        /** @var ChronologyProtector */
        protected $chronProt;
        /** @var object|string Class name or object With profileIn/profileOut methods */
@@ -72,35 +72,9 @@ abstract class LBFactory {
        /** @var string Agent name for query profiling */
        protected $agent;
 
-       const SHUTDOWN_NO_CHRONPROT = 0; // don't save DB positions at all
-       const SHUTDOWN_CHRONPROT_ASYNC = 1; // save DB positions, but don't wait on remote DCs
-       const SHUTDOWN_CHRONPROT_SYNC = 2; // save DB positions, waiting on all DCs
-
        private static $loggerFields =
                [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
 
-       /**
-        * Construct a manager of ILoadBalancer objects
-        *
-        * Sub-classes will extend the required keys in $conf with additional parameters
-        *
-        * @param $conf $params Array with keys:
-        *  - localDomain: A DatabaseDomain or domain ID string.
-        *  - readOnlyReason : Reason the master DB is read-only if so [optional]
-        *  - srvCache : BagOStuff object for server cache [optional]
-        *  - memCache : BagOStuff object for cluster memory cache [optional]
-        *  - wanCache : WANObjectCache object [optional]
-        *  - hostname : The name of the current server [optional]
-        *  - cliMode: Whether the execution context is a CLI script. [optional]
-        *  - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
-        *  - trxProfiler: TransactionProfiler instance. [optional]
-        *  - replLogger: PSR-3 logger instance. [optional]
-        *  - connLogger: PSR-3 logger instance. [optional]
-        *  - queryLogger: PSR-3 logger instance. [optional]
-        *  - perfLogger: PSR-3 logger instance. [optional]
-        *  - errorLogger : Callback that takes an Exception and logs it. [optional]
-        * @throws InvalidArgumentException
-        */
        public function __construct( array $conf ) {
                $this->localDomain = isset( $conf['localDomain'] )
                        ? DatabaseDomain::newFromId( $conf['localDomain'] )
@@ -143,68 +117,11 @@ abstract class LBFactory {
                $this->ticket = mt_rand();
        }
 
-       /**
-        * Disables all load balancers. All connections are closed, and any attempt to
-        * open a new connection will result in a DBAccessError.
-        * @see ILoadBalancer::disable()
-        */
        public function destroy() {
                $this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
                $this->forEachLBCallMethod( 'disable' );
        }
 
-       /**
-        * Create a new load balancer object. The resulting object will be untracked,
-        * not chronology-protected, and the caller is responsible for cleaning it up.
-        *
-        * @param bool|string $domain Domain ID, or false for the current domain
-        * @return ILoadBalancer
-        */
-       abstract public function newMainLB( $domain = false );
-
-       /**
-        * Get a cached (tracked) load balancer object.
-        *
-        * @param bool|string $domain Domain ID, or false for the current domain
-        * @return ILoadBalancer
-        */
-       abstract public function getMainLB( $domain = false );
-
-       /**
-        * Create a new load balancer for external storage. The resulting object will be
-        * untracked, not chronology-protected, and the caller is responsible for
-        * cleaning it up.
-        *
-        * @param string $cluster External storage cluster, or false for core
-        * @param bool|string $domain Domain ID, or false for the current domain
-        * @return ILoadBalancer
-        */
-       abstract protected function newExternalLB( $cluster, $domain = false );
-
-       /**
-        * Get a cached (tracked) load balancer for external storage
-        *
-        * @param string $cluster External storage cluster, or false for core
-        * @param bool|string $domain Domain ID, or false for the current domain
-        * @return ILoadBalancer
-        */
-       abstract public function getExternalLB( $cluster, $domain = false );
-
-       /**
-        * Execute a function for each tracked load balancer
-        * The callback is called with the load balancer as the first parameter,
-        * and $params passed as the subsequent parameters.
-        *
-        * @param callable $callback
-        * @param array $params
-        */
-       abstract public function forEachLB( $callback, array $params = [] );
-
-       /**
-        * Prepare all tracked load balancers for shutdown
-        * @param integer $mode One of the class SHUTDOWN_* constants
-        * @param callable|null $workCallback Work to mask ChronologyProtector writes
-        */
        public function shutdown(
                $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
        ) {
@@ -233,43 +150,15 @@ abstract class LBFactory {
                );
        }
 
-       /**
-        * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshot
-        *
-        * @param string $fname Caller name
-        * @since 1.28
-        */
        public function flushReplicaSnapshots( $fname = __METHOD__ ) {
                $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] );
        }
 
-       /**
-        * Commit on all connections. Done for two reasons:
-        * 1. To commit changes to the masters.
-        * 2. To release the snapshot on all connections, master and replica DB.
-        * @param string $fname Caller name
-        * @param array $options Options map:
-        *   - maxWriteDuration: abort if more than this much time was spent in write queries
-        */
        public function commitAll( $fname = __METHOD__, array $options = [] ) {
                $this->commitMasterChanges( $fname, $options );
                $this->forEachLBCallMethod( 'commitAll', [ $fname ] );
        }
 
-       /**
-        * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set)
-        *
-        * The DBO_TRX setting will be reverted to the default in each of these methods:
-        *   - commitMasterChanges()
-        *   - rollbackMasterChanges()
-        *   - commitAll()
-        *
-        * This allows for custom transaction rounds from any outer transaction scope.
-        *
-        * @param string $fname
-        * @throws DBTransactionError
-        * @since 1.28
-        */
        public function beginMasterChanges( $fname = __METHOD__ ) {
                if ( $this->trxRoundId !== false ) {
                        throw new DBTransactionError(
@@ -282,13 +171,6 @@ abstract class LBFactory {
                $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
        }
 
-       /**
-        * Commit changes on all master connections
-        * @param string $fname Caller name
-        * @param array $options Options map:
-        *   - maxWriteDuration: abort if more than this much time was spent in write queries
-        * @throws Exception
-        */
        public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) {
                if ( $this->trxRoundId !== false && $this->trxRoundId !== $fname ) {
                        throw new DBTransactionError(
@@ -320,11 +202,6 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * Rollback changes on all master connections
-        * @param string $fname Caller name
-        * @since 1.23
-        */
        public function rollbackMasterChanges( $fname = __METHOD__ ) {
                $this->trxRoundId = false;
                $this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' );
@@ -358,11 +235,6 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * Determine if any master connection has pending changes
-        * @return bool
-        * @since 1.23
-        */
        public function hasMasterChanges() {
                $ret = false;
                $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) {
@@ -372,11 +244,6 @@ abstract class LBFactory {
                return $ret;
        }
 
-       /**
-        * Detemine if any lagged replica DB connection was used
-        * @return bool
-        * @since 1.28
-        */
        public function laggedReplicaUsed() {
                $ret = false;
                $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) {
@@ -386,12 +253,6 @@ abstract class LBFactory {
                return $ret;
        }
 
-       /**
-        * Determine if any master connection has pending/written changes from this request
-        * @param float $age How many seconds ago is "recent" [defaults to LB lag wait timeout]
-        * @return bool
-        * @since 1.27
-        */
        public function hasOrMadeRecentMasterChanges( $age = null ) {
                $ret = false;
                $this->forEachLB( function ( ILoadBalancer $lb ) use ( $age, &$ret ) {
@@ -400,30 +261,6 @@ abstract class LBFactory {
                return $ret;
        }
 
-       /**
-        * Waits for the replica DBs to catch up to the current master position
-        *
-        * Use this when updating very large numbers of rows, as in maintenance scripts,
-        * to avoid causing too much lag. Of course, this is a no-op if there are no replica DBs.
-        *
-        * By default this waits on all DB clusters actually used in this request.
-        * This makes sense when lag being waiting on is caused by the code that does this check.
-        * In that case, setting "ifWritesSince" can avoid the overhead of waiting for clusters
-        * that were not changed since the last wait check. To forcefully wait on a specific cluster
-        * for a given domain, use the 'domain' parameter. To forcefully wait on an "external" cluster,
-        * use the "cluster" parameter.
-        *
-        * Never call this function after a large DB write that is *still* in a transaction.
-        * It only makes sense to call this after the possible lag inducing changes were committed.
-        *
-        * @param array $opts Optional fields that include:
-        *   - domain : wait on the load balancer DBs that handles the given domain ID
-        *   - cluster : wait on the given external load balancer DBs
-        *   - timeout : Max wait time. Default: ~60 seconds
-        *   - ifWritesSince: Only wait if writes were done since this UNIX timestamp
-        * @throws DBReplicationWaitError If a timeout or error occured waiting on a DB cluster
-        * @since 1.27
-        */
        public function waitForReplication( array $opts = [] ) {
                $opts += [
                        'domain' => false,
@@ -493,15 +330,6 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * Add a callback to be run in every call to waitForReplication() before waiting
-        *
-        * Callbacks must clear any transactions that they start
-        *
-        * @param string $name Callback name
-        * @param callable|null $callback Use null to unset a callback
-        * @since 1.28
-        */
        public function setWaitForReplicationListener( $name, callable $callback = null ) {
                if ( $callback ) {
                        $this->replicationWaitCallbacks[$name] = $callback;
@@ -510,36 +338,22 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * Get a token asserting that no transaction writes are active
-        *
-        * @param string $fname Caller name (e.g. __METHOD__)
-        * @return mixed A value to pass to commitAndWaitForReplication()
-        * @since 1.28
-        */
        public function getEmptyTransactionTicket( $fname ) {
                if ( $this->hasMasterChanges() ) {
-                       $this->queryLogger->error( __METHOD__ . ": $fname does not have outer scope." );
+                       $this->queryLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
+                               ( new RuntimeException() )->getTraceAsString() );
+
                        return null;
                }
 
                return $this->ticket;
        }
 
-       /**
-        * Convenience method for safely running commitMasterChanges()/waitForReplication()
-        *
-        * This will commit and wait unless $ticket indicates it is unsafe to do so
-        *
-        * @param string $fname Caller name (e.g. __METHOD__)
-        * @param mixed $ticket Result of getEmptyTransactionTicket()
-        * @param array $opts Options to waitForReplication()
-        * @throws DBReplicationWaitError
-        * @since 1.28
-        */
        public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) {
                if ( $ticket !== $this->ticket ) {
-                       $this->perfLogger->error( __METHOD__ . ": $fname does not have outer scope." );
+                       $this->perfLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
+                               ( new RuntimeException() )->getTraceAsString() );
+
                        return;
                }
 
@@ -561,22 +375,10 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * @param string $dbName DB master name (e.g. "db1052")
-        * @return float|bool UNIX timestamp when client last touched the DB or false if not recent
-        * @since 1.28
-        */
        public function getChronologyProtectorTouched( $dbName ) {
                return $this->getChronologyProtector()->getTouched( $dbName );
        }
 
-       /**
-        * Disable the ChronologyProtector for all load balancers
-        *
-        * This can be called at the start of special API entry points
-        *
-        * @since 1.27
-        */
        public function disableChronologyProtection() {
                $this->getChronologyProtector()->setEnabled( false );
        }
@@ -677,12 +479,6 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * Set a new table prefix for the existing local domain ID for testing
-        *
-        * @param string $prefix
-        * @since 1.28
-        */
        public function setDomainPrefix( $prefix ) {
                $this->localDomain = new DatabaseDomain(
                        $this->localDomain->getDatabase(),
@@ -695,32 +491,14 @@ abstract class LBFactory {
                } );
        }
 
-       /**
-        * Close all open database connections on all open load balancers.
-        * @since 1.28
-        */
        public function closeAll() {
                $this->forEachLBCallMethod( 'closeAll', [] );
        }
 
-       /**
-        * @param string $agent Agent name for query profiling
-        * @since 1.28
-        */
        public function setAgentName( $agent ) {
                $this->agent = $agent;
        }
 
-       /**
-        * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed
-        *
-        * Note that unlike cookies, this works accross domains
-        *
-        * @param string $url
-        * @param float $time UNIX timestamp just before shutdown() was called
-        * @return string
-        * @since 1.28
-        */
        public function appendPreShutdownTimeAsQuery( $url, $time ) {
                $usedCluster = 0;
                $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
@@ -734,13 +512,6 @@ abstract class LBFactory {
                return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
        }
 
-       /**
-        * @param array $info Map of fields, including:
-        *   - IPAddress : IP address
-        *   - UserAgent : User-Agent HTTP header
-        *   - ChronologyProtection : cookie/header value specifying ChronologyProtector usage
-        * @since 1.28
-        */
        public function setRequestInfo( array $info ) {
                $this->requestInfo = $info + $this->requestInfo;
        }
index 25e1fe0..2fb8c4b 100644 (file)
@@ -261,7 +261,7 @@ class LBFactoryMulti extends LBFactory {
         * @throws InvalidArgumentException
         * @return LoadBalancer
         */
-       protected function newExternalLB( $cluster, $domain = false ) {
+       public function newExternalLB( $cluster, $domain = false ) {
                if ( !isset( $this->externalLoads[$cluster] ) ) {
                        throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
                }
@@ -359,7 +359,7 @@ class LBFactoryMulti extends LBFactory {
                        }
                        $serverInfo['hostName'] = $serverName;
                        $serverInfo['load'] = $load;
-                       $serverInfo += [ 'flags' => DBO_DEFAULT ];
+                       $serverInfo += [ 'flags' => IDatabase::DBO_DEFAULT ];
 
                        $servers[] = $serverInfo;
                }
index 4ed4347..610052f 100644 (file)
@@ -97,7 +97,7 @@ class LBFactorySimple extends LBFactory {
         * @return LoadBalancer
         * @throws InvalidArgumentException
         */
-       protected function newExternalLB( $cluster, $domain = false ) {
+       public function newExternalLB( $cluster, $domain = false ) {
                if ( !isset( $this->externalClusters[$cluster] ) ) {
                        throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"." );
                }
index 4beb5d8..af4a350 100644 (file)
@@ -73,7 +73,7 @@ class LBFactorySingle extends LBFactory {
         * @param bool|string $wiki Wiki ID, or false for the current wiki
         * @return LoadBalancerSingle
         */
-       protected function newExternalLB( $cluster, $wiki = false ) {
+       public function newExternalLB( $cluster, $wiki = false ) {
                return $this->lb;
        }
 
index 3b2479f..aa7d1b4 100644 (file)
@@ -60,9 +60,9 @@
  *   - Read-only archive clones: set 'is static' in the server configuration maps. This will
  *      treat all such DBs as having 0 lag.
  *   - SQL load balancing proxy: any proxy should handle lag checks on its own, so the 'max lag'
- *     parameter should probably be set to INF in the server configuration maps. This will make
- *     the load balancer ignore whatever it detects as the lag of the logical replica is (which
- *     would probably just randomly bounce around).
+ *      parameter should probably be set to INF in the server configuration maps. This will make
+ *      the load balancer ignore whatever it detects as the lag of the logical replica is (which
+ *      would probably just randomly bounce around).
  *
  * If using a SQL proxy service, it would probably be best to have two proxy hosts for the
  * load balancer to talk to. One would be the 'host' of the master server entry and another for
  * @ingroup Database
  */
 interface ILoadBalancer {
+       /** @var integer Request a replica DB connection */
+       const DB_REPLICA = -1;
+       /** @var integer Request a master DB connection */
+       const DB_MASTER = -2;
+
        /**
         * Construct a manager of IDatabase connection objects
         *
@@ -211,6 +216,7 @@ interface ILoadBalancer {
         * @param int $i Server index
         * @param string|bool $domain Domain ID, or false for the current domain
         * @return IDatabase|bool Returns false on errors
+        * @throws DBAccessError
         */
        public function openConnection( $i, $domain = false );
 
index bda185a..36e4c6c 100644 (file)
@@ -516,6 +516,15 @@ class LoadBalancer implements ILoadBalancer {
                return $ok;
        }
 
+       /**
+        * @see ILoadBalancer::getConnection()
+        *
+        * @param int $i
+        * @param array $groups
+        * @param bool $domain
+        * @return Database
+        * @throws DBConnectionError
+        */
        public function getConnection( $i, $groups = [], $domain = false ) {
                if ( $i === null || $i === false ) {
                        throw new InvalidArgumentException( 'Attempt to call ' . __METHOD__ .
@@ -530,10 +539,10 @@ class LoadBalancer implements ILoadBalancer {
                        ? [ false ] // check one "group": the generic pool
                        : (array)$groups;
 
-               $masterOnly = ( $i == DB_MASTER || $i == $this->getWriterIndex() );
+               $masterOnly = ( $i == self::DB_MASTER || $i == $this->getWriterIndex() );
                $oldConnsOpened = $this->connsOpened; // connections open now
 
-               if ( $i == DB_MASTER ) {
+               if ( $i == self::DB_MASTER ) {
                        $i = $this->getWriterIndex();
                } else {
                        # Try to find an available server in any the query groups (in order)
@@ -547,7 +556,7 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                # Operation-based index
-               if ( $i == DB_REPLICA ) {
+               if ( $i == self::DB_REPLICA ) {
                        $this->mLastError = 'Unknown error'; // reset error string
                        # Try the general server pool if $groups are unavailable.
                        $i = in_array( false, $groups, true )
@@ -556,15 +565,18 @@ class LoadBalancer implements ILoadBalancer {
                        # Couldn't find a working server in getReaderIndex()?
                        if ( $i === false ) {
                                $this->mLastError = 'No working replica DB server: ' . $this->mLastError;
-
-                               return $this->reportConnectionError();
+                               // Throw an exception
+                               $this->reportConnectionError();
+                               return null; // not reached
                        }
                }
 
                # Now we have an explicit index into the servers array
                $conn = $this->openConnection( $i, $domain );
                if ( !$conn ) {
-                       return $this->reportConnectionError();
+                       // Throw an exception
+                       $this->reportConnectionError();
+                       return null; // not reached
                }
 
                # Profile any new connections that happen
@@ -589,7 +601,7 @@ class LoadBalancer implements ILoadBalancer {
                        /**
                         * This can happen in code like:
                         *   foreach ( $dbs as $db ) {
-                        *     $conn = $lb->getConnection( DB_REPLICA, [], $db );
+                        *     $conn = $lb->getConnection( $lb::DB_REPLICA, [], $db );
                         *     ...
                         *     $lb->reuseConnection( $conn );
                         *   }
@@ -637,6 +649,14 @@ class LoadBalancer implements ILoadBalancer {
                return new DBConnRef( $this, [ $db, $groups, $domain ] );
        }
 
+       /**
+        * @see ILoadBalancer::openConnection()
+        *
+        * @param int $i
+        * @param bool $domain
+        * @return bool|Database
+        * @throws DBAccessError
+        */
        public function openConnection( $i, $domain = false ) {
                if ( $this->localDomain->equals( $domain ) || $domain === $this->localDomainIdAlias ) {
                        $domain = false; // local connection requested
@@ -691,7 +711,7 @@ class LoadBalancer implements ILoadBalancer {
         *
         * @param int $i Server index
         * @param string $domain Domain ID to open
-        * @return IDatabase
+        * @return Database
         */
        private function openForeignConnection( $i, $domain ) {
                $domainInstance = DatabaseDomain::newFromId( $domain );
@@ -775,7 +795,7 @@ class LoadBalancer implements ILoadBalancer {
         *
         * @param array $server
         * @param string|bool $dbNameOverride Use "" to not select any database
-        * @return IDatabase
+        * @return Database
         * @throws DBAccessError
         * @throws InvalidArgumentException
         */
@@ -816,7 +836,7 @@ class LoadBalancer implements ILoadBalancer {
                $server['agent'] = $this->agent;
                // Use DBO_DEFAULT flags by default for LoadBalancer managed databases. Assume that the
                // application calls LoadBalancer::commitMasterChanges() before the PHP script completes.
-               $server['flags'] = isset( $server['flags'] ) ? $server['flags'] : DBO_DEFAULT;
+               $server['flags'] = isset( $server['flags'] ) ? $server['flags'] : IDatabase::DBO_DEFAULT;
 
                // Create a live connection object
                try {
@@ -829,7 +849,7 @@ class LoadBalancer implements ILoadBalancer {
 
                $db->setLBInfo( $server );
                $db->setLazyMasterHandle(
-                       $this->getLazyConnectionRef( DB_MASTER, [], $db->getDomainID() )
+                       $this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() )
                );
                $db->setTableAliases( $this->tableAliases );
 
@@ -847,10 +867,9 @@ class LoadBalancer implements ILoadBalancer {
 
        /**
         * @throws DBConnectionError
-        * @return bool
         */
        private function reportConnectionError() {
-               $conn = $this->mErrorConnection; // The connection which caused the error
+               $conn = $this->mErrorConnection; // the connection which caused the error
                $context = [
                        'method' => __METHOD__,
                        'last_error' => $this->mLastError,
@@ -875,8 +894,6 @@ class LoadBalancer implements ILoadBalancer {
                        // throws DBConnectionError
                        $conn->reportConnectionError( "{$this->mLastError} ({$context['db_server']})" );
                }
-
-               return false; /* not reached */
        }
 
        public function getWriterIndex() {
@@ -928,7 +945,7 @@ class LoadBalancer implements ILoadBalancer {
                        for ( $i = 1; $i < $serverCount; $i++ ) {
                                $conn = $this->getAnyOpenConnection( $i );
                                if ( $conn ) {
-                                       return $conn->getSlavePos();
+                                       return $conn->getReplicaPos();
                                }
                        }
                } else {
@@ -1170,10 +1187,10 @@ class LoadBalancer implements ILoadBalancer {
         * @param IDatabase $conn
         */
        private function applyTransactionRoundFlags( IDatabase $conn ) {
-               if ( $conn->getFlag( DBO_DEFAULT ) ) {
+               if ( $conn->getFlag( $conn::DBO_DEFAULT ) ) {
                        // DBO_TRX is controlled entirely by CLI mode presence with DBO_DEFAULT.
                        // Force DBO_TRX even in CLI mode since a commit round is expected soon.
-                       $conn->setFlag( DBO_TRX, $conn::REMEMBER_PRIOR );
+                       $conn->setFlag( $conn::DBO_TRX, $conn::REMEMBER_PRIOR );
                        // If config has explicitly requested DBO_TRX be either on or off by not
                        // setting DBO_DEFAULT, then respect that. Forcing no transactions is useful
                        // for things like blob stores (ExternalStore) which want auto-commit mode.
@@ -1184,7 +1201,7 @@ class LoadBalancer implements ILoadBalancer {
         * @param IDatabase $conn
         */
        private function undoTransactionRoundFlags( IDatabase $conn ) {
-               if ( $conn->getFlag( DBO_DEFAULT ) ) {
+               if ( $conn->getFlag( $conn::DBO_DEFAULT ) ) {
                        $conn->restoreFlags( $conn::RESTORE_PRIOR );
                }
        }
@@ -1238,7 +1255,7 @@ class LoadBalancer implements ILoadBalancer {
                if ( !$this->laggedReplicaMode && $this->getServerCount() > 1 ) {
                        try {
                                // See if laggedReplicaMode gets set
-                               $conn = $this->getConnection( DB_REPLICA, false, $domain );
+                               $conn = $this->getConnection( self::DB_REPLICA, false, $domain );
                                $this->reuseConnection( $conn );
                        } catch ( DBConnectionError $e ) {
                                // Avoid expensive re-connect attempts and failures
@@ -1305,7 +1322,7 @@ class LoadBalancer implements ILoadBalancer {
                        function () use ( $domain, $conn ) {
                                $this->trxProfiler->setSilenced( true );
                                try {
-                                       $dbw = $conn ?: $this->getConnection( DB_MASTER, [], $domain );
+                                       $dbw = $conn ?: $this->getConnection( self::DB_MASTER, [], $domain );
                                        $readOnly = (int)$dbw->serverIsReadOnly();
                                        if ( !$conn ) {
                                                $this->reuseConnection( $dbw );
@@ -1432,7 +1449,7 @@ class LoadBalancer implements ILoadBalancer {
 
                if ( !$pos ) {
                        // Get the current master position
-                       $dbw = $this->getConnection( DB_MASTER );
+                       $dbw = $this->getConnection( self::DB_MASTER );
                        $pos = $dbw->getMasterPos();
                        $this->reuseConnection( $dbw );
                }
diff --git a/includes/libs/redis/RedisConnRef.php b/includes/libs/redis/RedisConnRef.php
new file mode 100644 (file)
index 0000000..f2bb855
--- /dev/null
@@ -0,0 +1,182 @@
+<?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
+ * @author Aaron Schulz
+ */
+use Psr\Log\LoggerInterface;
+use Psr\Log\LoggerAwareInterface;
+
+/**
+ * Helper class to handle automatically marking connectons as reusable (via RAII pattern)
+ *
+ * This class simply wraps the Redis class and can be used the same way
+ *
+ * @ingroup Redis
+ * @since 1.21
+ */
+class RedisConnRef implements LoggerAwareInterface {
+       /** @var RedisConnectionPool */
+       protected $pool;
+       /** @var Redis */
+       protected $conn;
+
+       protected $server; // string
+       protected $lastError; // string
+
+       /**
+        * @var LoggerInterface
+        */
+       protected $logger;
+
+       /**
+        * @param RedisConnectionPool $pool
+        * @param string $server
+        * @param Redis $conn
+        * @param LoggerInterface $logger
+        */
+       public function __construct(
+               RedisConnectionPool $pool, $server, Redis $conn, LoggerInterface $logger
+       ) {
+               $this->pool = $pool;
+               $this->server = $server;
+               $this->conn = $conn;
+               $this->logger = $logger;
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * @return string
+        * @since 1.23
+        */
+       public function getServer() {
+               return $this->server;
+       }
+
+       public function getLastError() {
+               return $this->lastError;
+       }
+
+       public function clearLastError() {
+               $this->lastError = null;
+       }
+
+       public function __call( $name, $arguments ) {
+               $conn = $this->conn; // convenience
+
+               // Work around https://github.com/nicolasff/phpredis/issues/70
+               $lname = strtolower( $name );
+               if ( ( $lname === 'blpop' || $lname == 'brpop' )
+                       && is_array( $arguments[0] ) && isset( $arguments[1] )
+               ) {
+                       $this->pool->resetTimeout( $conn, $arguments[1] + 1 );
+               } elseif ( $lname === 'brpoplpush' && isset( $arguments[2] ) ) {
+                       $this->pool->resetTimeout( $conn, $arguments[2] + 1 );
+               }
+
+               $conn->clearLastError();
+               try {
+                       $res = call_user_func_array( [ $conn, $name ], $arguments );
+                       if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
+                               $this->pool->reauthenticateConnection( $this->server, $conn );
+                               $conn->clearLastError();
+                               $res = call_user_func_array( [ $conn, $name ], $arguments );
+                               $this->logger->info(
+                                       "Used automatic re-authentication for method '$name'.",
+                                       [ 'redis_server' => $this->server ]
+                               );
+                       }
+               } catch ( RedisException $e ) {
+                       $this->pool->resetTimeout( $conn ); // restore
+                       throw $e;
+               }
+
+               $this->lastError = $conn->getLastError() ?: $this->lastError;
+
+               $this->pool->resetTimeout( $conn ); // restore
+
+               return $res;
+       }
+
+       /**
+        * @param string $script
+        * @param array $params
+        * @param int $numKeys
+        * @return mixed
+        * @throws RedisException
+        */
+       public function luaEval( $script, array $params, $numKeys ) {
+               $sha1 = sha1( $script ); // 40 char hex
+               $conn = $this->conn; // convenience
+               $server = $this->server; // convenience
+
+               // Try to run the server-side cached copy of the script
+               $conn->clearLastError();
+               $res = $conn->evalSha( $sha1, $params, $numKeys );
+               // If we got a permission error reply that means that (a) we are not in
+               // multi()/pipeline() and (b) some connection problem likely occurred. If
+               // the password the client gave was just wrong, an exception should have
+               // been thrown back in getConnection() previously.
+               if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
+                       $this->pool->reauthenticateConnection( $server, $conn );
+                       $conn->clearLastError();
+                       $res = $conn->eval( $script, $params, $numKeys );
+                       $this->logger->info(
+                               "Used automatic re-authentication for Lua script '$sha1'.",
+                               [ 'redis_server' => $server ]
+                       );
+               }
+               // If the script is not in cache, use eval() to retry and cache it
+               if ( preg_match( '/^NOSCRIPT/', $conn->getLastError() ) ) {
+                       $conn->clearLastError();
+                       $res = $conn->eval( $script, $params, $numKeys );
+                       $this->logger->info(
+                               "Used eval() for Lua script '$sha1'.",
+                               [ 'redis_server' => $server ]
+                       );
+               }
+
+               if ( $conn->getLastError() ) { // script bug?
+                       $this->logger->error(
+                               'Lua script error on server "{redis_server}": {lua_error}',
+                               [
+                                       'redis_server' => $server,
+                                       'lua_error' => $conn->getLastError()
+                               ]
+                       );
+               }
+
+               $this->lastError = $conn->getLastError() ?: $this->lastError;
+
+               return $res;
+       }
+
+       /**
+        * @param Redis $conn
+        * @return bool
+        */
+       public function isConnIdentical( Redis $conn ) {
+               return $this->conn === $conn;
+       }
+
+       function __destruct() {
+               $this->pool->freeConnection( $this->server, $this->conn );
+       }
+}
diff --git a/includes/libs/redis/RedisConnectionPool.php b/includes/libs/redis/RedisConnectionPool.php
new file mode 100644 (file)
index 0000000..49d09a9
--- /dev/null
@@ -0,0 +1,417 @@
+<?php
+/**
+ * Redis client connection pooling manager.
+ *
+ * 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
+ * @defgroup Redis Redis
+ * @author Aaron Schulz
+ */
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Helper class to manage Redis connections.
+ *
+ * This can be used to get handle wrappers that free the handle when the wrapper
+ * leaves scope. The maximum number of free handles (connections) is configurable.
+ * This provides an easy way to cache connection handles that may also have state,
+ * such as a handle does between multi() and exec(), and without hoarding connections.
+ * The wrappers use PHP magic methods so that calling functions on them calls the
+ * function of the actual Redis object handle.
+ *
+ * @ingroup Redis
+ * @since 1.21
+ */
+class RedisConnectionPool implements LoggerAwareInterface {
+       /** @var string Connection timeout in seconds */
+       protected $connectTimeout;
+       /** @var string Read timeout in seconds */
+       protected $readTimeout;
+       /** @var string Plaintext auth password */
+       protected $password;
+       /** @var bool Whether connections persist */
+       protected $persistent;
+       /** @var int Serializer to use (Redis::SERIALIZER_*) */
+       protected $serializer;
+
+       /** @var int Current idle pool size */
+       protected $idlePoolSize = 0;
+
+       /** @var array (server name => ((connection info array),...) */
+       protected $connections = [];
+       /** @var array (server name => UNIX timestamp) */
+       protected $downServers = [];
+
+       /** @var array (pool ID => RedisConnectionPool) */
+       protected static $instances = [];
+
+       /** integer; seconds to cache servers as "down". */
+       const SERVER_DOWN_TTL = 30;
+
+       /**
+        * @var LoggerInterface
+        */
+       protected $logger;
+
+       /**
+        * @param array $options
+        * @throws Exception
+        */
+       protected function __construct( array $options ) {
+               if ( !class_exists( 'Redis' ) ) {
+                       throw new RuntimeException(
+                               __CLASS__ . ' requires a Redis client library. ' .
+                               'See https://www.mediawiki.org/wiki/Redis#Setup' );
+               }
+               $this->logger = isset( $options['logger'] )
+                       ? $options['logger']
+                       : new \Psr\Log\NullLogger();
+               $this->connectTimeout = $options['connectTimeout'];
+               $this->readTimeout = $options['readTimeout'];
+               $this->persistent = $options['persistent'];
+               $this->password = $options['password'];
+               if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) {
+                       $this->serializer = Redis::SERIALIZER_PHP;
+               } elseif ( $options['serializer'] === 'igbinary' ) {
+                       $this->serializer = Redis::SERIALIZER_IGBINARY;
+               } elseif ( $options['serializer'] === 'none' ) {
+                       $this->serializer = Redis::SERIALIZER_NONE;
+               } else {
+                       throw new InvalidArgumentException( "Invalid serializer specified." );
+               }
+       }
+
+       /**
+        * @param LoggerInterface $logger
+        * @return null
+        */
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * @param array $options
+        * @return array
+        */
+       protected static function applyDefaultConfig( array $options ) {
+               if ( !isset( $options['connectTimeout'] ) ) {
+                       $options['connectTimeout'] = 1;
+               }
+               if ( !isset( $options['readTimeout'] ) ) {
+                       $options['readTimeout'] = 1;
+               }
+               if ( !isset( $options['persistent'] ) ) {
+                       $options['persistent'] = false;
+               }
+               if ( !isset( $options['password'] ) ) {
+                       $options['password'] = null;
+               }
+
+               return $options;
+       }
+
+       /**
+        * @param array $options
+        * $options include:
+        *   - connectTimeout : The timeout for new connections, in seconds.
+        *                      Optional, default is 1 second.
+        *   - readTimeout    : The timeout for operation reads, in seconds.
+        *                      Commands like BLPOP can fail if told to wait longer than this.
+        *                      Optional, default is 1 second.
+        *   - persistent     : Set this to true to allow connections to persist across
+        *                      multiple web requests. False by default.
+        *   - password       : The authentication password, will be sent to Redis in clear text.
+        *                      Optional, if it is unspecified, no AUTH command will be sent.
+        *   - serializer     : Set to "php", "igbinary", or "none". Default is "php".
+        * @return RedisConnectionPool
+        */
+       public static function singleton( array $options ) {
+               $options = self::applyDefaultConfig( $options );
+               // Map the options to a unique hash...
+               ksort( $options ); // normalize to avoid pool fragmentation
+               $id = sha1( serialize( $options ) );
+               // Initialize the object at the hash as needed...
+               if ( !isset( self::$instances[$id] ) ) {
+                       self::$instances[$id] = new self( $options );
+               }
+
+               return self::$instances[$id];
+       }
+
+       /**
+        * Destroy all singleton() instances
+        * @since 1.27
+        */
+       public static function destroySingletons() {
+               self::$instances = [];
+       }
+
+       /**
+        * Get a connection to a redis server. Based on code in RedisBagOStuff.php.
+        *
+        * @param string $server A hostname/port combination or the absolute path of a UNIX socket.
+        *                       If a hostname is specified but no port, port 6379 will be used.
+        * @param LoggerInterface $logger PSR-3 logger intance. [optional]
+        * @return RedisConnRef|bool Returns false on failure
+        * @throws MWException
+        */
+       public function getConnection( $server, LoggerInterface $logger = null ) {
+               $logger = $logger ?: $this->logger;
+               // Check the listing "dead" servers which have had a connection errors.
+               // Servers are marked dead for a limited period of time, to
+               // avoid excessive overhead from repeated connection timeouts.
+               if ( isset( $this->downServers[$server] ) ) {
+                       $now = time();
+                       if ( $now > $this->downServers[$server] ) {
+                               // Dead time expired
+                               unset( $this->downServers[$server] );
+                       } else {
+                               // Server is dead
+                               $logger->debug(
+                                       'Server "{redis_server}" is marked down for another ' .
+                                       ( $this->downServers[$server] - $now ) . 'seconds',
+                                       [ 'redis_server' => $server ]
+                               );
+
+                               return false;
+                       }
+               }
+
+               // Check if a connection is already free for use
+               if ( isset( $this->connections[$server] ) ) {
+                       foreach ( $this->connections[$server] as &$connection ) {
+                               if ( $connection['free'] ) {
+                                       $connection['free'] = false;
+                                       --$this->idlePoolSize;
+
+                                       return new RedisConnRef(
+                                               $this, $server, $connection['conn'], $logger
+                                       );
+                               }
+                       }
+               }
+
+               if ( !$server ) {
+                       throw new InvalidArgumentException(
+                               __CLASS__ . ": invalid configured server \"$server\"" );
+               } elseif ( substr( $server, 0, 1 ) === '/' ) {
+                       // UNIX domain socket
+                       // These are required by the redis extension to start with a slash, but
+                       // we still need to set the port to a special value to make it work.
+                       $host = $server;
+                       $port = 0;
+               } else {
+                       // TCP connection
+                       if ( preg_match( '/^\[(.+)\]:(\d+)$/', $server, $m ) ) {
+                               list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip, port)
+                       } elseif ( preg_match( '/^([^:]+):(\d+)$/', $server, $m ) ) {
+                               list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip or path, port)
+                       } else {
+                               list( $host, $port ) = [ $server, 6379 ]; // (ip or path, port)
+                       }
+               }
+
+               $conn = new Redis();
+               try {
+                       if ( $this->persistent ) {
+                               $result = $conn->pconnect( $host, $port, $this->connectTimeout );
+                       } else {
+                               $result = $conn->connect( $host, $port, $this->connectTimeout );
+                       }
+                       if ( !$result ) {
+                               $logger->error(
+                                       'Could not connect to server "{redis_server}"',
+                                       [ 'redis_server' => $server ]
+                               );
+                               // Mark server down for some time to avoid further timeouts
+                               $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
+
+                               return false;
+                       }
+                       if ( $this->password !== null ) {
+                               if ( !$conn->auth( $this->password ) ) {
+                                       $logger->error(
+                                               'Authentication error connecting to "{redis_server}"',
+                                               [ 'redis_server' => $server ]
+                                       );
+                               }
+                       }
+               } catch ( RedisException $e ) {
+                       $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
+                       $logger->error(
+                               'Redis exception connecting to "{redis_server}"',
+                               [
+                                       'redis_server' => $server,
+                                       'exception' => $e,
+                               ]
+                       );
+
+                       return false;
+               }
+
+               if ( $conn ) {
+                       $conn->setOption( Redis::OPT_READ_TIMEOUT, $this->readTimeout );
+                       $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
+                       $this->connections[$server][] = [ 'conn' => $conn, 'free' => false ];
+
+                       return new RedisConnRef( $this, $server, $conn, $logger );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Mark a connection to a server as free to return to the pool
+        *
+        * @param string $server
+        * @param Redis $conn
+        * @return bool
+        */
+       public function freeConnection( $server, Redis $conn ) {
+               $found = false;
+
+               foreach ( $this->connections[$server] as &$connection ) {
+                       if ( $connection['conn'] === $conn && !$connection['free'] ) {
+                               $connection['free'] = true;
+                               ++$this->idlePoolSize;
+                               break;
+                       }
+               }
+
+               $this->closeExcessIdleConections();
+
+               return $found;
+       }
+
+       /**
+        * Close any extra idle connections if there are more than the limit
+        */
+       protected function closeExcessIdleConections() {
+               if ( $this->idlePoolSize <= count( $this->connections ) ) {
+                       return; // nothing to do (no more connections than servers)
+               }
+
+               foreach ( $this->connections as &$serverConnections ) {
+                       foreach ( $serverConnections as $key => &$connection ) {
+                               if ( $connection['free'] ) {
+                                       unset( $serverConnections[$key] );
+                                       if ( --$this->idlePoolSize <= count( $this->connections ) ) {
+                                               return; // done (no more connections than servers)
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * The redis extension throws an exception in response to various read, write
+        * and protocol errors. Sometimes it also closes the connection, sometimes
+        * not. The safest response for us is to explicitly destroy the connection
+        * object and let it be reopened during the next request.
+        *
+        * @param string $server
+        * @param RedisConnRef $cref
+        * @param RedisException $e
+        * @deprecated since 1.23
+        */
+       public function handleException( $server, RedisConnRef $cref, RedisException $e ) {
+               $this->handleError( $cref, $e );
+       }
+
+       /**
+        * The redis extension throws an exception in response to various read, write
+        * and protocol errors. Sometimes it also closes the connection, sometimes
+        * not. The safest response for us is to explicitly destroy the connection
+        * object and let it be reopened during the next request.
+        *
+        * @param RedisConnRef $cref
+        * @param RedisException $e
+        */
+       public function handleError( RedisConnRef $cref, RedisException $e ) {
+               $server = $cref->getServer();
+               $this->logger->error(
+                       'Redis exception on server "{redis_server}"',
+                       [
+                               'redis_server' => $server,
+                               'exception' => $e,
+                       ]
+               );
+               foreach ( $this->connections[$server] as $key => $connection ) {
+                       if ( $cref->isConnIdentical( $connection['conn'] ) ) {
+                               $this->idlePoolSize -= $connection['free'] ? 1 : 0;
+                               unset( $this->connections[$server][$key] );
+                               break;
+                       }
+               }
+       }
+
+       /**
+        * Re-send an AUTH request to the redis server (useful after disconnects).
+        *
+        * This works around an upstream bug in phpredis. phpredis hides disconnects by transparently
+        * reconnecting, but it neglects to re-authenticate the new connection. To the user of the
+        * phpredis client API this manifests as a seemingly random tendency of connections to lose
+        * their authentication status.
+        *
+        * This method is for internal use only.
+        *
+        * @see https://github.com/nicolasff/phpredis/issues/403
+        *
+        * @param string $server
+        * @param Redis $conn
+        * @return bool Success
+        */
+       public function reauthenticateConnection( $server, Redis $conn ) {
+               if ( $this->password !== null ) {
+                       if ( !$conn->auth( $this->password ) ) {
+                               $this->logger->error(
+                                       'Authentication error connecting to "{redis_server}"',
+                                       [ 'redis_server' => $server ]
+                               );
+
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Adjust or reset the connection handle read timeout value
+        *
+        * @param Redis $conn
+        * @param int $timeout Optional
+        */
+       public function resetTimeout( Redis $conn, $timeout = null ) {
+               $conn->setOption( Redis::OPT_READ_TIMEOUT, $timeout ?: $this->readTimeout );
+       }
+
+       /**
+        * Make sure connections are closed for sanity
+        */
+       function __destruct() {
+               foreach ( $this->connections as $server => &$serverConnections ) {
+                       foreach ( $serverConnections as $key => &$connection ) {
+                               /** @var Redis $conn */
+                               $conn = $connection['conn'];
+                               $conn->close();
+                       }
+               }
+       }
+}
index 7cada84..b02985a 100644 (file)
@@ -224,7 +224,7 @@ class ConvertibleTimestamp {
        }
 
        /**
-        * Calculate the difference between two ConvertableTimestamp objects.
+        * Calculate the difference between two ConvertibleTimestamp objects.
         *
         * @param ConvertibleTimestamp $relativeTo Base time to calculate difference from
         * @return DateInterval|bool The DateInterval object representing the
diff --git a/includes/objectcache/RedisBagOStuff.php b/includes/objectcache/RedisBagOStuff.php
deleted file mode 100644 (file)
index 64cd686..0000000
+++ /dev/null
@@ -1,433 +0,0 @@
-<?php
-/**
- * Object caching using Redis (http://redis.io/).
- *
- * 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
- */
-
-/**
- * Redis-based caching module for redis server >= 2.6.12
- *
- * @note: avoid use of Redis::MULTI transactions for twemproxy support
- */
-class RedisBagOStuff extends BagOStuff {
-       /** @var RedisConnectionPool */
-       protected $redisPool;
-       /** @var array List of server names */
-       protected $servers;
-       /** @var array Map of (tag => server name) */
-       protected $serverTagMap;
-       /** @var bool */
-       protected $automaticFailover;
-
-       /**
-        * Construct a RedisBagOStuff object. Parameters are:
-        *
-        *   - servers: An array of server names. A server name may be a hostname,
-        *     a hostname/port combination or the absolute path of a UNIX socket.
-        *     If a hostname is specified but no port, the standard port number
-        *     6379 will be used. Arrays keys can be used to specify the tag to
-        *     hash on in place of the host/port. Required.
-        *
-        *   - connectTimeout: The timeout for new connections, in seconds. Optional,
-        *     default is 1 second.
-        *
-        *   - persistent: Set this to true to allow connections to persist across
-        *     multiple web requests. False by default.
-        *
-        *   - password: The authentication password, will be sent to Redis in
-        *     clear text. Optional, if it is unspecified, no AUTH command will be
-        *     sent.
-        *
-        *   - automaticFailover: If this is false, then each key will be mapped to
-        *     a single server, and if that server is down, any requests for that key
-        *     will fail. If this is true, a connection failure will cause the client
-        *     to immediately try the next server in the list (as determined by a
-        *     consistent hashing algorithm). True by default. This has the
-        *     potential to create consistency issues if a server is slow enough to
-        *     flap, for example if it is in swap death.
-        * @param array $params
-        */
-       function __construct( $params ) {
-               parent::__construct( $params );
-               $redisConf = [ 'serializer' => 'none' ]; // manage that in this class
-               foreach ( [ 'connectTimeout', 'persistent', 'password' ] as $opt ) {
-                       if ( isset( $params[$opt] ) ) {
-                               $redisConf[$opt] = $params[$opt];
-                       }
-               }
-               $this->redisPool = RedisConnectionPool::singleton( $redisConf );
-
-               $this->servers = $params['servers'];
-               foreach ( $this->servers as $key => $server ) {
-                       $this->serverTagMap[is_int( $key ) ? $server : $key] = $server;
-               }
-
-               if ( isset( $params['automaticFailover'] ) ) {
-                       $this->automaticFailover = $params['automaticFailover'];
-               } else {
-                       $this->automaticFailover = true;
-               }
-
-               $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_NONE;
-       }
-
-       protected function doGet( $key, $flags = 0 ) {
-               list( $server, $conn ) = $this->getConnection( $key );
-               if ( !$conn ) {
-                       return false;
-               }
-               try {
-                       $value = $conn->get( $key );
-                       $result = $this->unserialize( $value );
-               } catch ( RedisException $e ) {
-                       $result = false;
-                       $this->handleException( $conn, $e );
-               }
-
-               $this->logRequest( 'get', $key, $server, $result );
-               return $result;
-       }
-
-       public function set( $key, $value, $expiry = 0, $flags = 0 ) {
-               list( $server, $conn ) = $this->getConnection( $key );
-               if ( !$conn ) {
-                       return false;
-               }
-               $expiry = $this->convertToRelative( $expiry );
-               try {
-                       if ( $expiry ) {
-                               $result = $conn->setex( $key, $expiry, $this->serialize( $value ) );
-                       } else {
-                               // No expiry, that is very different from zero expiry in Redis
-                               $result = $conn->set( $key, $this->serialize( $value ) );
-                       }
-               } catch ( RedisException $e ) {
-                       $result = false;
-                       $this->handleException( $conn, $e );
-               }
-
-               $this->logRequest( 'set', $key, $server, $result );
-               return $result;
-       }
-
-       public function delete( $key ) {
-               list( $server, $conn ) = $this->getConnection( $key );
-               if ( !$conn ) {
-                       return false;
-               }
-               try {
-                       $conn->delete( $key );
-                       // Return true even if the key didn't exist
-                       $result = true;
-               } catch ( RedisException $e ) {
-                       $result = false;
-                       $this->handleException( $conn, $e );
-               }
-
-               $this->logRequest( 'delete', $key, $server, $result );
-               return $result;
-       }
-
-       public function getMulti( array $keys, $flags = 0 ) {
-               $batches = [];
-               $conns = [];
-               foreach ( $keys as $key ) {
-                       list( $server, $conn ) = $this->getConnection( $key );
-                       if ( !$conn ) {
-                               continue;
-                       }
-                       $conns[$server] = $conn;
-                       $batches[$server][] = $key;
-               }
-               $result = [];
-               foreach ( $batches as $server => $batchKeys ) {
-                       $conn = $conns[$server];
-                       try {
-                               $conn->multi( Redis::PIPELINE );
-                               foreach ( $batchKeys as $key ) {
-                                       $conn->get( $key );
-                               }
-                               $batchResult = $conn->exec();
-                               if ( $batchResult === false ) {
-                                       $this->debug( "multi request to $server failed" );
-                                       continue;
-                               }
-                               foreach ( $batchResult as $i => $value ) {
-                                       if ( $value !== false ) {
-                                               $result[$batchKeys[$i]] = $this->unserialize( $value );
-                                       }
-                               }
-                       } catch ( RedisException $e ) {
-                               $this->handleException( $conn, $e );
-                       }
-               }
-
-               $this->debug( "getMulti for " . count( $keys ) . " keys " .
-                       "returned " . count( $result ) . " results" );
-               return $result;
-       }
-
-       /**
-        * @param array $data
-        * @param int $expiry
-        * @return bool
-        */
-       public function setMulti( array $data, $expiry = 0 ) {
-               $batches = [];
-               $conns = [];
-               foreach ( $data as $key => $value ) {
-                       list( $server, $conn ) = $this->getConnection( $key );
-                       if ( !$conn ) {
-                               continue;
-                       }
-                       $conns[$server] = $conn;
-                       $batches[$server][] = $key;
-               }
-
-               $expiry = $this->convertToRelative( $expiry );
-               $result = true;
-               foreach ( $batches as $server => $batchKeys ) {
-                       $conn = $conns[$server];
-                       try {
-                               $conn->multi( Redis::PIPELINE );
-                               foreach ( $batchKeys as $key ) {
-                                       if ( $expiry ) {
-                                               $conn->setex( $key, $expiry, $this->serialize( $data[$key] ) );
-                                       } else {
-                                               $conn->set( $key, $this->serialize( $data[$key] ) );
-                                       }
-                               }
-                               $batchResult = $conn->exec();
-                               if ( $batchResult === false ) {
-                                       $this->debug( "setMulti request to $server failed" );
-                                       continue;
-                               }
-                               foreach ( $batchResult as $value ) {
-                                       if ( $value === false ) {
-                                               $result = false;
-                                       }
-                               }
-                       } catch ( RedisException $e ) {
-                               $this->handleException( $server, $conn, $e );
-                               $result = false;
-                       }
-               }
-
-               return $result;
-       }
-
-       public function add( $key, $value, $expiry = 0 ) {
-               list( $server, $conn ) = $this->getConnection( $key );
-               if ( !$conn ) {
-                       return false;
-               }
-               $expiry = $this->convertToRelative( $expiry );
-               try {
-                       if ( $expiry ) {
-                               $result = $conn->set(
-                                       $key,
-                                       $this->serialize( $value ),
-                                       [ 'nx', 'ex' => $expiry ]
-                               );
-                       } else {
-                               $result = $conn->setnx( $key, $this->serialize( $value ) );
-                       }
-               } catch ( RedisException $e ) {
-                       $result = false;
-                       $this->handleException( $conn, $e );
-               }
-
-               $this->logRequest( 'add', $key, $server, $result );
-               return $result;
-       }
-
-       /**
-        * Non-atomic implementation of incr().
-        *
-        * Probably all callers actually want incr() to atomically initialise
-        * values to zero if they don't exist, as provided by the Redis INCR
-        * command. But we are constrained by the memcached-like interface to
-        * return null in that case. Once the key exists, further increments are
-        * atomic.
-        * @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 ) {
-               list( $server, $conn ) = $this->getConnection( $key );
-               if ( !$conn ) {
-                       return false;
-               }
-               try {
-                       if ( !$conn->exists( $key ) ) {
-                               return null;
-                       }
-                       // @FIXME: on races, the key may have a 0 TTL
-                       $result = $conn->incrBy( $key, $value );
-               } catch ( RedisException $e ) {
-                       $result = false;
-                       $this->handleException( $conn, $e );
-               }
-
-               $this->logRequest( 'incr', $key, $server, $result );
-               return $result;
-       }
-
-       public function changeTTL( $key, $expiry = 0 ) {
-               list( $server, $conn ) = $this->getConnection( $key );
-               if ( !$conn ) {
-                       return false;
-               }
-
-               $expiry = $this->convertToRelative( $expiry );
-               try {
-                       $result = $conn->expire( $key, $expiry );
-               } catch ( RedisException $e ) {
-                       $result = false;
-                       $this->handleException( $conn, $e );
-               }
-
-               $this->logRequest( 'expire', $key, $server, $result );
-               return $result;
-       }
-
-       public function modifySimpleRelayEvent( array $event ) {
-               if ( array_key_exists( 'val', $event ) ) {
-                       $event['val'] = serialize( $event['val'] ); // this class uses PHP serialization
-               }
-
-               return $event;
-       }
-
-       /**
-        * @param mixed $data
-        * @return string
-        */
-       protected function serialize( $data ) {
-               // Serialize anything but integers so INCR/DECR work
-               // Do not store integer-like strings as integers to avoid type confusion (bug 60563)
-               return is_int( $data ) ? $data : serialize( $data );
-       }
-
-       /**
-        * @param string $data
-        * @return mixed
-        */
-       protected function unserialize( $data ) {
-               $int = intval( $data );
-               return $data === (string)$int ? $int : unserialize( $data );
-       }
-
-       /**
-        * Get a Redis object with a connection suitable for fetching the specified key
-        * @param string $key
-        * @return array (server, RedisConnRef) or (false, false)
-        */
-       protected function getConnection( $key ) {
-               $candidates = array_keys( $this->serverTagMap );
-
-               if ( count( $this->servers ) > 1 ) {
-                       ArrayUtils::consistentHashSort( $candidates, $key, '/' );
-                       if ( !$this->automaticFailover ) {
-                               $candidates = array_slice( $candidates, 0, 1 );
-                       }
-               }
-
-               while ( ( $tag = array_shift( $candidates ) ) !== null ) {
-                       $server = $this->serverTagMap[$tag];
-                       $conn = $this->redisPool->getConnection( $server );
-                       if ( !$conn ) {
-                               continue;
-                       }
-
-                       // If automatic failover is enabled, check that the server's link
-                       // to its master (if any) is up -- but only if there are other
-                       // viable candidates left to consider. Also, getMasterLinkStatus()
-                       // does not work with twemproxy, though $candidates will be empty
-                       // by now in such cases.
-                       if ( $this->automaticFailover && $candidates ) {
-                               try {
-                                       if ( $this->getMasterLinkStatus( $conn ) === 'down' ) {
-                                               // If the master cannot be reached, fail-over to the next server.
-                                               // If masters are in data-center A, and replica DBs in data-center B,
-                                               // this helps avoid the case were fail-over happens in A but not
-                                               // to the corresponding server in B (e.g. read/write mismatch).
-                                               continue;
-                                       }
-                               } catch ( RedisException $e ) {
-                                       // Server is not accepting commands
-                                       $this->handleException( $conn, $e );
-                                       continue;
-                               }
-                       }
-
-                       return [ $server, $conn ];
-               }
-
-               $this->setLastError( BagOStuff::ERR_UNREACHABLE );
-
-               return [ false, false ];
-       }
-
-       /**
-        * Check the master link status of a Redis server that is configured as a replica DB.
-        * @param RedisConnRef $conn
-        * @return string|null Master link status (either 'up' or 'down'), or null
-        *  if the server is not a replica DB.
-        */
-       protected function getMasterLinkStatus( RedisConnRef $conn ) {
-               $info = $conn->info();
-               return isset( $info['master_link_status'] )
-                       ? $info['master_link_status']
-                       : null;
-       }
-
-       /**
-        * Log a fatal error
-        * @param string $msg
-        */
-       protected function logError( $msg ) {
-               $this->logger->error( "Redis error: $msg" );
-       }
-
-       /**
-        * The redis extension throws an exception in response to various read, write
-        * and protocol errors. Sometimes it also closes the connection, sometimes
-        * not. The safest response for us is to explicitly destroy the connection
-        * object and let it be reopened during the next request.
-        * @param RedisConnRef $conn
-        * @param Exception $e
-        */
-       protected function handleException( RedisConnRef $conn, $e ) {
-               $this->setLastError( BagOStuff::ERR_UNEXPECTED );
-               $this->redisPool->handleError( $conn, $e );
-       }
-
-       /**
-        * Send information about a single request to the debug log
-        * @param string $method
-        * @param string $key
-        * @param string $server
-        * @param bool $result
-        */
-       public function logRequest( $method, $key, $server, $result ) {
-               $this->debug( "$method $key on $server: " .
-                       ( $result === false ? "failure" : "success" ) );
-       }
-}
index 6d370b0..770ae31 100644 (file)
@@ -2091,10 +2091,11 @@ class Article implements Page {
         * @see WikiPage::doDeleteArticleReal
         */
        public function doDeleteArticleReal(
-               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
+               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
+               $tags = []
        ) {
                return $this->mPage->doDeleteArticleReal(
-                       $reason, $suppress, $u1, $u2, $error, $user
+                       $reason, $suppress, $u1, $u2, $error, $user, $tags
                );
        }
 
index fe0fffc..50c5030 100644 (file)
@@ -2882,12 +2882,14 @@ class WikiPage implements Page, IDBAccessObject {
         * @param bool $u2 Unused
         * @param array|string &$error Array of errors to append to
         * @param User $user The deleting user
+        * @param array $tags Tags to apply to the deletion action
         * @return Status Status object; if successful, $status->value is the log_id of the
         *   deletion log entry. If the page couldn't be deleted because it wasn't
         *   found, $status is a non-fatal 'cannotdelete' error
         */
        public function doDeleteArticleReal(
-               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
+               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
+               $tags = []
        ) {
                global $wgUser, $wgContentHandlerUseDB;
 
@@ -3026,6 +3028,7 @@ class WikiPage implements Page, IDBAccessObject {
                $logEntry->setPerformer( $user );
                $logEntry->setTarget( $logTitle );
                $logEntry->setComment( $reason );
+               $logEntry->setTags( $tags );
                $logid = $logEntry->insert();
 
                $dbw->onTransactionPreCommitOrIdle(
index 5e8db07..5a15ddf 100644 (file)
@@ -18,6 +18,7 @@
  * @file
  * @author Aaron Schulz
  */
+use Psr\Log\LoggerInterface;
 
 /**
  * Version of PoolCounter that uses Redis
@@ -55,6 +56,8 @@ class PoolCounterRedis extends PoolCounter {
        protected $ring;
        /** @var RedisConnectionPool */
        protected $pool;
+       /** @var LoggerInterface */
+       protected $logger;
        /** @var array (server label => host) map */
        protected $serversByLabel;
        /** @var string SHA-1 of the key */
@@ -87,6 +90,7 @@ class PoolCounterRedis extends PoolCounter {
 
                $conf['redisConfig']['serializer'] = 'none'; // for use with Lua
                $this->pool = RedisConnectionPool::singleton( $conf['redisConfig'] );
+               $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'redis' );
 
                $this->keySha1 = sha1( $this->key );
                $met = ini_get( 'max_execution_time' ); // usually 0 in CLI mode
@@ -107,7 +111,7 @@ class PoolCounterRedis extends PoolCounter {
                        $servers = $this->ring->getLocations( $this->key, 3 );
                        ArrayUtils::consistentHashSort( $servers, $this->key );
                        foreach ( $servers as $server ) {
-                               $conn = $this->pool->getConnection( $this->serversByLabel[$server] );
+                               $conn = $this->pool->getConnection( $this->serversByLabel[$server], $this->logger );
                                if ( $conn ) {
                                        break;
                                }
@@ -151,6 +155,7 @@ class PoolCounterRedis extends PoolCounter {
 
                // @codingStandardsIgnoreStart Generic.Files.LineLength
                static $script =
+               /** @lang Lua */
 <<<LUA
                local kSlots,kSlotsNextRelease,kWakeup,kWaiting = unpack(KEYS)
                local rMaxWorkers,rExpiry,rSlot,rSlotTime,rAwakeAll,rTime = unpack(ARGV)
@@ -287,6 +292,7 @@ LUA;
         */
        protected function initAndPopPoolSlotList( RedisConnRef $conn, $now ) {
                static $script =
+               /** @lang Lua */
 <<<LUA
                local kSlots,kSlotsNextRelease,kSlotWaits = unpack(KEYS)
                local rMaxWorkers,rMaxQueue,rTimeout,rExpiry,rSess,rTime = unpack(ARGV)
@@ -355,6 +361,7 @@ LUA;
         */
        protected function registerAcquisitionTime( RedisConnRef $conn, $slot, $now ) {
                static $script =
+               /** @lang Lua */
 <<<LUA
                local kSlots,kSlotsNextRelease,kSlotWaits = unpack(KEYS)
                local rSlot,rExpiry,rSess,rTime = unpack(ARGV)
index 6806ee5..6b5316f 100644 (file)
@@ -14,6 +14,15 @@ interface SearchIndexField {
        const INDEX_TYPE_DATETIME = 4;
        const INDEX_TYPE_NESTED = 5;
        const INDEX_TYPE_BOOL = 6;
+
+       /**
+        * SHORT_TEXT is meant to be used with short text made of mostly ascii
+        * technical information. Generally a language agnostic analysis chain
+        * is used and aggressive splitting to increase recall.
+        * E.g suited for mime/type
+        */
+       const INDEX_TYPE_SHORT_TEXT = 7;
+
        /**
         * Generic field flags.
         */
index 9975e41..ed3cd5e 100644 (file)
@@ -146,19 +146,9 @@ class SpecialBotPasswords extends FormSpecialPage {
                        ];
 
                        $fields['restrictions'] = [
-                               'type' => 'textarea',
-                               'label-message' => 'botpasswords-label-restrictions',
+                               'class' => 'HTMLRestrictionsField',
                                'required' => true,
-                               'default' => $this->botPassword->getRestrictions()->toJson( true ),
-                               'rows' => 5,
-                               'validation-callback' => function ( $v ) {
-                                       try {
-                                               MWRestrictions::newFromJson( $v );
-                                               return true;
-                                       } catch ( InvalidArgumentException $ex ) {
-                                               return $ex->getMessage();
-                                       }
-                               },
+                               'default' => $this->botPassword->getRestrictions(),
                        ];
 
                } else {
@@ -282,7 +272,7 @@ class SpecialBotPasswords extends FormSpecialPage {
                $bp = BotPassword::newUnsaved( [
                        'centralId' => $this->userId,
                        'appId' => $this->par,
-                       'restrictions' => MWRestrictions::newFromJson( $data['restrictions'] ),
+                       'restrictions' => $data['restrictions'],
                        'grants' => array_merge(
                                MWGrants::getHiddenGrants(),
                                preg_replace( '/^grant-/', '', $data['grants'] )
index dd7f0ed..87276a1 100644 (file)
@@ -156,10 +156,20 @@ class SpecialChangeContentModel extends FormSpecialPage {
                }
 
                $this->title = Title::newFromText( $data['pagetitle'] );
+               $titleWithNewContentModel = clone $this->title;
+               $titleWithNewContentModel->setContentModel( $data['model'] );
                $user = $this->getUser();
-               // Check permissions and make sure the user has permission to edit the specific page
-               $errors = $this->title->getUserPermissionsErrors( 'editcontentmodel', $user );
-               $errors = wfMergeErrorArrays( $errors, $this->title->getUserPermissionsErrors( 'edit', $user ) );
+               // Check permissions and make sure the user has permission to:
+               $errors = wfMergeErrorArrays(
+                       // edit the contentmodel of the page
+                       $this->title->getUserPermissionsErrors( 'editcontentmodel', $user ),
+                       // edit the page under the old content model
+                       $this->title->getUserPermissionsErrors( 'edit', $user ),
+                       // edit the contentmodel under the new content model
+                       $titleWithNewContentModel->getUserPermissionsErrors( 'editcontentmodel', $user ),
+                       // edit the page under the new content model
+                       $titleWithNewContentModel->getUserPermissionsErrors( 'edit', $user )
+               );
                if ( $errors ) {
                        $out = $this->getOutput();
                        $wikitext = $out->formatPermissionsErrorMessage( $errors );
index 8aff690..cd3299c 100644 (file)
@@ -159,6 +159,9 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                        if ( preg_match( '/^namespace=(\d+)$/', $bit, $m ) ) {
                                $opts['namespace'] = $m[1];
                        }
+                       if ( preg_match( '/^tagfilter=(.*)$/', $bit, $m ) ) {
+                               $opts['tagfilter'] = $m[1];
+                       }
                }
        }
 
index 617e8f5..caf88a1 100644 (file)
@@ -27,6 +27,7 @@ class MWRestrictions {
 
        /**
         * @param array $restrictions
+        * @throws InvalidArgumentException
         */
        protected function __construct( array $restrictions = null ) {
                if ( $restrictions !== null ) {
@@ -44,6 +45,7 @@ class MWRestrictions {
        /**
         * @param array $restrictions
         * @return MWRestrictions
+        * @throws InvalidArgumentException
         */
        public static function newFromArray( array $restrictions ) {
                return new self( $restrictions );
@@ -52,6 +54,7 @@ class MWRestrictions {
        /**
         * @param string $json JSON representation of the restrictions
         * @return MWRestrictions
+        * @throws InvalidArgumentException
         */
        public static function newFromJson( $json ) {
                $restrictions = FormatJson::decode( $json, true );
index b91596b..ca188ba 100644 (file)
@@ -2,6 +2,7 @@ Authors (alphabetically)
 
 Alex Monk <krenair@wikimedia.org>
 Bartosz Dziewoński <bdziewonski@wikimedia.org>
+Brad Jorsch <bjorsch@wikimedia.org>
 Ed Sanders <esanders@wikimedia.org>
 Florian Schmidt <florian.schmidt.welzow@t-online.de>
 James D. Forrester <jforrester@wikimedia.org>
diff --git a/includes/widget/DateTimeInputWidget.php b/includes/widget/DateTimeInputWidget.php
new file mode 100644 (file)
index 0000000..f0d5cdb
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+/**
+ * MediaWiki Widgets – DateTimeInputWidget class.
+ *
+ * @copyright 2016 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+namespace MediaWiki\Widget;
+
+use OOUI\Tag;
+
+/**
+ * Date-time input widget.
+ */
+class DateTimeInputWidget extends \OOUI\InputWidget {
+
+       protected $type = null;
+       protected $min = null;
+       protected $max = null;
+       protected $clearable = null;
+
+       /**
+        * @param array $config Configuration options
+        * @param string $config['type'] 'date', 'time', or 'datetime'
+        * @param string $config['min'] Minimum date, time, or datetime
+        * @param string $config['max'] Maximum date, time, or datetime
+        * @param bool $config['clearable'] Whether to provide for blanking the value.
+        */
+       public function __construct( array $config = [] ) {
+               // We need $this->type set before calling the parent constructor
+               if ( isset( $config['type'] ) ) {
+                       $this->type = $config['type'];
+               } else {
+                       throw new \InvalidArgumentException( '$config[\'type\'] must be specified' );
+               }
+
+               // Parent constructor
+               parent::__construct( $config );
+
+               // Properties, which are ignored in PHP and just shipped back to JS
+               if ( isset( $config['min'] ) ) {
+                       $this->min = $config['min'];
+               }
+               if ( isset( $config['max'] ) ) {
+                       $this->max = $config['max'];
+               }
+               if ( isset( $config['clearable'] ) ) {
+                       $this->clearable = $config['clearable'];
+               }
+
+               // Initialization
+               $this->addClasses( [ 'mw-widgets-datetime-dateTimeInputWidget' ] );
+       }
+
+       protected function getJavaScriptClassName() {
+               return 'mw.widgets.datetime.DateTimeInputWidget';
+       }
+
+       public function getConfig( &$config ) {
+               $config['type'] = $this->type;
+               if ( $this->min !== null ) {
+                       $config['min'] = $this->min;
+               }
+               if ( $this->max !== null ) {
+                       $config['max'] = $this->max;
+               }
+               if ( $this->clearable !== null ) {
+                       $config['clearable'] = $this->clearable;
+               }
+               return parent::getConfig( $config );
+       }
+
+       protected function getInputElement( $config ) {
+               return ( new Tag( 'input' ) )->setAttributes( [ 'type' => $this->type ] );
+       }
+}
index 699185e..88eb4bf 100644 (file)
        "yourpasswordagain": "Pasoë lom lageuëm rahsia:",
        "createacct-yourpasswordagain": "Peunyo lageuëm rahsia",
        "createacct-yourpasswordagain-ph": "Pasoë lom lageuëm rahsia",
-       "remembermypassword": "Ingat lôn tamöng bak peuramban nyoë (keu paléng trép $1 {{PLURAL:$1|uroë|days}})",
        "userlogin-remembermypassword": "Peubiyeuë lôn tamöng",
        "userlogin-signwithsecure": "Ngui server aman",
        "yourdomainname": "Domain droeneuh:",
        "post-expand-template-inclusion-category": "Laman ngön seunipat seunaleuëk nyang leubèh bataih",
        "post-expand-template-argument-warning": "'''Ingat:''' Laman nyoe na paléng h'an saboh alasan seunaleuëk nyang na sunipat èkspansi nyang raya that.\nAlasan-alasan nyan hana geupeureumeuën.",
        "post-expand-template-argument-category": "Laman ngön dalèh seunaleuëk nyang hana geupeureumeuën",
-       "cantcreateaccounttitle": "Han jeut peugöt nan ureueng ngui",
        "cantcreateaccount-text": "Peuneugöt nan ureueng ngui nibak alamat IP ('''$1''') ka geutheun lé [[User:$3|$3]].\n\nDalèh $3 nyoe nakeuh ''$2''",
        "viewpagelogs": "Eu log laman nyoë",
        "nohistory": "Hana riwayat neuandam awai keu ôn nyoe.",
        "search-interwiki-more": "(lom)",
        "searchrelated": "meusambat",
        "searchall": "ban dum",
+       "search-showingresults": "{{PLURAL:$4|Hasé <strong>$1</strong> nibak <strong>$3</strong>|Hasé <strong>$1 - $2</strong> nibak <strong>$3</strong>}}",
        "search-nonefound": "Hana hasé nyang paih lagèë neulakèë",
        "powersearch-legend": "Mita lanjut",
        "powersearch-ns": "Mita bak ruweuëng nan:",
        "exif-orientation": "Orientasi",
        "exif-xresolution": "Resolusi linteuëng",
        "exif-yresolution": "Rèsolusi buju",
+       "exif-datetime": "Uroë buleuën ngön watèë neuubah beureukaih",
        "exif-software": "Software geungui",
        "exif-exifversion": "Versi Exif",
        "exif-colorspace": "Ruweuëng wareuna",
index ef0da8e..380a919 100644 (file)
        "eauthentsent": "تم إرسال رسالة تأكيد إلكترونية إلى العنوان المسمى.\nقبل إرسال أي رسالة أخرى لذلك الحساب، عليك أن تتبع التعليمات الواردة في الرسالة، لتأكيد أن هذا الحساب هو لك بالفعل.",
        "throttled-mailpassword": "تم بالفعل إرسال تذكير بكلمة السر، في ال{{PLURAL:$1||ساعة الماضية|ساعتين الماضيتين|$1 ساعات الماضية|$1 ساعة الماضية}}.\nلمنع التخريب، سيتم إرسال تذكير واحد كل {{PLURAL:$1||ساعة|ساعتين|$1 ساعات|$1 ساعة}}.",
        "mailerror": "خطأ أثناء إرسال البريد: $1",
-       "acct_creation_throttle_hit": "Ø£Ù\86شأ Ø²Ù\88ار Ù\87Ø°Ù\87 Ø§Ù\84Ù\88Ù\8aÙ\83Ù\8a Ø¨Ø§Ø³ØªØ®Ø¯Ø§Ù\85 Ø¹Ù\86Ù\88اÙ\86 Ø¢Ù\8aبÙ\8aÙ\83 {{PLURAL:$1||حسابا Ù\88احدا|حسابÙ\8aÙ\86|$1 Ø­Ø³Ø§Ø¨Ø§Øª|$1 Ø­Ø³Ø§Ø¨Ø§|$1 Ø­Ø³Ø§Ø¨}} Ù\81Ù\8a Ø§Ù\84Ù\8aÙ\88Ù\85 Ø§Ù\84Ù\85اضÙ\8a، وهو الحد الأقصى المسموح به في هذه الفترة الزمنية.\nوكنتيجة لذلك، لن يتمكن الزوار الذين يستخدمون عنوان الأيبي هذا من إنشاء أي حسابات أخرى حاليا.",
+       "acct_creation_throttle_hit": "Ø£Ù\86شأ Ø²Ù\88ار Ù\87Ø°Ù\87 Ø§Ù\84Ù\88Ù\8aÙ\83Ù\8a Ø¨Ø§Ø³ØªØ®Ø¯Ø§Ù\85 Ø¹Ù\86Ù\88اÙ\86 Ø§Ù\84Ø£Ù\8aبÙ\8a Ø§Ù\84خاص Ø¨Ù\83 {{PLURAL:$1||حسابا Ù\88احدا|حسابÙ\8aÙ\86|$1 Ø­Ø³Ø§Ø¨Ø§Øª|$1 Ø­Ø³Ø§Ø¨Ø§|$1 Ø­Ø³Ø§Ø¨}} Ù\81Ù\8a Ø¢Ø®Ø± $2، وهو الحد الأقصى المسموح به في هذه الفترة الزمنية.\nوكنتيجة لذلك، لن يتمكن الزوار الذين يستخدمون عنوان الأيبي هذا من إنشاء أي حسابات أخرى حاليا.",
        "emailauthenticated": "تم تأكيد بريدك الإلكتروني في $2 الساعة $3.",
        "emailnotauthenticated": "لم يؤكد بريدك الإلكتروني حتى الآن.\nلن يتم إرسال رسائل لأي من الميزات التالية.",
        "noemailprefs": "حدد عنوان بريد إلكتروني في تفضيلاتك لتفعيل هذه الخصائص.",
        "botpasswords-label-resetpassword": "أعد ضبط كلمة السر",
        "botpasswords-label-grants": "المنح التي يمكن تطبيقها:",
        "botpasswords-help-grants": "كل منحة تعطي وصولا لصلاحيات المستخدم المعروضة التي يمتلكها حساب المستخدم بالفعل. انظر [[Special:ListGrants|جدول المنح]] للمزيد من المعلومات.",
-       "botpasswords-label-restrictions": "قيود الاستخدام:",
        "botpasswords-label-grants-column": "الممنوح",
        "botpasswords-bad-appid": "اسم البوت \"$1\" غير صحيح.",
        "botpasswords-insert-failed": "فشل في اضافة  اسم البوت \"$1\".هل اضيف بالفعل؟",
        "passwordreset-emailelement": "اسم {{GENDER:$1\n|المستخدم|المستخدمة}}: \n$1\n\nكلمة السر المؤقتة: \n$2",
        "passwordreset-emailsentemail": "إذا كان هذا العنوان البريد مرتبط بحسابك، من ثم سيتم إرسال بريد إلكتروني لإعادة تعيين كلمة السر.",
        "passwordreset-emailsentusername": "إذا كان هناك عنوان بريد إلكتروني مرتبط بهذا المستخدم، ثم سيتم إرسال بريد إلكتروني لإعادة تعيين كلمة السر.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|رسالة|رسائل}} البريد الإلكتروني لضبط كلمة السر تم إرسالها. {{PLURAL:$1|اسم المستخدم وكلمة السر معروضان|قائمة أسماء المستخدمين وكلمات السر معروضة}} بالأسفل.",
-       "passwordreset-emailerror-capture2": "إرسال بريد إلى {{GENDER:$2|المستخدم|المستخدمة}} فشل: $1 {{PLURAL:$3|اسم المستخدم وكلمة السر معروضان|lقائمة أسماء المستخدمين كلمات السر معروضة}} بالأسفل.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|رسالة|رسائل}} البريد الإلكتروني لضبط كلمة السر تم إرسالها. {{PLURAL:$1|اسم المستخدم وكلمة السر معروضان|قائمة أسماء المستخدمين وكلمات السر معروضة}} هنا.",
+       "passwordreset-emailerror-capture2": "إرسال بريد إلى {{GENDER:$2|المستخدم|المستخدمة}} فشل: $1 {{PLURAL:$3|اسم المستخدم وكلمة السر معروضان|قائمة أسماء المستخدمين وكلمات السر معروضة}} هنا.",
        "passwordreset-nocaller": "يجب أن يتم توفير مستدعي",
        "passwordreset-nosuchcaller": "المستدعي غير موجود: $1",
        "passwordreset-ignored": "إعادة ضبط كلمة السر لم تتم التعامل معها. ربما لا موفر تم ضبطه؟",
        "unlinkaccounts-success": "الحساب تم فك وصله.",
        "authenticationdatachange-ignored": "تغيير بيانات التحقق لم يتم التعامل معه. ربما لم يتم ضبط موفر؟",
        "userjsispublic": "من فضلك لاحظ: صفحات الجافاسكريبت الفرعية لا ينبغي أن تحتوي غلى بيانات سرية بما أنها يمكن رؤيتها بواسطة المستخدمين الآخرين.",
-       "usercssispublic": "من فضل لاحظ: صفحات الCSS الفرعية لا ينبغي أن تحتوي على بيانات سرية بما أنها يمكن رؤيتها بواسطة المستخدمين الآخرين."
+       "usercssispublic": "من فضل لاحظ: صفحات الCSS الفرعية لا ينبغي أن تحتوي على بيانات سرية بما أنها يمكن رؤيتها بواسطة المستخدمين الآخرين.",
+       "restrictionsfield-badip": "عنوان أيبي أو نطاق غير صحيح: $1",
+       "restrictionsfield-label": "نطاقات الأيبي المسموح بها:",
+       "restrictionsfield-help": "عنوان أيبي أو نطاق CIDR واحد لكل سطر. لتفعيل كل شيء، استخدم<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index 0920f68..aab16b4 100644 (file)
        "talk": "Alderique",
        "views": "Vistes",
        "toolbox": "Ferramientes",
+       "tool-link-userrights": "Cambiar los grupos {{GENDER:$1|del usuariu|de la usuaria}}",
+       "tool-link-emailuser": "Unviar un corréu electrónicu a {{GENDER:$1|esti usuariu|esta usuaria}}",
        "userpage": "Ver la páxina d'usuariu",
        "projectpage": "Ver la páxina del proyeutu",
        "imagepage": "Ver la páxina del ficheru",
        "eauthentsent": "Unvióse un corréu electrónicu de confirmación a la direición indicada.\nEnantes de que s'unvie nengún otru corréu a la cuenta, has de siguir les instrucciones d'esi corréu pa confirmar que la cuenta ye daveres de to.",
        "throttled-mailpassword": "Yá s'unvió un corréu de reaniciu la clave {{PLURAL:$1|na postrer hora|nes postreres $1 hores}}.\nPa evitar abusos, namái s'unviará un corréu de reaniciu cada {{PLURAL:$1|hora|$1 hores}}.",
        "mailerror": "Fallu al unviar el corréu: $1",
-       "acct_creation_throttle_hit": "Los visitantes d'esta wiki qu'usen la to direición IP yá crearon güei {{PLURAL:$1|1 cuenta|$1 cuentes}}, que ye'l máximu almitíu nesti periodu de tiempu.\nPoro, los visitantes qu'usen esta direición IP nun puen crear más cuentes de momentu.",
+       "acct_creation_throttle_hit": "Los visitantes d'esta wiki qu'usen la to direición IP yá crearon {{PLURAL:$1|1 cuenta|$1 cuentes}} nel periodu de $2, que ye'l máximu almitíu nesi tiempu.\nPoro, los visitantes qu'usen esta direición IP nun pueden crear más cuentes pol momentu.",
        "emailauthenticated": "La so direición de corréu electrónicu confirmóse'l $2 a les $3.",
        "emailnotauthenticated": "La so direición de corréu electrónicu inda nun se confirmó.\nNun s'unviará corréu pa nenguna de les funciones siguientes.",
        "noemailprefs": "Conseña una direición de corréu electrónicu nes tos preferencies pa que funcionen eses carauterístiques.",
        "botpasswords-label-resetpassword": "Reestablecer la contraseña",
        "botpasswords-label-grants": "Permisos aplicables:",
        "botpasswords-help-grants": "Cada permisu da accesu a los permisos de usuario llistaos que yá tenga la cuenta. Mira la [[Special:ListGrants|tabla de permisos]] pa más información.",
-       "botpasswords-label-restrictions": "Torgues d'usu:",
        "botpasswords-label-grants-column": "Permitío",
        "botpasswords-bad-appid": "El nome del bot \"$1\" nun ye válidu.",
        "botpasswords-insert-failed": "Nun pudo amestase'l nome de bot «$1». ¿Taba añadíu yá?",
        "passwordreset-emailelement": "Nome d'usuariu: \n$1\n\nContraseña temporal: \n$2",
        "passwordreset-emailsentemail": "Si esta direición de corréu electrónicu ta asociada cola to cuenta, unviaráse un corréu pa reaniciar la contraseña.",
        "passwordreset-emailsentusername": "Si hai una direición de corréu electrónicu asociada con esti nome d'usuariu, unviaráse un corréu electrónicu pa reaniciar la contraseña.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Unvióse'l corréu|Unviáronse los correos}} de reaniciu de contraseña. {{PLURAL:$1|El nome d'usuariu y la contraseña|La llista de nomes d'usuarios y contraseñes}} amuésase de siguío.",
-       "passwordreset-emailerror-capture2": "Nun foi posible mandar un corréu electrónicu {{Gender:$2|al usuariu|a la usuaria}}: $1 {{PLURAL:$3|El nome d'usuariu y la contraseña|La llista de nomes d'usuarios y contraseñes}} amuésase de siguío.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Unvióse'l corréu|Unviáronse los correos}} de reaniciu de contraseña. {{PLURAL:$1|El nome d'usuariu y la contraseña|La llista de nomes d'usuarios y contraseñes}} amuésase equí.",
+       "passwordreset-emailerror-capture2": "Nun pudo unviase un corréu electrónicu {{GENDER:$2|al usuariu|a la usuaria}}: $1 {{PLURAL:$3|El nome d'usuariu y la contraseña|La llista de nomes d'usuarios y contraseñes}} amuésase equí.",
        "passwordreset-nocaller": "Tien d'apurrise un llamador",
        "passwordreset-nosuchcaller": "El llamador nun esiste: $1",
        "passwordreset-ignored": "Nun se llogró'l reaniciu de la contraseña. ¿Seique nun se configuró un proveedor?",
index 1dd05c7..7a4ccf1 100644 (file)
        "yourpasswordagain": "Серһүҙҙе ҡабаттан яҙыу",
        "createacct-yourpasswordagain": "Серһүҙҙе раҫлағыҙ",
        "createacct-yourpasswordagain-ph": "Серһүҙҙе тағы бер тапҡыр яҙығыҙ",
-       "remembermypassword": "Был браузерҙа (иң күбендә $1 {{PLURAL:$1|көнгә}}) иҫәп яҙыуым хәтерләнһен",
        "userlogin-remembermypassword": "Системала ҡалырға",
        "userlogin-signwithsecure": "Һаҡланыулы тоташыу",
        "cannotloginnow-title": "Хәҙер үк инеп булмай",
        "botpasswords-label-resetpassword": "Серһүҙҙе ташлатыу",
        "botpasswords-label-grants": "Ҡулланылған рөхсәттәр:",
        "botpasswords-help-grants": "Һәр рөхсәт иҫәп яҙмаһы булған ҡулланыусы хоҡуҡтарын ҡулланырға рөхсәт бирә. Тулыраҡ мәғлүмәт өсөн [[Special:ListGrants|рөхсәт таблицаһын]] ҡарағыҙ.",
-       "botpasswords-label-restrictions": "Ҡулланыуҙы сикләү:",
        "botpasswords-label-grants-column": "Рөхсәт",
        "botpasswords-bad-appid": "$1 исемле робот ярамай.",
        "botpasswords-insert-failed": "$1 исемле роботты өҫтәп булманы. Бәлки өҫтәлгән булғандыр?",
        "undo-failure": "Ара үҙгәртеүҙәр тура килмәү сәбәпле төҙәтеүҙе кире алып булмай.",
        "undo-norev": "Үҙгәртеүҙе кире алып булмай, сөнки юҡ йәки юйылған.",
        "undo-nochange": "Төҙәтеү кире ҡайтарылған.",
-       "undo-summary": "[[Special:Contributions/$2|$2]] ҡулланыусыһының ([[User talk:$2|фекер алышыу]]) $1 үҙгәртеүенән баш тартыу",
+       "undo-summary": "Ҡулланыусы [[Special:Contributions/$2|$2]] ([[User talk:$2|фекер алышыу]]) $1 үҙгәртеүенән баш тартты",
        "undo-summary-username-hidden": "Исеме йәшерелгән ҡатнашыусының төҙәтеүен  $1 кире ҡағыу",
        "cantcreateaccount-text": "Был IP-адрестан (<b>$1</b>) иҫәп яҙыуҙары булдырыу [[User:$3|$3]] тарафынан тыйылған.\n\n$3 белдергән сәбәп: ''$2''",
        "cantcreateaccount-range-text": "{{GENDER:$3|Ҡатнашыусы}} [[User:$3|$3]] һеҙҙең IP-адрес ингән (<strong>$4</strong>) <strong>$1</strong> диапозонында иҫәп яҙмаһын булдырмаҫҡа {{GENDER:$3|тыйыу}} ҡуйҙы.\n\nОшо сәбәп күһәтелгән: $2.",
        "htmlform-title-not-exists": "$1 юҡ",
        "htmlform-user-not-exists": "<strong>$1</strong> ғәмәлдә юҡ",
        "htmlform-user-not-valid": "<strong>$1</strong> — ярамаған иҫәп яҙмаһы",
-       "sqlite-has-fts": "$1, тулы текст буйынса эҙләү мөмкинлеге менән",
-       "sqlite-no-fts": "$1, тулы текст буйынса эҙләү мөмкинлекһеҙ",
        "logentry-delete-delete": "$1 $3 битен {{GENDER:$2|юйҙы}}",
        "logentry-delete-restore": "$1 $3 битен {{GENDER:$2|тергеҙҙе}}",
        "logentry-delete-event": "$1 журналдағы {{PLURAL:$5|яҙманы}} $3: $4 {{GENDER:$2|үҙгәртте}}",
index 2a75b5c..894c727 100644 (file)
        "eauthentsent": "Пацьверджаньне было дасланае на пазначаны адрас электроннай пошты.\nУ лісьце ўтрымліваюцца інструкцыі, па выкананьні якіх Вы зможаце пацьвердзіць, што адрас сапраўды належыць Вам, і на гэты адрас будзе дасылацца пошта адсюль.",
        "throttled-mailpassword": "Ліст пра скіданьне паролю ўжо быў дасланы за $1 {{PLURAL:$1|апошнюю гадзіну|апошнія гадзіны|апошніх гадзінаў}}.\nКаб пазьбегнуць злоўжываньняў напамін будзе дасылацца не часьцей як аднойчы за $1 {{PLURAL:$1|гадзіну|гадзіны|гадзінаў}}.",
        "mailerror": "Памылка пры адпраўцы электроннай пошты: $1",
-       "acct_creation_throttle_hit": "Ð\9dаведвалÑ\8cнÑ\96кÑ\96 Ð³Ñ\8dÑ\82ай Ð²Ñ\96кÑ\96, Ñ\8fкÑ\96Ñ\8f ÐºÐ°Ñ\80Ñ\8bÑ\81Ñ\82алÑ\96Ñ\81Ñ\8f Ð\92аÑ\88Ñ\8bм Ð\86Р-адÑ\80аÑ\81ам, Ñ\83жо Ñ\81Ñ\82ваÑ\80Ñ\8bлÑ\96 $1 {{PLURAL:$1|Ñ\80аÑ\85Ñ\83нак Ñ\83\80аÑ\85Ñ\83нкÑ\96 Ñ\9e\80аÑ\85Ñ\83нкаÑ\9e Ñ\83}} Ð°Ð¿Ð¾Ñ\88нÑ\96Ñ\8f Ð´Ð½Ñ\96, Ñ\88Ñ\82о Ð¿ÐµÑ\80авÑ\8bÑ\88ае Ð¼Ð°ÐºÑ\81Ñ\8bмалÑ\8cнÑ\83Ñ\8e Ð´Ð°Ð·Ð²Ð¾Ð»ÐµÐ½Ñ\83Ñ\8e ÐºÐ¾Ð»Ñ\8cкаÑ\81Ñ\8cÑ\86Ñ\8c Ð·Ð° Ð³Ñ\8dÑ\82Ñ\8b Ð¿Ñ\8dÑ\80Ñ\8bÑ\8fд.\nУ Ð²Ñ\8bнÑ\96кÑ\83, Ð½Ð°Ð²ÐµÐ´Ð²Ð°Ð»Ñ\8cнÑ\96кÑ\96, Ñ\8fкÑ\96Ñ\8f ÐºÐ°Ñ\80Ñ\8bÑ\81Ñ\82аÑ\8eÑ\86Ñ\86а Ð³Ñ\8dÑ\82Ñ\8bм Ð\86Р-адÑ\80аÑ\81ам, Ð½Ñ\8f Ð¼Ð¾Ð³Ñ\83Ñ\86Ñ\8c Ñ\81Ñ\82ваÑ\80Ñ\8bÑ\86Ñ\8c Ð·Ð°Ñ\80аз Ð±Ð¾Ð»ÐµÐ¹ Ñ\80аÑ\85Ñ\83нкаÑ\9e.",
+       "acct_creation_throttle_hit": "Ð\9dаведнÑ\96кÑ\96 Ð³Ñ\8dÑ\82ай Ð²Ñ\96кÑ\96, Ñ\8fкÑ\96Ñ\8f ÐºÐ°Ñ\80Ñ\8bÑ\81Ñ\82алÑ\96Ñ\81Ñ\8f Ð\92аÑ\88Ñ\8bм Ð\86Р-адÑ\80аÑ\81ам, Ñ\83жо Ñ\81Ñ\82ваÑ\80Ñ\8bлÑ\96 $1 {{PLURAL:$1|Ñ\80аÑ\85Ñ\83нак|Ñ\80аÑ\85Ñ\83нкÑ\96\80аÑ\85Ñ\83нкаÑ\9e}} Ð·Ð° $2, Ñ\88Ñ\82о Ð¿ÐµÑ\80авÑ\8bÑ\88ае Ð¼Ð°ÐºÑ\81Ñ\8bмалÑ\8cнÑ\83Ñ\8e Ð´Ð°Ð·Ð²Ð¾Ð»ÐµÐ½Ñ\83Ñ\8e ÐºÐ¾Ð»Ñ\8cкаÑ\81Ñ\8cÑ\86Ñ\8c Ð·Ð° Ð³Ñ\8dÑ\82Ñ\8b Ð¿Ñ\8dÑ\80Ñ\8bÑ\8fд.\nУ Ð²Ñ\8bнÑ\96кÑ\83, Ð½Ð°Ð²ÐµÐ´Ð½Ñ\96кÑ\96, Ñ\8fкÑ\96Ñ\8f ÐºÐ°Ñ\80Ñ\8bÑ\81Ñ\82аÑ\8eÑ\86Ñ\86а Ð³Ñ\8dÑ\82Ñ\8bм Ð\86Р-адÑ\80аÑ\81ам, Ð¿Ð°ÐºÑ\83лÑ\8c Ð½Ñ\8f Ð¼Ð¾Ð³Ñ\83Ñ\86Ñ\8c Ñ\81Ñ\82ваÑ\80аÑ\86Ñ\8c Ñ\80аÑ\85Ñ\83нкÑ\96.",
        "emailauthenticated": "Ваш адрас электроннай пошты быў пацьверджаны $2 у $3.",
        "emailnotauthenticated": "Ваш адрас электроннай пошты яшчэ не пацьверджаны.\nЛісты электроннай поштай для наступных магчымасьцяў дасылацца ня будуць.",
        "noemailprefs": "Пазначце адрас электроннай пошты ў Вашых наладах, каб актывізаваць гэтыя магчымасьці.",
        "botpasswords-label-resetpassword": "Скінуць пароль",
        "botpasswords-label-grants": "Прыдатныя дазволы:",
        "botpasswords-help-grants": "Кожны дазвол дае доступ да правоў удзельніка, якія ўжо мае рахунак удзельніка. Глядзіце [[Special:ListGrants|табліцу дазволаў]] дзеля дадатковых зьвестак.",
-       "botpasswords-label-restrictions": "Абмежаваньні на выкарыстаньне:",
        "botpasswords-label-grants-column": "Дазволена",
        "botpasswords-bad-appid": "Назва робата «$1» зьяўляецца няслушнай.",
        "botpasswords-insert-failed": "Не атрымалася дадаць робата зь імем «$1». Магчыма, ён ужо быў дададзены?",
        "showdiff": "Паказаць зьмены",
        "blankarticle": "<strong>Папярэджаньне:</strong> вы ствараеце пустую старонку.\nКалі вы націсьніце «{{int:savearticle}}» яшчэ раз, старонка будзе створаная без аніякага зьместу.",
        "anoneditwarning": "<strong>Папярэджаньне</strong>: вы не ўвайшлі ў сыстэму. Ваш IP-адрас будзе бачны ўсім, калі вы адрэдагуеце старонку. Калі вы <strong>[$1 ўвойдзеце]</strong> або <strong>[$2 створыце рахунак]</strong>, вашыя рэдагаваньні будуць зьвязаныя з вашым імем карыстальніка, а таксама вам будуць даступныя дадатковыя перавагі.",
-       "anonpreviewwarning": "''Вы не ўвайшлі ў сыстэму. Падчас захаваньня Ваш IP-адрас будзе дададзены ў гісторыю рэдагаваньняў старонкі.''",
+       "anonpreviewwarning": "<em>Вы не ўвайшлі ў сыстэму. Па захаваньні старонкі ваш IP-адрас будзе дададзены ў яе гісторыю рэдагаваньняў.</em>",
        "missingsummary": "'''Напамін:''' Вы не пазначылі кароткае апісаньне зьменаў.\nКалі Вы націсьніце кнопку «Запісаць» яшчэ раз, Вашае рэдагаваньне будзе запісанае без апісаньня.",
        "selfredirect": "<strong>Папярэджаньне:</strong> вы перанакіроўваеце старонку саму на сябе.\nМагчыма, вы пазначылі няслушную старонку для перанакіраваньня або вы рэдагуеце ня тую старонку.\nКалі вы націсьніце «{{int:savearticle}}» яшчэ раз, перанакіраваньне будзе створанае.",
        "missingcommenttext": "Калі ласка, увядзіце камэнтар ніжэй.",
        "log-action-filter-patrol": "Тып патруляваньня:",
        "log-action-filter-protect": "Тып абароны:",
        "log-action-filter-rights": "Тып зьмены правоў:",
+       "log-action-filter-suppress": "Тып хаваньня:",
+       "log-action-filter-upload": "Тып загрузкі:",
        "log-action-filter-all": "Усе",
        "log-action-filter-block-block": "Заблякаваць",
        "log-action-filter-block-reblock": "Зьмяненьне блякаваньня",
index b7255f3..7ce602c 100644 (file)
        "botpasswords-label-resetpassword": "Скінуць пароль",
        "botpasswords-label-grants": "Прыдатныя дазволы:",
        "botpasswords-help-grants": "Кожны дазвол дае доступ да правоў удзельніка, якія ўжо прызначаны ўліковаму запісу удзельніка. Глядзіце [[Special:ListGrants|табліцу дазволаў]] для атрымання дадатковых зьвестак.",
-       "botpasswords-label-restrictions": "Абмежаванні на выкарыстанне:",
        "botpasswords-label-grants-column": "Дазволена",
        "botpasswords-bad-appid": "Назва робата \"$1\" недапушчальная.",
        "botpasswords-insert-failed": "Не ўдалося дадаць робату назву \"$1\". Магчыма, яна ўжо дададзена?",
index 255097d..b357dea 100644 (file)
        "botpasswords-label-cancel": "Отказване",
        "botpasswords-label-delete": "Изтриване",
        "botpasswords-label-resetpassword": "Възстановяване на парола",
-       "botpasswords-label-restrictions": "Ограничения на употребата:",
        "botpasswords-created-title": "Паролата на бота е създадена",
        "botpasswords-created-body": "Паролата на бот „$1“ на потребител „$2“ е създадена.",
        "botpasswords-updated-title": "Паролата на бота е обновена",
        "pageinfo-category-files": "Брой файлове",
        "markaspatrolleddiff": "Отбелязване като проверена редакция",
        "markaspatrolledtext": "Отбелязване на редакцията като проверена",
+       "markaspatrolledtext-file": "Маркирай версията на файла като проверена",
        "markedaspatrolled": "Проверена редакция",
        "markedaspatrolledtext": "Избраната редакция на [[:$1]] беше отбелязана като патрулирана.",
        "rcpatroldisabled": "Патрулът е деактивиран",
        "svg-long-error": "Невалиден SVG файл: $1",
        "show-big-image": "Оригинален файл",
        "show-big-image-preview": "Размер на този преглед: $1.",
+       "show-big-image-preview-differ": "Размер на този $3 предварителен преглед на изходния $2 файл: $1.",
        "show-big-image-other": "{{PLURAL:$2|Друга разделителна способност|Други разделителни способности}}: $1.",
        "show-big-image-size": "$1 × $2 пиксела",
        "file-info-gif-looped": "непрекъснато повтаряне",
        "newimages-legend": "Име на файл",
        "newimages-label": "Име на файл (или част от него):",
        "newimages-showbots": "Показване на качвания от ботове",
+       "newimages-hidepatrolled": "Скрий проверените качвания",
        "noimages": "Няма нищо.",
        "ilsubmit": "Търсене",
        "bydate": "по дата",
        "exif-countrycodecreated": "Код на държавата, където е направена снимката",
        "exif-provinceorstatecreated": "Област или щат, където е направена снимката",
        "exif-citycreated": "Град, в който е направена снимката",
+       "exif-worldregiondest": "Показан регион на света",
+       "exif-countrydest": "Показана държава",
+       "exif-countrycodedest": "Код на показаната държава",
+       "exif-provinceorstatedest": "Показана провинция или щат",
+       "exif-citydest": "Показан град",
+       "exif-sublocationdest": "Показан район на града",
        "exif-objectname": "Кратко заглавие",
        "exif-specialinstructions": "Специални инструкции",
        "exif-headline": "Заглавие",
        "tags-active-yes": "Да",
        "tags-active-no": "Не",
        "tags-source-extension": "Дефиниран от софтуера",
+       "tags-source-manual": "Прилага се ръчно от потребители и ботове.",
        "tags-source-none": "Вече не се използва",
        "tags-edit": "редактиране",
        "tags-delete": "изтриване",
        "tags-activate": "активиране",
        "tags-deactivate": "спиране",
        "tags-hitcount": "$1 {{PLURAL:$1|промяна|промени}}",
+       "tags-manage-no-permission": "Нямате права за управление на етикети за промени.",
+       "tags-manage-blocked": "Вие не можете да управлявате етикети за промени, докато сте блокирани.",
        "tags-create-heading": "Създаване на нов етикет",
        "tags-create-explanation": "По подразбиране, новосъздадените етикети са достъпни за използване от потребители и ботове.",
        "tags-create-tag-name": "Име на етикета:",
        "tags-delete-not-found": "Етикет „$1“ не съществува.",
        "tags-activate-title": "Активиране на етикета",
        "tags-activate-reason": "Причина:",
+       "tags-activate-not-allowed": "Eтикет \"$1\" не е възможно да бъде активиран.",
        "tags-activate-not-found": "Етикет „$1“ не съществува.",
        "tags-activate-submit": "Активиране",
        "tags-deactivate-title": "Деактивиране на етикета",
        "htmlform-chosen-placeholder": "Избиране",
        "htmlform-cloner-create": "Добавяне на още",
        "htmlform-cloner-delete": "Премахване",
+       "htmlform-title-badnamespace": "[[:$1]] не е в именното пространство \"{{ns:$2}}\".",
        "htmlform-title-not-exists": "$1 не съществува.",
+       "htmlform-user-not-exists": "<strong>$1</strong> не съществува.",
+       "htmlform-user-not-valid": "<strong>$1</strong> не е валидно потребителско име.",
        "logentry-delete-delete": "$1 {{GENDER:$2|изтри}} страницата $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|възстанови}} страницата $3",
        "logentry-delete-revision": "$1 {{GENDER:$2|промени}} видимостта на {{PLURAL:$5|една редакция|$5 редакции}} в страница $3: $4",
        "expand_templates_generate_xml": "Показване на дървото от разбора на XML",
        "expand_templates_generate_rawhtml": "Показване на суров HTML",
        "expand_templates_preview": "Преглед",
+       "pagelanguage": "Промяна на езика на страницата",
        "pagelang-name": "Страница",
        "pagelang-language": "Език",
        "pagelang-use-default": "Използване на езика по подразбиране",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>изключено</strong>)",
        "mediastatistics": "Медия статистики",
        "mediastatistics-table-mimetype": "MIME тип",
+       "mediastatistics-table-extensions": "Възможни разширения",
        "mediastatistics-table-count": "Брой файлове",
        "mediastatistics-table-totalbytes": "Общ размер",
        "mediastatistics-header-unknown": "Неизвестно",
+       "mediastatistics-header-bitmap": "Растерни изображения",
+       "mediastatistics-header-drawing": "Рисунки (векторни изображения)",
        "mediastatistics-header-audio": "Аудио",
        "mediastatistics-header-video": "Видео",
+       "mediastatistics-header-multimedia": "Мултимедия",
        "mediastatistics-header-total": "Всички файлове",
        "json-error-syntax": "Синтактична грешка",
        "headline-anchor-title": "Препратка към този раздел",
        "log-action-filter-block-block": "Блокиране",
        "log-action-filter-block-reblock": "Промяна на блокирането",
        "log-action-filter-block-unblock": "Отблокиране",
+       "log-action-filter-delete-delete": "Изтриване на страница",
+       "log-action-filter-delete-restore": "Възстановяване на страница",
+       "log-action-filter-managetags-delete": "Премахване на етикет",
+       "log-action-filter-managetags-activate": "Активиране на етикет",
+       "log-action-filter-managetags-deactivate": "Деактивиране на етикет",
        "log-action-filter-upload-upload": "Ново качване",
        "log-action-filter-upload-overwrite": "Повторно качване",
        "authmanager-authplugin-setpass-bad-domain": "Невалиден домейн.",
index d00c9d7..4ec9f55 100644 (file)
        "eauthentsent": "মনোনীত ই-মেইল ঠিকানায় একটি নিশ্চিতকরণ ই-মেইল পাঠানো হয়েছে।\nঐ অ্যাকাউন্টটে অন্য কোন ই-মেইল পাঠানোর আগে আপনাকে ই-মেইলের নির্দেশগুলি অনুসরণ করতে হবে, যাতে অ্যাকাউন্টটি যে আসলেই আপনার, তা নিশ্চিত হয়।",
        "throttled-mailpassword": "বিগত {{PLURAL:$1|ঘণ্টার|$1 ঘণ্টার}} মধ্যে ইতিমধ্যেই একবার পাসওয়ার্ড বদলের তথ্য পাঠানো হয়েছে। অপব্যবহার রোধে প্রতি {{PLURAL:$1|ঘণ্টায়|$1 ঘণ্টায়}} কেবল একবার পাসওয়ার্ড বদলের তথ্য পাঠানো যাবে।",
        "mailerror": "ইমেইল পাঠাতে সমস্যা: $1",
-       "acct_creation_throttle_hit": "কেউ আপনার আইপি ঠিকানা ব্যবহার করে বিগত সময়ে {{PLURAL:$1|১টি অ্যাকাউন্ট|$1টি অ্যাকাউন্ট}} তৈরি করেছেন, যা এই সময়ের জন্য সর্বোচ্চ অনুমোদনকৃত। ফলে, এই আইপি ঠিকানা থেকে কেউ এই মুহুর্তে নতুন অ্যাকাউন্ট তৈরি করতে পারবে না।",
+       "acct_creation_throttle_hit": "কেউ আপনার আইপি ঠিকানা ব্যবহার করে বিগত $2 {{PLURAL:$1|১টি অ্যাকাউন্ট|$1টি অ্যাকাউন্ট}} তৈরি করেছেন, যা এই সময়ের জন্য সর্বোচ্চ অনুমোদনকৃত। ফলে, এই আইপি ঠিকানা থেকে কেউ এই মুহুর্তে নতুন অ্যাকাউন্ট তৈরি করতে পারবে না।",
        "emailauthenticated": "আপনার ইমেইল ঠিকানাটি $2 তারিখের $3 এ নিশ্চিত করা হয়েছে।",
        "emailnotauthenticated": "আপনার ই-মেইলের ঠিকানা এখনও যাচাই করা হয়নি।\nনিচের বৈশিষ্ট্যগুলোর (features) জন্য কোনো ই-মেইল পাঠানো হবে না।",
        "noemailprefs": "এই বৈশিষ্টটি কাজ করাতে হলে একটি ই-মেইল ঠিকানা নির্ধারণ করতে হবে।",
        "botpasswords-no-central-id": "বট পাসওয়ার্ড ব্যবহার করার জন্য, আপনাকে একটি কেন্দ্রীভূত অ্যাকাউন্টে প্রবেশ করতে হবে।",
        "botpasswords-existing": "বিদ্যমান বট শব্দচাবি",
        "botpasswords-createnew": "একটি নতুন বট পাসওয়ার্ড তৈরি করুন",
-       "botpasswords-editexisting": "à¦\8fà¦\95à¦\9fি à¦¬à¦¿à¦¦à§\8dযমান à¦¬à¦\9f à¦¶à¦¬à§\8dদà¦\9aাবি পরিবর্তন করুন",
+       "botpasswords-editexisting": "à¦\8fà¦\95à¦\9fি à¦¬à¦¿à¦¦à§\8dযমান à¦¬à¦\9f à¦ªà¦¾à¦¸à¦\93য়ারà§\8dড পরিবর্তন করুন",
        "botpasswords-label-appid": "বটের নাম:",
        "botpasswords-label-create": "তৈরি করো",
        "botpasswords-label-update": "হালনাগাদ",
        "botpasswords-label-delete": "অপসারণ",
        "botpasswords-label-resetpassword": "পাসওয়ার্ড পুনঃস্থাপন",
        "botpasswords-label-grants": "প্রয়োগযোগ্য মঞ্জুরি:",
-       "botpasswords-label-restrictions": "ব্যবহারের সীমাবদ্ধতা:",
        "botpasswords-label-grants-column": "অনুমোদিত",
        "botpasswords-bad-appid": "\"$1\" বট নামটি সঠিক নয়।",
        "botpasswords-insert-failed": "\"$1\" নামের বট যুক্ত করা যায়নি। আগে থেকেই তালিকায় রয়েছে?",
index 92c4cb3..c2b1d88 100644 (file)
        "timezoneregion-indian": "Indijski okean",
        "timezoneregion-pacific": "Tihi okean",
        "allowemail": "Dozvoli e-poštu od ostalih korisnika",
-       "prefs-searchoptions": "Traži",
+       "prefs-searchoptions": "Pretraga",
        "prefs-namespaces": "Imenski prostori",
        "default": "predodređeno",
        "prefs-files": "Datoteke",
index d5dbe64..1c3ddf7 100644 (file)
        "botpasswords-label-delete": "Suprimeix",
        "botpasswords-label-resetpassword": "Reinicia la contrasenya",
        "botpasswords-label-grants": "Permisos aplicables:",
-       "botpasswords-label-restrictions": "Restriccions d'ús:",
        "botpasswords-label-grants-column": "Concedit",
        "botpasswords-bad-appid": "El nom del bot «$1» no és vàlid.",
        "botpasswords-insert-failed": "No s'ha pogut afegir el nom del bot «$1». Ja hi estava afegit?",
index 8d380cd..15b39de 100644 (file)
@@ -21,8 +21,8 @@
        "tog-newpageshidepatrolled": "Káung kī sĭng hiĕk dăng-dăng gà̤-dēng ī-gĭng giēng-că guó gì hiĕk",
        "tog-extendwatchlist": "敆擴展監視單單臺中顯示所有其更改,伓啻最近其更改",
        "tog-usenewrc": "按頁顯示最近修改共監視列表臺中其群組改變",
-       "tog-numberheadings": "自動編號其標題",
-       "tog-showtoolbar": "顯示編輯工具欄",
+       "tog-numberheadings": "Biĕu-dà̤ cê̤ṳ-dông piĕng-hô̤",
+       "tog-showtoolbar": "Hiēng-sê piĕng-cĭk gă-sĭ-dèu",
        "tog-editondblclick": "雙擊就修改頁面",
        "tog-editsectiononrightclick": "啟用右擊標題編輯段落",
        "tog-watchcreations": "加添我開其頁面共我上傳其文件遘我其監視單",
index 088c746..c35d64a 100644 (file)
        "botpasswords-label-update": "Карлаяккха",
        "botpasswords-label-cancel": "Юхаяккха",
        "botpasswords-label-delete": "ДӀаяккхар",
-       "botpasswords-label-restrictions": "Лелоран доза тохар:",
        "botpasswords-label-grants-column": "Магийна",
        "botpasswords-bad-appid": "«$1» ботан цӀе магийна яц.",
        "botpasswords-created-body": "Ботан «$1» пароль кхиамца кхоьллина.",
        "special-characters-group-ipa": "ДАЭ (IPA)",
        "special-characters-group-symbols": "Символаш",
        "special-characters-group-greek": "Грекийн",
+       "special-characters-group-greekextended": "Грекийн шординарш",
        "special-characters-group-cyrillic": "Кирилан",
        "special-characters-group-arabic": "Ӏарбийн",
        "special-characters-group-arabicextended": "Iаьрбийн шординарш",
index cae8039..d12fe17 100644 (file)
        "botpasswords-label-resetpassword": "Resetovat heslo",
        "botpasswords-label-grants": "Použitelná oprávnění:",
        "botpasswords-help-grants": "Každé přidělení dává přístup k uvedeným uživatelským oprávněním, která uživatelský účet již má. Viz [[Special:ListGrants|table of grants]] pro více informací.",
-       "botpasswords-label-restrictions": "Omezení užití:",
        "botpasswords-label-grants-column": "Přiděleno",
        "botpasswords-bad-appid": "Název bota „$1“ není platný.",
        "botpasswords-insert-failed": "Nepodařilo se přidat název bota „$1“. Nebyl už přidán?",
index 4272464..c2eeeba 100644 (file)
        "botpasswords-label-delete": "Slet",
        "botpasswords-label-resetpassword": "Nulstil adgangskode",
        "botpasswords-label-grants": "Tilgængelige bevillinger:",
-       "botpasswords-label-restrictions": "Begrænsninger for brug:",
        "resetpass_forbidden": "Adgangskoder kan ikke ændres",
        "resetpass-no-info": "Du skal være logget på for at komme direkte til denne side.",
        "resetpass-submit-loggedin": "Skift adgangskode",
index 9d42c88..4497e13 100644 (file)
        "eauthentsent": "Eine Bestätigungs-E-Mail wurde an die angegebene Adresse verschickt.\n\nBevor eine E-Mail von anderen Benutzern über die E-Mail-Funktion empfangen werden kann, muss die Adresse und ihre tatsächliche Zugehörigkeit zu diesem Benutzerkonto erst bestätigt werden. Bitte befolge die Hinweise in der Bestätigungs-E-Mail.",
        "throttled-mailpassword": "Es wurde innerhalb der letzten {{PLURAL:$1|Stunde|$1 Stunden}} bereits eine Passwortzurücksetzungs-E-Mail angefordert. Um einen Missbrauch der Funktion zu verhindern, kann nur {{PLURAL:$1|einmal pro Stunde|alle $1 Stunden}} eine Passwortzurücksetzungs-E-Mail angefordert werden.",
        "mailerror": "Fehler beim Senden der E-Mail: $1",
-       "acct_creation_throttle_hit": "Besucher dieses Wikis, die deine IP-Adresse verwenden, haben innerhalb des letzten Tages {{PLURAL:$1|1 Benutzerkonto|$1 Benutzerkonten}} erstellt, was die maximal erlaubte Anzahl in dieser Zeitperiode ist.\n\nBesucher, die diese IP-Adresse verwenden, können momentan keine Benutzerkonten mehr erstellen.",
+       "acct_creation_throttle_hit": "Besucher dieses Wikis, die deine IP-Adresse verwenden, haben innerhalb der letzten $2 {{PLURAL:$1|ein Benutzerkonto|$1 Benutzerkonten}} erstellt, was die maximal erlaubte Anzahl in dieser Zeitperiode ist.\n\nBesucher, die diese IP-Adresse verwenden, können momentan keine Benutzerkonten mehr erstellen.",
        "emailauthenticated": "Deine E-Mail-Adresse wurde am $2 um $3 Uhr bestätigt.",
        "emailnotauthenticated": "Deine E-Mail-Adresse ist noch nicht bestätigt.\nDie folgenden E-Mail-Funktionen stehen erst nach erfolgreicher Bestätigung zur Verfügung.",
        "noemailprefs": "Gib eine E-Mail-Adresse in den Einstellungen an, damit die nachfolgenden Funktionen zur Verfügung stehen.",
        "botpasswords-label-resetpassword": "Passwort zurücksetzen",
        "botpasswords-label-grants": "Anwendbare Berechtigungen:",
        "botpasswords-help-grants": "Jede Berechtigung gibt Zugriff auf gelistete Benutzerrechte, die ein Benutzerkonto bereits hat. Siehe die [[Special:ListGrants|Tabelle]] für weitere Informationen.",
-       "botpasswords-label-restrictions": "Verwendungsbeschränkungen:",
        "botpasswords-label-grants-column": "Gewährt",
        "botpasswords-bad-appid": "Der Botname „$1“ ist nicht gültig.",
        "botpasswords-insert-failed": "Der Botname „$1“ konnte nicht hinzugefügt werden. Wurde er bereits hinzugefügt?",
        "htmlform-cloner-create": "Weitere hinzufügen",
        "htmlform-cloner-delete": "Entfernen",
        "htmlform-cloner-required": "Es ist mindestens ein Wert erforderlich.",
+       "htmlform-date-toolow": "Der angegebene Wert liegt vor dem frühesten erlaubten Datum $1.",
        "htmlform-title-badnamespace": "[[:$1]] ist nicht im Namensraum „{{ns:$2}}“.",
        "htmlform-title-not-creatable": "„$1“ ist kein erstellbarer Seitentitel",
        "htmlform-title-not-exists": "$1 ist nicht vorhanden.",
        "unlinkaccounts-success": "Das Benutzerkonto wurde getrennt.",
        "authenticationdatachange-ignored": "Die Änderung der Authentifizierungsdaten wurde nicht bearbeitet. Vielleicht wurde kein Anbieter konfiguriert?",
        "userjsispublic": "Bitte beachten: JavaScript-Unterseiten sollten keine vertraulichen Daten enthalten, da sie von anderen Benutzern eingesehen werden können.",
-       "usercssispublic": "Bitte beachten: CSS-Unterseiten sollten keine vertraulichen Daten enthalten, da sie von anderen Benutzern eingesehen werden können."
+       "usercssispublic": "Bitte beachten: CSS-Unterseiten sollten keine vertraulichen Daten enthalten, da sie von anderen Benutzern eingesehen werden können.",
+       "restrictionsfield-badip": "Ungültige IP-Adresse oder ungültiger IP-Adressbereich: $1",
+       "restrictionsfield-label": "Erlaubte IP-Adressbereiche:",
+       "restrictionsfield-help": "Eine IP-Adresse oder ein CIDR-Bereich pro Zeile. Um alles zu aktivieren, verwende<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index fe63a3b..57b91f9 100644 (file)
@@ -42,6 +42,7 @@
        "tog-watchdefault": "Pel u dosyeyê ke mı vurnayê lista mına seyrkerdışi ke",
        "tog-watchmoves": "Pel u dosyeyê ke mı kırıştê lista mına seyrkerdışi ke",
        "tog-watchdeletion": "Pel u dosyeyê ke mı esterıtê lista mına seyrkerdışi ke",
+       "tog-watchuploads": "Dosya yë kı mı kerdë bar lista seyran kı",
        "tog-watchrollback": "Pelê ke mı peyser ardi inan lista mına seyrkerdışi ke",
        "tog-minordefault": "Vurnayışanê xo pêrune ''vurnayışo qıckek'' nışan bıde",
        "tog-previewontop": "Verqayti pela nuştışi ser de bımocne",
@@ -51,7 +52,7 @@
        "tog-enotifminoredits": "Pelan de vurnayışanê qıckekan u dosyan de ki mı rê e-mail bırışe",
        "tog-enotifrevealaddr": "Adresa e-posteyê mı posteyê xeberan de bımocne",
        "tog-shownumberswatching": "Amarê karberanê seyrkerdoğan bımocne",
-       "tog-oldsig": "İmzaya mewcude:",
+       "tog-oldsig": "İmzaya mewcud:",
        "tog-fancysig": "İmza rê mameleyê wikimeqaley bıke (bê gıreyo otomatik)",
        "tog-uselivepreview": "Verqayto giyane bıgureyne",
        "tog-forceeditsummary": "Mı ke xulasa veng verdaye, hay a mı ser de",
@@ -59,6 +60,7 @@
        "tog-watchlisthidebots": "Lista seyrkerdışi ra vurnayışanê boti bınımne",
        "tog-watchlisthideminor": "Vurnayışanê qıckekan lista mına seyrkerdışi de bınımne",
        "tog-watchlisthideliu": "Lista seyrkerdışi ra vurnayışanê karberanê cıkewteyan bınımne",
+       "tog-watchlistreloadautomatically": "Filtra vıriyayış dı listey seyri otomatikman anewe kı",
        "tog-watchlisthideanons": "Lista seyrkerdışi ra vurnayışanê karberanê anoniman bınımne",
        "tog-watchlisthidepatrolled": "Lista seyrkerdışi ra vurnayışanê qontrolkerdeyan bınımne",
        "tog-watchlisthidecategorization": "Pera kategorizasyoni bınımne",
@@ -67,7 +69,7 @@
        "tog-showhiddencats": "Kategoriyanê nımneya bıasne",
        "tog-norollbackdiff": "Peyser ardışi ra dıme ferqi measne",
        "tog-useeditwarning": "Wexto ke mı yew pela nizami be vurnayışanê nêqeydbiyayeyan caverdê, hay be mı ser de",
-       "tog-prefershttps": "Ronışten akerden de  greyo itimadın bıkarne",
+       "tog-prefershttps": "Ronışten akerden de tım greyo itimadın bıkarne",
        "underline-always": "Tım",
        "underline-never": "Qet",
        "underline-default": "Cild ya zi cıgeyrayoğo hesebiyaye",
        "category-file-count-limited": "{{PLURAL:$1|Dosya cêrêne|$1 Dosyê cêrêni}} na kategoriye derê.",
        "listingcontinuesabbrev": "dewam...",
        "index-category": "Pelê endeksıni",
-       "noindex-category": "Pelê ke zerrekê cı çıniyo",
+       "noindex-category": "Bê indeksın perri",
        "broken-file-category": "Peleye ke gıreyê dosyeyanê ğeletan muhtewa kenê",
        "categoryviewer-pagedlinks": "($1) ($2)",
        "about": "Heqa cı de",
        "article": "Pela zerreki",
        "newwindow": "(pençereyê newey de beno a)",
-       "cancel": "Bıtexelne",
+       "cancel": "Bıterkın",
        "moredotdotdot": "Vêşi...",
-       "morenotlisted": "Vêşi lista nêbi...",
+       "morenotlisted": "Na lista qay kemi ya.",
        "mypage": "Pele",
        "mytalk": "Mesac",
        "anontalk": "Werênayış",
        "newpage": "Pela newiye",
        "talkpage": "Ena pele sero werêne",
        "talkpagelinktext": "werênayış",
-       "specialpage": "Pela xısusiye",
+       "specialpage": "Perra bağsi",
        "personaltools": "Hacetê şexsiy",
        "articlepage": "Pera zerreki bıvin",
-       "talk": "Werênayış",
+       "talk": "Vaten",
        "views": "Asayışi",
        "toolbox": "Haceti",
+       "tool-link-userrights": "Grubanê {{GENDER:$1|karberi}} bıvırnë",
+       "tool-link-emailuser": "E-posta ya në{{GENDER:$1|karberi}}",
        "userpage": "Pela karberi bıvêne",
        "projectpage": "Pela proceyi bıvêne",
        "imagepage": "Pera dosya bıasne",
        "jumptonavigation": "Pusula",
        "jumptosearch": "cı geyre",
        "view-pool-error": "Qaytê qısuri mekerên, serverê ma enıka zêde bar gırewto xo ser.\nHedê xo ra zêde karberi kenê ke seyrê na pele bıkerê.\nŞıma rê zehmet, tenê vınderên, heta ke reyna kenê ke ena pele kewê.\n\n$1",
+       "generic-pool-error": "Üzgünüz, şu an sunucular aşırı yüklendi.\nÇok fazla kullanıcı bu sayfayı görüntülemeye çalışıyor.\nLütfen bu sayfaya  tekrar erişmeyi denemeden önce biraz bekleyin.",
        "pool-timeout": "Kılitbiyayışi sero wextê vınetışi",
        "pool-queuefull": "Rêza hewze pırra",
        "pool-errorunknown": "Xeta nêzanıtiye",
+       "pool-servererror": "Amordoğa xızmeti ya istifade nëbena $1",
        "poolcounter-usage-error": "Xırab karyayış:$1",
        "aboutsite": "Heqa {{SITENAME}} de",
        "aboutpage": "Project:Heqa",
        "mainpage": "Pela Seri",
        "mainpage-description": "Pela seri",
        "policy-url": "Project:Terzê hereketi",
-       "portal": "Portalê cemaeti",
-       "portal-url": "Project:Portalê cemaeti",
+       "portal": "Portalë Å\9fëlıgi",
+       "portal-url": "Project:Portalë Å\9fëlıgi",
        "privacy": "Politikaya nımıteyiye",
        "privacypage": "Project:Xısusiyetê nımıtışi",
        "badaccess": "Xeta mısadey",
        "readonly_lag": "Daegeh (database) otomatikmen kılit bi, sureo ke  daegehê bınêni resay daegehê serêni.",
        "internalerror": "Xeta zerreki",
        "internalerror_info": "Xeta zerreki: $1",
+       "internalerror-fatal-exception": "Babet da \"$1\" dı xırab xeta",
        "filecopyerror": "\"$1\" qaydê na \"$2\" dosya nêbeno.",
        "filerenameerror": "nameyê \"$1\" dosya nêvuriya no name \"$2\" ri.",
        "filedeleteerror": "Na \"$1\" dosya hewn a nêşi .",
        "directorycreateerror": "\"$1\" rêzkiyê ey nêvırazya",
+       "directoryreadonlyerror": "Rëzena \"$1\" salt-wanëna.",
+       "directorynotreadableerror": "Rëzena $1 wanebıyayi niya",
        "filenotfound": "Na \"$1\" dosya nêasena.",
        "unexpected": "Endek texmin nêbeni: \"$1\"=\"$2\".",
        "formerror": "Xeta: Form nêerşawiyeno",
        "no-null-revision": "Qandé \"$1\" zew rewizyono newe névıraziya.",
        "badtitle": "Sernameyo xırabın",
        "badtitletext": "Sernameyê pela ke şıma waşt, nêvêrd, vengo ya zi zıwano miyanêno ğelet gırêdaye ya zi sernameyê wiki.\nBeno ke, tede yew ya zi zêdê işareti estê ke sernameyan de nêxebetiyenê.",
+       "title-invalid-empty": "Waziyaye sernamey perrer  venonyana teyna canamey nami sero esto.",
        "perfcached": "Datay cı ver hazır biye. No semedê ra nıkayin niyo! tewr zaf {{PLURAL:$1|netice|$1 netice}} debêno de",
        "perfcachedts": "Cêr de malumatê nımıteyi esti, demdê newe kerdışo peyın: $1. Tewr zaf {{PLURAL:$4|netice|$4 neticey cı}} debyayo de",
        "querypage-no-updates": "Rocanebiyayışê na pele nıka cadayiyê.\nDayiyi tiya nıka newe nêbenê.",
        "createacct-yourpasswordagain-ph": "Parola fına cıkewe",
        "userlogin-remembermypassword": "Mı biya xo viri",
        "userlogin-signwithsecure": "Ebe teqdimkerê asayişın cıkewe",
+       "cannotlogin-title": "Cı nëkewtë",
        "cannotloginnow-title": "Enewke ronıştışo nêabeno",
        "cannotloginnow-text": "$1 karkerdışa ronıştış akerdış mıkum niyo.",
+       "cannotcreateaccount-title": "Nêşenay hesab rakerê",
        "yourdomainname": "Yewdestê şıma:",
        "password-change-forbidden": "Şıma na wiki de nêşenê parola bıvurnê.",
        "externaldberror": "Ya database de xeta esta ya zi heqê şıma çino şıma no hesab bıvurni.",
        "wrongpasswordempty": "Parola tola, venga. tekrar bınuse.",
        "passwordtooshort": "Paroley gani tewr senık be {{PLURAL:$1|1 karakter|$1 karakteran}} derg bê.",
        "passwordtoolong": "Paroleyi be {{PLURAL:$1|1 karakter|$1 karakteran}} ra derg nêbenê.",
+       "passwordtoopopular": "Parolay kehana ker kerıdşi rë mısade nëdeyë no.  Ju parolaya xas bıweçinë",
        "password-name-match": "Parola u nameyê şıma gani zeypê (seypê) nêbo.",
        "password-login-forbidden": "Nameyê nê karberi û gurenayışê parola biyo qedeğen.",
        "mailmypassword": "Parola reset ke",
        "eauthentsent": "Adresok şıma qeyd kerdo wıcayré e-posta rışiyé.\nHetana şıma ne e-posta néwweyniyé, şımaé zewbi e-posta do nérışiyo.",
        "throttled-mailpassword": "Eyarkerdışê parola xora zerreyê {{PLURAL:$1|yew saete|$1 saetan}} erşawiya.\nSeba xırabgurenayışê xızmete ra, her {{PLURAL:$1|yew saete|$1 saetan}} de rey tenya yew eyarkerdışê parola erşawiyeno.",
        "mailerror": "Erşawıtışe xetayê e-posta: $1",
-       "acct_creation_throttle_hit": "Yew ten IP adresê şıma xebıtnayo u kewto no wiki, roco peyin de {{PLURAL:$1|1 hesab|$1 hesab}} vıraşto.\nxulasa ney kesê ke IP adresê şıma xebıtneni hini nêeşkeni ney ra zêdêr hesab akeri.",
+       "acct_creation_throttle_hit": "Yew ten IP adresê şıma xebıtnayo u kewto no wiki, $2roco peyin de {{PLURAL:$1|1 hesabi|$1 hesaban}} vıraşto.\nxulasa ney kesê ke IP adresê şıma xebıtneni hini nêeşkeni ney ra zêdêr hesab akeri.",
        "emailauthenticated": "E-postay şıma $2 sehat $3 dı biya araşt",
        "emailnotauthenticated": "Adresa e-pota da şıma qebul nébiya.\nQandé céréna şımaré teba do nérışiyo.",
        "noemailprefs": "Hesab biyo a.",
        "passwordreset-emailsentemail": "Eke na seba hesabê şıma yew adresa e-posteyê qeydına, yew e-posteyê parola nênkerdışi rışiyeno.",
        "passwordreset-invalideamil": "Adresê eposta raşt niya",
        "changeemail": "E-posta adresa xo wedarne",
-       "changeemail-header": "E-posya adresta hesabdê xo bıvurnê",
+       "changeemail-header": "E-posta adresa xo vuriyayışi rë ena former pır kerë. Eger kı şıma qayılë kı e postay adresi ra wedarnë se formi rıştış dı heruna e posta veng verdë",
        "changeemail-no-info": "Şıma gani bıkewê pele ke derdest bıresê na pele.",
        "changeemail-oldemail": "E-postay şımawa nıkaêne:",
        "changeemail-newemail": "E-postay şımawa newiye:",
        "last": "peyên",
        "page_first": "verên",
        "page_last": "peyên",
-       "histlegend": "Ferqê weçinıtışi: Qutiya versiyonan seba têversanayış işaret ke û dest be ''enter''i ya zi gocega cêrêne ro ne.<br />\nCedwel: <strong>({{int:ferq}})</strong> = ferqê verziyonê peyêni, <strong>({{int:peyên}})</strong> = ferqê versiyonê verêni, <strong>{{int:q}}</strong> = vurnayışo werdi.",
+       "histlegend": "Ferqê weçinayışi: Qutiya versiyonan seba têversanayış işaret ke u dest be ''enter''i ya zi gocega cêrêne ro ne.<br />\nCetwel: <strong>({{int:ferq}})</strong> = ferqê verziyonê peyêni, <strong>({{int:peyên}})</strong> = ferqê versiyonê verêni, <strong>{{int:q}}</strong> = vurnayışo werdi yo.",
        "history-fieldset-title": "Çımberz verori",
        "history-show-deleted": "Tenya esterıtey",
        "histfirst": "Verênêr",
        "search-category": "(kategori $1)",
        "search-file-match": "(zerreyê dosya yewbini gêno)",
        "search-suggest": "To va: $1",
-       "search-rewritten": "Neticey $ ra asenê.  Herunda ney wa neticey $2 ra bıasê?",
+       "search-rewritten": "Neticey $ ra asenê.  Herunda ney wa neticanë $2'i bıvin",
        "search-interwiki-caption": "Proceyê bıray",
        "search-interwiki-default": "$1 ra neticey:",
        "search-interwiki-more": "(véşi)",
        "prefs-labs": "Xacetê labs",
        "prefs-user-pages": "Pelê karberi",
        "prefs-personal": "Pela karberi",
-       "prefs-rc": "Vurriyayışê peyêni",
+       "prefs-rc": "Vıryayışë newey",
        "prefs-watchlist": "Lista seyrkerdışi",
        "prefs-editwatchlist": "Lista seyrkerdışi bıvurne",
        "prefs-editwatchlist-label": "Listey serkerdışanê cıkewtışi timar kerê",
        "undeletepagetext": "{{PLURAL:$1|pelo|$1 pelo}} cerın hewn a şiyo labele hema zi arşiv de yo u tepiya geriyeno.\nArşiv daimi pak beno.",
        "undelete-fieldset-title": "revizyonan tepiya bar ker",
        "undeleteextrahelp": "Qey ardışê pel u verê pelani tuşê '''tepiya biya!'''yi bıtıknê. qey ciya ciya ardışê verê pelani zi qutiye tesdiqi nişane kerê u tuşê '''tepiya biya!'''yi bıtıknê '''''{{int:undeletebtn}}'''''.. qey hewn a kerdışê qutiya tesdiqan u qey sıfır kerdışê cayê sebebani zi tuşê '''agêr caverd/aça ker'''i bıtıknê '''''{{int:undeletebtn}}'''''..",
-       "undeleterevisions": "$1 {{PLURAL:$1|revizyon|revizyon}} arşiw bi",
+       "undeleterevisions": "$1 {{PLURAL:$1|revizyon|revizyon}} esteriya yë",
        "undeletehistory": "eke şıma pel tepiya biyari heme revizyonî zi tepiya yeni.\neke yew pel hewn a biyo u pê nameyê o peli newe ra yew pel bıvıraziyo, revizyonê o pelê verıni zerreyê no pel de aseno.",
        "undeleterevdel": "eke pelo serın de netice bıdo ya zi revizyoni qısmen hewn a bıbiy hewn a kerdışi tepiya nêgeriyeno.",
        "undeletehistorynoadmin": "na madde hewn a biya. sebebê hewna kerdışi u teferruatê karber ê ke maddeyi vıraştı cêr de diyayî. revizyonê hewn a biyayeyani têna serkari vineni",
        "undeletedrevisions": "pêro piya{{PLURAL:$1|1 qeyd|$1 qeyd}} tepiya anciya.",
        "undeletedrevisions-files": "{{PLURAL:$1|1 revizyon|$1 revizyon}} u {{PLURAL:$2|1 dosya|$2 dosya}} ameyê halê xo yê verıni",
        "undeletedfiles": "{{PLURAL:$1|1 dosya|$1 dosya}} tepiya anciyayi.",
-       "cannotundelete": "Besternayışo nêbeno:\n$1",
+       "cannotundelete": "Besternayışonhemembyana tayno nêbeno:\n$1",
        "undeletedpage": "'''$1 pel tepiya anciya'''\n\nqey karê tepiya ardışi u qey karê hewn a kerdışê verıni bıewnê [[Special:Log/delete|qeydê hewn a kerdışi]].",
        "undelete-header": "Peleyê ke veror de besterneyayê êna bıvinê: [[Special:Log/delete|qeydê esterneya]].",
        "undelete-search-title": "Bıgeyre pelanê eserıtiyan",
        "sp-contributions-newbies-sub": "Qe hesebê newe",
        "sp-contributions-newbies-title": "Îştîrakê karberî ser hesabê neweyî",
        "sp-contributions-blocklog": "qeydê kılitbiyayeyi",
-       "sp-contributions-deleted": "iştırakê karberi esterdi",
+       "sp-contributions-deleted": "iştırakê {{GENDER:$1|karberi}} esterdi",
        "sp-contributions-uploads": "Barkerdışi",
        "sp-contributions-logs": "qeydi",
        "sp-contributions-talk": "werênayış",
        "tooltip-p-logo": "Pela seri bıvêne",
        "tooltip-n-mainpage": "Şo pela seri",
        "tooltip-n-mainpage-description": "Şo pela seri",
-       "tooltip-n-portal": "Heqa proceyi de, çı şenay bıkerê, çı koti vêniyeno",
+       "tooltip-n-portal": "Heqa proceyi de, kes çı şeno bıkero, çı koti vêniyeno",
        "tooltip-n-currentevents": "Vurnayışanê peyênan de melumatê pey bıvêne",
        "tooltip-n-recentchanges": "Wiki de yew lista vurriyayışanê peyênan",
        "tooltip-n-randompage": "Pelê da raştameyiye bar ke",
        "tooltip-ca-nstab-category": "Pela kategoriye bıvêne",
        "tooltip-minoredit": "Nay vırnayışa werdi nışan bıkeré",
        "tooltip-save": "Vurnayışanê xo qeyd ke",
+       "tooltip-publish": "Vurnayışê xo vıla kı",
        "tooltip-preview": "Vurnayışané ğo çımra ravyarné. Verdé qeyd kerdışi eneri bıkarné!",
        "tooltip-diff": "Metni sero vurnayışan mocneno",
        "tooltip-compareselectedversions": "Ena per de ferqê rewziyonan de dı weçinaya bıvinê",
        "pageinfo-article-id": "Kamiya pele",
        "pageinfo-language": "Zıwanê zerreyê pele",
        "pageinfo-content-model": "Modela zerreka perer",
+       "pageinfo-content-model-change": "bıvurne",
        "pageinfo-robot-policy": "Weziyetê motor de cıgeyrayışi",
        "pageinfo-robot-index": "İndeksbiyayen",
        "pageinfo-robot-noindex": "İndeksnêbiyayen",
        "pageinfo-watchers": "Amariya pela serykeran",
+       "pageinfo-visiting-watchers": "Amora merdumanë vuriyayışanë peyënan weynayan",
        "pageinfo-few-watchers": "$1 ra tayê {{PLURAL:$1|seyrker|seyrkeri}}",
        "pageinfo-redirects-name": "Hetenayışê na perer",
        "pageinfo-redirects-value": "$1",
        "newimages-summary": "Ena pela xasi dosyayi ke peni de bar biyayeyi mocnane.",
        "newimages-legend": "Avrêc",
        "newimages-label": "Nameyê dosya ( ya zi parçe ey)",
+       "newimages-showbots": "Selaganë boti bıvin",
+       "newimages-hidepatrolled": "Selaganë dewriyeyan bıvinë",
        "noimages": "Çik çini yo.",
        "ilsubmit": "Cı geyre",
        "bydate": "goreyê zemani",
        "exif-compression-34712": "JPEG2000",
        "exif-copyrighted-true": "Heqê telifiye",
        "exif-copyrighted-false": "Telifiya waziyeta eyara",
+       "exif-photometricinterpretation-1": "Siya u sıpe (siya 0)",
        "exif-photometricinterpretation-2": "RGB",
        "exif-photometricinterpretation-6": "YCbCr",
        "exif-unknowndate": "Tarix nizanyano",
        "watchlistedit-clear-legend": "Lista serykerdışê pak kerê",
        "watchlistedit-clear-explain": "Listeya serykerdış da şıma dı sernamey pêro besteryay",
        "watchlistedit-clear-titles": "Sernamey:",
+       "watchlisttools-clear": "Lista serykerdışê xo pak kı",
        "watchlisttools-view": "Vurnayışanê elaqedaran bıvêne",
        "watchlisttools-edit": "Lista seyrkerdışi bıvêne û bıvurne",
        "watchlisttools-raw": "Lista seyrkerdışia xame bıvurne",
        "hebrew-calendar-m12-gen": "Elul",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|mesac]])",
        "timezone-utc": "[[UTC]]",
+       "timezone-local": "Lokal",
        "duplicate-defaultsort": "'''Tembe:''' Hesıbyaye sırmey ratnayış de \"$2\" sırmey ratnayış de \"$1\"i nêhesıbneno.",
        "version": "Versiyon",
        "version-extensions": "Ekstensiyonî ke ronaye",
index 02def5d..45a79b0 100644 (file)
@@ -35,7 +35,7 @@
        "tog-enotifminoredits": "पानाहरू र फाइलहरूमी सामान्य सम्पादन भयालै मुइलाई ई-मेल गरियोस्",
        "tog-enotifrevealaddr": "जानकारी इ-मेलहरूमी मेरो इ-मेल खुलाउन्या",
        "tog-shownumberswatching": "निगरानी गरिरहेका प्रयोगकर्ताहरूको संख्या धेखाउन्या",
-       "tog-oldsig": "अहिलको हस्ताक्षर:",
+       "tog-oldsig": "तमरà¥\8b à¤\85हिलà¤\95à¥\8b à¤¹à¤¸à¥\8dताà¤\95à¥\8dषर:",
        "tog-fancysig": "मेरा दस्तखतलाई विकि पाठको रुपमी लिने (स्वत लिङ्क बिना)",
        "tog-uselivepreview": "प्रत्यक्ष पैल्लीकोरुप प्रयोग गर",
        "tog-forceeditsummary": "खाली सम्पादन शीर्षक प्रविष्टि गरेपछा मलाई सोधन्या",
@@ -52,7 +52,7 @@
        "tog-showhiddencats": "लुकाइएका श्रेणीहरू धेखाउन्या",
        "tog-norollbackdiff": "पैलास्थितिमी फर्काएपछा भिन्नता हटाउन्या",
        "tog-useeditwarning": "सम्पादनहरू सङ्ग्रह नगरिएका अवस्थामी अर्को पानामी जान खोज्या चेतावनी धेखाउन्या",
-       "tog-prefershttps": "पà¥\8dरवà¥\87श à¤\97रà¥\8dदा जबलै सुरक्षित जडानको प्रयोग गर्न्या",
+       "tog-prefershttps": "पà¥\8dरवà¥\87श à¤\97रनà¥\8dà¤\9cà¥\8dया जबलै सुरक्षित जडानको प्रयोग गर्न्या",
        "underline-always": "सधैं",
        "underline-never": "कभैई नाई",
        "underline-default": "खोल अथवा ब्राउजर पैलीकाजसो",
        "talk": "कुरणिकाआनी",
        "views": "अवलोकन गरऽ",
        "toolbox": "औजारअन",
+       "tool-link-userrights": "परिवर्तन{{GENDER:$1|प्रयोगकर्ता}}समूहहरू",
+       "tool-link-emailuser": "यो ईमेल{{GENDER:$|प्रयोगकर्ता}}",
        "userpage": "प्रयोगकर्ता पाना हेर्न्या",
        "projectpage": "प्रोजेक्ट पानो हेर्न्या",
        "imagepage": "चित्र पानो हेर",
index f43b50c..dfe09b4 100644 (file)
        "eauthentsent": "Un mesâg ed cunfèirma l'é stê spidî a l'indirés ed pôsta eletrônica sgnê ché. L'utèint per prèir inviêr di mesâg ed pôsta eletrônica al dēv andêr a drē al j istrusiòun scréti, in môd da cunfermêr ch' l'é ló al legétim proprietâri 'd l'indirés.",
        "throttled-mailpassword": "Un mesâg ed pôsta eletrônica 'd arnōv ed la cêva 'd ingrès l'é bèle stê inviê da mēno 'd {{PLURAL:$1|1 ōra|$1 ōri}}. Per pervèder abûş, la funziòun 'd arnōv ed la cêva 'd ingrès la pōl èser druvêda sōl 'na vôlta ògni {{PLURAL:$1|1 ōra|$1 ōri}}.",
        "mailerror": "Erōr int la spedisiòun dal mesâg $1",
-       "acct_creation_throttle_hit": "{{PLURAL:$1|1 registrasiòun l'é bèle stêda fâta |$1 registrasiòun în bèle stêdi fâti}} da quelchidûn cun al tó 'stès indirés IP int l'ûltem dé: l'é al mâsim permés in cól peréiod ed tèimp ché. Per còst j utèint che drōven cl 'indirés IP ché, p'r al mumèint,  an 's pōl mìa registrêr.",
+       "acct_creation_throttle_hit": "{{PLURAL:$1|1 registrasiòun l'é bèle stêda fâta |$1 registrasiòun în bèle stêdi fâti}} da quelchidûn cun al tó 'stès indirés IP int l'ûltem $2, ch'l'é al mâsim permés in cól peréiod ed tèimp ché. Per còst j utèint che drōven cl 'indirés IP ché, p'r al mumèint,  an 's pōl mìa registrêr.",
        "emailauthenticated": "L'indirés ed pôsta eletrônica l'é stê cunfermê al $2 al $3.",
        "emailnotauthenticated": "L'indirés ed pôsta eletrônica an n'é mìa incòra stê cunfermê.\nA gnirâ mìa spidî mesâg ed pôsta eletrônica p'r al funsiòun in elèinch ché sòta.",
        "noemailprefs": "Scréver un indirés ed pôsta eletrônica per fêr funsionêr st' al funsiòun.",
        "group-bot-member": "{{GENDER:$1|bot}}",
        "group-sysop-member": "{{GENDER:$1|aministradōr}}",
        "group-bureaucrat-member": "{{GENDER:$1|funsionâri}}",
-       "group-suppress-member": "{{GENDER:$1|oversight}}",
+       "group-suppress-member": "{{GENDER:$1|suppressor}}",
        "grouppage-user": "{{ns:project}}:Utèint",
        "grouppage-autoconfirmed": "{{ns:project}}:Utèint convalidê da per ló",
        "grouppage-bot": "{{ns:project}}:Bot",
        "grouppage-sysop": "{{ns:project}}:Aministradōr",
        "grouppage-bureaucrat": "{{ns:project}}:Funsionâri",
-       "grouppage-suppress": "{{ns:project}}:Oversight",
+       "grouppage-suppress": "{{ns:project}}:Suppressor",
        "right-read": "Al lēş al pàgini",
        "right-edit": "Mudéfica pàgini",
        "right-createpage": "Ét pō fêr al pàgini (fōra che 'l pàgini 'd discusiòun).",
        "right-override-export-depth": "Pôrta fōra al pàgini cun insèm al pàgini coleghêdi per 'na larghèsa ed 5",
        "right-sendemail": "Spidés pôsta eletrônica a êter utèint",
        "right-passwordreset": "A vèd i mesâg 'd arnōv ed la cêva 'ed ingrès",
-       "right-managechangetags": "Fà e tó via i [[Special:Tags|tag]] dal databêş",
+       "right-managechangetags": "Fa e mèt in funsiòun/blôca al j [[Special:Tags|etichèti]]",
        "right-applychangetags": "Tâca dal [[Special:Tags|tichèti]] al tō mudéfichi",
        "right-changetags": "Zûta e tó via [[Special:Tags|tichèti]] precîşi só versiòun ónichi o vōş ed regéster",
        "newuserlogpage": "Utèint nōv",
        "rightslogtext": "Ché sòt a gh' é la lésta dal mudéfichi a i dirét dê a j utèint.",
        "action-read": "lēzer cla pàgina ché",
        "action-edit": "Mudifichêr cla pàgina ché",
-       "action-createpage": "inventêr pàgini",
-       "action-createtalk": "fêr 'l pàgini 'd discusiòun.",
+       "action-createpage": "fà cla pàgina ché",
+       "action-createtalk": "fêr cla pàgina 'd discusiòun ché.",
        "action-createaccount": "fêr cla registrasiòun ché",
        "action-history": "vèder la stôria 'd cla pàgina ché",
        "action-minoredit": "sgnêr cla mudéfica che cme céca",
        "action-viewmyprivateinfo": "guêrda al tō infurmasiòun personêli",
        "action-editmyprivateinfo": "mudéfica al tō infurmasiòun personêli",
        "action-editcontentmodel": "câmbia al mudèl dèinter a 'na pàgina",
-       "action-managechangetags": "fà e tó via i tag dal databêÅ\9f",
+       "action-managechangetags": "fêr e mèter in funsiòun/bluchêr al j etichèti",
        "action-applychangetags": "tachêr dal tichèti al tō mudéfichi",
        "action-changetags": "zuntêr o tōr via tichèti precîşi só versiòun ónichi o vōş ed regéster",
        "nchanges": "$1\n{{PLURAL:$1|mudéfica|mudéfichi}}",
        "newpageletter": "N",
        "boteditletter": "b",
        "number_of_watching_users_pageview": "[vésta da {{PLURAL:$1|un utèint|$1 utèint}}]",
-       "rc_categories": "Lémita al categoréi (divîşi da \"|\")",
-       "rc_categories_any": "Bast' ech sia",
+       "rc_categories": "Lémita al categoréi (separêdi da \"|\")",
+       "rc_categories_any": "Bast' ech sia fra còli sgnêdi",
        "rc-change-size-new": "$1 {{PLURAL:$1|byte|byte}} dôp la mudéfica",
        "newsectionsummary": "/* $1 */ sesiòn nōva",
        "rc-enhanced-expand": "Fà vèder i particulêr.",
        "backend-fail-read": "An n'é mìa pusébil lēzer al file \"$1\".",
        "backend-fail-create": "An n'é mìa pusébil fêr al file \"$1\".",
        "backend-fail-maxsize": "L'é impusébil fêr al file \"$1\" perché l'é pió grôs ed {{PLURAL:$2|un|$2}} byte.",
-       "backend-fail-readonly": "Al prugrâma 'd memôria \"$1\" adèsa a 's pōl sōl lēzer. La ragiòun dêda l'é: \"$2\".",
+       "backend-fail-readonly": "Al prugrâma 'd memôria \"$1\" adèsa a 's pōl sōl lēzer. La ragiòun dêda l'é: <em>$2</em>.",
        "backend-fail-synced": "Al file \"$1\" l'é in un stêt mìa lôgich cun al sistēma 'd la memôria intêrna.",
        "backend-fail-connect": "Impusébil coleghêres al sistēma 'd memôria \"$1\".",
        "backend-fail-internal": "É sucès un erōr mìa cgnusû int al sistēma  ed memôria \"$1\".",
        "whatlinkshere-prev": "{{PLURAL:$1|còl préma|quî préma $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|còl dôp|quî dôp $1}}",
        "whatlinkshere-links": "← colegamèint",
-       "whatlinkshere-hideredirs": "$1redirect",
+       "whatlinkshere-hideredirs": "$1 redirect",
        "whatlinkshere-hidetrans": "$1 uniòun",
        "whatlinkshere-hidelinks": "$1 colegamèint",
        "whatlinkshere-hideimages": "$1 colegamèint da file",
        "import-upload-filename": "Nòm dal file:",
        "import-comment": "Argumèint:",
        "import-upload": "Cârga infurmasiòun XML",
-       "tooltip-pt-userpage": "La  pàgina utèint",
-       "tooltip-pt-mytalk": "La  pàgina 'd discusiòun.",
-       "tooltip-pt-preferences": "Al  preferèinsi.",
+       "tooltip-pt-userpage": "La {{GENDER:|tó}} pàgina utèint",
+       "tooltip-pt-mytalk": "La {{GENDER:|tó}} pàgina 'd discusiòun.",
+       "tooltip-pt-preferences": "Al  {{GENDER:|tó}} preferèinsi.",
        "tooltip-pt-watchlist": "Elèinch dal pàgini che t'é drē tgnîr sòt ôc.",
-       "tooltip-pt-mycontris": "Elèinch di  lavōr.",
+       "tooltip-pt-mycontris": "Elèinch di {{GENDER:|tō}}  lavōr.",
        "tooltip-pt-login": "A 's cunsélia 'd fêr la registrasiòun, ânca s' an n'é mia ubligatôri.",
        "tooltip-pt-logout": "Và fōra",
        "tooltip-ca-talk": "Guêrda al discusiòun relatîvi a cla pàgina chè.",
-       "tooltip-ca-edit": "Ét pō mudifiche cla pàgina ché. Per piaşèir drōva al ptòun \"Guêrda préma 'd salvêr\" per vèder còl che t'é fât.",
+       "tooltip-ca-edit": "Mudéfica cla pàgina ché",
        "tooltip-ca-addsection": "Cumîncia 'na sesiòun nōva.",
        "tooltip-ca-viewsource": "Cla pàgina ché l'é sòta prutesiòun, mó 't pō vèder al só côdis surzéia.",
        "tooltip-ca-history": "Versiòun ed préma fâti a cla pàgina ché.",
        "tooltip-t-whatlinkshere": "Elèinch ed tót' al pàgini ch'în coleghêdi a còsta.",
        "tooltip-t-recentchangeslinked": "Elèinch dal j ûltmi mudéfichi al pàgini coleghêdi a còsta.",
        "tooltip-feed-atom": "Feed Atom per cla pàgina ché.",
-       "tooltip-t-contributions": "Lèsta di lavōr fât da cl'utèint ché.",
-       "tooltip-t-emailuser": "Mânda un mesâg cun la pòsta eletrônica a cl'utèint ché",
+       "tooltip-t-contributions": "Lèsta di lavōr fât da {{GENDER:$1|cl'utèint|cl'utèinta}}  ché.",
+       "tooltip-t-emailuser": "Mânda un mesâg cun la pòsta eletrônica a{{GENDER:$1|cl'utèint|cl'utèinta}}  ché",
        "tooltip-t-upload": "Cârga un file",
        "tooltip-t-specialpages": "Elèinch ed tót al pàgini specêli",
        "tooltip-t-print": "Per stampêr cla pàgina ché.",
        "tooltip-t-permalink": "Colegamèint fés a cla versiòun ché 'd  la pàgina.",
        "tooltip-ca-nstab-main": "Guêrda la pàgina",
        "tooltip-ca-nstab-user": "Guêrda la pàgina utèint",
-       "tooltip-ca-nstab-special": "Còsta ché l'é 'na pàgina specêlal l'an pōl mìa èser mudifichêda",
+       "tooltip-ca-nstab-special": "Còsta ché l'é 'na pàgina specêla l'an pōl mìa èser mudifichêda",
        "tooltip-ca-nstab-project": "Guêrda la pàgina dal prugèt",
        "tooltip-ca-nstab-image": "Guêrda la pàgina dal file",
        "tooltip-ca-nstab-template": "Guêrda 'l mudèl",
index a684a3f..615c4c1 100644 (file)
@@ -77,7 +77,7 @@
        "tog-enotifminoredits": "Να μου αποστέλλεται μήνυμα ηλεκτρονικού ταχυδρομείου και για αλλαγές μικρής κλίμακας σε σελίδες και αρχεία",
        "tog-enotifrevealaddr": "Αποκάλυψη της ηλεκτρονικής μου διεύθυνσης σε ειδοποιήσεις ηλεκτρονικού ταχυδρομείου",
        "tog-shownumberswatching": "Εμφάνιση του αριθμού των συνδεδεμένων χρηστών",
-       "tog-oldsig": "Î¥Ï\80άÏ\81Ï\87οÏ\85Ï\83α Ï\85Ï\80ογÏ\81αÏ\86ή:",
+       "tog-oldsig": "Î\97 Ï\84Ï\81έÏ\87οÏ\85Ï\83α Ï\85Ï\80ογÏ\81αÏ\86ή Ï\83αÏ\82:",
        "tog-fancysig": "Μεταχείριση υπογραφής ως κώδικα wiki (χωρίς αυτόματο σύνδεσμο)",
        "tog-uselivepreview": "Χρήση προεπισκόπησης σε ζωντανό χρόνο",
        "tog-forceeditsummary": "Να ειδοποιούμαι κατά την εισαγωγή κενής σύνοψης επεξεργασίας",
@@ -94,7 +94,7 @@
        "tog-showhiddencats": "Εμφάνιση κρυμμένων κατηγοριών",
        "tog-norollbackdiff": "Παράλειψη εμφάνισης διαφορών μετά την εκτέλεση επαναφοράς",
        "tog-useeditwarning": "Προειδοποίηση όταν εγκαταλείπω μία σελίδα επεξεργασίας χωρίς να έχω πρώτα αποθηκεύσει τις αλλαγές",
-       "tog-prefershttps": "Να γίνεται πάντα χρήση ασφαλούς σύνδεσης όταν ο χρήστης είναι συνδεδεμένος",
+       "tog-prefershttps": "Να γίνεται πάντα χρήση ασφαλούς σύνδεσης ενώ είμαι σε σύνδεση",
        "underline-always": "Πάντα",
        "underline-never": "Ποτέ",
        "underline-default": "Προεπιλογή από θέμα εμφάνισης ή από περιηγητή",
        "category-file-count-limited": "Η τρέχουσα κατηγορία περιέχει {{PLURAL:$1|το ακόλουθο αρχείο|τα ακόλουθα $1 αρχεία}}.",
        "listingcontinuesabbrev": "συνεχίζεται",
        "index-category": "Σελίδες καταλογογραφημένες για μηχανές αναζήτησης",
-       "noindex-category": "Σελίδες μη καταλογογραφημένες για μηχανές αναζήτησης",
+       "noindex-category": "Σελίδες μη καταλογογραφημένες",
        "broken-file-category": "Σελίδες με κατεστραμμένους συνδέσμους",
        "about": "Σχετικά",
        "article": "Σελίδα περιεχομένου",
        "newwindow": "(ανοίγει σε ξεχωριστό παράθυρο)",
        "cancel": "Ακύρωση",
        "moredotdotdot": "Περισσότερα...",
-       "morenotlisted": "Î\91Ï\85Ï\84ή Î· Î»Î¯Ï\83Ï\84α Î´ÎµÎ½ ÎµÎ¯Î½Î±Î¹ Ï\80λήÏ\81ης.",
+       "morenotlisted": "Î\91Ï\85Ï\84ή Î· Î»Î¯Ï\83Ï\84α Î¼Ï\80οÏ\81εί Î½Î± ÎµÎ¯Î½Î±Î¹ ÎµÎ»Î»Î¹Ï\80ής.",
        "mypage": "Σελίδα",
        "mytalk": "Συζήτηση",
        "anontalk": "Σελίδα συζήτησης αυτής της διεύθυνσης IP",
        "talk": "Συζήτηση",
        "views": "Προβολές",
        "toolbox": "Εργαλεία",
+       "tool-link-userrights": "Αλλαγή ομάδων {{GENDER:$1|χρήστη}}",
+       "tool-link-emailuser": "Αποστολή e-mail {{GENDER:$1|στο|στη}} χρήστη",
        "userpage": "Προβολή σελίδας χρήστη",
        "projectpage": "Προβολή σελίδας εγχειρήματος",
        "imagepage": "Προβολή σελίδας αρχείου",
        "eauthentsent": "Ένα μήνυμα επαλήθευσης έχει σταλεί στην ηλεκτρονική διεύθυνση που έχετε δηλώσει.\nΠριν αρχίσει η αποστολή μηνυμάτων στη συγκεκριμένη διεύθυνση, πρέπει να ακολουθήσετε τις οδηγίες που βρίσκονται στο μήνυμα που σας έχει σταλεί, για να επαληθεύσετε ότι η συγκεκριμένη ηλεκτρονική διεύθυνση ανήκει πραγματικά σε εσάς.",
        "throttled-mailpassword": "Ένα email επαναφοράς κωδικού έχει ήδη αποσταλεί, μέσα {{PLURAL:$1|στην τελευταία ώρα|στις τελευταίες $1 ώρες}}.\nΓια την αποφυγή κατάχρησης, μόνο ένα email επαναφοράς κωδικού θα στέλνεται ανά {{PLURAL:$1|ώρα|$1 ώρες}}.",
        "mailerror": "Σφάλμα στην αποστολή του μηνύματος: $1",
-       "acct_creation_throttle_hit": "Î\95Ï\80ιÏ\83κέÏ\80Ï\84εÏ\82 Î±Ï\85Ï\84οÏ\8d Ï\84οÏ\85 wiki Î¼Îµ Ï\84ην Î´Î¹ÎµÏ\8dθÏ\85νÏ\83η IP Ï\83αÏ\82 Î­Ï\87οÏ\85ν Î®Î´Î· Î´Î·Î¼Î¹Î¿Ï\85Ï\81γήÏ\83ει {{PLURAL:$1|ένα Î»Î¿Î³Î±Ï\81ιαÏ\83μÏ\8c|$1 Î»Î¿Î³Î±Ï\81ιαÏ\83μοÏ\8dÏ\82}}, ÎºÎ±Ï\84ά Ï\84ην Ï\84ελεÏ\85Ï\84αία Î¼Î¯Î± Î·Î¼Î­Ï\81α, που είναι και ο μέγιστος επιτρεπόμενος αριθμός.\nΩς αποτέλεσμα, επισκέπτες αυτού του wiki με αυτήν την διεύθυνση IP δεν μπορούν αυτή την στιγμή να δημιουργήσουν περισσότερους λογαριασμούς.",
+       "acct_creation_throttle_hit": "Î\95Ï\80ιÏ\83κέÏ\80Ï\84εÏ\82 Î±Ï\85Ï\84οÏ\8d Ï\84οÏ\85 wiki Î¼Îµ Ï\84ην Î´Î¹ÎµÏ\8dθÏ\85νÏ\83η IP Ï\83αÏ\82 Î­Ï\87οÏ\85ν Î®Î´Î· Î´Î·Î¼Î¹Î¿Ï\85Ï\81γήÏ\83ει {{PLURAL:$1|ένα Î»Î¿Î³Î±Ï\81ιαÏ\83μÏ\8c|$1 Î»Î¿Î³Î±Ï\81ιαÏ\83μοÏ\8dÏ\82}}, ÎºÎ±Ï\84ά Ï\83ε Ï\80εÏ\81ίοδο $2, που είναι και ο μέγιστος επιτρεπόμενος αριθμός.\nΩς αποτέλεσμα, επισκέπτες αυτού του wiki με αυτήν την διεύθυνση IP δεν μπορούν αυτή την στιγμή να δημιουργήσουν περισσότερους λογαριασμούς.",
        "emailauthenticated": "Η διεύθυνσή σας ηλεκτρονικού ταχυδρομείου επιβεβαιώθηκε στις $2 και ώρα $3.",
        "emailnotauthenticated": "Η ηλεκτρονική σας διεύθυνση δεν έχει επαληθευτεί ακόμα.\nΚανένα μήνυμα ηλεκτρονικού ταχυδρομείου δεν θα σταλεί για τις ακόλουθες λειτουργίες.",
        "noemailprefs": "Δεν έχει ορισθεί ηλεκτρονική διεύθυνση, οι λειτουργίες που ακολουθούν δεν θα είναι δυνατόν να ολοκληρωθούν.",
        "botpasswords-label-resetpassword": "Επαναφορά κωδικού",
        "botpasswords-label-grants": "Ισχύουσες άδειες:",
        "botpasswords-help-grants": "Κάθε παραχώρηση δίνει πρόσβαση στα ορισμένα δικαιώματα χρήστη που που ήδη έχει ένας λογαριασμός χρήστη. Δείτε τη [[Special:ListGrants|πίνακας παραχωρήσεων]] για περισσότερες πληροφορίες.",
-       "botpasswords-label-restrictions": "Περιορισμοί χρήσης:",
        "botpasswords-label-grants-column": "Χορηγήθηκε",
        "botpasswords-bad-appid": "Η ονομασία του ρομπότ «$1» δεν είναι έγκυρη.",
        "botpasswords-insert-failed": "Αποτυχία να προστεθεί το όνομα bot \"$1\". Έχει ήδη προστεθεί;",
        "botpasswords-updated-body": "Ο κωδικός πρόσβασης του ρομπότ «$1» του χρήστη «$2» ενημερώθηκε.",
        "botpasswords-deleted-title": "Ο κωδικός πρόσβασης του ρομπότ διαγράφτηκε",
        "botpasswords-deleted-body": "Ο κωδικός πρόσβασης για το όνομα ρομπότ \"$1\" του χρήστη \"$2\" διαγράφηκε.",
-       "botpasswords-newpassword": "Ο νέος κωδικός πρόσβασης για να συνδεθείτε με το <strong>$1</strong> είναι <strong>$2</strong>. <em>Παρακαλούμε σημειώστε το για μελλοντική αναφορά.</em>",
+       "botpasswords-newpassword": "Ο νέος κωδικός πρόσβασης για να συνδεθείτε με το <strong>$1</strong> είναι <strong>$2</strong>. <em>Παρακαλούμε σημειώστε το για μελλοντική αναφορά.</em><br />(Για παλιά bot που απαιτούν το όνομα σύνδεσης να είναι το ίδιο με το τελικό όνομα χρήστη, μπορείτε επίσης να χρησιμοποιήσετε το  <strong>$3</strong> ως όνομα χρήστη και <strong>$4</strong> ως κωδικό.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider δεν είναι διαθέσιμο.",
        "botpasswords-restriction-failed": "Περιορισμοί κωδικών πρόσβασης bot εμποδίζουν τη συγκεκριμένη σύνδεση.",
        "resetpass_forbidden": "Οι κωδικοί πρόσβασης δεν μπορούν να αλλαχθούν",
        "revdelete-unsuppress": "Αφαίρεσε περιορισμούς στις αποκατεστημένες αναθεωρήσεις",
        "revdelete-log": "Αιτία:",
        "revdelete-submit": "Εφαρμογή {{PLURAL:$1|στην επιλεγμένη αναθεώρηση|στις επιλεγμένες αναθεωρήσεις}}",
-       "revdelete-success": "'''Η ορατότητα έκδοσης ενημερώθηκε επιτυχώς.'''",
+       "revdelete-success": "Η ορατότητα έκδοσης ενημερώθηκε επιτυχώς.",
        "revdelete-failure": "'''Η ορατότητα της επεξεργασίας δεν ήταν δυνατόν να ενημερωθεί:''' $1",
        "logdelete-success": "Η ορατότητα γεγονότος τέθηκε επιτυχώς.",
        "logdelete-failure": "'''Η ορατότητα του καταλόγου δεν μπορούσε να ρυθμιστεί:'''\n$1",
        "apisandbox-sending-request": "Αποστολή αιτήματος API...",
        "apisandbox-loading-results": "Λήψη αποτελεσμάτων API...",
        "apisandbox-request-url-label": "Αίτηση URL:",
-       "apisandbox-request-time": "Χρόνος αιτήματος: $1",
+       "apisandbox-request-time": "Χρόνος αιτήματος: {{PLURAL:$1|$1 ms}}",
        "booksources": "Πηγές βιβλίων",
        "booksources-search-legend": "Αναζήτηση για πηγές βιβλίων",
        "booksources-isbn": "ISBN:",
        "sp-contributions-newbies-title": "Συνεισφορές χρηστών για νέους λογαριασμούς",
        "sp-contributions-blocklog": "αρχείο καταγραφών φραγών",
        "sp-contributions-suppresslog": "διαγεγραμμένες συνεισφορές {{GENDER:$1|χρήστη|χρήστριας}}",
-       "sp-contributions-deleted": "διαγεγραμμένες συνεισφορές χρήστη",
+       "sp-contributions-deleted": "διαγεγραμμένες συνεισφορές {{GENDER:$1|χρήστη|χρήστριας}}",
        "sp-contributions-uploads": "ανεβάσματα αρχείων",
        "sp-contributions-logs": "καταγραφές",
        "sp-contributions-talk": "συζήτηση",
        "import-nonewrevisions": "Καμία αναθεώρηση δεν εισήχθει (όλες είτε ήταν ήδη παρούσες, ή παραλήφθηκαν λόγω σφαλμάτων).",
        "xml-error-string": "$1 στη γραμμή $2, στήλη $3 (byte $4): $5",
        "import-upload": "Ανέβασμα δεδομένων XML",
-       "import-token-mismatch": "Απώλεια των στοιχείων της συνόδου. Παρακαλούμε προσπαθήστε ξανά.",
+       "import-token-mismatch": "Απώλεια δεδομένων περιόδου λειτουργίας.\n\nΜπορεί να έχουν αποσυνδεθεί. <strong>Παρακαλούμε βεβαιωθείτε ότι είστε ακόμα συνδεδεμένοι και προσπαθήστε ξανά</strong>.\nΑν εξακολουθεί να μην λειτουργεί, δοκιμάστε να [[Special:UserLogout|αποσυνδεθείτε]] και επανασυνδεθείτε, και ελέγξτε ότι ο browser σας επιτρέπει cookies από αυτό τον ιστοτόπο.",
        "import-invalid-interwiki": "Δεν είναι δυνατή η εισαγωγή από το καθορισμένο wiki.",
        "import-error-edit": "Η σελίδα «$1» δεν εισήχθη επειδή δεν σας επιτρέπεται να την επεξεργαστείτε.",
        "import-error-create": "Η σελίδα «$1» δεν εισήχθη επειδή δεν σας επιτρέπεται να την δημιουργήσετε.",
        "pageinfo-article-id": "Αναγνωριστικό σελίδας",
        "pageinfo-language": "Γλώσσα περιεχομένου σελίδας",
        "pageinfo-content-model": "Μοντέλο περιεχομένου σελίδας",
+       "pageinfo-content-model-change": "αλλαγή",
        "pageinfo-robot-policy": "Ευρετηρίαση από ρομπότ",
        "pageinfo-robot-index": "Επιτρεπτό",
        "pageinfo-robot-noindex": "Μη επιτρεπτό",
        "scarytranscludefailed-httpstatus": "[Η λήψη προτύπου απέτυχε για  το $1: HTTP  $2]",
        "scarytranscludetoolong": "[Η διεύθυνση URL είναι πολύ μεγάλη.]",
        "deletedwhileediting": "'''Προσοχή''': Αυτή η σελίδα έχει διαγραφεί αφότου ξεκινήσατε την επεξεργασία!",
-       "confirmrecreate": "Ο χρήστης [[User:$1|$1]] ([[User talk:$1|συζήτηση]]) διέγραψε αυτή τη σελίδα αφότου ξεκινήσατε την επεξεργασία με αιτιολόγηση:\n: ''$2''\nΠαρακαλώ επιβεβαιώστε ότι θέλετε πραγματικά να ξαναδημιουργήσετε αυτή τη σελίδα.",
-       "confirmrecreate-noreason": "Ο χρήστης [[User:$1|$1]] ([[User talk:$1|συζήτηση]]) διέγραψε αυτή τη σελίδα αφότου ξεκινήσατε την επεξεργασία.\nΠαρακαλούμε επιβεβαιώστε ότι θέλετε πραγματικά να ξαναδημιουργήσετε αυτή τη σελίδα.",
+       "confirmrecreate": "Ο χρήστης [[User:$1|$1]] ([[User talk:$1|συζήτηση]]) διέγραψε αυτή τη σελίδα αφότου ξεκινήσατε την επεξεργασία με αιτιολόγηση:\n: <em>$2</em>\nΠαρακαλούμε επιβεβαιώστε ότι θέλετε πραγματικά να ξαναδημιουργήσετε αυτή τη σελίδα.",
+       "confirmrecreate-noreason": "{{GENDER:$1|Ο χρήστης|Η χρήστρια}} [[User:$1|$1]] ([[User talk:$1|συζήτηση]]) διέγραψε αυτή τη σελίδα αφότου ξεκινήσατε την επεξεργασία.\nΠαρακαλούμε επιβεβαιώστε ότι θέλετε πραγματικά να ξαναδημιουργήσετε αυτή τη σελίδα.",
        "recreate": "Αναδημιουργία",
        "confirm_purge_button": "Εντάξει",
        "confirm-purge-top": "Καθαρισμός της λανθάνουσας μνήμης αυτής της σελίδας.",
        "tags-actions-header": "Ενέργειες",
        "tags-active-yes": "Ναι",
        "tags-active-no": "Όχι",
-       "tags-source-extension": "Î\9fÏ\81ιζÏ\8cμενη Î±Ï\80Ï\8c ÎµÏ\80έκÏ\84αÏ\83η",
+       "tags-source-extension": "Î\9fÏ\81ίζεÏ\84αι Î±Ï\80Ï\8c Ï\84ο Î»Î¿Î³Î¹Ï\83μικÏ\8c",
        "tags-source-manual": "Εφαρμοζόμενη με μη αυτόματο τρόπο από χρήστες και ρομπότ",
        "tags-source-none": "Όχι σε χρήση πλέον",
        "tags-edit": "επεξεργασία",
        "htmlform-title-not-exists": "Το $1 δεν υπάρχει.",
        "htmlform-user-not-exists": "Δεν υπάρχει χρήστης με όνομα <strong>$1</strong>.",
        "htmlform-user-not-valid": "Το <strong>$1</strong> δεν είναι έγκυρο όνομα χρήστη.",
-       "sqlite-has-fts": "$1 με υποστήριξη αναζήτησης πλήρους κειμένου",
-       "sqlite-no-fts": "$1 χωρίς την υποστήριξη αναζήτησης πλήρους κειμένου",
        "logentry-delete-delete": "{{GENDER:$2|Ο|Η}} $1 διέγραψε τη σελίδα $3",
        "logentry-delete-restore": "Ο/Η $1 αποκατέστησε τη σελίδα $3",
        "logentry-delete-event": "{{GENDER:$2|Ο|Η}} $1 άλλαξε την ορατότητα {{PLURAL:$5|ενός καταγραφόμενου συμβάντος|$5 καταγραφόμενων συμβάντων}} στο $3: $4",
index e04e21f..cbe755d 100644 (file)
        "botpasswords-label-resetpassword": "Reset the password",
        "botpasswords-label-grants": "Applicable grants:",
        "botpasswords-help-grants": "Each grant gives access to listed user rights that a user account already has. See the [[Special:ListGrants|table of grants]] for more information.",
-       "botpasswords-label-restrictions": "Usage restrictions:",
        "botpasswords-label-grants-column": "Granted",
        "botpasswords-bad-appid": "The bot name \"$1\" is not valid.",
        "botpasswords-insert-failed": "Failed to add bot name \"$1\". Was it already added?",
        "upload-dialog-disabled": "File uploads using this dialog are disabled on this wiki.",
        "upload-dialog-title": "Upload file",
        "upload-dialog-button-cancel": "Cancel",
+       "upload-dialog-button-back": "Back",
        "upload-dialog-button-done": "Done",
        "upload-dialog-button-save": "Save",
        "upload-dialog-button-upload": "Upload",
        "htmlform-cloner-create": "Add more",
        "htmlform-cloner-delete": "Remove",
        "htmlform-cloner-required": "At least one value is required.",
+       "htmlform-date-placeholder": "YYYY-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "YYYY-MM-DD HH:MM:SS",
+       "htmlform-date-invalid": "The value you specified is not a recognized date. Try using YYYY-MM-DD format.",
+       "htmlform-time-invalid": "The value you specified is not a recognized time. Try using HH:MM:SS format.",
+       "htmlform-datetime-invalid": "The value you specified is not a recognized date and time. Try using YYYY-MM-DD HH:MM:SS format.",
+       "htmlform-date-toolow": "The value you specified is before the earliest allowed date of $1.",
+       "htmlform-date-toohigh": "The value you specified is after the latest allowed date of $1.",
+       "htmlform-time-toolow": "The value you specified is before the earliest allowed time of $1.",
+       "htmlform-time-toohigh": "The value you specified is after the latest allowed time of $1.",
+       "htmlform-datetime-toolow": "The value you specified is before the earliest allowed date and time of $1.",
+       "htmlform-datetime-toohigh": "The value you specified is after the latest allowed date and time of $1.",
        "htmlform-title-badnamespace": "[[:$1]] is not in the \"{{ns:$2}}\" namespace.",
        "htmlform-title-not-creatable": "\"$1\" is not a creatable page title",
        "htmlform-title-not-exists": "$1 does not exist.",
        "unlinkaccounts-success": "The account was unlinked.",
        "authenticationdatachange-ignored": "The authentication data change was not handled. Maybe no provider was configured?",
        "userjsispublic": "Please note: JavaScript subpages should not contain confidential data as they are viewable by other users.",
-       "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users."
+       "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users.",
+       "restrictionsfield-badip": "Invalid IP address or range: $1",
+       "restrictionsfield-label": "Allowed IP ranges:",
+       "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index 7d9c922..a155ea3 100644 (file)
@@ -76,7 +76,7 @@
        "tog-enotifminoredits": "Sendi al mi ankaŭ retmesaĝojn pro malgrandaj redaktoj de paĝoj kaj dosieroj",
        "tog-enotifrevealaddr": "Malkaŝi mian retadreson en informaj retpoŝtaĵoj",
        "tog-shownumberswatching": "Montri la nombron da priatentaj uzantoj",
-       "tog-oldsig": "Ekzistanta subskribo:",
+       "tog-oldsig": "Via ekzistanta subskribo:",
        "tog-fancysig": "Trakti subskribon kiel vikitekston (sen aŭtomata ligo)",
        "tog-uselivepreview": "Uzadi tujan antaŭrigardon",
        "tog-forceeditsummary": "Averti min kiam mi konservas malplenan redaktoresumon",
@@ -93,7 +93,7 @@
        "tog-showhiddencats": "Montri kaŝitajn kategoriojn",
        "tog-norollbackdiff": "Ne montri diferencon post plenumado de ŝanĝomalfaro",
        "tog-useeditwarning": "Averti min kiam mi forlasas redaktan paĝon kun nekonservitaj ŝanĝoj",
-       "tog-prefershttps": "Ĉiam uzu sekuran konekton ensalutite",
+       "tog-prefershttps": "Ĉiam uzi sekuran konekton ensalutite",
        "underline-always": "Ĉiam",
        "underline-never": "Neniam",
        "underline-default": "Pravaloro laŭ foliumilo",
        "newwindow": "(en nova fenestro)",
        "cancel": "Nuligi",
        "moredotdotdot": "Pli...",
-       "morenotlisted": "Ĉi tiu listo ne estas kompleta.",
+       "morenotlisted": "Ĉi tiu listo povas esti nekompleta.",
        "mypage": "Paĝo",
        "mytalk": "Diskuto",
        "anontalk": "Diskuto",
        "eauthentsent": "Konfirma retmesaĝo estis sendita al la nomita retadreso. Antaŭ ol iu ajn alia mesaĝo estos sendita al la konto, vi devos sekvi la instrukciojn en la mesaĝo por konfirmi ke la konto ja estas via.",
        "throttled-mailpassword": "Retpoŝto kun reŝargita pasvorto estis jam sendita ene de la {{PLURAL:$1|lasta horo|lastaj $1 horoj}}.\nPor preventi misuzon, nur unu reŝargita pasvorto estos sendita dum {{PLURAL:$1|horo|$1 horoj}}.",
        "mailerror": "Okazis eraro sendante retpoŝtaĵon: $1",
-       "acct_creation_throttle_hit": "Vizitintoj al ĉi tiu vikio uzintaj vian IP-adreson kreis {{PLURAL:$1|1 konton|$1 kontojn}} dum la lasta tago, {{PLURAL:$1|kiu|kiuj}} estas la maksimume permesita en ĉi tiu tempoperiodo.\nTial, vizitantoj kun ĉi tiu IP-adreso ne povas krei pliajn kontojn ĉi-momente.",
+       "acct_creation_throttle_hit": "Vizitintoj al ĉi tiu vikio uzintaj vian IP-adreson kreis {{PLURAL:$1|1 konton|$1 kontojn}} dum la lasta $2, kio estas la maksimumo permesita en ĉi tiu tempoperiodo.\nTial, vizitantoj kun ĉi tiu IP-adreso ne povas krei pliajn kontojn ĉi-momente.",
        "emailauthenticated": "Via retadreso estis konfirmita ekde $2 je $3.",
        "emailnotauthenticated": "Via retadreso ne jam estas aŭtentigata.\nNeniu retpoŝto estos sendita por iu el la jenaj funkcioj.",
        "noemailprefs": "Donu retpoŝtan adreson en viaj preferoj, por ke ĉi tiuj funkcioj estu je dispono.",
        "botpasswords-label-resetpassword": "Rekomencigi la pasvorton",
        "botpasswords-label-grants": "Uzeblaj permesdonoj:",
        "botpasswords-help-grants": "Ĉiu permesdono provizas aliron al listitaj uzantaj permisoj, kiujn uzantkonto jam havas. Vidu la [[Special:ListGrants|tabelon de permesdonoj]] por pli da informo.",
-       "botpasswords-label-restrictions": "Limigoj de uzado:",
        "botpasswords-label-grants-column": "Permeso donita",
        "botpasswords-bad-appid": "La robota nomo \"$1\" estas malvalida.",
        "botpasswords-insert-failed": "Aldono de la robota nomo \"$1\" ne sukcesis. Ĉu ĝi jam estis aldonita?",
        "undeletedrevisions": "{{PLURAL:$1|1 versio restarigita|$1 versioj restarigitaj}}",
        "undeletedrevisions-files": "{{PLURAL:$1|1 versio|$1 versioj}} kaj {{PLURAL:$2|1 dosiero|$2 dosieroj}} restarigitaj",
        "undeletedfiles": "{{PLURAL:$1|1 dosiero restarigita|$1 dosieroj restarigitaj}}",
-       "cannotundelete": "Restarigo malsukcesis: \n$1",
+       "cannotundelete": "Iu aŭ ĉiuj restarigoj malsukcesis: \n$1",
        "undeletedpage": "'''$1 estis restarigita'''\n\nKonsultu la [[Special:Log/delete|deletion log]] por protokolo pri la lastatempaj forigoj kaj restarigoj.",
        "undelete-header": "Konsulti la [[Special:Log/delete|protokolo de forigoj]] por lastatempaj forigoj.",
        "undelete-search-title": "Serĉi forigitajn paĝojn",
        "tags-actions-header": "Agoj",
        "tags-active-yes": "Jes",
        "tags-active-no": "Ne",
-       "tags-source-extension": "Difinita de etendaĵo",
+       "tags-source-extension": "Difinita de la programo",
        "tags-source-manual": "Aldonita permane de uzantoj aŭ robotoj",
        "tags-source-none": "Ne plu uzata",
        "tags-edit": "redakti",
index 814fcf8..922729f 100644 (file)
        "talk": "Discusión",
        "views": "Vistas",
        "toolbox": "Herramientas",
+       "tool-link-userrights": "Modificar grupos {{GENDER:$1|del usuario|de la usuaria}}",
+       "tool-link-emailuser": "Enviar un correo a {{GENDER:$1|este usuario|esta usuaria}}",
        "userpage": "Ver página de usuario",
        "projectpage": "Ver página del proyecto",
        "imagepage": "Ver página del archivo",
        "botpasswords-label-resetpassword": "Restablecer la contraseña",
        "botpasswords-label-grants": "Permisos aplicables:",
        "botpasswords-help-grants": "Cada concesión le da acceso a los permisos listados que el usuario ya posea. Véase la [[Special:ListGrants|lista de concesiones]] para más información.",
-       "botpasswords-label-restrictions": "Restricciones de uso:",
        "botpasswords-label-grants-column": "Concedido",
        "botpasswords-bad-appid": "El nombre del bot \"$1\" no es válido.",
        "botpasswords-insert-failed": "No se pudo agregar el nombre del bot \"$1\". ¿Ya ha sido añadido?",
        "passwordreset-emailelement": "Nombre de {{GENDER:$1|usuario|usuaria}}: \n$1\n\nContraseña temporal: \n$2",
        "passwordreset-emailsentemail": "Si esta dirección de correo electrónico está asociada a tu cuenta, entonces se enviará un correo electrónico para restablecer la contraseña.",
        "passwordreset-emailsentusername": "Si existe una dirección de correo electrónico asociada a este nombre de usuario, entonces se enviará un correo para restablecer la contraseña.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|El e-mail de restablecimiento de contraseña ha sido enviado|Los e-mails de restablecimiento de contraseña han sido enviados}}. {{PLURAL:$1|El nombre de usuario y la contraseña se muestra a continuación|La lista de nombres de usuarios y contraseñas se muestra a continuación}}.",
-       "passwordreset-emailerror-capture2": "No fue posible mandar un correo electrónico {{Gender:$2|al usuario|a la usuaria}}: $1 {{PLURAL:$3|El nombre de usuario y la contraseña|La lista de nombres de usuarios y contraseñas}} se muestra a continuación.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|El e-mail de restablecimiento de contraseña ha sido enviado|Los e-mails de restablecimiento de contraseña han sido enviados}}. {{PLURAL:$1|El nombre de usuario y la contraseña se muestra|La lista de nombres de usuarios y contraseñas se muestra}} aquí.",
+       "passwordreset-emailerror-capture2": "No fue posible mandar un correo electrónico {{GENDER:$2|al usuario|a la usuaria}}: $1 {{PLURAL:$3|El nombre de usuario y la contraseña|La lista de nombres de usuarios y contraseñas}} se muestra aquí.",
        "passwordreset-nocaller": "Debe de proporcionarse un interlocutor",
        "passwordreset-nosuchcaller": "La persona que llama no existe: $1",
        "passwordreset-ignored": "No se logró el reestablecimiento de la contraseña. ¿Tal vez no se configuró un proveedor?",
        "tags-actions-header": "Acciones",
        "tags-active-yes": "Sí",
        "tags-active-no": "No",
-       "tags-source-extension": "Definida por una extensión",
+       "tags-source-extension": "Definida por el software",
        "tags-source-manual": "Aplicada manualmente por usuarios y bots",
        "tags-source-none": "No se utiliza más",
        "tags-edit": "editar",
index 863e3d7..37a6d51 100644 (file)
@@ -80,7 +80,7 @@
        "tog-enotifminoredits": "Lähetä sähköpostiviesti myös pienistä muokkauksista",
        "tog-enotifrevealaddr": "Näytä sähköpostiosoitteeni muille lähetetyissä ilmoituksissa",
        "tog-shownumberswatching": "Näytä sivua tarkkailevien käyttäjien määrä",
-       "tog-oldsig": "Nykyinen allekirjoitus:",
+       "tog-oldsig": "Nykyinen allekirjoituksesi:",
        "tog-fancysig": "Muotoilematon allekirjoitus ilman automaattista linkkiä",
        "tog-uselivepreview": "Käytä välitöntä esikatselua",
        "tog-forceeditsummary": "Huomauta minua, jos en ole kirjoittanut yhteenvetoa",
        "newwindow": "(avautuu uuteen ikkunaan)",
        "cancel": "Peruuta",
        "moredotdotdot": "Lisää...",
-       "morenotlisted": "Tämä luettelo ei ole täydellinen.",
+       "morenotlisted": "Tämä luettelo ei ehkä ole täydellinen.",
        "mypage": "Käyttäjäsivu",
        "mytalk": "Keskustelu",
        "anontalk": "Keskustelu",
        "eauthentsent": "Varmennussähköposti on lähetetty annettuun sähköpostiosoitteeseen.\nMuita viestejä ei lähetetä, ennen kuin olet toiminut viestin ohjeiden mukaan ja varmistanut, että sähköpostiosoite kuuluu sinulle.",
        "throttled-mailpassword": "Salasananpalautusviesti on lähetetty {{PLURAL:$1|kuluvan|kuluvien $1}} tunnin aikana. Salasananpalautusviestejä lähetetään enintään {{PLURAL:$1|tunnin|$1 tunnin}} välein.",
        "mailerror": "Virhe lähetettäessä sähköpostia: $1",
-       "acct_creation_throttle_hit": "IP-osoitteestasi on luotu tähän wikiin jo {{PLURAL:$1|yksi tunnus|$1 tunnusta}} päivän aikana, joka suurin sallittu määrä tälle ajalle.\nTästä johtuen tästä IP-osoitteesta ei voi tällä hetkellä luoda uusia tunnuksia.",
+       "acct_creation_throttle_hit": "IP-osoitteestasi on luotu tähän wikiin jo {{PLURAL:$1|yksi tunnus|$1 tunnusta}} viimeisen $2 aikana, joka on suurin sallittu määrä tälle ajalle.\nTästä johtuen tästä IP-osoitteesta ei voi tällä hetkellä luoda uusia tunnuksia.",
        "emailauthenticated": "Sähköpostiosoitteesi varmennettiin $2 kello $3.",
        "emailnotauthenticated": "Sähköpostiosoitettasi ei ole vielä varmennettu.\nSähköpostia ei lähetetä liittyen alla oleviin toimintoihin.",
        "noemailprefs": "Sähköpostiosoitetta ei ole määritelty.",
        "botpasswords-label-delete": "Poista",
        "botpasswords-label-resetpassword": "Hanki uusi salasana",
        "botpasswords-label-grants": "Valittavissa olevat toimintaoikeudet:",
-       "botpasswords-label-restrictions": "Käyttörajoitukset:",
        "botpasswords-label-grants-column": "Myönnetään",
        "botpasswords-bad-appid": "Botin nimi \"$1\" ei kelpaa.",
        "botpasswords-insert-failed": "Botin nimen \"$1\" lisääminen epäonnsitui. Onko se jo lisätty?",
index f07e45c..cb79079 100644 (file)
        "eauthentsent": "Un courriel de confirmation a été envoyé à l’adresse indiquée.\nAvant qu’un autre courriel ne soit envoyé à ce compte, vous devrez suivre les instructions du courriel et confirmer que le compte est bien le vôtre.",
        "throttled-mailpassword": "Un courriel de réinitialisation de votre mot de passe a déjà été envoyé durant {{PLURAL:$1|la dernière heure|les $1 dernières heures}}. \nAfin d’éviter les abus, un seul courriel de réinitialisation de votre mot de passe sera envoyé par {{PLURAL:$1|heure|intervalle de $1 heures}}.",
        "mailerror": "Erreur lors de l’envoi du courriel : $1",
-       "acct_creation_throttle_hit": "Les visiteurs de ce wiki qui utilisent votre adresse IP ont créé {{PLURAL:$1|un compte|$1 comptes}} au cours des dernières 24 heures, ce qui est la limite maximale autorisée dans cet intervalle de temps.\nPar conséquent, la création de comptes pour les visiteurs utilisant cette adresse IP est temporairement suspendue.",
+       "acct_creation_throttle_hit": "Les visiteurs de ce wiki qui utilisent votre adresse IP ont créé {{PLURAL:$1|un compte|$1 comptes}} durant les dernières $2, ce qui est la limite maximale autorisée dans cet intervalle de temps.\nPar conséquent, la création de comptes pour les visiteurs utilisant cette adresse IP est temporairement suspendue.",
        "emailauthenticated": "Votre adresse de courriel a été confirmée le $2 à $3.",
        "emailnotauthenticated": "Votre adresse de courriel n’est pas encore confirmée.\nAucun courriel ne sera envoyé pour chacune des fonctions suivantes.",
        "noemailprefs": "Indiquez une adresse de courriel dans vos préférences pour utiliser ces fonctions.",
        "botpasswords-label-resetpassword": "Réinitialiser le mot de passe",
        "botpasswords-label-grants": "Droits applicables :",
        "botpasswords-help-grants": "Chaque droit accordé donne accès à la liste des droits utilisateurs dont l’utilisateur dispose déjà. Voyez le [[Special:ListGrants|tableau des droits]] pour plus d’informations.",
-       "botpasswords-label-restrictions": "Restrictions d’utilisation :",
        "botpasswords-label-grants-column": "Accordé",
        "botpasswords-bad-appid": "Le nom de robot « $1 » n’est pas valide.",
        "botpasswords-insert-failed": "Échec de l’ajout du nom de robot « $1 ». A-t-il déjà été ajouté ?",
        "passwordreset-emailelement": "Nom d’utilisateur : \n$1\n\nMot de passe temporaire : \n$2",
        "passwordreset-emailsentemail": "Si cette adresse de courriel est associée à votre compte, alors un courriel de réinitialisation de mot de passe sera envoyé.",
        "passwordreset-emailsentusername": "S’il y a une adresse de courriel associée à ce nom d’utilisateur, alors un courriel de réinitialisation de mot de passe sera envoyé.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Le courriel de réinitialisation du mot de passe a été envoyé|Les courriels de réinitialisation du mot de passe ont été envoyés}}. {{PLURAL:$1|Le nom d’utilisateur et le mot de passe sont affichés|La liste des noms d’utilisateur et mots de passe est affichée}} ci-dessous.",
-       "passwordreset-emailerror-capture2": "L’envoi de courriel à {{GENDER:$2|l’utilisateur|l’utilisatrice}} a échoué : $1 {{PLURAL:$3|Le nom d’utilisateur et le mot de passe sont affichés|La liste des noms d’utilisateur et des mots de passe est affichée}} ci-dessous.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Le courriel de réinitialisation du mot de passe a été envoyé|Les courriels de réinitialisation du mot de passe ont été envoyés}}. {{PLURAL:$1|Le nom d’utilisateur et le mot de passe sont affichés|La liste des noms d’utilisateur et mots de passe est affichée}} ici.",
+       "passwordreset-emailerror-capture2": "L’envoi de courriel à {{GENDER:$2|l’utilisateur|l’utilisatrice}} a échoué : $1 {{PLURAL:$3|Le nom d’utilisateur et le mot de passe sont affichés|La liste des noms d’utilisateur et des mots de passe est affichée}} ici.",
        "passwordreset-nocaller": "Un appelant doit être fourni",
        "passwordreset-nosuchcaller": "L’appelant n’existe pas : $1",
        "passwordreset-ignored": "La réinitialisation du mot de passe n’a pas été gérée. Peut-être qu’aucun fournisseur n’a été configuré ?",
        "exif-gpsdifferential": "Correction différentielle GPS",
        "exif-jpegfilecomment": "Commentaire de fichier JPEG",
        "exif-keywords": "Mots-clés",
-       "exif-worldregioncreated": "Région du monde dans laquelle la photo a été prise",
+       "exif-worldregioncreated": "Région du monde  la photo a été prise",
        "exif-countrycreated": "Pays dans lequel la photo a été prise",
        "exif-countrycodecreated": "Code du pays dans lequel la photo a été prise",
        "exif-provinceorstatecreated": "Province ou État dans lequel la photo a été prise",
        "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",
        "tags-title": "Balises",
-       "tags-intro": "Cette page liste les balises que le logiciel peut utiliser pour marquer une modification et la signification de chacune.",
+       "tags-intro": "Cette page liste les balises que le logiciel peut utiliser pour marquer une modification et la signification de chacune d’elles.",
        "tags-tag": "Nom de la balise",
        "tags-display-header": "Apparence dans les listes de modifications",
        "tags-description-header": "Description complète de la balise",
        "tags-active-yes": "Oui",
        "tags-active-no": "Non",
        "tags-source-extension": "Défini par le logiciel",
-       "tags-source-manual": "Appliquée manuellement par les utilisateurs et les bots",
+       "tags-source-manual": "Appliquée manuellement par les utilisateurs et les robots",
        "tags-source-none": "Obsolète",
        "tags-edit": "modifier",
        "tags-delete": "supprimer",
        "tags-manage-no-permission": "Vous n'avez pas la permission de gérer les modifications de balises.",
        "tags-manage-blocked": "Vous ne pouvez pas accéder à l’interface de modification des balises lorsque vous êtes bloqué{{GENDER:||e}}.",
        "tags-create-heading": "Créer une nouvelle balise",
-       "tags-create-explanation": "Par défaut, les nouvelles balises créées seront disponibles pour les utilisateurs et les bots.",
+       "tags-create-explanation": "Par défaut, les nouvelles balises créées seront disponibles pour les utilisateurs et les robots.",
        "tags-create-tag-name": "Nom de la balise :",
        "tags-create-reason": "Raison :",
        "tags-create-submit": "Créer",
        "unlinkaccounts-success": "Le compte a été dissocié.",
        "authenticationdatachange-ignored": "Les modifications de données d’authentification n’ont pas été gérées. Peut-être aucun fournisseur n’a-t-il été configuré ?",
        "userjsispublic": "Veuillez noter: les sous-pages JavaScript ne doivent pas contenir de données confidentielles parce qu'elles sont visibles des autres utilisateurs.",
-       "usercssispublic": "Veuillez noter: les sous-pages CSS ne doivent pas contenir de données confidentielles parce qu'elles sont visibles des autres utilisateurs."
+       "usercssispublic": "Veuillez noter: les sous-pages CSS ne doivent pas contenir de données confidentielles parce qu'elles sont visibles des autres utilisateurs.",
+       "restrictionsfield-badip": "Adresse IP ou plage non valide : $1",
+       "restrictionsfield-label": "Plages IP autorisées :",
+       "restrictionsfield-help": "Une adresse IP ou une plage CIDR par ligne. Pour tout activer, utiliser <br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index cf91cbe..c1dcd52 100644 (file)
        "tog-minordefault": "Marcar todas as edicións como pequenas por defecto",
        "tog-previewontop": "Mostrar a vista previa antes da caixa de edición",
        "tog-previewonfirst": "Mostrar a vista previa na primeira edición",
-       "tog-enotifwatchlistpages": "Desexo recibir un aviso por correo electrónico cando unha páxina ou un ficheiro da miña lista de vixilancia sufra algún cambio",
-       "tog-enotifusertalkpages": "Desexo recibir un aviso por correo electrónico cando a miña páxina de conversa cambie",
-       "tog-enotifminoredits": "Enviádeme tamén unha mensaxe de correo electrónico cando se produzan edicións pequenas nas páxinas ou nos ficheiros",
+       "tog-enotifwatchlistpages": "Recibir un aviso por correo electrónico cando unha páxina ou un ficheiro da miña lista de vixilancia sufra algún cambio",
+       "tog-enotifusertalkpages": "Recibir un aviso por correo electrónico cando a miña páxina de conversa sufra algún cambio",
+       "tog-enotifminoredits": "Recibir tamén unha mensaxe de correo electrónico cando se produzan edicións pequenas nas páxinas ou nos ficheiros",
        "tog-enotifrevealaddr": "Revelar o meu enderezo de correo electrónico nos correos de notificación",
        "tog-shownumberswatching": "Mostrar o número de usuarios que están a vixiar",
        "tog-oldsig": "A súa sinatura actual:",
        "tog-fancysig": "Tratar a sinatura como se fose texto wiki (sen ligazón automática)",
        "tog-uselivepreview": "Usar a vista previa en tempo real",
-       "tog-forceeditsummary": "Avisádeme cando o campo resumo estea baleiro",
+       "tog-forceeditsummary": "Avisar cando o campo resumo estea baleiro",
        "tog-watchlisthideown": "Agochar as edicións propias na lista de vixilancia",
        "tog-watchlisthidebots": "Agochar as edicións dos bots na lista de vixilancia",
        "tog-watchlisthideminor": "Agochar as edicións pequenas na lista de vixilancia",
        "tog-watchlisthideanons": "Agochar as edicións dos usuarios anónimos na lista de vixilancia",
        "tog-watchlisthidepatrolled": "Agochar as edicións patrulladas na lista de vixilancia",
        "tog-watchlisthidecategorization": "Agochar a categorización das páxinas",
-       "tog-ccmeonemails": "Enviádeme ao meu enderezo unha copia das mensaxes de correo electrónico que envíe a outros usuarios",
+       "tog-ccmeonemails": "Recibir no meu enderezo unha copia das mensaxes de correo electrónico que envíe a outros usuarios",
        "tog-diffonly": "Non mostrar o contido da páxina debaixo das diferenzas entre edicións",
        "tog-showhiddencats": "Mostrar as categorías ocultas",
        "tog-norollbackdiff": "Omitir as diferenzas despois de levar a cabo unha reversión de edicións",
-       "tog-useeditwarning": "Avisádeme cando deixe unha páxina de edición cos cambios sen gardar",
-       "tog-prefershttps": "Utilizar sempre unha conexión segura mentres acceda ao sistema",
+       "tog-useeditwarning": "Avisar ao deixar unha páxina de edición cos cambios sen gardar",
+       "tog-prefershttps": "Utilizar sempre unha conexión segura para acceder ao sistema",
        "underline-always": "Sempre",
        "underline-never": "Nunca",
        "underline-default": "Opción predeterminada da aparencia ou do navegador",
        "views": "Vistas",
        "toolbox": "Ferramentas",
        "tool-link-userrights": "Modificar os grupos {{GENDER:$1|do usuario|da usuaria}}",
-       "tool-link-emailuser": "Enviar un correo electrónico {{GENDER:$1|ó usuario|á usuaria}}",
+       "tool-link-emailuser": "Enviar un correo electrónico {{GENDER:$1|ao usuario|á usuaria}}",
        "userpage": "Ver a páxina {{GENDER:{{BASEPAGENAME}}|do usuario|da usuaria}}",
        "projectpage": "Ver a páxina do proxecto",
        "imagepage": "Ver a páxina do ficheiro",
        "databaseerror-query": "Pescuda: $1",
        "databaseerror-function": "Función: $1",
        "databaseerror-error": "Erro: $1",
-       "transaction-duration-limit-exceeded": "Para evitar crear un gran atraso na replicación, esta transacción abortouse xa que a duración de escritura ($1) excedeu o límite de $2 {{PLURAL:$2|segundo|segundos}} .\nSe está a cambiar moitos obxectos ao mesmo tempo, procure facer operacións múltiples máis pequenas no seu lugar.",
+       "transaction-duration-limit-exceeded": "Para evitar crear un grande atraso na replicación, esta transacción abortouse xa que a duración de escritura ($1) excedeu o límite de $2 segundos.\nSe está a cambiar moitos obxectos ao mesmo tempo, procure facer operacións múltiples máis pequenas no seu lugar.",
        "laggedslavemode": "'''Aviso:''' A páxina pode non conter as actualizacións recentes.",
        "readonly": "Base de datos pechada",
        "enterlockreason": "Dea unha razón para o peche, incluíndo unha estimación de até cando se manterá",
        "createacct-reason-ph": "Por que crea outra conta?",
        "createacct-reason-help": "Mensaxe que se mostra no rexistro de creación de contas",
        "createacct-submit": "Crear a conta",
-       "createacct-another-submit": "Crear conta",
+       "createacct-another-submit": "Crear conta",
        "createacct-continue-submit": "Continuar a creación da conta",
        "createacct-another-continue-submit": "Continuar a creación da conta",
        "createacct-benefit-heading": "Xente coma vostede elabora {{SITENAME}}.",
        "eauthentsent": "Envióuselle un correo electrónico de confirmación ao enderezo especificado.\nAntes de que se lle envíe calquera outro correo a esta conta terá que seguir as instrucións que aparecen nesa mensaxe para confirmar que a conta é realmente súa.",
        "throttled-mailpassword": "Enviouse un correo electrónico de restablecemento do contrasinal {{PLURAL:$1|na última hora|nas últimas $1 horas}}.\nPara evitar o abuso do sistema só se enviará unha mensaxe de restablecemento cada {{PLURAL:$1|hora|$1 horas}}.",
        "mailerror": "Produciuse un erro ao enviar o correo electrónico: $1",
-       "acct_creation_throttle_hit": "Alguén que visitou este wiki co seu enderezo IP creou, no último día, {{PLURAL:$1|unha conta|$1 contas}}, que é o máximo permitido neste período de tempo.\nComo resultado, os visitantes que usen este enderezo IP non poden crear máis contas nestes intres.",
+       "acct_creation_throttle_hit": "Alguén que visitou este wiki co seu enderezo IP creou {{PLURAL:$1|unha conta|$1 contas}} nos últimos ̩$2 días, que é o máximo permitido neste período de tempo.\nComo resultado, os visitantes que usen este enderezo IP non poden crear máis contas nestes intres.",
        "emailauthenticated": "O seu enderezo de correo electrónico foi confirmado o $2 ás $3.",
        "emailnotauthenticated": "O seu enderezo de correo electrónico aínda non foi confirmado.\nNon se enviará ningunha mensaxe por ningunha das seguintes características.",
        "noemailprefs": "Especifique un enderezo de correo electrónico se quere que funcione esta opción.",
        "resetpass_submit": "Establecer o contrasinal e acceder ao sistema",
        "changepassword-success": "O seu contrasinal foi modificado!",
        "changepassword-throttled": "Fixo demasiados intentos de acceder ao sistema.\nPor favor, agarde $1 antes de probar outra vez.",
-       "botpasswords": "Contrasinais de Bot",
-       "botpasswords-summary": "Os <em>contrasinais de Bot</em> permiten acceder a unha conta de usuario por medio da API sen usar as crecenciais de acceso da conta principal. Os dereitos de usuario dispoñibles cando se accede ao sistema cun contrasinal de bot poden estar restrinxidos.",
-       "botpasswords-disabled": "Os contrasinais de bot non están habilitados.",
-       "botpasswords-no-central-id": "Para usar contrasinais de bot debes acceder ao sistema cunha conta centralizada.",
-       "botpasswords-existing": "Contrasinais de bot existentes",
+       "botpasswords": "Contrasinais de bots",
+       "botpasswords-summary": "Os <em>contrasinais de bots</em> permiten acceder a unha conta de usuario por medio da API sen usar as crecenciais de acceso da conta principal. Os dereitos de usuario dispoñibles cando se accede ao sistema cun contrasinal de bot poden estar restrinxidos.\n\nSe non sabe por que quere facer isto, probablemente signifique que non o queira facer. Ningunha persoa debería pedirlle a vostede que xere unha destas claves para entregarlla.",
+       "botpasswords-disabled": "Os contrasinais de bots non están habilitados.",
+       "botpasswords-no-central-id": "Para usar os contrasinais de bots debe acceder ao sistema cunha conta centralizada.",
+       "botpasswords-existing": "Contrasinais de bots existentes",
        "botpasswords-createnew": "Crear un novo contrasinal de bot",
        "botpasswords-editexisting": "Editar un contrasinal de bot xa existente",
        "botpasswords-label-appid": "Nome do bot:",
        "botpasswords-label-delete": "Borrar",
        "botpasswords-label-resetpassword": "Restablecer o contrasinal",
        "botpasswords-label-grants": "Permisos aplicables:",
-       "botpasswords-help-grants": "Cada permiso da acceso aos permisos de usuario listados que a conta xa teña. Vexa a [[Special:ListGrants|táboa de permisos]] para máis información.",
-       "botpasswords-label-restrictions": "Restriccións de uso:",
+       "botpasswords-help-grants": "Cada permiso dá acceso aos permisos de usuario listados que a conta xa teña. Consulte a [[Special:ListGrants|táboa de permisos]] para obter máis información.",
        "botpasswords-label-grants-column": "Concedido",
        "botpasswords-bad-appid": "O nome de bot \"$1\" non é válido.",
        "botpasswords-insert-failed": "Erro ao engadir o nome de bot \"$1\". Revise se xa foi engadido previamente.",
        "botpasswords-update-failed": "Erro ao actualizar o nome de bot \"$1\". Revise se foi borrado.",
-       "botpasswords-created-title": "Contrasinal de bot creado",
+       "botpasswords-created-title": "Creouse o contrasinal de bot",
        "botpasswords-created-body": "Creouse o contrasinal para o bot de nome \"$1\" do usuario \"$2\".",
-       "botpasswords-updated-title": "Contrasinal de bot actualizado",
-       "botpasswords-updated-body": "O contrasinal de bot do bot de nome \"$1\" do usuario \"$2\" foi actualizado.",
-       "botpasswords-deleted-title": "Contrasinal de bot borrado",
-       "botpasswords-deleted-body": "O contrasinal de bot do bot de nome \"$1\" do usuario \"$2\" foi borrado.",
-       "botpasswords-newpassword": "O novo contrasinal para acceder con <strong>$1</strong> é <strong>$2</strong>. <em>Por favor, rexistre isto para referencias futuras.</em><br />(Para bots vellos que requiren que o nome de acceso sexa o mesmo que o nome de usuario eventual, pode usar tamén <strong>$3</strong> como nome de usuario e <strong>$4</strong>  como contrasinal.)",
+       "botpasswords-updated-title": "Actualizouse o contrasinal de bot",
+       "botpasswords-updated-body": "Actualizouse o contrasinal para o bot de nome \"$1\" do usuario \"$2\".",
+       "botpasswords-deleted-title": "Borrouse o contrasinal de bot",
+       "botpasswords-deleted-body": "Borrouse o contrasinal para o bot de nome \"$1\" do usuario \"$2\".",
+       "botpasswords-newpassword": "O novo contrasinal para acceder con <strong>$1</strong> é <strong>$2</strong>. <em>Por favor, conserve isto para referencias futuras.</em><br />(Para os bots vellos que requiren que o nome de acceso sexa o mesmo que o nome de usuario eventual, pode usar tamén <strong>$3</strong> como nome de usuario e <strong>$4</strong> como contrasinal.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider non está dispoñible.",
-       "botpasswords-restriction-failed": "Restricións de contrasinal de bots evitaron esta conexión.",
+       "botpasswords-restriction-failed": "Algunhas restricións de contrasinal de bots evitaron esta conexión.",
        "botpasswords-invalid-name": "O nome de usuario especificado non contén o separador de contrasinal de bot (\"$1\").",
        "botpasswords-not-exist": "O usuario \"$1\" non ten un contrasinal de bot de nome \"$2\".",
        "resetpass_forbidden": "Non se poden mudar os contrasinais",
        "passwordreset-emailelement": "Nome de usuario: \n$1\n\nContrasinal temporal: \n$2",
        "passwordreset-emailsentemail": "Se esta é unha dirección de correo electrónico asociada á súa conta, entón enviarase un correo electrónico para o restablecemento do seu contrasinal.",
        "passwordreset-emailsentusername": "Se hai unha dirección de correo electrónico asociada con este nome de usuario, entón enviarase un correo electrónico para o restablecemento do contrasinal.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|O correo de reinicialización do contrasinal foi enviado|Os correos de reinicialización do contrasinal foron enviados}}. {{PLURAL:$1|O nome de usuario e contrasinal móstrase abaixo|A lista de nomes de usuarios e contrasinais móstranse abaixo}}.",
-       "passwordreset-emailerror-capture2": "O envío do correo {{GENDER:$2|ó usuario|á usuaria}} fallou: $1 {{PLURAL:$3|O nome de usuario e contrasinal móstrase abaixo|A lista de usuarios e contrasinais móstranse abaixo}}.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|O correo de reinicialización do contrasinal foi enviado|Os correos de reinicialización do contrasinal foron enviados}}. {{PLURAL:$1|O nome de usuario e contrasinal móstrase aquí|A lista de nomes de usuarios e contrasinais móstranse aquí}}.",
+       "passwordreset-emailerror-capture2": "O envío do correo {{GENDER:$2|ó usuario|á usuaria}} fallou: $1 {{PLURAL:$3|O nome de usuario e contrasinal móstrase aquí|A lista de usuarios e contrasinais móstranse aquí}}.",
        "passwordreset-nocaller": "Cómpre proporcionar un chamador",
        "passwordreset-nosuchcaller": "O chamador non existe: $1",
        "passwordreset-ignored": "A reinicialización do contrasinal non puido realizarse. Quizais non configurou o provedor?",
        "savearticle": "Gardar a páxina",
        "savechanges": "Gardar os cambios",
        "publishpage": "Publicar a páxina",
-       "publishchanges": "Publicar cambios",
+       "publishchanges": "Publicar os cambios",
        "preview": "Vista previa",
        "showpreview": "Mostrar a vista previa",
        "showdiff": "Mostrar os cambios",
        "prefs-resetpass": "Cambiar o contrasinal",
        "prefs-changeemail": "Cambiar ou eliminar o enderezo de correo electrónico",
        "prefs-setemail": "Establecer un enderezo de correo electrónico",
-       "prefs-email": "Opcións de correo electrónico",
+       "prefs-email": "Opcións do correo electrónico",
        "prefs-rendering": "Aparencia",
        "saveprefs": "Gardar",
        "restoreprefs": "Restaurar todas as preferencias por defecto (en todas as seccións)",
        "badsig": "Sinatura non válida; comprobe o código HTML utilizado.",
        "badsiglength": "A súa sinatura é demasiado longa.\nHa de ter menos {{PLURAL:$1|dun carácter|de $1 caracteres}}.",
        "yourgender": "Cal das seguintes oracións referidas a vostede é a máis axeitada?",
-       "gender-unknown": "Ao mencionarlle, o software empregará verbas de xénero neutral sempre que sexa posible",
+       "gender-unknown": "Ao facer mención á súa persoa, o software empregará verbas de xénero neutro sempre que sexa posible",
        "gender-male": "El edita as páxinas do wiki",
        "gender-female": "Ela edita as páxinas do wiki",
        "prefs-help-gender": "Definir esta preferencia é opcional.\nO software usa este valor para dirixirse á súa persoa e para facerlle mencións mediante o xénero gramatical axeitado.\nEsta información será pública.",
        "email": "Correo electrónico",
-       "prefs-help-realname": "O nome real é opcional.\nEn caso de revelalo, utilizarase para atribuírlle o seu traballo.",
+       "prefs-help-realname": "O nome real é opcional.\nEn caso de revelalo, ha utilizarse para atribuírlle o seu traballo.",
        "prefs-help-email": "O enderezo de correo electrónico é opcional, pero permite que se lle envíe un contrasinal novo se se esquece del.",
-       "prefs-help-email-others": "Tamén pode optar por deixar aos outros que se poidan poñer en contacto con vostede a través da súa páxina de usuario sen necesidade de revelar a súa identidade.",
+       "prefs-help-email-others": "Tamén pode optar por deixar que outras persoas se poñan en contacto con vostede a través dunha ligazón na súa páxina de usuario e de conversa.\nO seu enderezo non se revela cando contacten con vostede.",
        "prefs-help-email-required": "Cómpre o enderezo de correo electrónico.",
        "prefs-info": "Información básica",
        "prefs-i18n": "Internacionalización",
        "upload-form-label-infoform-description-tooltip": "Describa brevemente todo o destacable acerca do traballo.\nPara unha foto, mencione as cousas principais que se representan, a ocasión ou o lugar.",
        "upload-form-label-usage-title": "Uso",
        "upload-form-label-usage-filename": "Nome do ficheiro",
-       "upload-form-label-own-work": "Isto é o meu propio traballo",
+       "upload-form-label-own-work": "Isto é unha obra propia",
        "upload-form-label-infoform-categories": "Categorías",
        "upload-form-label-infoform-date": "Data",
        "upload-form-label-own-work-message-generic-local": "Confirmo que estou a cargar este ficheiro seguindo os termos de uso e políticas de licenza de {{SITENAME}}.",
        "protectedpages-performer": "Protector",
        "protectedpages-params": "Parámetros da protección",
        "protectedpages-reason": "Motivo",
-       "protectedpages-submit": "Mostrar páxinas",
+       "protectedpages-submit": "Mostrar as páxinas",
        "protectedpages-unknown-timestamp": "Descoñecido",
        "protectedpages-unknown-performer": "Usuario descoñecido",
        "protectedtitles": "Títulos protexidos",
        "protectedtitles-summary": "Esta páxina lista os títulos que están protexidos actualmente fronte á creación. Para obter unha lista de páxinas existentes protexidas, consulte [[{{#special:ProtectedPages}}|{{int:protectedpages}}]].",
        "protectedtitlesempty": "Actualmente non hai ningún título protexido con eses parámetros.",
-       "protectedtitles-submit": "Mostrar títulos",
+       "protectedtitles-submit": "Mostrar os títulos",
        "listusers": "Lista de usuarios",
        "listusers-editsonly": "Mostrar só os usuarios con edicións",
        "listusers-creationsort": "Ordenar por data de creación",
        "nopagetext": "A páxina que especificou non existe.",
        "pager-newer-n": "{{PLURAL:$1|unha posterior|$1 posteriores}}",
        "pager-older-n": "{{PLURAL:$1|unha anterior|$1 anteriores}}",
-       "suppress": "Supresor",
+       "suppress": "Suprimir",
        "querypage-disabled": "Esta páxina especial está desactivada por razóns de rendemento.",
        "apihelp": "Axuda coa API",
        "apihelp-no-such-module": "Non se atopou o módulo \"$1\".",
        "unlinkaccounts-success": "A conta foi desvinculada.",
        "authenticationdatachange-ignored": "Os cambios de datos de autenticación non foron xerados. Está configurado o provedor?",
        "userjsispublic": "Lembre: As subpáxinas JavaScript non deberían conter datos confidenciais porque outros usuarios poden velos.",
-       "usercssispublic": "Lembre: As subpáxinas CSS non deberían conter datos confidenciais porque outros usuarios poden velos."
+       "usercssispublic": "Lembre: As subpáxinas CSS non deberían conter datos confidenciais porque outros usuarios poden velos.",
+       "restrictionsfield-badip": "Enderezo IP ou rango de IP non válido: $1",
+       "restrictionsfield-label": "Rangos de IP permitidos:",
+       "restrictionsfield-help": "Un único enderezo IP ou rango CIDR por liña. Para habilitalos todos, utilice<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index 75ce391..0f3e624 100644 (file)
        "views": "𐍃𐌹𐌿𐌽𐌴𐌹𐍃",
        "toolbox": "𐍃𐌰𐍂𐍅𐌰𐌽𐍃",
        "projectpage": "𐌰𐌽𐌳𐌷𐌿𐌻𐌴𐌹 𐍆𐌰𐌿𐍂𐌰𐍅𐌰𐌿𐍂𐍀𐌰𐌻𐌰𐌿𐍆",
+       "mediawikipage": "𐌰𐌽𐌳𐌷𐌿𐌻𐌴𐌹 𐍅𐌰𐌿𐍂𐌳𐌰𐌻𐌰𐌿𐍆",
        "viewhelppage": "𐌰𐌽𐌳𐌷𐌿𐌻𐌴𐌹 𐌷𐌹𐌻𐍀𐌰𐌻𐌰𐌿𐍆",
        "otherlanguages": "𐌰𐌽𐌸𐌰𐍂𐌰𐌹𐌼 𐍂𐌰𐌶𐌳𐍉𐌼",
        "redirectedfrom": "(𐌹𐍃 {{GENDER:𐍄𐌹𐌿𐌷𐌰𐌽𐍃|𐍄𐌹𐌿𐌷𐌰𐌽𐌰}} 𐌷𐌹𐌳𐍂𐌴 𐍆𐍂𐌰𐌼 $1)",
        "privacypage": "Project:𐌲𐌰𐍂𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃 𐍃𐌿𐌽𐌳𐍂𐍉𐍅𐌹𐍃𐌰𐌽𐌰",
        "retrievedfrom": "𐌲𐌰𐌽𐌿𐌼𐌰𐌽 𐍆𐍂𐌰𐌼 \"$1\"",
        "youhavenewmessages": "{{PLURAL:$3|𐌷𐌰𐌱𐌰𐌹𐍃}} $1 ($2).",
+       "newmessageslinkplural": "{{PLURAL:$1|𐌽𐌹𐍅𐌹 𐍅𐌰𐌿𐍂𐌳|999=𐌽𐌹𐌿𐌾𐌰 𐍅𐌰𐌿𐍂𐌳𐌰}}",
+       "youhavenewmessagesmulti": "𐌷𐌰𐌱𐌰𐌹𐍃 𐌽𐌹𐌿𐌾𐌰 𐍅𐌰𐌿𐍂𐌳𐌰 𐌰𐌽𐌰 $1",
        "editsection": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹",
        "editold": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹",
        "editlink": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹",
        "nstab-special": "𐌿𐍃𐍃𐌹𐌽𐌳𐍃 𐌻𐌰𐌿𐍆𐍃",
        "nstab-project": "𐍆𐌰𐌿𐍂𐌰𐍅𐌰𐌿𐍂𐍀𐌰𐌻𐌰𐌿𐍆𐍃",
        "nstab-image": "𐍆𐌰𐌴𐌹𐌻",
+       "nstab-mediawiki": "𐍅𐌰𐌿𐍂𐌳",
        "nstab-template": "𐍃𐌺𐌴𐌹𐍂𐌴𐌹𐌽𐌹𐍆𐍂𐌹𐍃𐌰𐌷𐍄𐍃",
        "nstab-help": "𐌷𐌹𐌻𐍀𐌰𐌻𐌰𐌿𐍆𐍃",
        "nstab-category": "𐌺𐌿𐌽𐌹",
        "randompage": "𐌸𐌿𐍃 𐌿𐌽𐌺𐌿𐌽𐌸𐍃 𐌻𐌰𐌿𐍆𐍃",
        "statistics": "𐍂𐌰𐌸𐌾𐍉𐌽𐍃",
        "brokenredirects-edit": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹",
-       "brokenredirects-delete": "(𐍄𐌰𐌹𐍂𐌰𐌽)",
+       "brokenredirects-delete": "𐍆𐍂𐌰𐌵𐌹𐍃𐍄𐌴𐌹",
        "nbytes": "$1 {{PLURAL:$1|𐌱𐌹𐍄|𐌱𐌰𐍄𐌰}}",
        "ncategories": "$1 {{PLURAL:$1|𐌺𐌿𐌽𐌾𐌰|𐌺𐌿𐌽𐌾𐍉𐍃}}",
        "nlinks": "$1 {{PLURAL:$1|𐌲𐌰𐍅𐌹𐍃𐍃|𐌲𐌰𐍅𐌹𐍃𐍃𐌴𐌹𐍃}}",
        "allarticles": "𐌰𐌻𐌻𐌰𐌹 𐌻𐌰𐌿𐌱𐍉𐍃",
        "allpagessubmit": "𐌲𐌰𐌲𐌲",
        "categories": "𐌺𐌿𐌽𐌾𐌰",
-       "linksearch-ns": "ð\90\8d\83ð\90\8c´ð\90\8c¹ð\90\8c³ð\90\8d\89ð\90\8d\86ð\90\8c´ð\90\8d\82ð\90\8c°:",
+       "linksearch-ns": "ð\90\8c½ð\90\8c°ð\90\8c¼ð\90\8c°ð\90\8d\82ð\90\8c¿ð\90\8c¼:",
        "emailuser": "{{GENDER: 𐍃𐌰𐌽𐌳𐌴𐌹 𐌴-𐌱𐍉𐌺𐍉𐍃 𐌳𐌿 𐌸𐌰𐌼𐌼𐌰 𐌱𐍂𐌿𐌺𐌾𐌰𐌽𐌳|𐍃𐌰𐌽𐌳𐌴𐌹 𐌴-𐌱𐍉𐌺𐍉𐍃 𐌳𐌿 𐌸𐌹𐌶𐌰𐌹 𐌱𐍂𐌿𐌺𐌾𐌰𐌽𐌳𐌾𐌰𐌹}}",
        "watchlist": "𐍅𐌹𐍄𐌰𐍅𐌹𐌺𐍉",
        "mywatchlist": "𐌻𐌰𐌹𐍃𐍄𐌰𐌻𐌴𐌹𐍃𐍄𐌰",
        "unwatching": "Niwita...",
        "created": "𐌲𐌰𐍃𐌺𐌰𐍀𐌾𐌰𐌽",
        "deletepage": "𐍆𐍂𐌰𐌵𐌹𐍃𐍄𐌴𐌹 𐌻𐌰𐌿𐌱𐌰",
-       "delete-legend": "ð\90\8d\84ð\90\8c°ð\90\8c¹ð\90\8d\82ð\90\8c°ð\90\8c½",
+       "delete-legend": "ð\90\8d\86ð\90\8d\82ð\90\8c°ð\90\8cµð\90\8c¹ð\90\8d\83ð\90\8d\84ð\90\8c´ð\90\8c¹",
        "actioncomplete": "𐍅𐌰𐍃𐌿𐌷 𐌹𐍄𐌰 𐌲𐌰𐌿𐍃𐍄𐌹𐌿𐌷𐌰𐌽",
        "dellogpage": "𐍄𐌰𐌹𐍂𐌰 𐌰𐌹𐍂𐍅𐌱𐍉𐌺𐌰",
        "deleteotherreason": "𐌰𐌽𐌸𐌰𐍂/𐌼𐌰𐌹𐍃 𐌼𐌹𐍄𐍉𐌽𐍃:",
index 0eff530..bdfcfb5 100644 (file)
        "title-invalid-characters": "כותרת הדף המבוקש מכילה תווים בלתי תקינים: \"$1\".",
        "title-invalid-relative": "בכותרת יש נתיב יחסי. כותרת דפים יחסיות (./, ../) אינן תקינות, משום שלעתים קרובות הן יהיו בלתי־ניתנות להשגה כשתטופלנה על־ידי הדפדפן של המשתמש.",
        "title-invalid-magic-tilde": "כותרת הדף המבוקש מכילה רצף טילדות מיוחד שאינו תקין (<nowiki>~~~</nowiki>).",
-       "title-invalid-too-long": "כותרת הדף המבוקש ארוכה מדי. היא צריכה להיות לכל היותר באורך {{PLURAL:$1|בייט אחד|$1 בייטים}} בקידוד UTF-8.",
+       "title-invalid-too-long": "כותרת הדף המבוקש ארוכה מדי. היא צריכה להיות לכל היותר באורך של {{PLURAL:$1|בייט אחד|$1 בייטים}} בקידוד UTF-8.",
        "title-invalid-leading-colon": "כותרת הדף המבוקש מכילה תו נקודתיים בלתי תקין בתחילתה.",
-       "perfcached": "המידע הבא הוא עותק שמור בזיכרון המטמון של המידע, ועשוי שלא להיות מעודכן. לכל היותר {{PLURAL:$1|תוצאה אחת נשמרת|$1 תוצאות נשמרות}} בזיכרון המטמון.",
-       "perfcachedts": "המידע הבא הוא עותק שמור בזיכרון המטמון של המידע, שעודכן לאחרונה ב־$1. לכל היותר {{PLURAL:$4|תוצאה אחת נשמרת|$4 תוצאות נשמרות}} בזיכרון המטמון.",
+       "perfcached": "המידע הבא הוא עותק שמור בזיכרון המטמון, ועשוי שלא להיות מעודכן. לכל היותר {{PLURAL:$1|תוצאה אחת נשמרת|$1 תוצאות נשמרות}} בזיכרון המטמון.",
+       "perfcachedts": "המידע הבא הוא עותק שמור בזיכרון המטמון, שעודכן לאחרונה ב־$1. לכל היותר {{PLURAL:$4|תוצאה אחת נשמרת|$4 תוצאות נשמרות}} בזיכרון המטמון.",
        "querypage-no-updates": "העדכונים לדף זה כרגע מופסקים, והמידע לא יעודכן באופן שוטף.",
        "viewsource": "הצגת מקור",
        "viewsource-title": "הצגת המקור של הדף \"$1\"",
        "translateinterface": "כדי להוסיף או לשנות תרגומים של הודעות מערכת עבור כל אתרי הוויקי, יש להשתמש ב־[https://translatewiki.net/ translatewiki.net], פרויקט התרגום של מדיה־ויקי.",
        "cascadeprotected": "דף זה מוגן מעריכה כי הוא מוכלל {{PLURAL:$1|בדף הבא, שמופעלת עליו|בדפים הבאים, שמופעלת עליהם}} הגנה מדורגת:\n$2",
        "namespaceprotected": "אין {{GENDER:|לך|לך|לכם}} הרשאה לערוך דפים במרחב השם <strong>$1</strong>.",
-       "customcssprotected": "×\90×\99× ×\9a ×\9e×\95רש×\94 ×\9cער×\95×\9a ×\93×£ CSS ×\96×\94 ×\9b×\99×\95×\95×\9f ×©×\94×\95×\90 ×\9b×\95×\9cל הגדרות אישיות של משתמש אחר.",
-       "customjsprotected": "×\90×\99× ×\9a ×\9e×\95רש×\94 ×\9cער×\95×\9a ×\93×£ JavaScript ×\96×\94 ×\9b×\99×\95×\95×\9f ×©×\94×\95×\90 ×\9b×\95×\9cל הגדרות אישיות של משתמש אחר.",
-       "mycustomcssprotected": "אין לך הרשאה לערוך דף CSS זה.",
-       "mycustomjsprotected": "אין לך הרשאה לערוך דף JavaScript זה.",
-       "myprivateinfoprotected": "אין לך הרשאה לערוך את המידע הפרטי שלך.",
-       "mypreferencesprotected": "אין לך הרשאה לערוך את ההעדפות שלך.",
+       "customcssprotected": "×\90×\99×\9f {{GENDER:|×\9c×\9a\9c×\9a\9c×\9b×\9d}} ×\94רש×\90×\94 ×\9cער×\95×\9a ×\90ת ×\93×£ ×\94Ö¾CSS ×\94×\96×\94, ×\9eש×\95×\9d ×©×\94×\95×\90 ×\9e×\9b×\99ל הגדרות אישיות של משתמש אחר.",
+       "customjsprotected": "×\90×\99×\9f {{GENDER:|×\9c×\9a\9c×\9a\9c×\9b×\9d}} ×\94רש×\90×\94 ×\9cער×\95×\9a ×\90ת ×\93×£ ×\94Ö¾JavaScript ×\94×\96×\94, ×\9eש×\95×\9d ×©×\94×\95×\90 ×\9e×\9b×\99ל הגדרות אישיות של משתמש אחר.",
+       "mycustomcssprotected": "אין {{GENDER:|לך|לך|לכם}} הרשאה לערוך את דף ה־CSS הזה.",
+       "mycustomjsprotected": "אין {{GENDER:|לך|לך|לכם}} הרשאה לערוך את דף ה־JavaScript הזה.",
+       "myprivateinfoprotected": "אין {{GENDER:|לך|לך|לכם}} הרשאה לערוך את המידע הפרטי {{GENDER:|שלך|שלך|שלכם}}.",
+       "mypreferencesprotected": "אין {{GENDER:|לך|לך|לכם}} הרשאה לערוך את ההעדפות {{GENDER:|שלך|שלך|שלכם}}.",
        "ns-specialprotected": "לא ניתן לערוך דפים מיוחדים.",
-       "titleprotected": "[[User:$1|$1]] {{GENDER:$1|×\94פע×\99×\9c\94פע×\99×\9c×\94}} ×\94×\92× ×\94 ×¢×\9c ×\94×\93×£ ×\94×\96×\94 ×\9eפנ×\99 ×\99צ×\99ר×\94.\n×\94ס×\99×\91×\94 ×©× ×\99תנ×\94 ×\9c×\9b×\9a ×\94×\99×\90 <em>$2</em>.",
+       "titleprotected": "[[User:$1|$1]] {{GENDER:$1|×\94פע×\99×\9c\94פע×\99×\9c×\94}} ×¢×\9c ×\94×\93×£ ×\94×\96×\94 ×\94×\92× ×\94 ×\9eפנ×\99 ×\99צ×\99ר×\94.\n×\94ס×\99×\91×\94 ×©× ×\99תנ×\94 ×\9c×\94×\92× ×\94 ×\94×\99×\90: <em>$2</em>.",
        "filereadonlyerror": "לא ניתן לשנות את הקובץ \"$1\" כיוון שמאגר הקבצים \"$2\" במצב קריאה בלבד.\n\nמנהל המערכת שנעל את המאגר סיפק את ההסבר הבא: \"'''$3'''\".",
        "invalidtitle-knownnamespace": "כותרת בלתי־תקינה עם מרחב השם \"$2\" ושם דף \"$3\"",
        "invalidtitle-unknownnamespace": "כותרת בלתי־תקינה עם מרחב שם בלתי־ידוע מספר $1 ושם דף \"$2\"",
        "eauthentsent": "דוא\"ל אימות נשלח לכתובת הדוא\"ל שצוינה.\nלפני שדברי דוא\"ל אחרים יישלחו לחשבון הזה, יהיה עליכם לפעול לפי ההוראות בדוא\"ל, כדי לאשר שהחשבון אכן שייך לכם.",
        "throttled-mailpassword": "כבר נשלח דוא\"ל לאיפוס הסיסמה ב{{PLURAL:$1|שעה האחרונה|שעתיים האחרונות|־$1 השעות האחרונות}}.\nכדי למנוע ניצול לרעה, יכול להישלח רק דוא\"ל אחד כזה בכל {{PLURAL:$1|שעה|שעתיים|$1 שעות}}.",
        "mailerror": "שגיאה בשליחת דואר: $1",
-       "acct_creation_throttle_hit": "×\9e×\91קר×\99×\9d ×\91×\90תר ×\96×\94 ×\93ר×\9a ×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9b×\9d ×\9b×\91ר ×\99צר×\95 {{PLURAL:$1|×\97ש×\91×\95×\9f ×\90×\97×\93|$1 ×\97ש×\91×\95× ×\95ת}} ×\91×\99×\95×\9d ×\94×\90×\97ר×\95×\9f. ×\96×\94×\95 ×\94×\9eקס×\99×\9e×\95×\9d ×\94×\9e×\95תר ×\91תק×\95פ×\94 ×\96×\95.\n×\9cפ×\99×\9b×\9a, ×\9e×\91קר×\99×\9d ×\93ר×\9a ×\9bת×\95×\91ת ×\94Ö¾IP ×\94×\96×\90ת ×\9c×\90 ×\99×\9b×\95×\9c×\99×\9d ×\9c×\99צ×\95ר ×\97ש×\91×\95× ×\95ת × ×\95ספ×\99×\9d ×\91ר×\92×¢ ×\96×\94.",
+       "acct_creation_throttle_hit": "×\9e×\91קר×\99×\9d ×\91×\90תר ×\96×\94 ×\93ר×\9a ×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a ×\9b×\91ר ×\99צר×\95 {{PLURAL:$1|×\97ש×\91×\95×\9f ×\90×\97×\93|$1 ×\97ש×\91×\95× ×\95ת}} ×\91Ö¾$2. ×\96×\94×\95 ×\94×\9eקס×\99×\9e×\95×\9d ×\94×\9e×\95תר ×\91תק×\95פ×\94 ×\96×\95.\n×\9cפ×\99×\9b×\9a, ×\9bר×\92×¢ ×\9c×\90 × ×\99ת×\9f ×\9c×\99צ×\95ר ×\97ש×\91×\95× ×\95ת × ×\95ספ×\99×\9d ×\9e×\9bת×\95×\91ת ×\94Ö¾IP ×\94×\96×\95.",
        "emailauthenticated": "כתובת הדוא\"ל שלך אומתה ב־$2 בשעה $3.",
        "emailnotauthenticated": "כתובת הדוא\"ל שלכם עדיין לא אומתה.\nלא יישלח אליכם דוא\"ל עבור אף אחת מהתכונות הבאות.",
        "noemailprefs": "יש לציין כתובת דוא\"ל בהעדפות שלך כדי שתכונות אלה יעבדו.",
        "botpasswords-label-resetpassword": "איפוס ססמה",
        "botpasswords-label-grants": "זיכיונות מתאימים",
        "botpasswords-help-grants": "כל זיכיון נותן גישה להרשאות משתמש רשומות שיש לחשבון המשתמש. עיינו ב[[Special:ListGrants|טבלת הזיכיונות]] למידע נוסף.",
-       "botpasswords-label-restrictions": "הגבלות שימוש:",
        "botpasswords-label-grants-column": "ניתן זיכיון",
        "botpasswords-bad-appid": "שם הבוט \"$1\" אינו תקין.",
        "botpasswords-insert-failed": "הוספת שם הבוט \"$1\" נכשלה. האם הוא כבר נוסף?",
        "unlinkaccounts-success": "קישור החשבון בוטל.",
        "authenticationdatachange-ignored": "השינוי בנתוני האימות לא הצליח. ייתכן שלא הוגדר ספק.",
        "userjsispublic": "שימו לב: משתמשים אחרים יכולים לצפות בדפי ה־JavaScript שלכם, ולכן אין לכלול בהם מידע סודי.",
-       "usercssispublic": "שימו לב: משתמשים אחרים יכולים לצפות בדפי ה־CSS שלכם, ולכן אין לכלול בהם מידע סודי."
+       "usercssispublic": "שימו לב: משתמשים אחרים יכולים לצפות בדפי ה־CSS שלכם, ולכן אין לכלול בהם מידע סודי.",
+       "restrictionsfield-badip": "כתובת או טווח כתובות IP בלתי תקין: $1",
+       "restrictionsfield-label": "טווחי כתובות IP מותרים:",
+       "restrictionsfield-help": "כתובת IP אחת או טווח CIDR אחד בשורה. כדי לאפשר את הכול, ניתן להשתמש ב:<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index 850fdba..34e2f51 100644 (file)
@@ -62,7 +62,7 @@
        "tog-enotifminoredits": "Pošalji mi e-mail i kod manjih izmjena stranice",
        "tog-enotifrevealaddr": "Prikaži moju e-mail adresu u obavijestima o izmjeni",
        "tog-shownumberswatching": "Prikaži broj suradnika koji prate stranicu (u nedavnim izmjenama, popisu praćenja i samim člancima)",
-       "tog-oldsig": "Pregled postojećeg potpisa:",
+       "tog-oldsig": "Vaš postojeći potpis:",
        "tog-fancysig": "Običan potpis kao wikitekst (bez automatske poveznice)",
        "tog-uselivepreview": "Uključi trenutačni pretpregled",
        "tog-forceeditsummary": "Podsjeti me ako sažetak uređivanja ostavljam praznim",
        "august": "kolovoza",
        "september": "rujna",
        "october": "listopada",
-       "november": "studenoga",
+       "november": "studenog",
        "december": "prosinca",
        "january-gen": "siječnja",
        "february-gen": "veljače",
        "newwindow": "(otvara se u novom prozoru)",
        "cancel": "Odustani",
        "moredotdotdot": "Više...",
-       "morenotlisted": "Ovaj popis nije potpun.",
+       "morenotlisted": "Ovaj popis možda nije potpun.",
        "mypage": "Stranica",
        "mytalk": "Razgovor",
        "anontalk": "Razgovor",
        "print": "Ispiši",
        "view": "Vidi",
        "view-foreign": "vidi na projektu $1",
-       "edit": "uredi",
+       "edit": "Uredi",
        "edit-local": "Uredi lokalni opis",
        "create": "Započni",
        "create-local": "dodaj lokalni opis",
        "talk": "Razgovor",
        "views": "Pogledi",
        "toolbox": "Pomagala",
+       "tool-link-userrights": "Promijeni {{GENDER:$1|suradnikove|suradničine}} grupe",
+       "tool-link-emailuser": "Pošalji e-poštu {{GENDER:$1|suradniku|suradnici}}",
        "userpage": "Vidi suradnikovu stranicu",
        "projectpage": "Vidi stranicu o projektu",
        "imagepage": "Vidi stranicu datoteke",
        "redirectedfrom": "(Preusmjereno s $1)",
        "redirectpagesub": "Preusmjeravanje",
        "redirectto": "Preusmjerava na:",
-       "lastmodifiedat": "Datum i vrijeme posljednje promjene na ovoj stranici: $1 u $2",
+       "lastmodifiedat": "Ova stranica posljednji put je izmjenjena $1 u $2.",
        "viewcount": "Ova stranica je pogledana {{PLURAL:$1|$1 put|$1 puta}}.",
        "protectedpage": "Zaštićena stranica",
        "jumpto": "Skoči na:",
        "aboutpage": "Project:O_projektu_{{SITENAME}}",
        "copyright": "Sadržaji se koriste u skladu s $1.",
        "copyrightpage": "{{ns:project}}:Autorska prava",
-       "currentevents": "Aktualno",
+       "currentevents": "Novosti",
        "currentevents-url": "Project:Novosti",
        "disclaimers": "Odricanje od odgovornosti",
        "disclaimerpage": "Project:General_disclaimer",
        "viewsourceold": "vidi izvor",
        "editlink": "uredi",
        "viewsourcelink": "vidi izvornik",
-       "editsectionhint": "Uredi odlomak $1",
+       "editsectionhint": "Uredi odlomak: $1",
        "toc": "Sadržaj",
        "showtoc": "prikaži",
        "hidetoc": "sakrij",
        "red-link-title": "$1 (stranica ne postoji)",
        "sort-descending": "Sortiraj silazno",
        "sort-ascending": "Sortiraj uzlazno",
-       "nstab-main": "Članak",
+       "nstab-main": "Stranica",
        "nstab-user": "{{GENDER:{{BASEPAGENAME}}|Stranica suradnika|Stranica suradnice}}",
        "nstab-media": "Mediji",
        "nstab-special": "Posebna stranica",
        "virus-unknownscanner": "nepoznati antivirus:",
        "logouttext": "'''Odjavili ste se.'''\n\nNeke se stranice mogu prikazivati kao da ste još uvijek prijavljeni, sve dok ne očistite međuspremnik svog preglednika.",
        "cannotlogoutnow-title": "Odjava trenutno nije moguća.",
+       "cannotlogoutnow-text": "Odjava nije moguća tijekom uporabe $1.",
        "welcomeuser": "Dobrodošli, $1!",
        "welcomecreation-msg": "Vaš je suradnički račun otvoren.\nNe zaboravite prilagoditi Vaše [[Special:Preferences|{{SITENAME}} postavke]].",
        "yourname": "Suradničko ime",
        "createacct-yourpasswordagain-ph": "Unesite zaporku ponovno",
        "userlogin-remembermypassword": "Zapamti me",
        "userlogin-signwithsecure": "Rabi sigurnu vezu",
+       "cannotlogin-title": "Prijava nije moguća",
+       "cannotlogin-text": "Prijava nija moguća.",
        "cannotloginnow-title": "Prijava trenutno nije moguća.",
+       "cannotloginnow-text": "Prijava nije moguća tijekom uporabe $1.",
+       "cannotcreateaccount-title": "Nije moguće stvoriti račune",
        "yourdomainname": "Vaša domena",
        "password-change-forbidden": "Ne možete promjeniti zaporku na ovom projektu.",
        "externaldberror": "Došlo je do pogreške s vanjskom autorizacijom ili Vam nije dopušteno osvježavanje vanjskog suradničkog računa.",
        "resetpass_submit": "Postavite lozinku i prijavite se",
        "changepassword-success": "Zaporka je uspješno postavljena!",
        "changepassword-throttled": "Nedavno ste se previše puta pokušali prijaviti.\nMolimo Vas pričekajte $1 prije nego što pokušate ponovno.",
+       "botpasswords": "Lozinke botova",
+       "botpasswords-disabled": "Lozinke botova su onemogućene.",
+       "botpasswords-no-central-id": "Za uporabu lozinki botova, morate biti prijavljeni na središnji račun.",
+       "botpasswords-existing": "Postojeće lozinke botova",
+       "botpasswords-createnew": "Stvorite novu lozinku bota",
+       "botpasswords-editexisting": "Uredite postojeću lozinku bota",
+       "botpasswords-label-appid": "Ime bota:",
        "botpasswords-label-create": "Stvori",
        "botpasswords-label-update": "Ažuriraj",
        "botpasswords-label-cancel": "Odustani",
+       "botpasswords-label-delete": "Izbriši",
        "botpasswords-label-resetpassword": "Ponovno postavljanje lozinke",
+       "botpasswords-label-grants": "Primjenjive dozvole:",
+       "botpasswords-help-grants": "Svaka dozvola daje pristup navedenim suradničkim pravima koja su već dodjeljena suradničkom računu. Vidjeti [[Special:ListGrants|tablicu dozvola]] za više informacija.",
+       "botpasswords-label-grants-column": "Odobreno",
+       "botpasswords-bad-appid": "Ime bota \"$1\" nije valjano.",
        "botpasswords-insert-failed": "Nije moguće dodavanje imena bota \"$1\". Možda je već dodano?",
+       "botpasswords-update-failed": "Nije moguće ažurirati bot s imenom \"$1\". Možda je izbrisan?",
        "resetpass_forbidden": "Lozinka ne može biti promijenjena",
        "resetpass-no-info": "Morate biti prijavljeni da biste izravno pristupili ovoj stranici.",
        "resetpass-submit-loggedin": "Promijeni lozinku",
        "minoredit": "Ovo je manja promjena",
        "watchthis": "Prati ovu stranicu",
        "savearticle": "Sačuvaj stranicu",
+       "savechanges": "Spremi promjene",
        "publishpage": "Objavi stranicu",
        "publishchanges": "Objavi izmjene",
        "preview": "Pregled kako će stranica izgledati",
        "diff-multi-manyusers": "({{PLURAL:$1|Nije prikazana jedna međuinačica|Nisu prikazane $1 međuinačice|Nije prikazano $1 međuinačica}} više od {{PLURAL:$2|jednog|$2|$2}} suradnika)",
        "difference-missing-revision": "{{PLURAL:$2|Uređivanje|$2 uređivanja}} sljedeće šifre ($1) ne {{PLURAL:$2|postoji|postoje}}.\n\nOvo je obično uzrokovano kada kliknete na zastarjelu poveznicu na stranice koja je obrisana.\nViše informacija možete pronaći u [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} evidenciji brisanja].",
        "searchresults": "Rezultati pretrage",
-       "searchresults-title": "Rezultati traženja za \"$1\"",
+       "searchresults-title": "Rezultati pretrage za \"$1\"",
        "titlematches": "Pronađene stranice prema naslovu",
        "textmatches": "Pronađene stranice prema tekstu članka",
        "notextmatches": "Nema pronađenih stranica prema tekstu članka",
        "grant-blockusers": "Blokiraj i odblokiraj korisnike",
        "grant-createaccount": "Otvori račune",
        "grant-createeditmovepage": "Stvori, uredi i premjesti stranice",
-       "grant-editmyoptions": "Uredi korisničke postavke",
+       "grant-editmyoptions": "Uređivanje vlastitih suradničkih postavki",
+       "grant-editpage": "Uređivanje postojećih stranica",
+       "grant-editprotected": "Uređivanje zaštićenih stranica",
        "grant-highvolume": "Uređivanja velikog opsega",
        "grant-basic": "Osnovna prava",
        "grant-viewdeleted": "Prikaz izbrisanih datoteka i stranica",
        "rc-old-title": "izvorno ime bilo je \"$1\"",
        "recentchangeslinked": "Povezane stranice",
        "recentchangeslinked-feed": "Povezane stranice",
-       "recentchangeslinked-toolbox": "Povezane stranice",
+       "recentchangeslinked-toolbox": "Povezane promjene",
        "recentchangeslinked-title": "Povezane promjene sa stranicom \"$1\"",
        "recentchangeslinked-summary": "Ova posebna stranica pokazuje nedavne promjene na povezanim stranicama (ili stranicama određene kategorije). Stranice koje su na [[Special:Watchlist|Vašem popisu praćenja]] su '''podebljane'''.",
        "recentchangeslinked-page": "Naslov stranice:",
        "checkbox-select": "Odaberite: $1",
        "checkbox-all": "Sve",
        "checkbox-none": "Nijedan",
+       "checkbox-invert": "Obrnuto",
        "allpages": "Sve stranice",
        "nextpage": "Sljedeća stranica ($1)",
        "prevpage": "Prethodna stranica ($1)",
        "listgrouprights-removegroup-self-all": "Uklonite sve skupine iz vlastitog računa",
        "listgrouprights-namespaceprotection-header": "Ograničenja prostora imena",
        "listgrouprights-namespaceprotection-namespace": "Imenski prostor",
+       "listgrants": "Dozvole",
+       "listgrants-summary": "Slijedi popis dozvola s pridruženim pristupom suradničkim pravima. Suradnici mogu omogućiti aplikacijama uporabu svojih računa, ali s ograničenim ovlastima na temelju dozvola koje je suradnik dodijelio aplikaciji. Aplikacija koja djeluje u ime suradnika međutim ne može rabiti prava koje suradnik nema.\nMoguće su [[{{MediaWiki:Listgrouprights-helppage}}|dodatne informacije]] o pojedinim pravima.",
+       "listgrants-grant": "Dozvola",
+       "listgrants-rights": "Prava",
        "trackingcategories-nodesc": "Opis nije dostupan.",
        "mailnologin": "Nema adrese pošiljatelja",
        "mailnologintext": "Morate biti [[Special:UserLogin|prijavljeni]]\ni imati valjanu adresu e-pošte u svojim [[Special:Preferences|postavkama]]\nda bi mogli slati poštu drugim suradnicima.",
        "emailccsubject": "Kopija Vaše poruke suradniku $1: $2",
        "emailsent": "E-mail poslan",
        "emailsenttext": "Vaša poruka je poslana.",
-       "emailuserfooter": "Ova je poruka poslana od $1 za $2 uporabom \"elektroničke pošte\" s projekta {{SITENAME}}.",
+       "emailuserfooter": "Ovu je e-poruku {{GENDER:$1|poslao suradnik|poslala suradnica}} $1 {{GENDER:$2|suradniku $2|suradnici $2}} uporabom mogućnosti \"{{int:emailuser}}\" s projekta {{SITENAME}}.",
        "usermessage-summary": "Ostavljanje poruke sustava.",
        "usermessage-editor": "Uređivač sistemskih poruka",
        "watchlist": "Moj popis praćenja",
        "rollback-success": "uklonjeno uređivanje {{GENDER:$1|suradnika|suradnice}} $1\nvraćeno na posljednju inačicu {{GENDER:$2|suradnika|suradnice}} $2.",
        "sessionfailure-title": "Prekid sesije",
        "sessionfailure": "Uočili smo problem s Vašom prijavom. Zadnja naredba nije izvršena kako bi se izbjegla zloupotreba. Molimo Vas da se u pregledniku vratite natrag na prethodnu stranicu, ponovno je učitate i zatim pokušate opet.",
+       "changecontentmodel": "Promijeni model sadržaja stranice",
        "changecontentmodel-legend": "Promijeni model sadržaja",
        "changecontentmodel-title-label": "Naziv stranice",
        "changecontentmodel-model-label": "Novi model sadržaja",
        "tooltip-pt-preferences": "Vaše postavke",
        "tooltip-pt-watchlist": "Popis stranica koje pratite.",
        "tooltip-pt-mycontris": "Popis Vaših doprinosa",
-       "tooltip-pt-login": "Predlažemo Vam da se prijavite, ali nije obvezno.",
+       "tooltip-pt-login": "Predlažemo Vam da se prijavite, međutim nije obvezno.",
        "tooltip-pt-logout": "Odjavi se",
-       "tooltip-pt-createaccount": "Nudimo vam mogućnost da napravite račun i prijavite se, iako to nije nužno.",
+       "tooltip-pt-createaccount": "Predlažemo Vam mogućnost stvaranja računa i prijave, iako to nije nužno.",
        "tooltip-ca-talk": "Razgovor o stranici",
        "tooltip-ca-edit": "Uredi ovu stranicu",
        "tooltip-ca-addsection": "Dodaj novi odlomak",
        "tooltip-ca-viewsource": "Ova stranica je zaštićena. Možete pogledati izvorni kod.",
-       "tooltip-ca-history": "Ranije izmjene na ovoj stranici.",
+       "tooltip-ca-history": "Ranije izmjene na ovoj stranici",
        "tooltip-ca-protect": "Zaštiti ovu stranicu",
        "tooltip-ca-unprotect": "Ukloni zaštitu s ove stranice",
        "tooltip-ca-delete": "Izbriši ovu stranicu",
        "tooltip-ca-move": "Premjesti ovu stranicu",
        "tooltip-ca-watch": "Dodaj ovu stranicu na svoj popis praćenja",
        "tooltip-ca-unwatch": "Ukloni ovu stranicu s popisa praćenja",
-       "tooltip-search": "Pretraži ovaj wiki",
+       "tooltip-search": "Pretraži {{SITENAME}}",
        "tooltip-search-go": "Idi na stranicu s ovim imenom ako ona postoji",
        "tooltip-search-fulltext": "Traži ovaj tekst na svim stranicama",
-       "tooltip-p-logo": "Glavna stranica",
+       "tooltip-p-logo": "Posjeti glavnu stranicu",
        "tooltip-n-mainpage": "Posjeti glavnu stranicu",
        "tooltip-n-mainpage-description": "Posjeti glavnu stranicu",
-       "tooltip-n-portal": "O projektu, što možete učiniti, gdje je što",
-       "tooltip-n-currentevents": "O trenutačnim događajima",
-       "tooltip-n-recentchanges": "Popis nedavnih promjena u wikiju.",
+       "tooltip-n-portal": "O projektu, što možete učiniti, gdje se što nalazi",
+       "tooltip-n-currentevents": "Saznajte više o trenutačnim događajima",
+       "tooltip-n-recentchanges": "Popis nedavnih promjena u wikiju",
        "tooltip-n-randompage": "Učitaj slučajnu stranicu",
-       "tooltip-n-help": "Mjesto za pomoć suradnicima.",
-       "tooltip-t-whatlinkshere": "Popis stranica koje sadrže poveznice na ovu stranicu",
+       "tooltip-n-help": "Mjesto gdje se može dobiti pomoć",
+       "tooltip-t-whatlinkshere": "Popis svih stranica koje sadrže poveznice na ovu stranicu",
        "tooltip-t-recentchangeslinked": "Nedavne promjene na stranicama na koje vode ovdašnje poveznice",
        "tooltip-feed-rss": "RSS feed za ovu stranicu",
        "tooltip-feed-atom": "Atom feed za ovu stranicu",
        "tooltip-t-contributions": "Pogledaj popis doprinosa {{GENDER:$1|ovog suradnika|ove suradnice}}",
        "tooltip-t-emailuser": "Pošalji suradniku e-mail",
        "tooltip-t-info": "Više informacija o ovoj stranici",
-       "tooltip-t-upload": "Postavi slike i druge medije",
-       "tooltip-t-specialpages": "Popis posebnih stranica",
-       "tooltip-t-print": "Verzija za ispis ove stranice",
+       "tooltip-t-upload": "Postavi datoteke",
+       "tooltip-t-specialpages": "Popis svih posebnih stranica",
+       "tooltip-t-print": "Inačica za ispis ove stranice",
        "tooltip-t-permalink": "Trajna poveznica na ovu verziju stranice",
        "tooltip-ca-nstab-main": "Pogledaj sadržaj",
        "tooltip-ca-nstab-user": "Pogledaj suradničku stranicu",
        "mw-widgets-titleinput-description-redirect": "preusmjeravanje na $1",
        "log-action-filter-block": "Vrsta blokiranja:",
        "log-action-filter-newusers": "Način stvaranja računa:",
+       "log-action-filter-patrol": "Vrsta pregledavanja:",
        "log-action-filter-upload": "Vrsta postavljanja:",
        "log-action-filter-all": "sve",
        "log-action-filter-block-block": "blokiranje",
        "log-action-filter-newusers-create2": "stvorio registrirani suradnik",
        "log-action-filter-newusers-autocreate": "automatski stvoren",
        "log-action-filter-newusers-byemail": "stvoren lozinkom poslanom na e-poštu",
+       "log-action-filter-patrol-patrol": "Ručno pregledavanje",
+       "log-action-filter-patrol-autopatrol": "Automatsko pregledavanje",
        "log-action-filter-upload-upload": "novo postavljanje",
-       "log-action-filter-upload-overwrite": "ponovno postavljanje"
+       "log-action-filter-upload-overwrite": "ponovno postavljanje",
+       "changecredentials": "Promjena vjerodajnica",
+       "removecredentials": "Uklanjanje vjerodajnica"
 }
index 55603f9..cdd7d39 100644 (file)
        "eauthentsent": "Egy ellenőrző e-mailt küldtünk a megadott címre. Mielőtt más leveleket kaphatnál, igazolnod kell az e-mailben írt utasításoknak megfelelően, hogy valóban a tiéd a megadott cím.",
        "throttled-mailpassword": "Már elküldtünk egy jelszóemlékeztetőt az utóbbi {{PLURAL:$1|egy|$1}} órában.\nA visszaélések elkerülése végett {{PLURAL:$1|egy|$1}} óránként csak egy jelszó-emlékeztetőt küldünk.",
        "mailerror": "Hiba történt az e-mail küldése közben: $1",
-       "acct_creation_throttle_hit": "A wiki látogatói ezt az IP-címet használva $1 fiókot hoztak létre az elmúlt egy nap alatt. Ez a megengedett maximum ezen időtartam alatt, így az erről a címről látogatók jelenleg nem hozhatnak létre újabb fiókokat.",
+       "acct_creation_throttle_hit": "A wiki látogatói ezt az IP-címet használva $1 fiókot hoztak létre az elmúlt $2 alatt. Ez a megengedett maximum ezen időtartam alatt, így az erről a címről látogatók jelenleg nem hozhatnak létre újabb fiókokat.",
        "emailauthenticated": "Az e-mail címedet $2, $3-kor erősítetted meg.",
        "emailnotauthenticated": "Az e-mail címed még <strong>nincs megerősítve</strong>. E-mailek küldése és fogadása nem engedélyezett.",
        "noemailprefs": "Az alábbi funkciók használatához meg kell adnod az e-mail címedet.",
        "botpasswords-label-delete": "Törlés",
        "botpasswords-label-resetpassword": "Új jelszó kérése",
        "botpasswords-label-grants": "Elérhető jogosultságok:",
-       "botpasswords-label-restrictions": "Használati korlátozások:",
        "botpasswords-label-grants-column": "Megadva",
        "botpasswords-bad-appid": "A(z) „$1” botnév érvénytelen.",
        "botpasswords-insert-failed": "A(z) „$1” botnév hozzáadása sikertelen. Nem lehet, hogy már hozzá lett adva?",
        "right-sendemail": "e-mail küldése más felhasználóknak",
        "right-passwordreset": "Jelszó visszaállítási emailek megtekintése",
        "right-managechangetags": "[[Special:Tags|címkék]] létrehozása és (de)aktiválása",
-       "right-applychangetags": "[[Special:Tags|címkék]] alkalmazása a változakra",
+       "right-applychangetags": "[[Special:Tags|címkék]] alkalmazása saját változatokra",
        "right-changetags": "egyedi lapváltozatokon és naplóbejegyzéseken tetszőleges [[Special:Tags|címkék]] hozzáadása és törlése",
-       "right-deletechangetags": "[[Special:Tags|Címkék]] törlése az adatbázisból",
+       "right-deletechangetags": "[[Special:Tags|címkék]] törlése az adatbázisból",
        "grant-generic": "„$1” jogosultságcsomag",
        "grant-group-page-interaction": "interakció lapokkal",
        "grant-group-file-interaction": "interakció médiával",
        "action-viewmyprivateinfo": "személyes adatok megtekintése",
        "action-editmyprivateinfo": "személyes adatok szerkesztése",
        "action-editcontentmodel": "a lap tartalom modelljének szerkesztése",
-       "action-managechangetags": "adatbáziscímkék létrehozása és (de)aktiválása",
+       "action-managechangetags": "címkék létrehozása és (de)aktiválása",
        "action-applychangetags": "változtatások címkézése",
        "action-changetags": "egyedi változtatások és napló bejegyzések tetszőleges címkével való ellátása és törlése",
        "action-deletechangetags": "címkék törlése az adatbáziból",
        "uploaded-href-unsafe-target-svg": "Nem biztonságos adatra mutató href-et találtunk a feltöltött SVG-fájlban: URI-cél <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-animate-svg": "A feltöltött SVG fájlban \"animate\" taget találtam, ami az alábbi \"from\" attribútumával megváltoztathat egy href-et: <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-setting-event-handler-svg": "Eseménykezelő attribútumok beállítása blokkolva van, <code>&lt;$1 $2=\"$3\"&gt;</code> található a feltöltendő SVG fájlban.",
+       "uploaded-setting-href-svg": "A „set” címke használata „href” attribútum szülőelemhez adására blokkolva van.",
        "uploaded-setting-handler-svg": "Az SVG kódok, amelyek a \"handler\" attribútumot távolra/adatra/szkriptre állítják, le vannak tiltva. A feltöltött SVG fájlban a következőt találtam: <code>$1=\"$2\"</code>.",
        "uploaded-remote-url-svg": "Az SVG kódok, amelyek bármely stílus-attribútumot távoli URL-ra állítják, le vannak tiltva. A feltöltött SVG fájlban a következőt találtam: <code>$1=\"$2\"</code>.",
        "uploaded-image-filter-svg": "A feltöltött SVG fájl URL-t tartalmazó képfiltert tartalmaz: <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "logempty": "Nincs illeszkedő naplóbejegyzés.",
        "log-title-wildcard": "Így kezdődő címek keresése",
        "showhideselectedlogentries": "Kijelölt napló bejegyzések megjelenítése/elrejtése",
-       "log-edit-tags": "Kiválasztott napló címkék szerkesztése",
+       "log-edit-tags": "Kiválasztott naplóbejegyzések címkéinek szerkesztése",
        "checkbox-select": "Kiválasztás: $1",
        "checkbox-all": "Mind",
        "checkbox-none": "Nincs",
        "unblock": "Felhasználó blokkolásának feloldása",
        "blockip": "{{GENDER:$1|Felhasználó}} blokkolása",
        "blockip-legend": "Felhasználó blokkolása",
-       "blockiptext": "Az alábbi űrlap segítségével megvonhatod egy szerkesztő vagy IP-cím szerkesztési jogait.\nÜgyelj rá, hogy az intézkedésed mindig legyen tekintettel a vonatkozó [[{{MediaWiki:Policy-url}}|irányelvekre]].\nAdd meg a blokkolás okát is (például idézd a blokkolandó személy által vandalizált lapokat).",
+       "blockiptext": "Az alábbi űrlap segítségével megvonhatod egy szerkesztő vagy IP-cím szerkesztési jogait.\nEzt az eszközt csak vandalizmus megelőzésére, a vonatkozó [[{{MediaWiki:Policy-url}}|irányelvvel]] összhangban használd.\nAdd meg a blokkolás okát is (például idézd a blokkolandó személy által vandalizált lapokat).\nIP-tartományokat is blokkolhatsz a [https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR] szintaxissal; a legnagyobb engedélyezett tartomány /$1 IPv4 és /$2 IPv6 esetén.",
        "ipaddressorusername": "IP-cím vagy felhasználói név",
        "ipbexpiry": "Lejárat:",
        "ipbreason": "Ok:",
        "lockedbyandtime": "($1 zárta le $2 $3-kor)",
        "move-page": "$1 átnevezése",
        "move-page-legend": "Lap átnevezése",
-       "movepagetext": "Az alábbi űrlap használatával nevezhetsz át egy lapot, és helyezheted át teljes laptörténetét az új nevére.\nA régi cím az új címre való átirányítás lesz.\nFrissítheted a régi címre mutató átirányításokat, hogy azok automatikusan a megfelelő címre mutassanak;\nha nem teszed, ellenőrizd a [[Special:DoubleRedirects|dupla]] vagy [[Special:BrokenRedirects|hibás átirányításokat]].\nNeked kell biztosítanod, hogy a linkek továbbra is oda mutassanak, ahová mutatniuk kell.\n\nA lap '''nem''' nevezhető át, ha már van egy ugyanilyen című lap, hacsak nem üres vagy átirányítás, és nincs laptörténete.\nEz azt jelenti, hogy vissza tudsz nevezni egy tévedésből átnevezett lapot, és nem tudsz létező lapot véletlenül felülírni.\n\n'''FIGYELEM!'''\nNépszerű oldalak esetén ez drasztikus és nem várt változtatás lehet;\ngyőződj meg a folytatás előtt arról, hogy tisztában vagy a következményekkel.",
-       "movepagetext-noredirectfixer": "Az alábbi űrlap használatával nevezhetsz át egy lapot, és helyezheted át teljes laptörténetét az új nevére.\nA régi cím az új címre való átirányítás lesz.\nEllenőrizd a [[Special:DoubleRedirects|dupla]] és a [[Special:BrokenRedirects|hibás átirányításoknál]], hogy a linkek továbbra is oda mutatnak, ahová mutatniuk kell.\n\nA lap '''nem''' nevezhető át, ha már van egy ugyanilyen című lap, hacsak nem üres, vagy átirányítás, aminek nincs laptörténete.\nEz azt jelenti, hogy vissza tudsz nevezni egy tévedésből átnevezett lapot, de nem tudsz egy már létező lapot véletlenül felülírni.\n\n'''Figyelem!'''\nNépszerű oldalak esetén ez drasztikus és nem várt változtatás lehet;\ngyőződj meg a folytatás előtt arról, hogy tisztában vagy-e a következményekkel.",
+       "movepagetext": "Az alábbi űrlap használatával nevezhetsz át egy lapot, és helyezheted át teljes laptörténetét az új nevére.\nA régi cím az új címre való átirányítás lesz.\nFrissítheted a régi címre mutató átirányításokat, hogy azok automatikusan a megfelelő címre mutassanak;\nha nem teszed, ellenőrizd a [[Special:DoubleRedirects|dupla]] vagy [[Special:BrokenRedirects|hibás átirányításokat]].\nNeked kell biztosítanod, hogy a linkek továbbra is oda mutassanak, ahová mutatniuk kell.\n\nA lap <strong>nem</strong> nevezhető át, ha már van egy ugyanilyen című lap, hacsak nem üres vagy átirányítás, és nincs laptörténete.\nEz azt jelenti, hogy vissza tudsz nevezni egy tévedésből átnevezett lapot, és nem tudsz létező lapot véletlenül felülírni.\n\n<strong>Megjegyzés:</strong>\nNépszerű oldalak esetén ez drasztikus és nem várt változtatás lehet;\ngyőződj meg a folytatás előtt arról, hogy tisztában vagy a következményekkel.",
+       "movepagetext-noredirectfixer": "Az alábbi űrlap használatával nevezhetsz át egy lapot, és helyezheted át teljes laptörténetét az új nevére.\nA régi cím az új címre való átirányítás lesz.\nEllenőrizd a [[Special:DoubleRedirects|dupla]] és a [[Special:BrokenRedirects|hibás átirányításoknál]], hogy a linkek továbbra is oda mutatnak, ahová mutatniuk kell.\n\nA lap <strong>nem</strong> nevezhető át, ha már van egy ugyanilyen című lap, hacsak nem üres, vagy átirányítás, aminek nincs laptörténete.\nEz azt jelenti, hogy vissza tudsz nevezni egy tévedésből átnevezett lapot, de nem tudsz egy már létező lapot véletlenül felülírni.\n\n<strong>Megjegyzés:</strong>\nNépszerű oldalak esetén ez drasztikus és nem várt változtatás lehet;\ngyőződj meg a folytatás előtt arról, hogy tisztában vagy-e a következményekkel.",
        "movepagetalktext": "Ha bejelölöd ezt a pipát, akkor a laphoz tartozó vitalap automatikusan átneveződik az új címre, kivéve ha már létezik egy nem üres vitalap az új helyen.\n\nEbben az esetben a vitalapot külön, kézzel kell átnevezned vagy egyesítened a kívánságaid szerint.",
        "moveuserpage-warning": "'''Figyelem:''' Egy felhasználólapot készülsz átmozgatni. Csak a lap lesz átmozgatva, a szerkesztő ''nem'' lesz átnevezve.",
        "movecategorypage-warning": "<strong>Figyelmeztetés:</strong> Éppen egy kategórialapot készülsz átnevezni. Figyelj arra, hogy csak a lap lesz átnevezve, az idekategorizált lapok <em>nem</em> lesznek átkategorizálva.",
        "import-nonewrevisions": "Nincs változat importálva (mindet korábban importálták vagy a hiba miatt program kihagyta).",
        "xml-error-string": "$1 a(z) $2. sorban, $3. oszlopban ($4. bájt): $5",
        "import-upload": "XML-adatok feltöltése",
-       "import-token-mismatch": "Elveszett a session adat, próbálkozz újra.",
+       "import-token-mismatch": "Elveszett a munkamenetadatok.\n\nLehet, hogy ki vagy jelentkezve. <strong>Kérjük, győződj meg róla, hogy még mindig be vagy jelentkezve, majd próbálkozz újra!</strong> Ha ez továbbra sem sikerül, próbálj meg [[Special:UserLogout|kijelentkezni]], majd ismét bejelentkezni, és ellenőrizd, hogy a böngésződ elfogad sütiket erről az oldalról.",
        "import-invalid-interwiki": "A kijelölt wikiből nem lehet importálni.",
        "import-error-edit": "„$1” lap nem került importálásra, mert nem szerkesztheted azt.",
        "import-error-create": "„$1” lap nem került importálásra, mert nem hozhatod létre azt.",
        "confirmemail_invalidated": "E-mail-cím megerősíthetősége visszavonva",
        "invalidateemail": "E-mail-cím megerősíthetőségének visszavonása",
        "notificationemail_subject_changed": "Megváltozott az e-mail-címed a(z) {{SITENAME}} wikin",
+       "notificationemail_subject_removed": "E-mail-cím eltávolítva a(z) {{SITENAME}} wikin",
+       "notificationemail_body_changed": "Valaki (vélhetően te, a(z) $1 IP-címről) megváltoztatta a(z) „$2” fiókhoz tartozó e-mail-címet a(z) {{SITENAME}} wikin a következőre: „$3”.\n\nHa ez nem te voltál, azonnal lépj kapcsolatba egy adminisztrátorral.",
+       "notificationemail_body_removed": "Valaki (vélhetően te, a(z) $1 IP-címről) eltávolította a(z) „$2” fiókhoz tartozó e-mail-címet a(z) {{SITENAME}} wikin.\n\nHa ez nem te voltál, azonnal lépj kapcsolatba egy adminisztrátorral.",
        "scarytranscludedisabled": "[Wikiközi beillesztés le van tiltva]",
        "scarytranscludefailed": "[$1 sablon letöltése sikertelen]",
        "scarytranscludefailed-httpstatus": " [Nem sikerült betölteni a(z) $1 sablont: HTTP $2]",
        "version-other": "Egyéb",
        "version-mediahandlers": "Médiafájl-kezelők",
        "version-hooks": "Hookok",
-       "version-parser-extensiontags": "Az értelmező kiterjesztéseinek tagjei",
+       "version-parser-extensiontags": "Az értelmező kiterjesztéseinek címkéi",
        "version-parser-function-hooks": "Az értelmező függvényeinek hookjai",
        "version-hook-name": "Hook neve",
        "version-hook-subscribedby": "Használja",
        "version-libraries-description": "Leírás",
        "version-libraries-authors": "Szerzők",
        "redirect": "Átirányítás fájl, szerkesztő, olda, oldalváltozat vagy naplóazonosító alapján",
-       "redirect-summary": "Ez a speciális lap átirányít egy fájlra (megadott fájlnévvel), lapra (megadott lapváltozat- vagy lapazonosító számmal) vagy felhasználóra (felhasználó azonosítószáma alapján). Használat: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]] vagy [[{{#Special:Redirect}}/user/101]].",
+       "redirect-summary": "Ez a speciális lap átirányít egy fájlra (megadott fájlnévvel), lapra (megadott lapváltozat- vagy lapazonosító számmal), felhasználóra (felhasználó azonosítószáma alapján) vagy naplóbejegyzésre (naplóazonosító alapján). Használat: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]] vagy [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Mehet",
        "redirect-lookup": "Keresés:",
        "redirect-value": "Érték:",
        "intentionallyblankpage": "Ez a lap szándékosan maradt üresen",
        "external_image_whitelist": " #Ezt a sort hagyd pontosan így, ahogy van<pre>\n#Ide reguláris kifejezéseket írhatsz (azon részüket, amik a // közé mennek)\n#Ezek egyeztetve lesznek a külső képek URL-jeivel\n#Egyezés esetén képként fognak megjelenni, egyébként csak link fog rájuk mutatni\n#A #-tel kezdődő sorok megjegyzésnek számítanak\n#A kis- és nagybetűk nincsenek megkülönböztetve\n\n#A reguláris kifejezéseket ezen sor alá írd. Ezt a sort hagyd így, ahogy van.</pre>",
        "tags": "Lapváltozat-címkék",
-       "tag-filter": "[[Special:Tags|Címke]]szűrő:",
+       "tag-filter": "[[Special:Tags|Címkeszűrő]]:",
        "tag-filter-submit": "Szűrő",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Címke|Címkék}}]]: $2)",
+       "tag-mw-contentmodelchange": "tartalommodell-változtatás",
+       "tag-mw-contentmodelchange-description": "Szerkesztések, amelyek [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel megváltoztatják egy lap tartalommodelljét].",
        "tags-title": "Címkék",
        "tags-intro": "Ez a lap azokat a címkéket és jelentéseiket tartalmazza, amikkel a szoftver megjelölhet egy szerkesztést.",
        "tags-tag": "Címke neve",
        "tags-actions-header": "Műveletek",
        "tags-active-yes": "Igen",
        "tags-active-no": "Nem",
-       "tags-source-extension": "Egy kiterjesztés határozza meg",
+       "tags-source-extension": "A szoftver határozza meg",
        "tags-source-manual": "Manuálisan adják meg felhasználók és botok",
        "tags-source-none": "Már nincs használatban",
        "tags-edit": "szerkesztés",
        "tags-delete-not-allowed": "A kiterjesztés által létrehozott címkék nem törölhetők, ha a kiterjesztés nem engedélyezi kifejezetten azt.",
        "tags-delete-not-found": "A(z) „$1” címke nem létezik.",
        "tags-delete-too-many-uses": "A(z) „$1” címke több mint $2 lapváltoztatásban szerepel, ezáltal nem törölhető.",
-       "tags-delete-warnings-after-delete": "A(z) „$1” címke sikeresen törölve lett, de a következő {{PLURAL:$2|figyelmeztetést|figyelmeztetéseket}} találtam:",
-       "tags-delete-no-permission": "Nincs engedélye a változás címkéinek törléséhez.",
+       "tags-delete-warnings-after-delete": "A(z) „$1” címke törölve lett, de a következő {{PLURAL:$2|figyelmeztetést|figyelmeztetéseket}} találtam:",
+       "tags-delete-no-permission": "Nincs engedélyed változáscímkék törléséhez.",
        "tags-activate-title": "Címke aktiválása",
        "tags-activate-question": "Éppen a(z) „$1” címke aktiválására készülsz.",
        "tags-activate-reason": "Indoklás:",
        "tags-deactivate-reason": "Indoklás:",
        "tags-deactivate-not-allowed": "Nem lehetséges a(z) „$1” címkét deaktiválni.",
        "tags-deactivate-submit": "Deaktiválás",
-       "tags-apply-no-permission": "Nincs jogosultságod a szerkesztéseket címkékkel ellátni.",
-       "tags-apply-not-allowed-one": "A(z)  „$1” cimkét nem lehet manuálisan alkalmazni.",
-       "tags-apply-not-allowed-multi": "A következő {{PLURAL:$2|címkét|címkéket}} nem lehet manuálisan alkalmazni: $1",
-       "tags-update-no-permission": "Nincs jogosultságod egyedi változatok és napló bejegyzések címkézésére és címkék eltávolítására.",
-       "tags-update-add-not-allowed-one": "A(z) „$1” címkét nem lehet manuálisan alkalmazni.",
-       "tags-update-add-not-allowed-multi": "A következő {{PLURAL:$2|címkét|címkéket}} nem lehet manuálisan alkalmazni: $1",
+       "tags-apply-no-permission": "Nincs jogosultságod a szerkesztéseidet címkékkel ellátni.",
+       "tags-apply-blocked": "Nem módosíthatsz címkéket, amíg blokkolva vagy.",
+       "tags-apply-not-allowed-one": "A(z) „$1” címke nem alkalmazható manuálisan.",
+       "tags-apply-not-allowed-multi": "A következő {{PLURAL:$2|címke|címkék}} nem alkalmazhatók manuálisan: $1",
+       "tags-update-no-permission": "Nincs jogosultságod egyedi változatok és naplóbejegyzések címkézésére és címkék eltávolítására.",
+       "tags-update-blocked": "Nem adhatsz hozzá vagy távolíthatsz el címkéket, amíg blokkolva vagy.",
+       "tags-update-add-not-allowed-one": "A(z) „$1” címke nem adható hozzá manuálisan.",
+       "tags-update-add-not-allowed-multi": "A következő {{PLURAL:$2|címke|címkék}} nem adhatók hozzá manuálisan: $1",
        "tags-update-remove-not-allowed-one": "A  „$1” címkét nem lehet törölni.",
-       "tags-update-remove-not-allowed-multi": "A következő {{PLURAL:$2|címkét|címkéket}} nem lehet manuálisan eltávolítani: $1",
+       "tags-update-remove-not-allowed-multi": "A következő {{PLURAL:$2|címke|címkék}} nem távolíthatók el manuálisan: $1",
        "tags-edit-title": "Címkék szerkesztése",
        "tags-edit-manage-link": "Címkék kezelése",
        "tags-edit-revision-selected": "[[:$2]] kiválasztott {{PLURAL:$1|változata|változatai}}",
        "tags-edit-logentry-selected": "Kiválasztott napló {{PLURAL:$1|esemény|események}}:",
-       "tags-edit-revision-legend": "Címkék hozzáadás vagy eltávolítása {{PLURAL:$1|ehhez a változathoz|mind a(z) $1 változathoz}}",
+       "tags-edit-revision-legend": "Címkék hozzáadása vagy eltávolítása {{PLURAL:$1|ehhez a változathoz|mind a(z) $1 változathoz}}",
        "tags-edit-logentry-legend": "Címkék hozzáadás vagy eltávolítása {{PLURAL:$1|ehhez a napló bejegyzéshez|mind a(z) $1 napló bejegyzéshez}}",
        "tags-edit-existing-tags": "Létező címkék:",
        "tags-edit-existing-tags-none": "<em>Nincs</em>",
        "tags-edit-failure": "A változásokat nem sikerült alkalmazni:\n$1",
        "tags-edit-nooldid-title": "Érvénytelen változat",
        "tags-edit-nooldid-text": "Nem adtál meg a változatot, vagy a megadott változat nem létezik.",
-       "tags-edit-none-selected": "Válassz legalább egy címlét, amelyet hozzá akarsz adni, vagy törölni szeretnél.",
+       "tags-edit-none-selected": "Válassz legalább egy címkét, amelyet hozzá akarsz adni, vagy törölni szeretnél.",
        "comparepages": "Lapok összehasonlítása",
        "compare-page1": "1. lap",
        "compare-page2": "2. lap",
        "logentry-suppress-block": "$1 {{GENDER:$2|blokkolta}} „{{GENDER:$4|$3}}”-t $5 időtartamra $6",
        "logentry-suppress-reblock": "$1 {{GENDER:$2|módosította}} a blokk beállításokat „{{GENDER:$4|$3}}” szerkesztőnél $5 időtartamra $6",
        "logentry-import-upload": "$1 {{GENDER:$2|importálta}} $3 lapot fájl feltöltéssel",
+       "logentry-import-upload-details": "$1 {{GENDER:$2|importálta}} a(z) $3 lapot fájlfeltöltéssel ($4 lapváltozat).",
        "logentry-import-interwiki": "$1 {{GENDER:$2|importálta}} $3 lapot egy másik wikiből",
+       "logentry-import-interwiki-details": "$1 {{GENDER:$2|importálta}} a(z) $3 lapot a(z) $5 wikiről ($4 lapváltozat).",
        "logentry-merge-merge": "$1 {{GENDER:$2|összevonta}} $3 lapot $4 lappal ($5 változtig)",
        "logentry-move-move": "$1 átnevezte a(z) $3 lapot a következő névre: $4",
        "logentry-move-move-noredirect": "$1 átnevezte a(z) $3 lapot $4 lapra átirányítás nélkül",
        "logentry-newusers-byemail": "Szerkesztői lap $3 néven létrehozva $1 által, jelszó kiküldve emailben.",
        "logentry-newusers-autocreate": "$1 felhasználói fiók automatikusan létrehozva",
        "logentry-protect-move_prot": "$1 {{GENDER:$2|áthelyezte}} a védelmi beállításokat a(z) $4 címről a(z) $3 címre",
+       "logentry-protect-unprotect": "$1 {{GENDER:$2|eltávolította}} a védelmet a(z) $3 lapról",
        "logentry-protect-protect": "$1 {{GENDER:$2|levédte}} a(z) $3 lapot $4",
        "logentry-protect-protect-cascade": "$1 {{GENDER:$2|levédte}} a(z) $3 lapot $4 [kaszkádvédelem]",
        "logentry-protect-modify": "$1 {{GENDER:$2|megváltoztatta}} a(z) $3 lap védelmi szintjét $4",
-       "logentry-rights-rights": "$1 megváltoztatta $3 csoporttagságát erről: $4 erre: $5",
+       "logentry-protect-modify-cascade": "$1 {{GENDER:$2|megváltoztatta}} a(z) $3 lap védelmi szintjét $4 [kaszkád]",
+       "logentry-rights-rights": "$1 {{GENDER:$2|megváltoztatta}} {{GENDER:$6|$3}} csoporttagságát erről: $4 erre: $5",
        "logentry-rights-rights-legacy": "$1 megváltoztatta $3 csoporttagságát",
        "logentry-rights-autopromote": "$1 automatikusan előléptetve erről: $4 erre: $5",
        "logentry-upload-upload": "$1 {{GENDER:$2|feltöltötte}} ezt: $3",
        "logentry-upload-overwrite": "$1 $3 új verzióját {{GENDER:$2|töltötte}} fel",
        "logentry-upload-revert": "$1 {{GENDER:$2|feltöltötte}} $3-t",
-       "log-name-managetags": "Címke kezelő napló",
-       "log-description-managetags": "Ez a lista a a [[Special:Tags|címkéken]] kezelésével kapcsolatos. A napló csak azokat a tevékenységeket tartalmazza, amelyet az adminisztrátorok kézzel hajtottak végre. A wikiszoftver képes napló bejegyzés nélkül is létrehozni és törölni címkéket.",
+       "log-name-managetags": "Címkekezelési napló",
+       "log-description-managetags": "Ez a lap a [[Special:Tags|címkék]] kezelésével kapcsolatos tevékenységeket listázza. A napló csak azokat a műveleteket tartalmazza, amelyet az adminisztrátorok kézzel hajtottak végre; a wikiszoftver képes naplóbejegyzés nélkül is létrehozni és törölni címkéket.",
        "logentry-managetags-create": "$1 {{GENDER:$2|létrehozta}} a(z) „$4” címkét",
-       "logentry-managetags-delete": "$1 {{GENDER:$2|törölte}} a „$4” címkét (eltávolított $5 változatról és/vagy napló bejegyzésről)",
+       "logentry-managetags-delete": "$1 {{GENDER:$2|törölte}} a(z) „$4” címkét (eltávolítva $5 változatról {{PLURAL:$5|vagy|és/vagy}} naplóbejegyzésről)",
        "logentry-managetags-activate": "$1 {{GENDER:$2|aktiválta}} a „$4” címkét a szerkesztők és botok számára történő használatára",
        "logentry-managetags-deactivate": "$1 {{GENDER:$2|deaktiválta}} a „$4” címkét a szerkesztők és botok számára történő használatára",
        "log-name-tag": "Címkenapló",
-       "log-description-tag": "Ez a lap azt tartalmazza, amikor a szerkesztő egyedi változathoz vagy napló bejegyzéshez   [[Special:Tags|címkét]] vett fel, vagy törölte azt. A napló nem tartalmazza azokat a címkézéseket, amikor az szerkesztés, törlés, vagy hasonló tevékenység részeként történik.",
+       "log-description-tag": "Ez a lap azt tartalmazza, amikor felhasználók egyedi változathoz vagy naplóbejegyzéshez [[Special:Tags|címkét]] vettek fel, vagy törölték azt. A napló nem tartalmazza a címkézéseket, ha szerkesztés, törlés vagy más hasonló tevékenység részeként történnek.",
        "logentry-tag-update-add-revision": "$1 {{GENDER:$2|felvette}} a(z) $6 {{PLURAL:$7|címkét|címkéket}} a(z) $3 lap $4 változatához",
-       "logentry-tag-update-add-logentry": "$1 {{GENDER:$2|felvette}} a(z) $6 {{PLURAL:$7|címkét|címkéket}} a(z) $3 lap $5 napló bejegyzéséhez",
+       "logentry-tag-update-add-logentry": "$1 {{GENDER:$2|hozzáadta}} a(z) $6 {{PLURAL:$7|címkét|címkéket}} a(z) $3 lap $5 naplóbejegyzéséhez",
        "logentry-tag-update-remove-revision": "$1 {{GENDER:$2|eltávolította}} a(z) $8 {{PLURAL:$9|címkét|címkéket}} a(z) $3 lap $4 változatából",
        "logentry-tag-update-remove-logentry": "$1 {{GENDER:$2|eltávolította}} a(z) $8 {{PLURAL:$9|címkét|címkéket}} a(z) $3 lap $5 napló bejegyzéséből",
-       "logentry-tag-update-revision": "$1 {{GENDER:$2|frissítette}} a címkéket a(z) $3 lap $4 változatánál ({{PLURAL:$7|hozzáadta}}: $6; {{PLURAL:$9|eltávolította}}: $8)",
+       "logentry-tag-update-revision": "$1 {{GENDER:$2|frissítette}} a címkéket a(z) $3 lap $4 változatánál ({{PLURAL:$7|hozzáadva}}: $6; {{PLURAL:$9|eltávolítva}}: $8)",
        "logentry-tag-update-logentry": "$1 {{GENDER:$2|frissítette}} a címkéket a(z) $3 lap $5 napló bejegyzésénél ({{PLURAL:$7|hozzáadta}}: $6; {{PLURAL:$9|eltávolította}}: $8)",
        "rightsnone": "(semmi)",
        "revdelete-summary": "a szerkesztési összefoglalóját",
        "feedback-useragent": "User agent:",
        "searchsuggest-search": "Keresés",
        "searchsuggest-containing": "tartalmazza…",
+       "api-error-autoblocked": "Az IP-címed automatikusan blokkolva lett, mert korábban egy blokkolt szerkesztő használta.",
        "api-error-badaccess-groups": "Nincs jogod fájlokat feltölteni erre a wikire.",
        "api-error-badtoken": "Belső hiba: hibás token.",
        "api-error-blocked": "Letiltották a szerkesztési jogosultságodat.",
        "api-error-nomodule": "Belső hiba: nincs feltöltőmodul beállítva.",
        "api-error-ok-but-empty": "Belső hiba: nem érkezett válasz a kiszolgálótól.",
        "api-error-overwrite": "Létező fájlok felülírására nem engedélyezett.",
+       "api-error-ratelimited": "A megengedettnél több fájlt próbálsz feltölteni rövid időn belül.\nPróbálkozz újra néhány perc múlva.",
        "api-error-stashfailed": "Belső hiba: a kiszolgálünak nem sikerült eltárolni az ideiglenes fájlt.",
        "api-error-publishfailed": "Belső hiba: a kiszolgálónak nem sikerült közzétennie az ideiglenes fájlt.",
        "api-error-stasherror": "Hiba történt a fájl feltöltése közben.",
        "api-error-unknownerror": "Ismeretlen hiba: „$1”.",
        "api-error-uploaddisabled": "A feltöltés le van tiltva ezen a wikin.",
        "api-error-verification-error": "A fájl feltehetőleg sérült, vagy hibás a kiterjesztése.",
+       "api-error-was-deleted": "Ilyen nevű fájlt már töltöttek fel, majd törölték.",
        "duration-seconds": "{{PLURAL:$1|másodperc|másodperc}}",
        "duration-minutes": "$1 {{PLURAL:$1|perc|perc}}",
        "duration-hours": "{{PLURAL:$1|egy|$1}} óra",
        "expand_templates_generate_xml": "XML elemzési fa mutatása",
        "expand_templates_generate_rawhtml": "Nyers HTML megjelenítése",
        "expand_templates_preview": "Előnézet",
-       "expand_templates_preview_fail_html": "<em>Mivel a(z) {{SITENAME}} engedélyezi a nyers HTML használatát, és a kapcsolati adatok elvesztek, az előnézet el van rejtve a JavaScript támadások megelőzése érdekében.</em>\n\n<strong>Ha ez egy ligitim előnézet kérés, akkor próbáld meg újra!</strong>\nHa nem működik, akkor próbálj meg [[Special:UserLogout|kijelentkezni]] és újra bejelentkezni!",
+       "expand_templates_preview_fail_html": "<em>Mivel a(z) {{SITENAME}} engedélyezi a nyers HTML használatát, és a kapcsolati adatok elvesztek, az előnézet el van rejtve a JavaScript támadások megelőzése érdekében.</em>\n\n<strong>Ha ez egy legitim előnézetkérés, akkor próbáld meg újra!</strong>\nHa nem működik, akkor próbálj meg [[Special:UserLogout|kijelentkezni]] és újra bejelentkezni, és ellenőrizd, hogy a böngésződ elfogad-e sütiket erről az oldalról.",
        "expand_templates_preview_fail_html_anon": "<em>Mivel a(z) {{SITENAME}} engedélyezi a nyers HTML használatát, és a kapcsolati adatok elvesztek, az előnézet el van rejtve a JavaScript támadások megelőzése érdekében.</em>\n\n<strong>Ha ez egy legitim előnézet kérés, akkor próbálj meg [[Special:UserLogin|bejelentkezni]] és újra próbálni!</strong>",
+       "expand_templates_input_missing": "Legalább egy kevés bemeneti szöveget meg kell adnod.",
        "pagelanguage": "Oldal nyelvének megváltoztatása",
        "pagelang-name": "Oldal",
        "pagelang-language": "Nyelv",
        "special-characters-group-ipa": "IPA",
        "special-characters-group-symbols": "Szimbólumok",
        "special-characters-group-greek": "Görög",
+       "special-characters-group-greekextended": "Bővített görög",
        "special-characters-group-cyrillic": "Cirill",
        "special-characters-group-arabic": "Arab",
        "special-characters-group-arabicextended": "Arab (bővített)",
        "mw-widgets-dateinput-placeholder-month": "ÉÉÉÉ-HH",
        "mw-widgets-titleinput-description-new-page": "a lap még nem létezik",
        "mw-widgets-titleinput-description-redirect": "átirányítás ide: $1",
+       "sessionmanager-tie": "Nem kombinálható többféle hitelesítési típus: $1.",
        "sessionprovider-generic": "$1-munkamenetek",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "sütialapú munkamenetek",
        "sessionprovider-nocookies": "A sütik le lehetnek tiltva. Engedélyezd a sütiket, és próbáld meg újra!",
-       "randomrootpage": "Véletlen lap a gyökérből",
+       "randomrootpage": "Gyökérlap találomra",
        "log-action-filter-block": "Blokk típusa:",
+       "log-action-filter-contentmodel": "Tartalommodell-változtatás típusa:",
        "log-action-filter-delete": "Törlés típusa:",
        "log-action-filter-import": "Importálás típusa:",
+       "log-action-filter-managetags": "Címkekezelési művelet típusa:",
        "log-action-filter-move": "Átnevezés típusa:",
+       "log-action-filter-newusers": "Fióklétrehozás típusa:",
        "log-action-filter-patrol": "Járőrözés típusa:",
        "log-action-filter-protect": "Lapvédelem típusa:",
+       "log-action-filter-rights": "Jogosultságváltozás típusa:",
        "log-action-filter-upload": "Feltöltés típusa:",
        "log-action-filter-all": "Mind",
        "log-action-filter-block-block": "Blokk",
        "log-action-filter-block-reblock": "Blokk módosítása",
        "log-action-filter-block-unblock": "Blokk feloldása",
+       "log-action-filter-contentmodel-change": "Tartalommodell módosítása",
+       "log-action-filter-contentmodel-new": "Lap létrehozása nem alapértelmezett tartalommodellel",
        "log-action-filter-delete-delete": "Laptörlés",
        "log-action-filter-delete-restore": "Visszaállítás",
        "log-action-filter-delete-event": "Naplótörlés",
+       "log-action-filter-delete-revision": "Lapváltozat-törlés",
+       "log-action-filter-import-interwiki": "Wikiközi importálás",
+       "log-action-filter-import-upload": "Importálás XML-feltöltéssel",
        "log-action-filter-managetags-create": "Címke létrehozása",
        "log-action-filter-managetags-delete": "Címke törlése",
-       "log-action-filter-managetags-activate": "Tag aktiválása",
+       "log-action-filter-managetags-activate": "Címke aktiválása",
+       "log-action-filter-managetags-deactivate": "Címke deaktiválása",
+       "log-action-filter-move-move": "Átnevezés átirányítások felülírása nélkül",
+       "log-action-filter-move-move_redir": "Átnevezés átirányítások felülírásával",
        "log-action-filter-newusers-create": "Létrehozás által anonim felhasználó által",
        "log-action-filter-newusers-create2": "Létrehozás regisztrált felhasználó által",
        "log-action-filter-newusers-autocreate": "Automatikus létrehozás",
        "log-action-filter-newusers-byemail": "Létrehozás jelszóval, e-mail által küldve",
+       "log-action-filter-patrol-patrol": "Kézi ellenőrzés",
+       "log-action-filter-patrol-autopatrol": "Automatikus ellenőrzés",
        "log-action-filter-protect-protect": "Lapvédelem",
+       "log-action-filter-protect-modify": "Védelem módosítása",
        "log-action-filter-protect-unprotect": "Védelem feloldása",
+       "log-action-filter-protect-move_prot": "Védelem áthelyezése",
        "log-action-filter-rights-rights": "Kézi módosítás",
+       "log-action-filter-rights-autopromote": "Automatikus módosítás",
        "log-action-filter-upload-upload": "Új feltöltés",
+       "log-action-filter-upload-overwrite": "Újrafeltöltés",
+       "authmanager-authn-not-in-progress": "Hitelesítés nincs folyamatban, vagy a folyamat adatai elvesztek. Kérjük, indítsd újra az elejétől.",
+       "authmanager-authn-no-primary": "A megadott hitelesítő adatokkal nem lehet hitelesíteni.",
+       "authmanager-authn-no-local-user": "A megadott hitelesítő adatok nincsenek társítva egyetlen felhasználóval sem ezen a wikin.",
+       "authmanager-authn-autocreate-failed": "A helyi fiók automatikus létrehozása sikertelen: $1",
+       "authmanager-change-not-supported": "A megadott hitelesítő adatokat nem változtathatók meg, mivel semmi sem használná őket.",
        "authmanager-create-disabled": "Új fiók létrehozása tiltva.",
        "authmanager-create-from-login": "A fiókja létrehozásához, kérjük, töltse ki az alábbi mezőket.",
        "authmanager-create-not-in-progress": "Fiók létrehozása nincs folyamatban, vagy a folyamat adatai elvesztek. Kérjük, indítsa újra az elejétől.",
+       "authmanager-create-no-primary": "A megadott hitelesítő adatok nem használhatók fióklétrehozásra.",
+       "authmanager-link-no-primary": "A megadott hitelesítő adatok nem használhatók fiókok összekapcsolására.",
+       "authmanager-link-not-in-progress": "Fiókok összekapcsolása nincs folyamatban, vagy a folyamat adatai elvesztek. Kérjük, indítsd újra az elejétől.",
        "authmanager-authplugin-setpass-failed-title": "Jelszó megváltoztatása nem sikerült",
        "authmanager-authplugin-setpass-failed-message": "A hitelesítés beépülője megtagadta a jelszó módosítását.",
        "authmanager-authplugin-create-fail": "A hitelesítés beépülője megtagadta a fiók létrehozását.",
        "authmanager-autocreate-noperm": "Az automatikus fióklétrehozás nem engedélyezett.",
        "authmanager-autocreate-exception": "A fiókok automatikus létrehozását átmenetileg letiltottuk a korábbi hibák miatt.",
        "authmanager-userdoesnotexist": "A(z) „$1” felhasználó nincs regisztrálva.",
+       "authmanager-userlogin-remembermypassword-help": "Megjegyezze-e a jelszót a munkamenetet követően is.",
+       "authmanager-username-help": "Felhasználónév a hitelesítéshez.",
+       "authmanager-password-help": "Jelszó a hitelesítéshez.",
+       "authmanager-domain-help": "Tartomány külső hitelesítéshez.",
        "authmanager-retype-help": "Jelszó még egyszer a megerősítéshez.",
        "authmanager-email-label": "E-mail",
        "authmanager-email-help": "E-mail-cím",
        "authmanager-provider-password": "Jelszó alapú hitelesítés",
        "authmanager-provider-password-domain": "Jelszó - domain-alapú hitelesítés",
        "authmanager-provider-temporarypassword": "Ideiglenes jelszó",
+       "authprovider-confirmlink-request-label": "Összekapcsolandó fiókok",
+       "authprovider-confirmlink-success-line": "$1: Sikeresen összekapcsolva.",
+       "authprovider-confirmlink-failed": "A fiókok összekapcsolása nem volt teljesen sikeres: $1",
+       "authprovider-confirmlink-ok-help": "Folytatás az összekapcsolási hibák megjelenítése után.",
        "authprovider-resetpass-skip-label": "Kihagy",
+       "authprovider-resetpass-skip-help": "Jelszó visszaállításának kihagyása.",
+       "authform-nosession-login": "A hitelesítés sikeres volt, de a böngésződ nem tud „emlékezni” arra, hogy be vagy jelentkezve.\n\n$1",
+       "authform-nosession-signup": "A fiók létrejött, de a böngésződ nem tud „emlékezni” arra, hogy be vagy jelentkezve.\n\n$1",
+       "authform-newtoken": "Hiányzó token. $1",
+       "authform-notoken": "Hiányzó token",
+       "authform-wrongtoken": "Rossz token",
+       "specialpage-securitylevel-not-allowed-title": "Nem engedélyezett",
+       "specialpage-securitylevel-not-allowed": "Sajnáljuk, nem nézheted meg ezt a lapot, mert a személyazonosságod ellenőrzése nem sikerült.",
+       "authpage-cannot-login": "A bejelentkezés elkezdése sikertelen.",
+       "authpage-cannot-login-continue": "A bejelentkezés folytatása sikertelen. Valószínűleg lejárt a munkameneted.",
+       "authpage-cannot-create": "A fióklétrehozás elkezdése sikertelen.",
+       "authpage-cannot-create-continue": "A fióklétrehozás folytatása sikertelen. Valószínűleg lejárt a munkameneted.",
+       "authpage-cannot-link": "A fiókok összekapcsolásának elkezdése sikertelen.",
+       "authpage-cannot-link-continue": "A fiókok összekapcsolásának folytatása sikertelen. Valószínűleg lejárt a munkameneted.",
        "cannotauth-not-allowed-title": "Engedély megtagadva",
+       "cannotauth-not-allowed": "Nincs engedélyed az oldal használatára",
+       "changecredentials": "Hitelesítő adatok módosítása",
+       "changecredentials-submit": "Hitelesítő adatok módosítása",
+       "changecredentials-invalidsubpage": "$1 nem egy érvényes hitelesítőadat-típus.",
+       "changecredentials-success": "A hitelesítő adataid megváltoztak.",
+       "removecredentials": "Hitelesítő adatok eltávolítása",
+       "removecredentials-submit": "Hitelesítő adatok eltávolítása",
+       "removecredentials-invalidsubpage": "$1 nem egy érvényes hitelesítőadat-típus.",
+       "removecredentials-success": "A hitelesítő adataid eltávolítva.",
+       "credentialsform-provider": "Hitelesítő adatok típusa:",
        "credentialsform-account": "Fiók neve:",
        "cannotlink-no-provider-title": "Nincsenek csatolható fiókok",
        "cannotlink-no-provider": "Nincsenek csatolható fiókok.",
        "linkaccounts": "A fiókok csatolása",
-       "linkaccounts-success-text": "A fiók csatolva."
+       "linkaccounts-success-text": "A fiók csatolva.",
+       "linkaccounts-submit": "Fiókok összekapcsolása",
+       "unlinkaccounts": "Fiókok szétkapcsolása",
+       "unlinkaccounts-success": "A fiókok szétkapcsolva.",
+       "authenticationdatachange-ignored": "A hitelesítő adatok változtatása nincs kezelve. Talán nincs beállítva szolgáltató?",
+       "userjsispublic": "Figyelem: JavaScript-allapokon ne tárolj bizalmas adatokat, mivel minden felhasználó számára láthatóak.",
+       "usercssispublic": "Figyelem: CSS-allapokon ne tárolj bizalmas adatokat, mivel minden felhasználó számára láthatóak."
 }
index 20f83e5..5920b06 100644 (file)
        "botpasswords-label-cancel": "Չեղարկել",
        "botpasswords-label-delete": "Ջնջել",
        "botpasswords-label-resetpassword": "Վերականգնել ծածկագիրը",
-       "botpasswords-label-restrictions": "Օգտագործման սահմանափակումներ:",
        "botpasswords-label-grants-column": "Թույլատրված է",
        "botpasswords-bad-appid": "\"$1\" բոտի անունն անթույլատրելի է:",
        "botpasswords-created-title": "Բոտի ծածկագիրը ստեղծվել է",
        "nextn-title": "Հաջորդ $1 {{PLURAL:$1|արդյունքը|արդյունքները}}",
        "shown-title": "Յուրաքանչյուր էջում ցույց տալ $1 {{PLURAL:$1|գրառում|գրառումներ}}",
        "viewprevnext": "Դիտել ($1 {{int:pipe-separator}} $2) ($3)",
-       "searchmenu-exists": "'''Այս վիքիում, գոյություն ունի \"[[:$1]]\" անվանումով էջը։'''",
+       "searchmenu-exists": "'''Այս վիքիում գոյություն ունի \"[[:$1]]\" անվանումով էջ։'''",
        "searchmenu-new": "<strong>Ստեղծել «[[:$1]]» էջը այս վիքիում։</strong> {{PLURAL:$2|0=|Տես նաև քո որոնած բառով գտնված էջը|Տես նաև որոնման արդյունքները։}}",
        "searchprofile-articles": "Հիմնական էջեր",
        "searchprofile-images": "Մուլտիմեդիա",
index f6fb4b1..fc9b0a6 100644 (file)
        "botpasswords-label-resetpassword": "Setel ulang kata sandi",
        "botpasswords-label-grants": "Akses yang dapat diberikan:",
        "botpasswords-help-grants": "Tiap izin memberikan akses ke hak-hak pengguna yang telah dimiliki suatu akun pengguna. Lihat [[Special:ListGrants|tabel izin]] untuk informasi lebih lanjut.",
-       "botpasswords-label-restrictions": "Batasan penggunaan:",
        "botpasswords-label-grants-column": "Izin diberikan",
        "botpasswords-bad-appid": "Nama bot \"$1\" tidak valid.",
        "botpasswords-insert-failed": "Gagal menambah nama bot \"$1\". Apakah sudah ditambahkan sebelum ini?",
index c4b08ba..a5bdb02 100644 (file)
        "eauthentsent": "Un messaggio email di conferma è stato spedito all'indirizzo indicato.\nPer abilitare l'invio di messaggi email per questo utente è necessario seguire le istruzioni che vi sono indicate, in modo da confermare che si è i legittimi proprietari dell'indirizzo.",
        "throttled-mailpassword": "Una email di reimpostazione della password è già stata inviata da meno di {{PLURAL:$1|1 ora|$1 ore}}.\nPer prevenire abusi, la funzione di reimpostazione della password può essere usata solo una volta ogni {{PLURAL:$1|ora|$1 ore}}.",
        "mailerror": "Errore nell'invio del messaggio: $1",
-       "acct_creation_throttle_hit": "{{PLURAL:$1|1 registrazione è già stata effettuata|$1 registrazioni sono già state effettuate}} da qualcuno con il tuo stesso indirizzo IP nell'ultimo giorno: è il massimo consentito in questo periodo di tempo.\nPerciò, gli utenti che usano questo indirizzo IP non possono registrarsi per il momento.",
+       "acct_creation_throttle_hit": "{{PLURAL:$1|1 registrazione è già stata effettuata|$1 registrazioni sono già state effettuate}} da qualcuno con il tuo stesso indirizzo IP negli ultimi $2, che è il massimo consentito in questo periodo di tempo.\nPerciò, gli utenti che usano questo indirizzo IP non possono più registrarsi per il momento.",
        "emailauthenticated": "L'indirizzo email è stato confermato il $2 alle $3.",
        "emailnotauthenticated": "L'indirizzo di posta elettronica non è stato ancora confermato.\nNon verranno inviati messaggi email per le funzioni elencate di seguito.",
        "noemailprefs": "Indicare un indirizzo e-mail per attivare queste funzioni.",
        "botpasswords-label-resetpassword": "Reimposta la password",
        "botpasswords-label-grants": "Assegnazioni applicabili:",
        "botpasswords-help-grants": "Ogni assegnazione dà accesso ai diritti utente elencati che un'utenza ha già. Vedi la [[Special:ListGrants|tabella delle assegnazioni]] per ulteriori informazioni.",
-       "botpasswords-label-restrictions": "Restrizioni d'uso:",
        "botpasswords-label-grants-column": "Assegnazioni",
        "botpasswords-bad-appid": "Il nome bot \"$1\" non è valido.",
        "botpasswords-insert-failed": "Impossibile aggiungere il nome bot \"$1\". È stato già aggiunto?",
        "passwordreset-emailelement": "Nome utente: \n$1\n\nPassword temporanea: \n$2",
        "passwordreset-emailsentemail": "Se questo indirizzo di posta elettronica è associato con la tua utenza, allora verrà inviata una email per reimpostare la password.",
        "passwordreset-emailsentusername": "Se c'è un indirizzo di posta elettronica associato con questo nome utente, allora verrà inviata una email per reimpostare la password.",
-       "passwordreset-emailsent-capture2": "L'email di reimpostazione della password {{PLURAL:$1|è stata inviata|sono state inviate}}. {{PLURAL:$1|Il nome|L'elenco di nomi}} utente e password è mostrato di seguito.",
-       "passwordreset-emailerror-capture2": "Invio di email {{GENDER:$2|all'utente}} non riuscito: $1. {{PLURAL:$3|Il nome|L'elenco di nomi}} utente e password è mostrato di seguito.",
+       "passwordreset-emailsent-capture2": "L'email di reimpostazione della password {{PLURAL:$1|è stata inviata|sono state inviate}}. {{PLURAL:$1|Il nome|L'elenco di nomi}} utente e password è mostrato qui.",
+       "passwordreset-emailerror-capture2": "Invio di email {{GENDER:$2|all'utente}} non riuscito: $1. {{PLURAL:$3|Il nome|L'elenco di nomi}} utente e password è mostrato qui.",
        "passwordreset-nocaller": "Un chiamante deve essere fornito",
        "passwordreset-nosuchcaller": "Chiamante non esiste: $1",
        "passwordreset-ignored": "La reimpostazione della password non è stata gestita. Forse nessun provider è configurato?",
        "linkaccounts-success-text": "L'utenza è stata collegata.",
        "linkaccounts-submit": "Collega utenze",
        "unlinkaccounts": "Scollega utenze",
-       "unlinkaccounts-success": "L'utenza è stata scollegata."
+       "unlinkaccounts-success": "L'utenza è stata scollegata.",
+       "restrictionsfield-badip": "Indirizzo IP o intervallo non valido: $1",
+       "restrictionsfield-label": "Intervalli IP consentiti:",
+       "restrictionsfield-help": "Un indirizzo IP o intervallo CIDR per linea. Per consentire tutto, utilizza<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index 892ba1b..72b6873 100644 (file)
        "botpasswords-label-resetpassword": "パスワードをリセット",
        "botpasswords-label-grants": "該当する権限群",
        "botpasswords-help-grants": "各権限群は、一覧にある利用者権限で現在の利用者アカウントが既に有している権限を付与します。詳細については、[[Special:ListGrants|権限群の表]]をご覧ください。",
-       "botpasswords-label-restrictions": "使用制限:",
        "botpasswords-label-grants-column": "付与",
        "botpasswords-bad-appid": "ボット「$1」は有効ではありません。",
        "botpasswords-insert-failed": "ボット「$1」の追加に失敗しました。既に追加されていないか確認してください。",
index a105962..a85c60b 100644 (file)
        "botpasswords-label-resetpassword": "პაროლის აღდგენა",
        "botpasswords-label-grants": "გამოყენებადი ნებართვები:",
        "botpasswords-help-grants": "ყოველი ნებართვა იძლევა წვდომას ჩამოთვლილ მომხმარებელთა უფლებებზე, რომელიც მომხმარებელს აქვს. იხილეთ [[Special:ListGrants|ნებართვების ცხრილი]] მეტი ინფორმაციისთვის.",
-       "botpasswords-label-restrictions": "გამოყენების შეზღუდვები:",
        "botpasswords-label-grants-column": "მინიჭებულია",
        "botpasswords-bad-appid": "ბოტის სახელი \"$1\" არ არის მართებული.",
        "botpasswords-insert-failed": "ბოტის სახელის $1\" დამატება შეუძლებელია. უკვე დამატებულია?",
index 6d3296c..ef53d84 100644 (file)
@@ -15,7 +15,8 @@
                        "Batyrbek.kz",
                        "Matma Rex",
                        "Nemo bis",
-                       "Mormegil"
+                       "Mormegil",
+                       "Mirgulkali"
                ]
        },
        "tog-underline": "Сілтеменің астын сызу:",
        "talk": "Талқылау",
        "views": "Көрініс",
        "toolbox": "Құралдар",
+       "tool-link-emailuser": "Мұны электронды поштамен жіберіңіз {{GENDER:$1|user}}",
        "userpage": "Қатысушы бетін қарау",
        "projectpage": "Жоба бетін қарау",
        "imagepage": "Файл бетін қарау",
        "createaccountreason": "Себебі:",
        "createacct-reason": "Себебі:",
        "createacct-reason-ph": "Неге басқа тіркегі жасамақшысыз",
-       "createacct-submit": "Тіркелгіңізді жасаңыз",
+       "createacct-submit": "Тіркеліңіз",
        "createacct-another-submit": "Тіркелгі жасау",
        "createacct-continue-submit": "Тіркелуді жалғастыру",
        "createacct-benefit-heading": "{{SITENAME}} сіздермен жасалады.",
        "eauthentsent": "Құптау хаты көрсетілген е-пошта мекенжайына жөнелтілді.\nКез-келген басқа е-пошта хатын тіркелгіге жөнелту алдынан, тіркелгі шынымен сіздікі екенін құптау үшін хаттағы нұсқамаларға лесіңіз.",
        "throttled-mailpassword": "Соңғы {{PLURAL:$1|сағатта|$1 сағатта}} құпия сөзді өзгерту хаты әлдеқашан жіберілді.\nҚиянатты қақпайлау үшін {{PLURAL:$1|сағат|$1 сағат}} сайын тек бір ғана құпия сөзді өзгерту хаты жіберіледі.",
        "mailerror": "Хат жөнелту қатесі: $1",
-       "acct_creation_throttle_hit": "Сіздің IP мекенжайыңызбен осы уикиге кірушілер соңғы күнде {{PLURAL:$1|1 тіркелгі|$1 тіркелгі}} жасапты. Одан артық бұл уақыт аралығында рұқсат етілмейді.\nНәтижесінде осы IP мекенжайды пайдаланып кірушілер дәл қазіргі уақытта бірнеше тіркелгі жасай алмайды.",
+       "acct_creation_throttle_hit": "Сіздің IP мекенжайыңызбен осы уикиге кірушілер соңғы $2 {{PLURAL:$1|1 тіркелгі|$1 тіркелгі}} жасапты. Одан артық бұл уақыт аралығында рұқсат етілмейді.\nНәтижесінде осы IP мекенжайды пайдаланып кірушілер дәл қазіргі уақытта бірнеше тіркелгі жасай алмайды.",
        "emailauthenticated": "Е-пошта мекенжайыңыз расталған кезі: $3, $2.",
        "emailnotauthenticated": "Е-пошта мекенжайыңыз әлі расталған жоқ.\nКелесі әрбір мүмкіндіктер үшін еш хат жөнелтілмейді.",
        "noemailprefs": "Осы мүмкіндіктер істеуі үшін е-пошта мекен-жайыңызды енгізіңіз.",
        "botpasswords-label-delete": "Жою",
        "botpasswords-label-resetpassword": "Құпия сөзді қалпына кеттіру",
        "botpasswords-label-grants": "Қолданылатын гранттар:",
-       "botpasswords-label-restrictions": "Пайдалану шектеулері:",
        "botpasswords-bad-appid": "\"$1\" бот атауы жарамды емес.",
        "botpasswords-insert-failed": "\"$1\" бот атауын қосу орындалмады. Ол әлдеқашан қосылған ба еді?",
        "botpasswords-update-failed": "\"$1\" бот атауын жаңарту орындалмады. Ол әлдеқашан жойылған ба еді?",
        "passwordreset-emailtext-user": "$1 есімді қатысушы {{SITENAME}} сайтында ($4) құпия сөзді өзгертуге өтініш білдірді. Мына қатысушы {{PLURAL:$3|аккаунт|аккаунттар}} осы електронды почта қатысты:\n\n$2\n\n{{PLURAL:$3|Бұл уақытша құпия сөз|Бұл уақытша құпия сөздер}} {{PLURAL:$5|бір күнде|$5 күнде}}уақыты аяқталады.\nСіз кіруіңіз және жаңа құпия сөзді таңдауыңыз керек. Егер бұл өтінішті басқа біреу жасаса, немесе сіз  бұрынғы құпия сөзіңізді еске түсірсеңіз, және құпия сөзді ауыстыруды қаламасаңыз, сіз бұл хабарламаны ескермей және бұрыңғы құпия сөзді қолдана беруіңізге болады.",
        "passwordreset-emailelement": "Қатысушы есімі: \n$1\n\nУақытша құпия сөз: \n$2",
        "passwordreset-emailsentemail": "Бұл email мекенжайы тіркелгіңізге байланысқан, сол себепті құпия сөзді өзгерту электронды пошта арқылы жөнелтіледі.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|email has|emails have}}үшін құпия сөздің қалпына келтіру хабарламасы жіберілді. {{PLURAL:$1|username and password|list of usernames and passwords}} мында көрсетілген.",
+       "passwordreset-emailerror-capture2": "{{GENDER:$2|user}}-мен электронды поштамен хабарласу нәтижесіз қалды: $1 The {{PLURAL:$3|username and password|list of usernames and passwords}} мында көрсетілген.",
        "changeemail": "Е-пошта мекенжайын өзгерту немесе аластау",
        "changeemail-header": "Е-пошта мекен-жайының өзгертілуі",
        "changeemail-no-info": "Бұл бетке тікелей ену үшін жүйеге кіруіңіз керек.",
index 5d2c25c..f09c38b 100644 (file)
@@ -36,6 +36,7 @@
        "tog-hideminor": "ಇತ್ತೀಚಿನ ಬದಲಾವಣೆಗಳಲ್ಲಿ ಚಿಕ್ಕಪುಟ್ಟ ಸಂಪಾದನೆಗಳನ್ನು ಅಡಗಿಸಿ",
        "tog-hidepatrolled": "ಪಹರೆಯಲ್ಲಿ ಆದ ಸಂಪಾದನೆಗಳನ್ನು ಇತ್ತೀಚೆಗಿನ ಬದಲಾವಣೆಗಳಲ್ಲಿ ಅಡಗಿಸು",
        "tog-newpageshidepatrolled": "ಪಹರೆಯಲ್ಲಿ ಆದ ಪುಟಗಳನ್ನು ಹೊಸ ಪುಟಗಳ ಪಟ್ಟಿಯಲ್ಲಿ ಅಡಗಿಸು",
+       "tog-hidecategorization": "ಪುಟಗಳ ವರ್ಗೀಕರಣವನ್ನು ಅಡಗಿಸು",
        "tog-extendwatchlist": "ಕೇವಲ ಇತ್ತೀಚೆಗಿನ ಬದಲಾವಣೆಗಳಲ್ಲದೆ, ಎಲ್ಲಾ ಬದಲಾವಣೆಗಳನ್ನು ತೋರುವಂತೆ ಪಟ್ಟಿಯನ್ನು ವಿಸ್ತರಿಸಿ",
        "tog-usenewrc": "ಹೆಚ್ಚು ವರ್ಧಿಸಲಾದ ಇತ್ತೀಚಿನ ಬದಲಾವಣೆಗಳು ಪುಟ ಬಳಸು",
        "tog-numberheadings": "ತಲೆಬರಹಗಳಿಗೆ ಅಂಕಿಗಳನ್ನು ತೋರಿಸು",
@@ -46,6 +47,7 @@
        "tog-watchdefault": "ನಾನು ಸಂಪಾದಿಸುವ ಪುಟಗಳನ್ನು ವೀಕ್ಷಣಾಪಟ್ಟಿಗೆ ಸೇರಿಸು",
        "tog-watchmoves": "ನಾನು ಸ್ಥಳಾಂತರಿಸುವ ಪುಟಗಳನ್ನು ನನ್ನ ವೀಕ್ಷಣಾಪಟ್ಟಿಗೆ ಸೇರಿಸು",
        "tog-watchdeletion": "ನಾನು ಅಳಿಸುವ ಪುಟಗಳನ್ನು ನನ್ನ ವೀಕ್ಷಣಾ ಪಟ್ಟಿಗೆ ಸೇರಿಸು",
+       "tog-watchuploads": "ನಾನು ಹೊಸದಾಗಿ ಅಪ್‍ಲೋಡ್ ಮಾಡಿದ ಫೈಲ್‍ಗಳನ್ನು ನನ್ನ ವೀಕ್ಷಣಾಪಟ್ಟಿಗೆ ಸೇರಿಸು",
        "tog-watchrollback": "ನಾನು ಹಿಮ್ಮರಳುವಿಕೆಯನ್ನು ನಡೆಸಿದ ಪುಟಗಳನ್ನು ನನ್ನ ಗಮನಸೂಚಿಗೆ ಸೇರಿಸು",
        "tog-minordefault": "ನನ್ನ ಎಲ್ಲಾ ಸಂಪಾದನೆಗಳನ್ನು ಚುಟುಕಾದವು ಎಂದು ಗುರುತು ಮಾಡು",
        "tog-previewontop": "ಮುನ್ನೋಟವನ್ನು ಸಂಪಾದನೆ ಚೌಕದ ಮುಂಚೆ ತೋರು",
@@ -55,7 +57,7 @@
        "tog-enotifminoredits": "ಚಿಕ್ಕ-ಪುಟ್ಟ ಬದಲಾವಣೆಗಳಾದಾಗಲೂ ಇ-ಅಂಚೆ ಕಳುಹಿಸು",
        "tog-enotifrevealaddr": "ಪ್ರಕಟಣೆ ಇ-ಅಂಚೆಗಳಲ್ಲಿ ನನ್ನ ಇ-ಅಂಚೆ ವಿಳಾಸ ತೋರು",
        "tog-shownumberswatching": "ಪುಟವನ್ನು ವೀಕ್ಷಿಸುತ್ತಿರುವ ಸದಸ್ಯರ ಸಂಖ್ಯೆಯನ್ನು ತೋರಿಸು",
-       "tog-oldsig": "ಪ್ರಸ್ತುತ ಸಹಿ",
+       "tog-oldsig": "ನಿಮà³\8dಮ à²ªà³\8dರಸà³\8dತà³\81ತ à²¸à²¹à²¿",
        "tog-fancysig": "ಸರಳ ಸಹಿಗಳು (ಕೊಂಡಿ ಇಲ್ಲದಿರುವಂತೆ)",
        "tog-uselivepreview": "ನೇರ ಮುನ್ನೋಟವನ್ನು ಉಪಯೋಗಿಸಿ",
        "tog-forceeditsummary": "ಸಂಪಾದನೆ ಸಾರಾಂಶವನ್ನು ಖಾಲಿ ಬಿಟ್ಟಲ್ಲಿ ನೆನಪಿಸು",
        "tog-watchlisthideliu": "ಲಾಗ್ ಇನ್ ಆಗಿರುವ ಸದಸ್ಯರ ಸಂಪಾದನೆಗಳನ್ನು ವೀಕ್ಷಣಾಪಟ್ಟಿಯಲ್ಲಿ ಅಡಗಿಸು",
        "tog-watchlisthideanons": "ಅನಾಮಧೇಯ ಬಳಕೆದಾರರ ಸಂಪಾದನೆಗಳನ್ನು ವೀಕ್ಷಣಾಪಟ್ಟಿಯಲ್ಲಿ ಅಡಗಿಸು",
        "tog-watchlisthidepatrolled": "ವೀಕ್ಷಣಾ ಪತ್ತಿಯಲ್ಲಿ ಹಸ್ತುಕದರ್ ಬದಲಾವಣೆಗಳನ್ನು ಅದಗಿಸು",
+       "tog-watchlisthidecategorization": "ಪುಟಗಳ ವರ್ಗೀಕರಣವನ್ನು ಅಡಗಿಸು",
        "tog-ccmeonemails": "ಇತರರಿಗೆ ನಾನು ಕಳುಹಿಸುವ ಇ-ಅಂಚೆಯ ಪ್ರತಿಯನ್ನು ನನಗೂ ಕಳುಹಿಸು",
        "tog-diffonly": "ವ್ಯತ್ಯಾಸಗಳ ಕೆಳಗಿರುವ ಪುಟದ ವಿವರಗಳನ್ನು ತೋರಿಸಬೇಡ",
        "tog-showhiddencats": "ಅಡಗಿಸಲ್ಪಟ್ಟ ವರ್ಗಗಳನ್ನು ತೋರಿಸು",
-       "tog-norollbackdiff": "ತà³\8aಡà³\86ದà³\81ಹಾà²\95ಿದ à²¨à²\82ತರ à²µà³\8dಯತà³\8dಯಸವನà³\8dನà³\81 à²¬à²¿à²¦à³\81",
+       "tog-norollbackdiff": "ತà³\8aಡà³\86ದà³\81ಹಾà²\95ಿದ à²¨à²\82ತರ à²µà³\8dತà³\8dಯತà³\8dಯಾಸವನà³\8dನà³\81 à²¤à³\8bರಿಸಬà³\87ಡ",
        "tog-useeditwarning": "ಸಂಪಾದನೆಯನ್ನು ಉಳಿಸದೆ ಹೊರಟಲ್ಲಿ ನನಗೆ ಎಚ್ಚರಿಸು",
        "tog-prefershttps": "ಯಾವತ್ತು ಸಹ ಲಾಗಿನ್ ನಂತರ ಸುರಕ್ಷಿತ ಸಂಪರ್ಕವನ್ನು ಬಳಸಿ",
        "underline-always": "ಯಾವಾಗಲೂ",
        "october-date": "ಅಕ್ಟೋಬರ್ $1",
        "november-date": "ನವೆಂಬರ್ $1",
        "december-date": "ಡಿಸೆಂಬರ್ $1",
+       "period-am": "ಪೂರ್ವಾಹ್ನ",
+       "period-pm": "ಅಪರಾಹ್ನ",
        "pagecategories": "{{PLURAL:$1|ವರ್ಗ|ವರ್ಗಗಳು}}",
        "category_header": "\"$1\" ವರ್ಗದಲ್ಲಿರುವ ಲೇಖನಗಳು",
        "subcategories": "ಉಪವರ್ಗಗಳು",
        "newwindow": "(ಹೊಸ ಕಿಟಕಿಯಲ್ಲಿ ತೆರೆಯುತ್ತದೆ)",
        "cancel": "ರದ್ದುಮಾಡು",
        "moredotdotdot": "ಇನ್ನಷ್ಟು...",
-       "morenotlisted": "à²\88 à²ªà²\9fà³\8dà²\9fಿ à²ªà³\82ರ à²\87ಲà³\8dಲ.",
+       "morenotlisted": "à²\88 à²ªà²\9fà³\8dà²\9fಿ à²\85ಪರಿಪà³\82ರà³\8dಣವಾà²\97ಿರಬಹà³\81ದà³\81.",
        "mypage": "ಪುಟ",
        "mytalk": "ಚರ್ಚೆ",
        "anontalk": "ಚರ್ಚೆ",
        "yourpasswordagain": "ಪ್ರವೇಶ ಪದ ಮತ್ತೊಮ್ಮೆ ಟೈಪ್ ಮಾಡಿ",
        "createacct-yourpasswordagain": "ಪ್ರವೇಶಪದವನ್ನು ಧೃಡೀಕರಿಸಿ",
        "createacct-yourpasswordagain-ph": "ಪ್ರವೇಶಪದವನ್ನು ಮತ್ತೊಮ್ಮೆ ನಮೂದಿಸಿ",
-       "remembermypassword": "ಈ ಗಣಕಯಂತ್ರದಲ್ಲಿ ನನ್ನ ಲಾಗಿನ್ ನೆನಪಿನಲ್ಲಿಟ್ಟುಕೊ (ಗರಿಷ್ಠ $1 {{PLURAL:$1|ದಿನದ|ದಿನಗಳ}}ವರೆಗೆ)",
        "userlogin-remembermypassword": "ನನ್ನನ್ನು ಲಾಗಿನ್ ಆಗಿಯೇ ಇಡಿ",
        "userlogin-signwithsecure": "ಸುರಕ್ಷಿತವಾದ ಕನೆಕ್ಷನ್ ಉಪಯೋಗಿಸಿ.",
        "yourdomainname": "ನಿಮ್ಮ ಕ್ಷೇತ್ರ:",
index db9434b..1689175 100644 (file)
        "talk": "토론",
        "views": "보기",
        "toolbox": "도구",
+       "tool-link-userrights": "{{GENDER:$1|사용자}} 그룹 변경",
        "tool-link-emailuser": "이 {{GENDER:$1|사용자}}에게 이메일 보내기",
        "userpage": "사용자 문서 보기",
        "projectpage": "프로젝트 문서 보기",
        "botpasswords-label-resetpassword": "비밀번호 재설정",
        "botpasswords-label-grants": "적용할 수 있는 부여:",
        "botpasswords-help-grants": "각각 부여된 값은 목록에서 사용자 계정을 이미 갖고 있는 사용자 권한에 접근할 수 있는 권한을 줍니다. 자세한 정보는 [[Special:ListGrants|부여 표]]을 보세요.",
-       "botpasswords-label-restrictions": "사용 제한:",
        "botpasswords-label-grants-column": "승인됨",
        "botpasswords-bad-appid": "\"$1\"이라는 봇 이름은 유효하지 않습니다.",
        "botpasswords-insert-failed": "\"$1\" 봇 이름을 추가하는데 실패했습니다. 이미 등록되지 않았는지 확인하기 바랍니다.",
        "unlinkaccounts-success": "계정의 연결이 해제되었습니다.",
        "authenticationdatachange-ignored": "인증 데이터 변경을 처리하지 못했습니다. 제공자를 설정하지 않으셨습니까?",
        "userjsispublic": "주목해 주십시오: 자바스크립트의 하위 문서들은 다른 사용자들이 볼 수 있기 때문에 기밀 데이터를 포함해서는 안 됩니다.",
-       "usercssispublic": "주목해 주십시오: CSS의 하위 문서들은 다른 사용자들이 볼 수 있기 때문에 기밀 데이터를 포함해서는 안 됩니다."
+       "usercssispublic": "주목해 주십시오: CSS의 하위 문서들은 다른 사용자들이 볼 수 있기 때문에 기밀 데이터를 포함해서는 안 됩니다.",
+       "restrictionsfield-badip": "유효하지 않은 IP 주소나 대역: $1",
+       "restrictionsfield-label": "허용된 IP 대역:"
 }
index 08c5a65..a3edcfc 100644 (file)
        "talk": "Diskussioun",
        "views": "Affichagen",
        "toolbox": "Geschirkëscht",
+       "tool-link-userrights": "{{GENDER:$1|Benotzer}}gruppen änneren",
+       "tool-link-emailuser": "{{GENDER:$1|Dëser Benotzerin|Dësem Benotzer}} eng Mail schécken",
        "userpage": "Benotzersäit",
        "projectpage": "Meta-Text",
        "imagepage": "Billersäit kucken",
        "eauthentsent": "Eng Confirmatiouns-E-Mail gouf un déi Adress geschéckt déi Dir uginn hutt.\n\nIer iergendeng E-Mail vun anere Benotzer op dee Kont geschéckt ka ginn, musst Dir als éischt d'Instructiounen an der Confirmatiouns-E-Mail befollegen, fir ze bestätegen datt de Kont wierklech Ären eegenen ass.",
        "throttled-mailpassword": "An {{PLURAL:$1|der leschter Stonn|de leschte(n) $1 Stonnen}} eng E-Mail verschéckt fir d'Passwuert zréckzesetzen.\nFir de Mëssbrauch vun dëser Funktioun ze verhënneren kann nëmmen all {{PLURAL:$1|Stonn|$1 Stonnen}} sou eng Mail verschéckt ginn.",
        "mailerror": "Feeler beim Schécke vun der E-Mail: $1",
-       "acct_creation_throttle_hit": "Visiteure vun dëser Wiki déi Är IP-Adress hu {{PLURAL:$1|schonn $1 Benotzerkont|scho(nn) $1 Benotzerkonten}} an de leschten Deeg opgemaach, dëst ass déi maximal Zuel déi an dësem Zäitraum erlaabt ass.\nDofir kënne Visiteure déi dës IP-Adress benotzen den Ament keng Benotzerkonten opmaachen.",
+       "acct_creation_throttle_hit": "Visiteure vun dëser Wiki déi Är IP-Adress hu {{PLURAL:$1|schonn $1 Benotzerkont|scho(nn) $1 Benotzerkonten}} an de leschten $2 Deeg opgemaach, dëst ass déi maximal Zuel déi an dësem Zäitraum erlaabt ass.\nDofir kënne Visiteure déi dës IP-Adress benotzen den Ament keng Benotzerkonten opmaachen.",
        "emailauthenticated": "Är E-Mail-Adress gouf den $2 ëm $3 Auer bestätegt.",
        "emailnotauthenticated": "Är E-Mail Adress gouf nach net confirméiert.\nDowéinst gëtt fir keng vun dëse Funktiounen E-Maile geschéckt.",
        "noemailprefs": "Gitt eng E-Mailadress bei Ären Astellungen un, fir datt déi Funktioune funktionéieren.",
        "botpasswords-label-cancel": "Ofbriechen",
        "botpasswords-label-delete": "Läschen",
        "botpasswords-label-resetpassword": "D'Passwuert zrécksetzen",
+       "botpasswords-label-grants": "Applikabel Rechter:",
        "botpasswords-help-grants": "All Berechtegung gëtt Zougang op déi Benotzerrechter déi e Benotzerkont schonn huet. Kuckt d'[[Special:ListGrants|Tabell vun de Berechtigunge]] fir méi Informatiounen.",
-       "botpasswords-label-restrictions": "Limite fir d'Benotzen:",
        "botpasswords-label-grants-column": "Accordéiert",
        "botpasswords-bad-appid": "Den Numm vum Bot \"$1\" ass net valabel.",
        "botpasswords-insert-failed": "De Botnumm \"$1\" konnt net dobäigesat ginn. Gouf e schonn derbäigesat?",
+       "botpasswords-update-failed": "Den Numm vum Bot \"$1\" konnt net aktualiséiert ginn. Gouf e geläscht?",
        "botpasswords-created-title": "Botpasswuert ugeluecht",
        "botpasswords-created-body": "D'Botpasswuert fir de Bot-Numm \"$1\" vum Benotzer ''$2'' gouf ugeluecht.",
        "botpasswords-updated-title": "Botpasswuert aktualiséiert",
        "botpasswords-deleted-title": "Botpasswuert geläscht",
        "botpasswords-deleted-body": "D'Botpasswuert fir de Bot-Numm \"$1\" vum Benotzer ''$2'' gouf geläscht.",
        "botpasswords-newpassword": "Dat neit Passwuert fir sech mat <strong>$1</strong> anzeloggen ass <strong>$2</strong>.\n<em>Versuergt dat fir sech spéider dorop ze referéieren.</em><br />(Fir al Botten déi verlaangen datt de Login-Numm d'selwecht ass wéi den spéidere Benotzernumm, kënnt Dir och <strong>$3</strong> als Benotzernumm benotzten a(n) <strong>$4</strong> als Passwuert.)",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider ass net disponibel.",
        "botpasswords-not-exist": "De Benotzer \"$1\" huet kee Botpasswuert mam Numm \"$2\".",
        "resetpass_forbidden": "Passwierder kënnen net geännert ginn.",
        "resetpass_forbidden-reason": "Passwierder kënnen net geännert ginn: $1",
        "content-not-allowed-here": "\"$1\"-Inhalt ass op der Säit [[$2]] net erlaabt",
        "editwarning-warning": "Wann Dir dës Säit verloosst kann dat dozou féieren datt Dir all Ännerungen, déi Dir gemaach hutt, verléiert.\nWann Dir ageloggt sidd, kënnt Dir dës Warnung an der Sektioun \"{{int:prefs-editing}}\" vun Ären Astellungen ausschalten.",
        "editpage-invalidcontentmodel-title": "Modell vum Inhalt gëtt net ënnerstëtzt",
+       "editpage-invalidcontentmodel-text": "Den Inhaltsmodell \"$1\" gëtt net ënnerstëtzt.",
        "editpage-notsupportedcontentformat-title": "Format vum Inhalt gëtt net ënnerstëtzt",
        "editpage-notsupportedcontentformat-text": "De Format vum Inhalt $1 gëtt net vum Modell vum Inhalt $2 ënnerstëtzt.",
        "content-model-wikitext": "Wikitext",
        "content-model-css": "CSS",
        "content-json-empty-object": "Eidelen Objet",
        "content-json-empty-array": "Eidel Tabell",
+       "deprecated-self-close-category": "Säiten déi net valabel 'self-closed' HTML-Tags benotzen",
        "duplicate-args-warning": "<strong>Opgepasst:</strong> [[:$1]] rifft [[:$2]] mat méi wéi engem Wäert fir de Parameter \"$3\" op. Nëmmen de leschte Wäert gëtt benotzt.",
        "duplicate-args-category": "Säiten, déi duebel Argumenter a Schablounenopriff gebrauchen",
        "expensive-parserfunction-warning": "'''Opgepasst:'' Dës Säit huet ze vill Ufroe vu komplexe Parserfunktiounen.\n\nEt däerfen net méi wéi $2 {{PLURAL:$2|Ufro|Ufroe}} sinn, aktuell {{PLURAL:$2|ass et $1 Ufro|sinn et $1 Ufroe}}.",
        "showingresultsinrange": "Hei drënner {{PLURAL:$1|<strong>gëtt 1</strong> Resultat|gi(nn) <strong>$1</strong> Resultater}} aus dem Beräich #<strong>$2</strong> bis #<strong>$3</strong>.",
        "search-showingresults": "{{PLURAL:$4|Resultat <strong>$1</strong> of <strong>$3</strong>|Resultater <strong>$1 - $2</strong> vu(n) <strong>$3</strong>}}",
        "search-nonefound": "Fir Är Ufro gouf näischt fonnt.",
+       "search-nonefound-thiswiki": "Et gouf op dësem Site näischt fonnt wat Ärer Ufro entsprécht.",
        "powersearch-legend": "Erweidert Sich",
        "powersearch-ns": "Sichen an den Nummraim:",
        "powersearch-togglelabel": "Markéieren:",
        "action-viewmyprivateinfo": "Är privat Informatioune kucken",
        "action-editmyprivateinfo": "Är privat Informatiounen änneren",
        "action-editcontentmodel": "de Modell vum Inhalt vun enger Säit änneren",
+       "action-deletechangetags": "Markéierungen aus der Datebank läschen",
        "action-purge": "dës Säit eidelzemaachen",
        "nchanges": "$1 {{PLURAL:$1|Ännerung|Ännerungen}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|zanter dem leschte Passage}}",
        "file-thumbnail-no": "Den Numm vum Fichier fänkt mat <strong>$1</strong> un.\nDa deit drop hin datt et eng Minitaur ''(thumbnail)'' ass.\nWann Dir dat Bild a méi enger grousser Opléisung hutt, da luet dëst erop, wann net dann ännert w.e.g. den Numm vum Fichier.",
        "fileexists-forbidden": "Et gëtt schonn e Fichier mat dësem Numm an dee kann net iwwerschriwwe ginn.\nWann Dir de Fichier nach ëmmer eropluede wëllt, da gitt w.e.g. zréck a benotzt en neien Numm. [[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "E Fichier mat dësem Numm gëtt et schonn an dem gedeelte Repertoire.\nWann Dir dëse Fichier trotzdeem eropluede wëllt da gitt w.e.g. zréck a luet dëse Fichier ënner engem aneren Numm erop. [[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "Den eropgeluedene Fichier ass en exakten Duplikat vun der aktueller Versioun vu(n) <strong>[[:$1]]",
+       "fileexists-duplicate-version": "Den eropgeluedene Fichier ass en exakten Duplikat vun {{PLURAL:$2|enger eelerer Versioun|eelere Versioune}} vu(n) <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Dëse Fichier schéngt een Doublon vun {{PLURAL:$1|dësem Fichier|dëse Fichieren}} ze sinn:",
        "file-deleted-duplicate": "En identesche Fichier ([[:$1]]) gouf virdru geläscht. Kuckt w.e.g. an der Lëscht vum Läschen no, ier Dir en nach emol eropluet.",
        "file-deleted-duplicate-notitle": "En identesche Fichier gouf scho geläscht an den Titel gouf suppriméiert. Dir sollt e froen dee suppriméiert Date vu Fichiere kucken däerf fir d'Situatioun ze klären ier Dir de Fichier nach eng Kéier eroplued.",
        "php-uploaddisabledtext": "D'Eropluede vu Fichieren ass am PHP desaktivéiert. Kuckt w.e.g. d'Astellung ''file_uploads'' no.",
        "uploadscripted": "An dësem Fichier ass HTML- oder Scriptcode, dee vun engem Webbrowser falsch interpretéiert kéint ginn.",
        "upload-scripted-pi-callback": "Et ass net méiglech XML-Fichieren eropzelueden an deenen XML-Stylesheet Instruktioune fir d'Verschaffen drastinn",
+       "uploaded-hostile-svg": "Net sécheren CSS am Stilelement vum eropgeluedene SVG-Fichier fonnt.",
        "uploadscriptednamespace": "An dësem SVG-Fichier ass en illegalen Nummraum \"$1\"",
        "uploadinvalidxml": "Den XML am eropgelueden Fichier konnt net verschafft ginn.",
        "uploadvirus": "An dësem Fichier ass ee Virus! Detailer: $1",
        "changecontentmodel-success-title": "De Modell vum Inhalt gouf geännert",
        "changecontentmodel-success-text": "Den Typ vum Inhalt vu(n) [[:$1]] gouf geännert.",
        "changecontentmodel-cannot-convert": "Den Inhalt vu(n) [[:$1]] kann net op den Typ $2 ëmgewandelt ginn.",
+       "changecontentmodel-nodirectediting": "Den Inhaltsmodell $1 ënnerstëtzt keng direkt Ännerungen",
        "changecontentmodel-emptymodels-title": "Keng Modeller fir Inhalter disponibel",
        "logentry-contentmodel-change-revertlink": "zrécksetzen",
        "logentry-contentmodel-change-revert": "zrécksetzen",
        "logentry-newusers-create2": "De Benotzerkont $3 gouf vum $1 {{GENDER:$2|ugeluecht}}",
        "logentry-newusers-byemail": "De Benotzerkont $3 gouf vum $1 {{GENDER:$2|ugeluecht}} an d'Passwuert gouf per E-Mail geschéckt.",
        "logentry-newusers-autocreate": "De Benotzerkont $1 gouf automatesch {{GENDER:$2|ugeluecht}}",
+       "logentry-protect-unprotect": "$1 huet d'Spär vu(n) $3 {{GENDER:$2|ewechgeholl}}",
        "logentry-protect-protect": "$1 {{GENDER:$2|huet}} d'Säit $3 $4 gespaart",
        "logentry-protect-protect-cascade": "$1 {{GENDER:$2|huet}} d'Säit $3 $4 gespaart [Kaskadespär]",
        "logentry-rights-rights": "$1 {{GENDER:$2|huet}} d'Gruppen zou deenen {{GENDER:$6|d'|de}} $3 gehéiert vu(n) $4 op $5 geännert",
        "api-error-nomodule": "Interne Feeler: de Modul fir d'Eroplueden ass net agestallt.",
        "api-error-ok-but-empty": "Interne Feeler: keng Äntwert vum Server.",
        "api-error-overwrite": "D'Iwwerschreiwe vun engem Fichier ass net erlaabt.",
+       "api-error-ratelimited": "Dir probéiert fir méi ££Fichieren a kuerzer Zäit eropzeluede wéi der op dëser Wiki erlaabt sinn. Probéiert w.e.g. an e puer Minutten nach eng Kéier.",
        "api-error-stashfailed": "Interne Feeler: de Server konnt den temporäre Fichier net späicheren.",
        "api-error-publishfailed": "Interne Feeler: de Server konnt den temporäre Fichier net publizéieren.",
        "api-error-stasherror": "Beim Eropluede vum Fichier ass e Feeler geschitt.",
        "log-action-filter-block-reblock": "Ännere vun enger Spär",
        "log-action-filter-block-unblock": "Spär ophiewen",
        "log-action-filter-delete-delete": "Säite läschen",
+       "log-action-filter-delete-restore": "Säiterestauratioun",
+       "log-action-filter-delete-event": "Logbuch-Läschung",
        "log-action-filter-delete-revision": "Läsche vun enger Versioun",
        "log-action-filter-import-interwiki": "Transwiki-Import",
        "log-action-filter-import-upload": "Import duerch Eropluede vun engem XML",
+       "log-action-filter-managetags-create": "Uleeë vun engem Tag",
+       "log-action-filter-managetags-delete": "Läsche vun engem Tag",
+       "log-action-filter-managetags-activate": "Aktivatioun vun engem Tag",
+       "log-action-filter-managetags-deactivate": "Desaktivatioun vun engem Tag",
+       "log-action-filter-move-move": "Réckelen ouni Iwwerschreiwe vu Viruleedungen",
        "log-action-filter-move-move_redir": "Réckele mat Iwwerschreiwe vu Viruleedungen",
        "log-action-filter-newusers-create": "Ugeluecht vun engem anonyme Benotzer",
        "log-action-filter-newusers-create2": "Ugeluecht vun engem registréierte Benotzer",
        "authmanager-create-from-login": "Fir Äre Benotzerkont unzeleeën fëllt w.e.g. d'Felder hei drënner aus.",
        "authmanager-authplugin-setpass-failed-title": "Änner vum Passwuert huet net funktionéiert",
        "authmanager-authplugin-setpass-bad-domain": "Net valabelen Domain.",
+       "authmanager-autocreate-noperm": "Automatescht Uleeë vu Benotzerkonten ass net erlaabt.",
+       "authmanager-autocreate-exception": "Automatescht Uleeë vu Benotzerkonte gouf op Grond vu fréiere Feeler temporär ausgeschalt.",
        "authmanager-userdoesnotexist": "De Benotzerkont \"$1\" ass net registréiert.",
+       "authmanager-userlogin-remembermypassword-help": "Ob d'Passwuert méi laang verhal gi soll wéi d'Dauer vun der Sessioun.",
+       "authmanager-username-help": "Benotzernumm fir d'Authentifikatioun.",
+       "authmanager-password-help": "Passwuert fir d'Authentifikatioun.",
+       "authmanager-domain-help": "Domain fir extern Authentifikatioun",
        "authmanager-retype-help": "Passwuert nach eng Kéier fir ze konfirméieren",
        "authmanager-email-label": "E-Mail",
        "authmanager-email-help": "E-Mail-Adress",
        "authmanager-realname-label": "Richtegen Numm",
        "authmanager-realname-help": "Richtegen Numm vum Benotzer",
+       "authmanager-provider-password": "Authentifikatioun baséiert um Passwuert",
+       "authmanager-provider-password-domain": "Authentifikatioun baséiert um Passwuert an um Domain",
        "authmanager-provider-temporarypassword": "Temporäert Passwuert:",
+       "authprovider-confirmlink-request-label": "Benotzerkonten déi solle verbonn sinn",
+       "authprovider-confirmlink-success-line": "$1: Verbonn",
+       "authprovider-confirmlink-failed": "Verbanne vum Benotzerkont huet net richteg geklappt: $1",
        "authprovider-resetpass-skip-label": "Iwwersprangen",
        "authprovider-resetpass-skip-help": "D'Zrécksetze vum Passwuert iwwersprangen",
        "authform-notoken": "Toke feelt",
        "cannotlink-no-provider-title": "Et gëtt keng Benotzerkonte fir ze verlinken",
        "linkaccounts": "Benotzerkonte verbannen",
        "linkaccounts-submit": "Benotzerkonte verbannen",
-       "userjsispublic": "DEnkt drun: Op JavaScript-Ënnersäite solle keng vertraulech Informatioune stoe well se vun anere Benotzer kënne gesi ginn."
+       "userjsispublic": "DEnkt drun: Op JavaScript-Ënnersäite solle keng vertraulech Informatioune stoe well se vun anere Benotzer kënne gesi ginn.",
+       "restrictionsfield-badip": "Net valabel IP-Adress oder Beräich: $1",
+       "restrictionsfield-label": "Zougeloossen IP-Beräicher:"
 }
index ce6a28e..90f5c98 100644 (file)
        "botpasswords-label-resetpassword": "Atstatyti slaptažodį",
        "botpasswords-label-grants": "Taikomi leidimai:",
        "botpasswords-help-grants": "Kiekvienas leidimas suteikia prieigą prie išvardintų naudotojo leidimų, kuriuos paskyra jau turi.\nŽiūrėkite [[Special:ListGrants|leidimų lentelę]], norėdami rasti daugiau informacijos.",
-       "botpasswords-label-restrictions": "Naudojimo apribojimai:",
        "botpasswords-label-grants-column": "Leidžiama",
        "botpasswords-bad-appid": "Boto vardas \"$1\" nėra tinkamas.",
        "botpasswords-insert-failed": "Nepavyko pridėti boto vardo \"$1\". Gal jis jau pridėtas?",
        "version-libraries-description": "Aprašymas",
        "version-libraries-authors": "Autoriai",
        "redirect": "Nukreiptas iš failo, naudotojo, versijos arba žurnalo įrašo ID",
-       "redirect-summary": "Šis specialus puslapis peradresuoja į failą (nurodant failo pavadinimą), puslapį (nurodant versijos ID ar puslapio ID), naudotojo puslapį (nurodant skaitinį naudotojo ID), arba žurnalo įrašą (nurodant žurnalo įrašo ID).\nNaudojimas: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]], arba[[{{#Special:Redirect}}/logid/186]].",
+       "redirect-summary": "Šis specialus puslapis peradresuoja į failą (nurodant failo pavadinimą), puslapį (nurodant versijos ID ar puslapio ID), naudotojo puslapį (nurodant skaitinį naudotojo ID), arba žurnalo įrašą (nurodant žurnalo įrašo ID). Naudojimas:\n[[{{#Special:Redirect}}/file/Example.jpg]],\n[[{{#Special:Redirect}}/page/64308]],[[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]], arba\n[[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Eiti",
        "redirect-lookup": "Peržvalgos:",
        "redirect-value": "Vertė:",
index 380e2b3..82bd5ab 100644 (file)
        "botpasswords-label-cancel": "Atcelt",
        "botpasswords-label-delete": "Dzēst",
        "botpasswords-label-resetpassword": "Atiestatīt paroli",
-       "botpasswords-label-restrictions": "Lietošanas ierobežojumi:",
        "botpasswords-label-grants-column": "Piešķirts",
        "botpasswords-created-title": "Bota parole izveidota",
        "botpasswords-updated-title": "Bota parole atjaunināta",
        "subject-preview": "Temata pirmskats:",
        "blockedtitle": "Dalībnieks ir bloķēts.",
        "blockedtext": "'''Tavs lietotāja vārds vai IP adrese ir nobloķēta.'''\n\n$1 nobloķēja tavu lietotāja vārdu vai IP adresi.\nBloķējot norādītais iemesls bija: ''$2''.\n\n*Bloka sākums: $8\n*Bloka beigas: $6\n*Bija domāts nobloķēt: $7\n\nTu vari sazināties ar $1 vai kādu citu [[{{MediaWiki:Grouppage-sysop}}|administratoru]] lai apspriestu šo bloku.\n\nPievērs uzmanību, tam, ka ja tu neesi norādījis derīgu e-pasta adresi ''[[Special:Preferences|savās izvēlēs]]'', tev nedarbosies \"sūtīt e-pastu\" iespēja.\n\nTava IP adrese ir $3 un bloka identifikators ir #$5. Lūdzu iekļauj vienu no tiem, vai abus, visos turpmākajos pieprasījumos.",
-       "autoblockedtext": "Tava IP adrese ir tikusi automātiski nobloķēta, tāpēc, ka to (nupat kā) ir lietojis cits lietotājs, kuru nobloķēja $1.\nNorādītais bloķēšanas iemesls bija:\n\n:''$2''\n\n* Bloka sākums: $8\n* Bloka beigas: $6\n* Bija domāts nobloķēt: $7\n\nTu vari sazināties ar $1 vai kādu citu [[{{MediaWiki:Grouppage-sysop}}|adminu]] lai apspriestu šo bloku.\n\nAtceries, ka tu nevari lietot \"sūtīt e-pastu šim lietotājam\" iespēju, ja tu neesi norādījis derīgu e-pasta adresi savās [[Special:Preferences|lietotāja izvelēs]] un bloķējot tev nav aizbloķēta iespēja sūtīt e-pastu.\n\nTava pašreizējā IP adrese ir $3 un  bloka ID ir $5.\nLūdzu iekļauj šos visos ziņojumos, kurus sūti adminiem, apspriežot šo bloku.",
+       "autoblockedtext": "Tava IP adrese ir tikusi automātiski nobloķēta, tāpēc, ka to (nupat kā) ir lietojis cits dalībnieks, kuru nobloķēja $1.\nNorādītais bloķēšanas iemesls bija:\n\n:''$2''\n\n* Bloka sākums: $8\n* Bloka beigas: $6\n* Bija domāts nobloķēt: $7\n\nTu vari sazināties ar $1 vai kādu citu [[{{MediaWiki:Grouppage-sysop}}|adminu]] lai apspriestu šo bloku.\n\nAtceries, ka tu nevari lietot \"sūtīt e-pastu šim dalībniekam\" iespēju, ja tu neesi norādījis derīgu e-pasta adresi savās [[Special:Preferences|dalībnieka izvelēs]] un bloķējot tev nav aizbloķēta iespēja sūtīt e-pastu.\n\nTava pašreizējā IP adrese ir $3 un  bloka ID ir $5.\nLūdzu iekļauj šos visos ziņojumos, kurus sūti adminiem, apspriežot šo bloku.",
        "blockednoreason": "iemesls nav norādīts",
        "whitelistedittext": "Lūdzu $1, lai varētu labot lapas.",
        "confirmedittext": "Lai varētu izmainīt lapas, vispirms jāapstiprina savu e-pasta adresi.\nNorādi un apstiprini e-pasta adresi savos [[Special:Preferences|lietotāja uzstādījumos]].",
        "trackingcategories-disabled": "Kategorija ir atslēgta",
        "mailnologin": "Nav adreses, uz kuru sūtīt",
        "mailnologintext": "Tev jābūt [[Special:UserLogin|iegājušam]], kā arī tev jābūt [[Special:Preferences|norādītai]] derīgai e-pasta adresei, lai sūtītu e-pastu citiem lietotājiem.",
-       "emailuser": "Sūtīt e-pastu šim lietotājam",
+       "emailuser": "Sūtīt e-pastu šim dalībniekam",
        "emailuser-title-target": "Nosūtīt e-pastu {{GENDER:$1|šim dalībniekam|šai dalībniecei}}",
        "emailuser-title-notarget": "Sūtīt e-pastu lietotājam",
        "emailpagetext": "Ar šo veidni ir iespējams nosūtīt e-pastu šim {{GENDER:$1|lietotājam}}.\nTā e-pasta adrese, kuru tu esi norādījis [[Special:Preferences|savā izvēļu lapā]], parādīsies e-pasta \"From\" lauciņā, tādejādi saņēmējs varēs tev atbildēt.",
        "emailccme": "Atsūtīt man uz e-pastu mana ziņojuma kopiju.",
        "emailsent": "E-pasts nosūtīts",
        "emailsenttext": "Tavs e-pasts ir nosūtīts.",
-       "emailuserfooter": "Šis e-pasts ir lietotāja $1 sūtīts lietotājam $2, izmantojot \"Sūtīt e-pastu šim lietotājam\" funkciju {{SITENAME}}.",
+       "emailuserfooter": "Šis e-pasts ir dalībnieka $1 sūtīts dalībniekam $2, izmantojot \"Sūtīt e-pastu šim dalībniekam\" funkciju {{SITENAME}}.",
        "usermessage-summary": "Atstāt sistēmas ziņojumu.",
        "usermessage-editor": "Sistēmas ziņotājs",
        "watchlist": "Mani uzraugāmie raksti",
        "sp-contributions-blocked-notice": "Šis lietotājs pašlaik ir nobloķēts.\nPēdējais bloķēšanas reģistra ieraksts ir apskatāms zemāk:",
        "sp-contributions-blocked-notice-anon": "Šī IP adrese pašlaik ir nobloķēta.\nPēdējais bloķēšanas reģistra ieraksts ir apskatāms zemāk:",
        "sp-contributions-search": "Meklēt lietotāju veiktās izmaiņas",
-       "sp-contributions-username": "IP adrese vai lietotāja vārds:",
+       "sp-contributions-username": "IP adrese vai dalībnieka vārds:",
        "sp-contributions-toponly": "Rādīt tikai labojumus, kuri ir jaunākās versijas",
        "sp-contributions-submit": "Meklēt",
        "whatlinkshere": "Norādes uz šo rakstu",
        "emailblock": "e-pasts bloķēts",
        "blocklist-nousertalk": "nevar izmainīt savu diskusiju lapu",
        "ipblocklist-empty": "Bloķēšanas saraksts ir tukšs.",
-       "ipblocklist-no-results": "Norādītā IP adrese vai lietotājs nav bloķēts.",
+       "ipblocklist-no-results": "Norādītā IP adrese vai dalībnieks nav bloķēts.",
        "blocklink": "bloķēt",
        "unblocklink": "atbloķēt",
        "change-blocklink": "izmainīt bloku",
index 8f29932..0d7f4fb 100644 (file)
        "tog-editsectiononrightclick": "अनुभाग शीर्षक पर दाहिन क्लिक करै पर अनुभाग सम्पादित करी",
        "tog-watchcreations": "हमर बनाओल पृष्ठ हमर साकांक्ष सूचीमे राखी",
        "tog-watchdefault": "हमर सम्पादित पृष्ठ हमर साकांक्ष सूचीमे देखाबी",
-       "tog-watchmoves": "हमरादà¥\8dवारा à¤\98सà¥\8dà¤\95ाà¤\93ल पृष्ठ हमर साकांक्ष सूचीमे राखी",
-       "tog-watchdeletion": "हमरादà¥\8dवारा à¤®à¥\87à¤\9fाà¤\93ल पृष्ठ हमर साकांक्ष सूचीमे राखी",
-       "tog-watchrollback": "हमरादà¥\8dवारा à¤°à¥\8bलबà¥\8dयाà¤\95 कएल पृष्ठ हमर सांकक्ष सूचीमे राखी",
-       "tog-minordefault": "हमर सभ सम्पादन पूर्वन्यस्त रूपमे मामूली कही",
-       "tog-previewontop": "समà¥\8dपादन à¤ªà¥\87à¤\9fà¥\80à¤\95 à¤\8aपर à¤¦à¥\83शà¥\8dय देखाबी",
+       "tog-watchmoves": "हमरादà¥\8dवारा à¤¸à¥\8dथानानà¥\8dतरित पृष्ठ हमर साकांक्ष सूचीमे राखी",
+       "tog-watchdeletion": "हमरादà¥\8dवारा à¤®à¥\87à¤\9fाà¤\8fल पृष्ठ हमर साकांक्ष सूचीमे राखी",
+       "tog-watchrollback": "हमरादà¥\8dवारा à¤ªà¥\82रà¥\8dववत कएल पृष्ठ हमर सांकक्ष सूचीमे राखी",
+       "tog-minordefault": "हमर सभ सम्पादनसभ छोट परिवर्तनक रूपमे चिह्नित करी",
+       "tog-previewontop": "समà¥\8dपादन à¤¸à¤¨à¥\8dदà¥\82à¤\95 à¤¸à¤\81 à¤ªà¤¹à¤¿à¤¨à¥\87 à¤\9dलà¤\95 देखाबी",
        "tog-previewonfirst": "पहिल सम्पादनक बाद पूर्वावलोकन देखाबी",
        "tog-enotifwatchlistpages": "जौं हमर ध्यानसूचीक कोनो पन्नामे परिवर्तन हुअए तँ हमरा इमेल पठाबी",
        "tog-enotifusertalkpages": "हमर वार्ता पृष्ठ परिवर्तित भेला पर हमरा इमेल करी",
        "tog-watchlisthideminor": "हमर साकांक्ष सूचीसँ मामूली सम्पादन नुकाबी",
        "tog-watchlisthideliu": "साकांक्षसूचीसँ सम्प्रवेशित प्रयोक्ताक सम्पादन हटाबी",
        "tog-watchlisthideanons": "साकांक्षसूचीसँ अनाम प्रयोक्ताक सम्पादन हटाबी",
-       "tog-watchlisthidepatrolled": "साकांक्ष सूचीसँ संचालित सम्पादन नुकाबी",
+       "tog-watchlisthidepatrolled": "साकांक्ष सूचीसँ परीक्षित सम्पादन नुकाबी",
+       "tog-watchlisthidecategorization": "पृष्ठसभक श्रेणीकरण नुकाबी",
        "tog-ccmeonemails": "हमरद्वारा दोसर प्रयोक्ताक पठाओल ई-पत्रक कपी पठाबी",
        "tog-diffonly": "फाइल-अन्तर प्रणालीक नीचाँ पन्नाक सामिग्री नै देखाबी",
        "tog-showhiddencats": "नुकाएल श्रेणी देखाबी",
-       "tog-norollbackdiff": "समà¥\8dपादन à¤µà¤¾à¤ªà¤¸ à¤² à¤²à¥\87ला बाद अन्तर नै देखाबी",
-       "tog-useeditwarning": "à¤\9cब à¤¹à¤® à¤\95à¥\8bनà¥\8b à¤¸à¤®à¥\8dपादन à¤ªà¥\83षà¥\8dठà¤\95à¥\87 à¤¬à¤¿à¤¨à¤¾ à¤¸à¥\81रà¤\95à¥\8dषित à¤\95à¥\87नà¥\88 à¤¬à¤¦à¤²à¤¾à¤µ à¤¸à¤\82à¤\97 à¤\9bà¥\8bडि à¤¦à¤¿ à¤¤ à¤¹à¤®à¤°à¤¾ à¤¸à¥\82à¤\9aित à¤\95रà¥\80 ।",
-       "tog-prefershttps": "समà¥\8dपà¥\8dरवà¥\87शित à¤\95रलाà¤\95 à¤¬à¤¾à¤¦ à¤¸à¤¦à¥\88व à¤¸à¥\81रà¤\95à¥\8dषित à¤\95नà¥\87à¤\95à¥\8dशनà¤\95à¥\87 प्रयोग करी",
+       "tog-norollbackdiff": "समà¥\8dपादन à¤µà¤¾à¤ªà¤¸ à¤\95रलाà¤\95 बाद अन्तर नै देखाबी",
+       "tog-useeditwarning": "à¤\9cब à¤¹à¤® à¤\95à¥\8bनà¥\8b à¤¸à¤®à¥\8dपादन à¤ªà¥\83षà¥\8dठà¤\95à¥\87 à¤¬à¤¿à¤¨à¤¾ à¤¸à¥\81रà¤\95à¥\8dषित à¤\95à¥\87नà¥\88 à¤¬à¤¦à¤²à¤¾à¤µ à¤¸à¤\82à¤\97 à¤\9bà¥\8bडि à¤¦à¥\80 à¤¤à¤\81 à¤¹à¤®à¤°à¤¾ à¤¸à¥\82à¤\9aित à¤\95रà¥\80।",
+       "tog-prefershttps": "समà¥\8dपà¥\8dरवà¥\87शित à¤\95रलाà¤\95 à¤¬à¤¾à¤¦ à¤¸à¤¦à¥\88व à¤¸à¥\81रà¤\95à¥\8dषित à¤\95नà¥\87à¤\95à¥\8dसनà¤\95 प्रयोग करी",
        "underline-always": "सदिखन",
        "underline-never": "कखनो नै",
        "underline-default": "पूर्वन्यस्त गवेषक",
        "period-am": "पूर्वाह्न",
        "period-pm": "अपराह्न",
        "pagecategories": "{{PLURAL:$1|श्रेणी|श्रेणीसभ}}",
-       "category_header": "श्रेणी \"$1\" मे पन्ना सभ",
+       "category_header": "\"$1\" श्रेणीमे पृष्ठसभ",
        "subcategories": "उपश्रेणी",
        "category-media-header": "श्रेणी \"$1\" मे मिडिया",
        "category-empty": "<em>ई श्रेणीमे ई समय कोनो पृष्ठ या मिडिया नै अछि।</em>",
        "category-file-count": "{{PLURAL:$2|ई श्रेणीमे मात्र निम्नलिखित फाइल अछि।|ई श्रेणीमे निम्नलिखित {{PLURAL:$1|फाइल|$1 फाइलसभ}} अछि, कुल फाइलसभ $2}}",
        "category-file-count-limited": "ई श्रेणीमे निम्नलिखित {{PLURAL:$1|फाइल अछि।|फाइलसभ अछि।}}",
        "listingcontinuesabbrev": "शेष आगाँ।",
-       "index-category": "à¤\95à¥\8dरम à¤\95à¤\8fल à¤ªà¤¨à¥\8dनासभ",
-       "noindex-category": "à¤\95à¥\8dरम à¤¨à¥\88 à¤\95à¤\8fल à¤ªà¤¨à¥\8dनासभ",
-       "broken-file-category": "पनà¥\8dनासभ à¤\9cाà¤\87मे फाइल लिङ्कसभ टूटल हुअए",
+       "index-category": "सà¥\82à¤\9aà¥\80बदà¥\8dध à¤ªà¥\83षà¥\8dठ",
+       "noindex-category": "à¤\95à¥\8dरम à¤¨à¥\88 à¤\95à¥\87ल à¤ªà¥\83षà¥\8dठ",
+       "broken-file-category": "पनà¥\8dनासभ à¤\9cाहिमे फाइल लिङ्कसभ टूटल हुअए",
        "about": "क विषयमे",
        "article": "सामग्री लेख",
        "newwindow": "(नव विन्डोमे खुजत)",
        "cancel": "रद्द करी",
        "moredotdotdot": "आर...",
-       "morenotlisted": "à¤\88 à¤ªà¥\81रा सूची नै छी।",
+       "morenotlisted": "à¤\88 à¤ªà¥\82रà¥\8dण सूची नै छी।",
        "mypage": "पन्ना",
        "mytalk": "वार्ता",
        "anontalk": "वार्ता",
        "navigation": "सञ्चार",
        "and": "&#32;आर",
        "qbfind": "ताकी",
-       "qbbrowse": "à¤\97वà¥\87षण करी",
+       "qbbrowse": "बà¥\8dराà¤\89à¤\9c करी",
        "qbedit": "सम्पादन करी",
        "qbpageoptions": "ई पृष्ठ",
        "qbmyoptions": "हमर पृष्ठसभ",
        "faq": "त्वरित प्रश्नोत्तरी",
        "faqpage": "Project: त्वरित प्रश्नोत्तरी",
        "actions": "क्रियासभ",
-       "namespaces": "à¤\9aà¥\87नà¥\8dहासà¥\80 समूहसभ",
-       "variants": "पà¥\8dरà¤\95ारसभ",
+       "namespaces": "नामसà¥\8dथान समूहसभ",
+       "variants": "सà¤\82सà¥\8dà¤\95रण",
        "navigation-heading": "दिक्चालन सूची",
        "errorpagetitle": "त्रुटि",
        "returnto": "$1 पर आबी।",
        "searcharticle": "जाए",
        "history": "पृष्ठ इतिहास",
        "history_short": "इतिहास",
-       "updatedmarker": "हमर à¤\85नà¥\8dतिम à¤\86à¤\97मनसà¤\81 à¤ªà¤¹à¤¿à¤¨à¥\87 à¤\85दà¥\8dयतन à¤\95à¤\8fल",
+       "updatedmarker": "हमर à¤\85नà¥\8dतिम à¤\86à¤\97मनसà¤\81 à¤ªà¤¹à¤¿à¤¨à¥\87 à¤\85दà¥\8dयतन à¤\95à¥\87ल",
        "printableversion": "प्रिन्ट करबा योग्य",
        "permalink": "स्थायी लिङ्क",
        "print": "छापी",
        "talk": "वार्तालाप",
        "views": "दर्शाव",
        "toolbox": "उपकरण",
+       "tool-link-userrights": "{{GENDER:$1|सदस्य}} समूह परिवर्तन करी",
+       "tool-link-emailuser": "ई {{GENDER:$1|प्रयोक्ता}}के इमेल भेजी",
        "userpage": "प्रयोक्ता पन्ना देखी",
        "projectpage": "परियोजना पन्ना देखी",
        "imagepage": "फाइल पृष्ठ देखी",
        "redirectedfrom": "($1सँ पुनर्निर्देशित)",
        "redirectpagesub": "पृष्ठ पुनर्निर्देशित करी",
        "redirectto": "कऽ अनुप्रेषित:",
-       "lastmodifiedat": "à¤\88 à¤ªà¥\83षà¥\8dठà¤\95 à¤ªà¤¹à¤¿à¤¨à¥\81à¤\95ा à¤¬à¤¦à¤²à¤¾à¤µ $1 के $2 बजे भेल छल।",
+       "lastmodifiedat": "à¤\88 à¤ªà¥\83षà¥\8dठà¤\95 à¤¨à¤µà¥\80नतम à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन $1 के $2 बजे भेल छल।",
        "viewcount": "ई पृष्ठ {{PLURAL:$1|एक|$1}} बेर देखल गेल छल।",
        "protectedpage": "सुरक्षित पृष्ठ",
        "jumpto": "एतय जाए:",
        "privacy": "गोपनीयताक नियम",
        "privacypage": "Project:गोपनीयता नियम",
        "badaccess": "आज्ञा गल्ती",
-       "badaccess-group0": "à¤\85हाà¤\81à¤\95 à¤\86à¤\97à¥\8dरह à¤\95à¤\8fल क्रियाक करबाक अनुमति नै अछि।",
-       "badaccess-groups": "à¤\85हाà¤\81 à¤\9cà¥\87 à¤\95à¥\8dरिया à¤\86à¤\9cमà¥\87नà¥\87 à¤\9bà¥\80 à¤\93 à¤®à¤¾à¤¤à¥\8dर {{PLURAL:$2|$1 à¤¸à¤®à¥\82ह|$1 à¤¸à¤®à¥\82हसभ}}à¤\95 à¤¸à¤¦à¤¸à¥\8dय à¤¹à¥\80 à¤\95रि à¤¸à¤\95à¤\8fत अछि।",
+       "badaccess-group0": "à¤\85हाà¤\81à¤\95 à¤\86à¤\97à¥\8dरह à¤\95à¥\87ल क्रियाक करबाक अनुमति नै अछि।",
+       "badaccess-groups": "à¤\85हाà¤\81 à¤\9cà¥\87 à¤\95à¥\8dरिया à¤\86à¤\9cमà¥\87नà¥\87 à¤\9bà¥\80 à¤\93 à¤®à¤¾à¤¤à¥\8dर {{PLURAL:$2|$1 à¤¸à¤®à¥\82ह|$1 à¤¸à¤®à¥\82हसभ}}à¤\95 à¤¸à¤¦à¤¸à¥\8dय à¤®à¤¾à¤¤à¥\8dर à¤\95रि à¤¸à¤\95à¥\88त अछि।",
        "versionrequired": "मिडियाविकिक संस्करण $1 चाही",
        "versionrequiredtext": "ई पृष्ठ प्रयोग करैक लेल मिडियाविकिक $1 अवतरण जरुरी अछि।\nदेखी [[Special:Version|अवतरण पृष्ठ]]।",
        "ok": "ठीक अछि",
        "databaseerror-query": "अनुरोध: $1",
        "databaseerror-function": "फङ्क्सन: $1",
        "databaseerror-error": "त्रुटि: $1",
-       "laggedslavemode": "'''चेतौनी:''' पन्नापर सम्भव जे अद्यतन परिवर्तन नै हुअए।",
-       "readonly": "दतà¥\8dतनिधि प्रतिबन्धित",
-       "enterlockreason": "पà¥\8dरतिबनà¥\8dध à¤²à¥\87ल à¤\95ारण à¤¬à¤¤à¤¾à¤¬à¥\80, à¤¸à¤\82à¤\97à¥\87 à¤\8fà¤\95à¤\9fा à¤\85नà¥\8dदाà¤\9c à¤¸à¥\87हà¥\8b à¤¬à¤¤à¤¾à¤¬à¥\80 à¤\9cà¥\87 à¤\95à¤\96न à¤\88 à¤ªà¥\8dरतिबनà¥\8dध à¤¹à¤\9fाà¤\8fल à¤\9cाà¤\8fत।",
+       "laggedslavemode": "<strong>चेतौनी:</strong> पन्नापर सम्भव जे अद्यतन परिवर्तन नै हुअए।",
+       "readonly": "डà¥\87à¤\9fाबà¥\87स प्रतिबन्धित",
+       "enterlockreason": "पà¥\8dरतिबनà¥\8dध à¤²à¥\87ल à¤\95ारण à¤¬à¤¤à¤¾à¤¬à¥\80, à¤¸à¤\82à¤\97à¥\87 à¤\8fà¤\95à¤\9fा à¤\85नà¥\8dदाà¤\9c à¤¸à¥\87हà¥\8b à¤¬à¤¤à¤¾à¤¬à¥\80 à¤\9cà¥\87 à¤\95à¤\96न à¤\88 à¤ªà¥\8dरतिबनà¥\8dध à¤¹à¤\9fाà¤\8fल à¤\9cाà¤\87त।",
        "readonlytext": "अखन दत्तांशनिधि नव प्रविष्टि आ आन संशोधन लेल प्रतिबन्धित अछि, सम्भवतः सामान्त दत्तांशनिधि देखभाल लेल, तकर बाद ई सामान्य भऽ जाएत।\n\nसञ्चालक जे एकरा प्रतिबन्धित कएने छथि ई कारण दै छथि:$1",
        "missing-article": "दत्तनिधि पृष्ठक वान्छित पाठ्य नै ताकि सकल, माने \"$1\" $2\nएकर कारण कोनो पुरान फाइल चेन्हासी वा ऐतिहासिक लिङ्कक पाछाँ जाएब अछि, जे मेटा देल गेल छै।\nजौं ई तकर कारण नै अछि, तखन अहाँ तन्त्रांशमे कोनो दोष भेटल अछि।\nएकर खबरि पहुँचाबी [[Special:ListUsers/sysop|प्रबन्धक]]केँ, अपन सार्वत्रिक विभव सङ्केत सूचित करैत।",
        "missingarticle-rev": "(संशोधन#: $1)",
        "virus-badscanner": "खराप विन्यास: अज्ञात विषविधि बिम्बक: <em>$1</em>",
        "virus-scanfailed": "बिम्ब विफल (विध्यादेश $1)",
        "virus-unknownscanner": "अज्ञात विषविधि निरोधक",
-       "logouttext": "'''अहाँ निष्क्रमण कऽ गेल छी।'''\n\nअहाँ {{अन्तर्जाल}} प्रयोग अनाम भऽ कऽ सकै छी, वा अहाँ <span class='plainlinks'>[$1 log in again]</span> वएह आकि कोनो आन प्रयोक्ताक रूपमे सेहू प्रयोक कऽ सकै छी।\nई मोन राखू जे किछु पन्ना एना देखा पड़ि सकैए जेना अहाँ अखनो सम्प्रवेशित होइ, जावत अहाँ अपन गवेषकक उपस्मृति मेटा नै दै छी।",
+       "logouttext": "<strong>आब अहाँ निष्क्रमण कऽ गेल छी।</strong>\n\nध्यान दी कि जाबे धरि अहाँ अपन ब्राउजर क्यास खाली नै करब, किछ पृष्ठपर अखनो अहाँ सम्प्रवेशित देखाएब।",
        "cannotlogoutnow-title": "अखन प्रस्थान नै भऽ रहल अछि",
        "cannotlogoutnow-text": "$1 क उपयोग समय प्रस्थान नै कएल जा सकएत अछि।",
        "welcomeuser": "अहाँक स्वागत अछि, $1!",
        "createacct-yourpasswordagain-ph": "कुटशब्द पुनः लिखी",
        "userlogin-remembermypassword": "हमरा सम्प्रवेशित राखी",
        "userlogin-signwithsecure": "सुरक्षित कनेक्शनक प्रयोग करी",
+       "cannotlogin-title": "अखन प्रवेश नै भऽ रहल अछि",
+       "cannotlogin-text": "प्रवेश असम्भव अछि",
        "cannotloginnow-title": "अखन प्रवेश नै भऽ रहल अछि",
        "cannotloginnow-text": "$1 क उपयोग समय प्रवेश नै कएल जा सकएत अछि।",
+       "cannotcreateaccount-title": "खाता नै बनि सकत",
+       "cannotcreateaccount-text": "प्रत्यक्ष खाता निर्माण एहि विकिपर सक्षम नै अछि।",
        "yourdomainname": "अहाँक डोमेन (प्रभावक्षेत्र):",
        "password-change-forbidden": "अहाँ ई विकिमे कुटशब्द नै बदल सकैत छी।",
        "externaldberror": "या त प्रमाणिकरण डेटाबेसमे त्रुटि भएल अछि या फेर अहाँक अपन बाह्य खाता अपडेट करैक अनुमति नै अछि।",
        "login": "सम्प्रवेश",
+       "login-security": "अपन पहचान सत्यापित करी",
        "nav-login-createaccount": "सम्प्रवेश / खाता खोली",
        "userlogin": "सम्प्रवेश/ खाता बनाबी",
        "userloginnocreate": "सम्प्रवेश",
        "createacct-email-ph": "अपन ई-मेल पता लिखी",
        "createacct-another-email-ph": "ईमेल पता प्रदान करी",
        "createaccountmail": "एक अस्थायी यादृच्छिक कूटशब्द चुनी आ ओ निर्दिष्ट ई-मेल पता पर भेजी",
+       "createaccountmail-help": "एकर उपयोग बिना पासवर्ड जानने कियो आन व्यक्तिके खाता खोलैक लिए उपयोग कएल जा सकैत अछि ।",
        "createacct-realname": "असली नाम (वैकल्पिक)",
        "createaccountreason": "कारण:",
        "createacct-reason": "कारण:",
-       "createacct-reason-ph": "अहा इगो आर दोसर खाता कियाक बनउने जा रहल छि",
+       "createacct-reason-ph": "अहाँ एक अन्य खाता कियाक बनाए रहल छी",
+       "createacct-reason-help": "खाता निर्माण लगमे ई सन्देस देखाएल जाइत।",
        "createacct-submit": "अपन खाता बनाबी",
        "createacct-another-submit": "खाता बनाबी",
-       "createacct-benefit-heading": "{{SITENAME}} अहाँ जोका लोगसभद्वारा बनाएल गेल अछि।",
+       "createacct-continue-submit": "खाता निर्माण जारी राखी",
+       "createacct-another-continue-submit": "खाता निर्माण जारी राखी",
+       "createacct-benefit-heading": "{{SITENAME}} अहाँ जका लोकसभद्वारा बनाएल गेल अछि।",
        "createacct-benefit-body1": "$1 {{PLURAL:$1|सम्पादन|सम्पादनसभ}}",
        "createacct-benefit-body2": "{{PLURAL:$1|पन्ना|पन्नासभ}}",
        "createacct-benefit-body3": "सन्निकट {{PLURAL:$1|योगदानकर्ता|योगदानकर्तासभ}}",
        "nocookieslogin": "{{SITENAME}} प्रयोक्ताक सम्प्रवेशित करबा लेल ज्ञापकक प्रयोग करैत अछि।\nअहाँ ज्ञापकक अशक्त केने छी।\nकृपा कऽ ओकरा सक्रिय करी आ फेरसँ प्रयास करी।",
        "nocookiesfornew": "प्रयोक्ता खाजा नै खुजल, कारण हम ओकर जडि पूर्ण रूपेँ नै ताकि सकलौ।\nई दृढ करी जे ज्ञापक सक्रिय अछि, ई पन्नाक फेरसँ भारित करी आ फेरसँ प्रयास करी।",
        "noname": "अहाँ वैध प्रयोक्तानाम नै देने छी।",
-       "loginsuccesstitle": "समà¥\8dपà¥\8dरवà¥\87श à¤­à¤\8fल",
+       "loginsuccesstitle": "समà¥\8dपà¥\8dरवà¥\87श à¤­à¥\87ल",
        "loginsuccess": "<strong>अहाँ सम्प्रवेश केलहुँ {{SITENAME}} \"$1\"'''क रूपमे। </strong>",
        "nosuchuser": "\"$1\" नामसँ कोनो प्रयोक्ता नै अछि।\nप्रयोक्तानाम ब्रह्मक्षर-लघ्वक्षर भेद युक्त अछि।\nअपन ह्रिजै जाँची, वा [[Special:CreateAccount|नव खाता बनाबी]] ।",
        "nosuchusershort": "\"$1\" नामक कोनो प्रयोक्ता नै अछि।\nअपन हिजए सुधारी।",
        "eauthentsent": "एकटा पावती ई-पत्र निर्धारित ई-पत्र संकेतपर पठा देल गेल अछि।\nऐ खातापर कोनो दोसर ई-पत्र पठाएल जएबासँ पहिने, अहाँकेँ ऐ ई-पत्रक निर्देशक पालन करए पड़त, जइसँ ई पुष्ट भऽ सकए जे ई खाता वास्तवमे अहींक अछि।",
        "throttled-mailpassword": "एकटा कूटशब्द स्मारक पहिनहिये पठाएल गेल अछि, {{PLURAL:$1|घण्टा|$1 घण्टा}}क भीतर।\nदुरुपयोग रोकबा लेल, मात्र एकटा कूटशब्द {{PLURAL:$1|घण्टा|$1 घण्टा}}मे पठाएल जाएत।",
        "mailerror": "ई-पत्र पठेबामे त्रुटी: $1",
-       "acct_creation_throttle_hit": "अहाँके आइ॰पि. पतासँ आएल आगंतुक चौबीस घण्टा सँ बैसी ई विकिमे {{PLURAL:$1|एक खाता|$1 खाता}} बनौलक अछि, इ समयावधिमे ई अधिकतम सिमा छी। अतः अखन ई आइ॰पि. पताके प्रयोग करए वाला आगंतुक आर कोनो खाता नै खोइल सकएत अछि ।",
+       "acct_creation_throttle_hit": "अहाँक आइपी ठेगान सँ आएल आगन्तुक $2 सँ बैसी ई विकिमे {{PLURAL:$1|एक खाता|$1 खाता}} बनौलक अछि, इ समयावधिमे ई अधिकतम सिमा छी। अतः अखन ई आइपी पताके प्रयोग करैवाला आगन्तुक आर कोनो खाता नै खोइल सकैत अछि।",
        "emailauthenticated": "अहाँक ई-पत्र सङ्केत $2 क $3 बजे सत्यापित भेल।",
        "emailnotauthenticated": "अहाँक ई-पत्र सङ्केत अखन धरि सत्यापित नै भेल अछि।\nनिचा देल गेल कोनो सुविधाक लेल अहाँक ई-पत्र नै भेजल जाएत।",
        "noemailprefs": "ई सुविधा सभ कऽ प्रयोग करएक लेल अपन विकल्पमे ई-पत्र पता राखी।",
        "cannotchangeemail": "खाता ई-पत्र सङ्केत ऐ विकिपर बदलल नै जा सकैए।",
        "emaildisabled": "ई अन्तर्जाल ई-पत्र नै पठाएत।",
        "accountcreated": "खाता खुजि गेल",
-       "accountcreatedtext": "[[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|वारà¥\8dता]]) à¤\95à¥\87 à¤²à¥\87ल à¤\96ाता à¤\96à¥\8bलल à¤\97ेल अछि।",
+       "accountcreatedtext": "[[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|वारà¥\8dता]]) à¤\95à¥\87 à¤²à¥\87ल à¤\96ाता à¤¨à¤¿à¤°à¥\8dमाण à¤­ेल अछि।",
        "createaccount-title": "{{SITENAME}}क लेल खाता बनाबी",
        "createaccount-text": "कियो अहाँक ई-पत्र सङ्केत लेल एकटा खाता {{SITENAME}} पर खोललन्हि ($4) नाम भेल \"$2\", कूटशब्द भेल \"$3\"।\nअहाँ सम्प्रवेश करी आ अपन कूटशब्द बदली।\n\nअहाँ ई सन्देशके बिसरि सकै छी, जँ ई खाता भ्रमवश बनल हुअए।",
        "login-throttled": "अहाँ ढ़ेर रास सम्प्रवेश प्रयास केलहुँ।\nफेर प्रयास करबासँ पहिने कने काल थम्हू।",
-       "login-abort-generic": "à¤\85हाà¤\81à¤\95 à¤¸à¤®à¥\8dपà¥\8dरवà¥\87श à¤¸à¤«à¤² à¤¨à¥\88 à¤­à¥\87ल- à¤°à¥\8bà¤\95ल à¤\97à¤\8fल",
-       "login-migrated-generic": "à¤\85हाà¤\81à¤\95à¥\87 à¤\96ाता à¤®à¤¾à¤\87à¤\97à¥\8dरà¥\87à¤\9f à¤\95à¤\8fल गेल अछि, आर अहाँके प्रयोक्ता नाम आब ई विकिमे नै अछि।",
-       "loginlanguagelabel": "भाषा : $1",
-       "suspicious-userlogout": "à¤\85हाà¤\81à¤\95 à¤¨à¤¿à¤·à¥\8dà¤\95à¥\8dरमणà¤\95 à¤\85नà¥\81रà¥\8bध à¤¨à¥\88 à¤®à¤¾à¤¨à¤² à¤\97à¥\87ल à¤\95ारण à¤\88 à¤²à¤¾à¤\97ल à¤\9cà¥\87 à¤\88 à¤ªà¥\81रान à¤\97वà¥\87षà¤\95à¤\95 à¤²à¤¾à¤\97ि à¤µà¤¾ à¤¦à¥\8bसराà¤\87त à¤\89पसà¥\8dमà¥\83तद्वारा पठाओल गेल छल।",
+       "login-abort-generic": "à¤\85हाà¤\81à¤\95 à¤¸à¤®à¥\8dपà¥\8dरवà¥\87श à¤¸à¤«à¤² à¤¨à¥\88 à¤­à¥\87ल- à¤°à¥\8bà¤\95ल à¤\97à¥\87ल",
+       "login-migrated-generic": "à¤\85हाà¤\81à¤\95à¥\87 à¤\96ाता à¤®à¤¾à¤\87à¤\97à¥\8dरà¥\87à¤\9f à¤\95à¥\87ल गेल अछि, आर अहाँके प्रयोक्ता नाम आब ई विकिमे नै अछि।",
+       "loginlanguagelabel": "भाषा: $1",
+       "suspicious-userlogout": "à¤\85हाà¤\81à¤\95 à¤¨à¤¿à¤·à¥\8dà¤\95à¥\8dरमणà¤\95 à¤\85नà¥\81रà¥\8bध à¤¨à¥\88 à¤®à¤¾à¤¨à¤² à¤\97à¥\87ल à¤\95ारण à¤\88 à¤²à¤¾à¤\97ल à¤\9cà¥\87 à¤\88 à¤ªà¥\81रान à¤¬à¥\8dराà¤\89à¤\9cर à¤¤à¥\81à¤\9fल à¤µà¤¾ à¤\95à¥\8dयाà¤\9aिà¤\99 à¤ªà¥\8dरà¤\95à¥\8dसà¥\80द्वारा पठाओल गेल छल।",
        "createacct-another-realname-tip": "मूल नाम वैकल्पिक अछि।\nजँ अहाँ एकरा देबा लेल प्रयोग करै छी, ई अहाँक काजक श्रेय देबा लेल एकर प्रयोग कएल जाएत।",
        "pt-login": "सम्प्रवेश",
        "pt-login-button": "सम्प्रवेश",
+       "pt-login-continue-button": "प्रवेश जारी राखी",
        "pt-createaccount": "खाता खोलल जाए",
        "pt-userlogout": "निष्क्रमण",
        "php-mail-error-unknown": "पी.एच.पी.कऽ समाद कार्य() मे अज्ञात दोष भेल।",
        "passwordreset-emailtext-user": "प्रयोक्ता $1 {{अन्तर्जाल}} पर अहाँक खाता विवरणक {{SITENAME}} लेल फेरसँ ($4) आग्रह केने छथि। ई प्रयोक्ता {{PLURAL:$3|खाता अछि|खाता सभ अछि}} ऐ ई-पत्र संकेतसँ जुड़ल: $2\n{{PLURAL:$3| ई अस्थायी कूटशब्द|ई सभ अस्थायी कूटशब्द}} खतम भऽ जाएत {{PLURAL:$5|एक दिन|$5 दिन}} मे।\nअहाँ सम्प्रवेश करू आ एकटा नव कूटशब्द आब चुनू। जँ कियो दोसर ई आग्रह केने छथि, वा जँ अहाँकेँ अपन मूल कूटशब्द मोन पड़ि गेल अछि, आ अहाँ आब ओइ कूटशब्दकेँ नै बदलऽ चाहै छी, अहाँ ऐ संदेशकेँ बिसरि सकै छी आ अपन पुरान कूटशब्दक प्रयोग जारी राखि सकै छी।",
        "passwordreset-emailelement": "प्रयोक्ता: \n$1\n\nअस्थायी कूटशब्द: \n$2",
        "passwordreset-emailsentemail": "एकटा ई-पत्र मोन पाड़बा लेल पठाओल गेल अछि।",
+       "passwordreset-invalideamil": "अवैध इमेल ठेगान",
+       "passwordreset-nodata": "प्रयोगकर्ता नाम वा इमेल ठेगान नै देल गेल छल",
        "changeemail": "ई-मेल पता परिवर्तित करी",
        "changeemail-header": "अपन ईमेल पता परिवर्तन हेतु एकरा पुरा करी। यदि अहाँ अपन वर्तमान ईमेल पता हटाबैलेल चाहैत छी, तँ एकरा खाली छोडि दी आ एकरा भेजी।",
        "changeemail-no-info": "अहाँक ई पन्नाक सोझे प्रयोग करबालेल सम्प्रवेशित हुअए पडत।",
        "changeemail-oldemail": "वर्तमान ई-मेल पता:",
        "changeemail-newemail": "नव ई-मेल पता:",
+       "changeemail-newemail-help": "यदि अहाँ अपन इमेल ठेगान रिक्त राखैलेल चाहए छी तँ अहाँ ई स्थान खाली छोडि सकैत छी। मुदा अहाँ अपन पासवर्ड बिसरि गेलापर ओकरा इमेलद्वारा प्राप्त नै करि पेबै।",
        "changeemail-none": "(कोनो नै)",
        "changeemail-password": "अहाँक {{SITENAME}} कूटशब्द:",
        "changeemail-submit": "ई-मेल बदली",
        "changeemail-throttled": "अहाँ ढेर रास सम्प्रवेश प्रयास केलहुँ।\nफेर प्रयास करबासँ पहिने कने $1 काल थम्हू।",
+       "changeemail-nochange": "कृपया कोनो नव इमेल पता प्रविष्ट करी।",
        "resettokens": "टोकन रीसेट करी",
        "resettokens-text": "जे स्तोक अहाँके खाता सँ सम्बद्ध किछु विशिष्ट व्यक्तिगत जानकारी प्रदान करएत अछि, अहाँ वोकरा एतए सँ रिसेट कऽ सकएत छी।\n\nयदि अहाँ एकरा गलती सँ केकरो देखा देनए छी वा अहाँ के खाता ह्याक भ गेल अछि तहन अहाँके एकरा रिसेट कऽ देना चाही।",
        "resettokens-no-tokens": "रीसेट करवाक लेल कोनो टोकन नै अछि।",
        "minoredit": "अल्प सम्पादन",
        "watchthis": "ई पृष्ठके ध्यानसूचीमे राखी",
        "savearticle": "पन्नाक रक्षण करी",
+       "savechanges": "रक्षण करी",
+       "publishpage": "पृष्ठ प्रकाशित करी",
+       "publishchanges": "परिवर्तन प्रकाशित करी",
        "preview": "पूर्वावलोकन",
        "showpreview": "पूर्वप्रदर्शन",
        "showdiff": "परिवर्तन देखाबी",
        "missingsummary": "<strong>स्मारक:</strong> अहाँ सम्पादन सार नै देने छी।\nजँ अहाँ फेरसँ क्लिक करब \"{{int:savearticle}}\", अहाँक सम्पादन बिना एकर संरक्षित भऽ जाएत।",
        "selfredirect": "<strong>चेतावनी:</strong> आहाँ स्वेम के ई पन्ना पुनः निर्देशीत कएर रहल छी।\nआहाँ अनुप्रेषित के लेल गलत लक्ष्य निर्दिष्ट भ्या सकएत अछि, या आहाँ गलत पन्ना कें संपादन कैर सकएत छी।\nआहाँ फेरो से \"{{int:savearticle}}\" क्लिक करएत छी, रीडायरेक्ट ओनाहो भी बनाबल जेल अछि।",
        "missingcommenttext": "कृपा कऽ अपन विचार नीचाँ प्रविष्ट करी।",
-       "missingcommentheader": "'''स्मरण:''' अहाँ कोनो विषय/ शीर्षक ऐ टिप्पणीक लेल नै देने छी।\nजँ अहाँ फेरसँ क्लिक करब \"{{int:savearticle}}\" , अहाँक सम्पादन बिना एकर संरक्षित भऽ जाएत।",
+       "missingcommentheader": "<strong>अनुस्मारक:</strong> अहाँ ई टिप्पणीके कोनो शीर्षक नै देनए छी।\nयदि अहाँ \"{{int:savearticle}}\" पर पुन: क्लिक करैत छी तँ अहाँक परिवर्तन बिना शीर्षक रक्षण कएल जाइत।",
        "summary-preview": "सारांश पूर्वावलोकन",
        "subject-preview": "विषयक झलक:",
        "previewerrortext": "अहाँक परिवर्तनके पूर्वावलोकन करहिक समय एक त्रुटि आएल।",
        "userpage-userdoesnotexist": "प्रयोक्ता खाता \"$1\" पञ्जीकृत नै अछि।\nनिश्चय करी जे की अहाँ ई पन्ना बनेबाक/ सम्पादित करबाक इच्छुक छी।",
        "userpage-userdoesnotexist-view": "प्रयोक्ता खाता \"$1\" पञ्जीकृत नै अछि।",
        "blocked-notice-logextract": "ई प्रयोक्ता अखन प्रतिबन्धित अछि।\nअद्यतन प्रतिबन्धित  वृत्तलेख लेखा सन्दर्भ लेल नीचाँ देल अछि:",
-       "clearyourcache": "'''टिप्पणी:''' संरक्षणक बाद, अहाँकेँ परिवर्तन देखबा लेल अपन गवेषकक उपस्मृतिकेँ हटबए पड़त।\n''' मोजिल्ला/ फायरफॉक्स/ सफारी:''' दाबि कऽ राखू ''शिफ्ट'' केँ ''पुनर्भारित'' क्लिक करबाक समए, वा दाबू चाहे ''Ctrl-F5'' वा ''Ctrl-R'' (''Command-R'' मैकिनटोशपर);\n'''कन्करर: ''' क्लिक करू ''पुनर्भारित करू'' वा दाबू''F5'';\n'''ओपेरा:''' उपस्मृति खतम करू ''Tools → Preferences'';\n'''इन्टरनेट एक्सप्लोरर:''' दाबि कऽ राखू ''Ctrl'' क्लिक करबा काल ''नवीकरण,'' वा दाबू ''Ctrl-F5'' ।",
+       "clearyourcache": "<strong>टिप्पणी:</strong> संरक्षणक बाद, अहाँक परिवर्तन देखबा लेल अपन ब्राउजरक उपस्मृतिक हटबए पडत।\n<strong>फायरफक्स/ सफारी:<strong> <em>सिफ्ट</em>के दाबि <em>रिलोड</em>, वा <em>Ctrl-F5</em> वा <em>Ctrl-R</em> (म्याकपर <em>⌘-R</em>)\n<strong>गुगल क्रोम:</strong> <em>Ctrl-Shift-R</em> दाबी(म्याकपर <em>⌘-Shift-R</em>)\n<strong>इन्टरनेट एक्सप्लोरर:</strong> <em>Refresh</em> क्लिक करि <em>Ctrl</em> दाबि, वा <em>Ctrl-F5</em> दाबी\n<strong>ओपेरा:</strong> <em>Menu → Settings</em> पर जाए (म्याकपर <em>Opera → Preferences</em>) आ ओकर बाद <em>Privacy & security → Clear browsing data → Cached images and files</em>\n ।",
        "usercssyoucanpreview": "<strong>सङ्केत:</strong> सङ्ग्रह करैसँ पहिने अहाँ अपन नव सियसयसक जाँच लेल \"{{int:पूर्वदृश्य देखाउ}}\" बटनक प्रयोग करी।",
        "userjsyoucanpreview": "<strong>टिप</strong>  प्रयोग करी \"{{int:showpreview}}\" बटन अपन नव जावास्क्रिप्ट संरक्षण जँचबाक लेल।",
        "usercsspreview": "''' मोन राखू जे अहाँ मात्र अपन प्रयोक्ता  सी.एस.एस. क पूर्वदृश्य देख रहल छी।'''\n''' ई अखन धरि संरक्षित नै भऽ सकल!'''",
        "previewnote": "'''मोन राखू ई मातर पूर्वावलोकन छी।'''\nअहाँक परिवर्तन अखन धरि सँचिआएल नै गेल अछि!",
        "continue-editing": "सम्पादन क्षेत्र जाए",
        "previewconflict": "ई पूर्वदृश्य देखबैए उपरका सम्पादन क्षेत्रक पाठ, ई आएत जखन अहाँ संरक्षित करब।",
-       "session_fail_preview": "''' दुखी छी! अहाँक सत्रक दत्तांश खतम भऽ गेल तै कारणसँ अहाँक सम्पादनक निपटारा नै भऽ सकल।'''\nफेरसँ प्रयास करू।\nजँ ई फेरसँ काज नै करैए, प्रयोग करू [[Special:UserLogout|निष्क्रमण]] आ फेर सम्प्रवेश करू।",
-       "session_fail_preview_html": "''' दुखी छी! हम अहाँक सम्पादनक निष्पादन नै कऽ सकलहुँ कारण सत्रक दत्तांश खतम भऽ गेल।'''\n''कारण {{अन्तर्जाल}} लग काँच एच.टी.एम.एल. दत्तांश सक्रिय छै, पूर्वदृश्य जावास्क्रिप्ट आक्रमणक डरसँ नुकाएल राखल गेल अछि।''\n'''जँ ई वैध सम्पादन प्रयास अछि, कृपा कऽ पुनः प्रयास करू।'''\nजँ ई अखनो काज नै कऽ रहल अछि, प्रयास करू [[Special:UserLogout|निष्क्रमण कऽ रहल छी]] आ फेरसँ सम्प्रवेश।",
+       "session_fail_preview": "'''क्षमा करी! सेशन डाटा नष्ट होमएक कारण अहाँक परिवर्तन रक्षण नै कएल जा सकल।'''\nकृपया पुन: प्रयास करी । यदि एकर बादो सफल नै भेल तँ कृपया [[Special:UserLogout|लग आउट]] करि पुनः सम्प्रवेश करी।",
+       "session_fail_preview_html": "क्षमा करी! सेशन डाटा नष्ट होमएक कारण अहाँक परिवर्तन रक्षण नै कएल जा सकल।\n\n<em>चूँकि {{SITENAME}} पर रव एचटिएमएल सक्षम अछि, जाभास्क्रिप्ट हमला सँ बचावक लेल झलक नै देखाएल गेल अछि।</em>\n\n<strong>अगर ई अहाँक वैध सम्पादन यत्न छल, तँ कृपया पुनः प्रयास करी।</strong>\nयदि एकर बादो सफल नै भेल तँ कृपया [[Special:UserLogout|लग आउट]] करि पुनः सम्प्रवेश करी तथा जाँची यदि अहाँक ब्राउजर एहि साइट सँ कुकिजक अनुमति दैत अछि।",
        "token_suffix_mismatch": "'''अहाँक सम्पादन अस्वीकार कऽ देल गेल अछि कारण अहाँक ग्राहक प्रेष्यमान अंक विधानक विराम चेन्ह सभकेँ नष्ट कऽ देलन्हि।'''\nई सम्पादन पन्नाक पाठकेँ दूषित होएबासँ बचेबा लेल अमान्य कऽ देल गेल।\nई कखनो काल होइए जखन अहाँ जाल आधारित अनाम दोसरा लेल चल सेवा प्रयुक्त करै छी।",
        "edit_form_incomplete": "<strong>सम्पादन आवेदनक किछु भाग वितरक धरि नै पहुँचल; एक बेर फेर देखी जे अहाँक सम्पादन दुरुस्त अछि आ फेरसँ प्रयास करी।</strong>",
        "editing": "सम्पादन होइए $1",
        "yourdiff": "अन्तर",
        "copyrightwarning": "कृपा कय बुझू जे सभटा योगदान {{SITENAME}} ई बुझि कय देल जा रहल अछि जे ई निम्नांकितक अंतर्गत अछि $2 (देखू $1 जनकारीक हेतु). जौँ अहाँ चाहैत छी जी अहाँक रचना बिना रोकटोकक संपादित नहि हो किंवा बाँटल नहि जाय, तँ एकर योगदान एतय नहि करू। <br />\nएतय अहाँ ईहो सप्पत खाइत छी जी ई अहाँक अपन रचना छी आकि अहाँ एकरा कोनो सार्वजनिक डोमेन किंवा ओह्ने कोनो मँगनीक संदर्भ-स्थलसँ कॉपी कएने छी।\n< दृढ़> सर्वाधिकार सुरक्षित कार्य एतय नहि दी।!</दृढ़>",
        "copyrightwarning2": "कृपा कऽ बुझू जे सभटा योगदान {{अन्तर्जाल}} योगदानकर्ता द्वारा सम्पादित, बदलल वा हटाएल जा सकैत अछि।. जौँ अहाँ चाहैत छी जी अहाँक रचना बिना रोकटोकक संपादित नहि हो किंवा बाँटल नहि जाय, तँ एकर योगदान एतय नहि करू। <br />\nएतय अहाँ ईहो सप्पत खाइत छी जी ई अहाँक अपन रचना छी आकि अहाँ एकरा कोनो सार्वजनिक डोमेन किंवा ओहने कोनो मँगनीक संदर्भ-स्थलसँ कॉपी कएने छी(देखू $1 वर्णन लेल)।\n''' सर्वाधिकार सुरक्षित कार्य एतय नहि दी।!'''",
+       "editpage-cannot-use-custom-model": "ई पृष्ठक मुख्य सामग्री परिवर्तित नै भेल।",
        "longpageerror": "'''भ्रम: पाठ जे अहाँ देने छी से $1 किलोबाइट नमगर अछि,  जे अधिकतम आकार $2 किलोबाइट सँ बेशी नमगर अछि।'''\nई संरक्षित नै कएल जा सकत।",
-       "readonlywarning": "''' चेतौनी: ई दत्तनिधि सुस्थापन लेल प्रतिबन्धित कएल गेल अछि, से अहाँ अपन सम्पादनकेँ अखन संरक्षित नै कऽ सकब।'''\nअहाँ पाठकेँ कर्तनलेपन द्वारा एकटा टेक्स्ट संचिकामे धऽ सकै छी आ भविष्य लेल सुरक्षित राखि सकै छी।\n\nसंचालक जे एकरा प्रतिबन्धित केलन्हि से ई कारण देलन्हि: $1",
+       "readonlywarning": "<strong>सावधान: डेटाबेस सुस्थापन लेल बन्द कएल गेल अछि, एहिलेल अहाँक सम्पादन अखन रक्षण नै कएल जा सकत।\nयदि अहाँ पाठ कपी-पेस्टद्वारा एकटा टेक्स्ट सञ्चिकामे धऽ सकै छी आ भविष्य लेल सुरक्षित राखि सकै छी।</strong>\n\nसञ्चालक जे एकरा बन्द केलन्हि से ई कारण देलन्हि: $1",
        "protectedpagewarning": "''' चेतौनी: ई पन्ना संरक्षित अछि से खाली संचालन अधिकारयुक्त प्रयोक्ता एकरा सम्पादित कऽ सकै छथि।'''\nअद्यतन वृतलेख उल्लेख नीचाँ सन्दर्भ लेल देल जा रहल अछि:",
        "semiprotectedpagewarning": "'''चेतौनी:''' ई पन्ना संरक्षित अछि से खाली पंजीकृत प्रयोक्ता एकरा सम्पादित कऽ सकै छथि।\nअद्यतन वृतलेख उल्लेख नीचाँ सन्दर्भ लेल देल जा रहल अछि:",
        "cascadeprotectedwarning": "'''चेतौनी:''' ई पन्ना संरक्षित अछि से खाली संचालन अधिकारयुक्त प्रयोक्ता एकरा सम्पादित कऽ सकै छथि, कारण ई तराउपड़ी संरक्षित {{PLURAL:$1|पन्ना|पन्ना}}  मे शामिल अछि।",
        "undo-summary-username-hidden": "नुकाएल गेल प्रयोक्ताद्वारा केल गेल परिवर्तन $1 के पूर्ववत केल गेल",
        "cantcreateaccount-text": "(<strong>$1</strong>) अनिकेत पतासँ खाता निर्माण प्रतिबन्धित कएल गेल [[User:$3|$3]]।\n$3 द्वारा देल कारण अछि ''$2''",
        "cantcreateaccount-range-text": "<strong>$1</strong> के श्रेणी में आबई वाला आई॰पी पता सऽ, जएमें आहाँ कें आई॰पी पता (<strong>$4</strong>) शामिल अछि, नया खाता के रचना [[User:$3|$3]] द्वारा अवरोधित केल गेल अछि। \n\n$3 द्वारा देल गेल कारण अछि: \"$2\"",
-       "viewpagelogs": "à¤\88 à¤ªà¤¨à¥\8dनाà¤\95 à¤µà¥\83तà¥\8dतलà¥\87à¤\96सभ देखी",
+       "viewpagelogs": "à¤\88 à¤ªà¤¨à¥\8dनाà¤\95 à¤²à¤\97 देखी",
        "nohistory": "ऐ पन्ना लेल कोनो सम्पादन इतिहास नै अछि।",
        "currentrev": "नूतन संशोधन",
        "currentrev-asof": "$1 क समकालिक तखुनका संशोधन",
        "mergehistory-empty": "कोनो संशोधन मिज्झर नै कएल जा सकैए।",
        "mergehistory-done": "$3 {{PLURAL:$3|संशोधन|संशोधन सभ}} एकर $1 सफलता पूर्वक मिज्झर कएल गेल [[:$2]] मे।",
        "mergehistory-fail": "इतिहासक मिश्रणकेँ नै कऽ सकल, कृपा कऽ पन्ना आ समए परिमितिकेँ फेरसँ जाँचू।",
+       "mergehistory-fail-bad-timestamp": "समय सङ्ख्या अमान्य।",
+       "mergehistory-fail-invalid-source": "अमान्य स्रोत पृष्ठ।",
+       "mergehistory-fail-invalid-dest": "अमान्य लक्ष्य पृष्ठ।",
+       "mergehistory-fail-no-change": "इतिहास विलय कोनो भी अवतरणके विलय नै करि सकल। कृपया लेख आ समय पुन: देखी।",
+       "mergehistory-fail-permission": "इतिहास विलय हेतु अधिकार नै अछि।",
+       "mergehistory-fail-self-merge": "स्रोत आ लक्ष्य पन्ना सभ एक्के नै भऽ सकैए।",
+       "mergehistory-fail-timestamps-overlap": "स्रोत अवतरण भेजैवाला अवतरणक बाद आबि रहल अछि।",
+       "mergehistory-fail-toobig": "इतिहास विलय सम्भव नै अछि कियाकि अवतरण सीमा $1 सँ अधिक {{PLURAL:$1|अवतरण|अवतरणसभ}}के स्थानान्तरित करै पडत।",
        "mergehistory-no-source": "स्रोत पन्ना $1 नै अछि।",
        "mergehistory-no-destination": "लक्ष्य पन्ना $1 नै अछि।",
        "mergehistory-invalid-source": "स्रोत पन्ना एकटा मान्य शीर्षक हेबाक चाही।",
        "searchprofile-advanced-tooltip": "बनाएल नामस्थान सभमे ताकी",
        "search-result-size": "$1 ({{PLURAL:$2|1 शब्द|$2 शब्दसभ}})",
        "search-result-category-size": "{{PLURAL:$1|1 सदस्य|$1 सदस्य}} ({{PLURAL:$2|1 उपसंवर्ग|$2 उपसंवर्ग}}, {{PLURAL:$3|1 संचिका|$3 संचिका}})",
-       "search-redirect": "(रस्ता बदलेन $1)",
+       "search-redirect": "($1 सँ पुनर्निर्देशित)",
        "search-section": "(शाखा $1)",
        "search-category": "(श्रेणी $1)",
        "search-file-match": "(फाइल सामग्रीसे मेल खेलक अछि)",
        "search-suggest": "अहाँ मोने अछि जे:$1",
+       "search-rewritten": "$1 क परिणाम देखाए रहल अछि। ई $2 हेतु खोजि रहल अछि।",
        "search-interwiki-caption": "अन्य प्रकल्प",
        "search-interwiki-default": "$1 सँ परिणाम:",
        "search-interwiki-more": "(आर)",
        "showingresultsinrange": "नीचाँ एतऽ धरि {{PLURAL:$1|'''1''' परिणाम|'''$1''' परिणाम सभ}}  #'''$2''' सँ प्रारम्भ भऽ कऽ।",
        "search-showingresults": "{{PLURAL:$4|<strong>$3</strong> में से <strong>$1</strong> परिणाम|<strong>$3</strong> में सँ परिणाम <strong>$1 - $2</strong>}}",
        "search-nonefound": "अभ्यर्थनासँ मेल खाइत कोनो परिणाम नै भेटल।",
+       "search-nonefound-thiswiki": "अभ्यर्थनासँ मेल खाइत कोनो परिणाम नै भेटल।",
        "powersearch-legend": "विशेष खोज",
        "powersearch-ns": "निर्धारकमे खोज",
        "powersearch-togglelabel": "जाँची:",
        "prefs-watchlist-token": "साकांक्ष-सूची खेप:",
        "prefs-misc": "आर",
        "prefs-resetpass": "कूटशब्द बदली",
-       "prefs-changeemail": "à¤\88-पतà¥\8dर à¤¸à¤\82à¤\95à¥\87त à¤¬à¤¦à¤²à¥\82",
+       "prefs-changeemail": "à¤\87मà¥\87ल à¤ªà¤¤à¤¾ à¤ªà¤°à¤¿à¤µà¤°à¥\8dतित à¤\95रà¥\80",
        "prefs-setemail": "ई-पत्र ठेगान निर्धारित करी",
        "prefs-email": "ई-पत्र विकल्पसभ",
        "prefs-rendering": "मुँहकान",
        "saveprefs": "सङ्ग्रह करी",
-       "restoreprefs": "सभà¤\9fा à¤ªà¥\82रà¥\8dवनिरà¥\8dधारित à¤\9aयनà¤\95à¥\87à¤\81 à¤«à¥\87रसà¤\81 à¤\86नà¥\82",
+       "restoreprefs": "सभà¤\9fा à¤ªà¥\82रà¥\8dवनिरà¥\8dधारित à¤\9aयनà¤\95à¥\87à¤\81 à¤«à¥\87रसà¤\81 à¤\86नà¥\80",
        "prefs-editing": "सम्पादन कऽ रहल छी",
        "rows": "पाँतीसभ",
        "columns": "स्तम्भसभ",
        "searchresultshead": "ताकी",
-       "stub-threshold": "सà¥\80मा <a href=\"#\" class=\"stub\">à¤\95ाà¤\9fल à¤²à¤¾à¤\97ि</a> à¤¸à¤\81à¤\9aियाà¤\8fल (à¤\85षà¥\8dà¤\9fà¤\95):",
+       "stub-threshold": "à¤\86धार à¤²à¤¿à¤\99à¥\8dà¤\95 à¤¹à¥\87तà¥\81 à¤ªà¥\8dरारà¥\82पण ($1):",
        "stub-threshold-sample-link": "उदाहरण",
        "stub-threshold-disabled": "अशक्त कएल",
        "recentchangesdays": "आइ-काल्हिक परिवर्तनमे कतेक दिन देखाएल गेल:",
        "prefs-tokenwatchlist": "टोकन",
        "prefs-diffs": "अन्तर",
        "prefs-help-prefershttps": "इ प्राथमिकता अहाँके फेर स सम्प्रवेश करलाक बाद प्रभाव पडत।",
+       "prefswarning-warning": "अहाँ अपन पसन्दमे एहन परिवर्तन केनए छी जे अखनि धरि रक्षण नै केल गेल अछि। यदि अहाँ \"$1\" पर बिना क्लिक केनए ई पृष्ठ छोडि देबै तँ अहाँक पसन्द अपडेट नै केल जाइत।",
        "prefs-tabs-navigation-hint": "सुझाव: अहाँ टैब्स सूचीमे टैब्सके बीच आवागमन करवाक लेल बाम आर दाहिना बागलके कुंजिसभके उपयोग कइर सकैत छी।",
        "userrights": "प्रयोक्ता अधिकारक प्रबन्धन",
        "userrights-lookup-user": "प्रयोक्ता समूहसभक प्रबन्ध करी",
        "right-delete": "पन्ना मेटाबी",
        "right-bigdelete": "बेसी इतिहास भएल पन्ना सभ मेटाबी",
        "right-deletelogentry": "विशिष्ट लग प्रविष्टिसभके नुकाउ आ देखाउ",
-       "right-deleterevision": "निरà¥\8dधारित à¤¸à¤\82शà¥\8bधित à¤ªà¤¨à¥\8dना à¤®à¥\87à¤\9fाà¤\89 à¤\86 à¤«à¥\87रसà¤\81 à¤\86नà¥\82",
+       "right-deleterevision": "पà¥\83षà¥\8dठसभà¤\95 à¤µà¤¿à¤¶à¤¿à¤·à¥\8dà¤\9f à¤\85वतरण à¤¹à¤\9fाबà¥\80 à¤¤à¤¥à¤¾ à¤ªà¥\81नरà¥\8dसà¥\8dथापित à¤\95रà¥\80",
        "right-deletedhistory": "मेटाएल इतिहास प्रविष्टि देखू, बिना लागिक पाठक",
        "right-deletedtext": "मेटाएल पाठ आ दूटा मेटाएल संशोधनक बीचक परिवर्तन देखी",
        "right-browsearchive": "मेटाएल पन्ना ताकी",
        "right-override-export-depth": "५ परत धरि जा  पन्ना सभ निर्यात, जइमे लागिबला पन्ना सभ शामिल अछि, करू।",
        "right-sendemail": "ई-पत्र दोसर प्रयोक्ता लोकनिकेँ पठाउ",
        "right-passwordreset": "कूटशब्द पुनर्निर्धारण ई-पत्र देखू",
-       "right-managechangetags": "डेटाबेस से [[Special:Tags|नुकाबू]] बनाबु आर हटाबु",
+       "right-managechangetags": "[[Special:Tags|ट्यागसभ]] बनाबी आ नुकाबी",
        "right-applychangetags": "प्रयोग में लाबू [[Special:Tags|tags]] कक्रो बदलाव के साथ।",
        "right-changetags": "जमा करु आर हटाबु स्वतंत्र [[Special:Tags|टैग]] व्यक्तिगत अवतरण आर लॉग प्रविक्ति पे",
+       "right-deletechangetags": "डेटाबेस सँ [[Special:Tags|ट्यागसभ]] मेटाबी",
        "grant-generic": "\"$1\" अधिकार सङ्ग्रह",
        "grant-group-page-interaction": "पृष्ठसभसँ जोडी",
        "grant-group-file-interaction": "मिडियासँ जोडी",
-       "newuserlogpage": "प्रयोक्ता रचना वृत्तलेख",
+       "grant-group-watchlist-interaction": "ध्यानसूची सँ मेल करी",
+       "grant-group-email": "इमेल पठाबी",
+       "grant-group-high-volume": "उच्च कार्य गतिविधि करी",
+       "grant-group-customization": "पसन्द आ तय",
+       "grant-group-administration": "प्रबन्धकीय कार्य करी",
+       "grant-group-private-information": "अपन सम्बन्धमे निजी डेटा आनी",
+       "grant-group-other": "अन्य गतिविधि",
+       "grant-blockusers": "प्रतिबन्धित आ अप्रतिबन्धित करनाए",
+       "grant-createaccount": "खाता खोलल जाए",
+       "grant-createeditmovepage": "निर्माण, सम्पादन, आ स्थानान्तरण करनाए",
+       "grant-delete": "लेख, अवतरण आ लग हटेनाए",
+       "grant-editinterface": "मिडियाविकि नामस्थान आ सदस्य सिएसएस/जेएस सम्पादित करनाए",
+       "grant-editmycssjs": "अपन सदस्य सिएसएस/जेएस सम्पादित करी",
+       "grant-editmyoptions": "अपन सदस्य पसन्द सम्पादित करी",
+       "grant-editmywatchlist": "अपन साकांक्षसूची सम्पादित करी",
+       "grant-editpage": "बनल पृष्ठ सम्पादित करी",
+       "grant-editprotected": "सुरक्षित पृष्ठ सम्पादित करी",
+       "grant-highvolume": "अत्यधिक तेजी सँ सम्पादन",
+       "grant-oversight": "सदस्य नुकाबी आ अवतरण हटाबी",
+       "grant-patrol": "पृष्ठसभके जाँचल चिन्हित करी",
+       "grant-privateinfo": "निजी जानकारी आनी",
+       "grant-protect": "पृष्ठसभ सुरक्षित व असुरक्षित करनाए",
+       "grant-rollback": "पृष्ठ सँ सम्पादन पूर्ववत केनाए",
+       "grant-sendemail": "अन्य प्रयोगकर्ताके इ-मेल भेजी",
+       "grant-uploadeditmovefile": "फाइल अपलोड, बदलनाए, स्थानान्तरण करनाए",
+       "grant-uploadfile": "नव सञ्चिकासभ उपारोपित करी",
+       "grant-basic": "सामान्य अधिकार",
+       "grant-viewdeleted": "हटाएल फाइल व पृष्ठ देखी",
+       "grant-viewmywatchlist": "अपन साकांक्षसूची देखी",
+       "newuserlogpage": "प्रयोक्ता रचना लग",
        "newuserlogpagetext": "ई प्रयोक्ता निर्माणक वृत्तलेख अछि।",
        "rightslog": "प्रयोक्ता अधिकार वृत्तलेख",
        "rightslogtext": "ई प्रयोक्ता अधिकार परिवर्तन सभक वृतलेख छी।",
        "action-read": "ई पन्ना पढी",
        "action-edit": "ई पन्नाक सम्पादित करी",
-       "action-createpage": "पृष्ठ बनाबी",
-       "action-createtalk": "वार्ता पन्ना बनाबी",
+       "action-createpage": "à¤\88 à¤ªà¥\83षà¥\8dठ à¤¬à¤¨à¤¾à¤¬à¥\80",
+       "action-createtalk": "à¤\88 à¤µà¤¾à¤°à¥\8dता à¤ªà¤¨à¥\8dना à¤¬à¤¨à¤¾à¤¬à¥\80",
        "action-createaccount": "ई प्रयोक्ता खाता बनाबी",
+       "action-autocreateaccount": "स्वतः बाहरी सदस्य खाता बनाबी",
        "action-history": "पन्नाक इतिहास मिज्झर करी",
        "action-minoredit": "ऐ सम्पादनके मामूली कही",
        "action-move": "ई पृष्ठके स्थानान्तरित करी",
        "action-viewmyprivateinfo": "अपन व्यक्तिगत जानकारी देखु",
        "action-editmyprivateinfo": "अपन व्यक्तिगत जानकारी सम्पादित करु",
        "action-editcontentmodel": "एक पन्ना के सामग्री मॉडल कें सम्पादन।",
-       "action-managechangetags": "डà¥\87à¤\9fाबà¥\87स à¤¸à¥\87 à¤\9aिपà¥\8dपि à¤¬à¤¨à¤¾à¤¬à¥\81 à¤\86र à¤¹à¤\9fाबà¥\81",
+       "action-managechangetags": "à¤\9fà¥\8dयाà¤\97 à¤¬à¤¨à¤¾à¤¬à¥\80 à¤\86 à¤¸à¤\95à¥\8dषम (à¤\85सà¤\95à¥\8dषम) à¤\95रà¥\80",
        "action-applychangetags": "आहाँ के बदलाव के साथ टैग जोडू।",
        "action-changetags": "जमा करु आर हटाबु स्वतंत्र टैग व्यक्तिगत अवतरण आर लॉग प्रविक्ति पे",
+       "action-deletechangetags": "डेटाबेस सँ ट्याग मेटाबी",
+       "action-purge": "पृष्ठक क्यास खाली करी",
        "nchanges": "$1 {{PLURAL:$1|परिवर्त्तन|परिवर्त्तन}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|अंतिम बेर देखला के बाद स}}",
        "enhancedrc-history": "इतिहास",
        "recentchanges-label-plusminus": "पन्ना आकार ई बाइट सङ्ख्या सँ बदलल गेल",
        "recentchanges-legend-heading": "<strong>कुञ्जी:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} ([[Special:NewPages|नव पन्नसभक सूची]] सेहो देखी)",
+       "recentchanges-submit": "देखाबी",
        "rcnotefrom": "नीचाँमे '''$2''' सँ भेल परिवर्तन अछि ('''$1''' धरि देखाएल)।",
        "rclistfrom": "$3 $2 सँ शुरू भेल नव परिवर्तन देखी",
        "rcshowhideminor": "$1 अल्प सम्पादन",
        "rcshowhideminor-show": "देखाबी",
        "rcshowhideminor-hide": "नुकाबी",
-       "rcshowhidebots": "$1 स्वचालक",
+       "rcshowhidebots": "स्वचालक $1",
        "rcshowhidebots-show": "देखाबी",
        "rcshowhidebots-hide": "नुकाबी",
        "rcshowhideliu": "पञ्जीकृत प्रयोगकर्तासभ $1",
        "rcshowhidemine": "$1 हमर सम्पादनसभ",
        "rcshowhidemine-show": "देखाबी",
        "rcshowhidemine-hide": "नुकाबी",
+       "rcshowhidecategorization": "$1 पृष्ठ श्रेणीकरण",
        "rcshowhidecategorization-show": "देखाबी",
        "rcshowhidecategorization-hide": "नुकाबी",
        "rclinks": "पिछला $2 दिनमे भएल $1 परिवर्तन देखाबी<br />$3",
        "boteditletter": "ब",
        "unpatrolledletter": "!",
        "number_of_watching_users_pageview": "[$1 ध्यान राखैवाला {{PLURAL:$1|प्रयोक्ता|प्रयोक्तासभ}}]",
-       "rc_categories": "सà¤\82वरà¥\8dà¤\97 à¤¸à¥\80मित (\"|\" à¤¸à¤\81 à¤¹à¤\9fाà¤\89)",
-       "rc_categories_any": "कोनो",
+       "rc_categories": "शà¥\8dरà¥\87णà¥\80सभ à¤§à¤°à¤¿ à¤¸à¥\80मà¥\80त à¤°à¤¾à¤\96à¥\80 (\"|\" à¤¸à¤\81 à¤\85लà¤\97 à¤\95रà¥\80)",
+       "rc_categories_any": "कोनो भी चुनिन्दा",
        "rc-change-size": "$1",
        "rc-change-size-new": "परिवर्तनक बाद $1 {{PLURAL:$1|बाइट}}",
        "newsectionsummary": "/* $1 */ नव अनुभाग",
        "recentchangeslinked-summary": "ई विशेष पन्नासँ सम्बद्ध पन्ना सभमे (आकि कोनो विशेष वर्गक समूहमे) भेल परिवर्तनक सूची छी ।\n[[Special:Watchlist|your watchlist]]  पर पन्नासभ '''गाढ़''' अछि।",
        "recentchangeslinked-page": "पन्नाक नाम:",
        "recentchangeslinked-to": "देल पन्नाक सम्बन्धी पन्नामे परिवर्तन देखाबी",
+       "recentchanges-page-added-to-category": "[[:$1]] श्रेणीमे जुडल",
+       "recentchanges-page-removed-from-category": "[[:$1]] श्रेणी सँ हटल",
+       "autochange-username": "मिडियाविकि स्वतः परिवर्तन",
        "upload": "फाइल अपलोड करी",
        "uploadbtn": "फाइल अपलोड",
        "reuploaddesc": "उपारोपण रद्द करी आ उपारोपण आवेदन-पत्रपर जाए।",
        "upload-recreate-warning": "'''चेतौनी: ऐ नामक संचिका मेटा वा हटा देल गेल अछि।'''",
        "uploadtext": "निचुक्का पत्र संचिका उपारोपित करबा लेल प्रयोग करू।\nपहिलुका उपारोपित संचिका देखबा वा तकबा लेल जाउ [[Special:FileList|उपारोपित संचिका सभक सूची]], (पुनः) उपारोपित सेहो सम्प्रवेशित अछि [[Special:Log/upload|उपारोपित वृत्तलेख]] मे, मेटाएल सभ [[Special:Log/delete|मेटाएल वृत्तलेख]] मे।\nपन्नमे एकटा संचिका देबा लेल, ऐ पत्र सभमेसँ कोनो लागिक प्रयोग करू:\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.jpg]]</nowiki></code>''' संचिकाक पूर्ण संस्करण देखबा लेल\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.png|200px|thumb|left|alt text]]</nowiki></code>'''  २०० चित्राणु चाकर प्रकटन एकटा बक्शामे \"वैकल्पिक पाठ\" वामा कात वर्णनक रूपमे लिखल प्रयोग करू\n* '''<code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code>''' बिना संचिका देखेने सोझे संचिकाक लागि देब",
        "upload-permitted": "अनुमतित फाइल  {{PLURAL:$2|प्रकार}}: $1।",
-       "upload-preferred": "मà¥\8bनपसिनà¥\8dन à¤¸à¤\82à¤\9aिà¤\95ा à¤ªà¥\8dरà¤\95ार:$1 ।",
-       "upload-prohibited": "पà¥\8dरतिबनà¥\8dधित à¤¸à¤\82à¤\9aिà¤\95ा à¤ªà¥\8dरà¤\95ार:$1 ।",
+       "upload-preferred": "पसनà¥\8dदिदा à¤«à¤¾à¤\87ल {{PLURAL:$2|पà¥\8dरà¤\95ार|पà¥\8dरà¤\95ारसभ}}: $1।",
+       "upload-prohibited": "पà¥\8dरतिबनà¥\8dधित à¤¸à¤\9eà¥\8dà¤\9aिà¤\95ा {{PLURAL:$2|पà¥\8dरà¤\95ार|पà¥\8dरà¤\95ारसभ}}:$1 ।",
        "uploadlogpage": "उपारोपण वृत्तलेख",
        "uploadlogpagetext": "नीचाँ अद्यतन सञ्चिका उपारोपणक वर्णन अछि।\nदेखी [[Special:NewFiles|नव सञ्चिकाक बखारी]] बेसी स्पष्ट समुच्चा दृश्य लेल।",
        "filename": "सञ्चिका नाम",
        "uploaddisabledtext": "संचिका उपारोपण सभ अशक्त अछि।",
        "php-uploaddisabledtext": "पी.एच.पी.मे संचिका उपारोपण अशक्त अछि।\nकृपा कऽ संचिका उपारोपण विकल्प जाँचू।",
        "uploadscripted": "ई संचिका पर्यंकभाषा वा कूटलिपि युक्त अछि जे गवेषक द्वारा गलत रूपमे व्याख्यायित कएल जा सकैए।",
+       "upload-scripted-pi-callback": "ओ फाइल अपलोड नै केल जा सकैत अछि जाहिमे एक्सएमएल-स्टाइलसिट प्रसंस्करण निर्देश समाविष्ट अछि।",
+       "uploaded-script-svg": "अपलोड केल गेल एसभिजी फाइलमे स्क्रिप्ट अवयव \"$1\" पाबल गेल।",
        "uploadscriptednamespace": "इ एस॰वी॰जी फाइलमे अमान्य नामस्थान \"$1\" अछि।",
        "uploadinvalidxml": "अपलोड केएल गेल फाइलमे स्थित XML पार्स नै केएल जा सकैत अछि।",
        "uploadvirus": "ई संचिका विषविधियुक्त अछि।\nवर्णन:$1",
        "uploadstash-summary": "ई पन्ना उपारोपित संचिका सभक प्रवेश द्वार छी (वा उपारोपणक प्रक्रियामे) मुदा अखन धरि विकीमे प्रकाशित नै भेल अछि। ई सभ संचिका प्रयोक्ताक अतिरिक्त ककरो द्वारा देखल नै जा सकैए।",
        "uploadstash-clear": "नुकाएल बखारी सभक संचिकाकेँ साफ खतम करू",
        "uploadstash-nofiles": "अहाँ लग कोनो नुकाएल संचिका सभ नै अछि।",
-       "uploadstash-badtoken": "ओइ कार्यक सम्पादन असफल रहल, प्रायः अहाँक सम्पादन योग्यता खतम भऽ गेल अछि। फेरसँ प्रयास करू।",
-       "uploadstash-errclear": "सà¤\82à¤\9aिà¤\95ा à¤¸à¤­à¤\95à¥\87à¤\81 à¤\96तम à¤\95रब असफल रहल।",
+       "uploadstash-badtoken": "ओ कार्यक सम्पादन असफल रहल, प्रायः अहाँक सम्पादन योग्यता खतम भऽ गेल अछि। फेर सँ प्रयास करी।",
+       "uploadstash-errclear": "फाà¤\87लसभà¤\95à¥\87 à¤¸à¤¾à¤« à¤\95रनाà¤\8f असफल रहल।",
        "uploadstash-refresh": "संचिका सभक सूचीकेँ ताजा करू।",
+       "uploadstash-thumbnail": "छवि देखी",
        "invalid-chunk-offset": "एकट्ठे अमान्य बौस्तु",
        "img-auth-accessdenied": "प्रवेश प्रतिबन्धित",
        "img-auth-nopathinfo": "बाटक जानकारी नै अछि।\nअहाँक वितरक ऐ सूचनाकेँ प्रसारित नै कऽ सकत।\nई सी.जी.आइ.आधारित अछि आ चित्र-समर्थन केँ समर्थन नै दऽ सकत।\n[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization देखू image authorization.]",
        "randompage-nopages": "ऐमे दोसर पन्ना नै अछि {{PLURAL:$2|namespace|namespaces}}: $1 ।",
        "randomincategory": "श्रेणी में यादृच्छिक (रैंडम) पन्ना",
        "randomincategory-invalidcategory": "\"$1\" एक मान्य श्रेणी नाम नै अछि।",
+       "randomincategory-nopages": "[[:Category:$1|$1]] श्रेणीमे कोनो पृष्ठ नै अछि।",
        "randomincategory-category": "श्रेणी:",
+       "randomincategory-legend": "श्रेणीमे यादृच्छिक पृष्ठ",
+       "randomincategory-submit": "जाए",
        "randomredirect": "मिज्झर बदलेनबला लागि",
        "randomredirect-nopages": "नामस्थान \"$1\" मे कोनो बदलेनबला लागि नै अछि।",
        "statistics": "सांख्यिकी",
        "statistics-users": "पञ्जीकृत [[Special:ListUsers|प्रयोक्ता]]",
        "statistics-users-active": "सक्रिय प्रयोक्ता",
        "statistics-users-active-desc": "प्रयोक्ता जे अन्तिम {{PLURAL:$1|दिन|$1 दिन}} मे कोनो काज केने छथि",
+       "pageswithprop": "पृष्ठ जाहिमे पृष्ठ गुण अछि",
+       "pageswithprop-legend": "पृष्ठ जाहिमे पृष्ठ गुण अछि",
+       "pageswithprop-text": "ई पृष्ठ पृष्ठ गुणक उपयोग करि रहल पन्नासभ सूचीबद्ध करैत अछि।",
+       "pageswithprop-prop": "गुणक नाम:",
        "pageswithprop-submit": "जाए",
+       "pageswithprop-prophidden-long": "लम्बा पाठक गुण नुकाएल ($1) अछि",
+       "pageswithprop-prophidden-binary": "बाइनरी गुण ($1) नुकाएल अछि।",
        "doubleredirects": "द्वितीयक लागएबला बदलेन",
        "doubleredirectstext": "ई पन्ना ओइ पन्ना सभक संकलन छी जे बदलेन करैए दोसर बदलेनबला पन्नासँ।\nप्रत्येक पाँती पहिल आ दोसर बदलेनक लागि रखने अछि आ संगे दोसर बदलेनक लक्ष्य सेहो, जे वास्तवमे \"वास्तव\" लक्ष्य पन्ना अछि, जकरापर पहिल बदलेनकेँ जेबाक चाही। \n <del>Crossed out</del> प्रविष्टिक हल भेटल अछि।",
        "double-redirect-fixed-move": "[[$1]] घसकाएल गेल।\nई आब [[$2]] दिस जा रहल अछि।",
        "booksources-invalid-isbn": "देल आइ.एस.बी.एन. संख्या मान्य नै बुझाइत अछि; कृपा कऽ मूल स्रोतसँ द्वितीयक बनेबा काल भेल भ्रमकेँ जाँचू।",
        "specialloguserlabel": "कर्ता:",
        "speciallogtitlelabel": "प्रयोजन (शीर्षक अथवा {{ns:user}}:प्रयोगकर्तानाम):",
-       "log": "वृत्तलेख",
+       "log": "लौग",
+       "logeventslist-submit": "देखाबी",
        "all-logs-page": "सभ सार्वजनिक वृत्तलेख",
        "alllogstext": "{{अन्तर्जाल}} क सभटा उपलब्ध वृत्तलेखक संयुक्त दृश्य।\nअहाँ दृश्यकेँ संकीर्ण करबा लेल वृत्तलेखक एकटा प्रकार चुनि सकै छी, प्रयोक्तानाम (ब्रह्मक्षर-लघ्वक्षर विचारणीय), वा प्रभावित पन्ना (एतौ ब्रह्मक्षर-लघ्वक्षर विचारणीय)।",
        "logempty": "वृत्तलेखमे कोनो मेल खाइबला बौस्तु नै।",
        "log-title-wildcard": "खोज शीर्षक सभ ऐ पाठसँ प्रारम्भ",
        "showhideselectedlogentries": "देखाबी/ नुकाबी चयनित लग",
        "log-edit-tags": "चुनल गेल लग प्रविक्तिसभ एक सम्पादन ट्याग",
+       "checkbox-select": "चुनी: $1",
+       "checkbox-all": "सभटा",
+       "checkbox-none": "कोनो नै",
+       "checkbox-invert": "बदली",
        "allpages": "सभ पन्ना",
        "nextpage": "अगिला पन्ना ($1)",
        "prevpage": "पहिलुका पन्ना ($1)",
        "cachedspecial-viewing-cached-ts": "अहाँ इ पृष्ठ के क्यास कएल गएल अवतरण देख रहल छी, जे कि संभवतः वर्तमान अवस्था सँ भिन्न भऽ सकएत अछि।",
        "cachedspecial-refresh-now": "लब्का देखु",
        "categories": "श्रेणीसभ",
+       "categories-submit": "देखाबी",
        "categoriespagetext": "ई {{PLURAL:$1|संवर्गमे अछि|संवर्ग सभमे अछि}} पन्ना वा मीडिया।\n[[Special:UnusedCategories|Unused categories]] एतए देखाएल नै अछि।\nईहो देखू [[Special:WantedCategories|wanted categories]]।",
        "categoriesfrom": "पन्ना प्रदर्शन प्रारम्भ भेल:",
        "deletedcontributions": "मेटाएल प्रयोक्ता योगदान",
        "listusers-blocked": "(प्रतिबन्धित)",
        "activeusers": "सक्रिय प्रयोक्ता सभक सूची",
        "activeusers-intro": "ई ओहेन प्रयोक्ता सभक सूची अछि जे पछिला $1 {{PLURAL:$1|दिन|दिन}} मे किछु सक्रियता देखेने छथि।",
-       "activeusers-count": "$1 {{PLURAL:$1|समà¥\8dपादन}} à¤µà¤¿à¤\97त $3 {{PLURAL:$3|दिन|दिन}}मे",
+       "activeusers-count": "$1 {{PLURAL:$1|à¤\95ारà¥\8dय}} à¤ªà¤¿à¤\9bला $3 {{PLURAL:$3|दिन|दिनसभ}}मे",
        "activeusers-from": "प्रयोक्ता प्रदर्शन प्रारम्भ भेल:",
        "activeusers-hidebots": "स्वचालक नुकाबी",
        "activeusers-hidesysops": "प्रबन्धक नुकाबी",
        "activeusers-noresult": "कोनो प्रयोक्ता नै भेटल",
+       "activeusers-submit": "सक्रिय प्रयोगकर्ता देखाबी",
        "listgrouprights": "प्रयोगकर्ता समूह अधिकार",
        "listgrouprights-summary": "ई सभ प्रयोक्ता संवर्गक एकटा सूची अछि जे ऐ विकीपरपरिभाषित अछि ओकर संसर्गित प्रवेश अधिकारक संग।\nएतए [[{{MediaWiki:Listgrouprights-helppage}}|additional information]] व्यक्तिगत अधिकार लेल भऽ सकैए।",
        "listgrouprights-key": "* <span class=\"listgrouprights-granted\">देल अधिकार</span>\n* <span class=\"listgrouprights-revoked\">निकालल अधिकार</span>",
        "listgrouprights-namespaceprotection-header": "नामस्थान प्रतिबन्धित",
        "listgrouprights-namespaceprotection-namespace": "नामस्थान",
        "listgrouprights-namespaceprotection-restrictedto": "सांच(सभ) के संपादन करए लेल",
+       "listgrants": "प्रदान",
+       "listgrants-summary": "ई प्रदान केल गेल सूची छी। सदस्य अपन खाताक अनुपयोगक द्वारा उपयोग करि सकैत अछि, मुदा मात्र किछ सीमित अधिकार धरि। ई अधिकार सदस्यद्वारा देल गेल अधिकार धरि सीमित रहैत अछि । एतय [[{{MediaWiki:Listgrouprights-helppage}}|अन्य जानकारी]] सेहो अछि, जे एक अधिकारक बारेमे बताबैत अछि।",
+       "listgrants-grant": "अधिकार",
+       "listgrants-rights": "अधिकार",
        "trackingcategories": "श्रेणीके ट्रयाक करु",
+       "trackingcategories-summary": "ई पृष्ठ पर ओ जोडवाला श्रेणीसभक सूची मिलैत अछि जे स्वतः रूप सँ मिडियाविकि सफ्टवेयरद्वारा बनैत अछि। ओ सभक नाम सम्बन्धित प्रणाली सन्देस बदलै सँ {{ns:8}} नामस्थानमे बदलल जा सकैत अछि।",
        "trackingcategories-msg": "चिह्नित श्रेणी",
        "trackingcategories-name": "सन्देश नाम",
        "trackingcategories-desc": "श्रेणी समावेशीकरण मापदण्ड",
        "wlheader-showupdated": "पन्ना सभ जे अहाँक एतए अन्तिम बेर अएलाक बाद बदलल अछि तकर सूची देल अछि '''गाढ़''' मे",
        "wlnote": "नीचाँ {{PLURAL:$1|is the last change|are the last '''$1''' changes}} अन्तिम {{PLURAL:$2|hour|'''$2''' hours}} $3, $4 जेना।",
        "wlshowlast": "देखाउ अन्तिम $1 घण्टा $2 दिन",
+       "watchlist-hide": "नुकाबी",
+       "watchlist-submit": "देखाबी",
+       "wlshowtime": "समय श्रेणी देखाबी:",
+       "wlshowhideminor": "छोट सम्पादन",
+       "wlshowhidebots": "स्वचालक",
+       "wlshowhideliu": "पञ्जीकृत प्रयोक्तासभ",
+       "wlshowhideanons": "बेनामी प्रयोक्तासभ",
+       "wlshowhidepatr": "परीक्षित सम्पादन",
+       "wlshowhidemine": "हमर सम्पादन",
+       "wlshowhidecategorization": "पृष्ठ श्रेणीकरण",
        "watchlist-options": "साकांक्षसूचीक विकल्प",
        "watching": "ताकिमे...",
        "unwatching": "छोडल ...",
        "enotif_subject_deleted": "{{SITENAME}} पन्ना $1 के {{gender:$2|$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_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 देखी।",
+       "enotif_body_intro_changed": "{{SITENAME}} पृष्ठ $1 के {{gender:$2|$2}}द्वारा $PAGEEDITDATE क परिवर्तित केनए अछि, वर्तमान अवतरणक लेल $3 देखी।",
        "enotif_lastvisited": "देखू $1 अपन अन्तिम बेर अएलाक बादक परिवर्तन लेल।",
        "enotif_lastdiff": "ऐ परिवर्तनकेँ देखबा लेल $1 देखू।",
        "enotif_anon_editor": "गुप्त प्रयोक्ता $1",
        "deletepage": "पन्ना मेटाउ",
        "confirm": "पक्का छी",
        "excontent": "विषय छल:\"$1\"",
-       "excontentauthor": "पाठ छल:\"$1\" (आ एकमात्र योगदान दैबला छल \"[[Special:Contributions/$2|$2]]\")",
+       "excontentauthor": "पाठ छल:\"$1\" (आ एकमात्र योगदान दैबला छल \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|वार्ता]])",
        "exbeforeblank": "खतम होएबाक पहिने पाठ छल:\"$1\"",
        "delete-confirm": "$1 के मेटाबी",
        "delete-legend": "मेटाबी",
        "historywarning": "'''चेतौनी:''' जे पन्ना अहाँ मेटबैबला छी तकर इतिहास अछि लगभग $1 {{PLURAL:$1|revision|revisions}}:",
+       "historyaction-submit": "देखाबी",
        "confirmdeletetext": "अहाँ सभटा इतिहासक संग ऐ पन्नाकेँ हटाबऽ जा रहल छी।\nअहाँ ई सुनिश्चित करू जे अहाँ ई करऽ चाहै छी, अहाँकेँ एकर परिणामक अवगति अछि आ अहाँ ई ऐ [[{{MediaWiki:Policy-url}}|नीति]] क अनुसार कऽ रहल छी।",
        "actioncomplete": "क्रिया पूर्ण",
        "actionfailed": "कार्य नै भेल",
        "delete-toobig": "ऐ पन्नामे बड्ड बेसी सम्पादन इतिहास अछि, $1 सँ बेसी {{PLURAL:$1|revision|revisions}}।\nओइ सभ पन्नाक मेटाएब प्रतिबन्धित कएल गेल अछि जइसँ आकस्मिक क्षति नै हुअए {{जालस्थलक}}।",
        "delete-warning-toobig": "ऐ पन्नामे बड्ड सम्पादन इतिहास अछि, $1 सँ बेसी {{PLURAL:$1|revision|revisions}}।\nएकरा मेटेलापर दत्तनिधि क्रिया {{जालस्थल}} खतरामे पड़त;\nसतर्कीसँ आगाँ बढ़ू।",
        "deleteprotected": "अहाँ इ पन्ना नै मेटा सकए छी कियाकि ई सुरक्षण कएल गेल अछि",
-       "deleting-backlinks-warning": "'''चेतौनी:''' जे पृष्ठ अहाँ हटावए लेल जा रहल छी वोकरा में  [[Special:WhatLinksHere/{{FULLPAGENAME}}|अन्य पृष्ठ]] जुड़एत अछि अथवा वोकरा ट्रान्सक्ल्युड करएत अछि।",
+       "deleting-backlinks-warning": "<strong>चेतावनी:</strong> जे पृष्ठ अहाँ मेटाबै लेल जा रहल छी ओ  [[Special:WhatLinksHere/{{FULLPAGENAME}}|अन्य पृष्ठसभ]] सँ जुडल अछि अथवा ट्रान्सक्ल्युड करैत अछि।",
        "rollback": "प्रत्यावर्तित सम्पादन",
        "rollbacklink": "प्रत्यावर्तन",
        "rollbacklinkcount": "$1 {{PLURAL:$1|सम्पादन}} पूर्ववत करी",
        "revertpage": "सम्पादन आपस कएल गेल [[Special:Contributions/$2|$2]] ([[User talk:$2|talk]]) सँ अन्तिम संशोधन धरि एकरा द्वारा [[User:$1|$1]]।",
        "revertpage-nouser": "(प्रयोक्ताक नाम हटा देल गेल अछि) द्वारा केल गेल संपादनकेँ फेरसँ पुरान स्थितिमे आनि कऽ एकर पहिलुक [[User:$1|$1]] सँ बनल संस्करणकेँ फेरसँ ताजा संस्करण बनाऊ।",
        "rollback-success": "$1 केर सम्पादन हटाबी। \n$2 केर सम्पादित आखिरी अवतरणक पुनर्स्थापित करल गेल।",
+       "rollback-success-notify": "$1द्वारा पूर्ववत सम्पादन;\n$2द्वारा केल अन्तिम अवतरण पर वापस। [$3 परिवर्तन देखाबी]",
        "sessionfailure-title": "सत्र विफल भ गेल",
        "sessionfailure": "एहन लागैत अछि जे अहां के लागिन सत्र में कोनो त्रुटि अछि. सत्र अपहरण से बचाबय  सं सावधानीक लेल अहां के अहि क्रियाकलाप क रद्द क देल गेल. अहां पाछां के पृष्ठ पर जौउ आ पृष्ठ के फेर सं लोड क दोबारा कोशिश करू.",
-       "protectlogpage": "सुरक्षा लग",
+       "changecontentmodel": "पृष्ठ सामग्री मोडल परिवर्तन करी",
+       "changecontentmodel-legend": "पृष्ठ सामग्रीक नमूना",
+       "changecontentmodel-title-label": "पृष्ठ शीर्षक",
+       "changecontentmodel-model-label": "नव सामग्रीक नमूना",
+       "changecontentmodel-reason-label": "कारण:",
+       "changecontentmodel-submit": "परिवर्तन",
+       "changecontentmodel-success-title": "सामग्री नमूना परिवर्तन भेल",
+       "changecontentmodel-success-text": "[[:$1]]के सामग्रीक प्रकार परिवर्तित भेल।",
+       "changecontentmodel-cannot-convert": "[[:$1]]क सामग्री प्रकार $2 मे नै परिवर्तित केल जा सकल।",
+       "changecontentmodel-nodirectediting": "$1 सामग्री सीधा सम्पादन समर्थित नै करैत अछि",
+       "changecontentmodel-emptymodels-title": "कोनो सामग्री प्रारूप उपलब्ध नै",
+       "changecontentmodel-emptymodels-text": "[[:$1]]मे रहल सामग्री प्रकार परिवर्तित नै केल जा सकत।",
+       "log-name-contentmodel": "सामग्री परिवर्तन लग",
+       "log-description-contentmodel": "आयोजन जे ई पृष्ठक सामग्री सँ एनमेन होए",
+       "logentry-contentmodel-new": "$1द्वारा  $3 पृष्ठक {{GENDER:$2|निर्माण}} कोनो बिना मूल सामग्री प्रारूपके \"$5\"",
+       "logentry-contentmodel-change": "$1द्वारा $3 पृष्ठक सामग्री \"$4\" सँ \"$5\" {{GENDER:$2|परिवर्तित केलक}}",
+       "logentry-contentmodel-change-revertlink": "पूर्ववत करी",
+       "logentry-contentmodel-change-revert": "पूर्ववत करी",
+       "protectlogpage": "सुरक्षा लौग",
        "protectlogtext": "नीचाँ किछु पन्ना सुरक्षा परिवर्तनक सूची अछि।\nदेखू [[Special:ProtectedPages|protected pages list]] लगक कार्यरत पन्ना सुरक्षाकऽ सूची लेल।",
        "protectedarticle": "रक्षित \"[[$1]]\" कएल गेल",
        "modifiedarticleprotection": "\"[[$1]]\" लेल बदलैत रक्षा स्तर",
        "protect-locked-blocked": "अहाँ प्रतिबन्धमे रहि कऽ सुरक्षा स्तर नै बदलि सकै छी।\nएतए पन्ना '''$1''' लेल वर्तमान नियत कएल विकल्प अछि:",
        "protect-locked-dblock": "सक्रिय दत्तनिधि प्रतिबन्धक कारण सुरक्षा स्तर नै बदलल जा सकैए।\nएतए '''$1''' लेल वर्तमान नियत विकल्प देल अछि:",
        "protect-locked-access": "अहाँक खाता अहाँकेँ रक्षा स्तरमे परिवर्तनक अधिकार नै दैत अछि।\nएतए '''$1'''पन्नाक वर्तमान परिस्थिति देल गेल अछि:",
-       "protect-cascadeon": "à¤\88 à¤ªà¤¨à¥\8dना à¤\85à¤\96न à¤°à¤\95à¥\8dषित à¤\85à¤\9bि à¤\95ारण à¤\88 à¤\90 à¤®à¥\87 à¤¸à¤®à¥\8dमिलित à¤\85à¤\9bि {{PLURAL:$1|पनà¥\8dना, à¤\9cà¥\87 à¤\85à¤\9bि|पनà¥\8dना à¤¸à¤­, à¤\9cà¥\87 à¤¸à¤­ à¤\85à¤\9bि}} à¤¤à¤°à¤¾à¤\89पड़à¥\80 à¤°à¤\95à¥\8dषण à¤²à¤¾à¤\97à¥\82।\nà¤\85हाà¤\81 à¤\90 à¤ªà¤¨à¥\8dनाà¤\95 à¤°à¤\95à¥\8dषा à¤¸à¥\8dतरà¤\95à¥\87à¤\81 à¤¬à¤¦à¤²à¤¿ à¤¸à¤\95à¥\88 à¤\9bà¥\80, à¤®à¥\81दा à¤¤à¤¾à¤\87 à¤¸à¤\81 à¤¤à¤°à¤¾à¤\89पड़à¥\80 à¤°à¤\95à¥\8dषापर à¤\85सर à¤¨à¥\88 à¤ªà¤¡à¤¼त।",
+       "protect-cascadeon": "à¤\88 à¤ªà¤¨à¥\8dना à¤\85à¤\96न à¤¸à¤\82रà¤\95à¥\8dषित à¤\85à¤\9bि à¤\95ियाà¤\95à¥\80 à¤\8fहिमà¥\87 {{PLURAL:$1|पनà¥\8dना, à¤\9cà¥\87 à¤\85à¤\9bि|पनà¥\8dना à¤¸à¤­, à¤\9cà¥\87 à¤¸à¤­ à¤\85à¤\9bि}} à¤\95à¥\8dयासà¤\95à¥\87डिà¤\99 à¤¸à¤\82रà¤\95à¥\8dषण à¤¸à¤\95à¥\8dषम à¤\85à¤\9bि।\nà¤\85हाà¤\81 à¤\88 à¤ªà¤¨à¥\8dनाà¤\95 à¤¸à¥\81रà¤\95à¥\8dषा à¤¸à¥\8dतर à¤¬à¤¦à¤²à¤¿ à¤¸à¤\95à¥\88त à¤\9bà¥\80, à¤®à¥\81दा à¤¤à¤¾à¤¹à¤¿ à¤¸à¤\81 à¤\95à¥\8dयासà¤\95à¥\87डिà¤\99 à¤°à¤\95à¥\8dषापर à¤\85सर à¤¨à¥\88 à¤ªà¤¡त।",
        "protect-default": "सभ प्रयोक्ताकेँ अधिकार दएल जाए",
        "protect-fallback": "\"$1\" अधिकार भेल प्रयोक्तासभके अनुमति दएल जाए",
        "protect-level-autoconfirmed": "मात्र स्वत: स्थापित प्रयोक्ताकेँ अनुमति दएल जाए",
        "undeletepagetext": "ई {{PLURAL:$1|page has been deleted but is|$1 pages have been deleted but are}} अखनो जोगाएल पेटारमे अछि आ फेरसँ आनल नै जा सकैए।\nई जोगाएल पेटार बीच-बीचमे साफ करबाक चाही।",
        "undelete-fieldset-title": "संशोधन सभकेँ घुराउ",
        "undeleteextrahelp": "'''''{{int:undeletebtn}}''''' केँ क्लिक करू पन्नाक पूर्ण इतिहास अनबा लेल, सभटा विकल्पबक्सासँ चेन्ह हटाउ।\n'''''{{int:undeletebtn}}''''' क्लिक करू छाँटल मौलिक आकारमे अनबा लेल, संशोधन सभकेँ अनबा लेल सम्बन्धित बक्सा सभमे चेन्ह लगाउ।",
-       "undeleterevisions": "$1{{PLURAL:$1|संशोधन|संशोधन सभ}} पेटारमे जोगाएल",
+       "undeleterevisions": "$1{{PLURAL:$1|संशोधन|संशोधनसभ}} मेटाएल",
        "undeletehistory": "जँ अहाँ पन्नाकेँ फेरसँ अनै छी, सभटा संशोधन पुरान स्तरपर संशोधित भऽ जाएत।\nमेटेलाक बाद जँ एकटा नव पन्ना ओही नामसँ बनाएल गेल, आनल संशोधन सभ पुरान इतिहासमे आएत।",
        "undeleterevdel": "पुनः अननाइ सफल नै हएत जँ ई पन्नाक शीर्ष वा संचिका संशोधनकेँ आंशिक रूपेँ मेटबैए।\nओहेन स्थितिमे, अहाँ सभसँ नव मेटाएल संशोधनक आग्रह खतम कऽ सकै छी वा सोझाँ आनि सकै छी।",
        "undeletehistorynoadmin": "ई पन्ना मेटा देल गेल अछि।\nमेटेबाक कारण नीचाँक सारांशमे देल गेल अछि, प्रयोक्ता विवरणक संग जे ऐ पन्नाकेँ मेटेबासँ पूर्व संशोधित केने छथि।\nऐ मेटाएल संशोधन सभक पाठ मात्र संचालक सभ लग उपलब्ध अछि।",
        "undeletedrevisions": "{{PLURAL:$1|1 revision|$1 revisions}} घुराएल",
        "undeletedrevisions-files": "{{PLURAL:$1|1 संशोधन|$1 संशोधन}} and {{PLURAL:$2|1 संचिका|$2 संचिका}} आनल",
        "undeletedfiles": "{{PLURAL:$1|1 संचिका|$1 संचिका सभ}} आनल",
-       "cannotundelete": "फà¥\87रसà¤\81 à¤¨à¥\88 à¤\86बि à¤¸à¤\95ल:\n$",
+       "cannotundelete": "à¤\95िà¤\9b à¤µà¤¾ à¤¸à¤­ à¤®à¥\87à¤\9fाà¤\8fल à¤µà¤¾à¤ªà¤¿à¤¸ à¤\85सफल:\n$1",
        "undeletedpage": "'''$1 के पुनर्स्थापित करल गेल अछि'''\n\nलग पास में हटाओल गेल आ पुनर्स्थापित कएल गेल पन्ना सभके जानकारी के लेल [[Special:Log/delete|हटाओल गेल लग]] देखु।",
        "undelete-header": "हालक मेटाएल पन्ना के लेल [[Special:Log/delete|हटाएल लग]] देखू।",
        "undelete-search-title": "मेटाएल गेल पृष्ठ ताकी",
        "namespace": "चेन्हासी समूह:",
        "invert": "उनटा चयन",
        "tooltip-invert": "ऐ बक्साकेँ सही करू पन्ना परिवर्तनकेँ नुकेबा लेल चयनित नामस्थानक भीतर (आ संग लागल नामस्थान जँ सही कएल अछि तखन)",
+       "tooltip-whatlinkshere-invert": "चुनल गेल नामस्थान पृष्ठसभ सँ लिङ्कसभ नुकाबैक लेल ई सन्दूकके चिन्हित करी",
        "namespace_association": "सम्बद्ध चेन्हासी",
        "tooltip-namespace_association": "ई बक्साकेँ सही करी जइसँ वार्ता आ विषय नामस्थान समाहित कएल जा सकए चुनल नामस्थानमे",
        "blanknamespace": "(मुख्य)",
        "sp-contributions-newbies-sub": "नब प्रयोक्ताकऽ लेल",
        "sp-contributions-newbies-title": "नब प्रयोक्ताकऽ योगदान",
        "sp-contributions-blocklog": "प्रतिबन्धित वृत्तलेख",
-       "sp-contributions-suppresslog": "मेटाएल प्रयोक्ता योगदान सभ",
-       "sp-contributions-deleted": "प्रयोक्ताकऽ मेटाएल योगदान सभ",
+       "sp-contributions-suppresslog": "{{GENDER:$1|प्रयोगकर्ता}} योगदान दबाबी",
+       "sp-contributions-deleted": "{{GENDER:$1|प्रयोगकर्ता}}क मेटाएल योगदान",
        "sp-contributions-uploads": "उपारोपण",
        "sp-contributions-logs": "वृत्तलेख सभ",
        "sp-contributions-talk": "वार्त्ता",
        "sp-contributions-username": "अनिकेत संकेत वा प्रयोक्तानाम:",
        "sp-contributions-toponly": "मात्र ओइ सम्पादनकेँ देखाउ जे अद्यतन संशोधन छी।",
        "sp-contributions-newonly": "मात्र ओइ सम्पादन देखाउ जे पृष्ठ निर्मित भेल अछि",
+       "sp-contributions-hideminor": "अल्प सम्पादन नुकाबी",
        "sp-contributions-submit": "ताकू",
        "whatlinkshere": "एतय कोन लिङ्क अछि",
        "whatlinkshere-title": "\"$1\" सँ सम्बन्धित पन्नासभ",
        "whatlinkshere-hideredirs": "$1 पुनर्निर्देश",
        "whatlinkshere-hidetrans": "$1 ट्रान्स्क्ल्युजन्स",
        "whatlinkshere-hidelinks": "$1 लिङ्क",
-       "whatlinkshere-hideimages": "$1 à¤«à¤¾à¤\87ल à¤\9cडà¥\80 à¤¸à¤­",
+       "whatlinkshere-hideimages": "$1 à¤«à¤¾à¤\87ल à¤²à¤¿à¤\99à¥\8dà¤\95",
        "whatlinkshere-filters": "चलनीसभ",
+       "whatlinkshere-submit": "जाए",
        "autoblockid": "स्वतःप्रतिबन्धित #$1",
        "block": "प्रयोक्ताकेँ प्रतिबन्धित करू",
-       "unblock": "प्रयोक्ताकेँ प्रतिबन्धसँ हटाउ",
+       "unblock": "प्रयोक्ताकेँ प्रतिबन्ध सँ हटाबी",
        "blockip": "{{GENDER:$1|प्रयोक्ता}}क प्रतिबन्धित करी",
        "blockip-legend": "प्रयोक्ताकेँ प्रतिबन्धित करू",
        "blockiptext": "नीचाँक आवेदनक प्रयोग कोनो खास अनिकेत वा प्रयोक्तानामक लिखैक प्रवेशकेँ प्रतिबन्धित करबा लेल करू।\nई अतत्तः करैबलाक विरुद्ध प्रयुक्त हुअए, आ एकर अनुसार [[{{MediaWiki:Policy-url}}|policy]]।\nनीचाँ स्पष्ट कारण लिखू (जेना, खास पन्नाकेँ देखबैत जतए अतत्तः कएल गेल अछि)।",
        "ipb-unblock": "प्रयोक्ता वा अनिकेतकें अप्रतिबंधित करू",
        "ipb-blocklist": "अखुनका प्रतिबंधित देखू",
        "ipb-blocklist-contribs": "$1 लेल अवदान",
-       "unblockip": "प्रयोक्ताकेँ प्रतिबन्धसँ हटाउ",
+       "ipb-blocklist-duration-left": "$1 बाकी",
+       "unblockip": "प्रयोक्ताकेँ प्रतिबन्ध सँ हटाबी",
        "unblockiptext": "पहिनेसँ प्रतिबन्धित अनिकेत वा प्रयोक्तानामकेँ लिखबाक अधिकार देबा लेल निचुलका आवेदन भरू।",
        "ipusubmit": "ई  प्रतिबन्ध हटाउ",
        "unblocked": "[[User:$1|$1]] अप्रतिबन्धित कएल गेल",
        "block-log-flags-hiddenname": "प्रयोक्तानाम नुकाएल",
        "range_block_disabled": "समूह खण्ड बनेबाक संचालकक क्षमता अशक्त कएल गेल।",
        "ipb_expiry_invalid": "खतम हेबाक समए सही नै अछि।",
+       "ipb_expiry_old": "समाप्ती समय बीत चुकल अछि।",
        "ipb_expiry_temp": "नुकाएल प्रयोक्तानाम खण्ड स्थायी हेबाक चाही।",
        "ipb_hide_invalid": "ऐ खाताकेँ द्बा नै सकलौं; ऐ मे बड्ड बेसी सम्पादन हएत।",
        "ipb_already_blocked": "\"$1\" पहिनहियेसँ प्रतिबन्धित अछि",
        "proxyblockreason": "अहाँक अनिकेत पता प्रतिबन्धित भेल अछि कारण ई सोझे-सोझ दोसराइत अछि।\nअहाँ अपन अन्तर्जाल सेवा दाता वा तकनीकी सहायकसँ सम्पर्क करू आ ऐ गम्भीर सुरक्षा समस्याक सूचना दिअ।",
        "sorbsreason": "अहाँक अनिकेत सूचित अछि सोझे-सोझ दोसराइतक रूपमे {{जालस्थल}} क डी.एन.एस.बी.एल.मे।",
        "sorbs_create_account_reason": "अहाँक अनिकेत एतए सूचित अछि खुजल दोसराइत सन डी.एन.बी.एस.एल. मे जे प्रयोग कएल जाइए {{अन्तर्जाल}} द्वारा।",
+       "xffblockreason": "एक आइपी पता जे एक्स-फरवार्डेड-डर हेडरमे मौजूद छल, या तँ अहाँक छी या ओ प्रक्सी सर्भरक अछि जेकर अहाँ प्रयोग करि रहल छी आ ओहि पर प्रतिबन्ध लगाएल गेल अछि। वास्तविक कारण छल: $1",
        "cant-see-hidden-user": "जै प्रयोक्ताकेँ अहाँ प्रतिबन्धित करऽ चाहै छी से पहिनहियेसँ प्रतिबन्धित आ अदृश्य अछि।\nकारण अहाँ लग प्रयोक्ताकेँ अदृश्य करबाक अधिकार नै अछि, अहाँ प्रयोक्ताक प्रतिबन्धकेँ देख वा सम्पादित नै कऽ सकै छी।",
        "ipbblocked": "अहाँ दोसर प्रयोक्ताकेँ प्रतिबन्धित वा अप्रतिबन्धित नै कऽ सकै छी, कारण अहाँ स्वयं प्रतिबन्धित छी",
        "ipbnounblockself": "अहाँ अपने अप्रतिबन्धित नै भऽ सकै छी",
        "lockdbsuccesstext": "दत्तनिधि प्रतिबन्ध लगाएल गेल| <br />\nमोन राखू [[Special:UnlockDB|remove the lock]]अहांक रखरखाव ख़तम भेलाक बाद ।",
        "unlockdbsuccesstext": "दत्तनिधि अप्रतिबंधित ।",
        "lockfilenotwritable": "दत्तांशनिधि प्रतिबन्ध संचिका लिखबा योग्य नै अछि।\nदत्तांशनिधिकेँ प्रतिबन्धित वा अप्रतिबन्धित करबा लेल एकरा जाल वितरक द्वारा लिखबा योग्य हेबाक चाही।",
+       "databaselocked": "डाटाबेस पहिने सँ बन्द अछि।",
        "databasenotlocked": "दत्तांशनिधि प्रतिबन्धित नै अछि।",
        "lockedbyandtime": "(द्वारा {{GENDER:$1|$1}} केँ $2 बजे $3)",
        "move-page": "$1 स्थानान्तरित करी",
        "move-page-legend": "पृष्ठ स्थानान्तरण",
-       "movepagetext": "नà¥\80à¤\9aाà¤\81à¤\95 à¤«à¥\89रà¥\8dमà¤\95 à¤ªà¥\8dरयà¥\8bà¤\97 à¤ªà¤¨à¥\8dनाà¤\95 à¤¨à¤¾à¤® à¤¬à¤¦à¤²à¤¿ à¤¦à¥\87त, à¤\8fà¤\95र à¤¸à¤­à¤\9fा à¤\87तिहासà¤\95à¥\87à¤\81 à¤¨à¤µ à¤¨à¤¾à¤®à¤\95 à¤\85नà¥\8dतरà¥\8dà¤\97त à¤°à¤¾à¤\96ि à¤¦à¥\87त।\nपà¥\81रान à¤¶à¥\80रà¥\8dषà¤\95 à¤¨à¤µ à¤ªà¤¨à¥\8dना à¤²à¥\87ल à¤\8fà¤\95à¤\9fा à¤\98à¥\81रबà¥\88बला à¤ªà¤¨à¥\8dना à¤¬à¤¨à¤¿ à¤\9cाà¤\8fत।\nà¤\85हाà¤\81 à¤\98à¥\81रबà¥\88बला à¤ªà¤¨à¥\8dनाà¤\95à¥\87à¤\81 à¤\85दà¥\8dयतन à¤\95ऽ à¤¸à¤\95à¥\88 à¤\9bà¥\80 à¤\9cà¥\87 à¤®à¥\82ल à¤¶à¥\80रà¥\8dषà¤\95पर à¤¸à¥\8dवà¤\9aालित à¤°à¥\82पà¥\87à¤\81 à¤\9cाà¤\87त à¤\85à¤\9bि।\nà¤\9cà¥\8cà¤\82 à¤\85हाà¤\81 à¤\88 à¤¨à¥\88 à¤\95रबाà¤\95 à¤¨à¤¿à¤°à¥\8dणय à¤\95रà¥\88 à¤\9bà¥\80, à¤¨à¤¿à¤¶à¥\8dà¤\9aय à¤\95रà¥\82 à¤¤à¤\95बा à¤²à¥\87ल [[Special:DoubleRedirects|double]] à¤µà¤¾\n[[Special:BrokenRedirects|broken redirects]]\nà¤\85हाà¤\81 à¤\90 à¤²à¥\87ल à¤\9cिमà¥\8dमà¥\80दार à¤\9bà¥\80 à¤\9cà¥\87 à¤¸à¤®à¥\8dबनà¥\8dधित à¤²à¤¿à¤\82à¤\95 à¤\93तà¥\88 à¤\9cाà¤\8f à¤\9cतà¤\8f à¤\93à¤\95रा à¤\9cà¥\87बाà¤\95 à¤\9aाहà¥\80।\n\nमà¥\8bन à¤°à¤¾à¤\96à¥\82 à¤\95ि à¤ªà¤¨à¥\8dना '''नà¥\88''' à¤\98सà¤\95ाà¤\89 à¤\9cà¥\8cà¤\82 à¤¨à¤µ à¤¶à¥\80रà¥\8dषà¤\95पर à¤ªà¤¹à¤¿à¤¨à¤¹à¤¿à¤¯à¥\87सà¤\81 à¤ªà¤¨à¥\8dना à¤\85à¤\9bि, à¤\86 à¤¤à¤\96नà¥\87 à¤\88 à¤\95रà¥\82 à¤\9cà¤\96न à¤\93 à¤\96ालà¥\80 à¤¹à¥\81à¤\85à¤\8f à¤µà¤¾ à¤\93 à¤\8fà¤\95à¤\9fा à¤\98à¥\81मबà¥\88बला à¤ªà¤¨à¥\8dना à¤¹à¥\81à¤\85à¤\8f à¤µà¤¾ à¤\93à¤\87 à¤ªà¤¨à¥\8dनाà¤\95 à¤\95à¥\8bनà¥\8b à¤­à¥\82तà¤\95ालà¤\95 à¤¸à¤®à¥\8dपादन à¤\87तिहास à¤¨à¥\88 à¤¹à¥\81à¤\85à¤\8f।\nà¤\8fà¤\95र à¤®à¤¾à¤¨à¥\87 à¤­à¥\87ल à¤\9cà¥\87 à¤\85हाà¤\81 à¤\95à¥\8bनà¥\8b à¤ªà¤¨à¥\8dनाà¤\95 à¤¨à¤¾à¤® à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\95ऽ à¤ªà¤¾à¤\9bाà¤\81 à¤²à¤½ à¤\9cा à¤¸à¤\95à¥\88 à¤\9bà¥\80 à¤\9cतà¤\8f à¤\8fà¤\95र à¤¨à¤¾à¤®à¤®à¥\87 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\95à¤\8fल à¤\97à¥\87ल à¤°à¤¹à¤\8f à¤\9cà¥\8cà¤\82 à¤\85हाà¤\81सà¤\81 à¤\97लतà¥\80 à¤­à¥\87ल à¤\85à¤\9bि, à¤\86 à¤\85हाà¤\81 à¤\93à¤\87 à¤ªà¤¨à¥\8dनाà¤\95à¥\87à¤\81 à¤«à¥\87रसà¤\81 à¤¦à¥\8bबारा à¤¨à¥\88 à¤²à¤¿à¤\96 à¤¸à¤\95à¥\88 à¤\9bà¥\80।\n\n\n'''à¤\9aà¥\87तà¥\8cनà¥\80!'''\nà¤\88 à¤\8fà¤\95à¤\9fा à¤²à¥\8bà¤\95पà¥\8dरिय à¤ªà¤¨à¥\8dनाà¤\95 à¤²à¥\87ल à¤\8fà¤\95à¤\9fा à¤­à¤¯à¤\82à¤\95र à¤\86 à¤¬à¤¿à¤¨à¤¾ à¤\86शाà¤\95 à¤\95à¤\8fल à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤­à¤½ à¤¸à¤\95à¥\88à¤\8f।\nà¤\86à¤\97ाà¤\81 à¤¬à¤¢à¤¼à¥\88सà¤\81 à¤ªà¤¹à¤¿à¤¨à¥\87 à¤\85हाà¤\81 à¤\88 à¤¸à¥\81निशà¥\8dà¤\9aित à¤\95रà¥\82 जे अहाँ एकर परिणाम बुझै छी।",
+       "movepagetext": "नà¥\80à¤\9aाà¤\81à¤\95 à¤«à¤°à¥\8dमà¤\95 à¤ªà¥\8dरयà¥\8bà¤\97 à¤ªà¤¨à¥\8dनाà¤\95 à¤¨à¤¾à¤® à¤¬à¤¦à¤²à¤¿ à¤¦à¥\87त, à¤\8fà¤\95र à¤¸à¤­à¤\9fा à¤\87तिहास à¤¨à¤µ à¤¨à¤¾à¤®à¤\95 à¤\85नà¥\8dतरà¥\8dà¤\97त à¤°à¤¾à¤\96ि à¤¦à¥\87त।\nपà¥\81रान à¤¶à¥\80रà¥\8dषà¤\95 à¤¨à¤µ à¤ªà¤¨à¥\8dना à¤²à¥\87ल à¤\8fà¤\95à¤\9fा à¤ªà¥\81नारà¥\8dनिरà¥\8dदà¥\87शित à¤ªà¤¨à¥\8dना à¤¬à¤¨à¤¿ à¤\9cाà¤\87त।\nà¤\85हाà¤\81 à¤ªà¥\81नरà¥\8dनिरà¥\8dदà¥\87शन à¤ªà¤¨à¥\8dनाà¤\95 à¤\85दà¥\8dयतन à¤\95ऽ à¤¸à¤\95à¥\88 à¤\9bà¥\80 à¤\9cà¥\87 à¤®à¥\82ल à¤¶à¥\80रà¥\8dषà¤\95पर à¤¸à¥\8dवà¤\9aालित à¤°à¥\82पà¥\87à¤\81 à¤\9cाà¤\87त à¤\85à¤\9bि।\nà¤\9cà¥\8cà¤\82 à¤\85हाà¤\81 à¤\88 à¤¨à¥\88 à¤\95रबाà¤\95 à¤¨à¤¿à¤°à¥\8dणय à¤\95रà¥\88 à¤\9bà¥\80, à¤¨à¤¿à¤¶à¥\8dà¤\9aय à¤\95रà¥\80 à¤¤à¤\95बा à¤²à¥\87ल [[Special:DoubleRedirects|बहà¥\81 à¤ªà¥\81नरà¥\8dनिरà¥\8dदà¥\87शन]] à¤µà¤¾\n[[Special:BrokenRedirects|तà¥\81à¤\9fल à¤ªà¥\81नरà¥\8dनिरà¥\8dदà¥\87शन]]\nà¤\85हाà¤\81 à¤\88 à¤²à¥\87ल à¤\9cिमà¥\8dमà¥\87दार à¤\9bà¥\80 à¤\9cà¥\87 à¤¸à¤®à¥\8dबनà¥\8dधित à¤²à¤¿à¤\99à¥\8dà¤\95 à¤\93तà¥\88 à¤\9cाà¤\8f à¤\9cतà¤\8f à¤\93à¤\95रा à¤\9cà¥\87बाà¤\95 à¤\9aाहà¥\80।\n\nमà¥\8bन à¤°à¤¾à¤\96à¥\82 à¤\95ि à¤ªà¤¨à¥\8dना <strong>नà¥\88</strong> à¤¸à¥\8dथानानà¥\8dतरित à¤\9cà¥\8cà¤\82 à¤¨à¤µ à¤¶à¥\80रà¥\8dषà¤\95पर à¤ªà¤¹à¤¿à¤¨à¤¹à¤¿à¤¯à¥\87सà¤\81 à¤ªà¤¨à¥\8dना à¤\85à¤\9bि, à¤\86 à¤¤à¤\96नà¥\87 à¤\88 à¤\95रà¥\80 à¤\9cà¤\96न à¤\93 à¤\96ालà¥\80 à¤¹à¥\81à¤\85à¤\8f à¤µà¤¾ à¤\93 à¤\8fà¤\95à¤\9fा à¤ªà¥\81नरà¥\8dनिरà¥\8dदà¥\87शित à¤ªà¤¨à¥\8dना à¤¹à¥\81à¤\85à¤\8f à¤µà¤¾ à¤\93à¤\87 à¤ªà¤¨à¥\8dनाà¤\95 à¤\95à¥\8bनà¥\8b à¤­à¥\82तà¤\95ालà¤\95 à¤¸à¤®à¥\8dपादन à¤\87तिहास à¤¨à¥\88 à¤¹à¥\81à¤\85à¤\8f।\nà¤\8fà¤\95र à¤®à¤¾à¤¨à¥\87 à¤­à¥\87ल à¤\9cà¥\87 à¤\85हाà¤\81 à¤\95à¥\8bनà¥\8b à¤ªà¤¨à¥\8dनाà¤\95 à¤¨à¤¾à¤® à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\95ऽ à¤ªà¤¾à¤\9bाà¤\81 à¤²à¤½ à¤\9cा à¤¸à¤\95à¥\88 à¤\9bà¥\80 à¤\9cतà¤\8f à¤\8fà¤\95र à¤¨à¤¾à¤®à¤®à¥\87 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\95à¤\8fल à¤\97à¥\87ल à¤°à¤¹à¤\8f à¤\9cà¥\8cà¤\82 à¤\85हाà¤\81सà¤\81 à¤\97लतà¥\80 à¤­à¥\87ल à¤\85à¤\9bि, à¤\86 à¤\85हाà¤\81 à¤\93à¤\87 à¤ªà¤¨à¥\8dनाà¤\95à¥\87à¤\81 à¤«à¥\87रसà¤\81 à¤¦à¥\8bबारा à¤¨à¥\88 à¤²à¤¿à¤\96 à¤¸à¤\95à¥\88 à¤\9bà¥\80।\n\n\n<strong>à¤\9aà¥\87तावनà¥\80!</strong>\nà¤\88 à¤\8fà¤\95à¤\9fा à¤²à¥\8bà¤\95पà¥\8dरिय à¤ªà¤¨à¥\8dनाà¤\95 à¤²à¥\87ल à¤\8fà¤\95à¤\9fा à¤­à¤¯à¤\82à¤\95र à¤\86 à¤¬à¤¿à¤¨à¤¾ à¤\86शाà¤\95 à¤\95à¤\8fल à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤­à¤½ à¤¸à¤\95à¥\88à¤\8f।\nà¤\86à¤\97ाà¤\81 à¤¬à¤¢à¥\88सà¤\81 à¤ªà¤¹à¤¿à¤¨à¥\87 à¤\85हाà¤\81 à¤\88 à¤¸à¥\81निशà¥\8dà¤\9aित à¤\95रà¥\80 जे अहाँ एकर परिणाम बुझै छी।",
        "movepagetext-noredirectfixer": "नीचाँक फॉर्मक प्रयोग पन्नाक नाम बदलि देत, एकर सभटा इतिहासकेँ नव नामक अन्तर्गत राखि देत।\nपुरान शीर्षक नव पन्ना लेल एकटा घुरबैबला पन्ना बनि जाएत।\nनिश्चय करू तकबा लेल [[Special:DoubleRedirects|double]] वा[[Special:BrokenRedirects|broken redirects]]।\nअहाँ ऐ लेल जिम्मीदार छी जे सम्बन्धित लिंक ओतै जाए जतए ओकरा जेबाक चाही।\n\nमोन राखू कि पन्ना '''नै''' घसकत जौं नव शीर्षकपर पहिनहियेसँ पन्ना अछि, आ तखने ई करू जखन ओ खाली हुअए वा ओ एकटा घुमबैबला पन्ना हुअए वा ओइ पन्नाक कोनो भूतकालक सम्पादन इतिहास नै हुअए।\nएकर माने भेल जे अहाँ कोनो पन्नाक नाम परिवर्तन कऽ पाछाँ लऽ जा सकै छी जतए एकर नाममे परिवर्तन कएल गेल रहए जौं अहाँसँ गलती भेल अछि, आ अहाँ ओइ पन्नाकेँ फेरसँ दोबारा नै लिख सकै छी।\n\n\n'''चेतौनी!'''\nई एकटा लोकप्रिय पन्नाक लेल एकटा भयंकर आ बिना आशाक कएल परिवर्तन भऽ सकैए।\nआगाँ बढ़ैसँ पहिने अहाँ ई सुनिश्चित करू जे अहाँ एकर परिणाम बुझै छी।",
        "movepagetalktext": "सम्बन्धित चौबटिया पन्ना स्वचालित रूपेँ घसकत एकर संग '''जौं:'''\n*एकटा खाली-नै चौबटिया पन्ना पहिनहियेसँ नव नामक संग अछि, वा\n*अहाँ नीचाँक बॉक्स टिक हटा दी।\n\nताइ परिस्थितिमे, अहाँकेँ अपनेसँ पन्नाकेँ, आवश्यकतानुसार, घसकाबऽ वा मिज्झर करऽ पड़त।",
        "moveuserpage-warning": "'''चेतौनी!'''अहाँ एकटा प्रयोक्ता पन्ना घसका रहल छी | मोन राखू कि खाली पन्ना घसकत आ प्रयोक्ताक नाम ''नै'' बदलत ।",
        "movenotallowedfile": "अहाँकेँ संचिका सभकेँ घसकेबाक अधिकार नै अछि।",
        "cant-move-user-page": "अहाँकेँ प्रयोक्ता पन्ना सभकेँ घसकेबाक अधिकार नै अछि (उपपन्ना सभकेँ छोड़ि कऽ)।",
        "cant-move-to-user-page": "अहाँकेँ कोनो पन्नाकेँ प्रयोक्ता पन्ना लग घसकेबाक अधिकार नै अछि (प्रयोक्ता उपपन्ना लग छोड़ि कऽ)।",
-       "newtitle": "नव शीर्षकपर:",
+       "newtitle": "नव शीर्षक:",
        "move-watch": "लिङ्क पन्ना आ लक्षित पन्ना देखी",
        "movepagebtn": "नाम परिवर्तन करी",
        "pagemovedsub": "घसकल",
        "movenosubpage": "अहि पन्ना कऽ कोनो उप पन्ना नहि अछि।",
        "movereason": "कारण:",
        "revertmove": "फेरसँ वएह",
-       "delete_and_move_text": "==हटाबैक जरूरत==\nलक्ष्य पृष्ठ \"[[:$1]]\" पहिने सें अस्तित्व में अछि. \nनाम के बदलहि ले की अहां एकरा हटाबय चाहैत छी ?",
+       "delete_and_move_text": "लक्ष्य पृष्ठ \"[[:$1]]\" पहिने सँ अस्तित्वमे अछि।\nअहाँ एकरा स्थानान्तरण करै लेल एकरा मेटाबैलेल चाहै छी?",
        "delete_and_move_confirm": "हँ, पन्ना मेटाउ",
        "delete_and_move_reason": "\"[[$1]]\" सँ घसकेबा लेल जगह बनेबा लेल मेटाएल गेल",
        "selfmove": "स्रोत आ लक्ष्यक शीर्षक एक अछि;\nपृष्ठ अप्पन ठाम पर स्थानांतरित नहि भ सकत.",
        "immobile-target-namespace-iw": "अंतरविकी लिँक पन्ना घसकेबा लेल उचित लक्ष्य नै अछि।",
        "immobile-source-page": "अहि पृष्ठ के अहां कतौ नहि ल जा सकब",
        "immobile-target-page": "ओइ लक्ष्य शीर्षक धरि नै घसका सकल।",
+       "bad-target-model": "वाञ्छित स्थान भिन्न सामग्री नमूनाक प्रयोग करैत अछि। $1 के बदलि $2 नै केल जा सकैत अछि।",
        "imagenocrossnamespace": "संचिकाकेँ गएर संचिका नामस्थान धरि नै लए जा सकल।",
        "nonfile-cannot-move-to-file": "गएर संचिकाकेँ  संचिका नामस्थान धरि नै लए जा सकल।",
        "imagetypemismatch": "नव संचिका विस्तारक अपन प्रकारसँ मेल नै खाइए।",
        "move-leave-redirect": "एक पुनर्निर्देशन पाछा छोडी",
        "protectedpagemovewarning": "''' चेतौनी: ई पन्ना संरक्षित अछि से खाली संचालन अधिकारयुक्त प्रयोक्ता एकरा घुसका सकैत छथि।'''\nनव वृतलेख उल्लेख नीचाँ सन्दर्भ लेल देल जा रहल अछि:",
        "semiprotectedpagemovewarning": "'''नोट:''' ई पन्ना संरक्षित अछि से खाली पंजीकृत प्रयोक्ता एकरा घुसका सकैत छथि।\nनव वृतलेख उल्लेख नीचाँ सन्दर्भ लेल देल जा रहल अछि:",
-       "move-over-sharedrepo": "[[:$1]] अछि एकटा साझी बखारीमे। कोनो संचिकाकेँ ऐ नामसँ अनलापर साझीबला एकटा संचिका मेटा जाएत।",
+       "move-over-sharedrepo": "साझा बखारीमे [[:$1]] अछि। कोनो सञ्चिकाके ई नाम सँ आनलापर एकटा सञ्चिका मेटा जाइत।",
        "file-exists-sharedrepo": "साझी बखारीमे ऐ नामसँ पहिनहियेसँ एकटा संचिका अछि।\nकृपा कऽ दोसर नाम चुनू।",
-       "export": "पनà¥\8dना à¤¸à¤­à¤\95à¥\87à¤\81 à¤ªà¤ à¤¾à¤\89",
+       "export": "पà¥\83षà¥\8dठसभ à¤¨à¤¿à¤°à¥\8dयात à¤\95रà¥\80",
        "exporttext": "अहाँ पाठ आ कोनो पन्ना/ वा पन्ना-सभक सम्पादन इतिहासकेँ दोसर ठाम कोनो एक्स.एम.एल. संचिकामे लपेट कऽ पठा सकै छी।\nई कोनो दोसर विकीमे मीडियाविकीक प्रयोग कऽ [[Special:Import|import page]] द्वारा आयात कएल जा सकैए।\n\nपन्ना सभक निर्यात लेल, नीचाँक पाठ बक्शामे शीर्षक सभ भरू, प्रति पाँती एक शीर्षक, आ चुनू जे अहाँ अखुनका आ पहिलुका सभटा संशोधन राखऽ चाहै छी, पन्ना इतिहास पाँतीक संग, आकि अखुनका संशोधन पछिला सम्पादनक सूचनाक संग।\n\nबादबला स्थितिमे अहाँ एकटा लागिक प्रयोग कऽ सकै छी, जेना \"[[{{MediaWiki:Mainpage}}]]\" पन्ना लेल [[{{#Special:Export}}/{{MediaWiki:Mainpage}}]]।",
        "exportall": "पन्ना सभकेँ निर्यात करू",
        "exportcuronly": "अखुनका संशोधन मात्र लिअ, पूरा इतिहास नै।",
        "export-download": "संचिका रूपमे संरक्षण करू",
        "export-templates": "सभटा नमूना शामिल करू",
        "export-pagelinks": "लागिबला पन्ना सभकेँ एतेक तह धरि राखू:",
+       "export-manual": "स्वयं सँ पृष्ठ जोडी:",
        "allmessages": "प्रणालीक सन्देश",
        "allmessagesname": "नाम",
        "allmessagesdefault": "पूर्वनिर्धारित सन्देश पाठ",
        "djvu_page_error": "डेजावू पन्ना सकक बाहर अछि",
        "djvu_no_xml": "डेजावू संचिकाक एक्स.एम.एल. नै आनि सकलौं",
        "thumbnail-temp-create": "अस्थायी थम्बनेल फाइल बनाबए में असफल",
+       "thumbnail-dest-create": "थम्बनेलके ई स्थान पर सुरक्षित नै केल जा सकैए।",
        "thumbnail_invalid_params": "अमान्य लघुचित्र परिमिति",
+       "thumbnail_toobigimagearea": "सञ्चिका जेकर आकार $1 सँ बेसी अछि।",
        "thumbnail_dest_directory": "लक्ष्य निर्देशिका नै बना सकल",
        "thumbnail_image-type": "चित्र प्रकार समर्थित नै अछि",
        "thumbnail_gd-library": "अपूर्ण जी.डी.पुस्तकालय विन्यास: प्रकार्य $1 अनुपस्थित",
        "import-interwiki-text": "एकटा विकी आ पन्ना शीर्षक आनैलेल चुनू।\nसंशोधन तिथि आ सम्पादकक नाम सुरक्षित रहत।\nसभटा ट्रान्सविकी आयात क्रिया सम्प्रवेशित [[Special:Log/import|आयात लग]] पर रहत।",
        "import-interwiki-sourcewiki": "मूल विकि:",
        "import-interwiki-sourcepage": "मूल पन्ना:",
-       "import-interwiki-history": "à¤\85à¤\8f à¤ªà¤¨à¥\8dनाà¤\95 à¤¸à¤­à¤\9fा à¤\87तिहास à¤¸à¤\82शà¥\8bधनà¤\95 à¤¦à¥\8dवितà¥\80यà¤\95 à¤¬à¤¨à¤¾à¤\89",
-       "import-interwiki-templates": "सभà¤\9fा à¤¨à¤®à¥\82ना à¤¶à¤¾à¤®à¤¿à¤² à¤\95रà¥\82",
-       "import-interwiki-submit": "à¤\86नà¥\82",
+       "import-interwiki-history": "à¤\88 à¤ªà¤¨à¥\8dनाà¤\95 à¤¸à¤­à¤\9fा à¤\87तिहास à¤¸à¤\82शà¥\8bधनà¤\95 à¤\95पà¥\80 à¤\95रà¥\80",
+       "import-interwiki-templates": "सभà¤\9fा à¤\86à¤\95à¥\83ति à¤¶à¤¾à¤®à¤¿à¤² à¤\95रà¥\80",
+       "import-interwiki-submit": "à¤\86यात",
        "import-mapping-default": "पूर्व निर्धारित स्थान सभ पर आयात करी",
        "import-mapping-namespace": "कोनो नामस्थान पर आयात करी",
        "import-mapping-subpage": "निम्न लिखित पृष्ठ के उपपृष्ठ के रूप में आयात करी:",
        "import-nonewrevisions": "सभटा संशोधन पहिनहियेसँ आयातित अछि।",
        "xml-error-string": "$1 पाँतीपर $2, col $3 (byte $4): $5",
        "import-upload": "एक्स.एम.एल. दत्तांश उपारोपित करू",
-       "import-token-mismatch": "à¤\8fà¤\95 à¤\89à¤\96राहाà¤\95 à¤¦à¤¤à¥\8dताà¤\82श à¤\96तम à¤­à¤½ à¤\97à¥\87ल।\nफà¥\87रसà¤\81 à¤ªà¥\8dरयास à¤\95रà¥\82।",
+       "import-token-mismatch": "सà¥\87शन à¤¡à¤¾à¤\9fा à¤¨à¤·à¥\8dà¤\9f à¤­à¥\87ल।\nà¤\85हाà¤\81 à¤¸à¤¾à¤¯à¤¦ à¤²à¤\97 à¤\86à¤\89à¤\9f à¤\95 à¤\97à¥\87ल à¤\9bà¥\80।<strong>à¤\95à¥\83पया à¤\9cाà¤\81à¤\9a à¤\95रà¥\80 à¤\95à¥\80 à¤\85हाà¤\81 à¤¸à¤®à¥\8dपà¥\8dरवà¥\87शित à¤\9bà¥\80</strong>।\nयदि à¤\8fà¤\95र à¤¬à¤¾à¤¦à¥\8b à¤¸à¤«à¤² à¤¨à¥\88 à¤­à¥\87ल à¤¤à¤\81 à¤\95à¥\83पया [[Special:UserLogout|लà¤\97 à¤\86à¤\89à¤\9f]] à¤\95रि à¤ªà¥\81नà¤\83 à¤¸à¤®à¥\8dपà¥\8dरवà¥\87श à¤\95रà¥\80।",
        "import-invalid-interwiki": "विशिष्ट विकीसँ आयात नै कऽ सकै छी।",
        "import-error-edit": "\"$1\" पन्ना आयातित नै कएल गेल अछि कारण अहाँकेँ एकरा सम्पादित करबाक अधिकार नै अछि।",
        "import-error-create": "\"$1\" पन्ना आयातित नै कएल गेल अछि कारण अहाँकेँ एकरा निर्माण करबाक अधिकार नै अछि।",
        "import-error-interwiki": "पृष्ठ \"$1\" आयात नै केल गेल कियाकि एकर नाम अन्तरविकि जडी बनाबै के लेल आरक्षित अछि।",
        "import-error-special": "पृष्ठ \"$1\" आयात नै केल गेल कियाकि इ एक एहन विशेष नामस्थान के अन्तर्गत आबैत अछि जे में पृष्ठ पृष्ठ नै बनाएल जा सकैत अछि।",
        "import-error-invalid": "पृष्ठ \"$1\" आयात नै केल गेल कियाकि इ आयात पश्चात जे नाम रहत यो इ विकी पर अमान्य अछि।",
+       "import-error-unserialize": "पृष्ठ \"$1\" क संशोधन $2के क्रम सँ हटाएल नै जा सकल। संशोधनक बारेमे बताएल गेल अछि की सामग्री नमूना $3 क क्रम $4 के रूप प्रयोगमे लाबल गेल छल।",
        "import-options-wrong": "गलत {{PLURAL:$2|विकल्प}}: <nowiki>$1</nowiki>",
        "import-rootpage-invalid": "दयाल गेल उपसर्ग पन्ना शीर्षक अमान्य अछि ।",
        "import-rootpage-nosubpage": "दयाल गेल उपसर्ग पन्ना \"$1\" के नामस्थान में उप-पन्ना नै बनाबाल जा सकएत अछि ।",
        "tooltip-pt-preferences": "{{GENDER:|अहाँक}} अभिरुचीसभ",
        "tooltip-pt-watchlist": "पन्नासभ जेकर परिवर्तन पर अहाँक नजरि अछि",
        "tooltip-pt-mycontris": "{{GENDER:|अहाँक}} योगदानक सूची",
+       "tooltip-pt-anoncontribs": "ई आइपी पता सँ सम्पादनक सूची",
        "tooltip-pt-login": "अहाँक खाता खोलक लेल प्रोत्साहित कएल जाइत अछि; मुदा ई अनिवार्य नै अछि",
        "tooltip-pt-logout": "फेर आयब",
        "tooltip-pt-createaccount": "अहाँक खाता खोलक लेल प्रोत्साहित कएल जाइत अछि; मुदा ई अनिवार्य नै अछि",
        "tooltip-t-whatlinkshere": "सभ विकी-पन्नाक सूची जकर एतय लिङ्क अछि",
        "tooltip-t-recentchangeslinked": "ई पृष्ठक लगक पन्नामे भेल नव परिवर्तनसभ",
        "tooltip-feed-rss": "ऐ पन्ना लेल आर.एस.एस. सूचना",
-       "tooltip-feed-atom": "à¤\90 à¤ªà¤¨à¥\8dना à¤²à¥\87ल à¤\85णà¥\81 à¤¸à¤®à¤¦à¤¿à¤¯à¤¾",
+       "tooltip-feed-atom": "à¤\88 à¤ªà¥\83षà¥\8dठà¤\95 à¤\8fà¤\9fम à¤«à¤¿à¤¡",
        "tooltip-t-contributions": "ई {{GENDER:$1|प्रयोक्ताक}} योगदानक सूची देखी",
-       "tooltip-t-emailuser": "ई प्रयोगकर्ताक ई-पत्र पठाबी",
+       "tooltip-t-emailuser": "{{GENDER:$1|ई प्रयोगकर्ता}}के इमेल भेजी",
        "tooltip-t-info": "ई पृष्ठ के सम्बन्धमें आर बैंसी जानकारी",
        "tooltip-t-upload": "चित्र आकि मिडिया फाइल अपलोड करी",
-       "tooltip-t-specialpages": "सभà¤\9fा à¤µà¤¿à¤¶à¥\87ष à¤ªà¤¨à¥\8dनाक सूची",
+       "tooltip-t-specialpages": "समà¥\8dपà¥\82रà¥\8dण à¤µà¤¿à¤¶à¥\87ष à¤ªà¤¨à¥\8dनासभक सूची",
        "tooltip-t-print": "ई पृष्ठक छपैबला रूप",
        "tooltip-t-permalink": "पृष्ठक ई संस्करणक स्थायी लिङ्क",
        "tooltip-ca-nstab-main": "सामग्री वाला पृष्ठ देखी",
        "tooltip-ca-nstab-category": "श्रेणी पन्ना देखी",
        "tooltip-minoredit": "एकरा मामली सम्पादन चिन्हित करू",
        "tooltip-save": "अपन परिवर्तन सुरक्षित करी",
+       "tooltip-publish": "परिवर्तन प्रकाशित करी",
        "tooltip-preview": "परिवर्तनक प्रदर्शन, संरक्षण सँ पहिने एकर प्रयोग करी!",
        "tooltip-diff": "ई पाठमे अहाँद्वारा कएल परिवर्तन देखी।",
        "tooltip-compareselectedversions": "ऐ पन्नाक दू टा चयन कएल संशोधनक बीचक अन्तर देखू",
        "pageinfo-article-id": "पन्ना आई॰डी॰",
        "pageinfo-language": "पन्ना सामग्री भाषा",
        "pageinfo-content-model": "पन्ना सामग्री के नमूना",
+       "pageinfo-content-model-change": "परिवर्तन",
        "pageinfo-robot-policy": "बोटद्वारा अनुक्रमण",
        "pageinfo-robot-index": "मान्य",
        "pageinfo-robot-noindex": "अमान्य",
        "pageinfo-watchers": "जानकारक संख्या",
+       "pageinfo-visiting-watchers": "पृष्ठ देखनिहारक सङ्ख्या जे हालक सम्पादनमे आबए।",
        "pageinfo-few-watchers": "$1 स कम ध्यान दीए {{PLURAL:$1|वाला}}",
+       "pageinfo-few-visiting-watchers": "भ सकैत अछि या नै भी कि कियो ई हाल क सम्पादनद्वारा कोनो प्रयोक्ता आएल होए।",
        "pageinfo-redirects-name": "ई पन्नाक पुनर्निर्देशसभ सङ्ख्या",
        "pageinfo-subpages-name": "इ पन्ना के उप-पन्ना",
        "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|पुनर्निर्देश}}; $3 {{PLURAL:$3|ग़ैर-पुनर्निर्देश}})",
        "pageinfo-category-files": "फाइल सभके संख्या",
        "markaspatrolleddiff": "जाँच सम्पन्न करी",
        "markaspatrolledtext": "देखि लेल गेल, एहन चिन्ह लगाऊ",
+       "markaspatrolledtext-file": "ई फाइल संस्करणके जांचल चिन्हित करी",
        "markedaspatrolled": "जाँच सम्पन्न करी",
        "markedaspatrolledtext": "[[:$1]]क चयनित अवतरणक जाँच सम्पन्न भेल।",
        "rcpatroldisabled": "हालमे भेल परिवर्तनक परीक्षण अक्षम अछि",
        "svg-long-error": "अमान्य एस॰वी॰जी फ़ाइल: $1",
        "show-big-image": "पूर्ण आनन्तर्य",
        "show-big-image-preview": "ऐ पूर्वदृश्यक आकार: $1.",
+       "show-big-image-preview-differ": "पूर्वावलोकन $3 क आकार $2 फाइल: $1",
        "show-big-image-other": "दोसर {{PLURAL:$2|resolution|resolutions}}: $1।",
        "show-big-image-size": "$1 × $2 चित्राणु",
        "file-info-gif-looped": "घुरियाएल",
        "variantname-zh-sg": "sg",
        "variantname-zh-my": "my",
        "variantname-zh": "zh",
-       "metadata": "पà¥\8dरदतà¥\8dताà¤\82श",
+       "metadata": "मà¥\87à¤\9fाडà¥\87à¤\9fा",
        "metadata-help": "ई फाइल अतिरिक्त सूचना दैत अछि, सम्भवतः ई अंकीय कैमरा वा स्कैनर द्वारा बनाएल वा अंकण कए जोड़ल गेल अछि।\nजौं फाइलकेँ मूल रूपसँ परिवर्धित कएल गेल हएत तँ किछु विवरण पूर्ण रूपसँ परिवर्धित फाइलमे नै देखाएल गेल हएत।",
        "metadata-expand": "बढ़ाओल विवरण देखाउ।",
        "metadata-collapse": "विस्तृत विवरण नुकाउ",
        "confirm-watch-top": "ऐ पन्नाकेँ अपन साकांक्ष सूचीमे जोड़ू",
        "confirm-unwatch-button": "ठीक अछि",
        "confirm-unwatch-top": "ऐ पन्नाकेँ हमर साकांक्ष सूचीसँ हटाउ",
+       "confirm-rollback-button": "ठीक अछि",
+       "confirm-rollback-top": "ई पृष्ठ सम्पादन पूर्ववत करी?",
        "quotation-marks": "\"$1\"",
        "imgmultipageprev": "← पहिलुका पृष्ठ",
        "imgmultipagenext": "अगुलका पृष्ठ →",
        "watchlistedit-raw-done": "अहाँक साकांक्ष-सूची अद्यतन कएल गेल।",
        "watchlistedit-raw-added": "{{PLURAL:$1|1 शीर्षक छल|$1शीर्षक सभ रहए}} जोड़ल गेल:",
        "watchlistedit-raw-removed": "{{PLURAL:$1|1 शीर्षक छल|$1शीर्षक सभ रहए}} हटाएल गेल:",
-       "watchlistedit-clear-title": "साà¤\95ाà¤\82à¤\95à¥\8dष-सà¥\82à¤\9aà¥\80 à¤®à¥\87à¤\9fाà¤\93ल à¤\97à¥\87ल",
+       "watchlistedit-clear-title": "धà¥\8dयानसà¥\82à¤\9aà¥\80 à¤\96ालà¥\80 à¤\95रà¥\80",
        "watchlistedit-clear-legend": "साकांक्ष-सूची मेटाउ",
        "watchlistedit-clear-explain": "एही ठाम रहल सभ शिर्षक अहाँक साकांक्ष-सूची से मेटा जाएत",
        "watchlistedit-clear-titles": "शीर्षक",
        "watchlisttools-edit": "साकांक्षसूची देखी आ सम्पादित करी",
        "watchlisttools-raw": "काँच साकांक्षसूची सम्पादित करी",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|वार्ता]])",
+       "timezone-local": "स्थानीय",
        "duplicate-defaultsort": "'''चेतौनी:''' पूर्वनिर्धारित विन्यास चाभी \"$2\" पहिलुका पूर्वनिर्धारित विन्यास चाभी \"$1\" केँ खतम करैए।",
        "duplicate-displaytitle": "<strong>चेतना:</strong> शीर्षक दिखाबु \"$2\" पूर्व दिखाएल गेल शीर्षक \"$1\" पे हाबी भऽ रहल अछि।",
+       "restricted-displaytitle": "<strong>चेतावनी :</strong> प्रदर्शित शीर्षक \"$1\" नजरअन्दाज केल गेल अछि, कियाकि ई वास्तविक शीर्षक सँ नै मिलैत अछि।",
        "invalid-indicator-name": "<strong>त्रुटि:</strong> पन्ना स्थिति सुचीत <code>नाम</code> गुण खाली नै रहना चाही।",
        "version": "संस्करण",
        "version-extensions": "संस्करणक आगाँ",
        "version-ext-colheader-description": "विवरण",
        "version-ext-colheader-credits": "लेखक",
        "version-license-title": "$1 के लेल अधिकार",
+       "version-license-not-found": "ई एक्सटेन्सनक लेल कोनो विस्तृत लाइसेन्स जानकारी नै भेट सकल।",
        "version-credits-title": "$1 के लेल श्रेय",
+       "version-credits-not-found": "ई एक्सटेन्सनक लेल कोनो विस्तृत श्रेय जानकारी नै भेट सकल।",
        "version-poweredby-credits": "ई विकी चालित अछि '''[https://www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2",
        "version-poweredby-others": "आन",
        "version-poweredby-translators": "translatewiki.net अनुवादक",
        "version-libraries": "स्थापित लाइब्रेरी",
        "version-libraries-library": "लाइब्रेरी",
        "version-libraries-version": "संस्करण",
-       "redirect": "अनुप्रेषित करु फ़ाइल, प्रयोगकर्ता, वा संशोधन पहीचान के आधार में",
-       "redirect-summary": "ई विशेष पन्ना फ़ाइलनाम प्रदान करै पे फ़ाइल नाम के, पन्न आइ॰दी अथवा अवतरण आइ॰दी दुनु पे पन्ना के,आर साथी सदस्य आइ॰दी दुनु पे सदस्य पन्ना के पुनर्प्रेषित करएत अछि । उदाहरण: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], या [[{{#Special:Redirect}}/user/101]]।",
+       "version-libraries-license": "अनुज्ञापत्र",
+       "version-libraries-description": "विवरण",
+       "version-libraries-authors": "लेखक",
+       "redirect": "फाइल, सदस्य, पृष्ठ, अवतरण या लग आइडीद्वारा अनुप्रेषित",
+       "redirect-summary": "ई विशेष पन्ना फाइलनाम प्रदान करै पर फाइल नामके, पन्न आइडी अथवा अवतरण आइडी दुनु पर पन्नाके, आर साथी सदस्य आइडी दुनु पर सदस्य पन्नाके पुनर्प्रेषित करैत अछि । उदाहरण: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], या [[{{#Special:Redirect}}/user/101]]।",
        "redirect-submit": "जाए",
        "redirect-lookup": "ताकू:",
        "redirect-value": "मूल्य:",
        "redirect-page": "पन्ना आई॰डी॰",
        "redirect-revision": "पन्ना अवतरण संख्या",
        "redirect-file": "फाइल नाम",
+       "redirect-logid": "प्रवेश आइडी",
        "redirect-not-exists": "बैनर नैं मिल्ल",
        "fileduplicatesearch": "द्वितीयक संचिका ताकू",
        "fileduplicatesearch-summary": "हैश मानक आधारपर द्वितीयक संचिका ताकू।",
        "intentionallyblankpage": "ई पन्ना पलानि कऽ खाली छोड़ल गेल।",
        "external_image_whitelist": "# ऐ पाँतीकेँ एकदम ओहिना छोड़ि दियौ जेना ई अछि<pre>\n# सामान्य वैचारिक खण्ड नीचाँ राखू (// क बीचक खण्ड मात्र)।\n# ई सभ बाहरी (ताजाताजी लागि) चित्रक सार्वत्रिक विभव संकेतसँ मेल खुआएल जाएत\n# ओ सभ जे मेल खाएत से चित्रक रूपमे प्रदर्शित हएत, नै तँ खाली एकटा चित्रक लागि देखाएल जाएत\n# # सँ शुरू भेल पाँती टिप्पणीक रूपमे देखल जाएत।\n# ई ब्रह्मक्षर-लघ्वक्षरक फेरासँ स्वतंत्र अछि।\n\n# सभटा सामान्य कथन ऐ पाँतीसँ ऊपर राखू। ऐ पाँतीकेँ एकदम ओहिना छोड़ू जेना ई अछि </pre>",
        "tags": "मान्य परिवर्तन चेन्ह सभ",
-       "tag-filter": "[[Special:Tags|Tag]] छन्ना:",
+       "tag-filter": "[[Special:Tags|ट्याग]] छन्ना:",
        "tag-filter-submit": "चलनी",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|ट्याग|ट्यागसभ}}]]: $2)",
+       "tag-mw-contentmodelchange": "सामग्री परिवर्तन लग",
        "tags-title": "चेन्ह सभ",
        "tags-intro": "ई पन्ना चेन्ह सभकेँ सूचित करैए जे तंत्रांश सम्पादनसँ चिन्हित करए, आ ओकर अर्थ सेहो।",
        "tags-tag": "चेन्हक नाम",
        "tags-actions-header": "क्रिया सभ",
        "tags-active-yes": "हँ",
        "tags-active-no": "नै",
-       "tags-source-extension": "à¤\8fà¤\95à¥\8dसà¤\9fà¥\87नà¥\8dसनद्वारा परिभाषित",
+       "tags-source-extension": "सफà¥\8dà¤\9fवà¥\87यरद्वारा परिभाषित",
        "tags-source-manual": "प्रयोक्तासभ आर बोटद्वारा नियमानुसार लागू",
        "tags-source-none": "आब प्रयोग में नै",
        "tags-edit": "सम्पादन करी",
        "tags-deactivate": "निष्क्रिय करी",
        "tags-hitcount": "$1 {{PLURAL:$1|परिवर्तन|परिवर्तनसभ}}",
        "tags-manage-no-permission": "अहाँकेँ पन्ना घसकेबाक अधिकार नै अछि।",
+       "tags-manage-blocked": "अहाँ प्रतिबन्धित रहैत समय ट्यागमे कोनो जोडए या हटाबैक कार्य नै करि सकैत छी।",
        "tags-create-heading": "एकटा नयाँ विकि-समूह बनाबु",
        "tags-create-explanation": "पुनः निर्धारित रूप से, नवनिर्मित टैग प्रयोगकर्तासभ आर बॉट के लेल हाजीर राहत।",
        "tags-create-tag-name": "चेन्हक नाम:",
        "tags-edit-manage-link": "ट्याग व्यवस्थापन",
        "tags-edit-revision-selected": "[[:$2]] {{PLURAL:$1|क|के}} चयनित अवतरण:",
        "tags-edit-logentry-selected": "{{PLURAL:$1|चुनल वृत्तलेख घटना|चुनल वृत्तलेख घटना सभ}}:",
-       "tags-edit-existing-tags-none": "''कोनो नै''",
+       "tags-edit-existing-tags-none": "<em>कोनो नै</em>",
        "tags-edit-new-tags": "नव ट्याग:",
        "tags-edit-add": "इ ट्यागसभ जोडी:",
        "tags-edit-remove": "इ ट्यागसभ हटाबी:",
        "tags-edit-chosen-placeholder": "किछु ट्याग चुनी",
        "tags-edit-chosen-no-results": "ए नामक ट्याग नै भेटल",
        "tags-edit-reason": "कारण:",
+       "tags-edit-revision-submit": "बदलाव जोडी {{PLURAL:$1|ई अवतरण|$1 अवतरण}}मे",
+       "tags-edit-logentry-submit": "बदलाव जोडी {{PLURAL:$1|ई लग प्रवक्ति|$1 लग प्रवक्तिसभ}}मे",
+       "tags-edit-success": "बदलाव सफलता लागू भेल।",
+       "tags-edit-failure": "परिवर्तन नै जोडल जा सकैत अछि: $1",
+       "tags-edit-nooldid-title": "अमान्य लक्ष्य संशोधन",
        "comparepages": "पन्ना सभक तुलना करू",
        "compare-page1": "पन्ना १",
        "compare-page2": "पन्ना २",
        "compare-title-not-exists": "जे शीर्षक अहाँ कहलौं से अछिये नै।",
        "compare-revision-not-exists": "जे संशोधन अहाँ कहलौं से अछिये नै।",
        "dberr-problems": "दुखी छी! ई जालस्थल तकनीकी समस्या अनुभव कऽ अछि।",
-       "dberr-again": "à¤\95िà¤\9bà¥\81 à¤\95ाल à¤¬à¤¾à¤\9f à¤¤à¤¾à¤\95à¥\82 à¤\86 à¤«à¥\87रसà¤\81 à¤­à¤¾à¤°à¤¿à¤¤ à¤\95रà¥\82।",
+       "dberr-again": "à¤\95िà¤\9bà¥\81 à¤\95ाल à¤°à¥\81à¤\95à¥\80 à¤\86 à¤«à¥\87रसà¤\81 à¤\9cानà¤\95ारà¥\80 à¤­à¤°à¥\80।",
        "dberr-info": "(दत्तनिधि वितरकके सम्पर्क नै कऽ सकल: $1)",
        "dberr-info-hidden": "(दत्तनिधि वितरकके सम्पर्क नै कऽ सकल: $1)",
        "dberr-usegoogle": "ऐ बीचमे अहाँ गूगलसँ खोज कऽ सकै छी।",
        "logentry-newusers-byemail": "$1 द्वारा प्रयोक्ता खाता $3 {{GENDER:$2|बनाओल}} गेल आ कूटशब्द ई-पत्र द्वारा भेजल गेल",
        "logentry-newusers-autocreate": "खाता $1 छल {{GENDER:$2|बनाएल}} स्वतः",
        "logentry-upload-upload": "$1 {{GENDER:$2|ए}} $3 अपलोड केलक",
-       "log-name-tag": "ट्याग लग",
+       "log-name-tag": "à¤\9fà¥\8dयाà¤\97 à¤²à¥\8cà¤\97",
        "rightsnone": "(कोनो नै)",
        "revdelete-summary": "सम्पादन सारांश",
        "feedback-adding": "पन्ना उपर प्रतिक्रिया जोडु ...",
        "expand_templates_remove_comments": "टिप्पणी हटाउ",
        "expand_templates_remove_nowiki": "परिणाम में <nowiki> ट्याग हटाउ",
        "expand_templates_generate_xml": "XML के पार्स (parse) वृक्ष देखाउ",
+       "pagelanguage": "पृष्ठ भाषा परिवर्तन करी",
        "pagelang-name": "पन्ना",
        "pagelang-language": "भाषा",
+       "pagelang-use-default": "डिफल्ट भाषा प्रयोग करी",
        "pagelang-select-lang": "भाषा चुनु",
+       "pagelang-submit": "भेजी",
        "right-pagelang": "पृष्ठ के भाषा परिवर्तन करू",
        "action-pagelang": "पृष्ठ के भाषा परिवर्तन करू",
+       "log-name-pagelang": "भाषा परिवर्तन लग",
+       "log-description-pagelang": "ई पृष्ठ भाषासभमे परिवर्तनक लग छी।",
+       "logentry-pagelang-pagelang": "$1 {{GENDER:$2|बदलि देल गेल}} पृष्ठ भाषा $3 क लेल $4 सँ $5।",
+       "mediastatistics": "मिडिया तथ्याङ्क",
        "special-characters-group-latin": "ल्याटिन",
        "special-characters-group-latinextended": "ल्याटिन विस्तारित",
        "special-characters-group-ipa": "आइपीए",
        "special-characters-group-khmer": "खमेर",
        "special-characters-title-endash": "एन डैश",
        "special-characters-title-emdash": "एम डैश",
-       "special-characters-title-minus": "ऋण चिह्न"
+       "special-characters-title-minus": "ऋण चिह्न",
+       "randomrootpage": "अविशिष्ट मूल पृष्ठ",
+       "log-action-filter-block": "प्रतिबन्धक प्रकार:",
+       "log-action-filter-delete": "मेटबैक प्रकार:",
+       "log-action-filter-import": "आयातक प्रकार:",
+       "log-action-filter-move": "स्थानान्तरणक प्रकार:",
+       "log-action-filter-newusers": "खाता निर्माणक प्रकार:",
+       "log-action-filter-patrol": "परीक्षणक प्रकार:",
+       "log-action-filter-protect": "सुरक्षाक प्रकार:",
+       "log-action-filter-rights": "अधिकार परिवर्तनक प्रकार:",
+       "log-action-filter-all": "सभटा",
+       "log-action-filter-block-block": "अवरोध",
+       "log-action-filter-block-reblock": "अवरोध परिवर्तन",
+       "log-action-filter-block-unblock": "अवरोधरहित",
+       "log-action-filter-contentmodel-change": "सामग्रीक नमूना परिवर्तन"
 }
index 199eb7c..de1353c 100644 (file)
        "eauthentsent": "На назначената адреса е испратена потврдна порака.\nПред да се испрати друга порака на корисничката сметка, ќе морате да ги проследите напатствијата во пораката, за да потврдите дека таа корисничка сметка е навистина ваша.",
        "throttled-mailpassword": "Веќе е испратена порака за измена на лозинката во {{PLURAL:$1|изминатиов час|изминативе $1 часа}}.\nЗа да се спречи злоупотреба, само едно потсетување може да се праќа на {{PLURAL:$1|секој час|секои $1 часа}}.",
        "mailerror": "Грешка при испраќање на е-поштата: $1",
-       "acct_creation_throttle_hit": "Ð\9aоÑ\80иÑ\81ниÑ\86и Ð½Ð° Ð¾Ð²Ð° Ð²Ð¸ÐºÐ¸ ÐºÐ¾Ñ\80иÑ\81Ñ\82еÑ\98Ñ\9cи Ñ\98а Ð²Ð°Ñ\88аÑ\82а IP-адÑ\80еÑ\81а Ñ\81оздале {{PLURAL:$1|1 ÐºÐ¾Ñ\80иÑ\81ниÑ\87ка Ñ\81меÑ\82ка|$1 ÐºÐ¾Ñ\80иÑ\81ниÑ\87ки Ñ\81меÑ\82ки}} Ð²Ð¾ Ð¿Ð¾Ñ\81ледниве Ð´ÐµÐ½Ð¾Ð²Ð¸, при што е достигнат максималниот број на кориснички сметки предвиден и овозможен за овој период.\nКако резултат на ова, посетителите кои ја користат оваа IP-адреса во моментов нема да можат да создаваат нови сметки.",
+       "acct_creation_throttle_hit": "Ð\9fоÑ\81еÑ\82иÑ\82ели Ð½Ð° Ð¾Ð²Ð° Ð²Ð¸ÐºÐ¸ ÐºÐ¾Ñ\80иÑ\81Ñ\82еÑ\98Ñ\9cи Ñ\98а Ð²Ð°Ñ\88аÑ\82а IP-адÑ\80еÑ\81а Ñ\81оздале {{PLURAL:$1|1 Ñ\81меÑ\82ка|$1 Ñ\81меÑ\82ки}} Ð²Ð¾ Ð¿Ð¾Ñ\81ледниве $2, при што е достигнат максималниот број на кориснички сметки предвиден и овозможен за овој период.\nКако резултат на ова, посетителите кои ја користат оваа IP-адреса во моментов нема да можат да создаваат нови сметки.",
        "emailauthenticated": "Вашата е-пошта адреса е потврдена на $2 во $3 ч.",
        "emailnotauthenticated": "Вашата е-поштенска адреса сè уште не е потврдена.\nНема да биде испратена е-пошта во ниту еден од следниве случаи.",
        "noemailprefs": "Наведете е-поштенска адреса за да функционираат следниве својства.",
        "botpasswords-label-resetpassword": "Ставете нова лозинка",
        "botpasswords-label-grants": "Применливи доделувања:",
        "botpasswords-help-grants": "Секое доделување дава пристап до список до наведени права што веќе ги има корисничката сметка. Повеќе ќе најдете на [[Special:ListGrants|табелата со доделувања]].",
-       "botpasswords-label-restrictions": "Ограничувања на употребата:",
        "botpasswords-label-grants-column": "Доделено",
        "botpasswords-bad-appid": "Името на ботот „$1“ е неважечко.",
        "botpasswords-insert-failed": "Не успеав да го додадам името на ботот „$1“. Да не е веќе додадено?",
        "passwordreset-emailelement": "Корисничко име: \n$1\n\nПривремена лозинка: \n$2",
        "passwordreset-emailsentemail": "Ако ова е регистрираната е-пошта поврзана со вашата сметка, тогаш ќе ви биде испратено писмо за задавање на нова лозинка.",
        "passwordreset-emailsentusername": "Ако има соодветна регистрирана е-пошта поврзана со ова корисничко име, тогаш ќе ви биде испратена порака за промена на лозинката.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Ð\95-поÑ\88Ñ\82аÑ\82а Ð·Ð° Ð·Ð°Ð´Ð°Ð²Ð°Ñ\9aе Ð½Ð° Ð½Ð¾Ð²Ð° Ð»Ð¾Ð·Ð¸Ð½ÐºÐ°|Ð\95-поÑ\88Ñ\82аÑ\82а Ð·Ð° Ð·Ð°Ð´Ð°Ð²Ð°Ñ\9aе Ð½Ð° Ð½Ð¾Ð²Ð¸ Ð»Ð¾Ð·Ð¸Ð½ÐºÐ¸}} Ðµ Ð¸Ñ\81пÑ\80аÑ\82ена. Ð\9fодолÑ\83 е {{PLURAL:$1|е прикажано корисничкото име и лозинката|прикажан список на кориснички имиња и лозинки}}.",
-       "passwordreset-emailerror-capture2": "Ð\98Ñ\81пÑ\80аÑ\9cаÑ\9aеÑ\82о Ðµ-поÑ\88Ñ\82а Ð½Ð° {{GENDER:$2|коÑ\80иÑ\81никоÑ\82}} Ð½Ðµ Ñ\83Ñ\81пеа: $1 Ð\9fодолÑ\83 е {{PLURAL:$3|прикажано корисничкото име и лозинката|прикажан список на кориснички имиња и лозинки}}.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Ð\95-поÑ\88Ñ\82аÑ\82а Ð·Ð° Ð·Ð°Ð´Ð°Ð²Ð°Ñ\9aе Ð½Ð° Ð½Ð¾Ð²Ð° Ð»Ð¾Ð·Ð¸Ð½ÐºÐ°|Ð\95-поÑ\88Ñ\82аÑ\82а Ð·Ð° Ð·Ð°Ð´Ð°Ð²Ð°Ñ\9aе Ð½Ð° Ð½Ð¾Ð²Ð¸ Ð»Ð¾Ð·Ð¸Ð½ÐºÐ¸}} Ðµ Ð¸Ñ\81пÑ\80аÑ\82ена. Ð¢Ñ\83ка е {{PLURAL:$1|е прикажано корисничкото име и лозинката|прикажан список на кориснички имиња и лозинки}}.",
+       "passwordreset-emailerror-capture2": "Ð\98Ñ\81пÑ\80аÑ\9cаÑ\9aеÑ\82о Ðµ-поÑ\88Ñ\82а Ð½Ð° {{GENDER:$2|коÑ\80иÑ\81никоÑ\82}} Ð½Ðµ Ñ\83Ñ\81пеа: $1 Ð¢Ñ\83ка е {{PLURAL:$3|прикажано корисничкото име и лозинката|прикажан список на кориснички имиња и лозинки}}.",
        "passwordreset-nocaller": "Мора да се укаже повикувач",
        "passwordreset-nosuchcaller": "Повикувачот не постои: $1",
        "passwordreset-ignored": "Менувањето на лозинката не успеа. Можеби не е поставен услужник?",
        "unlinkaccounts-success": "Сметката е одврзана.",
        "authenticationdatachange-ignored": "Промената на податоците во заверката не е обработена. Можеби не е поставен услужник?",
        "userjsispublic": "Напомена: потстраниците со JavaScript не треба да содржат дсоверливи податоци бидејќи истите се видливи и за други корисници.",
-       "usercssispublic": "Напомена: потстраниците со CSS не треба да содржат дсоверливи податоци бидејќи истите се видливи и за други корисници."
+       "usercssispublic": "Напомена: потстраниците со CSS не треба да содржат дсоверливи податоци бидејќи истите се видливи и за други корисници.",
+       "restrictionsfield-badip": "Неважечки IP-дијапазон на адреси: $1",
+       "restrictionsfield-label": "Допуштени IP-опсези:",
+       "restrictionsfield-help": "Една IP-адреса или CIDR-опсег по ред. За да овозможите сè, користете<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index dc3b4ff..fe944a7 100644 (file)
        "talk": "Diskusjon",
        "views": "Visninger",
        "toolbox": "Verktøy",
+       "tool-link-userrights": "Endre {{GENDER:$1|brukergrupper}}",
+       "tool-link-emailuser": "Send {{GENDER:$1|brukeren}} en e-post",
        "userpage": "Vis brukerside",
        "projectpage": "Vis prosjektside",
        "imagepage": "Vis filside",
        "eauthentsent": "En bekreftelsesmelding ble sendt til oppgitt e-postadresse. Før andre e-poster kan sendes til kontoen må du følge instruksjonene i e-posten for å bekrefte at kontoen faktisk er din.",
        "throttled-mailpassword": "En passordtilbakestillingsepost har allerede blitt sendt for mindre enn {{PLURAL:$1|en time|$1 timer}} siden.\nFor å forhindre misbruk kan kun én passordtilbakestillingsepost sendes per {{PLURAL:$1|time|$1 timer}}.",
        "mailerror": "Feil under sending av e-post: $1",
-       "acct_creation_throttle_hit": "Gjester med samme IP-adresse som deg har opprettet {{PLURAL:$1|én konto|$1 kontoer}} det siste døgnet, og det er ikke tillatt å opprette flere.\nSom et resultat kan det ikke opprettes flere kontoer fra denne IP-adressen.",
+       "acct_creation_throttle_hit": "Gjester med samme IP-adresse som deg har opprettet {{PLURAL:$1|én konto|$1 kontoer}} i løpet av $2, og det er ikke tillatt å opprette flere.\nSom et resultat kan det for tiden ikke opprettes flere kontoer fra denne IP-adressen.",
        "emailauthenticated": "Din e-postadresse ble bekreftet den $2 kl. $3.",
        "emailnotauthenticated": "Din e-postadresse er ikke bekreftet. Du vil ikke kunne motta e-post for noen av følgende egenskaper.",
        "noemailprefs": "Oppgi en e-postadresse for at disse funksjonene skal fungere.",
        "botpasswords-label-resetpassword": "Tilbakestill passord",
        "botpasswords-label-grants": "Tilgjengelige tildelinger:",
        "botpasswords-help-grants": "Hver tildeling gir tilgang til opplistede brukerrettigheter som brukerkontoen allerede har. Se [[Special:ListGrants|tildelingstabellen]] for mer informasjon.",
-       "botpasswords-label-restrictions": "Bruksbegrensninger:",
        "botpasswords-label-grants-column": "Bevilget",
        "botpasswords-bad-appid": "Robotnavnet \"$1\" er ikke gyldig.",
        "botpasswords-insert-failed": "Kunne ikke legge til robotnavnet \"$1\". Har det allerede blitt lagt til?",
        "passwordreset-emailelement": "Brukernavn: \n$1\n\nMidlertidig passord: \n$2",
        "passwordreset-emailsentemail": "Hvis denne epostadressen er koblet til din konto, så vil det bli sendt en epost om tilbakestilling av passord.",
        "passwordreset-emailsentusername": "Hvis det finnes en epostadresse knyttet til dette brukernavnet, vil en epost med informasjon om tilbakestilling av passord bli sendt.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|E-post}} om passordtilbakestilling har blitt sendt. {{PLURAL:$1|Brukernavnet og passordet|Listen over brukernavn og passord}} vises under.",
-       "passwordreset-emailerror-capture2": "Kunne ikke sende e-post til {{GENDER:$2|brukeren}}: $1 {{PLURAL:$3|Brukernavnet og passordet|Listen over brukernavn og passord}} vises under.",
+       "passwordreset-emailsent-capture2": "{{PLURALS:$1|E-posten|E-postene}} om passordtilbakestilling har blitt sendt. {{PLURAL:$1|Brukernavnet og passordet|Listen over brukernavn og passord}} vises under.",
+       "passwordreset-emailerror-capture2": "Kunne ikke sende e-post til {{GENDER:$2|brukeren}}: $1 {{PLURAL:$3|Brukernavnet og passordet|Listen over brukernavn og passord}} vises her.",
        "passwordreset-nocaller": "En bruker må angis",
        "passwordreset-nosuchcaller": "Brukeren finnes ikke: $1",
        "passwordreset-ignored": "Passordtilbakestillingen ble ikke håndtert. Har ingen leverandør blitt konfigurert?",
        "nopagetext": "Siden du ville flytte finnes ikke.",
        "pager-newer-n": "{{PLURAL:$1|1 nyere|$1 nyere}}",
        "pager-older-n": "{{PLURAL:$1|1 eldre|$1 eldre}}",
-       "suppress": "Historikkrydding",
+       "suppress": "Undertrykk",
        "querypage-disabled": "Denne spesialsiden er deaktivert av ytelsesårsaker.",
        "apihelp": "API hjelp",
        "apihelp-no-such-module": "Modulen «$1» ikke funnet.",
        "sp-contributions-newbies-sub": "For nybegynnere",
        "sp-contributions-newbies-title": "Bidrag av nye kontoer",
        "sp-contributions-blocklog": "blokkeringslogg",
-       "sp-contributions-suppresslog": "undertrykte brukerbidrag",
+       "sp-contributions-suppresslog": "undertrykte {{GENDER:$1|brukerbidrag}}",
        "sp-contributions-deleted": "slettede {{GENDER:$1|brukerbidrag}}",
        "sp-contributions-uploads": "opplastinger",
        "sp-contributions-logs": "logger",
        "tags-deactivate-not-allowed": "Det er ikke mulig å deaktivere taggen «$1».",
        "tags-deactivate-submit": "Deaktiver",
        "tags-apply-no-permission": "Du har ikke tilgang til å legge til merker sammen med dine endringer.",
+       "tags-apply-blocked": "Du kan ikke bruke endringstagger med endringene dine mens du er blokkert.",
        "tags-apply-not-allowed-one": "Merket «$1» kan ikke legges til manuelt.",
        "tags-apply-not-allowed-multi": "{{PLURAL:$2|Det følgende merket|De følgende merkene}} kan ikke legges til manuelt: $1",
        "tags-update-no-permission": "Du har ikke tilgang til å legge til eller fjerne merker fra individuelle revisjoner eller loggposter.",
        "log-action-filter-patrol": "Type patruljering:",
        "log-action-filter-protect": "Type beskyttelse:",
        "log-action-filter-rights": "Type rettighetsendring:",
+       "log-action-filter-suppress": "Type undertrykking:",
        "log-action-filter-upload": "Type opplasting:",
        "log-action-filter-all": "Alle",
        "log-action-filter-block-block": "Blokkering",
        "log-action-filter-protect-move_prot": "Flyttingsbeskyttelse",
        "log-action-filter-rights-rights": "Manuell endring",
        "log-action-filter-rights-autopromote": "Automatisk endring",
+       "log-action-filter-suppress-event": "Loggundertrykking",
+       "log-action-filter-suppress-revision": "Revisjonsundertrykking",
+       "log-action-filter-suppress-delete": "Sideundertrykking",
+       "log-action-filter-suppress-block": "Brukerundertrykking ved blokkering",
+       "log-action-filter-suppress-reblock": "Brukerundertrykking ved gjenblokkering",
        "log-action-filter-upload-upload": "Ny opplasting",
        "log-action-filter-upload-overwrite": "Gjenopplasting",
        "authmanager-authn-not-in-progress": "Autentisering foregår ikke eller sesjonsdata er tapt. Start igjen fra begynnelsen.",
        "linkaccounts-submit": "Lenk kontoer",
        "unlinkaccounts": "Fjern lenking av kontoer",
        "unlinkaccounts-success": "Kontoens lenking ble fjernet.",
+       "authenticationdatachange-ignored": "Autentiseringsdataendringen ble ikke håndtert. Muligens ble ingen tilbyder konfigurert?",
        "userjsispublic": "Merk: JavaScript-undersidene bør ikke inneholde konfidensielle data, siden de kan ses av andre brukere.",
-       "usercssispublic": "Merk: CSS-undersidene bør ikke inneholde konfidensielle data siden de kan ses av andre brukere."
+       "usercssispublic": "Merk: CSS-undersidene bør ikke inneholde konfidensielle data siden de kan ses av andre brukere.",
+       "restrictionsfield-badip": "Ugyldig IP-adresse eller intervall: $1",
+       "restrictionsfield-label": "Tillatte IP-intervaller:",
+       "restrictionsfield-help": "Én IP-adresse eller CIDR-intervall per linje. For å slå på alt, bruk <br /><code>0.0.0.0/0</code><br /><code>::/0</code>"
 }
index f794610..9839755 100644 (file)
        "botpasswords-label-resetpassword": "Het wachtwoord opnieuw instellen",
        "botpasswords-label-grants": "Van toepassing zijnde rechten:",
        "botpasswords-help-grants": "Iedere toestemming geeft toegang tot de opgegeven gebruikersrechten die de gebruiker al heeft. Zie [[Special:ListGrants|overzicht van rechten]] voor meer informatie.",
-       "botpasswords-label-restrictions": "Gebruiksbeperkingen:",
        "botpasswords-label-grants-column": "Toegewezen",
        "botpasswords-bad-appid": "De botnaam \"$1\" is niet geldig.",
        "botpasswords-insert-failed": "Toevoegen van botnaam \"$1\" mislukt. Is deze misschien al toegevoegd?",
        "recentchangeslinked-page": "Paginanaam:",
        "recentchangeslinked-to": "Wijzigingen aan pagina's met koppelingen naar deze pagina bekijken",
        "recentchanges-page-added-to-category": "[[:$1]] aan categorie toegevoegd",
-       "recentchanges-page-added-to-category-bundled": "[[:$1]] en [[Special:WhatLinksHere/$1|{{PLURAL:$2|één pagina|$2 pagina's}}]] zijn toegevoegd aan categorie",
+       "recentchanges-page-added-to-category-bundled": "[[:$1]] is toegevoegd aan de categorie, [[Special:WhatLinksHere/$1|deze pagina is opgenomen in andere pagina's]]",
        "recentchanges-page-removed-from-category": "[[:$1]] is verwijderd uit categorie",
-       "recentchanges-page-removed-from-category-bundled": "[[:$1]] en {{PLURAL:$2|één pagina|$2 pagina's}} zijn verwijderd uit categorie",
+       "recentchanges-page-removed-from-category-bundled": "[[:$1]] is verwijderd uit de categorie, [[Special:WhatLinksHere/$1|deze pagina is opgenomen in andere pagina's]]",
        "autochange-username": "Automatische wijziging van MediaWiki",
        "upload": "Bestand uploaden",
        "uploadbtn": "Bestand uploaden",
index 52740a6..34ac95f 100644 (file)
@@ -32,6 +32,7 @@
        "tog-watchdefault": "ମୁଁ ବଦଳେଇଥିବା ପୃଷ୍ଠା ଏବଂ ଫାଇଲଗୁଡ଼ିକୁ ମୋର ଦେଖଣାତାଲିକାରେ ଯୋଡ଼ନ୍ତୁ",
        "tog-watchmoves": "ମୁଁ ଘୁଞ୍ଚାଇଥିବା ପୃଷ୍ଠା ଏବଂ ଫାଇଲଗୁଡ଼ିକୁ ମୋର ଦେଖଣାତାଲିକାରେ ଯୋଡ଼ନ୍ତୁ",
        "tog-watchdeletion": "ମୁଁ ଲିଭାଇଥିବା ପୃଷ୍ଠା ଏବଂ ଫାଇଲଗୁଡ଼ିକୁ ମୋର ଦେଖଣାତାଲିକାରେ ଯୋଡ଼ନ୍ତୁ",
+       "tog-watchuploads": "ମୋ ଦେଖଣାତାଲିକାରେ ମୁଁ ଅପଲୋଡ଼ କରୁଥିବା ନୂଆ ଫାଇଲ ଯୋଡ଼ନ୍ତୁ",
        "tog-watchrollback": "ମୁଁ ପଛକୁ ଫେରାଇଦେଇଥିବା ମୋ ଦେଖଣାତାଲିକାର ପୃଷ୍ଠାସବୁକୁ ଯୋଡ଼ନ୍ତୁ",
        "tog-minordefault": "ସବୁଯାକ ସମ୍ପାଦନାକୁ ଆପେ ଛୋଟ ବଦଳ ଭାବରେ ସୂଚିତ କରିବେ",
        "tog-previewontop": "ଏଡ଼ିଟ ବାକ୍ସ ଆଗରୁ ଦେଖଣା ଦେଖାଇବେ",
@@ -41,7 +42,7 @@
        "tog-enotifminoredits": "ପୃଷ୍ଠାରେ ଏବଂ ଫାଇଲଗୁଡିକରେ ଛୋଟ ଛୋଟ ବଦଳ ହେଲେ ବି ମୋତେ ଇ-ମେଲ କରିବେ",
        "tog-enotifrevealaddr": "ନୋଟିଫିକେସନ ଇମେଲରେ ମୋ ଇ-ମେଲ ଦେଖାଇବେ",
        "tog-shownumberswatching": "ଦେଖୁଥିବା ବ୍ୟବହାରକାରୀଙ୍କ ସଂଖ୍ୟା ଦେଖାନ୍ତୁ",
-       "tog-oldsig": "à¬\8fବà­\87à¬\95ାର ଦସ୍ତଖତ:",
+       "tog-oldsig": "à¬\86ପଣà¬\99à­\8dà¬\95ର à¬\8fବà­\87ର ଦସ୍ତଖତ:",
        "tog-fancysig": "ଦସ୍ତଖତକୁ ଉଇକିଟେକ୍ସଟ ଭାବରେ ଗଣିବେ (ଆପେଆପେ ଥିବା ଲିଙ୍କ ବିନା)",
        "tog-uselivepreview": "ସାଥେ ସାଥେ ଚାଲିଥିବା ଦେଖଣା ବ୍ୟବହାର କରିବେ",
        "tog-forceeditsummary": "ଖାଲି ସମ୍ପାଦନା ସାରକଥାକୁ ଯିବା ବେଳେ ମୋତେ ଜଣାଇବେ",
index 0953ab2..ce00a81 100644 (file)
        "botpasswords-label-delete": "Usuń",
        "botpasswords-label-resetpassword": "Zresetuj hasło",
        "botpasswords-label-grants": "Zastosowane uprawnienia:",
-       "botpasswords-label-restrictions": "Ograniczenia użytkowania:",
        "botpasswords-label-grants-column": "Przyznane",
        "botpasswords-bad-appid": "Nazwa bota \"$1\" nie jest prawidłowa.",
        "botpasswords-insert-failed": "Nie udało się dodać robota o nazwie \"$1\". Czy był już wcześniej dodany?",
index 21272c1..15b5bee 100644 (file)
@@ -12,7 +12,8 @@
                        "Obaid Raza",
                        "Macofe",
                        "Matma Rex",
-                       "Saanvel"
+                       "Saanvel",
+                       "Satdeep gill"
                ]
        },
        "tog-underline": "حوڑ تھلے لین:",
        "yourpasswordagain": "کنجی فیر لکھو:",
        "createacct-yourpasswordagain": "کنجی پکی کرو",
        "createacct-yourpasswordagain-ph": "کنجی فیر پاؤ",
-       "remembermypassword": "اس براؤزر تے میرا ورتن ناں یاد رکھو ($1 {{PLURAL:$1|دن|دناں}} واسطے)",
        "userlogin-remembermypassword": "مینوں لاگ ان رکھو",
        "yourdomainname": "تواڈا علاقہ:",
        "externaldberror": "ڈیٹابیس چ توانوں پہچاننے چ کوئی مسئلہ ہویا اے یا فیر تسی اپنا بارلا کھاتا نئیں بدل سکدے۔",
        "passwordreset-emailtext-user": "ورتنوالے $1 نے {{سائیٹناں}} تے تواڈے کھاتے بارے پچھیا اے {{SITENAME}} لئی ($4)۔ تھلے دتا گیا ورتن {{PLURAL:$3|کھاتہ|کھاتے}} ایس ای-میل نال جڑدا اے۔\n\n$2\n\n{{PLURAL:$3|ایہ عارضی کنجی|اے عارضی کنجیاں}} مک جائیگا {{PLURAL:$5|اک دن|$5 دن}}۔ تسیں ہن لاکان ہوو تے نویں کنجی چنو۔ اگر کسے ہور نے اے چٹھی پیجی یا توانوں اپنی پہلی کنجی یاد آگئی اے تے تسیں اونوں بدلنا نئیں چاندے تے تسیں ایس سنیعے نوں پھل جاؤ تے پرانی کنجی نال ای کم چلاؤ۔",
        "passwordreset-emailelement": "ورتن ناں: \n$1\n\nعارضی کنجی: \n$2",
        "passwordreset-emailsentemail": "یاد کران واسطے اک ای-میل پیج دتی گئی اے۔",
-       "passwordreset-emailsent-capture": "اک یاد کران والی ای-میل پیج دتی گئی اے، جیہڑی تھلے دسی گئی اے۔",
-       "passwordreset-emailerror-capture": "اک یادکراؤ ای-میل بنائی گئی اے، جیہڑی کہ تھلے دسی گئی اے، پر ورتن والے تک پیجنا نئیں ہوسکیا:$1",
        "changeemail": "ای-میل پتہ بدلو",
        "changeemail-header": "کھاتے دا ای-میل پتہ بدلو",
        "changeemail-no-info": "تسی لاگ ان ہوکے ای اس صفحے نوں ویکھ سکدے او۔",
        "minoredit": "اے نکا جیا کم اے",
        "watchthis": "اس صفے تے اکھ رکھو",
        "savearticle": "کم بچاؤ",
+       "savechanges": "کم بچاؤ",
        "preview": "وکھاؤ",
        "showpreview": "کچا کم ویکھو",
        "showdiff": "تبدیلیاں وکھاؤ",
        "undo-failure": "تبدیلی واپس نئیں ہوسکدی وشکار ہویاں تبدیلیاں ہون دی وجہ توں۔",
        "undo-norev": "تبدیلی واپس نئیں ہوسکدی کیوں جے ایہ ہے ای نئیں یا مٹا دتی گئی اے۔",
        "undo-summary": "$1 دی کیتی ہوئی ریوین [[Special:Contributions/$2|$2]] ([[User talk:$2|گل]]) واپس کرو",
-       "cantcreateaccounttitle": "کھاتہ نئیں کھول سکدے",
        "cantcreateaccount-text": "کھاتہ بنانا ایس آئی پی پتے  ('''$1''')  لئی  [[User:$3|$3]] نے روک دتی اے۔\n$3 نے ''$2'' وجہ دسی اے۔",
        "viewpagelogs": "صفحے دے لاگ ویکھو",
        "nohistory": "اس صفحے دی پرانی لکھائی دی کوئی تاریخ نئیں۔",
        "htmlform-submit": "رکھو",
        "htmlform-reset": "تبدیلیاں واپس",
        "htmlform-selectorother-other": "ہور",
-       "sqlite-has-fts": "$1 پوری لکھت کھوج مدد نال",
-       "sqlite-no-fts": "$1 بنا کسے لکھت مدد دے",
        "logentry-delete-delete": "$1 {{GENDER:$2|مٹایا}} صفہ $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|بچایا}} صفہ $3",
        "logentry-delete-event": "$1 پلٹے وکھالہ {{PLURAL:$5|اک لاگ ایونٹ|$5 لاگ ایونٹس}} تے $3: $4",
        "special-characters-group-gujarati": "گجراتی",
        "special-characters-group-thai": "تھائی",
        "special-characters-group-lao": "لاؤ",
-       "special-characters-group-khmer": "کھیمر",
-       "api-error-blacklisted": "مہربانی کرکے وکھری سرخی چنو۔"
+       "special-characters-group-khmer": "کھیمر"
 }
index 3d38947..668515e 100644 (file)
                        "LucyDiniz",
                        "Tusca",
                        "Cristofer Alves",
-                       "Tark"
+                       "Tark",
+                       "O Andarilho"
                ]
        },
        "tog-underline": "Sublinhar links:",
        "tog-enotifminoredits": "Notificar-me por email também sobre edições menores de páginas ou arquivos",
        "tog-enotifrevealaddr": "Revelar meu endereço de email nas mensagens de notificação",
        "tog-shownumberswatching": "Mostrar o número de usuários que estão vigiando",
-       "tog-oldsig": "Assinatura existente:",
+       "tog-oldsig": "Sua Assinatura Existente:",
        "tog-fancysig": "Tratar assinatura como wikitexto (sem link automático)",
        "tog-uselivepreview": "Utilizar pré-visualização em tempo real",
        "tog-forceeditsummary": "Avisar-me ao introduzir um sumário de edição vazio",
        "tog-showhiddencats": "Exibir categorias ocultas",
        "tog-norollbackdiff": "Omitir diferenças após desfazer edições em bloco",
        "tog-useeditwarning": "Avisar-me quando eu deixar uma janela de edição sem ter salvo as alterações",
-       "tog-prefershttps": "Usar sempre uma conexão segura quando estiver conectado",
+       "tog-prefershttps": "Usar sempre uma conexão segura enquanto estiver conectado",
        "underline-always": "Sempre",
        "underline-never": "Nunca",
        "underline-default": "Padrão do navegador/skin",
        "newwindow": "(abre numa nova janela)",
        "cancel": "Cancelar",
        "moredotdotdot": "Mais...",
-       "morenotlisted": "Esta lista não está completa.",
+       "morenotlisted": "Esta lista está incompleta.",
        "mypage": "Página",
        "mytalk": "Discussão",
        "anontalk": "Discussão",
        "botpasswords-label-resetpassword": "Redefinir a sua senha",
        "botpasswords-label-grants": "Permissões aplicáveis",
        "botpasswords-help-grants": "Cada permissão da acesso à lista permissões de usuários que um usuário já tenha. Veja o [[Special:ListGrants|Lista de Permissões]] para mais informações.",
-       "botpasswords-label-restrictions": "Restrições de uso:",
        "botpasswords-label-grants-column": "Concedido",
        "botpasswords-bad-appid": "O nome de robô \"$1\" não é válido.",
        "botpasswords-insert-failed": "Falha ao adicionar o nome de robô \"$1\". Ele já foi adicionado?",
index 00896dc..bdb05bf 100644 (file)
        "botpasswords-label-delete": "Eliminar",
        "botpasswords-label-resetpassword": "Redefinir palavra-passe",
        "botpasswords-label-grants": "Permissões aplicáveis:",
-       "botpasswords-label-restrictions": "Restrições de uso:",
        "botpasswords-label-grants-column": "Concedido",
        "botpasswords-bad-appid": "O nome do robô \"$1\" não é válido.",
        "botpasswords-insert-failed": "Falhou ao adicionar o nome do robô \"$1\". Já foi adicionado?",
index 91aa460..e2b8292 100644 (file)
        "botpasswords-label-resetpassword": "Label for the checkbox to reset the actual password for the current bot password.",
        "botpasswords-label-grants": "Label for the checkmatrix for selecting grants allowed when the bot password is used.\n\ngrant: Vidu http://komputeko.net/index_en.php?vorto=grant sed \"konced/i\" egale funkcius.",
        "botpasswords-help-grants": "Help text for the grant selection checkmatrix.",
-       "botpasswords-label-restrictions": "Label for the textarea field in which JSON defining access restrictions (e.g. which IP address ranges are allowed) is entered.",
        "botpasswords-label-grants-column": "Label for the checkbox column on the checkmatrix for selecting grants allowed when the bot password is used.",
        "botpasswords-bad-appid": "Used as an error message when an invalid \"bot name\" is supplied on [[Special:BotPasswords]]. Parameters:\n* $1 - The rejected bot name.",
        "botpasswords-insert-failed": "Error message when saving a new bot password failed. It's likely that the failure was because the user resubmitted the form after a previous successful save. Parameters:\n* $1 - Bot name",
        "upload-dialog-disabled": "Message shown when the upload dialog functionality is disabled. (This doesn't mean that uploads in general are disabled, only this specific method of uploading.)",
        "upload-dialog-title": "Title of the upload dialog box\n{{Identical|Upload file}}",
        "upload-dialog-button-cancel": "Button to cancel the dialog\n{{Identical|Cancel}}",
+       "upload-dialog-button-back": "Button to go back the dialog\n{{Identical|Back}}",
        "upload-dialog-button-done": "Button to close the dialog once upload is complete\n{{Identical|Done}}",
        "upload-dialog-button-save": "Button to save the file after upload finishes and metadata is filled out, part 2 of a multi-step upload form\n{{Identical|Save}}",
        "upload-dialog-button-upload": "Button to initiate upload, part 1 of a multi-step upload form\n{{Identical|Upload}}",
        "htmlform-cloner-create": "Used as the text for the button that adds a row to a multi-input HTML form element.\n\nSee also:\n* {{msg-mw|htmlform-cloner-delete}}\n* {{msg-mw|htmlform-cloner-required}}",
        "htmlform-cloner-delete": "Used as the text for the button that removes a row from a multi-input HTML form element\n\nSee also:\n* {{msg-mw|htmlform-cloner-create}}\n* {{msg-mw|htmlform-cloner-required}}\n{{Identical|Remove}}",
        "htmlform-cloner-required": "Used as an error message in HTML forms.\n\nSee also:\n* {{msg-mw|htmlform-required}}\n* {{msg-mw|htmlform-cloner-create}}\n* {{msg-mw|htmlform-cloner-delete}}",
+       "htmlform-date-placeholder": "Used as initial placeholder text in \"date\" input boxes. This date MUST be formatted as a 4-digit year, 2-digit month, and 2-digit day, in the Gregorian calendar, separated with ASCII hyphen ('-') characters. You can localise the letters to your language or script, but you should not change the format.",
+       "htmlform-time-placeholder": "Used as initial placeholder text in \"time\" input boxes. This time MUST be formatted as a 2-digit hour 00 to 23, a 2-digit minute, and an optional 2-digit second, all separated by ASCII colons. You can localise the letters to your language or script, but you should not change the format.",
+       "htmlform-datetime-placeholder": "Used as initial placeholder text in \"datetime\" input boxes. This date and time MUST be formatted as a 4-digit year, 2-digit month, and 2-digit day, in the Gregorian calendar, separated with ASCII hyphen ('-') characters, followed by a space (or the letter 'T'), followed by a time formatted as a 2-digit hour 00 to 23, a 2-digit minute, and an optional 2-digit second, all separated by ASCII colons. You can localise the letters to your language or script, but you should not change the format.",
+       "htmlform-date-invalid": "Used as error message in HTML forms. This date MUST be formatted as a 4-digit year, 2-digit month, and 2-digit day, in the Gregorian calendar, separated with ASCII hyphen ('-') characters. You can localise the letters to your language or script, but you should not change the format.\n\n* {{msg-mw|Htmlform-invalid-input}}\n* {{msg-mw|Apifeatureusage-htmlform-date-placeholder}}\n* {{msg-mw|Apifeatureusage-htmlform-date-toolow}}\n* {{msg-mw|Apifeatureusage-htmlform-date-toohigh}}\n* {{msg-mw|Htmlform-required}}",
+       "htmlform-time-invalid": "Used as error message in HTML forms. This time MUST be formatted as a 2-digit hour 00 to 23, a 2-digit minute, and an optional 2-digit second, all separated by ASCII colons. You can localise the letters to your language or script, but you should not change the format.\n\n* {{msg-mw|Htmlform-invalid-input}}\n* {{msg-mw|Apifeatureusage-htmlform-time-placeholder}}\n* {{msg-mw|Apifeatureusage-htmlform-time-toolow}}\n* {{msg-mw|Apifeatureusage-htmlform-time-toohigh}}\n* {{msg-mw|Htmlform-required}}",
+       "htmlform-datetime-invalid": "Used as error message in HTML forms. This date and time MUST be formatted as a 4-digit year, 2-digit month, and 2-digit day, in the Gregorian calendar, separated with ASCII hyphen ('-') characters, followed by a space (or the letter 'T'), followed by a time formatted as a 2-digit hour 00 to 23, a 2-digit minute, and an optional 2-digit second, all separated by ASCII colons. You can localise the letters to your language or script, but you should not change the format.\n\n* {{msg-mw|Htmlform-invalid-input}}\n* {{msg-mw|Apifeatureusage-htmlform-datetime-placeholder}}\n* {{msg-mw|Apifeatureusage-htmlform-datetime-toolow}}\n* {{msg-mw|Apifeatureusage-htmlform-datetime-toohigh}}\n* {{msg-mw|Htmlform-required}}",
+       "htmlform-date-toolow": "Used as error message in HTML forms. Parameters:\n* $1 - minimum date\nSee also:\n* {{msg-mw|Htmlform-invalid-input}}\n* {{msg-mw|Htmlform-required}}\n* {{msg-mw|Apifeatureusage-htmlform-date-invalid}}\n* {{msg-mw|Apifeatureusage-htmlform-date-toohigh}}",
+       "htmlform-date-toohigh": "Used as error message in HTML forms. Parameters:\n* $1 - maximum date\nSee also:\n* {{msg-mw|Htmlform-invalid-input}}\n* {{msg-mw|Htmlform-required}}\n* {{msg-mw|Apifeatureusage-htmlform-date-invalid}}\n* {{msg-mw|Apifeatureusage-htmlform-date-toolow}}",
+       "htmlform-time-toolow": "Used as error message in HTML forms. Parameters:\n* $1 - minimum date\nSee also:\n* {{msg-mw|Htmlform-invalid-input}}\n* {{msg-mw|Htmlform-required}}\n* {{msg-mw|Apifeatureusage-htmlform-time-invalid}}\n* {{msg-mw|Apifeatureusage-htmlform-time-toohigh}}",
+       "htmlform-time-toohigh": "Used as error message in HTML forms. Parameters:\n* $1 - maximum date\nSee also:\n* {{msg-mw|Htmlform-invalid-input}}\n* {{msg-mw|Htmlform-required}}\n* {{msg-mw|Apifeatureusage-htmlform-time-invalid}}\n* {{msg-mw|Apifeatureusage-htmlform-time-toolow}}",
+       "htmlform-datetime-toolow": "Used as error message in HTML forms. Parameters:\n* $1 - minimum date\nSee also:\n* {{msg-mw|Htmlform-invalid-input}}\n* {{msg-mw|Htmlform-required}}\n* {{msg-mw|Apifeatureusage-htmlform-datetime-invalid}}\n* {{msg-mw|Apifeatureusage-htmlform-datetime-toohigh}}",
+       "htmlform-datetime-toohigh": "Used as error message in HTML forms. Parameters:\n* $1 - maximum date\nSee also:\n* {{msg-mw|Htmlform-invalid-input}}\n* {{msg-mw|Htmlform-required}}\n* {{msg-mw|Apifeatureusage-htmlform-datetime-invalid}}\n* {{msg-mw|Apifeatureusage-htmlform-datetime-toolow}}",
        "htmlform-title-badnamespace": "Error message shown if the page title provided by the user is not in the required namespace. $1 is the page, $2 is the numerical namespace index.",
        "htmlform-title-not-creatable": "Error message shown if the page title provided by the user is not creatable (a special page). $1 is the page title.",
        "htmlform-title-not-exists": "Error message shown if the page title provided by the user does not exist. $1 is the page title.",
        "unlinkaccounts-success": "Account unlinking form success message",
        "authenticationdatachange-ignored": "Shown when authentication data change was unsuccessful due to configuration problems.\n\nCf. e.g. {{msg-mw|Passwordreset-ignored}}.",
        "userjsispublic": "A reminder to users that Javascript subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .js. See also {{msg-mw|usercssispublic}}.",
-       "usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}"
+       "usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}",
+       "restrictionsfield-badip": "An error message shown when one entered an invalid IP address or range in a restrictions field (such as Special:BotPassword). $1 is the IP address.",
+       "restrictionsfield-label": "Field label shown for restriction fields (e.g. on Special:BotPassword).",
+       "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword)."
 }
index c0db2ac..c2c42c2 100644 (file)
        "botpasswords-label-delete": "Șterge",
        "botpasswords-label-resetpassword": "Resetează parola",
        "botpasswords-label-grants": "Permisiuni aplicabile:",
-       "botpasswords-label-restrictions": "Restricții de utilizare:",
        "botpasswords-label-grants-column": "Permise",
        "botpasswords-bad-appid": "Numele de robot „$1” nu este valid.",
        "resetpass_forbidden": "Parolele nu pot fi schimbate.",
index 87c43ed..6dd8846 100644 (file)
        "eauthentsent": "На указанный адрес электронной почты отправлено письмо. \nЧтобы получать письма в дальнейшем, следуйте изложенным там инструкциям для подтверждения, что этот адрес действительно принадлежит вам.",
        "throttled-mailpassword": "Функция напоминания пароля уже использовалась в течение {{PLURAL:$1|1=последнего часа|последних $1 часов}}.\nДля предотвращения злоупотреблений, разрешено запрашивать не более одного напоминания {{PLURAL:$1|за $1 час|за $1 часов|за $1 часа|1=в час}}.",
        "mailerror": "Ошибка при отправке почты: $1",
-       "acct_creation_throttle_hit": "Ð\97а Ñ\81Ñ\83Ñ\82ки Ñ\81 Ð²Ð°Ñ\88его IP-адÑ\80еÑ\81а {{PLURAL:$1|бÑ\8bла Ñ\81оздана $1 Ñ\83Ñ\87Ñ\91Ñ\82наÑ\8f Ð·Ð°Ð¿Ð¸Ñ\81Ñ\8c|бÑ\8bло Ñ\81оздано $1 Ñ\83Ñ\87Ñ\91Ñ\82нÑ\8bÑ\85 Ð·Ð°Ð¿Ð¸Ñ\81ей|бÑ\8bли Ñ\81озданÑ\8b $1 Ñ\83Ñ\87Ñ\91Ñ\82нÑ\8bÑ\85 Ð·Ð°Ð¿Ð¸Ñ\81и|1=Ñ\83же Ð±Ñ\8bла Ñ\81оздана Ñ\83Ñ\87Ñ\91Ñ\82наÑ\8f Ð·Ð°Ð¿Ð¸Ñ\81Ñ\8c}} — это предельное количество для данного отрезка времени.\nВ результате, пользователи с этим IP-адресом в данный момент больше не могут создавать новых учётных записей.",
+       "acct_creation_throttle_hit": "Ð\9fоÑ\81еÑ\82иÑ\82ели Ñ\81 Ð²Ð°Ñ\88его IP-адÑ\80еÑ\81а {{PLURAL:$1|бÑ\8bла Ñ\81оздана $1 Ñ\83Ñ\87Ñ\91Ñ\82наÑ\8f Ð·Ð°Ð¿Ð¸Ñ\81Ñ\8c|бÑ\8bло Ñ\81оздано $1 Ñ\83Ñ\87Ñ\91Ñ\82нÑ\8bÑ\85 Ð·Ð°Ð¿Ð¸Ñ\81ей|бÑ\8bли Ñ\81озданÑ\8b $1 Ñ\83Ñ\87Ñ\91Ñ\82нÑ\8bÑ\85 Ð·Ð°Ð¿Ð¸Ñ\81и}} Ð·Ð° Ð¿Ð¾Ñ\81ледние $2 — это предельное количество для данного отрезка времени.\nВ результате, пользователи с этим IP-адресом в данный момент больше не могут создавать новых учётных записей.",
        "emailauthenticated": "Ваш адрес электронной почты подтверждён $2 в $3.",
        "emailnotauthenticated": "Ваш адрес электронной почты ещё не был подтверждён.\nПисьма не будут отправляться ни для одной из следующий функций.",
        "noemailprefs": "Адрес электронной почты не был указан, функции вики-движка по работе с эл. почтой отключены.",
        "botpasswords-label-resetpassword": "Сбросить пароль",
        "botpasswords-label-grants": "Применимые разрешения:",
        "botpasswords-help-grants": "Каждое разрешение даёт доступ к перечисленным правам участника, которые уже есть у учётной записи участника. См. [[Special:ListGrants|таблицу разрешений]] для получения дополнительной информации.",
-       "botpasswords-label-restrictions": "Ограничения на использование:",
        "botpasswords-label-grants-column": "Разрешено",
        "botpasswords-bad-appid": "Имя бота «$1» является недопустимым.",
        "botpasswords-insert-failed": "Не удалось добавить бота с именем «$1». Возможно, он был уже добавлен?",
        "passwordreset-emailelement": "Имя участника: \n$1\n\nВременный пароль: \n$2",
        "passwordreset-emailsentemail": "Если это адрес электронной почты связан с вашей учётной записью, вам будет отправлено письмо для сброса пароля.",
        "passwordreset-emailsentusername": "Если есть адрес электронной почты, связанный с этим именем участника, то будет отправлено письмо для восстановления пароля.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Ð\9fиÑ\81Ñ\8cмо|Ð\9fиÑ\81Ñ\8cма}} Ð´Ð»Ñ\8f Ñ\81бÑ\80оÑ\81а Ð¿Ð°Ñ\80олÑ\8f {{PLURAL:$1|бÑ\8bло Ð¾Ñ\82пÑ\80авлено|бÑ\8bли Ð¾Ñ\82пÑ\80авленÑ\8b}}. {{PLURAL:$1|Ð\9bогин Ð¸ Ð¿Ð°Ñ\80олÑ\8c Ð¿Ð¾ÐºÐ°Ð·Ð°Ð½Ñ\8b|СпиÑ\81ок Ð»Ð¾Ð³Ð¸Ð½Ð¾Ð² Ð¸ Ð¿Ð°Ñ\80олей Ð¿Ð¾ÐºÐ°Ð·Ð°Ð½}} Ð½Ð¸Ð¶Ðµ.",
-       "passwordreset-emailerror-capture2": "Отправка {{GENDER:$2|участнику}} письма по электронной почте не удалась: $1 В {{PLURAL:$3|логин и пароль показаны|список логинов и паролей показан}} ниже.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Ð\9fиÑ\81Ñ\8cмо|Ð\9fиÑ\81Ñ\8cма}} Ð´Ð»Ñ\8f Ñ\81бÑ\80оÑ\81а Ð¿Ð°Ñ\80олÑ\8f {{PLURAL:$1|бÑ\8bло Ð¾Ñ\82пÑ\80авлено|бÑ\8bли Ð¾Ñ\82пÑ\80авленÑ\8b}}. {{PLURAL:$1|Ð\9bогин Ð¸ Ð¿Ð°Ñ\80олÑ\8c Ð¿Ð¾ÐºÐ°Ð·Ð°Ð½Ñ\8b|СпиÑ\81ок Ð»Ð¾Ð³Ð¸Ð½Ð¾Ð² Ð¸ Ð¿Ð°Ñ\80олей Ð¿Ð¾ÐºÐ°Ð·Ð°Ð½}} Ð·Ð´ÐµÑ\81Ñ\8c.",
+       "passwordreset-emailerror-capture2": "Отправка {{GENDER:$2|участнику|участнице}} письма по электронной почте не удалась: $1\n{{PLURAL:$3|Логин и пароль показаны|Список логинов и паролей показан}} здесь.",
        "passwordreset-nocaller": "Должен быть предоставлен источник вызова",
        "passwordreset-nosuchcaller": "Источник вызова не существует: $1",
        "passwordreset-ignored": "Сброс пароля не был обработан. Может быть, не был настроен ни один провайдер?",
        "unlinkaccounts-success": "Учетная запись была отвязан.",
        "authenticationdatachange-ignored": "Изменение данных для проверки подлинности не было обработано. Может быть, не был настроен ни один провайдер?",
        "userjsispublic": "Обратите внимание: подстраницы JavaScript не должны содержать конфиденциальные сведения, поскольку они доступны для просмотра другим участникам.",
-       "usercssispublic": "Обратите внимание: подстраницы CSS не должны содержать конфиденциальные сведения, поскольку они доступны для просмотра другим участникам."
+       "usercssispublic": "Обратите внимание: подстраницы CSS не должны содержать конфиденциальные сведения, поскольку они доступны для просмотра другим участникам.",
+       "restrictionsfield-badip": "Недопустимый IP-адрес или диапазон адресов: $1",
+       "restrictionsfield-label": "Разрешённые диапазоны IP-адресов:",
+       "restrictionsfield-help": "По одному IP-адресу или CIDR-диапазону в строке. Чтобы разрешить всё, используйте <br /><code>0.0.0.0/0</code><br /><code>::/0</code>"
 }
index 18b68d6..598e10d 100644 (file)
        "botpasswords-label-resetpassword": "Аһарыгы саҥаттан",
        "botpasswords-label-grants": "Туттуллар көҥүллэр:",
        "botpasswords-help-grants": " Кыттааччы учуоттуур суруйуутугар баар ыйыллыбыт кыттааччы быраабыгар киирэргэ кыах биэрэр. к. [[Special:ListGrants|көҥүллэр табылыыссаларын]] эбии информацияны ылар туһугар.",
-       "botpasswords-label-restrictions": "Туттарга хааччахтаах:",
        "botpasswords-label-grants-column": "Көҥүллэннэ",
        "botpasswords-bad-appid": "Маннык аат «$1» сатаммат.",
        "botpasswords-insert-failed": "«$1» диэн ааттаах оруобаты эбэр табыллыбата. Баҕар хайыы-үйэ эбиллибитэ буолаарай?",
index b080fcf..c541349 100644 (file)
        "botpasswords-label-resetpassword": "Obnoviť heslo",
        "botpasswords-label-grants": "Príslušné oprávnenia:",
        "botpasswords-help-grants": "Každé oprávnenie poskytuje prístup k uvedeným právam používateľa, ktoré už používateľský účet má. Ďalšie informácie nájdete v [[Special:ListGrants|tabuľke oprávnení]].",
-       "botpasswords-label-restrictions": "Obmedzenie použitia:",
        "botpasswords-label-grants-column": "Udelené",
        "botpasswords-bad-appid": "Názov bota „$1“ nie je platný.",
        "botpasswords-insert-failed": "Nepodarilo sa pridať názov bota „$1“. Je už pridaný?",
        "wlshowlast": "Zobraziť posledných $1 hodín $2 dní",
        "watchlist-hide": "Skryť",
        "watchlist-submit": "Zobraziť",
-       "wlshowtime": "Zobrazené obdobie:",
+       "wlshowtime": "Obdobie:",
        "wlshowhideminor": "drobné úpravy",
        "wlshowhidebots": "botov",
        "wlshowhideliu": "registrovaných",
index 12edaa7..71cc9ad 100644 (file)
        "editold": "spremeni",
        "viewsourceold": "izvorno besedilo",
        "editlink": "uredi",
-       "viewsourcelink": "izvorna koda",
+       "viewsourcelink": "izvorno besedilo",
        "editsectionhint": "Spremeni razdelek: $1",
        "toc": "Vsebina",
        "showtoc": "prikaži",
        "eauthentsent": "E-sporočilo je bilo poslano na navedeni e-naslov.\nČe želite tja poslati še katero, sledite navodilom v e-sporočilu, da potrdite lastništvo računa.",
        "throttled-mailpassword": "E-pošto za ponastavitev gesla smo v {{PLURAL:$1|zadnji uri|zadnjih $1 urah}} že poslali.\nZa preprečevanje zlorab lahko na {{PLURAL:$1|uro|$1 uri|$1 ure|$1 ur}} pošljemo samo eno sporočilo za ponastavitev gesla.",
        "mailerror": "Napaka pri pošiljanju pošte: $1",
-       "acct_creation_throttle_hit": "Obiskovalci {{GRAMMAR:rodilnik|{{SITENAME}}}} so s tem IP-naslovom v zadnjih 24 urah ustvarili že $1 {{PLURAL:$1|uporabniški račun|uporabniška računa|uporabniške račune|uporabniških računov|uporabniških računov}} in s tem dosegli največje dopustno število v omenjenem časovnem obdobju. Novih računov zato s tem IP-naslovom trenutno žal ne morete več ustvariti.\n\n== Urejate prek posredniškega strežnika? ==\n\nČe urejate prek AOL ali iz Bližnjega vzhoda, Afrike, Avstralije, Nove Zelandije ali iz šole, knjižnice ali podjetja, si IP-naslov morda delite z drugimi uporabniki. Če je tako, ste to sporočilo morda prejeli, čeprav niste ustvarili še nobenega računa. Znova se lahko poskusite registrirati po nekaj urah.",
+       "acct_creation_throttle_hit": "Obiskovalci {{GRAMMAR:rodilnik|{{SITENAME}}}} so s tem IP-naslovom v zadnjih $2 ustvarili že $1 {{PLURAL:$1|uporabniški račun|uporabniška računa|uporabniške račune|uporabniških računov}} in s tem dosegli največje dopustno število v omenjenem časovnem obdobju. Novih računov zato s tem IP-naslovom trenutno žal ne morete več ustvariti.",
        "emailauthenticated": "Vaš e-poštni naslov je bil potrjen dne $2 ob $3.",
        "emailnotauthenticated": "Vaš e-poštni naslov še ni potrjen.\nZa navedene možnosti e-pošte ne bomo pošiljali.",
        "noemailprefs": "E-poštnega naslova niste vnesli, zato naslednje možnosti ne bodo delovale.",
        "botpasswords-label-resetpassword": "Ponastavi geslo",
        "botpasswords-label-grants": "Veljavne pravice:",
        "botpasswords-help-grants": "Vsaka pravica dodeli dostop do navedenih uporabniških pravic, ki jih uporabniški račun že ima. Za več informacij si oglejte [[Special:ListGrants|tabelo pravic]].",
-       "botpasswords-label-restrictions": "Omejitve uporabe:",
        "botpasswords-label-grants-column": "Odobreno",
        "botpasswords-bad-appid": "Ime bota »$1« ni veljavno.",
        "botpasswords-insert-failed": "Dodajanje imena bota »$1« ni uspelo. Ste ga že dodali?",
        "unlinkaccounts-success": "Račun smo razvezali.",
        "authenticationdatachange-ignored": "Sprememba overitvenih podatkov ni bila obdelana. Morda ni bil konfiguriran noben ponudnik?",
        "userjsispublic": "Pomnite: Podstrani JavaScript naj ne vsebujejo zaupnih podatkov, saj so vidne tudi drugim uporabnikom.",
-       "usercssispublic": "Pomnite: Podstrani CSS naj ne vsebujejo zaupnih podatkov, saj so vidne tudi drugim uporabnikom."
+       "usercssispublic": "Pomnite: Podstrani CSS naj ne vsebujejo zaupnih podatkov, saj so vidne tudi drugim uporabnikom.",
+       "restrictionsfield-badip": "Neveljaven IP-naslov ali obseg: $1",
+       "restrictionsfield-label": "Dovoljeni IP-obsegi:",
+       "restrictionsfield-help": "En IP-naslov ali CIDR-območje na vrstico. Da omogočite vse, uporabite<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index e2e04db..868012a 100644 (file)
        "botpasswords-label-resetpassword": "Återställ lösenordet",
        "botpasswords-label-grants": "Tillgängliga beviljanden:",
        "botpasswords-help-grants": "Varje beviljande ger åtkomst till listade användarrättigheter som ett användarkonto redan har. Se [[Special:ListGrants|tabellen över beviljanden]] för mer information.",
-       "botpasswords-label-restrictions": "Användningsbegränsningar:",
        "botpasswords-label-grants-column": "Beviljas",
        "botpasswords-bad-appid": "Botnamnet \"$1\" är inte giltigt.",
        "botpasswords-insert-failed": "Kunde inte lägga till botnamnet \"$1\". Har det redan lagts till?",
index 1cf4767..80aca5a 100644 (file)
        "botpasswords-label-delete": "Sil",
        "botpasswords-label-resetpassword": "Şifreyi sıfırla",
        "botpasswords-label-grants": "Geçerli ayrıcalıklar:",
-       "botpasswords-label-restrictions": "Kullanım kısıtlamaları:",
        "botpasswords-label-grants-column": "Verilen",
        "botpasswords-bad-appid": "Bot ismi \"$1\" geçerli değil.",
        "botpasswords-insert-failed": "Bot adı \"$1\" eklenemedi. Zaten eklenmiş olmalı?",
index 0af6224..5009a53 100644 (file)
@@ -52,7 +52,7 @@
        "tog-enotifminoredits": "Кече үзгәртүләр турында да электрон почтага хәбәр җибәрелсен",
        "tog-enotifrevealaddr": "Хәбәрләрдә e-mail адресым күрсәтелсен",
        "tog-shownumberswatching": "Битне күзәтү исемлекләренә өстәгән кулланучылар санын күрсәтелсен",
-       "tog-oldsig": "Хәзерге имза:",
+       "tog-oldsig": "Хәзерге имзагыз:",
        "tog-fancysig": "Имзаның шәхси вики-билгеләмәсе (автоматик сылтамасыз)",
        "tog-uselivepreview": "Тиз карап алуны куллану",
        "tog-forceeditsummary": "Үзгәртүләрне тасвирлау юлы тутырылмаган булса, кисәтү",
        "category-file-count-limited": "Бу төркемдә {{PLURAL:$1|$1 файл|1=бары тик бер файл}}.",
        "listingcontinuesabbrev": "дәвамы",
        "index-category": "Индексланган битләр",
-       "noindex-category": "Ð\98ндекÑ\81ланмаган битләр",
+       "noindex-category": "Ð\91илгелÓ\99нмÓ\99Ò¯Ñ\87е битләр",
        "broken-file-category": "Файлларга эшләми торган сылтамалар булган битләр",
        "about": "Тасвирлама",
        "article": "Мәкалә",
        "newwindow": "(яңа тәрәзәдә ачыла)",
        "cancel": "Баш тарту",
        "moredotdotdot": "Дәвамы…",
-       "morenotlisted": "Исемлек тулы түгел.",
+       "morenotlisted": "Исемлек тулы булмаска мөмкин.",
        "mypage": "Бит",
        "mytalk": "Бәхәс бите",
        "anontalk": "Бәхәс",
        "yourpasswordagain": "Серсүзне кабат кертү:",
        "createacct-yourpasswordagain": "Серсүзне раслагыз",
        "createacct-yourpasswordagain-ph": "Серсүзне кабаттан кертегез",
-       "remembermypassword": "Хисап язмам бу браузерда саклансын (иң күбе $1 {{PLURAL:$1|көн}})",
        "userlogin-remembermypassword": "Системада калырга",
        "userlogin-signwithsecure": "Якланган кушылу",
        "yourdomainname": "Сезнең доменыгыз:",
        "botpasswords-label-delete": "Бетерү",
        "botpasswords-label-resetpassword": "Серсүзне ташлау",
        "botpasswords-label-grants": "Кулланылган рөхсәтләр:",
-       "botpasswords-label-restrictions": "Куллану чикләүләре:",
        "botpasswords-label-grants-column": "Рөхсәт",
        "botpasswords-bad-appid": "Атамасы «$1» булган бот исеме ярамый.",
        "botpasswords-created-title": "Бот серсүзе булдырылды",
        "suppress": "Яшерү",
        "apihelp": "API ярдәм",
        "apihelp-no-such-module": "«$1» модуле табылмады.",
+       "apisandbox-reset": "Чистарту",
+       "apisandbox-retry": "Кабатлау",
+       "apisandbox-examples": "Мисаллар",
+       "apisandbox-dynamic-parameters": "Өстәмә параметрлар",
+       "apisandbox-results": "Нәтиҗәләр",
        "booksources": "Китап чыганаклары",
        "booksources-search-legend": "Китап чыганакларыны эзләү",
        "booksources-search": "Эзләү",
        "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": "Яклану көндәлеге",
        "blockipsuccesssub": "Тыю башкарылган",
        "ipb-unblock-addr": "$1 кулланучысын тыюдан азат итү",
        "ipb-unblock": "Кулланучы яки IP адресы тыюдан азат итү",
+       "ipb-blocklist-duration-left": "$1 калды",
        "unblockip": "Кулланучыны тыюдан азат итү",
        "ipusubmit": "Бу тыюны туктату",
+       "blocklist": "Тыелган кулланучылар",
        "ipblocklist": "Тыелган кулланучылар",
        "blocklist-timestamp": "Дата/вакыт",
        "blocklist-target": "Максат",
        "articleexists": "Мондый исемле бит бар инде, яисә мондый исем рөхсәт ителми.\nЗинһар башка исем сайлагыз.",
        "movetalk": "Бәйләнешле бәхәс битен күчерү",
        "movelogpage": "Күчерү көндәлеге",
+       "movesubpage": "{{PLURAL:$1|1=Асбит|Асбитләр}}",
        "movereason": "Сәбәп:",
        "revertmove": "кире кайту",
        "delete_and_move_confirm": "Әйе, битне бетерү",
        "importstart": "Битләрне импортлау...",
        "import-revision-count": "$1 {{PLURAL:$1|юрама}}",
        "importnopages": "Импортлау өчен битләр юк.",
+       "xml-error-string": "$2 юлда, $3 урында ($4 байт) $1: $5",
        "importlogpage": "Кертү көндәлеге",
        "javascripttest": "JavaScript тикшерү",
        "tooltip-pt-userpage": "{{GENDER:|Кулланучы}} битегез",
        "pageinfo-length": "Бит озынлыгы (байтларда)",
        "pageinfo-article-id": "Бит идентификаторы",
        "pageinfo-language": "Битнең теле",
+       "pageinfo-content-model-change": "үзгәртү",
        "pageinfo-robot-index": "Рөхсәт",
        "pageinfo-robot-noindex": "Рөхсәтсез",
        "pageinfo-firstuser": "Битне төзүче",
        "exif-usageterms": "Куллану шартлары",
        "exif-orientation-1": "Нормаль",
        "exif-orientation-3": "180° ка борылган",
+       "exif-componentsconfiguration-0": "юк",
+       "exif-exposureprogram-0": "Билгесез",
+       "exif-exposureprogram-1": "Кулдан җайлау режимы",
+       "exif-exposureprogram-2": "Программалы режим (гади)",
+       "exif-subjectdistance-value": "$1 {{PLURAL:$1|метр}}",
        "exif-meteringmode-0": "Билгесез",
+       "exif-meteringmode-1": "Уртача",
        "exif-meteringmode-3": "Нокталы",
        "exif-meteringmode-4": "Мультинокталы",
        "exif-meteringmode-255": "Башка",
        "exif-lightsource-4": "Яктылык",
        "exif-lightsource-9": "Яхшы һава торышы",
        "exif-lightsource-11": "Күләгә",
+       "exif-flash-mode-3": "автоматик режим",
+       "exif-focalplaneresolutionunit-2": "дюйм",
        "exif-sensingmethod-1": "Билгесез",
        "exif-scenecapturetype-0": "Стандарт",
        "exif-scenecapturetype-1": "Ландшафт",
        "exif-gpsstatus-v": "Мәгълүматларны җибәрүгә әзер",
        "exif-gpsspeed-k": "км/сәг",
        "exif-gpsspeed-m": "миля/сәг",
+       "exif-gpsspeed-n": "Төен",
+       "exif-gpsdestdistance-k": "Километр",
+       "exif-gpsdestdistance-m": "Миль",
+       "exif-gpsdestdistance-n": "Диңгез миле",
+       "exif-gpsdop-excellent": "Шәп ($1)",
+       "exif-gpsdop-good": "Яхшы ($1)",
+       "exif-gpsdop-moderate": "Уртача ($1)",
+       "exif-gpsdop-fair": "Ярыйсы ($1)",
+       "exif-gpsdop-poor": "Начар ($1)",
+       "exif-dc-date": "Дата(лар)",
+       "exif-dc-publisher": "Нәшрият",
+       "exif-dc-relation": "Бәйле медиа",
+       "exif-dc-rights": "Хокуклар",
+       "exif-dc-source": "Чыганак медиа",
+       "exif-dc-type": "Медиа төре",
+       "exif-rating-rejected": "Кире кагылды",
+       "exif-isospeedratings-overflow": "65535-тән күп",
        "namespacesall": "барлык",
        "monthsall": "барлык",
        "recreate": "Яңадан ясау",
        "confirm_purge_button": "OK",
        "confirm-purge-top": "Бу битнең кэшы чистартылсынмы?",
        "confirm-purge-bottom": "Кэшны чистартудан соң аның соңгы юрамасы күрсәтеләчәк.",
+       "confirm-watch-button": "OK",
+       "confirm-unwatch-button": "ОК",
+       "confirm-rollback-button": "ОК",
        "pipe-separator": "&#32;|&#32;",
+       "quotation-marks": "«$1»",
        "imgmultipageprev": "← алдагы бит",
        "imgmultipagenext": "алдагы бит →",
        "imgmultigo": "Күчү!",
        "imgmultigoto": "$1 битенә күчү",
+       "img-lang-go": "Башкару",
        "ascending_abbrev": "үсү",
        "descending_abbrev": "кимү",
        "table_pager_next": "Киләсе бит",
        "version-specialpages": "Махсус битләр",
        "version-other": "Башка",
        "version-hook-subscribedby": "Түбәндәгеләргә язылган:",
+       "version-no-ext-name": "[исемсез]",
        "version-license": "MediaWiki лицензиясе",
+       "version-ext-license": "Лицензия",
+       "version-ext-colheader-name": "Киңәйтүләр",
+       "version-skin-colheader-name": "Күренеш",
+       "version-ext-colheader-version": "Юрама",
+       "version-ext-colheader-license": "Лицензия",
+       "version-ext-colheader-description": "Тасвирлама",
+       "version-ext-colheader-credits": "Авторлар",
+       "version-poweredby-others": "башкалар",
+       "version-poweredby-translators": "translatewiki.net тәрҗемәчеләре",
        "version-software": "Урнаштырылган программа белән тәэмин ителешне",
        "version-software-product": "Продукт",
        "version-software-version": "Версия",
+       "version-entrypoints-header-url": "URL",
+       "version-libraries-library": "Китапханә",
+       "version-libraries-version": "Юрама",
+       "version-libraries-license": "Лицензия",
+       "version-libraries-description": "Тасвирлама",
+       "version-libraries-authors": "Авторлар",
        "fileduplicatesearch": "Бер үк файлларны эзләү",
        "fileduplicatesearch-submit": "Эзләү",
        "specialpages": "Махсус битләр",
        "tags-title": "Теглар",
        "tags-intro": "Әлеге сәхифәдә төзәтүләрне билгеләгән, программа тәэмин итә торган теглар исемлеге һәм шул тегларның аңламнары китерелгән.",
        "tags-tag": "Тег исеме",
+       "tags-source-header": "Чыганак",
+       "tags-active-yes": "Әйе",
+       "tags-active-no": "Юк",
        "tags-edit": "үзгәртү",
        "comparepages": "Битләрне чагыштыру",
        "compare-page1": "Беренче сәхифә",
        "htmlform-submit": "Җибәрү",
        "htmlform-reset": "Үзгәртүләрне кире кайтару",
        "htmlform-selectorother-other": "Башка",
+       "htmlform-no": "Юк",
+       "htmlform-yes": "Әйе",
        "htmlform-cloner-delete": "Бетерү",
        "logentry-delete-delete": "$1 $3 битен {{GENDER:$2|бетерә}}",
        "revdelete-content-hid": "эчтәлек яшерелгән",
        "rightsnone": "(юк)",
        "revdelete-summary": "үзгәртүләр тасвирламасы",
        "feedback-adding": "Фикерне сәхифәгә өстәү ...",
+       "feedback-back": "Артка",
        "feedback-bugnew": "Мин тикшердем. Яңа хата турында хәбәр итү",
        "feedback-bugornote": "Әгәр дә сез техник проблеманы җентекләп тасвирларга әзер икәнсез, зинһар өчен, [$1 хата турында хәбәр итегез].\nБашка очракта сез түбәндәге гади форманы куллана аласыз. Сезнең шәрехләмә \"[$3 $2]\" сәхифәсенә сезнең кулланучы исеме һәм сез кулланган браузер исеме белән бергә өстәләчәк.",
        "feedback-cancel": "Баш тарту",
        "feedback-close": "Әзер",
+       "feedback-error-title": "Хата",
        "feedback-error1": "Хата. APIдан билгесез нәтиҗә",
        "feedback-error2": "Хата: төзәтү уңышсыз килеп чыкты",
        "feedback-error3": "Хата: APIдан җавап юк.",
        "feedback-subject": "Тема:",
        "feedback-submit": "Җибәрү",
        "feedback-thanks": "Рәхмәт! Сезнең фикер \"[$2 $1]\" сәхифәсенә куелды.",
+       "feedback-thanks-title": "Рәхмәт!",
        "searchsuggest-search": "Эзләү",
        "searchsuggest-containing": "эчтәлек...",
        "api-error-badaccess-groups": "Сезгә бу викигә файллар өстәү рөхсәт ителмәгән",
        "api-error-unknownerror": "Билгесез хата: \"$1\".",
        "api-error-uploaddisabled": "Бу викидә файллар йөкләү мөмкинлеге сүндерелгән.",
        "api-error-verification-error": "Бәлки, бу файл бозылгандыр яки дөрес түгел киңәйтелмәгә ия.",
+       "duration-seconds": "$1 {{PLURAL:$1|секунд}}",
        "duration-minutes": "$1 {{PLURAL:$1|минут}}",
        "duration-hours": "$1 {{PLURAL:$1|сәгать}}",
        "duration-days": "$1 {{PLURAL:$1|көн}}",
+       "duration-weeks": "$1 {{PLURAL:$1|атна}}",
+       "duration-years": "$1 {{PLURAL:$1|ел}}",
+       "duration-decades": "$1 {{PLURAL:$1|дистә ел}}",
+       "duration-centuries": "$1 {{PLURAL:$1|гасыр}}",
+       "duration-millennia": "$1 {{PLURAL:$1|меңьеллык}}",
+       "limitreport-cputime-value": "$1 {{PLURAL:$1|секунд}}",
+       "limitreport-walltime-value": "$1 {{PLURAL:$1|секунд}}",
+       "limitreport-postexpandincludesize-value": "$1/$2 {{PLURAL:$2|байт}}",
+       "limitreport-templateargumentsize-value": "$1/$2 {{PLURAL:$2|байт}}",
        "expandtemplates": "Үрнәкләрне ачу",
+       "expand_templates_output": "Нәтиҗә",
        "expand_templates_ok": "OK",
+       "expand_templates_preview": "Алдан карау",
+       "pagelang-name": "Бит",
+       "pagelang-language": "Тел",
+       "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (ачык)",
+       "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>ябык</strong>)",
        "mediastatistics": "Медиа хисабы",
        "special-characters-group-latin": "Латин",
        "special-characters-group-latinextended": "Латин (киңәйтелгән)",
        "special-characters-group-persian": "Фарсы",
        "special-characters-group-hebrew": "Яхүд",
        "special-characters-group-bangla": "Бенгаль",
+       "special-characters-group-tamil": "Тамиль",
        "special-characters-group-telugu": "Телугу",
        "special-characters-group-sinhala": "Сингаль",
        "special-characters-group-gujarati": "Гуҗарати",
+       "special-characters-group-devanagari": "Деванагари",
        "special-characters-group-thai": "Таиланд",
        "special-characters-group-lao": "Лаос",
        "special-characters-group-khmer": "Кһмер"
index b708f01..0dd4408 100644 (file)
        "eauthentsent": "На вказану адресу електронної пошти відправлено лист підтвердження.\nЩоб отримувати надалі будь-які повідомлення, необхідно підтвердити, що обліковий запис належить справді Вам, за процедурою, описаною в листі.",
        "throttled-mailpassword": "Листа для оновлення пароля вже було надіслано електронною поштою протягом {{PLURAL:$1|1=останньої години|останніх $1 годин}}.\nДля попередження зловживань дозволено надсилати тільки одного листа оновлення пароля за {{PLURAL:$1|годину|$1 години|$1 годин}}.",
        "mailerror": "Помилка надсилання пошти: $1",
-       "acct_creation_throttle_hit": "Відвідувачі з вашої IP-адреси вже створили $1 {{PLURAL:$1|обліковий запис|облікових записи|облікових записів}} за останню добу, що є максимумом для цього відрізка часу.\nТаким чином, користувачі з цієї IP-адреси не можуть на цей момент створювати нових облікових записів.",
+       "acct_creation_throttle_hit": "Відвідувачі з вашої IP-адреси вже створили $1 {{PLURAL:$1|обліковий запис|облікові записи|облікових записів}} за останню $2, що є максимумом для цього відрізка часу.\nТаким чином, користувачі з цієї IP-адреси не можуть на цей момент створювати нових облікових записів.",
        "emailauthenticated": "Вашу адресу електронної пошти було підтверджено $2  о  $3.",
        "emailnotauthenticated": "Адресу вашої електронної пошти ще не підтверджено. Надсилання листів неможливе у жодній з наступних опцій.",
        "noemailprefs": "Вкажіть адресу електронної пошти, щоб уможливити наступні поштові функції вікі.",
        "botpasswords-label-resetpassword": "Скинути пароль",
        "botpasswords-label-grants": "Придатні дозволи:",
        "botpasswords-help-grants": "Кожен дозвіл дає доступ до перелічених прав користувача, які вже є у облікового запису користувача. Див. [[Special:ListGrants|таблицю дозволів]] для отримання додаткової інформації.",
-       "botpasswords-label-restrictions": "Обмеження на використання:",
        "botpasswords-label-grants-column": "Дозволено",
        "botpasswords-bad-appid": "Ім'я бота «$1» є недопустимим.",
        "botpasswords-insert-failed": "Не вдалось додати бота з іменем «$1». Можливо, він вже був доданий?",
        "passwordreset-emailelement": "Ім'я користувача: \n$1\n\nТимчасовий пароль: \n$2",
        "passwordreset-emailsentemail": "Якщо ця електронна адреса асоційована з вашим обліковим записом, то лист для відновлення пароля буде відправлено на неї.",
        "passwordreset-emailsentusername": "Якщо існує електронна адреса, яка асоційована з цим обліковим записом, на неї буде надіслано лист для відновлення пароля.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Електронний лист|Електронні листи}} скидання паролю було надіслано. {{PLURAL:$1|Ім'я користувача і пароль|Список імен користувачів і паролів}} показано нижче.",
-       "passwordreset-emailerror-capture2": "Не вдалося надіслати листа {{GENDER:$2|користувачу|користувачці}}: $1 {{PLURAL:$3|Ім'я користувача і пароль|список імен користувачів і паролів}} показано нижче.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Електронний лист|Електронні листи}} скидання паролю було надіслано. {{PLURAL:$1|Ім'я користувача і пароль|Список імен користувачів і паролів}} показано тут.",
+       "passwordreset-emailerror-capture2": "Не вдалося надіслати листа {{GENDER:$2|користувачу|користувачці}}: $1 {{PLURAL:$3|Ім'я користувача і пароль|список імен користувачів і паролів}} показано тут.",
        "passwordreset-nocaller": "Має бути надане джерело виклику",
        "passwordreset-nosuchcaller": "Джерело виклику не існує: $1",
        "passwordreset-ignored": "Скидання пароля не відбулося. Можливо, не було налашатовано надавача?",
        "unlinkaccounts-success": "Обліковий запис було відв'язано.",
        "authenticationdatachange-ignored": "Неопрацьована зміна облікових даних. Можливо, жоден з провайдерів не був налаштований?",
        "userjsispublic": "Будь ласка, зверніть увагу: підсторінки JavaScript не повинні містити конфіденційних даних, бо їх можуть бачити інші користувачі.",
-       "usercssispublic": "Будь ласка, зверніть увагу: підсторінки CSS не повинні містити конфіденційних даних, бо їх можуть бачити інші користувачі."
+       "usercssispublic": "Будь ласка, зверніть увагу: підсторінки CSS не повинні містити конфіденційних даних, бо їх можуть бачити інші користувачі.",
+       "restrictionsfield-badip": "Недійсна IP-адреса або діапазон: $1",
+       "restrictionsfield-label": "Дозволені діапазони IP-адрес:",
+       "restrictionsfield-help": "Одна IP-адреса або CIDR-діапазон на рядок. Щоб увімкнути все, використайте<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index 1cdb51b..7c0b6c2 100644 (file)
        "actions": "اقدامات",
        "namespaces": "نام فضا",
        "variants": "متغیرات",
-       "navigation-heading": "Ù\82ائÙ\85Û\81 رہنمائی",
+       "navigation-heading": "Ù\81Û\81رست رہنمائی",
        "errorpagetitle": "نقص",
        "returnto": "واپس $1 پر جائیں",
        "tagline": "{{SITENAME}} سے",
        "personaltools": "ذاتی آلات",
        "articlepage": "مندرجاتی صفحہ دیکھیے",
        "talk": "تبادلہٴ خیال",
-       "views": "مناظر",
+       "views": "مشاہدات",
        "toolbox": "آلات",
        "tool-link-userrights": "{{GENDER:$1|صارف}} کے گروہوں میں تبدیلی کریں",
        "tool-link-emailuser": "اس {{GENDER:$1|صارف}} کو برقی خط لکھیں",
        "lastmodifiedat": "اس صفحہ میں آخری بار مورخہ $1ء کو $2 بجے ترمیم کی گئی۔",
        "viewcount": "اِس صفحہ تک {{PLURAL:$1|ایک‌بار|$1 مرتبہ}} رسائی کی گئی",
        "protectedpage": "محفوظ شدہ صفحہ",
-       "jumpto": ":چھلانگ بطرف",
+       "jumpto": "یہاں جائیں:",
        "jumptonavigation": "رہنمائی",
-       "jumptosearch": "تلاش",
+       "jumptosearch": "تلاش کریں",
        "view-pool-error": "معذرت کے ساتھ، تمام معیلات پر اِس وقت اِضافی بوجھ ہے.\nبہت زیادہ صارفین اِس وقت یہ صفحہ ملاحظہ کرنے کی کوشش کررہے ہیں.\nبرائے مہربانی! صفحہ دیکھنے کیلئے دوبارہ کوشش کرنے سے پہلے ذرا انتظار فرمالیجئے.\n\n$1",
        "generic-pool-error": "ہم معذرت خواہ ہیں! معیلات (سرورز) پر اِس وقت اِضافی بوجھ ہے.\nصارفین کی کثیر تعداد اِس وقت یہی صفحہ ملاحظہ کرنے کی کوشش کررہی ہے.\nبرائے مہربانی!دوبارہ کوشش کرنے سے پہلے ذرا انتظار فرمائیے.",
        "pool-timeout": "مقفل کرنے کے لیے انتظار کی مہلت ختم",
        "currentevents": "حالیہ واقعات",
        "currentevents-url": "Project:حالیہ واقعات",
        "disclaimers": "اظہار لا تعلقی",
-       "disclaimerpage": "Project:عام اعلان",
+       "disclaimerpage": "Project:عمومی اظہار لا تعلقی",
        "edithelp": "معاونت براۓ ترمیم",
        "helppage-top-gethelp": "مدد",
        "mainpage": "صفحۂ اول",
        "editlink": "ترمیم",
        "viewsourcelink": "ماخذ دیکھیں",
        "editsectionhint": "ترمیم قطعہ: $1",
-       "toc": "Ù\81Û\81رست",
+       "toc": "Ù\85Ù\86درجات",
        "showtoc": "دکھائیں",
        "hidetoc": "چھپائیں",
        "collapsible-collapse": "خاتمے",
        "delete-hook-aborted": "حذف شدگی روک دی گئی\nوضاحت نہیں کی گئی",
        "no-null-revision": "صفحہ \"$1\" کے لیے نیا خالی نسخہ نہیں بنایا جا سکتا",
        "badtitle": "خراب عنوان",
-       "badtitletext": "درخواست شدہ صفحہ کا عنوان ناقص، خالی، یا کوئی غلط ربط شدہ بین لسانی یا بین ویکی عنوان ہے.\nشاید اِس میں ایک یا زیادہ ایسے حروف موجود ہوں جو عنوانات میں استعمال نہیں ہوسکتے.",
+       "badtitletext": "درخواست شدہ صفحہ کا عنوان ناقص، خالی، یا کوئی غلط ربط شدہ بین اللسانی یا بین الویکی عنوان ہے۔\nشاید اِس میں ایک یا زیادہ ایسے حروف موجود ہوں جو عنوانات میں استعمال نہیں ہو سکتے۔",
        "title-invalid-empty": "درخواست شدہ عنوان خالی ہے یا اس میں محض نام فضا کا نام ہے۔",
        "title-invalid-utf8": "درخواست شدہ عنوان میں نادرست یونیکوڈ حروف موجود ہیں۔",
        "title-invalid-interwiki": "درخواست شدہ عنوان میں ایسا بین الویکی ربط موجود ہے جسے عنوانات میں استعمال نہیں کیا جا سکتا۔",
        "perfcached": "ذیلی ڈیٹا ابطن شدہ (cached) ہے اور اِس کے پُرانے ہونے کا امکان ہے. A maximum of {{PLURAL:$1|one result is|$1 results are}} available in the cache.",
        "perfcachedts": "ذیل میں درج معلومات کیشے شدہ ہے اور آخری بار اس کی تجدید $1 کو کی گئی تھی۔ کیشے میں زیادہ سے زیادہ {{PLURAL:$4|ایک نتیجہ دستیاب ہے|$4 دستیاب ہیں}}۔",
        "querypage-no-updates": "اِس صفحہ کیلئے بتاریخات فی الحال ناقابل بنائی گئی ہیں.\nیہاں کا ڈیٹا ابھی تازہ نہیں کیا جائے گا.",
-       "viewsource": "Ù\85سÙ\88دÛ\81",
+       "viewsource": "Ù\85اخذ Ø¯Û\8cÚ©Ú¾Û\8cÚº",
        "viewsource-title": "$1 کا مسودہ دیکھیں",
        "actionthrottled": "Action throttled",
        "actionthrottledtext": "ایک ضد سپم معیار کے طور پر آپ کے لیے مختصر وقت میں متعدد دفعہ یہ اقدام کرنے کے لیے حد متعین کی گئی ہے، اور آپ یہ حد پار کرچکے ہیں.\nبراہِ کرم، کچھ منٹس بعد دوبارہ کوشش کریں۔",
        "userlogin-yourname-ph": "اپنا صارف نام درج کریں",
        "createacct-another-username-ph": "صارف نام درج کریں",
        "yourpassword": "پاس ورڈ:",
-       "userlogin-yourpassword": "کلمۂ شناخت",
+       "userlogin-yourpassword": "پاس ورڈ",
        "userlogin-yourpassword-ph": "اپنا کلمہ شناخت دیں",
-       "createacct-yourpassword-ph": "ایک پاس ورڈ داخل کریں",
+       "createacct-yourpassword-ph": "پاس ورڈ درج کریں",
        "yourpasswordagain": "کلمۂ شناخت دوبارہ لکھیں",
        "createacct-yourpasswordagain": "پاس ورڈ کی تصدیق کریں",
-       "createacct-yourpasswordagain-ph": "پاس ورڈ پھر داخل کریں",
-       "userlogin-remembermypassword": "Ù\85جھÛ\92 Ø¯Ø§Ø®Ù\84 Ø±Ú©Ú¾Û\92",
+       "createacct-yourpasswordagain-ph": "پاس ورڈ دوبارہ درج کریں",
+       "userlogin-remembermypassword": "Ù\84اگ Ø§Ù\86 Ø¨Ø±Ù\82رار Ø±Ú©Ú¾Û\8cÚº",
        "userlogin-signwithsecure": "محفوظ رابطہ (کنکشن) استعمال کریں",
        "cannotlogin-title": "داخل نہیں ہو سکتے",
        "cannotlogin-text": "داخل ہونا ممکن نہیں۔",
        "userlogin-reauth": "آپ {{GENDER:$1|$1}} ہیں، اس کی تصدیق کے لیے آپ کا داخل ہونا ناگزیر ہے۔",
        "userlogin-createanother": "دوسرا کھاتہ تخلیق کریں",
        "createacct-emailrequired": "ای میل پتہ",
-       "createacct-emailoptional": "اÛ\8c Ù\85Û\8cÙ\84 Ø§Û\8cÚ\88رÛ\8cس (اختیاری)",
+       "createacct-emailoptional": "برÙ\82Û\8c Ú\88اک Ù¾ØªØ§ (اختیاری)",
        "createacct-email-ph": "اپنا برقی پتہ لکھیں",
-       "createacct-another-email-ph": "برقی پتہ لکھیں",
+       "createacct-another-email-ph": "برقی ڈاک پتا لکھیں",
        "createaccountmail": "عارضی پاسورڈ استعمال کریں اور اسے متعینہ برقی ڈاک پتہ پر ارسال کریں",
        "createaccountmail-help": "پاس ورڈ معلوم کیے بغیر کسی دوسرے شخص کا کھاتہ بنانے کے لیے اسے استعمال کیا جا سکتا ہے۔",
        "createacct-realname": "اصلی نام (اختیاری)",
        "createacct-reason": "وجہ",
        "createacct-reason-ph": "آپ دوسرا کھاتہ کیوں تخلیق کررہے ہیں",
        "createacct-reason-help": "نوشتہ کھاتہ سازی میں نظر آنے والا پیغام",
-       "createacct-submit": "آپ Ú©ا کھاتا بنائیں",
+       "createacct-submit": "اپÙ\86ا کھاتا بنائیں",
        "createacct-another-submit": "کھاتہ بنائیں",
        "createacct-continue-submit": "کھاتہ سازی جاری رکھیں",
        "createacct-another-continue-submit": "کھاتہ سازی جاری رکھیں",
-       "createacct-benefit-heading": "{{SITENAME}} آپ جیسے لوگوں کی طرف سے بنایا گیا ہے ۔",
+       "createacct-benefit-heading": "{{SITENAME}} آپ جیسے علم دوست افراد کا مرہون منت ہے۔",
        "createacct-benefit-body1": "{{PLURAL:$1|ترمیم|ترامیم}}",
-       "createacct-benefit-body2": "$1 {{PLURAL:$1|صفحہ|صفحات}}",
-       "createacct-benefit-body3": "حالیہ {{PLURAL:$1|شرکت کرنے والا|شرکت کرنے والے}}",
+       "createacct-benefit-body2": "$1 {{PLURAL:$1|مضمون|مضامین}}",
+       "createacct-benefit-body3": "حالیہ {{PLURAL:$1|مشارکت کنندہ|مشارکت کنندگان}}",
        "badretype": "درج شدہ کلمۂ شناخت اصل سے مطابقت نہیں رکھتا۔",
        "usernameinprogress": "انتظار فرمائیے!<br />\nاس صارف نام سے کھاتہ بننے کا عمل ابھی جاری ہے۔",
        "userexists": "داخل کردہ اسم صارف پہلے سے مستعمل ہے۔\nبراہِ کرم! کوئی دوسرا اسم منتخب کیجئے۔",
        "eauthentsent": "ایک تصدیقی برقی خط نامزد کیے گئے برقی پتہ پر ارسال کردیا گیا ہے۔\nآپ کو موصول ہوئے برقی خط میں ہدایات پر عمل کرکے اس بات کی توثیق کرلیں کہ مذکورہ برقی پتہ آپ کا ہی ہے۔",
        "throttled-mailpassword": "گزشتہ {{PLURAL:$1|گھنٹے|$1 گھنٹوں}} کے دوران پہلے سے ہی پارلفظ (پاسورڈ) کی تبدیلی کے لیے برقی خط بھیجا گيا ہے۔\nناجائز استعمال کے سدّباب کیلئے، {{PLURAL:$1|گھنٹہ|$1 گھنٹوں}} کے دوران صرف ایک برقی خط بھیجا جاسکتا ہے۔",
        "mailerror": "مسلہ دوران ترسیل خط:$1",
-       "acct_creation_throttle_hit": "آپکی آئی.پی کے ذریعے اِس ویکی پر آنے والے صارفین نے پچھلے ایک دِن میں {{PLURAL:$1|1 کھاتہ بنایا ہے|$1 کھاتے بنائے ہیں}}، جو کہ مذکورہ وقت میں کافی ہیں.\nلہٰذا، آپکی آئی.پی استعمال کرنے والے صارفین اِس وقت مزید کھاتے نہیں بناسکتے.",
+       "acct_creation_throttle_hit": "آپکی آئی پی کے ذریعے اِس ویکی پر آنے والے صارفین نے پچھلے $2 میں {{PLURAL:$1|1 کھاتہ بنایا ہے|$1 کھاتے بنائے ہیں}} جو اس مدت کے لیے کافی ہیں۔\nلہٰذا آپ کی آئی پی استعمال کرنے والے صارفین اِس وقت مزید کھاتے نہیں بنا سکتے۔",
        "emailauthenticated": "آپ کے برقی ڈاک پتہ کی تصدیق مورخہ $2 بوقت $3 بجے ہوئی۔",
        "emailnotauthenticated": "آپ کے برقی پتہ کی ابھی تصدیق نہیں ہوئی ہے۔\nدرج ذیل میں سے کسی بھی چیز کیلئے آپ کے برقی پتہ پر برقی ڈاک ارسال نہیں کی جائے گی۔",
        "noemailprefs": "اِن خصائص کو کام میں لانے کیلئے اپنے ترجیحات میں برقی ڈاک کا پتہ متعین کیجئے.",
        "loginlanguagelabel": "زبان: $1",
        "suspicious-userlogout": "کھاتے سے خارج ہونے کی درخواست رد کر دی گئی ہے کیونکہ ایسا معلوم ہوتا ہے یہ درخواست کسی شکستہ براؤزر یا کیشے کی حامل پراکسی سے بھیجی گئی تھی۔",
        "createacct-another-realname-tip": "حقیقی نام اختیاری ہے۔\nاگر آپ اسے فراہم کریں تو آپ کے کاموں کو اس نام سے منسوب کرنے کے لیے استعمال کیا جائے گا۔",
-       "pt-login": "داخل ہوجائیے",
+       "pt-login": "داخل ہوں",
        "pt-login-button": "داخل ہو",
        "pt-login-continue-button": "داخل ہوں",
        "pt-createaccount": "کھاتا بنائیں",
        "botpasswords-label-delete": "حذف کریں",
        "botpasswords-label-resetpassword": "پاس ورڈ تبدیل کریں",
        "botpasswords-label-grants": "قابل تطبیق عطیے:",
-       "botpasswords-label-restrictions": "استعمال کی پابندیاں:",
        "botpasswords-label-grants-column": "دے دیا گیا",
        "botpasswords-bad-appid": "روبہ نام \"$1\" درست نہیں۔",
        "botpasswords-insert-failed": "روبہ نام \"$1\" کو شامل کرنے میں ناکامی۔ کیا اسے پہلے شامل کیا جا چکا ہے؟",
        "passwordreset-emailelement": "صارف نام:\n$1\n\nعارضی پاس ورڈ: \n$2",
        "passwordreset-emailsentemail": "اگر یہ برقی ڈاک پتا آپ کے کھاتے سے منسلک ہے تو پاس ورڈ کی ترتیب نو کا برقی خط بھیج دیا جائے گا۔",
        "passwordreset-emailsentusername": "اگر کوئی برقی ڈاک پتا آپ کے کھاتے سے منسلک ہے تو پاس ورڈ کی ترتیب نو کا برقی خط بھیج دیا جائے گا۔",
-       "passwordreset-emailsent-capture2": "پاس ورڈ کی ترتیب نو {{PLURAL:$1|کا برقی خط بھیج دیا گیا ہے|کے برقی خطوط بھیج دیے گئے ہیں}}۔ {{PLURAL:$1|صارف نام اور پاس ورڈ|صارف ناموں اور ان کے پاس ورڈ کی فہرست}} ذیل میں ملاحظہ فرمائیں۔",
-       "passwordreset-emailerror-capture2": "{{GENDER:$2|صارف}} کو برقی خط بھیجنے میں ناکامی: $1\n{{PLURAL:$3|صارف نام اور پاس ورڈ|صارف ناموں کی فہرست اور ان کے پاس ورڈ}} ذیل میں ملاحظہ فرمائیں۔",
+       "passwordreset-emailsent-capture2": "پاس ورڈ کی ترتیب نو {{PLURAL:$1|کا برقی خط بھیج دیا گیا ہے|کے برقی خطوط بھیج دیے گئے ہیں}}۔ {{PLURAL:$1|صارف نام اور پاس ورڈ|صارف ناموں اور ان کے پاس ورڈ کی فہرست}} یہاں ملاحظہ فرمائیں۔",
+       "passwordreset-emailerror-capture2": "{{GENDER:$2|صارف}} کو برقی خط بھیجنے میں ناکامی: $1\n{{PLURAL:$3|صارف نام اور پاس ورڈ|صارف ناموں کی فہرست اور ان کے پاس ورڈ}} یہاں ملاحظہ فرمائیں۔",
        "passwordreset-nocaller": "کالر کا فراہم کیا جانا لازمی ہے",
        "passwordreset-nosuchcaller": "کالر موجود نہیں: $1",
-       "passwordreset-ignored": "پاس ورڈ کی ترتیب نو مکمل نہیں ہو سکی۔ شاید کوئی پرووائڈر فراہم نہیں کیا گیا تھا؟",
+       "passwordreset-ignored": "پاس ورڈ کی ترتیب نو مکمل نہیں ہو سکی۔ شاید کوئی پرووائڈر فراہم نہیں کیا گیا؟",
        "passwordreset-invalideamil": "نادرست برقی ڈاک پتا",
        "passwordreset-nodata": "کوئی صارف نام اور نہ کوئی برقی ڈاک پتا فراہم کیا گیا",
        "changeemail": "برقی ڈاک پتا تبدیل یا حذف کریں",
        "link_sample": "ربط کا عنوان",
        "link_tip": "اندرونی ربط",
        "extlink_sample": "http://www.example.com ربط کا عنوان",
-       "extlink_tip": "بیرونی ربط (یاد رکھئے http:// prefix)",
+       "extlink_tip": "بیرونی ربط (http:// کا سابقہ نہ بھولیں)",
        "headline_sample": "شہ سرخی",
        "headline_tip": "شہ سرخی درجہ دوم",
        "nowiki_sample": "غیرشکلبندشدہ متن یہاں درج کریں",
-       "nowiki_tip": "ویکی شکلبندی نظرانداز کریں",
-       "image_tip": "Ù¾Û\8cÙ\88ستÛ\81 Ù\85Ù\84Ù\81",
+       "nowiki_tip": "ویکی فارمیٹ کو نظرانداز کریں",
+       "image_tip": "Ù¾Û\8cÙ\88ستÛ\81 Ù\81ائÙ\84",
        "media_tip": "فائل کا ربط",
        "sig_tip": "آپکا دستخط بمع مہرِوقت",
        "hr_tip": "اُفقی لکیر (زیادہ استعمال نہ کریں)",
        "edit_form_incomplete": "<strong>خانہ ترمیم سے کچھ حصے سرور تک نہیں پہنچ سکے ہیں؛ براہ کرم اپنی ترامیم کو دوبارہ جانچ لیں کہ آیا وہ برقرار ہیں یا نہیں اور دوبارہ کوشش کریں۔</strong>",
        "editing": "آپ \"$1\" میں ترمیم کر رہے ہیں۔",
        "creating": "زیر تخلیق $1",
-       "editingsection": "$1 Ú©Û\92 Ù\82طعÛ\81 Ú©Û\8c ØªØ¯Ù\88Û\8cÙ\86",
+       "editingsection": "$1 Ú©Û\92 Ù\82طعÛ\81 Ú©Û\8c ØªØ±Ù\85Û\8cÙ\85",
        "editingcomment": "زیرترمیم $1 (نیا قطعہ)",
        "editconflict": "تنازعہ ترمیم:$1",
        "explainconflict": "آپکی تدوین شروع ہونے کے بعد شاید کسی نے یہ صفحہ تبدیل کردیا ہے.\nبالائی خانۂ متن میں صفحہ کا موجودہ مواد ہے.\nآپ کی تبدیلیاں نچلے متن خانہ میں دکھائی گئی ہیں.\nآپ کو اپنی تبدیلیاں موجودہ متن میں ضم کرنا ہوں گی.\n\"محفوظ\" کا بٹن ٹک کرنے سے '''صرف''' بالائی متن محفوظ ہوگا.",
        "content-model-wikitext": "ویکی متن",
        "content-model-text": "سادہ متن",
        "content-model-javascript": "جاوا اسکرپٹ",
+       "content-model-css": "سی ایس ایس",
        "content-json-empty-object": "خالی آبجیکٹ",
        "content-json-empty-array": "خالی ایرے",
        "deprecated-self-close-category": "صفحات مع نادرست ایچ ٹی ایم ایل ٹیگ",
        "currentrev-asof": "حالیہ نسخہ بمطابق $1",
        "revisionasof": "نسخہ بمطابق $1",
        "revision-info": "نظرثانی بتاریخ $1 از {{GENDER:$6|$2}}$7",
-       "previousrevision": "â\86\90پراÙ\86Û\8c ØªØ¯Ù\88Û\8cÙ\86",
+       "previousrevision": "â\86\92 Ù¾Ø±Ø§Ù\86ا Ù\86سخÛ\81",
        "nextrevision": "→اگلا اعادہ",
        "currentrevisionlink": "حالیہ نظرثانی",
        "cur": " رائج",
        "rev-suppressed-unhide-diff": "اس فرق کی کسی ایک ترمیم کو <strong>پوشیدہ کر دیا گیا ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔\nتاہم اگر آپ چاہیں تو [$1 اس فرق کو ابھی بھی دیکھ سکتے ہیں]۔",
        "rev-deleted-diff-view": "اس فرق کی کسی ایک ترمیم کو <strong>حذف کر دیا گیا ہے</strong>۔\nآپ اس فرق کو دیکھ سکتے ہیں؛ مزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
        "rev-suppressed-diff-view": "اس فرق کی کسی ایک ترمیم کو <strong>پوشیدہ کر دیا گیا ہے</strong>۔\nآپ اس فرق کو دیکھ سکتے ہیں؛ مزید تفصیلات [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} نوشتہ پوشیدگی] میں دیکھی جا سکتی ہیں۔",
-       "rev-delundel": "دکھاؤ/چھپاؤ",
+       "rev-delundel": "مرئیت تبدیل کریں",
        "rev-showdeleted": "دکھاؤ",
        "revisiondelete": "نظرثانی حذف کریں/واپس لائیں",
        "revdelete-nooldid-title": "ناقص مقصود نظرثانی",
        "revdelete-confirm": "برائے مہربانی! یقین دِہانی کرلیجئے کہ آپ واقعی ایسا کرنا چاہتے ہیں، آپ اِس کے نتائج سے باخبر ہیں، اور آپ یہ [[{{MediaWiki:Policy-url}}|پالیسی]] کے مطابق کررہے ہیں.",
        "revdelete-legend": "رویتی پابندیاں لگائیں",
        "revdelete-hide-text": "نظرثانی متن چھپاؤ",
-       "revdelete-hide-image": "Ù\85Ø´Ù\85Ù\88Ù\84اتÙ\90 Ù\85Ù\84Ù\81 Ú\86ھپاؤ",
+       "revdelete-hide-image": "Ù\81ائÙ\84 Ú©Û\92 Ù\85Ø´Ù\85Ù\88Ù\84ات Ú\86ھپائÛ\8cÚº",
        "revdelete-hide-name": "ہدف اور پیرامیٹرز کو چھپائیں",
        "revdelete-hide-comment": "ترمیمی تبصرہ چھپاؤ",
        "revdelete-hide-user": "ترمیم کار کا اسمِ صارف / آئی.پی پتہ چُھپاؤ",
        "difference-title": "\"$1\" کے نسخوں کے درمیان فرق",
        "difference-title-multipage": "«$1» اور «$2» صفحوں کے درمیان فرق",
        "difference-multipage": "(فرق مابین صفحات)",
-       "lineno": "لکیر $1:",
+       "lineno": "سطر $1:",
        "compareselectedversions": "منتخب متـن کا موازنہ",
        "showhideselectedversions": "منتخب نسخوں کی مرئیت تبدیل کریں",
        "editundo": "رد ترمیم",
        "diff-multi-otherusers": "({{PLURAL:$2|ایک دوسرے صارف|$2 صارفین}} {{PLURAL:$1|کا ایک درمیانی نسخہ نہیں دکھایا گیا|$1 کے درمیانی نسخے نہیں دکھائے گئے}})",
        "diff-multi-manyusers": "($2 سے زیادہ {{PLURAL:$2|صارف|صارفین}} {{PLURAL:$1|کا ایک درمیانی نسخہ نہیں دکھایا گیا|$1 کے درمیانی نسخے نہیں دکھائے گئے}})",
        "difference-missing-revision": "اس فرق ($1) {{PLURAL:$2|کا ایک نسخہ نہیں ملا|$2 کے نسخے نہیں ملے}}۔\n\nعموماً ایسا اس وقت ہوتا ہے جب کسی حذف شدہ صفحہ کے نسخوں کے درمیان میں فرق تلاش کرنے کی کوشش کی جائے۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
-       "searchresults": "تلاش کا نتیجہ",
-       "searchresults-title": "نتائجِ تلاش برائے \"$1\"",
+       "searchresults": "تلاش کے نتائج",
+       "searchresults-title": "«$1» کے نتائج تلاش",
        "titlematches": "عنوان صفحہ سے ملتا ہے",
        "textmatches": "متن صفحہ سے ملتا ہے",
        "notextmatches": "کوئی بھی مماثل متن موجود نہیں",
        "prev-page": "پچھلا صفحہ",
        "next-page": "اگلا صفحہ",
        "prevn-title": "پچھلے $1 {{PLURAL:$1|نتیجہ|نتائج}}",
-       "nextn-title": "آگے $1 {{PLURAL:$1|نتیجہ|نتائج}}",
-       "shown-title": "فی صفحہ $1 {{PLURAL:$1|نتیجہ|نتائج}} دِکھاؤ",
+       "nextn-title": "{{PLURAL:$1|اگلا|اگلے}} $1 {{PLURAL:$1|نتیجہ|نتائج}}",
+       "shown-title": "فی صفحہ $1 {{PLURAL:$1|نتیجہ|نتائج}} دکھائیں",
        "viewprevnext": "($1 {{int:pipe-separator}} $2) دیکھیں ($3)",
        "searchmenu-exists": "<strong>اِس ویکی پر «[[:$1]]» نامی ایک صفحہ موجود ہے۔</strong> {{PLURAL:$2|0=|تلاش کے دیگر نتائج بھی ملاحظہ فرمائیں۔}}",
        "searchmenu-new": "<strong>صفحہ \"[[:$1]]\" کو اس ویکی پر تخلیق کریں</strong> {{PLURAL:$2|0=|وہ صفحہ بھی دیکھے جو ٓپ کے تلاش میں پایا گیا|ان نتائج کو بھی دیکھے جو پائے گئے}}",
-       "searchprofile-articles": "مشمولاتی صفحات",
-       "searchprofile-images": "کثیرالوسیط",
+       "searchprofile-articles": "مواد کے حامل صفحات",
+       "searchprofile-images": "ملٹی میڈیا",
        "searchprofile-everything": "سب کچھ",
        "searchprofile-advanced": "پیشرفتہ",
-       "searchprofile-articles-tooltip": "$1 میں تلاش",
-       "searchprofile-images-tooltip": "تلاش برائے ملفات",
-       "searchprofile-everything-tooltip": " تلاش تمام مشمولات (بشمول تبادلۂ خیال صفحات) میں",
-       "searchprofile-advanced-tooltip": "اپÙ\86Û\8c Ù¾Ø³Ù\86د Ú©Û\92 Ø¬Ø§Ø¦Û\92 Ù\86اÙ\85 Ù\85Û\8cÚº ØªÙ\84اش",
+       "searchprofile-articles-tooltip": "$1 میں تلاش کریں",
+       "searchprofile-images-tooltip": "فائلیں تلاش کریں",
+       "searchprofile-everything-tooltip": "تمام مندرجات (بشمول تبادلۂ خیال صفحات) میں تلاش کریں",
+       "searchprofile-advanced-tooltip": "حسب Ù\85رضÛ\8c Ù\86اÙ\85 Ù\81ضا Ù\85Û\8cÚº ØªÙ\84اش Ú©Ø±Û\8cÚº",
        "search-result-size": "$1 ({{PLURAL:$2|1 لفظ|$2 الفاظ}})",
-       "search-result-category-size": "{{PLURAL:$1|1 رُکن|$1 اراکین}} ({{PLURAL:$2|1 ذیلی زمرہ|$2 ذیلی زمرہ جات}}, {{PLURAL:$3|1 ملف|$3 ملفات}})",
+       "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",
        "prefs-watchlist-token": "زیر نظر فہرست کی کلید:",
        "prefs-misc": "دیگر",
        "prefs-resetpass": "پاس ورڈ تبدیل کریں",
-       "prefs-changeemail": "برقی ڈاک پتہ (e-mail address) تبدیل کریں",
+       "prefs-changeemail": "برقی ڈاک پتا تبدیل یا حذف کریں",
        "prefs-setemail": "برقی پتہ دیں",
        "prefs-email": "برقی خط کے اختیارات",
        "prefs-rendering": "ظاہریت",
        "prefs-help-realname": "حقیقی نام اختیاری ہے۔\nاگر آپ درج کریں تو اسے آپ کے کاموں کو آپ سے منسوب کرنے کے لیے استعمال کیا جائے گا۔",
        "prefs-help-email": "برقی ڈاک پتے کا اندراج اختیاری ہے، عموماً اس کی ضرورت اس وقت پڑتی ہے جب آپ اپنا پاس ورڈ بھول چکے ہوں اور نیا پاس ورڈ رکھنا چاہتے ہوں۔",
        "prefs-help-email-others": "یہ ممکن ہے کہ آپ دیگر صارفین کو اس بات کی اجازت دیں کہ وہ آپ کے صارف یا تبادلۂ خیال صفحہ پر موجود ربط کے ذریعہ آپ کو برقی خط بھیج سکیں۔\nجب صارفین اس طرح آپ سے رابطہ کریں گے تو انہیں آپ کا برقی ڈاک پتہ نظر نہیں آئے گا۔",
-       "prefs-help-email-required": "برقی ڈاک پتہ چاہئے.",
+       "prefs-help-email-required": "برقی ڈاک پتا درکار ہے۔",
        "prefs-info": "بنیادی معلومات",
        "prefs-i18n": "بین الاقوامیت",
        "prefs-signature": "دستخط",
        "recentchanges-label-minor": "یہ ایک معمولی ترمیم ہے",
        "recentchanges-label-bot": "اس ترمیم کو ایک روبہ نے انجام دیا ہے",
        "recentchanges-label-unpatrolled": "اس ترمیم کی اب تک مراجعت نہیں کی گئی",
-       "recentchanges-label-plusminus": "صÙ\81Ø­Û\81 Ú©Ø§ Ø­Ø¬Ù\85 ØªØ¨Ø¯Û\8cÙ\84 Ø´Ø¯Û\81 Ø¨Ù\84حاظ Ø¨Ø§Ø¦Ù¹ Ù\85Ù\82دار",
+       "recentchanges-label-plusminus": "صÙ\81Ø­Û\81 Ú©Ø§ ØªØ¨Ø¯Û\8cÙ\84 Ø´Ø¯Û\81 Ø­Ø¬Ù\85 Ø¨Ù\84حاظ ØªØ¹Ø¯Ø§Ø¯ Ø¨Ø§Ø¦Ù¹",
        "recentchanges-legend-heading": "<strong>اختصارات:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (نیز [[Special:NewPages|جدید صفحات کی فہرست]]) ملاحظہ فرمائیں",
        "recentchanges-submit": "دکھائیں",
        "rcshowhideminor": "معمولی ترامیم $1",
        "rcshowhideminor-show": "دکھائیں",
        "rcshowhideminor-hide": "چھپائیں",
-       "rcshowhidebots": "خودکار صارف $1",
+       "rcshowhidebots": "خودکار صارفین $1",
        "rcshowhidebots-show": "دکھائیں",
        "rcshowhidebots-hide": "چھپائیں",
-       "rcshowhideliu": "داخل شدہ صارف $1",
+       "rcshowhideliu": "مندرج صارفین $1",
        "rcshowhideliu-show": "دکھائیں",
        "rcshowhideliu-hide": "چھپائیں",
        "rcshowhideanons": "گمنام صارف $1",
        "recentchangeslinked-feed": "متعلقہ تبدیلیاں",
        "recentchangeslinked-toolbox": "متعلقہ تبدیلیاں",
        "recentchangeslinked-title": "\"$1\" سے متعلقہ تبدیلیاں",
-       "recentchangeslinked-summary": "یہ ان تبدیلیوں کی فہرست ہے جو حال ہی میں کسی مخصوص صفحہ سے مربوط صفحات (یا مخصوص زمرہ کے اراکین) میں کی گئی ہیں\n\n[[Special:Watchlist|آپ کی زیر نظر فہرست]] میں یہ صفحات متجل (bold) نظر آئیں گےـ",
-       "recentchangeslinked-page": "صÙ\81Ø­Û\82 Ù\85Ù\86صÙ\88بÛ\81 Ø¯Û\8cکھئÛ\92",
+       "recentchangeslinked-summary": "یہ ان تبدیلیوں کی فہرست ہے جو حال ہی میں کسی مخصوص صفحہ سے مربوط صفحات (یا مخصوص زمرہ کے اراکین) میں کی گئی ہیں۔\n\n[[Special:Watchlist|آپ کی زیر نظر فہرست]] میں یہ صفحات <strong>جلی</strong نظر آئیں گےـ",
+       "recentchangeslinked-page": "صÙ\81Ø­Û\81 Ú©Ø§ Ù\86اÙ\85:",
        "recentchangeslinked-to": "اس کی بجائے درج کردہ صفحہ سے مربوط صفحات کی تبدیلیاں دکھائیں",
        "recentchanges-page-added-to-category": "[[:$1]] کو زمرہ میں شامل کیا گیا",
        "recentchanges-page-added-to-category-bundled": "[[:$1]] کو زمرہ میں شامل کر دیا گیا، [[Special:WhatLinksHere/$1|یہ صفحہ دیگر صفحات میں بھی موجود ہے]]",
        "recentchanges-page-removed-from-category": "[[:$1]] کو زمرہ سے ہٹایا",
        "recentchanges-page-removed-from-category-bundled": "[[:$1]] زمرے سے ہٹا دیا گیا ہے، [[Special:WhatLinksHere/$1|یہ صفحہ دیگر صفحات میں بھی موجود ہے]]",
        "autochange-username": "میڈیاویکی خودکار تبدیلیاں",
-       "upload": "اپلوڈ",
+       "upload": "فائل اپلوڈ کریں",
        "uploadbtn": "فائل اپلوڈ کریں",
-       "reuploaddesc": "زبراثÙ\82اÙ\84 Ù\88رÙ\82Û\81 (Ù\81ارÙ\85) Ú©Û\8cجاÙ\86ب Ù\88اپسÛ\94",
+       "reuploaddesc": "اپÙ\84Ù\88Ú\88 Ù\85Ù\86سÙ\88Ø® Ú©Ø±Ú©Û\92 Ø§Ù¾Ù\84Ù\88Ú\88 Ù\81ارÙ\85 Ú©Û\8c Ø¬Ø§Ù\86ب Ù\88اپس Ø¬Ø§Ø¦Û\8cÚº",
        "upload-tryagain": "فائل کی تبدیل شدہ وضاحت روانہ کریں",
        "uploadnologin": "آپ داخل شدہ حالت میں نہیں",
        "uploadnologintext": "فائلیں اپلوڈ کرنے کے لیے براہ کرم $1 ہوں",
        "upload-permitted": "اجازت یافتہ فائلوں کی {{PLURAL:$2|قسم|قسمیں}}: $1",
        "upload-preferred": "ترجیحی فائلوں کی {{PLURAL:$2|قسم|قسمیں}}: $1",
        "upload-prohibited": "ممنوع فائلوں کی {{PLURAL:$2|قسم|قسمیں}}: $1",
-       "uploadlogpage": "Ù\86Ù\88شتÛ\82 Ø²Ø¨Ø±Ø§Ø«Ù\82اÙ\84 (اپ Ù\84Ù\88Ú\88 Ù\84اگ)",
-       "uploadlogpagetext": "درج Ø°Û\8cÙ\84 Ù\85Û\8cÚº Ø­Ø§Ù\84Û\8cÛ\81 Ø²Ø¨Ø±Ø§Ø«Ù\82اÙ\84 (اپ Ù\84Ù\88Ú\88) Ú©Û\8c Ú¯Ø¦Û\8c Ø§Ù\85Ù\84اÙ\81 (Ù\81ائÙ\84Ù\88Úº) Ú©Û\8c Ù\81Û\81رست Ø¯Û\8c Ú¯Ø¦Û\8c Û\81Û\92۔",
+       "uploadlogpage": "Ù\86Ù\88شتÛ\81 Ø§Ù¾Ù\84Ù\88Ú\88",
+       "uploadlogpagetext": "Ø°Û\8cÙ\84 Ù\85Û\8cÚº Ø­Ø§Ù\84Û\8cÛ\81 Ø§Ù¾Ù\84Ù\88Ú\88 Ú©Ø±Ø¯Û\81 Ù\81ائÙ\84Ù\88Úº Ú©Û\8c Ù\81Û\81رست Ù\85Ù\88جÙ\88د Û\81Û\92Û\94\nÙ\85زÛ\8cد Ø¨ØµØ±Û\8c Ø¬Ø§Ø¦Ø²Û\92 Ú©Û\92 Ù\84Û\8cÛ\92 [[Special:NewFiles|Ù\86ئÛ\8c Ù\81ائÙ\84Ù\88Úº Ú©Ø§ Ù\86گارخاÙ\86Û\81]] Ù\85Ù\84احظÛ\81 Ù\81رÙ\85ائÛ\8cÚº۔",
        "filename": "فائل کا نام",
        "filedesc": "خلاصہ",
        "fileuploadsummary": "خلاصہ :",
        "filereuploadsummary": "فائل کی تبدیلیاں:",
        "filestatus": "کاپی رائٹ کی صورت حال:",
        "filesource": "ذرائع",
-       "ignorewarning": "انتباہ نظرانداز کرتے ہوۓ بہرصورت ملف (فائل) کو محفوظ کرلیا جاۓ۔",
+       "ignorewarning": "انتباہ نظر انداز کرتے ہوئے فائل کو بہرصورت محفوظ کر لیا جائے",
        "ignorewarnings": "ہر انتباہ نظرانداز کردیا جاۓ۔",
        "minlength1": "فائل کے ناموں میں کم از کم ایک حرف ہونا ضروری ہے۔",
        "illegalfilename": "اس فائل کے نام \"$1\" میں ایسے حروف موجود ہیں جو صفحہ کے عنوانات میں ممنوع ہیں۔\nبراہ کرم فائل کا نام تبدیل کرکے دوبارہ اپلوڈ کرنے کی کوشش کریں۔",
        "filename-toolong": "فائل کے نام 240 بائٹ سے زیادہ طویل نہ ہوں۔",
-       "badfilename": "Ù\85Ù\84Ù\81 (Ù\81ائÙ\84) Ú©Ø§ Ù\86اÙ\85 \"$1\" Ø\8c ØªØ¨Ø¯Û\8cÙ\84 Ú©Ø±Ø¯Û\8cا Ú¯Û\8cا۔",
+       "badfilename": "Ù\81ائÙ\84 Ú©Ø§ Ù\86اÙ\85 Â«$1» Ú©Ø± Ø¯Û\8cا Ú¯Û\8cا Û\81Û\92۔",
        "filetype-mime-mismatch": "فائل کی توسیع «$1.‎» فائل کی MIME قسم ($2) کے مطابق نہیں۔",
        "filetype-badmime": "MIME قسم \"$1\" کی فائلوں کو اپلوڈ کرنے کی اجازت نہیں ہے۔",
        "filetype-bad-ie-mime": "اس فائل کو اپلوڈ نہیں کیا جا سکتا کیونکہ انٹرنیٹ ایکسپلورر اسے «$1» سمجھے گا جس کی اجازت نہیں اور اس نوع کی فائل کے خطرناک ہونے کا احتمال ہے۔",
        "file-exists-duplicate": "پیش نظر فائل درج ذیل {{PLURAL:$1|فائل|فائلوں}} کی نقل ہے:",
        "file-deleted-duplicate": "اس فائل ([[:$1]]) سے ملتی جلتی دوسری فائل کو پہلے حذف کیا جا چکا ہے۔\nچنانچہ اسے دوبارہ اپلوڈ کرنے سے قبل اُس پرانی فائل کے حذف کا تاریخچہ جانچ لیں۔",
        "file-deleted-duplicate-notitle": "اس فائل سے ملتی جلتی دوسری فائل کو پہلے حذف کیا اور اس عنوان کو ممنوع قرار دیا جا چکا ہے۔\nاسے دوبارہ اپلوڈ کرنے سے قبل کسی ایسے شخص سے اس صورت حال کا جائزہ لینے کی درخواست کریں جسے ممنوع فائلوں کی معلومات تک رسائی حاصل ہو۔",
-       "uploadwarning": "اÙ\86تباÛ\81 Ø¨Û\81 Ø³Ù\84سÙ\84Û\82 Ø²Ø¨Ø±Ø§Ø«Ù\82اÙ\84",
+       "uploadwarning": "اپÙ\84Ù\88Ú\88 Ø§Ù\86تباÛ\81",
        "uploadwarning-text": "ذیل میں موجود فائل کی وضاحت میں تبدیلی کریں اور دوبارہ کوشش کریں۔",
        "savefile": "فائل محفوظ کریں",
        "uploaddisabled": "اپلوڈ غیر فعال ہے۔",
        "uploadinvalidxml": "اپلوڈ کردہ فائل میں موجود ایکس ایم ایل کا تجزیہ نہیں کیا جا سکا۔",
        "uploadvirus": "اس فائل میں وائرس موجود ہے!\nتفصیلات: $1",
        "upload-source": "اصل فائل",
-       "sourcefilename": "اسÙ\85 Ù\85Ù\84Ù\81 (Ù\81ائÙ\84) Ú©Ø§ Ù\85Ù\86بع:",
+       "sourcefilename": "اصÙ\84 Ù\81ائÙ\84 Ú©Ø§ Ù\86اÙ\85:",
        "sourceurl": "اصل یوآرایل",
-       "destfilename": "تعین شدہ اسم ملف:",
+       "destfilename": "ہدف فائل کا نام:",
        "upload-maxfilesize": "فائل کا زیادہ سے زیادہ حجم: $1",
        "upload-description": "فائل کی وضاحت",
        "upload-options": "اپلوڈ کے اختیارات",
        "upload-form-label-infoform-name": "نام",
        "upload-form-label-infoform-description": "تفصیل",
        "upload-form-label-usage-title": "استعمال",
-       "upload-form-label-usage-filename": "Ù\85Ù\84Ù\81 نام",
+       "upload-form-label-usage-filename": "Ù\81ائÙ\84 Ú©Ø§ نام",
        "upload-form-label-own-work": "یہ میرا ذاتی کام ہے",
        "upload-form-label-infoform-categories": "زمرہ جات",
        "upload-form-label-infoform-date": "تاریخ",
        "lockmanager-fail-svr-release": "$1 سرور کے قفل ہٹائے نہیں جا سکے۔",
        "zip-file-open-error": "مواد کی جانچ کے لیے زپ فائل کھولنے کے دوران میں کوئی نقص واقع ہوا۔",
        "zip-wrong-format": "یہ زپ فائل نہیں تھی۔",
+       "uploadstash": "پوشیدہ اپلوڈ کریں",
+       "uploadstash-summary": "اس صفحہ کے ذریعہ ان فائلوں تک رسائی حاصل ہوگی جو اپلوڈ ہو چکی ہیں یا ہو رہی ہیں لیکن اب تک ویکی پر شائع نہیں ہوئیں۔ یہ فائلیں محض اپلوڈ کنندہ صارفین ہی کو نظر آتی ہیں۔",
+       "uploadstash-clear": "پوشیدہ فائلوں کو صاف کریں",
+       "uploadstash-nofiles": "آپ کے پاس پوشیدہ فائلیں نہیں ہیں۔",
+       "uploadstash-badtoken": "اس کارروائی کی انجام دہی ناکام رہی، شاید آپ کے ترمیمی وثیقوں کی مدت ختم ہو چکی ہے۔ براہ کرم دوبارہ کوشش کریں۔",
        "uploadstash-errclear": "فائل کی صفائی ناکام۔",
        "uploadstash-refresh": "فائلوں کی فہرست کو تازہ کریں",
        "uploadstash-thumbnail": "تھمب نیل دیکھیں",
+       "uploadstash-exception": "اپلوڈ کردہ کو نہاں خانہ ($1) میں رکھا نہ جا سکا: ''$2''۔",
        "invalid-chunk-offset": "آفسیٹ کا قطعہ نادرست ہے",
        "img-auth-accessdenied": "رسائی معطل",
+       "img-auth-nopathinfo": "PATH_INFO مفقود ہے۔\nآپ کے سرور کو اس معلومات کی ترسیل کے لیے مرتب نہیں کیا گیا ہے۔\nممکن ہے یہ سی جی آئی پر مبنی ہو اور img_auth کو قبول نہ کرتا ہو۔\nبراہ کرم https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization کو ملاحظہ کریں۔",
+       "img-auth-notindir": "اپلوڈ کے لیے ترتیب شدہ ڈائرکٹری میں درخواست کردہ راستہ موجود نہیں ہے۔",
+       "img-auth-badtitle": "«$1» سے کسی درست عنوان کی تشکیل نہیں کی جا سکتی۔",
+       "img-auth-nologinnWL": "آپ داخل نہیں ہیں اور فہرست سفید میں «$1» نہیں ہے۔",
        "img-auth-nofile": "فائل «$1» موجود نہیں ہے۔",
+       "img-auth-isdir": "آپ «$1» ڈائرکٹری کو کھولنے کی کوشش کر رہے ہیں۔\nمحض فائل تک رسائی کی اجازت ہے۔",
+       "img-auth-streaming": "«$1» کی نمائش جاری ہے۔",
+       "img-auth-public": "img_auth.php کا فنکشن کسی نجی ویکی سے فائلیں اخذ کرنے کا کام کرتا ہے۔\nلیکن اس ویکی کو عوامی ویکی کے طور پر ترتیب دیا گیا ہے۔\nلہذا اضافی تحفظ کی خاطر img_auth.php کو غیر فعال رکھا گیا ہے۔",
+       "img-auth-noread": "صارف کو «$1» کے پڑھنے کی اجازت نہیں۔",
        "http-invalid-url": "نادرست یوآرایل: $1",
+       "http-invalid-scheme": "«$1» سے شروع ہونے والے یوآرایل معاونت یافتہ نہیں ہیں۔",
        "http-request-error": "ایچ ٹی ٹی پی کی درخواست کسی نامعلوم نقص کی بنا پر ناکام ہوگئی۔",
        "http-read-error": "HTTP خواندگی میں نقص۔",
        "http-timed-out": "HTTP درخواست کی مہلت ختم ہو گئی۔",
        "listfiles-summary": "اس خصوصی صفحہ میں تمام اپلوڈ کردہ فائلیں نظر آئیں گی۔",
        "listfiles_search_for": "میڈیا کے نام کو تلاش کریں:",
        "listfiles-userdoesnotexist": "«$1» کے نام سے کھاتہ موجود نہیں۔",
-       "imgfile": "Ù\85Ù\84Ù\81",
-       "listfiles": "فہرست فائل",
+       "imgfile": "Ù\81ائÙ\84",
+       "listfiles": "فائلوں کی فہرست",
        "listfiles_thumb": "تھمب نیل",
        "listfiles_date": "تاریخ",
        "listfiles_name": "نام",
        "listfiles_user": "صارف",
        "listfiles_size": "حجم",
-       "listfiles_description": "تفصیل",
-       "listfiles_count": "Ù\88رÚ\98Ù\86",
+       "listfiles_description": "وضاحت",
+       "listfiles_count": "Ù\86سخÛ\92",
        "listfiles-show-all": "تصویروں کے پرانے نسخے شامل کریں",
        "listfiles-latestversion": "موجودہ ورژن",
        "listfiles-latestversion-yes": "ہاں",
        "listfiles-latestversion-no": "نہیں",
-       "file-anchor-link": "Ù\85Ù\84Ù\81",
-       "filehist": "Ù\85Ù\84Ù\81 Ú©Û\8c ØªØ§Ø±Û\8cØ®",
-       "filehist-help": "یہ دیکھنے کیلئے کہ کسی خاص وقت پر ملف کس طرح ظاہر ہوتا تھا اُس تاریخ یا وقت پر طق کیجئے۔",
+       "file-anchor-link": "Ù\81ائÙ\84",
+       "filehist": "Ù\81ائÙ\84 Ú©Ø§ ØªØ§Ø±Û\8cØ®Ú\86Û\81",
+       "filehist-help": "کسی خاص وقت یا تاریخ میں یہ فائل کیسی نظر آتی تھی، اسے دیکھنے کے لیے اس وقت/تاریخ پر کلک کریں۔",
        "filehist-deleteall": "سب حذف",
        "filehist-deleteone": "حذف",
        "filehist-revert": "رجوع",
        "filehist-current": "حالیہ",
        "filehist-datetime": "تاریخ/وقت",
-       "filehist-thumb": "اظÙ\81Ù\88رÛ\81",
-       "filehist-thumbtext": "$1 کا تھمب نیل (thumbnail) ورژن",
+       "filehist-thumb": "تھÙ\85ب Ù\86Û\8cÙ\84",
+       "filehist-thumbtext": "مورخہ $1 کا تھمب نیل",
        "filehist-nothumb": "تھمب نیل نہیں ہے",
        "filehist-user": "صارف",
        "filehist-dimensions": "ابعاد",
        "filehist-filesize": "تصویر کا حجم",
        "filehist-comment": "تبصرہ",
-       "imagelinks": "ملف کا استعمال",
-       "linkstoimage": "اِس ملف کے ساتھ درج ذیل {{PLURAL:$1|صفحہ مربوط ہے|$1 صفحات مربوط ہیں}}",
-       "nolinkstoimage": "ایسے کوئی صفحات نہیں جو اس ملف (فائل) سے رابطہ رکھتے ہوں۔",
+       "imagelinks": "فائل کا استعمال",
+       "linkstoimage": "اِس فائل سے درج ذیل {{PLURAL:$1|صفحہ مربوط ہے|$1 صفحات مربوط ہیں}}:",
+       "linkstoimage-more": "اس فائل سے  $1 سے زیادہ {{PLURAL:$1|صفحے|صفحات}} مربوط ہیں۔\nذیل میں محض اس فائل سے {{PLURAL:$1|اولین مربوط صفحہ|اولین $1 مربوط صفحات}} کی فہرست درج ہے۔\nتاہم [[Special:WhatLinksHere/$2|مکمل فہرست]] بھی دیکھی جا سکتی ہے۔",
+       "nolinkstoimage": "اس فائل سے مربوط کوئی صفحہ موجود نہیں ہے۔",
        "morelinkstoimage": "اس فائل کے [[Special:WhatLinksHere/$1|مزید روابط]] ملاحظہ فرمائیں۔",
        "linkstoimage-redirect": "$1 (فائل رجوع مکرر) $2",
        "duplicatesoffile": "ذیل میں موجود {{PLURAL:$1|فائل|فائلیں}} اس فائل کی نقل {{PLURAL:$1|ہے|ہیں}}\n([[Special:FileDuplicateSearch/$2|مزید تفصیلات]]):",
        "sharedupload": "یہ فائل $1 میں موجود ہے، نیز ممکن ہے دیگر منصوبوں میں بھی مستعمل ہو۔",
        "sharedupload-desc-there": "یہ فائل $1 میں موجود ہے، نیز ممکن ہے دیگر منصوبوں میں بھی مستعمل ہو۔\nمزید معلومات کے لیے براہ کرم [$2 فائل کا صفحۂ وضاحت] ملاحظہ فرمائیں۔",
-       "sharedupload-desc-here": "Û\8cÛ\81 Ù\85Ù\84Ù\81 $1 Ø³Û\92 Û\81Û\92 Ø§Ù\88ر Ø¯Ù\88سرÛ\92 Ù\85Ù\86صÙ\88بÙ\88Úº Ù\85Û\8cÚº Ø§Ø³ØªØ¹Ù\85اÙ\84 Û\81Ù\88سکتا Û\81Û\92Û\94\nاÙ\90س Ú©Û\92 [$2 Ù\85Ù\84Ù\81اتÛ\8c ØµÙ\81Ø­Û\82 Ù\88ضاحت] Ø³Û\92 ØªÙ\81صÛ\8cÙ\84 Ø¯Ø±Ø¬ Ø°Û\8cÙ\84 ہے۔",
+       "sharedupload-desc-here": "Û\8cÛ\81 Ù\81ائÙ\84 $1 Ú©Û\8c Û\81Û\92 Ù\86Û\8cز Ù\85Ù\85Ú©Ù\86 Û\81Û\92 Ø¯Ù\88سرÛ\92 Ù\85Ù\86صÙ\88بÙ\88Úº Ù\85Û\8cÚº Ø¨Ú¾Û\8c Ø²Û\8cر Ø§Ø³ØªØ¹Ù\85اÙ\84 Û\81Ù\88Û\94\nاÙ\90س Ú©Û\92 [$2 ØµÙ\81Ø­Û\82 Ù\88ضاحت] Ù\85Û\8cÚº Ø¯Ø±Ø¬ Ù\88ضاحت Ø°Û\8cÙ\84 Ù\85Û\8cÚº Ù\85Ù\88جÙ\88د ہے۔",
        "sharedupload-desc-edit": "یہ فائل $1 میں موجود ہے، نیز ممکن ہے دیگر منصوبوں میں بھی مستعمل ہو۔\nاگر آپ [$2 فائل کے صفحۂ وضاحت] میں موجود معلومات میں ترمیم کرنا چاہیں تو وہاں کر سکتے ہیں۔",
        "sharedupload-desc-create": "یہ فائل $1 میں موجود ہے، نیز ممکن ہے دیگر منصوبوں میں بھی مستعمل ہو۔\nاگر آپ [$2 فائل کے صفحۂ وضاحت] میں موجود معلومات میں ترمیم کرنا چاہیں تو وہاں کر سکتے ہیں۔",
        "filepage-nofile": "اس نام سے کوئی فائل موجود نہیں ہے۔",
        "uploadnewversion-linktext": "اس فائل کا نیا نسخہ اپلوڈ کریں",
        "shared-repo-from": "از $1",
        "shared-repo": "مشترکہ ذخیرہ",
-       "upload-disallowed-here": "آپ اوپر چھڑا کر اس ملف کو نہیں لکھ سکتے۔",
+       "upload-disallowed-here": "آپ اس فائل کو برتحریر نہیں کر سکتے۔",
        "filerevert": "$1 کا استرجع کریں",
        "filerevert-legend": "فائل کا استرجع کریں",
+       "filerevert-intro": "آپ <strong>[[Media:$1|$1]]</strong> فائل کو [$4 مورخہ $2 بوقت $3 بجے کے نسخے] کی جانب واپس پھیر رہے ہیں۔",
        "filerevert-comment": "وجہ:",
        "filerevert-defaultcomment": "مورخہ $1 $2 بجے ($3) کے نسخے کی جانب واپس پھیر دیا گیا",
        "filerevert-submit": "استرجع کریں",
+       "filerevert-success": "<strong>[[Media:$1|$1]]</strong> فائل کو [$4 مورخہ $2 بوقت $3 بجے کے نسخے] کی جانب واپس پھیر دیا گیا۔",
+       "filerevert-badversion": "فراہم کردہ وقت کے مطابق اس فائل کا قدیم اور مقامی نسخہ موجود نہیں ہے۔",
+       "filerevert-identical": "اس فائل کا موجودہ نسخہ منتخب نسخے کی نقل ہے۔",
        "filedelete": "$1 کو حذف کریں",
        "filedelete-legend": "فائل حذف کریں",
+       "filedelete-intro": "آپ <strong>[[Media:$1|$1]]</strong> فائل کو اس کے مکمل تاریخچہ سمیت حذف کرنے جا رہے ہیں۔",
+       "filedelete-intro-old": "آپ <strong>[[Media:$1|$1]]</strong> فائل کے [$4 مورخہ $2 بوقت $3 بجے کے نسخے] کو حذف کر رہے ہیں۔",
        "filedelete-comment": "وجہ:",
        "filedelete-submit": "حذف کریں",
        "filedelete-success": " (\"اقدام مکمل ہوا\")۔",
        "filedelete-success-old": " (\"اقدام مکمل ہوا\")",
        "filedelete-nofile": "<strong>$1</strong> موجود نہیں ہے۔",
+       "filedelete-nofile-old": "درج کردہ خصوصیات کا حامل <strong>$1</strong> کا وثق شدہ نسخہ موجود نہیں ہے۔",
        "filedelete-otherreason": "دوسری/اضافی وجہ:",
        "filedelete-reason-otherlist": "دوسری وجہ",
        "filedelete-reason-dropdown": "* عمومی وجوہات حذف\n** کاپی رائٹ کی خلاف ورزی\n** دوہری فائل",
        "filedelete-edit-reasonlist": "حذف کی وجوہات میں ترمیم کریں",
+       "filedelete-maintenance": "نگہداشت کے دوران میں فائلوں کی حذف شدگی اور بحالی کو عارضی طور پر غیر فعال کیا گیا ہے۔",
        "filedelete-maintenance-title": "فائل حذف نہیں کی جا سکتی",
        "mimesearch": "MIME تلاش",
+       "mimesearch-summary": "اس صفحہ کے ذریعہ MIME طرز کے مطابق فائلوں کی تلاش کی جا سکتی ہے۔\nاندراج: contenttype/subtype یا contenttype/* کی شکل میں درج کریں، مثلاً <code>image/jpeg</code>",
        "mimetype": "MIME قسم:",
        "download": "زیراثقال (ڈاؤن لوڈ)",
        "unwatchedpages": "نادیدہ صفحات",
        "listredirects": "فہرست متبادل ربط",
        "listduplicatedfiles": "مکررات کے ساتھ فائلوں کی فہرست",
+       "listduplicatedfiles-summary": "اس صفحہ میں ان فائلوں کی فہرست موجود ہے جن کا حالیہ نسخہ کسی دوسری فائل کے تازہ ترین نسخہ کی نقل ہے۔ اس فہرست میں محض مقامی فائلیں درج کی گئی ہیں۔",
+       "listduplicatedfiles-entry": "[[:File:$1|$1]] [[$3|{{PLURAL:$2|کی ایک نقل ہے|$2 نقلیں ہیں}}]]۔",
        "unusedtemplates": "غیر استعمال شدہ سانچے",
+       "unusedtemplatestext": "اس صفحہ میں {{ns:template}} نام فضا کے ان تمام صفحات کی فہرست درج ہے جو کسی دوسرے صفحہ میں شامل نہیں ہیں۔\nانہیں حذف کرنے سے قبل سانچوں سے مربوط دیگر صفحات کو جانچنا نہ بھولیں۔",
        "unusedtemplateswlh": "دیگر روابط",
-       "randompage": "جستہ جستہ",
+       "randompage": "جستہ جستہ مطالعہ",
        "randompage-nopages": "{{PLURAL:$2|اس نام فضا|ان نام فضا}} میں کوئی صفحہ موجود نہیں ہے: $1",
        "randomincategory": "زمرہ میں بے ترتیب صفحہ",
        "randomincategory-invalidcategory": "عنوان «$1» زمرے کا درست نام نہیں ہے۔",
        "statistics-edits-average": "فی صفحہ اوسط ترامیم",
        "statistics-users": "مندرج [[خاص:فہرست صارفین، صارف فہرست|صارفین]]",
        "statistics-users-active": "متحرک صارفین",
+       "statistics-users-active-desc": "گزشتہ {{PLURAL:$1|day|$1 دنوں}} میں متحرک رہنے والے صارفین",
        "pageswithprop": "صفحات مع خاصیت صفحہ",
        "pageswithprop-legend": "صفحات مع خاصیت صفحہ",
        "pageswithprop-text": "اس صفحہ میں ان تمام صفحات کی فہرست موجود ہے جس کسی مخصوص خاصیت صفحہ کو استعمال کر رہے ہیں۔",
        "pageswithprop-prop": "نام خاصیت:",
        "pageswithprop-submit": "ٹھیک",
+       "pageswithprop-prophidden-long": "طویل متن کی خاصیت کی پوشیدہ قدر ($1)",
+       "pageswithprop-prophidden-binary": "ثنائی خاصیت کی پوشیدہ قدر ($1)",
        "doubleredirects": "دوہرے متبادل ربط",
+       "doubleredirectstext": "اس صفحہ میں رجوع مکررات ہی کی جانب رجوع مکرر صفحات کی فہرست درج ہے۔\nہر قطار میں پہلے اور دوسرے رجوع مکرر کے روابط، نیز دوسرے رجوع مکرر کا ہدف صفحہ بھی درج ہے، جو عموماً \"حقیقی\" ہدف صفحہ ہوتا ہے اور پہلا رجوع مکرر درحقیقت اسی کی جانب اشارہ کرتا ہے۔\n<del>کشیدہ</del> اندراج درست کر دیے گئے ہیں۔",
        "double-redirect-fixed-move": "[[$1]] کو منتقل کر دیا گیا۔\nیہ از خود تازہ ہو گیا اور اب [[$2]] سے رجوع مکرر ہے۔",
+       "double-redirect-fixed-maintenance": "نگہداشت کے دوران میں [[$1]] سے [[$2]] کی جانب دوہرے رجوع مکرر کی خودکار درستی۔",
+       "double-redirect-fixer": "مصلح رجوع مکرر",
        "brokenredirects": "نامکمل متبادل ربط",
        "brokenredirectstext": "ذیل کے رجوع مکررات غیر موجود صفحات سے مربوط ہیں:",
        "brokenredirects-edit": "ترمیم کریں",
        "withoutinterwiki-legend": "سابقہ",
        "withoutinterwiki-submit": "دکھائیں",
        "fewestrevisions": "کم نظرِ ثانی شدہ مضامین",
-       "nbytes": "$1 {{PLURAL:$1|لکمہ|لکمہ جات}}",
+       "nbytes": "$1 {{PLURAL:$1|بائٹ}}",
        "ncategories": "{{PLURAL:$1|زمرہ|زمرہ جات}} $1",
        "ninterwikis": "$1 {{PLURAL:$1|بین الویکی ربط|بین الویکی روابط}}",
        "nlinks": "$1 {{PLURAL:$1|ربط|روابط}}",
        "allpagesto": "اس حرف پر ختم ہونے والے صفحات دکھائیں:",
        "allarticles": "تمام مقالات",
        "allinnamespace": "تمام صفحات ($1 نام فضا)",
-       "allpagessubmit": "چلو",
+       "allpagessubmit": "چلیں",
        "allpagesprefix": "مطلوبہ سابقہ سے شروع ہونے والے صفحات کی نمائش:",
        "allpages-bad-ns": "{{SITENAME}} میں «$1» نام فضا موجود نہیں۔",
        "allpages-hide-redirects": "رجوع مکررات چھپائیں",
        "usermessage-summary": "نظامی پیغام کی ترسیل۔",
        "usermessage-editor": "نظامی پیغام رساں",
        "watchlist": "میری زیرنظرفہرست",
-       "mywatchlist": "زیرنظرفہرست",
+       "mywatchlist": "زیرنظر فہرست",
        "watchlistfor2": "براۓ $1 ($2)",
        "nowatchlist": "آپ کی زیرنظر فہرست میں کوئی مواد موجود نہیں ہے۔",
        "watchlistanontext": "اپنی زیرنظر فہرست میں موجود مواد کو دیکھنے اور ان میں ترمیم کرنے کے لیے براہ کرم لاگ ان کریں۔",
        "confirmdeletetext": "آپ اس صفحے کو اس سے ملحقہ تاریخچہ سمیت حذف کر رہے ہیں۔ براہ مہربانی اس بات کی تصدیق کر لیں کہ آپ اس عمل کے نتائج سے بخوبی آگاہ ہیں، اور یہ بھی جانچ لیں کہ آیا آپ کا یہ اقدام [[{{MediaWiki:Policy-url}}|حکمت عملی]] کے دائرے میں ہے یا نہیں۔",
        "actioncomplete": "اقدام تکمیل کو پہنچا",
        "actionfailed": "عمل ناکام",
-       "deletedtext": "\"$1\" کو حذف کر دیا گیا ہے ۔\nحالیہ حذف شدگی کے تاریخ نامہ کیلیۓ  $2  دیکھیۓ",
+       "deletedtext": "\"$1\" کو حذف کر دیا گیا ہے۔\nحالیہ حذف شدگیوں کی فہرست دیکھنے کے لیے $2 ملاحظہ فرمائیں۔",
        "dellogpage": "نوشتۂ حذف شدگی",
        "dellogpagetext": "حالیہ حذف شدگی کی فہرست درج ذیل ہے۔",
        "deletionlog": "نوشتۂ حذف شدگی",
        "changecontentmodel-emptymodels-text": "[[:$1]] میں موجود مواد کی نوعیت کو تبدیل نہیں کیا جا سکتا۔",
        "log-name-contentmodel": "نوشتہ تبدیلی نمونہ مواد",
        "log-description-contentmodel": "صفحہ کے مواد کے ماڈل سے متعلق واقعات",
+       "logentry-contentmodel-new": "$1 نے مواد کے غیر ڈیفالٹ ماڈل «$5» کے ذریعہ  صفحہ $3 کو {{GENDER:$2|تخلیق کیا}}",
        "logentry-contentmodel-change": "$1 نے صفحہ $3 کے مواد کی ساخت کو \"$4\" سے \"$5\" میں {{GENDER:$2|تبدیل کیا}}",
        "logentry-contentmodel-change-revertlink": "استرجع",
        "logentry-contentmodel-change-revert": "استرجع",
        "prot_1movedto2": "[[$1]] بجانب [[$2]] منتقل",
        "protect-badnamespace-title": "ناقابل حفاظت نام فضا",
        "protect-badnamespace-text": "اس نام فضا میں موجود صفحات کو محفوظ نہیں کیا جا سکتا۔",
+       "protect-norestrictiontypes-text": "اس صفحہ کو محفوظ نہیں کیا جا سکتا کیونکہ حفاظت کے مطلوبہ پیرامیٹر دستیاب نہیں ہیں۔",
        "protect-norestrictiontypes-title": "ناقابل حفاظت صفحہ",
        "protect-legend": "تحفظ کی تصدیق کریں",
        "protectcomment": "وجہ:",
        "protect-level-autoconfirmed": "محض خود توثیق شدہ صارفین کو اجازت ہے",
        "protect-level-sysop": "صرف منتظمین کو اجازت ہے",
        "protect-summary-cascade": "آبشاری",
-       "protect-expiring": "Ù\85دت Ø®Ø§ØªÙ\85Û\81  $1 (یو ٹی سی)",
-       "protect-expiring-local": "Ù\85دت Ø®Ø§ØªÙ\85Û\81  $1",
+       "protect-expiring": "Ù\88Ù\82ت Ø§Ø®ØªØªØ§Ù\85  $1 (یو ٹی سی)",
+       "protect-expiring-local": "Ù\88Ù\82ت Ø§Ø®ØªØªØ§Ù\85 $1",
        "protect-expiry-indefinite": "لا محدود",
        "protect-cascade": "اس صفحہ میں شامل صفحات کو محفوظ کریں (آبشاری حفاظت)",
        "protect-cantedit": "آپ اس صفحہ کے درجہ حفاظت میں تبدیلی نہیں کر سکتے کیونکہ آپ کو اس میں ترمیم کرنے کی اجازت نہیں ہے۔",
        "restriction-level-sysop": "مکمل محفوظ",
        "restriction-level-autoconfirmed": "نیم محفوظ",
        "restriction-level-all": "کوئی بھی سطح",
-       "undelete": "ضائع Ú©Ø±دہ صفحات دیکھیں",
+       "undelete": "حذÙ\81 Ø´دہ صفحات دیکھیں",
        "undeletepage": "معائنہ خذف شدہ صفحات",
        "undeletepagetitle": "'''ذیل میں [[:$1|$1]] کے حذف شدہ ترامیم درج ہیں۔'''",
        "viewdeletedpage": "حذف شدہ صفحات دیکھیے",
+       "undeletepagetext": "درج ذیل {{PLURAL:$1|صفحہ حذف کیا جا چکا ہے|$1 صفحات حذف کیے جا چکے ہیں}} لیکن وثق میں موجود {{PLURAL:$1|ہے|ہیں}} اور {{PLURAL:$1|اسے|انہیں}} بحال کیا جا سکتا ہے۔\nممکن ہے اس وثق کو وقتاً فوقتاً صاف کیا جاتا ہو۔",
        "undelete-fieldset-title": "ترامیم بحال کریں",
+       "undeleteextrahelp": "اس صفحہ کے مکمل تاریخچے کو بحال کرنے کے لیے تمام خانوں کو غیر منتخب چھوڑ دیں اور <strong><em>{{int:undeletebtn}}</em></strong> پر کلک کریں۔\nمنتخب نسخوں کی بحالی کے لیے مطلوبہ نسخوں کے بغل میں موجود خانوں کو نشان زد کریں اور <strong><em>{{int:undeletebtn}}</em></strong> پر کلک کریں۔",
        "undeleterevisions": "$1 {{PLURAL:$1|نسخہ حذف کیا گیا|نسخے حذف کیے گئے}}",
        "undeletehistory": "اگر آپ اس صفحہ کو بحال کرتے ہیں، تو اس صفحہ کے تاریخچہ میں موجود تمام ترامیم بھی بحال ہو جائیں گی۔\nاگر حذف شدگی کے بعد کوئی نیا صفحہ اسی نام سے بنایا گیا ہو، تو تمام بحال شدہ ترامیم گذشتہ تاریخچہ میں ظاہر ہوگی۔",
        "undeleterevdel": "اگر صفحہ یا فائل کے آخری نسخے کو جزوی طور پر حذف کیا جا رہا ہو تو بحالیٔ صفحہ کا اقدام مکمل نہیں ہوگا۔\nایسی صورت میں لازمی طور آپ حالیہ حذف شدہ نسخے کو بھی بحال کریں۔",
        "undeletehistorynoadmin": "اس صفحہ کو حذف کر دیا گیا ہے۔\nذیل میں صفحہ حذف کرنے کی وجہ درج ہے، اور ساتھ ہی ان صارفین کی تفصیلات بھی موجود ہیں جنہوں نے صفحہ حذف ہونے سے قبل اس میں ترمیم کی تھی۔\nحذف شدہ نسخوں کا اصل متن محض منتظمین کے لیے دستیاب ہے۔",
        "undelete-revision": "$3 کی جانب سے (مورخہ $4 بوقت $5 بجے) تحریر کردہ $1 کا حذف شدہ نسخہ:",
+       "undeleterevision-missing": "نسخہ نادرست یا غیر موجود ہے۔\nشاید آپ غلط ربط کو استعمال کر رہے ہیں یا متعلقہ نسخہ پہلے ہی بحال یا وثق سے حذف کر دیا گیا ہے۔",
+       "undeleterevision-duplicate-revid": "{{PLURAL:$1|ایک نسخہ بحال نہیں کیا جا سکا|$1 نسخے بحال نہیں کیے جا سکے}}، کیونکہ {{PLURAL:$1|اس کا|ان کے}} <code>rev_id</code> زیر استعمال ہے۔",
        "undelete-nodiff": "کوئی پرانا نسخہ نہیں ملا۔",
        "undeletebtn": "بحال",
        "undeletelink": "دیکھو/بحال کرو",
        "undeleteinvert": "انتخاب بالعکس",
        "undeletecomment": "وجہ:",
        "undeletedrevisions": "{{PLURAL:$1|1 نظر ثانی|$1 نظر ثانیاں}} بحال",
-       "undeletedrevisions-files": "{{PLURAL:$1|1 Ù\86ظر Ø«Ø§Ù\86Û\8c|$1 Ù\86ظر Ø«Ø§Ù\86Û\8cاں}} Ø§Ù\88ر {{PLURAL:$2|1 Ù\85Ù\84Ù\81|$2 Ø§Ù\85Ù\84اÙ\81}} بحال",
-       "undeletedfiles": "{{PLURAL:$1|1 Ù\85Ù\84Ù\81|$1 Ø§Ù\85Ù\84اÙ\81}} Ø¨Ø­Ø§Ù\84",
+       "undeletedrevisions-files": "{{PLURAL:$1|1 Ù\86سخÛ\81|$1 Ù\86سخÛ\92}} Ø§Ù\88ر {{PLURAL:$2|1 Ù\81ائÙ\84|$2 Ù\81ائÙ\84Û\8cÚº}} بحال",
+       "undeletedfiles": "{{PLURAL:$1|1 Ù\81ائÙ\84|$1 Ù\81ائÙ\84}} Ø¨Ø­Ø§Ù\84 Ú©Û\8c {{PLURAL:$1|گئÛ\8c|گئÛ\8cÚº}}",
        "cannotundelete": "کلی یا جزوی طور پر بحالی کا اقدام ناکام رہا:\n$1",
        "undeletedpage": "<strong>$1 کو بحال کر دیا گیا</strong>\n\nحالیہ حذف شدگیوں اور بحالیوں کا نوشتہ دیکھنے کے لیے [[Special:Log/delete|نوشتہ حذف شدگی]] ملاحظہ فرمائیں۔",
        "undelete-header": "حالیہ حذف شدہ صفحات کے لیے [[Special:Log/delete|نوشتۂ حذف شدگی]] دیکھیں۔",
        "undelete-search-prefix": "اظہار صفحات بآغاز از:",
        "undelete-search-submit": "تلاش",
        "undelete-no-results": "حذف شدہ صفحات میں ایسا کوئی صفحہ نہیں ملا",
+       "undelete-filename-mismatch": " $1 کے نسخے کو بحال نہیں کیا جا سکتا: فائل کا نام مطابقت نہیں رکھتا۔",
+       "undelete-bad-store-key": " $1 کے نسخے کو بحال نہیں کیا جا سکتا: حذف شدگی سے قبل فائل موجود نہیں تھی۔",
        "undelete-cleanup-error": "غیر مستعمل تاریخچہ «$1» کو حذف کرنے کے دوران میں نقص۔",
+       "undelete-missing-filearchive": "$1 شناختی نمبر کی فائل بحال نہیں کی جا سکی کیونکہ وہ ڈیٹابیس میں موجود نہیں ہے۔\nشاید اسے پہلے ہی بحال کر دیا گیا ہو۔",
        "undelete-error": "صفحہ کی بحالی کے دوران میں نقص",
        "undelete-error-short": "فائل کی بحالی کے دوران میں نقص: $1",
        "undelete-error-long": "فائل کی بحالی کے دوران میں نقص واقع ہوا:\n\n$1",
        "tooltip-whatlinkshere-invert": "منتخب نام فضا میں موجود صفحات کے روابط چھپانے کے لیے اس خانہ کو نشان زد کریں۔",
        "namespace_association": "ملحقہ نام فضا",
        "tooltip-namespace_association": "منتخب نام فضا سے منسلک تبادلۂ خیال یا ذیلی نام فضا کو شامل کرنے کے لیے اس خانہ کو نشان زد کریں",
-       "blanknamespace": "(مرکز)",
+       "blanknamespace": "(مرکزی)",
        "contributions": "{{GENDER:$1|صارف}} کی شراکتیں",
        "contributions-title": "صارف $1 کی شراکتیں",
-       "mycontris": "شراکت",
+       "mycontris": "شراکتیں",
        "anoncontribs": "شراکتیں",
        "contribsub2": "برائے {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "«$1» کے نام سے صارف کھاتہ مندرج نہیں ہے۔",
        "sp-contributions-newonly": "محض نئے صفحات دکھائیں",
        "sp-contributions-hideminor": "معمولی ترامیم چھپائیں",
        "sp-contributions-submit": "تلاش",
-       "whatlinkshere": "ادھر کونسا ربط ہے",
+       "whatlinkshere": "مربوط صفحات",
        "whatlinkshere-title": "\"$1\" سے مربوط صفحات",
        "whatlinkshere-page": "صفحہ:",
        "linkshere": "<strong>[[:$1]]</strong> سے درج ذیل صفحات مربوط ہیں:",
-       "nolinkshere": "'''[[:$1]]''' سے کوئی روابط نہیں۔",
+       "nolinkshere": "<strong>[[:$1]]</strong> سے کوئی صفحہ مربوط نہیں ہے۔",
+       "nolinkshere-ns": "منتخب نام فضا میں <strong>[[:$1]]</strong> سے مربوط کوئی صفحہ نہیں ہے۔",
        "isredirect": "رجوع مکرر صفحہ",
        "istemplate": "شامل شدہ",
        "isimage": "فائل کا ربط",
        "whatlinkshere-prev": "{{PLURAL:$1|پچھلا|پچھلے $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|اگلا|اگلے $1}}",
-       "whatlinkshere-links": "روابط ←",
+       "whatlinkshere-links": "→ روابط",
        "whatlinkshere-hideredirs": "رجوع مکررات $1",
        "whatlinkshere-hidetrans": "$1 استعمالات",
        "whatlinkshere-hidelinks": "روابط $1",
        "whatlinkshere-hideimages": "تصویر کے روابط $1",
-       "whatlinkshere-filters": "Ù\81Ù\84ٹرذ",
+       "whatlinkshere-filters": "Ù\85Ù\82طارات",
        "whatlinkshere-submit": "ٹھیک",
        "autoblockid": "خودکار پابندی #$1",
        "block": "صارف مسدود کریں",
        "blocklist-timestamp": "وقت کی مہر",
        "blocklist-target": "ہدف",
        "blocklist-expiry": "وقت اختتام",
+       "blocklist-by": "منتظم",
        "blocklist-params": "پابندی کے پیرامیٹر",
        "blocklist-reason": "وجہ",
        "ipblocklist-submit": "تلاش",
        "change-blocklink": "پابندی میں تبدیلی",
        "contribslink": "شراکتیں",
        "emaillink": "ای میل بھیجیں",
+       "autoblocker": "خودکار طور پر پابندی لگائی گئی ہے کیونکہ حال ہی میں آپ کا آئی پی پتا «[[User:$1|$1]]» نے استعمال کیا ہے۔\n$1 پر پابندی عائد کرنے کی وجہ یہ بتائی گئی: \"$2\"",
        "blocklogpage": "نوشتۂ پابندی",
+       "blocklog-showlog": "اس صارف پر پہلے پابندی عائد کی گئی تھی۔\nحوالہ کے لیے ذیل میں نوشتہ پابندی موجود ہے:",
+       "blocklog-showsuppresslog": "اس صارف پر پہلے پابندی عائد یا اسے پوشیدہ کیا گیا تھا۔\nحوالہ کے لیے ذیل میں نوشتہ ُپوشیدگی درج ہے:",
+       "blocklogentry": "«[[$1]]» پر $2 کے لیے پابندی عائد کی گئی ہے $3",
+       "reblock-logentry": "[[$1]] کی ترتیبات پابندی کو تبدیل کیا، اب میعاد $2 $3 پر ختم ہوگی",
+       "blocklogtext": "ذیل میں صارف پر پابندی عائد کرنے اور ہٹانے کا نوشتہ ہے۔\nخودکار طور پر ممنوع آئی پی پتے یہاں درج نہیں ہیں۔\nموجودہ جاری پابندیوں اور معطلیوں کی فہرست دیکھنے کے لیے [[Special:BlockList|فہرست پابندی]] ملاحظہ فرمائیں۔",
        "unblocklogentry": "$1 سے پابندی ہٹائی گئی",
        "block-log-flags-anononly": "محض نامعلوم صارفین",
        "block-log-flags-nocreate": "کھاتے کی تخلیق غیرفعال",
        "ipb-needreblock": "«$1» پر پہلے ہی پابندی لگا دی گئی ہے۔ کیا آپ ان ترتیبات کو تبدیل کرنا چاہتے ہیں؟",
        "ipb-otherblocks-header": "دیگر {{PLURAL:$1|پابندی|پابندیاں}}",
        "unblock-hideuser": "چونکہ اس صارف کا نام پوشیدہ ہے لہذا آپ اس صارف سے پابندی نہیں ہٹا سکتے۔",
+       "ipb_cant_unblock": "نقص: پابندی کا شناختی نمبر $1 نہیں ملا۔ شاید پہلے ہی یہ پابندی ہٹا دی گئی ہو۔",
+       "ipb_blocked_as_range": "نقص: آئی پتا $1 پر براہ راست پابندی نہیں ہے اور اس کی پابندی ختم نہیں کی جا سکتی۔\nتاہم اس آئی پی پر $2 رینج کے ایک حصے کے طور پر پابندی لگائی گئی ہے، چنانچہ اس رینج کی پابندی ختم کی جا سکتی ہے۔",
        "ip_range_invalid": "آئی پی پتے کی رینج نادرست ہے۔",
        "ip_range_toolarge": "/$1 سے زیادہ بڑی رینج پابندیوں کی اجازت نہیں ہے۔",
        "proxyblocker": "پراکسی مسدود کنندہ",
        "movepage-moved-redirect": "رجوع مکرر تخلیق کر دیا گیا۔",
        "movepage-moved-noredirect": "رجوع مکرر کو بننے سے روک دیا گیا ہے۔",
        "articleexists": "اس عنوان سے کوئی صفحہ پہلے ہی موجود ہے، یا آپکا منتخب کردہ نام مستعمل نہیں۔ براۓ مہربانی دوسرا نام منتخب کیجیۓ۔",
+       "cantmove-titleprotected": "آپ اس جگہ کسی صفحہ کو منتقل نہیں کر سکتے کیونکہ اس نئے عنوان کی تخلیق کو محفوظ کر دیا گیا ہے۔",
        "movetalk": "ملحقہ تبادلۂ خیال صفحہ بھی منتقل کریں",
        "move-subpages": "ذیلی صفحات منتقل کریں ($1 سے زیادہ)",
        "move-talk-subpages": "تبادلۂ خیال صفحہ کے ذیلی صفحات منتقل کریں ($1 سے زیادہ)",
        "immobile-target-namespace-iw": "صفحہ منتقل کرنے کے لیے بین الویکی ربط درست ہدف نہیں ہے۔",
        "immobile-source-page": "اس صفحہ کو منتقل نہیں کیا جا سکتا۔",
        "immobile-target-page": "اس ہدف عنوان کی جانب منتقل نہیں کیا جا سکتا۔",
+       "bad-target-model": "مطلوبہ منزل میں مواد کا مختلف ماڈل زیر استعمال ہے۔ لہذا $1 سے $2 میں تبدیلی نہیں ہو سکی۔",
        "imagenocrossnamespace": "فائل کو غیر فائل نام فضا میں منتقل نہیں کیا جا سکتا۔",
        "nonfile-cannot-move-to-file": "غیر فائل کو فائل نام فضا میں منتقل نہیں کیا جا سکتا۔",
        "imagetypemismatch": "نئی فائل کی توسیع اس کی نوعیت کے مطابق نہیں ہے۔",
        "move-leave-redirect": "پیچھے رجوع مکرر بنائیں",
        "protectedpagemovewarning": "<strong>انتباہ:</strong> اس صفحہ کو محفوظ کر دیا گیا ہے اور اب محض منتظمین ہی اسے منتقل کر سکتے ہیں۔\nحوالہ کے لیے نوشتہ کا جدید اندراج ذیل میں درج ہے:",
        "semiprotectedpagemovewarning": "<strong>اطلاع:</strong> یہ صفحہ نیم محفوظ ہے اور اسے محض مندرج صارفین ہی منتقل کر سکتے ہیں۔\nذیل میں حوالہ کے لیے نوشتہ کا تازہ ترین اندراج موجود ہے:",
+       "move-over-sharedrepo": "[[:$1]] ایک مشترکہ ذخیرے میں موجود ہے۔ چنانچہ اس عنوان کی جانب کسی فائل کو منتقل کرنے پر مشترکہ فائل منسوخ ہو جائے گی۔",
+       "file-exists-sharedrepo": "فائل کا منتخب کردہ نام ایک مشترکہ ذخیرے میں پہلے ہی سے زیر استعمال ہے۔\nبراہ کرم کوئی دوسرا نام درج کریں۔",
        "export": "برآمد صفحات",
        "exportall": "تمام صفحات برآمد کریں",
        "exportcuronly": "مکمل تاریخچہ کی بجائے محض موجودہ نسخہ کو شامل کریں",
        "export-addns": "شامل کریں",
        "export-download": "فائل کے طور پر محفوظ کریں",
        "export-templates": "سانچے شامل کریں",
+       "export-pagelinks": "مربوط صفحات کو اس گہرائی تک شامل کریں:",
        "export-manual": "صفحات کو دستی طور پر شامل کریں:",
        "allmessages": "نظامی پیغامات",
        "allmessagesname": "نام",
        "thumbnail-temp-create": "تھمب نیل کی عارضی فائل نہیں بنائی جا سکتی",
        "thumbnail-dest-create": "تھمب نیل کو ہدف جگہ پر محفوظ نہیں کیا جا سکتا۔",
        "thumbnail_invalid_params": "تھمب نیل کے پیرامیٹر نادرست ہیں",
+       "thumbnail_toobigimagearea": "فائل کے ابعاد $1 سے زیادہ ہیں۔",
+       "thumbnail_dest_directory": "مقصود ڈائرکٹری کو بنایا نہیں جا سکا",
        "thumbnail_image-type": "تصویر کی نوعیت معاونت یافتہ نہیں ہے",
        "thumbnail_image-missing": "معلوم ہوتا ہے کہ یہ فائل موجود نہیں: $1",
+       "thumbnail_image-failure-limit": "حال میں اس تھمب نیل کو بنانے کی ($1 یا زائد) متعدد ناکام کوششیں کی گئی ہیں۔ براہ کرم کچھ دیر بعد دوبارہ کوشش کریں۔",
        "import": "درآمد صفحات",
        "importinterwiki": "دوسرے ویکی سے درآمد کریں",
        "import-interwiki-text": "درآمد کرنے کے لیے ویکی اور صفحہ کا عنوان منتخب کریں۔\nنسخوں کی تاریخ اور نسخہ نویسوں کے نام محفوظ رکھے جائیں گے۔\nدوسری ویکیوں سے درآمد کردہ ہر چیز کو [[Special:Log/import|نوشتہ درآمد]] میں درج کیا جاتا ہے۔",
        "import-mapping-subpage": "درج ذیل صفحہ کے ذیلی صفحات کے طور پر درآمد کریں:",
        "import-upload-filename": "فائل کا نام:",
        "import-comment": "تبصرہ:",
+       "importtext": "براہ کرم [[Special:Export|برآمد کی سہولت]] کے ذریعہ اصل ویکی سے فائل برآمد کریں۔\nاور اسے اپنے کمپیوٹر میں محفوظ کرکے یہاں اپلوڈ کریں۔",
        "importstart": "صفحات درآمد کیے جا رہے ہیں۔۔۔",
        "import-revision-count": "$1 {{PLURAL:$1|نسخہ|نسخے}}",
        "importnopages": "درآمد کرنے کے لیے کوئی صفحہ نہیں ہے۔",
        "imported-log-entries": "درآمد کردہ $1 {{PLURAL:$1|اندراج نوشتہ|اندراجات نوشتہ}}۔",
        "importfailed": "درآمد ناکام: <nowiki>$1</nowiki>",
+       "importunknownsource": "درآمد کے ماخذ کی نوعیت نامعلوم ہے",
        "importcantopen": "درآمد فائل کھل نہیں سکی",
        "importbadinterwiki": "غلط بین الویکی ربط",
        "importsuccess": "درآمد مکمل!",
+       "importnosources": "کسی ایسی ویکی کا اندراج نہیں کیا گیا جہاں سے درآمد کرنا ہے اور تاریخچے کے براہ راست اپلوڈ غیر فعال ہیں۔",
        "importnofile": "کسی درآمد فائل کو اپلوڈ نہیں کیا گیا۔",
+       "importuploaderrorsize": "درآمد فائل کی اپلوڈ ناکام ہوئی۔\nفائل اپلوڈ کے اجازت یافتہ حجم سے بڑی ہے۔",
+       "importuploaderrorpartial": "درآمد فائل کی اپلوڈ ناکام ہوئی۔\nاس فائل کا محض ایک حصہ اپلوڈ ہوا۔",
+       "importuploaderrortemp": "درآمد فائل کی اپلوڈ ناکام ہوئی۔\nعارضی فولڈر موجود نہیں۔",
+       "import-parse-failure": "درآمد شدہ ایکس ایم ایل کا تجزیہ ناکام",
        "import-noarticle": "درآمد کرنے کے لیے کوئی صفحہ موجود نہیں!",
+       "import-nonewrevisions": "کسی نسخے کو درآمد نہیں کیا گیا (شاید وہ سب پہلے سے موجود ہیں یا کسی نقص کی بنا پر چھوڑ دیے گئے ہیں)۔",
+       "xml-error-string": "سطر نمبر $2، ستون نمبر $3 میں $1 ($4 بائٹ): $5",
+       "import-upload": "ایکس ایم ایل ڈیٹا اپلوڈ کریں",
+       "import-token-mismatch": "معذرت! نشست کے مواد میں خامی کی وجہ سے آپ کی  ترمیم مکمل نہیں ہو سکی۔\n\nشاید آپ اپنے کھاتے سے خارج ہو گئے ہیں۔ <strong>براہ کرم اس بات کی تصدیق کر لیں کہ آپ داخل ہیں اور دوبارہ کوشش کریں۔</strong> اگر آپ کو پھر بھی مشکل پیش آرہی ہو تو ایک بار [[Special:UserLogout|خارج ہو کر]] واپس داخل ہو جائیں اور اپنے براؤزر کو جانچ لیں کہ آیا وہ اس سائٹ کی کوکیز اخذ کر رہا ہے یا نہیں۔",
        "import-invalid-interwiki": "اس ویکی سے درآمد نہیں کیا جا سکتا۔",
        "import-error-edit": "صفحہ «$1» درآمد نہیں کیا جا سکا کیونکہ آپ کو اس میں ترمیم کرنے کی اجازت نہیں ہے۔",
        "import-error-create": "صفحہ «$1» درآمد نہیں کیا جا سکا کیونکہ آپ کو اسے تخلیق کرنے کی اجازت نہیں ہے۔",
        "import-error-interwiki": "صفحہ «$1» درآمد نہیں کیا جا سکا کیونکہ اس کا نام بیرونی ربط (بین الویکی) کے لیے محفوظ ہے۔",
        "import-error-special": "صفحہ «$1» درآمد نہیں کیا جا سکا کیونکہ یہ اس خصوصی نام فضا سے متعلق ہے جس میں صفحات بنانے کی اجازت نہیں۔",
        "import-error-invalid": "صفحہ «$1» درآمد نہیں کیا جا سکا کیونکہ درآمد کے بعد اس صفحہ کا جو نام ہوگا وہ اس ویکی پر نادرست ہے۔",
+       "import-error-unserialize": "صفحہ «$1» کے نسخہ $2 کے تسلسل کو ختم نہیں کیا جا سکا۔ اس نسخے کے متعلق اطلاع دی گئی ہے کہ اس میں مواد کے ماڈل $3 کو $4 کے تسلسل کے طور پر استعمال کیا گیا تھا۔",
+       "import-error-bad-location": "نسخہ $2 کو جس میں مواد کا ماڈل $3 زیر استعمال ہے اس ویکی کے \"$1\" میں نہیں رکھا جا سکا، کیونکہ اس صفحہ کا ماڈل اس ماڈل سے مختلف ہے۔",
        "import-options-wrong": "غلط {{PLURAL:$2|اختیار|اختیارات}}: <nowiki>$1</nowiki>",
        "import-rootpage-invalid": "درج کردہ ماخذی صفحہ کا عنوان نادرست ہے۔",
+       "import-rootpage-nosubpage": "اصل صفحہ کی نام فضا \"$1\" میں ذیلی صفحات کی اجازت نہیں۔",
        "importlogpage": "نوشتہ درآمد",
+       "importlogpagetext": "دوسری ویکیوں سے تاریخچہ سمیت صفحوں کی انتظامی درآمد کے اقدامات۔",
        "import-logentry-upload-detail": "$1 {{PLURAL:$1|نسخہ|نسخے}} درآمد {{PLURAL:$1|کیا گیا|کیے گئے}}",
        "import-logentry-interwiki-detail": "$2 سے $1 {{PLURAL:$1|نسخہ|نسخے}} درآمد {{PLURAL:$1|کیا گیا|کیے گئے}}",
        "javascripttest": "جاوا اسکرپٹ کی آزمائش",
        "javascripttest-pagetext-unknownaction": "نامعلوم اقدام \"$1\"",
+       "javascripttest-qunit-intro": "mediawiki.org پر [$1 آزمائشی دستاویز] ملاحظہ فرمائیں",
        "tooltip-pt-userpage": "آپ کا صارف صفحہ",
        "tooltip-pt-anonuserpage": "آپ جس آئی پی سے ترمیم کاری کر رہے ہیں اس کا صارف صفحہ",
        "tooltip-pt-mytalk": "آپ کا تبادلہ خیال صفحہ",
        "tooltip-pt-watchlist": "اُن صفحات کی فہرست جن کی تبدیلیاں آپ کی زیرِنظر ہیں",
        "tooltip-pt-mycontris": "آپ کی شراکتوں کی فہرست",
        "tooltip-pt-anoncontribs": "اس آئی پی پتے سے انجام دی جانے والی تمام ترامیم کی فہرست",
-       "tooltip-pt-login": "آپ کیلئے داخلِ نوشتہ ہونا اچھا ہے؛ تاہم، یہ ضروری نہیں",
-       "tooltip-pt-logout": "خارجِ نوشتہ ہوجائیں",
-       "tooltip-pt-createaccount": "آپ کو مدعو کیا جاتا ہے کہ کھاتہ بنائیں۔تاہم کھاتہ بنانا لازم نہیں۔",
-       "tooltip-ca-talk": "مضمون بارے تبادلۂ خیال",
-       "tooltip-ca-edit": "اس ØµÙ\81Ø­Û\92 Ù¾Ø± ترمیم کریں",
-       "tooltip-ca-addsection": "نیا قطعہ شروع کیجئے",
+       "tooltip-pt-login": "کھاتے میں داخل ہونے کی سفارش کی جاتی ہے؛ تاہم یہ ضروری نہیں",
+       "tooltip-pt-logout": "خارج ہوجائیں",
+       "tooltip-pt-createaccount": "کھاتہ بنانے یا اس میں داخل ہونے کی سفارش کی جاتی ہے؛ تاہم یہ ضروری نہیں",
+       "tooltip-ca-talk": "مضمون کے متعلق گفتگو کریں",
+       "tooltip-ca-edit": "اس ØµÙ\81Ø­Û\81 Ù\85Û\8cÚº ترمیم کریں",
+       "tooltip-ca-addsection": "نیا قطعہ شروع کریں",
        "tooltip-ca-viewsource": "یہ ایک محفوظ شدہ صفحہ ہے.\nآپ اِس کا مآخذ دیکھ سکتے ہیں",
-       "tooltip-ca-history": "صÙ\81Ø­Û\82 Û\81ٰذا Ú©Û\8c Ø³Ø§Ø¨Ù\82Û\81 Ù\86ظرثاÙ\86Û\8c",
+       "tooltip-ca-history": "اس ØµÙ\81Ø­Û\81 Ú©Û\92 Ø³Ø§Ø¨Ù\82Û\81 Ù\86سخÛ\92",
        "tooltip-ca-protect": "یہ صفحہ محفوظ کیجئے",
        "tooltip-ca-unprotect": "اس صفحہ کی حفاظت میں تبدیلی کریں",
        "tooltip-ca-delete": "یہ صفحہ حذف کریں",
-       "tooltip-ca-move": "یہ صفحہ منتقل کریں",
+       "tooltip-ca-undelete": "حذف شدہ صفحہ کے نسخے بحال کریں",
+       "tooltip-ca-move": "اس صفحہ کو منتقل کریں",
        "tooltip-ca-watch": "اِس صفحہ کو اپنی زیرِنظرفہرست میں شامل کریں",
        "tooltip-ca-unwatch": "اِس صفحہ کو اپنی زیرِنظرفہرست سے ہٹائیں",
-       "tooltip-search": "تلاش {{SITENAME}}",
-       "tooltip-search-go": "اگر Ø¨Ø§Ù\84Ú©Ù\84 Ø§Ù\90سÛ\8c Ù\86اÙ\85 Ú©Ø§ ØµÙ\81Ø­Û\81 Ù\85Ù\88جÙ\88د Û\81Ù\88 ØªÙ\88 Ø§Ù\8fس ØµÙ\81Ø­Û\81 Ù¾Ø± Ø¬Ø§Ø¤",
-       "tooltip-search-fulltext": "اس متن کیلئے صفحات تلاش کریں",
-       "tooltip-p-logo": "سرÙ\88رÙ\82 Ù¾Ø± Ø¬Ø§Ø¦Û\8cÛ\92",
-       "tooltip-n-mainpage": "اصÙ\84 ØµÙ\81Ø­Û\81 Ù¾Ø± Ø¬Ø§Ø¦Û\8cÛ\92",
-       "tooltip-n-mainpage-description": "صفحہ اول پر جائیے",
-       "tooltip-n-portal": "منصوبہ کے متعلق، آپ کیا کرسکتے ہیں، چیزیں کہاں ڈھونڈنی ہیں",
-       "tooltip-n-currentevents": "حالیہ واقعات پر پس منظری معلومات دیکھیئے",
+       "tooltip-search": "{{SITENAME}} میں تلاش کریں",
+       "tooltip-search-go": "اگر Ø§Ø³Û\8c Ø¹Ù\86Ù\88اÙ\86 Ú©Ø§ ØµÙ\81Ø­Û\81 Ù\85Ù\88جÙ\88د Û\81Û\92 ØªÙ\88 Ø§Ø³ Ù\85Û\8cÚº Ø¬Ø§Ø¦Û\8cÚº",
+       "tooltip-search-fulltext": "اس عبارت کو صفحات میں تلاش کریں",
+       "tooltip-p-logo": "صÙ\81Ø­Û\82 Ø§Ù\88Ù\84 Ù¾Ø± Ø¬Ø§Ø¦Û\8cÚº",
+       "tooltip-n-mainpage": "صÙ\81Ø­Û\82 Ø§Ù\88Ù\84 Ù¾Ø± Ø¬Ø§Ø¦Û\8cÚº",
+       "tooltip-n-mainpage-description": "صفحہ اول پر جائیں",
+       "tooltip-n-portal": "اس منصوبہ کے متعلق، آپ کیا کرسکتے ہیں، چیزیں کہاں ڈھونڈی جائیں",
+       "tooltip-n-currentevents": "حالیہ واقعات سے متعلق پس منظری معلومات ایک نظر میں",
        "tooltip-n-recentchanges": "ویکی میں حالیہ تبدیلیوں کی فہرست",
-       "tooltip-n-randompage": "صفحات کا جستہ جستہ مطالعہ کریں",
-       "tooltip-n-help": "ڈھونڈ نکالنے کی جگہ",
-       "tooltip-t-whatlinkshere": "اÙ\8fÙ\86 ØªÙ\85اÙ\85 Ù\88Û\8cÚ©Û\8c ØµÙ\81حات Ú©Û\8c Ù\81Û\81رست Ø¬Ù\86 Ú©Ø§ Û\8cÛ\81اں Ø±Ø¨Ø· Û\81Û\92",
+       "tooltip-n-randompage": "مضامین کا جستہ جستہ مطالعہ کریں",
+       "tooltip-n-help": "مقام معاونت",
+       "tooltip-t-whatlinkshere": "اÙ\8fÙ\86 ØªÙ\85اÙ\85 Ù\88Û\8cÚ©Û\8c ØµÙ\81حات Ú©Û\8c Ù\81Û\81رست Ø¬Ù\88 Ø§Ø³ ØµÙ\81Ø­Û\81 Ø³Û\92 Ù\85ربÙ\88Ø· Û\81Û\8cÚº",
        "tooltip-t-recentchangeslinked": "اِس صفحہ سے مربوط صفحات میں حالیہ تبدیلیاں",
        "tooltip-feed-rss": "اِس صفحہ کیلئے اسس خورد",
-       "tooltip-feed-atom": "اِس صفحہ کیلئے اٹوم خورد",
+       "tooltip-feed-atom": "اِس صفحہ کا اٹوم فیڈ",
        "tooltip-t-contributions": "{{GENDER:$1|اس صارف}} کی شراکتوں کی فہرست",
        "tooltip-t-emailuser": "{{GENDER:$1|اس صارف}} کو برقی خط بھیجیں",
        "tooltip-t-info": "اس صفحہ کے بارے میں مزید معلومات",
-       "tooltip-t-upload": "زبراثقالِ ملفات",
-       "tooltip-t-specialpages": "تÙ\85اÙ\85 Ø®Ø§Øµ صفحات کی فہرست",
-       "tooltip-t-print": "اِس صفحہ کا قابلِ طبعہ نسخہ",
-       "tooltip-t-permalink": "صفحہ کے موجودہ نظرثانی کا مستقل ربط",
-       "tooltip-ca-nstab-main": "صفحۂ مضمون دیکھئے",
-       "tooltip-ca-nstab-user": "اÙ\90س ØµØ§Ø±Ù\81 Ú©Û\92 Ù\85ساÛ\81Ù\85ات Ú©Û\8c Ù\81Û\81رست Ø¯Û\8cکھئÛ\92",
+       "tooltip-t-upload": "فائلیں اپلوڈ کریں",
+       "tooltip-t-specialpages": "جÙ\85Ù\84Û\81 Ø®ØµÙ\88صÛ\8c صفحات کی فہرست",
+       "tooltip-t-print": "اِس صفحہ کا قابلِ طبع نسخہ",
+       "tooltip-t-permalink": "صفحہ کے اس نسخہ کا مستقل ربط",
+       "tooltip-ca-nstab-main": "مواد پر مشتمل صفحہ دیکھیں",
+       "tooltip-ca-nstab-user": "صارÙ\81 ØµÙ\81Ø­Û\81 Ø¯Û\8cÚ©Ú¾Û\8cÚº",
        "tooltip-ca-nstab-media": "میڈیا کا صفحہ دیکھیں",
-       "tooltip-ca-nstab-special": "Û\81Ù\85 Ù\85عذرت Ø®Ù\88اÛ\81 Û\81Û\8cÚº! Ø¢Ù¾ Ø§Ø³ [[Ù\88Û\8cÚ©Û\8cÙ¾Û\8cÚ\88Û\8cا:Ù\86اÙ\85 Ù\81ضا|Ù\86اÙ\85 Ù\81ضا]] Ù\85Û\8cÚº ØªØ±Ù\85Û\8cÙ\85 Ú©Ø§ Ø§Ø®ØªÛ\8cار Ù\86Û\81Û\8cÚº Ø±Ú©Ú¾ØªÛ\92Û\94",
+       "tooltip-ca-nstab-special": "Û\8cÛ\81 Ø§Û\8cÚ© Ø®ØµÙ\88صÛ\8c ØµÙ\81Ø­Û\81 Û\81Û\92Ø\8c Ø§Ø³ Ù\85Û\8cÚº ØªØ±Ù\85Û\8cÙ\85 Ù\86Û\81Û\8cÚº Ú©Û\8c Ø¬Ø§ Ø³Ú©ØªÛ\8c",
        "tooltip-ca-nstab-project": "صفحۂ صارف دیکھئے",
-       "tooltip-ca-nstab-image": "صفحۂ ملف دیکھئے",
+       "tooltip-ca-nstab-image": "فائل کا صفحہ دیکھیں",
        "tooltip-ca-nstab-mediawiki": "نظامی پیغام دیکھیں",
-       "tooltip-ca-nstab-template": "سانچہ دیکھئے",
+       "tooltip-ca-nstab-template": "سانچہ دیکھیں",
        "tooltip-ca-nstab-help": "صفحۂ معاونت دیکھیں",
-       "tooltip-ca-nstab-category": "زمرہ‌جاتی صفحہ دیکھئے",
+       "tooltip-ca-nstab-category": "زمرہ‌ دیکھیں",
        "tooltip-minoredit": "اِس تدوین کو بطورِ معمولی ترمیم نشانزد کیجئے",
-       "tooltip-save": "تبدیلیاں محفوظ کیجئے",
+       "tooltip-save": "تبدیلیاں محفوظ کریں",
        "tooltip-publish": "اپنی تبدیلیاں شائع کریں",
-       "tooltip-preview": "برائے مہربانی! محفوظ کرنے سے پہلے تبدیلیوں کا پیش منظر دیکھيے",
+       "tooltip-preview": "براہ مہربانی! محفوظ کرنے سے پہلے تبدیلیوں کی نمائش دیکھ لیں",
        "tooltip-diff": "دیکھئے کہ اپنے متن میں کیا تبدیلیاں کیں",
        "tooltip-compareselectedversions": "اِس صفحہ کی دو منتخب نظرثانیوں میں فرق دیکھئے",
        "tooltip-watch": "اِس صفحہ کو اپنی زیرِنظرفہرست میں شامل کریں",
        "tooltip-watchlistedit-normal-submit": "عناوین حذف کریں",
        "tooltip-watchlistedit-raw-submit": "زیرنظر فہرست کی تجدید کریں",
+       "tooltip-recreate": "حذف شدہ صفحہ ہونے کے باوجود اسے دوبارہ تخلیق کریں",
        "tooltip-upload": "اپلوڈ کریں",
        "tooltip-rollback": "پچھلے صارف کی کی گئی اِس صفحے پر استرجع شدہ ترامیم کو ایک کلِک میں واپس کریں",
        "tooltip-undo": "''استرجع'' اس ترمیم کو پچھلی ترمیم کے جانب واپس کردیگا اور نمائشی انداز میں خانہ ترمیم کھول دے گا۔ آپ مختصراً سبب بیان کرنے کے بھی مجاز ہونگے۔",
        "anonusers": "{{SITENAME}} {{PLURAL:$2|کا|کے}} گمنام {{PLURAL:$2|{{GENDER:$1|صارف}}|صارفین}} $1",
        "creditspage": "صفحہ کے انتسابات",
        "nocredits": "اس صفحہ کے انتسابات سے متعلق معلومات دستیاب نہیں ہیں۔",
+       "spamprotectiontitle": "مقطار فاضل کاری",
+       "spamprotectiontext": "آپ جس عبارت کو محفوظ کرنا چاہتے ہیں اسے مقطار فاضل کاری نے ممنوع کر رکھا ہے۔\nعین ممکن ہے یہ فہرست سیاہ میں درج کسی بیرونی سائٹ کے ربط کی وجہ سے ہو رہا ہو۔",
+       "spamprotectionmatch": "ذیل میں موجود متن کو مقطار فاضل کاری نے روک دیا ہے: $1",
+       "spambot_username": "میڈیاویکی محافظ فاضل کاری",
+       "spam_reverting": "اس آخری نسخہ کی جانب واپس پھیرا جا رہا ہے جس میں $1 کے روابط شامل نہیں",
+       "spam_blanking": "$1 کے روابط پر مشتمل تمام نسخے، صفائی جاری ہے",
+       "spam_deleting": "$1 کے روابط پر مشتمل تمام نسخے، حذف کیا جا رہا ہے",
+       "simpleantispam-label": "فاضل کاری مخالف پڑتال۔\nاسے <strong>نہ</strong> بھریں!",
        "pageinfo-title": "«$1» کی معلومات",
        "pageinfo-not-current": "معذرت، پرانی ترامیم کی ان معلومات کو فراہم کرنا ناممکن ہے۔",
        "pageinfo-header-basic": "بنیادی معلومات",
        "pageinfo-watchers": "تعداد ناظرین",
        "pageinfo-visiting-watchers": "حالیہ تبدیلیاں دیکھنے والے ناظرین کی تعداد",
        "pageinfo-few-watchers": "$1 سے کم {{PLURAL:$1|ناظر|ناظرین}}",
+       "pageinfo-few-visiting-watchers": "شاید کوئی صارف حالیہ ترامیم دیکھنے آیا ہو یا نہ آیا ہو",
        "pageinfo-redirects-name": "رجوع مکررات کی تعداد",
        "pageinfo-subpages-name": "اس صفحہ کے ذیلی صفحات کی تعداد",
        "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|رجوع مکرر|رجوع مکررات}}؛ $3 {{PLURAL:$3|غیر رجوع مکرر|غیر رجوع مکررات}})",
        "pageinfo-category-total": "اراکین کی مجموعی تعداد",
        "pageinfo-category-pages": "تعداد صفحات",
        "pageinfo-category-subcats": "تعداد ذیلی زمرہ جات",
-       "pageinfo-category-files": "تعداد املاف",
+       "pageinfo-category-files": "فائلوں کی تعداد",
        "markaspatrolleddiff": "بطور مراجعت شدہ نشان زد کریں",
        "markaspatrolledtext": "اس صفحہ کو بطور مراجعت شدہ نشان زد کریں",
        "markaspatrolledtext-file": "فائل کے اس نسخے کو مراجعت شدہ نشان زد کریں",
        "filedelete-missing": "فائل «$1» کو حذف نہیں کیا جا سکتا کیونکہ یہ موجود نہیں ہے۔",
        "filedelete-old-unregistered": "فائل «$1» کا منتخب نسخہ ڈیٹابیس میں موجود نہیں ہے۔",
        "filedelete-current-unregistered": "«$1» کے عنوان سے کوئی فائل ڈیٹابیس میں موجود نہیں ہے۔",
-       "previousdiff": "← پُرانی تدوین",
+       "filedelete-archive-read-only": "$1 کی وثق ڈائرکٹری میں ویب سرور نہیں لکھ پا رہا ہے۔",
+       "previousdiff": "→ پرانی ترمیم",
        "nextdiff": "صفحہ کا نام:",
+       "mediawarning": "<strong>انتباہ:</strong> شاید اس نوع کی فائل میں نقصان دہ کوڈ موجود ہے۔\nممکن ہے اسے چلانے پر آپ کا سسٹم مشکوک ہاتھوں میں چلا جائے۔",
        "imagemaxsize": "تصویر کی جسامت کی حد:<br /><em>(فائل کے توضیحی صفحات کے لیے)</em>",
        "thumbsize": "تھمب نیل کی جسامت:",
        "widthheightpage": "$1×$2، $3 {{PLURAL:$3|صفحہ|صفحات}}",
        "file-info": "فائل کا حجم: $1، MIME قسم: $2",
-       "file-info-size": "\n$1 × $2 عکصر (پکسلز)، حجم ملف: $3، MIME قسم: $4",
+       "file-info-size": "\n$1 × $2 پکسل، فائل کا حجم: $3، MIME قسم: $4",
        "file-info-size-pages": "$1 × $2 پکسل، فائل کا حجم: $3، MIME قسم: $4، $5 {{PLURAL:$5|صفحہ|صفحات}}",
-       "file-nohires": "اس Ø³Û\92 Ø¨Ú\91Û\8c ØªØµÙ\85Û\8cÙ\85 دستیاب نہیں۔",
+       "file-nohires": "اس Ø³Û\92 Ø²Û\8cادÛ\81 Ø±Û\8cزÙ\88Ù\84Û\8cÙ\88Ø´Ù\86 دستیاب نہیں۔",
        "svg-long-desc": "ایس وی جی فائل، ابعاد $1 × $2 پکسل، فائل کا حجم: $3",
        "svg-long-desc-animated": "متحرک ایس وی جی فائل، ابعاد $1 × $2 پکسل، فائل کا حجم: $3",
        "svg-long-error": "نادرست ایس وی جی فائل: $1",
-       "show-big-image": "اصÙ\84 Ù\85Ù\84Ù\81",
+       "show-big-image": "اصÙ\84 Ù\81ائÙ\84",
        "show-big-image-preview": "اس نمائش کا حجم:$1",
        "show-big-image-preview-differ": "اس $2 فائل کی $3 نمائش کا حجم: $1",
-       "show-big-image-other": "دیگر {{PLURAL:$2|تجویز|تجویزیں}}: $1۔",
-       "show-big-image-size": "$1 × $2 pixels",
+       "show-big-image-other": "دیگر {{PLURAL:$2|قرارداد|قراردادیں}}: $1۔",
+       "show-big-image-size": "$1 × $2 پکسل",
        "file-info-gif-looped": "چکردار",
        "file-info-gif-frames": "$1 {{PLURAL:$1|چوکھٹا|چوکھٹے}}",
        "file-info-png-looped": "چکردار",
        "saturday-at": "سنیچر بوقت $1",
        "sunday-at": "اتوار بوقت $1",
        "yesterday-at": "گزشتہ کل بوقت $1",
-       "bad_image_list": "شکلبند درج ذیل ہے:\n\nصرف فہرستی عناصر (* سے شروع ہونے والی لکیری) شامل کی جاتی ہیں۔\nکسی لکیر میں پہلا ربط کوئی خراب ملف کا ہونا چاہئے۔\nاُسی لکیر میں باقی آنے والے ربط کو مستثنیٰ قرار دیا جاتا ہے، مثلاً صفحات جہاں ملف لکیر کے وسط میں آسکتا ہے۔",
+       "bad_image_list": "فارمیٹ درج ذیل ہے:\n\nمحض فہرست میں موجود مندرجات (* سے شروع ہونے والی سطریں) شامل سمجھے جائیں گے۔\nسطر میں پہلا ربط کسی خراب فائل کا ہونا لازمی ہے۔\nاُسی سطر کے بقیہ روابط کو مستثنیٰ سمجھا جائے گا، مثلاً وہ صفحات جن میں فائل سطر میں موجود ہوں۔",
        "metadata": "میٹا ڈیٹا",
-       "metadata-help": "اÙ\90س Ù\85Ù\84Ù\81 Ù\85Û\8cÚº Ø§Ù\90ضاÙ\81Û\8c Ù\85عÙ\84Ù\88Ù\85ات Ø´Ø§Ù\85Ù\84 Û\81Û\8cÚºØ\8c Ø¬Ù\88 Ú©Û\81 Ø´Ø§Û\8cد Ø§Ù\8fس Ø±Ù\82Ù\85Û\8c Ú©Û\8cÙ\85رÛ\92 Û\8cا Ø³Ú©Û\8cÙ\86ر Ø³Û\92 Ø¢Ø¦Û\92 Û\81Û\8cÚº Ø¬Ø³ Ú©Û\92 Ø°Ø±Û\8cعÛ\92 Û\8cÛ\81 Ù\85Ù\84Ù\81 Ø¨Ù\86ائÛ\8c Ú¯Ø¦Û\8c ØªÚ¾Û\8cÛ\94\nاگر Ù\85Ù\84Ù\81 Ø§Ù¾Ù\86Û\8c Ø§ØµÙ\84 Ø­Ø§Ù\84ت Ù\85Û\8cÚº Ù\86Û\81Û\8cÚº Ø±Û\81Û\8c Û\81Û\92 ØªÙ\88 Ú©Ú\86Ú¾ ØªÙ\81اصÛ\8cÙ\84 ØªØ±Ù\85Û\8cÙ\85 Ø´Ø¯Û\81 Ù\85Ù\84Ù\81 Ú©Û\8c Ù\85Ú©Ù\85Ù\84 Ø·Ù\88ر Ù¾Ø± Ø¹Ú©Ø§Ø³Û\8c Ù\86Û\81Û\8cÚº Ú©Ø±Ù¾Ø§Ø¦Û\8cÚº Ú¯Û\92۔",
+       "metadata-help": "اÙ\90س Ù\81ائÙ\84 Ù\85Û\8cÚº Ø§Ù\90ضاÙ\81Û\8c Ù\85عÙ\84Ù\88Ù\85ات Ø´Ø§Ù\85Ù\84 Û\81Û\8cÚºØ\8c Ø¬Ù\88 Ø´Ø§Û\8cد Ø§Ù\8fس Ú\88Û\8cجÛ\8cÙ¹Ù\84 Ú©Û\8cÙ\85رÛ\92 Û\8cا Ø³Ú©Û\8cÙ\86ر Ø³Û\92 Ø¢Ø¦Û\8c Û\81Û\8cÚº Ø¬Ø³ Ú©Û\92 Ø°Ø±Û\8cعÛ\92 Û\8cÛ\81 Ù\81ائÙ\84 Ø¨Ù\86ائÛ\8c Ú¯Ø¦Û\8c ØªÚ¾Û\8cÛ\94\nاگر Ù\81ائÙ\84 Ø§Ù¾Ù\86Û\8c Ø§ØµÙ\84 Ø­Ø§Ù\84ت Ù\85Û\8cÚº Ù\86Û\81 Û\81Ù\88 ØªÙ\88 Ú©Ú\86Ú¾ Ù\85عÙ\84Ù\88Ù\85ات ØªØ±Ù\85Û\8cÙ\85 Ø´Ø¯Û\81 Ù\81ائÙ\84 Ú©Û\8c Ù\85Ú©Ù\85Ù\84 Ø·Ù\88ر Ù¾Ø± Ø¹Ú©Ø§Ø³Û\8c Ù\86Û\81Û\8cÚº Ú©Ø± Ù¾Ø§Ø¦Û\8cÚº Ú¯Û\8c۔",
        "metadata-expand": "تفصیلی معلومات دکھائیں",
        "metadata-collapse": "طویل تفاصیل چھپاؤ",
        "metadata-fields": "تصویر کے میٹاڈیٹا کے وہ خانے جو اس پیغام میں درج ہیں وہ تصویر کے صفحے پر شامل ہوتے ہیں نیز یہ اس وقت ظاہر ہوتے ہیں جب میٹاڈیٹا کو وسیع کیا جائے۔\nالبتہ دیگر خانے ابتدائی طور پر پوشیدہ ہوتے ہیں۔\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "exif-bitspersample": "بٹ فی جزو",
        "exif-compression": "نظام کمپریشن",
        "exif-photometricinterpretation": "پکسل کی ترکیب",
-       "exif-orientation": "پیشکش",
+       "exif-orientation": "جہت",
        "exif-samplesperpixel": "اجزا کی تعداد",
        "exif-planarconfiguration": "ڈیٹا کی ترتیب",
        "exif-ycbcrsubsampling": "Y کا C سے ذیلی نمونہ ساز تناسب",
        "exif-ycbcrpositioning": "Y اور C کی جگہ",
-       "exif-xresolution": "چھوڑاوی دکھاوت",
-       "exif-yresolution": "لمباوی دکھاوت",
+       "exif-xresolution": "افقی ریزولوشن",
+       "exif-yresolution": "عمودی ریزولیشن",
        "exif-stripoffsets": "تصویر کے دیٹا کا محل وقوع",
        "exif-rowsperstrip": "فی پٹی تعداد قطار",
        "exif-stripbytecounts": "بائٹ فی کمپریس شدہ پٹی",
        "exif-jpeginterchangeformatlength": "JPEG ڈیٹا کے بائٹ",
        "exif-whitepoint": "سفید نقطہ کے رنگ",
        "exif-primarychromaticities": "اساسیات کے رنگ",
-       "exif-datetime": "ملف کے تبدیلی کا تاریخ او وقت",
+       "exif-ycbcrcoefficients": "فضائے رنگ کی میٹرکس تبدیلی کی مقداریں",
+       "exif-referenceblackwhite": "سیاہ و سفید جوالے کی قدروں کی جوڑی",
+       "exif-datetime": "فائل کی تبدیلی کی تاریخ اور وقت",
        "exif-imagedescription": "تصویر کا عنوان",
-       "exif-make": "Ú©Û\8cÙ\85رÛ\92 Ú©Ø§ ØµØ§Ù\86ع",
+       "exif-make": "Ú©Û\8cÙ\85رÛ\81 Ø³Ø§Ø² Ú©Ù\85Ù¾Ù\86Û\8c",
        "exif-model": "کیمرے کا ماڈل",
-       "exif-software": "سافٹویئر استعمال",
+       "exif-software": "مستعمل سافٹ ویئر",
        "exif-artist": "مصنف",
        "exif-copyright": "کاپی رائٹ کا حامل",
        "exif-exifversion": "اکزیف ورژن",
+       "exif-flashpixversion": "Flashpix کا معاونت یافتہ نسخہ",
        "exif-colorspace": "رنگ فضا",
+       "exif-componentsconfiguration": "ہر عنصر کا مفہوم",
+       "exif-compressedbitsperpixel": "تصویر کے کمپریشن کی حالت",
        "exif-pixelxdimension": "تصویر کی چوڑائی",
        "exif-pixelydimension": "تصویر کی لمبائی",
        "exif-usercomment": "صارف کے تبصرے",
+       "exif-relatedsoundfile": "متعلقہ آڈیو فائل",
        "exif-datetimeoriginal": "ڈیٹا بنانے کا تاریخ اور وقت",
        "exif-datetimedigitized": "معددی کا تاریخ اور وقت",
+       "exif-subsectime": "تاریخ و وقت کے ذیلی سیکنڈ",
+       "exif-subsectimeoriginal": "اصل تاریخ و وقت کے ذیلی سیکنڈ",
+       "exif-subsectimedigitized": "ڈیجیٹل وقت و تاریخ کے ذیلی سیکنڈ",
        "exif-exposuretime": "نمائش کا وقت",
        "exif-exposuretime-format": "$1 سیکنڈ ($2)",
        "exif-fnumber": "ایف نمبر",
        "exif-exposurebiasvalue": "APEX نمائش کا نقص",
        "exif-maxaperturevalue": "زیادہ سے زیادہ لینڈ اپرچر",
        "exif-subjectdistance": "شئی کا فاصلہ",
+       "exif-meteringmode": "پیمائش کاری کی حالت",
        "exif-lightsource": "روشنی کا ماخذ",
        "exif-flash": "فلیش",
        "exif-focallength": "عدسہ کی ماسکی لمبائی",
        "exif-focalplaneresolutionunit": "درجہ ماسکہ کی تحلیل کی اکائی",
        "exif-subjectlocation": "شئی کا محل وقوع",
        "exif-exposureindex": "نمائش کا اشاریہ",
+       "exif-sensingmethod": "سینسنگ کا طریقہ",
        "exif-filesource": "فائل کا ماخذ",
        "exif-scenetype": "منظر کی نوعیت",
        "exif-customrendered": "تصویر کی شخصی پروسیسینگ",
        "exif-contrast": "امتیاز",
        "exif-saturation": "پُری",
        "exif-sharpness": "تیزی",
+       "exif-devicesettingdescription": "آلے کی ترتیبات کی وضاحت",
+       "exif-subjectdistancerange": "شئی کے فاصلے کی حد",
+       "exif-imageuniqueid": "تصویر کا منفرد شناختی نمبر",
+       "exif-gpsversionid": "جی پی ایس ٹیگ کا نسخہ",
        "exif-gpslatituderef": "شمالی یا جنوبی عرض البلد",
        "exif-gpslatitude": "عرض البلد",
        "exif-gpslongituderef": "مشرقی یا مغربی طول البلد",
        "exif-gpslongitude": "طول البلد",
-       "exif-gpsaltituderef": "اتفاع کا حوالہ",
+       "exif-gpsaltituderef": "ارتÙ\81اع Ú©Ø§ Ø­Ù\88اÙ\84Û\81",
        "exif-gpsaltitude": "ارتفاع",
        "exif-gpstimestamp": "جی پی ایس وقت (جوہری گھڑی)",
        "exif-gpssatellites": "پیمائش کے لیے مستعمل مصنوعی سیارے",
        "exif-source": "ماخذ",
        "exif-editstatus": "تصویر کی ادارتی کیفیت",
        "exif-urgency": "فوری طور پر",
+       "exif-fixtureidentifier": "مستقل شئی کا نام",
+       "exif-locationdest": "دکھایا گیا مقام",
+       "exif-locationdestcode": "دکھائے گئے مقام کا کوڈ",
+       "exif-objectcycle": "اس میڈیا کا مقصود دن کا وقت",
+       "exif-contact": "رابطہ کی معلومات",
        "exif-writer": "مصنف",
        "exif-languagecode": "زبان",
        "exif-iimversion": "IIM نسخہ",
        "exif-giffilecomment": "جی آئی ایف فائل کا تبصرہ",
        "exif-intellectualgenre": "شئی کی قسم",
        "exif-subjectnewscode": "موضوع کا کوڈ",
+       "exif-scenecode": "منظر کا IPTC کوڈ",
+       "exif-event": "دکھایا گیا واقعہ",
+       "exif-organisationinimage": "دکھائی گئی تنظیم",
+       "exif-personinimage": "دکھایا گیا شخص",
+       "exif-originalimageheight": "تراشنے سے قبل تصویر کی لمبائی",
+       "exif-originalimagewidth": "تراشنے سے قبل تصویر کی چوڑائی",
        "exif-compression-1": "غیر کمپریس شدہ",
+       "exif-compression-2": "CCITT گروپ 3 1 - ہف مین رن کی تبدیل شدہ لمبائی کی ابعادی اینکوڈنگ",
+       "exif-compression-3": "CCITT گروپ 3 کے فیکس کی اینکوڈنگ",
+       "exif-compression-4": "CCITT گروپ 4 کے فیکس کی اینکوڈنگ",
        "exif-copyrighted-true": "کاپی رائٹ شدہ",
        "exif-copyrighted-false": "کاپی رائٹ کی صورت حال متعین نہیں کی گئی",
        "exif-photometricinterpretation-1": "سیاہ اور سفید (سیاہ 0 ہے)",
        "exif-unknowndate": "نامعلوم تاریخ",
        "exif-orientation-1": "عام",
+       "exif-orientation-2": "افقی طور پر جھکایا ہوا",
+       "exif-orientation-3": "180° درجہ پر گھمایا ہوا",
+       "exif-orientation-4": "عمودی طور پر جھکایا ہوا",
+       "exif-orientation-5": "90° CCW گھمایا ہوا اور عمودی جھکایا ہوا",
+       "exif-orientation-6": "90° CCW گھمایا ہوا",
+       "exif-orientation-7": "90° CW گھمایا ہوا اور عمودی جھکایا ہوا",
+       "exif-orientation-8": "90° CW گھمایا ہوا",
+       "exif-planarconfiguration-1": "دبیز فامیٹ",
+       "exif-planarconfiguration-2": "مسطح فارمیٹ",
+       "exif-colorspace-65535": "نامعلوم قطر کا حامل",
        "exif-componentsconfiguration-0": "موجود نہیں",
        "exif-exposureprogram-0": "غیر متعین",
        "exif-exposureprogram-1": "دستی",
        "exif-exposureprogram-2": "عام پروگرام",
+       "exif-exposureprogram-3": "اپرچر کی ترجیح",
        "exif-exposureprogram-4": "شٹر کی ترجیح",
+       "exif-exposureprogram-5": "تخلیقی پروگرام (میدان کی گہرائی کی جانب جھکا ہوا)",
+       "exif-exposureprogram-6": "اقدامی پروگرام (شٹر کی تیز رفتار کی جانب جھکا ہوا)",
+       "exif-exposureprogram-7": "شبیہ کی حالت (نقطہ ارتکاز سے باہر کا پس منظر رکھنے والی قریبی تصویروں کے لیے)",
+       "exif-exposureprogram-8": "قدرتی منظر کی حالت (نقطہ ارتکاز میں موجود پس منظر رکھنے والی قدرتی مناظر کی تصویروں کے لیے)",
        "exif-subjectdistance-value": "$1 میٹر",
        "exif-meteringmode-0": "نامعلوم",
        "exif-meteringmode-1": "اوسط",
        "exif-lightsource-0": "نامعلوم",
        "exif-lightsource-1": "روشنی روز",
        "exif-lightsource-2": "فلورسینٹ",
+       "exif-lightsource-3": "ٹنگسٹن (گرم تاباں روشنی)",
        "exif-lightsource-4": "فلیش",
        "exif-lightsource-9": "اچھا موسم",
        "exif-lightsource-10": "ابرآلود موسم",
        "exif-lightsource-11": "سایہ",
+       "exif-lightsource-12": "صبح کا فلورسینٹ (D 5700 – 7100K)",
+       "exif-lightsource-13": "دن کا سفید فلورسینٹ (N 4600 – 5400K)",
+       "exif-lightsource-14": "خنک سفید فلورسینٹ (W 3900 – 4500K)",
+       "exif-lightsource-15": "سفید فلورسینٹ (WW 3200 – 3700K)",
+       "exif-lightsource-17": "معیاری روشنی A",
+       "exif-lightsource-18": "معیاری روشنی B",
+       "exif-lightsource-19": "معیاری روشنی C",
+       "exif-lightsource-24": "ٹنگسٹن کا آئیسو اسٹوڈیو",
+       "exif-lightsource-255": "روشنی کا دوسرا ماخذ",
+       "exif-flash-fired-0": "فلیش نہیں چلا",
+       "exif-flash-fired-1": "فلیش چالو ہوا",
+       "exif-flash-return-0": "منعکس روشنی کی دریافت کی کوئی سہولت نہیں ہے",
+       "exif-flash-return-2": "منعکس روشنی دریافت نہیں ہوئی",
+       "exif-flash-return-3": "منعکس روشنی دریافت ہوئی",
+       "exif-flash-mode-1": "فلیش چلنا لازمی",
+       "exif-flash-mode-2": "فلیش نہ چلنا لازمی",
+       "exif-flash-mode-3": "خودکار حالت",
+       "exif-flash-function-1": "فلیش کی سہولت نہیں",
+       "exif-flash-redeye-1": "سرخی چشم کی درستی کی حالت",
        "exif-focalplaneresolutionunit-2": "انچ",
        "exif-sensingmethod-1": "غیر وضاحتی",
+       "exif-sensingmethod-2": "علاقہ کی یک تراشہ رنگی کا سینسر",
+       "exif-sensingmethod-3": "علاقہ کی دو تراشہ رنگی کا سینسر",
+       "exif-sensingmethod-4": "علاقہ کی سہ تراشہ رنگی کا سینسر",
+       "exif-sensingmethod-5": "علاقہ میں رنگوں کی ترتیب کا سینسر",
+       "exif-sensingmethod-7": "سہ خطی سینسر",
+       "exif-sensingmethod-8": "رنگوں کی ترتیب کا خطی سینسر",
+       "exif-filesource-3": "ڈیجیٹل اسٹل کیمرا",
+       "exif-scenetype-1": "براہ راست کھینچی گئی تصویر",
        "exif-customrendered-0": "عام عمل",
        "exif-customrendered-1": "اپنی مرضی کے مطابق عمل",
+       "exif-exposuremode-0": "خودکار نمائش",
+       "exif-exposuremode-1": "دستی نمائش",
+       "exif-exposuremode-2": "آٹو بریکٹ",
+       "exif-whitebalance-0": "سفید رنگ کا خودکار توازن",
+       "exif-whitebalance-1": "سفید رنگ کا دستی توازن",
        "exif-scenecapturetype-0": "معیاری",
+       "exif-scenecapturetype-1": "افقی انداز",
+       "exif-scenecapturetype-2": "عمودی انداز",
+       "exif-scenecapturetype-3": "رات کا منظر",
+       "exif-gaincontrol-0": "کچھ نہیں",
+       "exif-gaincontrol-1": "لو گین اپ",
+       "exif-gaincontrol-2": "ہائی گین اپ",
+       "exif-gaincontrol-3": "لو گین ڈاؤن",
+       "exif-gaincontrol-4": "ہائی گین ڈاؤن",
        "exif-contrast-0": "عام",
        "exif-contrast-1": "نرم",
        "exif-contrast-2": "سخت",
        "exif-saturation-0": "عام",
+       "exif-saturation-1": "سیال رنگ",
+       "exif-saturation-2": "ٹھوس رنگ",
        "exif-sharpness-0": "عام",
        "exif-sharpness-1": "نرم",
        "exif-sharpness-2": "سخت",
        "exif-gpsaltitude-above-sealevel": "سطح سمندر سے $1 {{PLURAL:$1|میٹر}} بلند",
        "exif-gpsaltitude-below-sealevel": "سطح سمندر سے $1 {{PLURAL:$1|میٹر}} نیچے",
        "exif-gpsstatus-a": "پیمائش جاری ہے",
+       "exif-gpsstatus-v": "پیمائش پذیری",
+       "exif-gpsmeasuremode-2": "دو ابعادی پیمائش",
+       "exif-gpsmeasuremode-3": "سہ ابعادی پیمائش",
        "exif-gpsspeed-k": "کلو میٹر فی گھنٹہ",
        "exif-gpsspeed-m": "میل فی گھنٹہ",
+       "exif-gpsspeed-n": "گرہیں",
        "exif-gpsdestdistance-k": "کلومیٹر",
        "exif-gpsdestdistance-m": "میل",
        "exif-gpsdestdistance-n": "سمندری میل",
        "exif-gpsdirection-t": "اصلی سمت",
        "exif-gpsdirection-m": "مقناطیسی سمت",
        "exif-ycbcrpositioning-1": "وسط",
+       "exif-ycbcrpositioning-2": "مشترکہ منظر کشی",
        "exif-dc-contributor": "ترمیم کنندگان",
+       "exif-dc-coverage": "میڈیا کی مکانی یا زمانی وسعت",
        "exif-dc-date": "تاریخ",
        "exif-dc-publisher": "ناشر",
        "exif-dc-relation": "متعلقہ میڈیا",
        "exif-urgency-normal": "عام ($1)",
        "exif-urgency-low": "کم ($1)",
        "exif-urgency-high": "اعلیٰ ($1)",
+       "exif-urgency-other": "صارف کی وضاحت کردہ ترجیح ($1)",
        "namespacesall": "تمام",
        "monthsall": "تمام",
        "confirmemail": "اپنے برقی پتہ کی تصدیق کریں",
        "confirmemail_noemail": "آپ نے [[Special:Preferences|اپنی ترجیحات]] میں درست برقی ڈاک پتا نہیں دیا ہے۔",
+       "confirmemail_text": "{{SITENAME}} میں موجود برقی خط کی سہولتوں کو استعمال کرنے کے لیے آپ کے برقی ڈاک پتے کی تصدیق ضروری ہے۔\nاپنے پتے پر تصدیقی ڈاک روانہ کرنے کے لیے ذیل میں موجود بٹن پر کلک کریں۔\nموصولہ برقی خط میں آپ کو کوڈ پر مشتمل ایک ربط نظر آئے گا۔\nچنانچہ اپنے بڑقی ڈاک پتے کی تصدیق کے لیے اس ربط کو اپنے براؤزر میں کھولیں۔",
+       "confirmemail_pending": "آپ کو تصدیقی کوڈ پہلے ہی روانہ کیا جا چکا ہے۔\nاگر آپ نے ابھی اپنا کھاتہ بنایا ہے تو نئے کوڈ کی درخواست دینے سے قبل اس کے موصول ہونے کا کچھ دیر انتظار کر لیں۔",
        "confirmemail_send": "تصدیقی کوڈ بھیجیں",
        "confirmemail_sent": "تصدیقی برقی خط بھیجا گیا ہے۔",
        "confirmemail_oncreate": "آپ کے برقی ڈاک پتے پر تصدیقی کوڈ بھیجا گیا ہے۔\nیہ کوڈ داخل ہونے کے لیے ضروری نہیں، تاہم اس ویکی میں برقی ڈاک پر مبنی کسی سہولت کو فعال کرنے سے قبل آپ کو اس کوڈ کی ضرورت پڑے گی۔",
        "confirmemail_success": "آپ کے برقی ڈاک پتے کی تصدیق ہو چکی ہے۔\nاب آپ اپنے کھاتے میں [[Special:UserLogin|داخل ہو سکتے ہیں]]۔",
        "confirmemail_loggedin": "اب آپ کے برقی ڈاک پتے کی تصدیق ہو چکی ہے۔",
        "confirmemail_subject": "{{SITENAME}} کی جانب سے برقی ڈاک پتے کا تصدیقی پیغام",
+       "confirmemail_body": "کسی نے، غالباً آپ نے، اس آئی پی پتے $1 سے {{SITENAME}} میں «$2» کے نام سے کھاتہ بنایا اور اسی برقی ڈاک پتے کو استعمال کیا ہے۔\n\nاس بات کی تصدیق کے لیے کہ یہ کھاتہ آپ ہی کا ہے نیز {{SITENAME}} میں برقی خط کی سہولتوں کو فعال کرنے کے لیے ذیل میں موجود ربط کو اپنے براؤزر میں کھولیں:\n\n$3\n\nاگر آپ نے یہ کھاتہ *نہیں* کھولا ہے تو اس برقی ڈاک پتے کی تصدیق کو منسوخ کرنے کے لیے اس ربط پر جائیں:\n\n$5\n\nاس تصدیقی کوڈ کی مدت $4 تک ختم ہو جائے گی۔",
+       "confirmemail_body_changed": "کسی نے، غالباً آپ نے، اس آئی پی پتے $1 سے {{SITENAME}} میں «$2» کے نام سے موجود کھاتے کا برقی ڈاک پتہ تبدیل کیا ہے۔\n\nاس بات کی تصدیق کے لیے کہ یہ کھاتہ آپ ہی کا ہے نیز {{SITENAME}} میں برقی خط کی سہولتوں کو دوبارہ فعال کرنے کے لیے ذیل میں موجود ربط کو اپنے براؤزر میں کھولیں:\n\n$3\n\nاگر یہ کھاتہ آپ کا *نہیں* ہے تو اس برقی ڈاک پتے کی تصدیق کو منسوخ کرنے کے لیے اس ربط پر جائیں:\n\n$5\n\nاس تصدیقی کوڈ کی مدت $4 تک ختم ہو جائے گی۔",
+       "confirmemail_body_set": "کسی نے، غالباً آپ نے، اس آئی پی پتے $1 سے {{SITENAME}} میں «$2» کے نام سے موجود کھاتے میں یہ برقی ڈاک پتا دیا ہے۔\n\nاس بات کی تصدیق کے لیے کہ یہ کھاتہ آپ ہی کا ہے نیز {{SITENAME}} میں برقی خط کی سہولتوں کو فعال کرنے کے لیے ذیل میں موجود ربط کو اپنے براؤزر میں کھولیں:\n\n$3\n\nاگر یہ کھاتہ آپ کا *نہیں* ہے تو اس برقی ڈاک پتے کی تصدیق کو منسوخ کرنے کے لیے اس ربط پر جائیں:\n\n$5\n\nاس تصدیقی کوڈ کی مدت $4 تک ختم ہو جائے گی۔",
        "confirmemail_invalidated": "برقی ڈاک پتے کی تصدیق منسوخ ہو گئی",
        "invalidateemail": "برقی ڈاک کی تصدیق منسوخ کریں",
+       "notificationemail_subject_changed": "{{SITENAME}} میں درج کردہ برقی ڈاک پتا تبدیل ہو چکا ہے",
+       "notificationemail_subject_removed": "{{SITENAME}} میں درج کردہ برقی ڈاک پتا حذف ہو چکا ہے",
+       "notificationemail_body_changed": "کسی نے، غالباً آپ نے، اس آئی پی پتے $1 سے {{SITENAME}} میں «$2» کے نام سے موجود کھاتے کے برقی ڈاک پتے کو $3 میں تبدیل کیا ہے۔\n\nاگر وہ شخص آپ نہیں ہے تو سائٹ کے کسی منتظم سے فوراً رابطہ کریں۔",
+       "notificationemail_body_removed": "کسی نے، غالباً آپ نے، اس آئی پی پتے $1 سے {{SITENAME}} میں «$2» کے نام سے موجود کھاتے کے برقی ڈاک پتے کو حذف کیا ہے۔\n\nاگر وہ شخص آپ نہیں ہے تو سائٹ کے کسی منتظم سے فوراً رابطہ کریں۔",
+       "scarytranscludedisabled": "[بین الویکی شمولیت غیر فعال ہے]",
+       "scarytranscludefailed": "[$1 کے لیے سانچہ اخذ نہیں کیا جا سکا]",
+       "scarytranscludefailed-httpstatus": "[$1 کے لیے سانچہ اخذ نہیں کیا جا سکا: HTTP $2]",
        "scarytranscludetoolong": "[یوآرایل بہت طویل ہے]",
        "deletedwhileediting": "<strong>انتباہ:</strong>: آپ کے ترمیم شروع کرنے کے بعد یہ صفحہ حذف کیا جا چکا ہے!",
+       "confirmrecreate": "آپ کی ترمیم شروع ہونے کے بعد صارف [[User:$1|$1]] ([[User talk:$1|talk]]) نے اس صفحہ کو {{GENDER:$1|حذف کر دیا}}، اس کی وجہ حسب ذیل ہے:\n: <em>$2</em>\nبراہ کرم اس بات کی تصدیق کر لیں کہ آیا آپ واقعی اس صفحہ کو دوبارہ تخلیق کرنا چاہتے ہیں یا نہیں۔",
+       "confirmrecreate-noreason": "آپ کی ترمیم شروع ہونے کے بعد صارف [[User:$1|$1]] ([[User talk:$1|talk]]) نے اس صفحہ کو {{GENDER:$1|حذف کر دیا}}۔\nبراہ کرم اس بات کی تصدیق کر لیں کہ آیا آپ واقعی اس صفحہ کو دوبارہ تخلیق کرنا چاہتے ہیں یا نہیں۔",
        "recreate": "دوبارہ تخلیق کریں",
        "confirm_purge_button": "جی!",
+       "confirm-purge-top": "اس صفحہ کا کیشے صاف کریں؟",
+       "confirm-purge-bottom": "صفحہ کا کیشے صارف کرنے پر تازہ ترین نسخہ نظر آئے گا۔",
        "confirm-watch-button": "ٹھیک",
        "confirm-watch-top": "اس صفحہ کو آپ کی زیر نظر فہرست میں شامل کریں؟",
        "confirm-unwatch-button": "ٹھیک ہے",
        "imgmultigo": "جائیں!",
        "imgmultigoto": "$1 صفحہ پر جائیں",
        "img-lang-default": "(طے شدہ زبان)",
-       "img-lang-go": "ٹھیک",
+       "img-lang-info": "تصویر کا اس زبان میں ترجمہ کریں $1۔ $2",
+       "img-lang-go": "چلیں",
        "ascending_abbrev": "صعودی",
        "descending_abbrev": "نزولی",
        "table_pager_next": "اگلا صفحہ",
        "table_pager_first": "پہلا صفحہ",
        "table_pager_last": "آخری صفحہ",
        "table_pager_limit": "فی صفحہ $1 آئٹم دکھائیں",
-       "table_pager_limit_label": "آئٹم فی صفحہ:",
+       "table_pager_limit_label": "فی صفحہ اندراج:",
        "table_pager_limit_submit": "چلیں",
        "table_pager_empty": "کوئی نتیجہ برآمد نہیں ہوا",
        "autosumm-blank": "تمام مندرجات حذف",
        "autosumm-replace": "\"$1\" سے مواد کی تبدیلی",
        "autoredircomment": "[[$1]] سے رجوع مکرر",
-       "autosumm-new": "«$1» پر مشتمل نیا صفحہ بنایا",
+       "autosumm-new": "نے «$1» مواد پر مشتمل نیا صفحہ بنایا",
        "autosumm-newblank": "خالی صفحہ بنایا",
        "size-bytes": "$1 بائٹ",
+       "size-kilobytes": "$1 کلوبائٹ",
+       "lag-warn-normal": "گزشتہ $1 {{PLURAL:$1|سیکنڈ|سیکنڈوں}} میں ہونے والی تبدیلیاں شاید اس فہرست میں نظر نہ آئیں۔",
+       "lag-warn-high": "ڈیٹابیس سرور کی جانب سے بے حد تاخیر کی بنا پر گزشتہ $1 {{PLURAL:$1|سیکنڈ|سیکنڈوں}} میں ہونے والی تبدیلیاں شاید اس فہرست میں نظر نہ آئیں۔",
        "watchlistedit-normal-title": "زیر نظر فہرست میں ترمیم کریں",
        "watchlistedit-normal-legend": "زیرنظر فہرست سے عناوین نکالیں",
-       "watchlistedit-normal-submit": "عناوین نکالیں",
+       "watchlistedit-normal-explain": "آپ کی زیرنظر فہرست میں موجود عناوین ذیل میں موجود ہیں۔\nکسی عنوان کو حذف کرنے کے لیے اس کے سامنے موجود خانہ کو نشان زد کریں اور «{{int:Watchlistedit-normal-submit}}» پر کلک کریں۔\nنیز آپ [[Special:EditWatchlist/raw|خام فہرست]] میں بھی ترمیم کر سکتے ہیں۔",
+       "watchlistedit-normal-submit": "عناوین حذف کریں",
+       "watchlistedit-normal-done": "آپ کی زیرنظر فہرست سے {{PLURAL:$1|ایک عنوان حذف کیا گیا|$1 عناوین حذف کیے گئے}}:",
        "watchlistedit-raw-title": "خام زیرِنظرفہرست میں ترمیم کریں",
        "watchlistedit-raw-legend": "خام زیرِنظرفہرست میں ترمیم کریں",
+       "watchlistedit-raw-explain": "آپ کی زیرنظر فہرست میں موجود عناوین ذیل میں موجود ہیں، ان عناوین کو اس فہرست سے حذف کسی مزید عناوین شامل کیے جا سکتے ہیں؛\nفی سطر ایک عنوان درج کریں۔\nترمیم مکمل ہو جانے پر «{{int:Watchlistedit-raw-submit}}» پر کلک کریں۔\nنیز آپ اس میں ترمیم و تبدیلی کے لیے [[Special:EditWatchlist|معیاری خانہ ترمیم]] بھی استعمال کر سکتے ہیں۔",
        "watchlistedit-raw-titles": "عناوین:",
        "watchlistedit-raw-submit": "زیرنظر فہرست کی تجدید کریں",
        "watchlistedit-raw-done": "آپ کی زیرنظر فہرست کی تجدید ہو چکی ہے۔",
+       "watchlistedit-raw-added": "{{PLURAL:$1|1 عنوان |$1 عناوین}} شامل {{PLURAL:$1|کیا گیا|کیے گئے}}:",
+       "watchlistedit-raw-removed": "{{PLURAL:$1|1 عنوان حذف کیا گیا|$1 عناوین حذف کیے گئے}}:",
        "watchlistedit-clear-title": "اپنی زیر نظر فہرست صاف کریں",
        "watchlistedit-clear-legend": "اپنی زیر نظر فہرست صاف کریں",
+       "watchlistedit-clear-explain": "آپ کی زیرنظر فہرست سے تمام عناوین حذف کر دیے جائیں گے",
        "watchlistedit-clear-titles": "عناوین:",
+       "watchlistedit-clear-submit": "زیرنظر فہرست صاف کریں (یہ دائمی ہے!)",
+       "watchlistedit-clear-done": "آپ کی زیرنظر فہرست صاف ہو چکی ہے۔",
+       "watchlistedit-clear-removed": "{{PLURAL:$1|1 عنوان حذف کیا گیا|$1 عناوین حذف کیے گئے}}:",
+       "watchlistedit-too-many": "نمائش کے لیے صفحات کی تعداد بہت زیادہ ہے۔",
        "watchlisttools-clear": "زیرنظر فہرست کی صفائی",
        "watchlisttools-view": "متعلقہ تبدیلیاں دیکھیں",
        "watchlisttools-edit": "زیرِنظرفہرست دیکھیں اور تدوین کریں",
        "hijri-calendar-m12": "ذوالحجہ",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|تبادلۂ خیال]])",
        "timezone-local": "مقامی",
+       "duplicate-defaultsort": "<strong>انتباہ:</strong> سابقہ ابتدائی کلید ترتیب «$1» کی بجائے اب «$2» ہی ابتدائی کلید ترتیب ہوگی۔",
+       "duplicate-displaytitle": "<strong>انتباہ:</strong> سابقہ عنوان «$1» کی بجائے اب «$2» عنوان ہوگا۔",
        "restricted-displaytitle": "<strong>انتباہ!:</strong> عنوان \"$1\" کو نظر انداز کر دیا گیا ہے کیونکہ یہ متعلقہ صفحہ کے عنوان کا حقیقی متبادل نہیں ہے۔",
+       "invalid-indicator-name": "<strong>نقص:</strong> صفحہ کی صورت حال کے اشارہ نما کی <code>name</code> خاصیت خالی نہیں ہونی چاہیے۔",
        "version": "نسخہ",
        "version-extensions": "نصب شدہ توسیعات",
        "version-skins": "نصب شدہ پوشاکیں",
        "version-poweredby-others": "دیگر",
        "version-poweredby-translators": "translatewiki.net کے مترجمین",
        "version-credits-summary": "ہم درج ذیل اشخاص کی [[Special:Version|میڈیاویکی]] کی تعمیر میں شرکت کرنے کا اعتراف کرتے ہیں۔",
-       "version-license-info": "میڈیاویکی ایک آزاد سافٹ ویئر ہے؛ آپ اسے آزاد سافٹ ویئر فاؤنڈیش کی جانب سے شائع کردہ گنو عام عوامی اجازت نامہ کی شرائط کے تحت دوبارہ شائع/یا اس میں تبدیلی کر سکتے ہیں؛ خواہ مذکورہ اجازت نامہ کے نسخہ دوم کے تحت شائع کریں یا (حسب منشا) کسی جدید نسخے کے تحت۔\n\nیقیناً میڈیاویکی کو اس امید کے ساتھ شائع کیا گیا ہے کہ یہ مفید ثابت ہوگا، لیکن کوئی ضمانت نہیں ہے؛ نہ قابل تجارت ہونے کی اطلاقی ضمانت ہے اور نہ کسی مخصوص مقصد کے لیے موزوں ہونے کی۔ مزید تفصیلات کے لئے گنو کا عام عوامی اجازت نامہ ملاحظہ فرمائیں۔\n\nآپ کو اس پروگرام کے ساتھ  [{{SERVER}}{{SCRIPTPATH}}/COPYING گنو عام عوامی اجازت نامہ کا ایک نسخہ] بھی موصول ہوگا؛ اگر یہ نسخہ نہ ملے تو Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA پتے پر خط و کتابت کریں یا [//www.gnu.org/licenses/old-licenses/gpl-2.0.html اسے آن لائن پڑھیں]۔",
+       "version-license-info": "میڈیاویکی ایک آزاد سافٹ ویئر ہے؛ آپ اسے آزاد سافٹ ویئر فاؤنڈیشن کے شایع کردہ گنو عام عوامی اجازت نامے کی شرائط کے مطابق دوبارہ شایع یا اس میں تبدیلی کر سکتے ہیں، خواہ اس اجازت نامے کے نسخہ دوم کے مطابق شایع کریں یا حسب منشا کسی نئے نسخے کے مطابق۔\n\nیقیناً میڈیاویکی کی اشاعت سے امید ہے کہ یہ مفید ثابت ہوگا، لیکن اس کی  کوئی قطعی ضمانت نہیں ہے، نہ قابل تجارت ہونے کی اطلاقی ضمانت ہے اور نہ کسی خاص مقصد کے لیے موزوں ہونے کی۔ مزید تفصیل کے لیے گنو کا عام عوامی اجازت نامہ ملاحظہ فرمائیں۔\n\nآپ کو اس پروگرام کے ساتھ  [{{SERVER}}{{SCRIPTPATH}}/COPYING گنو عام عوامی اجازت نامہ کا ایک نسخہ] بھی موصول ہوگا؛ اگر یہ نسخہ نہ ملے تو Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA پتے پر خط و کتابت کریں یا [//www.gnu.org/licenses/old-licenses/gpl-2.0.html اسے آن لائن پڑھیں]۔",
        "version-software": "نصب شدہ سافٹ ویئر",
        "version-software-product": "مصنوعات",
        "version-software-version": "نسخہ",
        "intentionallyblankpage": "اس صفحہ کو دانستہ خالی چھوڑا گیا ہے۔",
        "external_image_whitelist": "#اس سطر کو ہو بہو ایسا ہی رہنے دیں<pre>\n#ذیل میں ریجیکس کی عبارتیں درج کریں (محض // کے درمیان)\n#ان عبارتوں کی بیرونی تصویروں کے روابط سے مطابقت کی جائے گی\n#جو مطابق ہو جائیں وہ تصویر کے طور پر نظر آئیں گے ورنہ محض تصویر کا ربط ظاہر ہوگا\n# علامت # سے شرع ہونے والی سطروں کو تبصرہ سمجھا جائے گا\n#چھوٹے بڑے حروف کو نظر انداز کیا جائے گا\n\nریجیکس کی تمام عبارتوں کو اس سطر کے اوپر رکھیں۔ اس سطر کو ہو بہو ایسا ہی رہنے دیں</pre>",
        "tags": "تبدیلی کے درست ٹیگ",
-       "tag-filter": "[[Special:Tags|لوحہ]] فلٹر:",
+       "tag-filter": "مقطار [[Special:Tags|ٹیگ]]:",
        "tag-filter-submit": "مقطار",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|ٹیگ}}]]: $2)",
        "tag-mw-contentmodelchange": "مواد کے ماڈل میں تبدیلی",
        "tags-create-warnings-below": "کیا آپ واقعی ٹیگ سازی جاری رکھنا چاہتے ہیں؟",
        "tags-delete-title": "حذف ٹیگ",
        "tags-delete-explanation-initial": "آپ «$1» ٹیگ کو ڈیٹابیس سے حذف کرنے جا رہے ہیں۔",
+       "tags-delete-explanation-in-use": "اس ٹیگ کو {{PLURAL:$2|$2 نسخے یا اندراج نوشتہ|تمام $2 نسخوں اور/یا اندراجات نوشتہ}} سے ہٹا دیا جائے گا جہاں یہ زیر استعمال ہے۔",
+       "tags-delete-explanation-warning": "یہ اقدام <strong>ناقابل تغیر</strong> ہے اور اسے <strong>واپس نہیں پھیرا جا سکتا</strong>، حتی کہ ڈیٹابیس کے منتظمین بھی اس معاملے میں معذور ہیں۔ لہذا اس بات کا یقین کر لیں کہ آیا یہ وہی ٹیگ ہے جسے آپ حذف کرنا چاہتے ہیں۔",
+       "tags-delete-explanation-active": "<strong> ٹیگ \"$1\" فعال ہے اور آئندہ بھی فعال رہے گا۔</strong> اسے روکنے کے لیے اس جگہ/ان جگہوں پر جائیں جہاں یہ ٹیگ زیر استعمال ہے اور وہاں اسے غیر فعال کر دیں۔",
        "tags-delete-reason": "وجہ:",
+       "tags-delete-submit": "اس ٹیگ کو اٹل طور پر حذف کریں",
+       "tags-delete-not-allowed": "کسی توسیع کے ذریعہ تخلیق کردہ ٹیگوں کو اس وقت تک حذف نہیں کیا جا سکتا جب تک متعلقہ توسیع خود اس کی سہولت فراہم نہ کرے۔",
        "tags-delete-not-found": "«$1» ٹیگ موجود نہیں ہے۔",
+       "tags-delete-too-many-uses": "ٹیگ \"$1\" کو $2 سے زائد {{PLURAL:$2|نسخے|نسخوں}} میں مستعمل ہے، چنانچہ اسے حذف نہیں کیا جا سکتا۔",
+       "tags-delete-warnings-after-delete": "ٹیگ \"$1\" حذف ہو چکا ہے، لیکن درج ذیل {{PLURAL:$2|انتباہ دیا گیا|انتباہات سامنے آئے}}:",
        "tags-delete-no-permission": "آپ کو تبدیلی کے ٹیگ حذف کرنے کی اجازت نہیں۔",
        "tags-activate-title": "ٹیگ فعال",
        "tags-activate-question": "آپ «$1» ٹیگ کو فعال کرنے جا رہے ہیں۔",
        "tags-edit-success": "تبدیلیاں نافذ کر دی گئیں۔",
        "tags-edit-failure": "تبدیلیاں نافذ نہیں کی جا سکیں:\n$1",
        "tags-edit-nooldid-title": "نادرست ہدف نسخہ",
+       "tags-edit-nooldid-text": "اس کارروائی کو انجام دینے کے لیے یا تو آپ نے کسی ہدف نسخے کا تعین نہیں کیا ہے، یا متعینہ نسخہ موجود نہیں ہے۔",
+       "tags-edit-none-selected": "اضافہ کرنے یا ہٹانے کے لیے کم از کم ایک ٹیگ منتخب کریں۔",
        "comparepages": "صفحات کا موازنہ کریں",
        "compare-page1": "صفحہ 1",
        "compare-page2": "صفحہ 2",
        "compare-revision-not-exists": "آپ کی اختصاصی نظرثانی موجود نہیں۔",
        "dberr-problems": "افسوس! اس ویب سائٹ کو تکنیکی مشکلات کا سامنا ہے۔",
        "dberr-again": "چند منٹ انتظار کے بعد دوبارہ کوشش کریں۔",
+       "dberr-info": "(ڈیٹا بیس تک رسائی نہیں مل سکی: $1)",
+       "dberr-info-hidden": "(ڈیٹا بیس تک رسائی نہیں مل سکی)",
+       "dberr-usegoogle": "اسی درمیان میں آپ گوگل کے ذریعہ تلاش کرنے کی کوشش کر سکتے ہیں۔",
+       "dberr-outofdate": "واضح رہے کہ ہمارے مواد کے متعلق ان کے اشاریے ممکن ہے پرانے ہو چکے ہوں۔",
+       "dberr-cachederror": "یہ درخواست شدہ صفحہ کا کیشے شدہ نسخہ ہے اور ممکن ہے تازہ نہ ہو۔",
        "htmlform-invalid-input": "آپ کے اندراج میں کچھ مسائل ہیں۔",
        "htmlform-select-badoption": "آپ کی درج کردہ قدر درست اختیار نہیں ہے۔",
        "htmlform-int-invalid": "آپ کی درج کردہ قدر عدد صحیح نہیں ہے۔",
        "revdelete-unrestricted": "منتظمین کے لیے کھول دیا گیا",
        "logentry-block-block": "$1 نے {{GENDER:$4|$3}} پر $5 کے وقت اختتام تک {{GENDER:$2|پابندی لگائی}} $6",
        "logentry-block-unblock": "$1 نے {{GENDER:$4|$3}} سے {{GENDER:$2|پابندی اٹھائی}}",
+       "logentry-block-reblock": "$1 نے {{GENDER:$4|$3}} کی ترتیبات پابندی کو {{GENDER:$2|تبدیل کیا}}، اب مدت اختتام $5 $6 ہے۔",
+       "logentry-suppress-block": "$1 نے {{GENDER:$4|$3}} پر $5 کے وقت اختتام تک {{GENDER:$2|پابندی لگائی}} $6",
+       "logentry-suppress-reblock": "$1 نے {{GENDER:$4|$3}} کی ترتیبات پابندی کو {{GENDER:$2|تبدیل کیا}}، اب مدت اختتام $5 $6 ہے۔",
+       "logentry-import-upload": "$1 نے $3 کو فائل اپلوڈ کی مدد سے {{GENDER:$2|درآمد کیا}}",
+       "logentry-import-upload-details": "$1 نے $3 کو فائل اپلوڈ کی مدد سے {{GENDER:$2|درآمد کیا}} ($4 {{PLURAL:$4|نسخہ|نسخے}})",
+       "logentry-import-interwiki": "$1 نے $3 کو دوسری ویکی سے {{GENDER:$2|درآمد کیا}}",
+       "logentry-import-interwiki-details": "$1 نے $3 کو $5 سے {{GENDER:$2|درآمد کیا}} ($4 {{PLURAL:$4|نسخہ|نسخے}})",
+       "logentry-merge-merge": "$1 نے $3 کو $4 میں {{GENDER:$2|ضم کیا}} ($5 تک نسخے)",
        "logentry-move-move": "$1 نے صفحہ $3 کو $4 کی جانب منتقل کیا",
+       "logentry-move-move-noredirect": "$1 نے صفحہ $3 کو $4 کی جانب بدون رجوع مکرر {{GENDER:$2|منتقل کیا}}",
+       "logentry-move-move_redir": "$1 نے رجوع مکرر ہٹا کر صفحہ $3 کو $4 کی جانب {{GENDER:$2|منتقل کیا}}",
        "logentry-move-move_redir-noredirect": "$1 نے صفحہ $3 کو رجوع مکرر چھوڑے بغیر $4 کی جانب جو رجوع مکر تھا {{GENDER:$2|منتقل کیا}}",
+       "logentry-patrol-patrol": "$1 نے صفحہ $3 کے نسخہ $4 کو مراجعت شدہ {{GENDER:$2|نشان زد کیا}}",
+       "logentry-patrol-patrol-auto": "$1 نے صفحہ $3 کے نسخہ $4 کو خودکار طور پر مراجعت شدہ {{GENDER:$2|نشان زد کیا}}",
+       "logentry-newusers-newusers": "صارف کھاتہ $1 {{GENDER:$2|تخلیق ہو چکا ہے}}",
        "logentry-newusers-create": "صارف کھاتہ $1 {{GENDER:$2|بنایا گیا}}",
+       "logentry-newusers-create2": "$1 نے صارف کھاتہ $3 {{GENDER:$2|تخلیق کیا}}",
+       "logentry-newusers-byemail": "$1 نے صارف کھاتہ $3 {{GENDER:$2|تخلیق کیا}} اور پاس ورڈ بذریعہ برقی خط روانہ کیا گیا ہے",
+       "logentry-newusers-autocreate": "صارف کھاتہ $1 خودکار طور پر {{GENDER:$2|تخلیق ہوا}}",
        "logentry-protect-move_prot": "$1 نے ترتیب درجہ حفاظت $4 سے $3 کی طرف {{GENDER:$2|منتقل کی}}",
+       "logentry-protect-unprotect": "$1 نے $3 سے حفاظت {{GENDER:$2|ختم کی}}",
        "logentry-protect-protect": "$1 نے $3 کو {{GENDER:$2|محفوظ کیا}}  $4",
+       "logentry-protect-protect-cascade": "$1 نے $3 کو {{GENDER:$2|محفوظ کیا}} $4 [آبشاری]",
        "logentry-protect-modify": "$1 نے $3 کا درجۂ حفاظت {{GENDER:$2|تبدیل کیا}} $4",
+       "logentry-protect-modify-cascade": "$1 نے $3 کا درجہ حفاظت {{GENDER:$2|تبدیل کیا}} $4 [آبشاری]",
        "logentry-rights-rights": "$1 نے {{GENDER:$6|$3}} کی گروہی رکنیت از $4 تا $5 {{GENDER:$2|تبدیل کی}}",
+       "logentry-rights-rights-legacy": "$1 نے $3 کی گروہی روکنیت کو {{GENDER:$2|تبدیل کیا}}",
+       "logentry-rights-autopromote": "$1 کو خودکار طور پر $4 سے $5 پر {{GENDER:$2|ترقی مل گئی}}",
        "logentry-upload-upload": "$1 {{GENDER:$2|اپلوڈ}} $3",
+       "logentry-upload-overwrite": "$1 نے $3 کا نیا نسخہ {{GENDER:$2|اپلوڈ کیا}}",
+       "logentry-upload-revert": "$1 نے $3 کو {{GENDER:$2|اپلوڈ کیا}}",
+       "log-name-managetags": "نوشتہ انتظام ٹیگ",
+       "log-description-managetags": "اس صفحہ میں [[Special:Tags|ٹیگوں]] سے متعلق انتظامی کاموں کی فہرست درج ہے۔ اس نوشتہ میں محض ان اقدامات کی فہرست ہے جنہیں کسی منتظم نے از خود انجام دیا ہو؛ تاہم ویکی سافٹویئر کی مدد سے ٹیگوں کو تخلیق یا حذف کیا جا سکتا ہے، جس کا اندراج اس نوشتہ میں ہونا ضروری نہیں۔",
+       "logentry-managetags-create": "$1 نے «$4» ٹیگ کو {{GENDER:$2|بنایا}}",
+       "logentry-managetags-delete": "$1 نے ٹیگ \"$4\" کو {{GENDER:$2|حذف کیا}} ($5 {{PLURAL:$5|نسخے یا اندراج نوشتہ|نسخوں یا اندراجات نوشتہ}} سے حذف کیا گیا)",
+       "logentry-managetags-activate": "$1 نے ٹیگ «$4» کو صارفین اور روبہ جات کے استعمال کے لیے {{GENDER:$2|فعال کیا}}",
+       "logentry-managetags-deactivate": "$1 نے ٹیگ «$4» کو صارفین اور روبہ جات کے استعمال کے لیے {{GENDER:$2|غیر فعال کیا}}",
        "log-name-tag": "نوشتہ ٹیگ",
+       "log-description-tag": "ذیل میں صارفین کی جانب سے انفرادی نسخوں یا اندراجات نوشتہ سے [[Special:Tags|ٹیگوں]] کے حذف و اضافہ کا نوشتہ دیکھا جا سکتا ہے۔ تاہم اس نوشتہ میں ٹیگ کاری کے اقدامات -مثلاً کب وہ کسی ترمیم یا حذف شدگی وغیرہ کا جزو بنے- کی فہرست نہیں ہے۔",
+       "logentry-tag-update-add-revision": "$1 نے صفحہ $3 کے نسخہ $4 پر $6 {{PLURAL:$7|ٹیگ|ٹیگوں}} کو {{GENDER:$2|شامل کیا}}",
+       "logentry-tag-update-add-logentry": "$1 نے صفحہ $3 کے اندراج نوشتہ $5 میں $6 {{PLURAL:$7|ٹیگ|ٹیگوں}} کو {{GENDER:$2|شامل کیا}}",
+       "logentry-tag-update-remove-revision": "$1 نے صفحہ $3 کے نسخہ $4 سے $8 {{PLURAL:$9|ٹیگ|ٹیگوں}} کو {{GENDER:$2|حذف کیا}}",
+       "logentry-tag-update-remove-logentry": "$1 نے صفحہ $3 کے اندراج نوشتہ $5 سے $8 {{PLURAL:$9|ٹیگ|ٹیگوں}} کو {{GENDER:$2|حذف کیا}}",
+       "logentry-tag-update-revision": "$1 نے صفحہ $3 کے نسخہ $4 پر موجود ٹیگوں کو {{GENDER:$2|تازہ کیا}} ({{PLURAL:$7|شامل کیا گیا|شامل کیے گئے}} $6؛ {{PLURAL:$9|حذف کیا گیا|حذف کیے گئے}} $8)",
+       "logentry-tag-update-logentry": "$1 نے صفحہ $3 کے اندراج نوشتہ $5 پر موجود ٹیگوں کو {{GENDER:$2|تازہ کیا}} ({{PLURAL:$7|شامل کیا گیا|شامل کیے گئے}} $6؛ {{PLURAL:$9|حذف کیا گیا|حذف کیے گئے}} $8)",
        "rightsnone": "(کچھ نہیں)",
        "revdelete-summary": "خلاصۂ تدوین",
+       "feedback-adding": "صفحہ میں تبصرہ درج کیا جا رہا ہے۔۔۔",
        "feedback-back": "واپس",
+       "feedback-bugcheck": "زبردست! جانچ لیں کہ کہیں پہلے ہی [$1 اس کی اطلاع نہ دے دی گئی ہو]۔",
+       "feedback-bugnew": "میں نے جانچ لیا ہے۔ نئی خامی کی شکایت کریں",
+       "feedback-bugornote": "اگر آپ کسی تکنیکی مسئلہ کو تفصیل سے بیان کر سکتے ہیں تو براہ کرم [$1 یہاں خامی کی اطلاع دیں]۔\nورنہ ذیل میں موجود فارم کا استعمال کریں۔ آپ کا تبصرہ آپ کے صارف نام کے ساتھ صفحہ «[$3 $2]» میں شائع کر دیا جائے گا۔",
        "feedback-cancel": "منسوخ",
        "feedback-close": "مکمل",
+       "feedback-external-bug-report-button": "تکنیکی خامی کی اطلاع دیں",
        "feedback-dialog-title": "تبصرہ روانہ کریں",
+       "feedback-dialog-intro": "اپنا تبصرہ شائع کرنے کے لیے ذیل میں موجود فارم کو استعمال کر سکتے ہیں۔ آپ کا تبصرہ آپ کے صارف نام کے ساتھ صفحہ «$1» میں شامل کر دیا جائے گا۔",
        "feedback-error-title": "نقص",
        "feedback-error1": "نقص: اے پی آئی کی جانب سے غیر معروف نتیجہ",
        "feedback-error2": "نقص: ترمیم ناکام ہو گئی",
        "feedback-error3": "نقص: اے پی آئی سے کوئی جواب نہیں",
+       "feedback-error4": "نقص: درج کردہ عنوان تبصرہ کے تحت شائع نہیں کیا جا سکا۔",
        "feedback-message": "پیغام:",
        "feedback-subject": "موضوع:",
        "feedback-submit": "روانہ کریں",
+       "feedback-terms": "میں اس امر سے بخوبی واقف ہوں کہ میری یوزر ایجنٹ معلومات کے تحت میرے زیر استعمال براؤزر اور آپریٹنگ سسٹم کے نسخے کی معلومات بھی شامل ہیں اور انہیں میرے تبصرے کے ساتھ عوامی طور پر شائع کیا جائے گا۔",
+       "feedback-termsofuse": "میں شرائط استعمال کے مطابق تبصرہ درج کرنے پر متفق ہوں۔",
+       "feedback-thanks": "شکریہ! آپ کا تبصرہ صفحہ «[$1 $2]» میں درج کر دیا گیا ہے۔",
        "feedback-thanks-title": "شکریہ!",
+       "feedback-useragent": "یوزر ایجنٹ:",
        "searchsuggest-search": "تلاش",
        "searchsuggest-containing": "نتائج...",
+       "api-error-autoblocked": "آپ کے آئی پی پتے پر خودکار طور پر پابندی لگا دی گئی ہے، کیونکہ اسے کسی ممنوع صارف نے استعمال کیا ہے۔",
+       "api-error-badaccess-groups": "آپ کو اس ویکی میں فائلیں اپلوڈ کرنے کی اجازت نہیں ہے۔",
+       "api-error-badtoken": "داخلی نقص: غلط ٹوکن۔",
+       "api-error-blocked": "آپ کی ترمیم کاری پر پابندی لگا دی گئی ہے۔",
+       "api-error-copyuploaddisabled": "یوآرایل کے ذریعہ اس سرور پر اپلوڈ کو غیر فعال کر دیا گیا ہے۔",
+       "api-error-duplicate": "یکساں مواد کی حامل {{PLURAL:$1|ایک اور فائل|مزید فائلیں}} ویکی پر موجود {{PLURAL:$1|ہے|ہیں}}۔",
+       "api-error-duplicate-archive": "یکساں مواد کی حامل {{PLURAL:$1|ایک اور فائل|مزید فائلیں}} ویکی پر موجود {{PLURAL:$1|تھی|تھیں}}، لیکن {{PLURAL:$1|اسے|انہیں}} حذف کر دیا گیا۔",
+       "api-error-empty-file": "آپ کی ارسال کردہ فائل خالی تھی۔",
+       "api-error-emptypage": "نئے خالی صفحات بنانے کی اجازت نہیں ہے۔",
+       "api-error-fetchfileerror": "داخلی نقص: فائل کو اخذ کرنے کے دوران میں کچھ غلط ہوا ہے۔",
+       "api-error-fileexists-forbidden": "«$1» کے نام سے ایک فائل پہلے سے موجود ہے، اسے تبدیل نہیں کیا جا سکتا۔",
+       "api-error-fileexists-shared-forbidden": "«$1» کے نام سے مشترکہ ذخیرے میں ایک فائل پہلے سے موجود ہے، اسے تبدیل نہیں کیا جا سکتا۔",
        "api-error-file-too-large": "آپ کی ارسال کردہ فائل بہت بڑی تھی۔",
        "api-error-filename-tooshort": "فائل کا نام انتہائی مختصر ہے۔",
        "api-error-filetype-banned": "فائل کی اس قسم پر پابندی عائد ہے۔",
+       "api-error-filetype-banned-type": "$1 نوعیت کی {{PLURAL:$4|فائل|فائلوں}} کی اجازت نہیں۔\nاجازت یافتہ نوعیت کی {{PLURAL:$3|فائل|فائلیں}} $2 {{PLURAL:$3|ہے|ہیں}}۔",
        "api-error-filetype-missing": "فائل کی توسیع موجود نہیں",
+       "api-error-hookaborted": "آپ نے جو تبدیلی کرنے کی کوشش کی اسے کسی توسیع نے منسوخ کر دیا۔",
+       "api-error-http": "داخلی نقص: سرور سے رابطہ نہیں ہو سکا",
        "api-error-illegal-filename": "اس نام کی فائل ممنوع ہے۔",
+       "api-error-internal-error": "داخلی نقص: ویکی پر آپ کے اپلوڈ کی انجام دہی کے دوران میں کچھ غلط واقع ہوا۔",
+       "api-error-invalid-file-key": "داخلی نقص: عارضی ذخیرے میں فائل نہیں مل سکی۔",
+       "api-error-missingparam": "داخلی نقص: درخواست میں مفقود متغیرات",
+       "api-error-missingresult": "داخلی نقص: نہیں بتایا جا سکتا کہ نقل و چسپاں کا عمل کامیاب ہوا یا نہیں۔",
+       "api-error-mustbeloggedin": "فائلیں اپلوڈ کرنے کے لیے آپ کا داخل ہونا ضروری ہے۔",
+       "api-error-mustbeposted": "داخلی نقص: یہ درخواست HTTP POST کی متقاضی ہے۔",
+       "api-error-noimageinfo": "اپلوڈ کامیاب رہا لیکن فائل کے متعلق سرور نے ہمیں کسی قسم کی معلومات بہم نہیں پہنچائیں۔",
+       "api-error-nomodule": "داخلی نقص: کسی ماڈیول کو مرتب نہیں کیا گیا۔",
+       "api-error-ok-but-empty": "داخلی نقص: سرور سے کوئی جواب نہیں ملا۔",
+       "api-error-overwrite": "موجودہ فائل کو دوبارہ اپلوڈ کرنے کی اجازت نہیں۔",
+       "api-error-ratelimited": "مختصر وقت میں آپ اس ویکی میں اجازت یافتہ تعداد سے زیادہ فائلوں کو اپلوڈ کرنے کی کوشش کر رہے ہیں۔\nبراہ کرم کچھ منٹ بعد دوبارہ کوشش کریں۔",
+       "api-error-stashfailed": "داخلی نقص: عارضی فائل رکھنے میں سرور کو ناکامی ہوئی۔",
+       "api-error-publishfailed": "داخل نقص: عارضی فائل شائع کرنے میں سرور کو ناکامی ہوئی۔",
+       "api-error-stasherror": "نہاں خانے میں فائل کو اپلوڈ کرتے وقت کوئی نقص واقع ہوا۔",
+       "api-error-stashedfilenotfound": "نہاں خانے میں رکھی گئی فائل وہاں سے اپلوڈ کرنے کے دوران نہیں ملی۔",
+       "api-error-stashpathinvalid": "وہ جگہ غلط ہے جہاں پوشیدہ فائل ملنی چاہیے تھی۔",
+       "api-error-stashfilestorage": "نہاں خانے میں فائل کو رکھتے وقت کوئی نقص واقع ہوا۔",
+       "api-error-stashzerolength": "سرور اس فائل کو پوشیدہ نہ کر سکا کیونکہ اس کی لمبائی صفر ہے۔",
+       "api-error-stashnotloggedin": "اپلوڈ کے نہاں خانے میں فائلوں کو محفوظ کرنے کے لیے آپ کا داخل ہونا ضروری ہے۔",
+       "api-error-stashwrongowner": "فائل کی جس کلید کے ذریعہ آپ نہاں خانے میں رسائی کی کوشش کر رہے ہیں وہ آپ کی نہیں ہے۔",
+       "api-error-stashnosuchfilekey": "فائل کی جس کلید کے ذریعہ آپ نہاں خانے میں رسائی کی کوشش کر رہے ہیں وہ موجود نہیں۔",
+       "api-error-timeout": "متوقع مدت کے دوران میں سرور نے کوئی جواب نہیں دیا۔",
        "api-error-unclassified": "نامعلوم نقص واقع ہوا۔",
        "api-error-unknown-code": "نامعلوم نقص: \"$1\" ۔",
+       "api-error-unknown-error": "داخلی نقص: آپ کی فائل کو اپلوڈ کرنے کے دوران میں کچھ غلط ہو گیا ہے۔",
+       "api-error-unknown-warning": "نامعلوم انتباہ: \"$1\"",
+       "api-error-unknownerror": "نامعلوم نقص: \"$1\"",
        "api-error-uploaddisabled": "اس ویکی پر اپلوڈ کی سہولت غیر فعال ہے۔",
        "api-error-verification-error": "شاید فائل خراب ہے یا غلط توسیع کی حامل ہے۔",
+       "api-error-was-deleted": "اس نام کی فائل پہلے اپلوڈ کی گئی تھی اور معاً بعد حذف کر دی گئی۔",
        "duration-seconds": "$1 {{PLURAL:$1|سیکنڈ}}",
        "duration-minutes": "$1 {{PLURAL:$1|منٹ}}",
        "duration-hours": "$1 {{PLURAL:$1|گھنٹہ|گھنٹے}}",
        "duration-decades": "$1 {{PLURAL:$1|دہائی|دہائیاں}}",
        "duration-centuries": "$1 {{PLURAL:$1|صدی|صدیاں}}",
        "duration-millennia": "$1 {{PLURAL:$1|ہزاریے|ہزاریہ}}",
+       "rotate-comment": "تصویر $1 {{PLURAL:$1|درجہ|درجے}} بائیں سے دائیں گھمائی گئی",
+       "limitreport-title": "تجزیاتی ڈیٹا:",
+       "limitreport-cputime": "سی پی یو استعمال کا وقت",
        "limitreport-cputime-value": "$1 {{PLURAL:$1|سیکنڈ}}",
+       "limitreport-walltime": "حقیقی استعمال کا وقت",
        "limitreport-walltime-value": "$1 {{PLURAL:$1|سیکنڈ}}",
+       "limitreport-ppvisitednodes": "پراسیسر کی مشاہدہ کردہ گرہوں کی تعداد",
+       "limitreport-ppgeneratednodes": "پراسیسر کی مدد  سے جاری کردہ گرہوں کی تعداد",
+       "limitreport-postexpandincludesize": "بعد از توسیع شمولیت کا حجم",
        "limitreport-postexpandincludesize-value": "$1/$2 {{PLURAL:$2|بائٹ}}",
+       "limitreport-templateargumentsize": "سانچہ آرگومنٹ کا حجم",
        "limitreport-templateargumentsize-value": "$1/$2 {{PLURAL:$2|بائٹ}}",
+       "limitreport-expansiondepth": "توسیع کی بلند ترین گہرائی",
+       "limitreport-expensivefunctioncount": "کثیر الاستعمال پارسر فنکشنوں کی تعداد",
        "expandtemplates": "سانچے کو وسیع کریں",
+       "expand_templates_intro": "اس خصوصی صفحہ میں ویکی کی عبارتوں کو اخذ کرکے ان میں موجود تمام مستعمل سانچوں کو کھولا جاتا ہے۔\nنیز اس صفحہ میں <code><nowiki>{{</nowiki>#language:…}}</code> جیسے پارسر فنکشنوں اور <code><nowiki>{{</nowiki>CURRENTDAY}}</code> جیسے متغیرات کی معاونت بھی رکھی گئی ہے۔\nدرحقیقت یہاں ہر چیز کو دوہرے محرابی قوسین میں کھول دیا جاتا ہے۔",
+       "expand_templates_title": "اس عبارت کا عنوان، مثلاً {{FULLPAGENAME}} وغیرہ کے لیے:",
        "expand_templates_input": "ان پٹ متن:",
        "expand_templates_output": "نتیجہ",
+       "expand_templates_xml_output": "XML آؤٹ پٹ",
+       "expand_templates_html_output": "ایچ ٹی ایم ایل کا خام نتیجہ",
        "expand_templates_ok": "ٹھیک ہے",
        "expand_templates_remove_comments": "تبصرے حذف کریں",
+       "expand_templates_remove_nowiki": "نتیجہ میں <nowiki> کے ٹیگوں کو چھپائیں",
+       "expand_templates_generate_xml": "ایکس ایم ایل تجزیہ کے درخت کو دکھائیں",
        "expand_templates_generate_rawhtml": "خام اہچ ٹی ایم ایل دکھائیں",
        "expand_templates_preview": "پیش نظارہ",
+       "expand_templates_preview_fail_html": "<em>چونکہ {{SITENAME}} نے خام ایچ ٹی ایم ایل فعال کر رکھا ہے اور نشست کا ڈیٹا گم ہو گیا ہے لہذا جاوا اسکرپٹ کے طوفان بدتمیزی سے تحفظ کے لیے نمائش کو پوشیدہ رکھا گیا ہے۔</em>\n\n<strong>اگر نمائش کی یہ کوشش درست ہے تو براہ کرم دوبارہ کوشش کریں۔</strong>\nاگر اب بھی کامیابی نہ ملے تو [[Special:UserLogout|خارج ہو کر]] دوبارہ داخل ہوں، نیز اپنے براؤز کی ترتیبات کو بھی جانچ لیں کہ آیا اس میں کوکیز کو ذخیرہ کرنے کی اجازت ہے یا نہیں۔",
+       "expand_templates_preview_fail_html_anon": "<em>چونکہ {{SITENAME}} نے خام ایچ ٹی ایم ایل فعال کر رکھا ہے اور آپ داخل نہیں ہیں لہذا جاوا اسکرپٹ کے طوفان بدتمیزی سے تحفظ کے لیے نمائش کو پوشیدہ رکھا گیا ہے۔</em>\n\n<strong>اگر نمائش کی یہ کوشش درست ہے تو براہ کرم [[Special:UserLogin|داخل ہوں]] اور دوبارہ کوشش کریں۔</strong>",
+       "expand_templates_input_missing": "آپ کو کم از کم کچھ متن درج کرنا ہوگا۔",
        "pagelanguage": "صفحے کی زبان تبدیل کریں",
        "pagelang-name": "صفحہ",
        "pagelang-language": "زبان",
        "action-pagelang": "صفحے کی زبان تبدیل کریں",
        "log-name-pagelang": "نوشتہ تبدیلی زبان",
        "log-description-pagelang": "ذیل میں زبانوں کے صفحہ میں ہونے والی تبدیلیوں کا نوشتہ ہے۔",
+       "logentry-pagelang-pagelang": "$1 نے $3 کی زبان کو $4 سے $5 میں {{GENDER:$2|تبدیل کیا}}",
+       "default-skin-not-found": "اوہ! <code>$wgDefaultSkin</code> میں <code>$1</code> کے نام سے درج شدہ آپ کی ویکی کی ابتدائی پوشاک دستیاب نہیں ہے۔\n\nایسا معلوم ہوتا ہے کہ آپ کی تنصیب میں حسب ذیل {{PLURAL:$4|پوشاک|پوشاکیں}} موجود {{PLURAL:$4|ہے|ہیں}}۔ {{PLURAL:$4|پوشاک|پوشاکوں}} کو فعال کرنے اور ابتدائی پوشاک کو منتخب کرنے کے بارے میں مزید تفصیل کے لیے [https://www.mediawiki.org/wiki/Manual:Skin_configuration رہنما: ترتیبات پوشاک] ملاحظہ فرمائیں۔\n\n$2\n\n;اگر آپ نے میڈیاویکی کو ابھی نصب کیا ہے تو:\n:شاید آپ نے اسے گٹ سے یا کوئی دوسرا طریقہ استعمال کرکے براہ راست سورس کوڈ سے نصب کیا ہے۔ میڈیاویکی 1.24 اور اس کے بعد کی اشاعتوں میں پوشاکیں شامل نہیں ہیں۔ چنانچہ [https://www.mediawiki.org/wiki/Category:All_skins میڈیاویکی ڈاٹ آرگ میں موجود پوشاکوں کی ڈائرکٹری] سے کچھ پوشاکیں نصب کرنے کی کوشش کریں، جس کے لئے آپ:\n:* [https://www.mediawiki.org/wiki/Download ٹاربال انسٹالر] ڈاؤنلوڈ کریں، اس میں متعدد پوشاکیں اور کچھ توسیعیں موجود ہیں۔ آپ اس کی <code>skins/</code> ڈائرکٹری کو نقل و چسپاں کر سکتے ہیں۔\n:* انفرادی پوشاکوں کے ٹاربال [https://www.mediawiki.org/wiki/Special:SkinDistributor میڈیاویکی ڈاٹ آرگ] سے ڈاؤنلوڈ کریں۔\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins گٹ کے ذریعہ پوشاکیں ڈاؤنلوڈ کریں]۔\n:اگر آپ میڈیاویکی کے ترقی دہندہ ہیں تو اس عمل کے دوران میں اپنے گٹ ذخیرے سے تعارض نہ کریں۔\n\n; اگر آپ نے ابھی میڈیاویکی کی تجدید کی ہو تو:\n: میڈیاویکی 1.24 اور اس کے بعد کی اشاعتوں میں خودکار طور پر نصب شدہ پوشاکیں فعال نہیں ہوتیں (مزید تفصیل کے لیے [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery رہنما: پوشاک کی خودکار دریافت] ملاحظہ فرمائیں)۔ نیز آپ {{PLURAL:$5|پوشاک|تمام پوشاکوں}} کو فعال کرنے کے لیے درج ذیل {{PLURAL:$5|سطر|سطروں}} کو <code>LocalSettings.php</code> میں چسپاں کر سکتے ہیں:\n\n<pre dir=\"ltr\">$3</pre>\n\n; اگر آپ نے ابھی <code>LocalSettings.php</code> میں تبدیلی کی ہو اور پوشاک فعال نہ ہو رہی ہو تو:\n: اپنی تبدیلی کو دوبارہ جانچ لیں، کہیں لکھنے کے دوران میں ہجے غلط نہ ہو گئے ہو۔",
+       "default-skin-not-found-no-skins": "اوہ! <code>$wgDefaultSkin</code> میں <code>$1</code> کے نام سے درج شدہ آپ کی ویکی کی ابتدائی پوشاک دستیاب نہیں ہے۔\n\nاور آپ نے کسی پوشاک کو نصب نہیں کیا ہے۔\n\n;اگر آپ نے میڈیاویکی کو ابھی نصب کیا ہے یا اس کی تجدید کی ہے تو:\n:شاید آپ نے اسے گٹ سے یا کوئی دوسرا طریقہ استعمال کرکے براہ راست سورس کوڈ سے نصب کیا ہے۔ میڈیاویکی 1.24 اور اس کے بعد کی اشاعتوں میں پوشاکیں شامل نہیں ہیں۔ چنانچہ [https://www.mediawiki.org/wiki/Category:All_skins میڈیاویکی ڈاٹ آرگ میں موجود پوشاکوں کی ڈائرکٹری] سے کچھ پوشاکیں نصب کرنے کی کوشش کریں، جس کے لئے آپ:\n:* [https://www.mediawiki.org/wiki/Download ٹاربال انسٹالر] ڈاؤنلوڈ کریں، اس میں متعدد پوشاکیں اور کچھ توسیعیں موجود ہیں۔ آپ اس کی <code>skins/</code> ڈائرکٹری کو نقل و چسپاں کر سکتے ہیں۔\n:* انفرادی پوشاکوں کے ٹاربال [https://www.mediawiki.org/wiki/Special:SkinDistributor میڈیاویکی ڈاٹ آرگ] سے ڈاؤنلوڈ کریں۔\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins گٹ کے ذریعہ پوشاکیں ڈاؤنلوڈ کریں]۔\n:اگر آپ میڈیاویکی کے ترقی دہندہ ہیں تو اس عمل کے دوران میں اپنے گٹ ذخیرے سے تعارض نہ کریں۔ نیز پوشاکوں کو فعال کرنے اور ابتدائی پوشاک کو منتخب کرنے کے بارے میں مزید تفصیل کے لیے [https://www.mediawiki.org/wiki/Manual:Skin_configuration رہنما: ترتیبات پوشاک] ملاحظہ فرمائیں۔",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (فعال)",
+       "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>غیر فعال</strong>)",
        "mediastatistics": "میڈیا کے اعداد و شمار",
+       "mediastatistics-summary": "اپلوڈ کردہ فائلوں کی اقسام کے اعداد و شمار۔ ان میں محض فائل کے تازہ ترین نسخے ہی شامل اور قدیم یا حذف شدہ نسخے خارج کر دیے گئے ہیں۔",
+       "mediastatistics-nbytes": "{{PLURAL:$1|$1 بائٹ}} ($2؛ $3%)",
+       "mediastatistics-bytespertype": "اس قطعہ کی تمام فائلوں کا کل حجم: {{PLURAL:$1|$1 بائٹ}} ($2؛ $3%)",
+       "mediastatistics-allbytes": "تمام فائلوں کا کل حجم: {{PLURAL:$1|$1 بائٹ}} ($2)",
        "mediastatistics-table-mimetype": "MIME قسم",
        "mediastatistics-table-extensions": "ممکنہ توسیعات",
        "mediastatistics-table-count": "فائلوں کی تعداد",
        "mediastatistics-header-drawing": "خاکے (ویکٹر تصویریں)",
        "mediastatistics-header-audio": "آڈیو",
        "mediastatistics-header-video": "ویڈیو",
+       "mediastatistics-header-multimedia": "رچ میڈیا",
        "mediastatistics-header-office": "دفتر",
        "mediastatistics-header-text": "متنی",
        "mediastatistics-header-executable": "قابل تنفیذ",
        "mediastatistics-header-archive": "کمپریس شدہ فارمیٹ",
        "mediastatistics-header-total": "تمام فائلیں",
+       "json-warn-trailing-comma": "آخر میں رہ جانے والے $1 {{PLURAL:$1|وقفہ|وقفوں}} کو جے سن سے حذف کر دیا گیا ہے",
        "json-error-unknown": "جے سن میں کوئی مسئلہ تھا۔ نقص: $1",
+       "json-error-depth": "اسٹیک کی گہرائی کی آخری حد تجاوز کر چکی ہے۔",
+       "json-error-state-mismatch": "نادرست یا بدشکل جے سن",
+       "json-error-ctrl-char": "کنٹرول حروف میں نقص، ممکنہ طور پر انہیں غلط کوڈ کیا گیا ہے",
        "json-error-syntax": "نحوی غلطی",
+       "json-error-utf8": "نادرست یو ٹی ایف 8 حروف، ممکنہ طور پر انہیں غلط کوڈ کیا گیا ہے",
+       "json-error-recursion": "اینکوڈ کی جانے والی قدر میں ایک یا زائد متواتر حوالے موجود ہیں",
+       "json-error-inf-or-nan": "NAN یا INF کی ایک یا زائد قدریں اینکوڈ کی جانے والی قدر میں موجود ہیں",
+       "json-error-unsupported-type": "ایسی قدر دی گئی ہے جسے اینکوڈ نہیں کیا جا سکتا",
        "headline-anchor-title": "اس قطعہ کا ربط",
        "special-characters-group-latin": "لاطینی محارف",
        "special-characters-group-latinextended": "وسیع لاطینی",
        "special-characters-group-thai": "سیامی",
        "special-characters-group-lao": "لاوسی",
        "special-characters-group-khmer": "کھمیری",
-       "special-characters-title-endash": "خط Ù\81اصÙ\84Û\81",
+       "special-characters-title-endash": "عÙ\84اÙ\85ت Ø®Ø·",
        "special-characters-title-emdash": "خط فاصل کشیدہ",
        "special-characters-title-minus": "علامت وضع",
        "mw-widgets-dateinput-no-date": "کسی تاریخ کو منتخب نہیں کیا گیا",
        "mw-widgets-titleinput-description-new-page": "صفحہ ابھی تک موجود نہیں",
        "mw-widgets-titleinput-description-redirect": "$1 کا رجوع مکرر",
+       "sessionmanager-tie": "تصدیقی درخواست کی متعدد قسموں کو یکجا نہیں کیا جا سکتا: $1",
        "sessionprovider-generic": "$1 کی نشستیں",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "کوکی پر مبنی نشستیں",
        "sessionprovider-nocookies": "شاید کوکی غیر فعال ہے۔ براہ کرم کوکی فعال کرنے کے بعد دوبارہ کوشش کریں۔",
+       "randomrootpage": "بے ترتيب بنیادی صفحہ",
        "log-action-filter-block": "پابندی کی نوعیت:",
        "log-action-filter-contentmodel": "مواد کے ماڈل کی تبدیلی کی نوعیت:",
        "log-action-filter-delete": "حذف کی نوعیت:",
        "log-action-filter-suppress-reblock": "بذریعہ باز پابندی صارف کی پوشیدگی",
        "log-action-filter-upload-upload": "نیا اپلوڈ",
        "log-action-filter-upload-overwrite": "دوبارہ لوڈ",
+       "authmanager-authn-not-in-progress": "تصدیق کا عمل جاری نہیں ہے یا نشست کا ڈیٹا گم ہو چکا ہے۔ براہ کرم آغاز سے دوبارہ کوشش کریں۔",
+       "authmanager-authn-no-primary": "فراہم کردہ وثیقوں کی تصدیق نہیں ہو سکی۔",
+       "authmanager-authn-no-local-user": "فراہم کردہ وثیقے اس ویکی کے کسی صارف سے منسلک نہیں ہیں۔",
+       "authmanager-authn-no-local-user-link": "فراہم کردہ وثیقے اس ویکی کے کسی صارف سے منسلک نہیں ہیں۔ کسی دوسرے طریقے سے لاگ ہوں یا نیا کھاتا بنائیں۔ اس صورت میں آپ کو یہ اختیار حاصل ہوگا کہ سابقہ وثیقوں کو اس کھاتے سے مربوط کر سکیں۔",
        "authmanager-authn-autocreate-failed": "خودکار مقامی کھاتہ سازی ناکام: $1",
+       "authmanager-change-not-supported": "فراہم کردہ وثیقے غیر مستعمل ہونے کی وجہ سے انہیں تبدیل نہیں کیا جا سکتا۔",
        "authmanager-create-disabled": "کھاتہ سازی غیر فعال ہے۔",
+       "authmanager-create-from-login": "اپنا کھاتہ بنانے کے لیے براہ کرم ذیل میں موجود خانوں کو پُر کریں۔",
+       "authmanager-create-not-in-progress": "کھاتہ سازی کا عمل جاری نہیں ہے یا نشست کا ڈیٹا گم ہو چکا ہے۔ براہ کرم آغاز سے دوبارہ کوشش کریں۔",
+       "authmanager-create-no-primary": "فراہم کردہ وثیقوں کو کھاتہ سازی کے لیے استعمال نہیں کیا جا سکا۔",
+       "authmanager-link-no-primary": "فراہم کردہ وثیقوں کو کھاتوں سے مربوط کرنے کے لیے استعمال نہیں کیا جا سکا۔",
+       "authmanager-link-not-in-progress": "کھاتوں کو مربوط کرنے کا عمل جاری نہ رہ سکا یا نشست کا ڈیٹا گم ہو چکا ہے۔ براہ کرم آغاز سے دوبارہ کوشش کریں۔",
        "authmanager-authplugin-setpass-failed-title": "پاس ورڈ کی تبدیلی ناکام رہی",
+       "authmanager-authplugin-setpass-failed-message": "تصدیقی ہلگ ان نے پاس ورڈ کی تبدیلی کو رد کر دیا۔",
+       "authmanager-authplugin-create-fail": "تصدیقی ہلگ ان نے کھاتہ سازی کو رد کر دیا۔",
+       "authmanager-authplugin-setpass-denied": "تصدیقی ہلگ ان میں پاس ورڈ کی تبدیلی کی اجازت نہیں ہے۔",
        "authmanager-authplugin-setpass-bad-domain": "نادرست ڈومین۔",
        "authmanager-autocreate-noperm": "خودکار کھاتہ سازی کی اجازت نہیں ہے۔",
        "authmanager-autocreate-exception": "سابقہ نقص کی وجہ سے عارضی طور پر خودکار کھاتہ سازی غیر فعال ہے۔",
        "authmanager-userdoesnotexist": "«$1» کے نام سے صارف کھاتہ مندرج نہیں ہے۔",
+       "authmanager-userlogin-remembermypassword-help": "نشست کی مدت سے زیادہ عرصہ کے لیے پاس ورڈ کو یاد رکھیں۔",
        "authmanager-username-help": "صارف نام برائے تصدیق",
        "authmanager-password-help": "پاس ورڈ برائے تصدیق",
        "authmanager-domain-help": "ڈومین برائے خارجی تصدیق",
        "authmanager-provider-password": "پاس ورڈ پر مبنی تصدیق",
        "authmanager-provider-password-domain": "پاس ورڈ اور ڈومین پر مبنی تصدیق",
        "authmanager-provider-temporarypassword": "عارضی پاس ورڈ",
+       "authprovider-confirmlink-message": "آپ کی جانب سے لاگ ان کی حالیہ کوششوں کے پیش نظر، آپ ذیل میں موجود کھاتوں کو اپنے ویکی کھاتے سے منسلک کر سکتے ہیں۔ چنانچہ انہیں مربوط کرنے کے بعد آپ ان کھاتوں کی مدد سے بھی داخل ہو سکیں گے۔ لہذا جنہیں منسلک کرنا ہو انہیں منتخب کریں۔",
        "authprovider-confirmlink-request-label": "جن کھاتوں کو مربوط کرنا ہو",
        "authprovider-confirmlink-success-line": "$1: کامیابی سے مربوط کر دیے گئے۔",
+       "authprovider-confirmlink-failed": "کھاتوں کو مربوط کرنے کا عمل مکمل نہیں ہو سکا: $1",
+       "authprovider-confirmlink-ok-help": "ناکامی کے پیغام کی نمائش کے بعد جاری رکھیں",
        "authprovider-resetpass-skip-label": "آگے بڑھیں",
        "authprovider-resetpass-skip-help": "پاس ورڈ کی ترتیب نو کو رہنے دیں",
+       "authform-nosession-login": "تصدیق کامیاب رہی لیکن آپ کا براؤزر لاگ ان کو \"برقرار\" نہیں رکھ سکا۔\n\n$1",
+       "authform-nosession-signup": "کھاتہ بن چکا ہے لیکن آپ کا براؤزر لاگ ان کو \"برقرار\" نہیں رکھ سکا۔\n\n$1",
+       "authform-newtoken": "ٹوکن مفقود۔ $1",
        "authform-notoken": "ٹوکن مفقود",
        "authform-wrongtoken": "غلط ٹوکن",
        "specialpage-securitylevel-not-allowed-title": "اجازت نہیں",
+       "specialpage-securitylevel-not-allowed": "معذرت، آپ کو اس صفحہ کے استعمال کی اجازت نہیں ہے کیونکہ آپ کی شناخت کی تصدیق نہیں ہو سکی۔",
+       "authpage-cannot-login": "لاگ ان شروع نہیں ہو سکا۔",
+       "authpage-cannot-login-continue": "لاگ ان جاری نہیں رہ سکتی۔ غالباً آپ کی نشست کی مدت ختم ہو چکی ہے۔",
+       "authpage-cannot-create": "کھاتہ سازی کا آغاز نہیں ہو سکا۔",
+       "authpage-cannot-create-continue": "کھاتہ سازی جاری نہیں رہ سکتی۔ غالباً آپ کی نشست کی مدت ختم ہو چکی ہے۔",
+       "authpage-cannot-link": "کھاتے کو مربوط کرنے کا عمل شروع نہیں ہو سکا۔",
+       "authpage-cannot-link-continue": "کھاتے کو مربوط کرنے کا عمل جاری نہیں رہ سکتا۔ غالباً آپ کی نشست کی مدت ختم ہو چکی ہے۔",
        "cannotauth-not-allowed-title": "اجازت رد کر دی گئی",
        "cannotauth-not-allowed": "آپ کو اس صفحہ کے استعمال کی اجازت نہیں",
        "changecredentials": "وثیقوں کو تبدیل کریں",
        "changecredentials-submit": "وثیقوں کو تبدیل کریں",
+       "changecredentials-invalidsubpage": "$1 وثیقے کی درست قسم نہیں ہے۔",
        "changecredentials-success": "آپ کے وثیقے تبدیل کر دیے گئے۔",
        "removecredentials": "وثیقے حذف کریں",
        "removecredentials-submit": "وثیقے حذف کریں",
+       "removecredentials-invalidsubpage": "$1 وثیقے کی درست قسم نہیں ہے۔",
        "removecredentials-success": "آپ کے وثیقے حذف کر دیے گئے۔",
        "credentialsform-provider": "وثیقوں کی نوعیت:",
        "credentialsform-account": "کھاتے کا نام:",
        "linkaccounts-submit": "کھاتوں کو مربوط کریں",
        "unlinkaccounts": "مربوط کھاتوں کو علاحدہ کریں",
        "unlinkaccounts-success": "مربوط کھاتہ علاحدہ کر دیا گیا۔",
+       "authenticationdatachange-ignored": "تصدیقی معلومات کی تبدیلی نہیں ہو سکی۔ شاید کوئی پرووائڈر فراہم نہیں کیا گیا؟",
        "userjsispublic": "براہ کرم اس بات کا خیال رکھیں کہ جاوا اسکرپٹ کے ذیلی صفحات میں خفیہ معلومات نہیں رکھی جانی چاہئیں کیونکہ ان صفحات کو دیگر صارفین بھی دیکھ سکتے ہیں۔",
-       "usercssispublic": "براہ کرم اس بات کا خیال رکھیں کہ سی ایس ایس کے ذیلی صفحات میں خفیہ معلومات نہیں رکھی جانی چاہئیں کیونکہ ان صفحات کو دیگر صارفین بھی دیکھ سکتے ہیں۔"
+       "usercssispublic": "براہ کرم اس بات کا خیال رکھیں کہ سی ایس ایس کے ذیلی صفحات میں خفیہ معلومات نہیں رکھی جانی چاہئیں کیونکہ ان صفحات کو دیگر صارفین بھی دیکھ سکتے ہیں۔",
+       "restrictionsfield-badip": "آئی پی پتا یا رینج نادرست ہے: $1",
+       "restrictionsfield-label": "آئی پی کی اجازت یافتہ رینج:",
+       "restrictionsfield-help": "فی سطر ایک آئی پی پتا یا سی آئی ڈی آر رینج۔ تمام کو فعال کرنے کے لیے <br><code>0.0.0.0/0</code><br><code>::/0</code> استعمال کریں"
 }
index 803c6a2..d2f0247 100644 (file)
        "botpasswords-label-cancel": "אַנולירן",
        "botpasswords-label-delete": "אויסמעקן",
        "botpasswords-label-resetpassword": "ווידערשטעלן פאַסווארט",
-       "botpasswords-label-restrictions": "באניץ באגרענעצונגען:",
        "botpasswords-label-grants-column": "נאכגעגעבן",
        "botpasswords-bad-appid": "דער באט נאמען \"$1\" איז אומגילטיק.",
        "botpasswords-created-title": "באט פאסווארט געשאפן",
        "randompage-nopages": "נישטא קיין בלעטער אין {{PLURAL:$2|דעם פאלגנדן נאמענטייל |די פאלגנדע נאמענטיילן}} \"$1\".",
        "randomincategory": "צופעליקער בלאט אין קאטעגאריע",
        "randomincategory-invalidcategory": "\"$1\" איז נישט קיין גילטיקער קאטעגאריע נאמען.",
-       "randomincategory-nopages": "נישט פאראן קיין בלעטער אין [[:Category:$1]].",
+       "randomincategory-nopages": "נישט פאראן קיין בלעטער אין דער [[:Category:$1]] קאטעגאריע.",
        "randomincategory-category": "קאַטעגאריע:",
        "randomincategory-legend": "צופעליקער בלאט אין קאטעגאריע",
        "randomincategory-submit": "גיין",
        "htmlform-cloner-create": "צולייגן נאך",
        "htmlform-cloner-delete": "אַראָפּנעמען",
        "htmlform-title-not-exists": "$1 עקזיסטירט נישט",
-       "sqlite-has-fts": "$1 מיט פולן-טעקסט זוכן שטיץ",
-       "sqlite-no-fts": "$1 אָן פֿולן-טעקסט זוכן שטיץ",
        "logentry-delete-delete": "$1 {{GENDER:$2|האט אויסגעמעקט}} בלאט $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|האט צוריקגעשטעלט }} בלאט $3",
        "logentry-delete-event": "$1 {{GENDER:$2|האט געענדערט}} די זעבארקייט פון {{PLURAL:$5|א לאגבוך אקטיוויטעט|$5 לאגבוך אקטיוויטעטן}} אויף $3: $4",
index bf09c1b..3798d91 100644 (file)
        "welcomecreation-msg": "您的账户已创建。\n如果需要,您可以更改您在{{SITENAME}}的[[Special:Preferences|参数设置]]。",
        "yourname": "用户名:",
        "userlogin-yourname": "用户名",
-       "userlogin-yourname-ph": "请输入的用户名",
+       "userlogin-yourname-ph": "请输入的用户名",
        "createacct-another-username-ph": "请输入用户名",
        "yourpassword": "密码:",
        "userlogin-yourpassword": "密码",
        "createaccount": "创建账户",
        "gotaccount": "已经拥有账户?请$1。",
        "gotaccountlink": "登录",
-       "userlogin-resetlink": "忘记的登录信息?",
+       "userlogin-resetlink": "忘记的登录信息?",
        "userlogin-resetpassword-link": "忘记密码?",
        "userlogin-helplink2": "登录帮助",
        "userlogin-loggedin": "您已经以{{GENDER:$1|$1}}的身份登录。使用下面的表格以其他用户的身份登录。",
        "eauthentsent": "一封确认信已经发送至您设定的邮件地址。\n在任何其他邮件发送至您的账户前,您将不得不根据邮件中的指示,确认那个账户确实是您的。",
        "throttled-mailpassword": "密码提醒已在最近$1小时内发送。为了安全起见,在每$1小时内只能发送一个密码提醒。",
        "mailerror": "发送邮件错误:$1",
-       "acct_creation_throttle_hit": "使用你的IP地址访问本wiki的访客在过去24小时中创建了{{PLURAL:$1|$1个账户}},达到了这段时间所允许的最大值。因此,使用该IP地址的访客现在不能再创建账户。",
+       "acct_creation_throttle_hit": "使用您的IP地址访问本wiki的访客在过去$2中创建了{{PLURAL:$1|$1个账户}},达到了这段时间所允许的最大值。因此,使用该IP地址的访客现在不能再创建账户。",
        "emailauthenticated": "您的电子邮件地址已于$2 $3确认。",
        "emailnotauthenticated": "您的邮件地址尚未确认。\n您将不会收到以下任何功能的邮件。",
        "noemailprefs": "指定一个电子邮箱地址以使用此功能。",
        "botpasswords-label-resetpassword": "重置密码",
        "botpasswords-label-grants": "应用授权:",
        "botpasswords-help-grants": "每个授权将会赋予被列出、且用户账户已拥有权限的访问权。参见[[Special:ListGrants|授权表]]以获取更多信息。",
-       "botpasswords-label-restrictions": "使用限制:",
        "botpasswords-label-grants-column": "已授权",
        "botpasswords-bad-appid": "机器人名“$1”无效。",
        "botpasswords-insert-failed": "无法添加机器人名“$1”。它是否已添加?",
        "action-userrights-interwiki": "编辑其他wiki用户的用户权限",
        "action-siteadmin": "锁定或解锁数据库",
        "action-sendemail": "发送电子邮件",
-       "action-editmywatchlist": "编辑的监视列表",
-       "action-viewmywatchlist": "查看的监视列表",
+       "action-editmywatchlist": "编辑的监视列表",
+       "action-viewmywatchlist": "查看的监视列表",
        "action-viewmyprivateinfo": "查看您的私人信息",
-       "action-editmyprivateinfo": "编辑的私人信息",
+       "action-editmyprivateinfo": "编辑的私人信息",
        "action-editcontentmodel": "编辑页面的内容模型",
        "action-managechangetags": "创建和(取消)激活标签",
        "action-applychangetags": "连同您的更改应用标签",
        "img-auth-public": "img_auth.php的功能是从非公开wiki输出文件。本wiki已被设置为公开。为了最佳安全状况,img_auth.php已停用。",
        "img-auth-noread": "用户无权读取“$1”。",
        "http-invalid-url": "无效URL:$1",
-       "http-invalid-scheme": "不支持带有“$1”的URL",
+       "http-invalid-scheme": "带“$1”方案的URL不受支持。",
        "http-request-error": "未知的错误令到HTTP请求失败。",
        "http-read-error": "HTTP读取错误。",
        "http-timed-out": "HTTP请求已过时。",
        "exif-imagewidth": "宽度",
        "exif-imagelength": "高度",
        "exif-bitspersample": "每像素字节数",
-       "exif-compression": "压缩方",
+       "exif-compression": "压缩方",
        "exif-photometricinterpretation": "像素构成",
        "exif-orientation": "方位",
        "exif-samplesperpixel": "像素数",
        "unlinkaccounts-success": "账户已取消链接。",
        "authenticationdatachange-ignored": "身份验证数据更改未处理。也许没有配置的提供者?",
        "userjsispublic": "请注意:JavaScript子页面不应包含机密数据,因为它们可以被其他用户查看。",
-       "usercssispublic": "请注意:CSS子页面不应包含机密数据,因为它们可以被其他用户查看。"
+       "usercssispublic": "请注意:CSS子页面不应包含机密数据,因为它们可以被其他用户查看。",
+       "restrictionsfield-badip": "无效的IP地址或段:$1",
+       "restrictionsfield-label": "允许的IP段:",
+       "restrictionsfield-help": "每行一个IP地址或CIDR段。要启用所有,可使用<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index 89168db..b81fbde 100644 (file)
@@ -1083,6 +1083,7 @@ return [
                        'resources/src/mediawiki/htmlform/autocomplete.js',
                        'resources/src/mediawiki/htmlform/autoinfuse.js',
                        'resources/src/mediawiki/htmlform/checkmatrix.js',
+                       'resources/src/mediawiki/htmlform/datetime.js',
                        'resources/src/mediawiki/htmlform/cloner.js',
                        'resources/src/mediawiki/htmlform/hide-if.js',
                        'resources/src/mediawiki/htmlform/multiselect.js',
@@ -1269,6 +1270,7 @@ return [
                'messages' => [
                        'upload-dialog-title',
                        'upload-dialog-button-cancel',
+                       'upload-dialog-button-back',
                        'upload-dialog-button-done',
                        'upload-dialog-button-save',
                        'upload-dialog-button-upload',
index 01d3442..91f797d 100644 (file)
                        } );
                }
 
+               // Our form input *should* be type="hidden". But if we're infusing from
+               // PHP, it's not.
+               if ( this.$input.attr( 'type' ) !== 'hidden' ) {
+                       try {
+                               this.$input.attr( 'type', 'hidden' );
+                       } catch ( e ) {
+                       }
+                       // IE <= 8, and IE 9 in quirks mode, doesn't allow changing the
+                       // type, so just hide the field with CSS. IE 9 in quirks mode
+                       // doesn't even throw an error, so do that unconditionally. Sigh.
+                       this.$input.css( 'display', 'none' );
+               }
+
                // Initialization
                this.setTabIndex( -1 );
 
diff --git a/resources/src/mediawiki/htmlform/datetime.js b/resources/src/mediawiki/htmlform/datetime.js
new file mode 100644 (file)
index 0000000..2fd2396
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * HTMLForm enhancements:
+ * Add minimal help for date and time fields
+ */
+( function ( mw ) {
+
+       mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+               var supported = {};
+
+               $root
+                       .find( 'input.mw-htmlform-datetime-field' )
+                       .each( function () {
+                               var input,
+                                       type = this.getAttribute( 'type' );
+
+                               if ( type !== 'date' && type !== 'time' && type !== 'datetime' ) {
+                                       // WTF?
+                                       return;
+                               }
+
+                               if ( supported[ type ] === undefined ) {
+                                       // Assume that if the browser implements validation (so it
+                                       // rejects "bogus" as a value) then it supports a proper UI too.
+                                       input = document.createElement( 'input' );
+                                       input.setAttribute( 'type', type );
+                                       input.value = 'bogus';
+                                       supported[ type ] = ( input.value !== 'bogus' );
+                               }
+
+                               if ( supported[ type ] ) {
+                                       if ( !this.getAttribute( 'min' ) ) {
+                                               this.setAttribute( 'min', this.getAttribute( 'data-min' ) );
+                                       }
+                                       if ( !this.getAttribute( 'max' ) ) {
+                                               this.setAttribute( 'max', this.getAttribute( 'data-max' ) );
+                                       }
+                                       if ( !this.getAttribute( 'step' ) ) {
+                                               this.setAttribute( 'step', this.getAttribute( 'data-step' ) );
+                                       }
+                               }
+                       } );
+       } );
+
+}( mediaWiki ) );
index e8a85f1..1ea7e04 100644 (file)
                        flags: 'safe',
                        action: 'cancel',
                        label: mw.msg( 'upload-dialog-button-cancel' ),
-                       modes: [ 'upload', 'insert', 'info' ]
+                       modes: [ 'upload', 'insert' ]
+               },
+               {
+                       flags: 'safe',
+                       action: 'cancelupload',
+                       label: mw.msg( 'upload-dialog-button-back' ),
+                       modes: [ 'info' ]
                },
                {
                        flags: [ 'primary', 'progressive' ],
                if ( action === 'cancel' ) {
                        return new OO.ui.Process( this.close() );
                }
+               if ( action === 'cancelupload' ) {
+                       return new OO.ui.Process( this.uploadBooklet.initialize() );
+               }
 
                return mw.Upload.Dialog.parent.prototype.getActionProcess.call( this, action );
        };
index 89bb83b..3122d42 100644 (file)
@@ -11,7 +11,7 @@
 ( function ( $ ) {
        'use strict';
 
-       var mw,
+       var mw, StringSet, log,
                hasOwn = Object.prototype.hasOwnProperty,
                slice = Array.prototype.slice,
                trackCallbacks = $.Callbacks( 'memory' ),
                return hash;
        }
 
+       StringSet = window.Set || ( function () {
+               /**
+                * @private
+                * @class
+                */
+               function StringSet() {
+                       this.set = {};
+               }
+               StringSet.prototype.add = function ( value ) {
+                       this.set[ value ] = true;
+               };
+               StringSet.prototype.has = function ( value ) {
+                       return this.set.hasOwnProperty( value );
+               };
+               return StringSet;
+       }() );
+
        /**
         * Create an object that can be read from or written to from methods that allow
         * interaction both with single and multiple properties at once.
                }
        };
 
+       log = ( function () {
+               // Also update the restoration of methods in mediawiki.log.js
+               // when adding or removing methods here.
+               var log = function () {},
+                       console = window.console;
+
+               /**
+                * @class mw.log
+                * @singleton
+                */
+
+               /**
+                * Write a message to the console's warning channel.
+                * Actions not supported by the browser console are silently ignored.
+                *
+                * @param {...string} msg Messages to output to console
+                */
+               log.warn = console && console.warn && Function.prototype.bind ?
+                       Function.prototype.bind.call( console.warn, console ) :
+                       $.noop;
+
+               /**
+                * Write a message to the console's error channel.
+                *
+                * Most browsers provide a stacktrace by default if the argument
+                * is a caught Error object.
+                *
+                * @since 1.26
+                * @param {Error|...string} msg Messages to output to console
+                */
+               log.error = console && console.error && Function.prototype.bind ?
+                       Function.prototype.bind.call( console.error, console ) :
+                       $.noop;
+
+               /**
+                * Create a property in a host object that, when accessed, will produce
+                * a deprecation warning in the console.
+                *
+                * @param {Object} obj Host object of deprecated property
+                * @param {string} key Name of property to create in `obj`
+                * @param {Mixed} val The value this property should return when accessed
+                * @param {string} [msg] Optional text to include in the deprecation message
+                */
+               log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
+                       obj[ key ] = val;
+               } : function ( obj, key, val, msg ) {
+                       msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
+                       var logged = new StringSet();
+                       function uniqueTrace() {
+                               var trace = new Error().stack;
+                               if ( logged.has( trace ) ) {
+                                       return false;
+                               }
+                               logged.add( trace );
+                               return true;
+                       }
+                       Object.defineProperty( obj, key, {
+                               configurable: true,
+                               enumerable: true,
+                               get: function () {
+                                       if ( uniqueTrace() ) {
+                                               mw.track( 'mw.deprecate', key );
+                                               mw.log.warn( msg );
+                                       }
+                                       return val;
+                               },
+                               set: function ( newVal ) {
+                                       if ( uniqueTrace() ) {
+                                               mw.track( 'mw.deprecate', key );
+                                               mw.log.warn( msg );
+                                       }
+                                       val = newVal;
+                               }
+                       } );
+
+               };
+
+               return log;
+       }() );
+
        /**
         * @class mw
         */
                },
 
                /**
-                * Dummy placeholder for {@link mw.log}
+                * No-op dummy placeholder for {@link mw.log} in debug mode.
                 *
                 * @method
                 */
-               log: ( function () {
-                       // Also update the restoration of methods in mediawiki.log.js
-                       // when adding or removing methods here.
-                       var log = function () {},
-                               console = window.console;
-
-                       /**
-                        * @class mw.log
-                        * @singleton
-                        */
-
-                       /**
-                        * Write a message to the console's warning channel.
-                        * Actions not supported by the browser console are silently ignored.
-                        *
-                        * @param {...string} msg Messages to output to console
-                        */
-                       log.warn = console && console.warn && Function.prototype.bind ?
-                               Function.prototype.bind.call( console.warn, console ) :
-                               $.noop;
-
-                       /**
-                        * Write a message to the console's error channel.
-                        *
-                        * Most browsers provide a stacktrace by default if the argument
-                        * is a caught Error object.
-                        *
-                        * @since 1.26
-                        * @param {Error|...string} msg Messages to output to console
-                        */
-                       log.error = console && console.error && Function.prototype.bind ?
-                               Function.prototype.bind.call( console.error, console ) :
-                               $.noop;
-
-                       /**
-                        * Create a property in a host object that, when accessed, will produce
-                        * a deprecation warning in the console with backtrace.
-                        *
-                        * @param {Object} obj Host object of deprecated property
-                        * @param {string} key Name of property to create in `obj`
-                        * @param {Mixed} val The value this property should return when accessed
-                        * @param {string} [msg] Optional text to include in the deprecation message
-                        */
-                       log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
-                               obj[ key ] = val;
-                       } : function ( obj, key, val, msg ) {
-                               /*globals Set */
-                               msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
-                               var logged, loggedIsSet, uniqueTrace;
-                               if ( window.Set ) {
-                                       logged = new Set();
-                                       loggedIsSet = true;
-                               } else {
-                                       logged = {};
-                                       loggedIsSet = false;
-                               }
-                               uniqueTrace = function () {
-                                       var trace = new Error().stack;
-                                       if ( loggedIsSet ) {
-                                               if ( logged.has( trace ) ) {
-                                                       return false;
-                                               }
-                                               logged.add( trace );
-                                               return true;
-                                       } else {
-                                               if ( logged.hasOwnProperty( trace ) ) {
-                                                       return false;
-                                               }
-                                               logged[ trace ] = 1;
-                                               return true;
-                                       }
-                               };
-                               Object.defineProperty( obj, key, {
-                                       configurable: true,
-                                       enumerable: true,
-                                       get: function () {
-                                               if ( uniqueTrace() ) {
-                                                       mw.track( 'mw.deprecate', key );
-                                                       mw.log.warn( msg );
-                                               }
-                                               return val;
-                                       },
-                                       set: function ( newVal ) {
-                                               if ( uniqueTrace() ) {
-                                                       mw.track( 'mw.deprecate', key );
-                                                       mw.log.warn( msg );
-                                               }
-                                               val = newVal;
-                                       }
-                               } );
-
-                       };
-
-                       return log;
-               }() ),
+               log: log,
 
                /**
                 * Client for ResourceLoader server end point.
                         *  dependencies, such that later modules depend on earlier modules. The array
                         *  contains the module names. If the array contains already some module names,
                         *  this function appends its result to the pre-existing array.
-                        * @param {Object} [unresolved] Hash used to track the current dependency
-                        *  chain; used to report loops in the dependency graph.
+                        * @param {StringSet} [unresolved] Used to track the current dependency
+                        *  chain, and to report loops in the dependency graph.
                         * @throws {Error} If any unregistered module or a dependency loop is encountered
                         */
                        function sortDependencies( module, resolved, unresolved ) {
                                }
                                // Create unresolved if not passed in
                                if ( !unresolved ) {
-                                       unresolved = {};
+                                       unresolved = new StringSet();
                                }
                                // Tracks down dependencies
                                deps = registry[ module ].dependencies;
                                for ( i = 0; i < deps.length; i++ ) {
                                        if ( $.inArray( deps[ i ], resolved ) === -1 ) {
-                                               if ( unresolved[ deps[ i ] ] ) {
+                                               if ( unresolved.has( deps[ i ] ) ) {
                                                        throw new Error( mw.format(
                                                                'Circular reference detected: $1 -> $2',
                                                                module,
                                                        ) );
                                                }
 
-                                               // Add to unresolved
-                                               unresolved[ module ] = true;
+                                               unresolved.add(  module );
                                                sortDependencies( deps[ i ], resolved, unresolved );
                                        }
                                }
                                                                markModuleReady();
                                                        }
                                                } catch ( e ) {
-                                                       // This needs to NOT use mw.log because these errors are common in production mode
-                                                       // and not in debug mode, such as when a symbol that should be global isn't exported
+                                                       // Use mw.track instead of mw.log because these errors are common in production mode
+                                                       // (e.g. undefined variable), and mw.log is only enabled in debug mode.
                                                        registry[ module ].state = 'error';
                                                        mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
                                                        handlePending( module );
                                }
                        }
 
-                       /**
-                        * Evaluate a batch of load.php responses retrieved from mw.loader.store.
-                        *
-                        * @private
-                        * @param {string[]} implementations Array containing pieces of JavaScript code in the
-                        *  form of calls to mw.loader#implement().
-                        * @param {Function} cb Callback in case of failure
-                        * @param {Error} cb.err
-                        */
-                       function batchEval( implementations, cb ) {
-                               if ( !implementations.length ) {
-                                       return;
-                               }
-                               mw.requestIdleCallback( function iterate( deadline ) {
-                                       while ( implementations[ 0 ] && deadline.timeRemaining() > 5 ) {
-                                               try {
-                                                       $.globalEval( implementations.shift() );
-                                               } catch ( err ) {
-                                                       cb( err );
-                                                       return;
-                                               }
-                                       }
-                                       if ( implementations[ 0 ] ) {
-                                               mw.requestIdleCallback( iterate );
-                                       }
-                               } );
-                       }
-
                        /* Public Members */
                        return {
                                /**
                                 * @protected
                                 */
                                work: function () {
-                                       var q, batch, implementations, sourceModules;
+                                       var q, batch, concatSource, origBatch;
 
                                        batch = [];
 
 
                                        mw.loader.store.init();
                                        if ( mw.loader.store.enabled ) {
-                                               implementations = [];
-                                               sourceModules = [];
+                                               concatSource = [];
+                                               origBatch = batch;
                                                batch = $.grep( batch, function ( module ) {
-                                                       var implementation = mw.loader.store.get( module );
-                                                       if ( implementation ) {
-                                                               implementations.push( implementation );
-                                                               sourceModules.push( module );
+                                                       var source = mw.loader.store.get( module );
+                                                       if ( source ) {
+                                                               concatSource.push( source );
                                                                return false;
                                                        }
                                                        return true;
                                                } );
-                                               batchEval( implementations, function ( err ) {
+                                               try {
+                                                       $.globalEval( concatSource.join( ';' ) );
+                                               } catch ( err ) {
                                                        // Not good, the cached mw.loader.implement calls failed! This should
                                                        // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
                                                        // Depending on how corrupt the string is, it is likely that some
                                                        // modules' implement() succeeded while the ones after the error will
                                                        // never run and leave their modules in the 'loading' state forever.
+
                                                        // Since this is an error not caused by an individual module but by
                                                        // something that infected the implement call itself, don't take any
                                                        // risks and clear everything in this cache.
                                                        mw.loader.store.clear();
+                                                       // Re-add the ones still pending back to the batch and let the server
+                                                       // repopulate these modules to the cache.
+                                                       // This means that at most one module will be useless (the one that had
+                                                       // the error) instead of all of them.
                                                        mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
-
-                                                       // Re-add the failed ones that are still pending back to the batch
-                                                       var failed = $.grep( sourceModules, function ( module ) {
+                                                       origBatch = $.grep( origBatch, function ( module ) {
                                                                return registry[ module ].state === 'loading';
                                                        } );
-                                                       batchRequest( failed );
-                                               } );
+                                                       batch = batch.concat( origBatch );
+                                               }
                                        }
 
                                        batchRequest( batch );
         * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
         *
         * @private
-        * @method log_
         * @param {string} topic Stream name passed by mw.track
         * @param {Object} data Data passed by mw.track
         * @param {Error} [data.exception]
         * @param {string} data.source Error source
         * @param {string} [data.module] Name of module which caused the error
         */
-       function log( topic, data ) {
+       function logError( topic, data ) {
                var msg,
                        e = data.exception,
                        source = data.source,
        }
 
        // Subscribe to error streams
-       mw.trackSubscribe( 'resourceloader.exception', log );
-       mw.trackSubscribe( 'resourceloader.assert', log );
+       mw.trackSubscribe( 'resourceloader.exception', logError );
+       mw.trackSubscribe( 'resourceloader.assert', logError );
 
        /**
         * Fired when all modules associated with the page have finished loading.
index 93fb470..c886817 100644 (file)
@@ -8,9 +8,8 @@
 
 ( function ( mw, $ ) {
 
-       // Reference to dummy
-       // We don't need the dummy, but it has other methods on it
-       // that we need to restore afterwards.
+       // Keep reference to the dummy placeholder from mediawiki.js
+       // The root is replaced below, but it has other methods that we need to restore.
        var original = mw.log,
                slice = Array.prototype.slice;
 
index 2c8b163..1e511f6 100644 (file)
@@ -20905,6 +20905,26 @@ Id starting with underscore
 
 !! end
 
+!! test
+Edit comment with link with more than one pipe (T99346)
+!! options
+comment
+!! wikitext
+[[Main Page|Many|pipes]]
+!! html
+<a href="/wiki/Main_Page" title="Main Page">Many|pipes</a>
+!! end
+
+!! test
+Complex edit comment with link with more than one pipe (T99346)
+!! options
+comment
+!! wikitext
+Created page with "<noinclude>[[Category:Requests for permissions/Bot|{{subst:#titleparts:{{subst:PAGENAME}}|1|3}}]]</noinclude> === [[User:MineoBot|]] 8=== {{Request for permissions/links|Mineo..."
+!! html
+Created page with &quot;&lt;noinclude&gt;<a href="/index.php?title=Category:Requests_for_permissions/Bot&amp;action=edit&amp;redlink=1" class="new" title="Category:Requests for permissions/Bot (page does not exist)">{{subst:#titleparts:{{subst:PAGENAME}}|1|3}}</a>&lt;/noinclude&gt; === <a href="/index.php?title=User:MineoBot&amp;action=edit&amp;redlink=1" class="new" title="User:MineoBot (page does not exist)">User:MineoBot</a> 8=== {{Request for permissions/links|Mineo...&quot;
+!! end
+
 !! test
 Space normalisation on autocomment (bug 22784)
 !! options
index 43577ca..c952229 100644 (file)
@@ -1178,19 +1178,23 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * Gets master database connections for all of the ExternalStoreDB
         * stores configured in $wgDefaultExternalStore.
         *
-        * @return array Array of DatabaseBase master connections
+        * @return DatabaseBase[] Array of DatabaseBase master connections
         */
 
        protected static function getExternalStoreDatabaseConnections() {
                global $wgDefaultExternalStore;
 
+               /** @var ExternalStoreDB $externalStoreDB */
                $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
                $defaultArray = (array) $wgDefaultExternalStore;
                $dbws = [];
                foreach ( $defaultArray as $url ) {
                        if ( strpos( $url, 'DB://' ) === 0 ) {
                                list( $proto, $cluster ) = explode( '://', $url, 2 );
-                               $dbw = $externalStoreDB->getMaster( $cluster );
+                               // Avoid getMaster() because setupDatabaseWithTestPrefix()
+                               // requires DatabaseBase instead of plain DBConnRef/IDatabase
+                               $lb = $externalStoreDB->getLoadBalancer( $cluster );
+                               $dbw = $lb->getConnection( DB_MASTER );
                                $dbws[] = $dbw;
                        }
                }
@@ -1309,14 +1313,17 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         *
         * @return array
         */
-       public static function listTables( $db ) {
+       public static function listTables( DatabaseBase $db ) {
                $prefix = $db->tablePrefix();
                $tables = $db->listTables( $prefix, __METHOD__ );
 
                if ( $db->getType() === 'mysql' ) {
-                       # bug 43571: cannot clone VIEWs under MySQL
-                       $views = $db->listViews( $prefix, __METHOD__ );
-                       $tables = array_diff( $tables, $views );
+                       static $viewListCache = null;
+                       if ( $viewListCache === null ) {
+                               $viewListCache = $db->listViews( null, __METHOD__ );
+                       }
+                       // T45571: cannot clone VIEWs under MySQL
+                       $tables = array_diff( $tables, $viewListCache );
                }
                array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
 
index 9480c2d..c014e84 100644 (file)
@@ -178,16 +178,12 @@ class DatabaseMysqlBaseTest extends MediaWikiTestCase {
 
                $db->method( 'query' )
                        ->with( $this->anything() )
-                       ->willReturn( null );
+                       ->willReturn( new FakeResultWrapper( [
+                               (object)[ 'Tables_in_' => 'view1' ],
+                               (object)[ 'Tables_in_' => 'view2' ],
+                               (object)[ 'Tables_in_' => 'myview' ]
+                       ] ) );
 
-               $db->method( 'fetchRow' )
-                       ->with( $this->anything() )
-                       ->will( $this->onConsecutiveCalls(
-                               [ 'Tables_in_' => 'view1' ],
-                               [ 'Tables_in_' => 'view2' ],
-                               [ 'Tables_in_' => 'myview' ],
-                               false  # no more rows
-                       ) );
                return $db;
        }
        /**
@@ -196,9 +192,6 @@ class DatabaseMysqlBaseTest extends MediaWikiTestCase {
        function testListviews() {
                $db = $this->getMockForViews();
 
-               // The first call populate an internal cache of views
-               $this->assertEquals( [ 'view1', 'view2', 'myview' ],
-                       $db->listViews() );
                $this->assertEquals( [ 'view1', 'view2', 'myview' ],
                        $db->listViews() );
 
@@ -213,42 +206,6 @@ class DatabaseMysqlBaseTest extends MediaWikiTestCase {
                        $db->listViews( '' ) );
        }
 
-       /**
-        * @covers DatabaseMysqlBase::isView
-        * @dataProvider provideViewExistanceChecks
-        */
-       function testIsView( $isView, $viewName ) {
-               $db = $this->getMockForViews();
-
-               switch ( $isView ) {
-                       case true:
-                               $this->assertTrue( $db->isView( $viewName ),
-                                       "$viewName should be considered a view" );
-                       break;
-
-                       case false:
-                               $this->assertFalse( $db->isView( $viewName ),
-                                       "$viewName has not been defined as a view" );
-                       break;
-               }
-
-       }
-
-       function provideViewExistanceChecks() {
-               return [
-                       // format: whether it is a view, view name
-                       [ true, 'view1' ],
-                       [ true, 'view2' ],
-                       [ true, 'myview' ],
-
-                       [ false, 'user' ],
-
-                       [ false, 'view10' ],
-                       [ false, 'my' ],
-                       [ false, 'OH_MY_GOD' ],  # they killed kenny!
-               ];
-       }
-
        /**
         * @dataProvider provideComparePositions
         */
diff --git a/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php b/tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php
new file mode 100644 (file)
index 0000000..9ec4f97
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+
+class HTMLRestrictionsFieldTest extends PHPUnit_Framework_TestCase {
+       public function testConstruct() {
+               $field = new HTMLRestrictionsField( [ 'fieldname' => 'restrictions' ] );
+               $this->assertNotEmpty( $field->getLabel(), 'has a default label' );
+               $this->assertNotEmpty( $field->getHelpText(), 'has a default help text' );
+               $this->assertEquals( MWRestrictions::newDefault(), $field->getDefault(),
+                       'defaults to the default MWRestrictions object' );
+
+               $field = new HTMLRestrictionsField( [
+                       'fieldname' => 'restrictions',
+                       'label' => 'foo',
+                       'help' => 'bar',
+                       'default' => 'baz',
+               ] );
+               $this->assertEquals( 'foo', $field->getLabel(), 'label can be customized' );
+               $this->assertEquals( 'bar', $field->getHelpText(), 'help text can be customized' );
+               $this->assertEquals( 'baz', $field->getDefault(), 'default can be customized' );
+       }
+
+       /**
+        * @dataProvider provideValidate
+        */
+       public function testForm( $text, $value ) {
+               $form = HTMLForm::factory( 'ooui', [
+                       'restrictions' => [ 'class' => HTMLRestrictionsField::class ],
+               ] );
+               $request = new FauxRequest( [ 'wprestrictions' => $text ], true );
+               $context = new DerivativeContext( RequestContext::getMain() );
+               $context->setRequest( $request );
+               $form->setContext( $context );
+               $form->setTitle( Title::newFromText( 'Main Page' ) )->setSubmitCallback( function () {
+                       return true;
+               } )->prepareForm();
+               $status = $form->trySubmit();
+
+               if ( $status instanceof StatusValue ) {
+                       $this->assertEquals( $value !== false, $status->isGood() );
+               } elseif ( $value === false ) {
+                       $this->assertNotSame( true, $status );
+               } else {
+                       $this->assertSame( true, $status );
+               }
+
+               if ( $value !== false ) {
+                       $restrictions = $form->mFieldData['restrictions'];
+                       $this->assertInstanceOf( MWRestrictions::class, $restrictions );
+                       $this->assertEquals( $value, $restrictions->toArray()['IPAddresses'] );
+               }
+
+               // sanity
+               $form->getHTML( $status );
+       }
+
+       public function provideValidate() {
+               return [
+                       // submitted text, value of 'IPAddresses' key or false for validation error
+                       [ null, [ '0.0.0.0/0', '::/0' ] ],
+                       [ '', [] ],
+                       [ "1.2.3.4\n::/0", [ '1.2.3.4', '::/0' ] ],
+                       [ "1.2.3.4\n::/x", false ],
+               ];
+       }
+}