Merge "Documentation link changes"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 7 Oct 2016 22:56:21 +0000 (22:56 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 7 Oct 2016 22:56:21 +0000 (22:56 +0000)
670 files changed:
.jscsrc
RELEASE-NOTES-1.28
autoload.php
composer.json
docs/extension.schema.json
docs/extension.schema.v1.json
docs/hooks.txt
includes/Block.php
includes/CategoryViewer.php
includes/DefaultSettings.php
includes/Defines.php
includes/EditPage.php
includes/FileDeleteForm.php
includes/GlobalFunctions.php
includes/Html.php
includes/HttpFunctions.php [deleted file]
includes/Linker.php
includes/MWTimestamp.php
includes/MediaWiki.php
includes/MediaWikiServices.php
includes/MergeHistory.php
includes/Message.php
includes/MimeMagic.php
includes/MovePage.php
includes/OutputPage.php
includes/PageProps.php
includes/PrefixSearch.php
includes/ProxyLookup.php [new file with mode: 0644]
includes/Revision.php
includes/Sanitizer.php
includes/ServiceWiring.php
includes/Services/CannotReplaceActiveServiceException.php [deleted file]
includes/Services/ContainerDisabledException.php [deleted file]
includes/Services/DestructibleService.php [deleted file]
includes/Services/NoSuchServiceException.php [deleted file]
includes/Services/SalvageableService.php [deleted file]
includes/Services/ServiceAlreadyDefinedException.php [deleted file]
includes/Services/ServiceContainer.php [deleted file]
includes/Services/ServiceDisabledException.php [deleted file]
includes/Setup.php
includes/Status.php
includes/StreamFile.php
includes/TemplateParser.php
includes/Title.php
includes/WatchedItemQueryService.php
includes/WatchedItemStore.php
includes/WebRequest.php
includes/WebResponse.php
includes/Xml.php
includes/actions/HistoryAction.php
includes/api/ApiAuthManagerHelper.php
includes/api/ApiBase.php
includes/api/ApiCSPReport.php
includes/api/ApiClearHasMsg.php
includes/api/ApiContinuationManager.php
includes/api/ApiDelete.php
includes/api/ApiFormatBase.php
includes/api/ApiFormatJson.php
includes/api/ApiHelp.php
includes/api/ApiHelpParamValueMessage.php
includes/api/ApiImageRotate.php
includes/api/ApiMain.php
includes/api/ApiPageSet.php
includes/api/ApiQuery.php
includes/api/ApiQueryAllImages.php
includes/api/ApiQueryAllRevisions.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryGeneratorBase.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQueryRevisions.php
includes/api/ApiQuerySearch.php
includes/api/ApiQueryUserContributions.php
includes/api/ApiResult.php
includes/api/SearchApi.php
includes/api/i18n/ast.json
includes/api/i18n/be-tarask.json
includes/api/i18n/bg.json
includes/api/i18n/bn.json
includes/api/i18n/diq.json
includes/api/i18n/en.json
includes/api/i18n/fr.json
includes/api/i18n/gl.json
includes/api/i18n/he.json
includes/api/i18n/hr.json [new file with mode: 0644]
includes/api/i18n/pl.json
includes/api/i18n/pt.json
includes/api/i18n/qqq.json
includes/api/i18n/zh-hans.json
includes/auth/AuthManager.php
includes/auth/AuthenticationResponse.php
includes/auth/ButtonAuthenticationRequest.php
includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php
includes/auth/LocalPasswordPrimaryAuthenticationProvider.php
includes/auth/PasswordDomainAuthenticationRequest.php
includes/auth/ResetPasswordSecondaryAuthenticationProvider.php
includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php
includes/auth/ThrottlePreAuthenticationProvider.php
includes/cache/BacklinkCache.php
includes/cache/FileCacheBase.php
includes/cache/HTMLFileCache.php
includes/cache/LinkBatch.php
includes/cache/MessageCache.php
includes/changes/ChangesFeed.php
includes/changes/RecentChange.php
includes/changetags/ChangeTags.php
includes/clientpool/RedisConnectionPool.php [deleted file]
includes/collation/IcuCollation.php
includes/compat/ScopedCallback.php [new file with mode: 0644]
includes/content/ContentHandler.php
includes/content/FileContentHandler.php [new file with mode: 0644]
includes/content/WikiTextStructure.php
includes/content/WikitextContentHandler.php
includes/context/ContextSource.php
includes/context/RequestContext.php
includes/dao/DBAccessBase.php
includes/db/CloneDatabase.php
includes/db/Database.php [deleted file]
includes/db/DatabaseMssql.php
includes/db/DatabaseMysql.php [deleted file]
includes/db/DatabaseMysqlBase.php [deleted file]
includes/db/DatabaseMysqli.php [deleted file]
includes/db/DatabaseOracle.php
includes/db/DatabasePostgres.php [deleted file]
includes/db/DatabaseSqlite.php [deleted file]
includes/db/MWLBFactory.php [new file with mode: 0644]
includes/db/loadbalancer/LBFactoryMW.php [deleted file]
includes/db/loadbalancer/LBFactoryMulti.php [deleted file]
includes/db/loadbalancer/LBFactorySimple.php [deleted file]
includes/db/loadbalancer/LBFactorySingle.php [deleted file]
includes/debug/logger/LegacyLogger.php
includes/deferred/DeferredUpdates.php
includes/deferred/LinksDeletionUpdate.php
includes/deferred/LinksUpdate.php
includes/diff/DifferenceEngine.php
includes/exception/MWException.php
includes/exception/MWExceptionHandler.php
includes/exception/MWExceptionRenderer.php
includes/externalstore/ExternalStoreDB.php
includes/filebackend/FSFile.php [deleted file]
includes/filebackend/FSFileBackend.php [deleted file]
includes/filebackend/FileBackend.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/TempFSFile.php [deleted file]
includes/filebackend/filejournal/DBFileJournal.php
includes/filebackend/filejournal/FileJournal.php [deleted file]
includes/filebackend/lockmanager/DBLockManager.php [deleted file]
includes/filebackend/lockmanager/FSLockManager.php [deleted file]
includes/filebackend/lockmanager/LockManager.php [deleted file]
includes/filebackend/lockmanager/LockManagerGroup.php
includes/filebackend/lockmanager/MemcLockManager.php [deleted file]
includes/filebackend/lockmanager/MySqlLockManager.php
includes/filebackend/lockmanager/PostgreSqlLockManager.php [deleted file]
includes/filebackend/lockmanager/QuorumLockManager.php [deleted file]
includes/filebackend/lockmanager/RedisLockManager.php [deleted file]
includes/filebackend/lockmanager/ScopedLock.php [deleted file]
includes/filerepo/FSRepo.php
includes/filerepo/FileBackendDBRepoWrapper.php
includes/filerepo/FileRepo.php
includes/filerepo/ForeignAPIRepo.php
includes/filerepo/ForeignDBRepo.php
includes/filerepo/ForeignDBViaLBRepo.php
includes/filerepo/LocalRepo.php
includes/filerepo/RepoGroup.php
includes/filerepo/file/ArchivedFile.php
includes/filerepo/file/File.php
includes/filerepo/file/ForeignAPIFile.php
includes/filerepo/file/ForeignDBFile.php
includes/filerepo/file/LocalFile.php
includes/filerepo/file/OldLocalFile.php
includes/gallery/TraditionalImageGallery.php
includes/htmlform/HTMLForm.php
includes/htmlform/HTMLFormField.php
includes/htmlform/OOUIHTMLForm.php
includes/htmlform/fields/HTMLDateTimeField.php [new file with mode: 0644]
includes/htmlform/fields/HTMLRestrictionsField.php [new file with mode: 0644]
includes/htmlform/fields/HTMLSubmitField.php
includes/http/CurlHttpRequest.php [new file with mode: 0644]
includes/http/Http.php [new file with mode: 0644]
includes/http/MWHttpRequest.php [new file with mode: 0644]
includes/http/PhpHttpRequest.php [new file with mode: 0644]
includes/import/WikiRevision.php
includes/installer/DatabaseInstaller.php
includes/installer/DatabaseUpdater.php
includes/installer/Installer.php
includes/installer/MssqlInstaller.php
includes/installer/MssqlUpdater.php
includes/installer/MysqlInstaller.php
includes/installer/MysqlUpdater.php
includes/installer/OracleInstaller.php
includes/installer/OracleUpdater.php
includes/installer/PostgresInstaller.php
includes/installer/PostgresUpdater.php
includes/installer/SqliteInstaller.php
includes/installer/SqliteUpdater.php
includes/installer/i18n/be-tarask.json
includes/installer/i18n/bg.json
includes/installer/i18n/bn.json
includes/installer/i18n/cs.json
includes/installer/i18n/de.json
includes/installer/i18n/diq.json
includes/installer/i18n/en.json
includes/installer/i18n/es.json
includes/installer/i18n/fr.json
includes/installer/i18n/gl.json
includes/installer/i18n/it.json
includes/installer/i18n/lb.json
includes/installer/i18n/lv.json
includes/installer/i18n/mai.json
includes/installer/i18n/mk.json
includes/installer/i18n/nb.json
includes/installer/i18n/ne.json
includes/installer/i18n/nl.json
includes/installer/i18n/pl.json
includes/installer/i18n/pt.json
includes/installer/i18n/qqq.json
includes/installer/i18n/ru.json
includes/installer/i18n/sd.json
includes/installer/i18n/sl.json
includes/installer/i18n/sv.json
includes/installer/i18n/zh-hans.json
includes/installer/i18n/zh-hant.json
includes/jobqueue/Job.php
includes/jobqueue/JobQueueRedis.php
includes/jobqueue/JobRunner.php
includes/jobqueue/aggregator/JobQueueAggregatorRedis.php
includes/libs/CryptRand.php [new file with mode: 0644]
includes/libs/IP.php [new file with mode: 0644]
includes/libs/MWCryptHash.php [new file with mode: 0644]
includes/libs/MemoizedCallable.php
includes/libs/MultiHttpClient.php
includes/libs/SamplingStatsdClient.php [deleted file]
includes/libs/ScopedCallback.php [deleted file]
includes/libs/StatusValue.php
includes/libs/WaitConditionLoop.php [deleted file]
includes/libs/filebackend/FSFile.php [new file with mode: 0644]
includes/libs/filebackend/FSFileBackend.php [new file with mode: 0644]
includes/libs/filebackend/FileBackend.php [new file with mode: 0644]
includes/libs/filebackend/FileBackendError.php [new file with mode: 0644]
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/TempFSFile.php [new file with mode: 0644]
includes/libs/filebackend/filejournal/FileJournal.php [new file with mode: 0644]
includes/libs/filebackend/filejournal/NullFileJournal.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/FSLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/LockManager.php [new file with mode: 0644]
includes/libs/lockmanager/MemcLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/NullLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/PostgreSqlLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/QuorumLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/RedisLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/ScopedLock.php [new file with mode: 0644]
includes/libs/objectcache/APCBagOStuff.php
includes/libs/objectcache/APCUBagOStuff.php [new file with mode: 0644]
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/HashBagOStuff.php
includes/libs/objectcache/MemcachedBagOStuff.php
includes/libs/objectcache/RedisBagOStuff.php [new file with mode: 0644]
includes/libs/objectcache/WANObjectCache.php
includes/libs/rdbms/ChronologyProtector.php [new file with mode: 0644]
includes/libs/rdbms/TransactionProfiler.php
includes/libs/rdbms/chronologyprotector/ChronologyProtector.php [deleted file]
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php [new file with mode: 0644]
includes/libs/rdbms/database/DatabaseDomain.php [new file with mode: 0644]
includes/libs/rdbms/database/DatabaseMysql.php [new file with mode: 0644]
includes/libs/rdbms/database/DatabaseMysqlBase.php [new file with mode: 0644]
includes/libs/rdbms/database/DatabaseMysqli.php [new file with mode: 0644]
includes/libs/rdbms/database/DatabasePostgres.php [new file with mode: 0644]
includes/libs/rdbms/database/DatabaseSqlite.php [new file with mode: 0644]
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/database/IMaintainableDatabase.php [new file with mode: 0644]
includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php
includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php
includes/libs/rdbms/database/resultwrapper/ResultWrapper.php
includes/libs/rdbms/database/utils/SavepointPostgres.php [new file with mode: 0644]
includes/libs/rdbms/defines.php
includes/libs/rdbms/encasing/LikeMatch.php
includes/libs/rdbms/exception/DBAccessError.php [new file with mode: 0644]
includes/libs/rdbms/exception/DBConnectionError.php [new file with mode: 0644]
includes/libs/rdbms/exception/DBError.php
includes/libs/rdbms/exception/DBExpectedError.php [new file with mode: 0644]
includes/libs/rdbms/exception/DBQueryError.php [new file with mode: 0644]
includes/libs/rdbms/exception/DBReadOnlyError.php [new file with mode: 0644]
includes/libs/rdbms/exception/DBReplicationWaitError.php [new file with mode: 0644]
includes/libs/rdbms/exception/DBTransactionError.php [new file with mode: 0644]
includes/libs/rdbms/exception/DBTransactionSizeError.php [new file with mode: 0644]
includes/libs/rdbms/exception/DBUnexpectedError.php [new file with mode: 0644]
includes/libs/rdbms/field/PostgresField.php [new file with mode: 0644]
includes/libs/rdbms/lbfactory/ILBFactory.php [new file with mode: 0644]
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/lbfactory/LBFactoryMulti.php [new file with mode: 0644]
includes/libs/rdbms/lbfactory/LBFactorySimple.php [new file with mode: 0644]
includes/libs/rdbms/lbfactory/LBFactorySingle.php [new file with mode: 0644]
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php [new file with mode: 0644]
includes/libs/rdbms/loadmonitor/ILoadMonitor.php
includes/libs/rdbms/loadmonitor/LoadMonitor.php
includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php
includes/libs/rdbms/loadmonitor/LoadMonitorNull.php
includes/libs/redis/RedisConnRef.php [new file with mode: 0644]
includes/libs/redis/RedisConnectionPool.php [new file with mode: 0644]
includes/libs/stats/SamplingStatsdClient.php [new file with mode: 0644]
includes/libs/time/ConvertableTimestamp.php [deleted file]
includes/libs/time/ConvertibleTimestamp.php [new file with mode: 0644]
includes/libs/virtualrest/VirtualRESTServiceClient.php
includes/libs/xmp/XMP.php [new file with mode: 0644]
includes/libs/xmp/XMPInfo.php [new file with mode: 0644]
includes/libs/xmp/XMPValidate.php [new file with mode: 0644]
includes/logging/LogEntry.php
includes/logging/LogEventsList.php
includes/media/BMP.php
includes/media/Bitmap.php
includes/media/DjVu.php
includes/media/ExifBitmap.php
includes/media/JpegMetadataExtractor.php
includes/media/MediaHandler.php
includes/media/PNG.php
includes/media/SVG.php
includes/media/Tiff.php
includes/media/TransformationalImageHandler.php
includes/media/WebP.php
includes/media/XCF.php
includes/media/XMP.php [deleted file]
includes/media/XMPInfo.php [deleted file]
includes/media/XMPValidate.php [deleted file]
includes/objectcache/ObjectCache.php
includes/objectcache/RedisBagOStuff.php [deleted file]
includes/objectcache/SqlBagOStuff.php
includes/page/Article.php
includes/page/WikiPage.php
includes/parser/Parser.php
includes/poolcounter/PoolCounterRedis.php
includes/profiler/Profiler.php
includes/profiler/SectionProfiler.php
includes/registration/ExtensionProcessor.php
includes/registration/ExtensionRegistry.php
includes/resourceloader/ResourceLoaderContext.php
includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/search/SearchEngine.php
includes/search/SearchIndexField.php
includes/services/CannotReplaceActiveServiceException.php [new file with mode: 0644]
includes/services/ContainerDisabledException.php [new file with mode: 0644]
includes/services/DestructibleService.php [new file with mode: 0644]
includes/services/NoSuchServiceException.php [new file with mode: 0644]
includes/services/SalvageableService.php [new file with mode: 0644]
includes/services/ServiceAlreadyDefinedException.php [new file with mode: 0644]
includes/services/ServiceContainer.php [new file with mode: 0644]
includes/services/ServiceDisabledException.php [new file with mode: 0644]
includes/site/DBSiteStore.php
includes/skins/BaseTemplate.php
includes/skins/SkinTemplate.php
includes/specialpage/AuthManagerSpecialPage.php
includes/specialpage/LoginSignupSpecialPage.php
includes/specials/SpecialBotPasswords.php
includes/specials/SpecialChangeContentModel.php
includes/specials/SpecialContributions.php
includes/specials/SpecialEditWatchlist.php
includes/specials/SpecialImport.php
includes/specials/SpecialMIMEsearch.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialNewpages.php [changed mode: 0644->0755]
includes/specials/SpecialPreferences.php
includes/specials/SpecialRandomInCategory.php
includes/specials/SpecialRecentchanges.php
includes/specials/SpecialUserrights.php
includes/specials/SpecialVersion.php
includes/tidy/Balancer.php
includes/upload/UploadBase.php
includes/upload/UploadFromChunks.php
includes/upload/UploadFromUrl.php
includes/upload/UploadStash.php
includes/user/BotPassword.php
includes/user/User.php
includes/utils/AutoloadGenerator.php
includes/utils/IP.php [deleted file]
includes/utils/MWCryptHKDF.php
includes/utils/MWCryptHash.php [deleted file]
includes/utils/MWCryptRand.php
includes/utils/MWFileProps.php [new file with mode: 0644]
includes/utils/MWRestrictions.php
includes/utils/UIDGenerator.php
includes/widget/AUTHORS.txt
includes/widget/DateTimeInputWidget.php [new file with mode: 0644]
languages/Language.php
languages/LanguageConverter.php
languages/classes/LanguageKm.php
languages/classes/LanguageMy.php
languages/i18n/ace.json
languages/i18n/an.json
languages/i18n/ar.json
languages/i18n/as.json
languages/i18n/ast.json
languages/i18n/az.json
languages/i18n/ba.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bg.json
languages/i18n/bn.json
languages/i18n/br.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/et.json
languages/i18n/eu.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gl.json
languages/i18n/gor.json
languages/i18n/got.json
languages/i18n/gsw.json
languages/i18n/gu.json
languages/i18n/he.json
languages/i18n/hi.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/hy.json
languages/i18n/ia.json
languages/i18n/id.json
languages/i18n/ilo.json
languages/i18n/is.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/jv.json
languages/i18n/ka.json
languages/i18n/kiu.json
languages/i18n/kk-cyrl.json
languages/i18n/kn.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/li.json
languages/i18n/lij.json
languages/i18n/lt.json
languages/i18n/lv.json
languages/i18n/lzz.json
languages/i18n/mai.json
languages/i18n/mk.json
languages/i18n/mr.json
languages/i18n/my.json
languages/i18n/nah.json
languages/i18n/nap.json
languages/i18n/nb.json
languages/i18n/ne.json
languages/i18n/nl.json
languages/i18n/nn.json
languages/i18n/olo.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/rm.json
languages/i18n/ro.json
languages/i18n/ru.json
languages/i18n/sa.json
languages/i18n/sah.json
languages/i18n/sd.json
languages/i18n/shn.json
languages/i18n/sk.json
languages/i18n/sl.json
languages/i18n/so.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/sv.json
languages/i18n/ta.json
languages/i18n/tcy.json
languages/i18n/te.json
languages/i18n/tl.json
languages/i18n/tr.json
languages/i18n/tt-cyrl.json
languages/i18n/uk.json
languages/i18n/ur.json
languages/i18n/uz.json
languages/i18n/vec.json
languages/i18n/vi.json
languages/i18n/yi.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesEn.php
languages/messages/MessagesOlo.php
load.php
maintenance/Maintenance.php
maintenance/addRFCandPMIDInterwiki.php [new file with mode: 0644]
maintenance/archives/patch-change_tag-ct_id.sql [new file with mode: 0644]
maintenance/archives/patch-tag_summary-ts_id.sql [new file with mode: 0644]
maintenance/archives/upgradeLogging.php
maintenance/benchmarks/bench_delete_truncate.php
maintenance/cleanupEmptyCategories.php
maintenance/convertUserOptions.php
maintenance/deleteOrphanedRevisions.php
maintenance/dumpIterator.php
maintenance/dumpTextPass.php
maintenance/fetchText.php
maintenance/importImages.php
maintenance/importTextFiles.php
maintenance/initEditCount.php
maintenance/interwiki.list
maintenance/interwiki.sql
maintenance/jsduck/custom_tags.rb
maintenance/jsduck/external.js
maintenance/lag.php
maintenance/language/checkDupeMessages.php
maintenance/language/digit2html.php
maintenance/migrateFileRepoLayout.php
maintenance/mssql/archives/patch-change_tag-ct_id.sql [new file with mode: 0644]
maintenance/mssql/archives/patch-tag_summary-ts_id.sql [new file with mode: 0644]
maintenance/mssql/tables.sql
maintenance/namespaceDupes.php
maintenance/oracle/archives/patch-change_tag-ct_id.sql [new file with mode: 0644]
maintenance/oracle/archives/patch-tag_summary-ts_id.sql [new file with mode: 0644]
maintenance/oracle/tables.sql
maintenance/orphans.php
maintenance/patchSql.php
maintenance/populateContentModel.php
maintenance/populateRecentChangesSource.php
maintenance/postgres/tables.sql
maintenance/preprocessorFuzzTest.php
maintenance/rebuildFileCache.php
maintenance/rebuildImages.php
maintenance/rebuildrecentchanges.php
maintenance/rebuildtextindex.php
maintenance/refreshImageMetadata.php
maintenance/refreshLinks.php
maintenance/sql.php
maintenance/sqlite/archives/patch-change_tag-ct_id.sql [new file with mode: 0644]
maintenance/sqlite/archives/patch-tag_summary-ts_id.sql [new file with mode: 0644]
maintenance/storage/compressOld.php
maintenance/storage/recompressTracked.php
maintenance/tables.sql
maintenance/updateCollation.php
maintenance/validateRegistrationFile.php
resources/Resources.php
resources/lib/oojs-ui/oojs-ui-apex.js
resources/lib/oojs-ui/oojs-ui-core-apex.css
resources/lib/oojs-ui/oojs-ui-core-mediawiki.css
resources/lib/oojs-ui/oojs-ui-core.js
resources/lib/oojs-ui/oojs-ui-mediawiki.js
resources/lib/oojs-ui/oojs-ui-toolbars-apex.css
resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css
resources/lib/oojs-ui/oojs-ui-toolbars.js
resources/lib/oojs-ui/oojs-ui-widgets-apex.css
resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css
resources/lib/oojs-ui/oojs-ui-widgets.js
resources/lib/oojs-ui/oojs-ui-windows-apex.css
resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css
resources/lib/oojs-ui/oojs-ui-windows.js
resources/src/mediawiki.legacy/images/help-question-hover.gif [deleted file]
resources/src/mediawiki.legacy/images/help-question.gif [deleted file]
resources/src/mediawiki.legacy/shared.css
resources/src/mediawiki.less/mediawiki.ui/mixins.less
resources/src/mediawiki.less/mediawiki.ui/variables.less
resources/src/mediawiki.special/mediawiki.special.apisandbox.js
resources/src/mediawiki.special/mediawiki.special.search.styles.css
resources/src/mediawiki.special/mediawiki.special.userlogin.login.css
resources/src/mediawiki.ui/components/buttons.less
resources/src/mediawiki.ui/components/forms.less
resources/src/mediawiki.ui/components/inputs.less
resources/src/mediawiki.ui/components/text.less
resources/src/mediawiki.widgets.datetime/DateTimeInputWidget.js
resources/src/mediawiki.widgets.datetime/mediawiki.widgets.datetime.definitions.less
resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.less
resources/src/mediawiki.widgets/mw.widgets.CategorySelector.js
resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.less
resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js
resources/src/mediawiki/api/upload.js
resources/src/mediawiki/htmlform/datetime.js [new file with mode: 0644]
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js
resources/src/mediawiki/mediawiki.Upload.BookletLayout.css
resources/src/mediawiki/mediawiki.Upload.Dialog.js
resources/src/mediawiki/mediawiki.feedback.js
resources/src/mediawiki/mediawiki.js
resources/src/mediawiki/mediawiki.log.js
resources/src/mediawiki/mediawiki.requestIdleCallback.js
resources/src/mediawiki/mediawiki.util.js
tests/common/TestsAutoLoader.php
tests/parser/ParserTestRunner.php
tests/parser/TestFileReader.php
tests/parser/parserTests.txt
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/GlobalFunctions/wfBCP47Test.php
tests/phpunit/includes/HtmlTest.php
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/PrefixSearchTest.php
tests/phpunit/includes/SanitizerTest.php
tests/phpunit/includes/StatusTest.php
tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php
tests/phpunit/includes/WatchedItemStoreUnitTest.php
tests/phpunit/includes/WebRequestTest.php
tests/phpunit/includes/api/ApiMainTest.php
tests/phpunit/includes/api/ApiOpenSearchTest.php
tests/phpunit/includes/api/ApiQueryWatchlistRawIntegrationTest.php
tests/phpunit/includes/api/ApiResultTest.php
tests/phpunit/includes/api/RandomImageGenerator.php
tests/phpunit/includes/auth/AuthManagerTest.php
tests/phpunit/includes/auth/AuthenticationResponseTest.php
tests/phpunit/includes/auth/TemporaryPasswordPrimaryAuthenticationProviderTest.php
tests/phpunit/includes/auth/ThrottlePreAuthenticationProviderTest.php
tests/phpunit/includes/content/FileContentHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/content/WikitextContentHandlerTest.php
tests/phpunit/includes/db/DatabaseMysqlBaseTest.php
tests/phpunit/includes/db/DatabaseSQLTest.php
tests/phpunit/includes/db/DatabaseSqliteTest.php
tests/phpunit/includes/db/DatabaseTest.php
tests/phpunit/includes/db/DatabaseTestHelper.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/filebackend/FileBackendTest.php
tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php
tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php [new file with mode: 0644]
tests/phpunit/includes/installer/DatabaseUpdaterTest.php
tests/phpunit/includes/libs/IPTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/MemoizedCallableTest.php
tests/phpunit/includes/libs/WaitConditionLoopTest.php [deleted file]
tests/phpunit/includes/libs/composer/ComposerJsonTest.php
tests/phpunit/includes/libs/composer/ComposerLockTest.php
tests/phpunit/includes/libs/objectcache/BagOStuffTest.php
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/time/ConvertableTimestampTest.php [deleted file]
tests/phpunit/includes/libs/time/ConvertibleTimestampTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/xmp/XMPTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/xmp/XMPValidateTest.php [new file with mode: 0644]
tests/phpunit/includes/media/MediaWikiMediaTestCase.php
tests/phpunit/includes/media/XMPTest.php [deleted file]
tests/phpunit/includes/media/XMPValidateTest.php [deleted file]
tests/phpunit/includes/page/WikiPageTest.php
tests/phpunit/includes/session/TestUtils.php
tests/phpunit/includes/utils/BatchRowUpdateTest.php
tests/phpunit/includes/utils/IPTest.php [deleted file]
tests/phpunit/includes/utils/MWCryptHKDFTest.php
tests/phpunit/mocks/filebackend/MockFSFile.php
tests/phpunit/mocks/filerepo/MockLocalRepo.php [new file with mode: 0644]
tests/phpunit/mocks/media/MockDjVuHandler.php
tests/phpunit/structure/ExtensionJsonValidationTest.php
tests/qunit/data/defineCallMwLoaderTestCallback.js
tests/qunit/data/requireCallMwLoaderTestCallback.js
tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js

diff --git a/.jscsrc b/.jscsrc
index f3db218..3f7e90d 100644 (file)
--- a/.jscsrc
+++ b/.jscsrc
@@ -10,7 +10,6 @@
                        "preset": "jsduck5",
                        "extra": {
                                "context": "some",
-                               "source": "some",
                                "see": "some"
                        }
                },
index c3a91c4..a31fa49 100644 (file)
@@ -6,6 +6,7 @@ MediaWiki 1.28 is an alpha-quality branch and is not recommended for use in
 production.
 
 === Configuration changes in 1.28 ===
+* $wgSend404Code now affects status code of action=history if the page is not there.
 * BREAKING CHANGE: $wgHTTPProxy is now *required* for all external requests
   made by MediaWiki via a proxy. Relying on the http_proxy environment
   variable is no longer supported.
@@ -26,7 +27,7 @@ production.
   https://www.mediawiki.org/beacon with basic information about the local
   MediaWiki installation. This data includes, for example, the type of system,
   PHP version, and chosen database backend. This behavior is off by default.
-* When $EditSubmitButtonLabelPublish is true, MediaWiki will label the button
+* When $wgEditSubmitButtonLabelPublish is true, MediaWiki will label the button
   to store-to-database-and-show-to-others as "Publish page"/"Publish changes";
   if false, the default, they will be "Save page"/"Save changes".
 * The 'editcontentmodel' permission is now granted to all logged-in users ('user').
@@ -60,17 +61,25 @@ production.
   on the wiki farm with a different domain, MediaWiki will instead alter the redirect
   URL to include a ?cpPosTime parameter that triggers the database synchronization when
   the URL is followed by the client. The same-domain case uses a new cpPosTime cookie.
+* Added new hooks, 'ApiQueryBaseBeforeQuery', 'ApiQueryBaseAfterQuery', and
+  'ApiQueryBaseProcessRow', to make it easier for extensions to add 'prop' and
+  'show' parameters to existing API query modules.
 
 === External library changes in 1.28 ===
 
 ==== Upgraded external libraries ====
 * Updated es5-shim from v4.1.5 to v4.5.8
+* Updated composer/semver from v1.4.1 to v1.4.2
+* Updated wikimedia/php-session-serializer from v1.0.3 to v1.0.4
 
 ==== New external libraries ====
+* Added wikimedia/scoped-callback v1.0.0
+* Added wikimedia/wait-condition-loop v1.0.1
 
 ==== Removed and replaced external libraries ====
 
 === Bug fixes in 1.28 ===
+* (T146496) action=history pages should return 404 HTTP error code if the page does not exist
 * (T137264) SECURITY: XSS in unclosed internal links
 * (T133147) SECURITY: Escape '<' and ']]>' in inline <style> blocks
 * (T133147) SECURITY: Require login to preview user CSS pages
@@ -108,12 +117,51 @@ production.
   indicated by a 'fromencoded' boolean alongside the existing 'from' parameter.
 * (T28680) action=paraminfo can now return info about all submodules of a
   module without listing them all explicitly.
+* (T146770) It is now possible to assert that the current user is a specific
+  named user, using the 'assertuser' parameter.
 
 === Action API internal changes in 1.28 ===
 * Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
   interact with ApiParse and ApiExpandTemplates.
 * (T139565) SECURITY: API: Generate head items in the context of the given title
 * (T115333) SECURITY: Check read permission when loading page content in ApiParse
+* ApiBase::getResultData() was removed (deprecated since 1.25)
+* ApiBase::makeHelpArrayToString() was removed (deprecated since 1.25)
+* ApiBase::makeHelpMsgParameters() was removed (deprecated since 1.25)
+* ApiBase::makeHelpMsg() was removed (deprecated since 1.25)
+* ApiFormatBase::formatHTML() was removed (deprecated since 1.25)
+* ApiFormatBase::getNeedsRawData() was removed (deprecated since 1.25)
+* ApiFormatBase::getWantsHelp() was removed (deprecated since 1.25)
+* ApiFormatBase::setBufferResult() was removed (deprecated since 1.25)
+* ApiFormatBase::setHelp() was removed (deprecated since 1.25)
+* ApiFormatBase::setUnescapeAmps() was removed (deprecated since 1.25)
+* ApiMain::makeHelpMsgHeader() was removed (deprecated since 1.25)
+* ApiMain::reallyMakeHelpMsg() was removed (deprecated since 1.25)
+* ApiMain::setHelp() was removed (deprecated since 1.25)
+* ApiResult::beginContinuation() was removed (deprecated since 1.25)
+* ApiResult::cleanUpUTF8() was removed (deprecated since 1.25)
+* ApiResult::convertStatusToArray() was removed (deprecated since 1.25)
+* ApiResult::disableSizeCheck() was removed (deprecated since 1.24)
+* ApiResult::enableSizeCheck() was removed (deprecated since 1.24)
+* ApiResult::endContinuation() was removed (deprecated since 1.25)
+* ApiResult::getData() was removed (deprecated since 1.25)
+* ApiResult::getIsRawMode() was removed (deprecated since 1.25)
+* ApiResult::setContent() was removed (deprecated since 1.25)
+* ApiResult::setContinueParam() was removed (deprecated since 1.25)
+* ApiResult::setElement() was removed (deprecated since 1.25)
+* ApiResult::setGeneratorContinueParam() was removed (deprecated since 1.25)
+* ApiResult::setIndexedTagName_internal() was removed (deprecated since 1.25)
+* ApiResult::setIndexedTagName_recursive() was removed (deprecated since 1.25)
+* ApiResult::setMainForContinuation() was removed (deprecated since 1.25)
+* ApiResult::setParsedLimit() was removed (deprecated since 1.25)
+* ApiResult::setRawMode() was removed (deprecated since 1.25)
+* ApiResult::size() was removed (deprecated since 1.25)
+* Added new hooks, 'ApiQueryBaseBeforeQuery', 'ApiQueryBaseAfterQuery', and
+  'ApiQueryBaseProcessRow', to make it easier for extensions to add 'prop' and
+  'show' parameters to existing API query modules. A query module can enable
+  these hooks by passing an array for $hookData to ApiQueryBase::select() and
+  by calling ApiQueryBase->processRow() before adding a row's data to the
+  result.
 
 === Languages updated in 1.28 ===
 
@@ -172,6 +220,12 @@ changes to languages because of Phabricator reports.
   Instead of --keep-uploads, use the same option to parserTests.php, but you
   must specify a directory with --upload-dir.
 * The 'jquery.arrowSteps' ResourceLoader module is now deprecated.
+* IP::isConfiguredProxy() and IP::isTrustedProxy() were removed. Callers should
+  migrate to using the same functions on a ProxyLookup instance, obtainable from
+  MediaWikiServices.
+* The ArticleAfterFetchContent, ArticleInsertComplete, ArticleSave, ArticleSaveComplete,
+  ArticleViewCustom, EditPageGetDiffText, EditPageGetPreviewText and ShowRawCssJs hooks
+  will now emit deprecation warnings if used.
 
 == Compatibility ==
 
index 5cd1b91..748d954 100644 (file)
@@ -5,10 +5,12 @@ global $wgAutoloadLocalClasses;
 
 $wgAutoloadLocalClasses = [
        'APCBagOStuff' => __DIR__ . '/includes/libs/objectcache/APCBagOStuff.php',
+       'APCUBagOStuff' => __DIR__ . '/includes/libs/objectcache/APCUBagOStuff.php',
        'AbstractContent' => __DIR__ . '/includes/content/AbstractContent.php',
        'Action' => __DIR__ . '/includes/actions/Action.php',
        'ActiveUsersPager' => __DIR__ . '/includes/specials/pagers/ActiveUsersPager.php',
        'ActivityUpdateJob' => __DIR__ . '/includes/jobqueue/jobs/ActivityUpdateJob.php',
+       'AddRFCAndPMIDInterwiki' => __DIR__ . '/maintenance/addRFCandPMIDInterwiki.php',
        'AjaxDispatcher' => __DIR__ . '/includes/AjaxDispatcher.php',
        'AjaxResponse' => __DIR__ . '/includes/AjaxResponse.php',
        'AllMessagesTablePager' => __DIR__ . '/includes/specials/pagers/AllMessagesTablePager.php',
@@ -242,7 +244,7 @@ $wgAutoloadLocalClasses = [
        'CheckStorage' => __DIR__ . '/maintenance/storage/checkStorage.php',
        'CheckSyntax' => __DIR__ . '/maintenance/checkSyntax.php',
        'CheckUsernames' => __DIR__ . '/maintenance/checkUsernames.php',
-       'ChronologyProtector' => __DIR__ . '/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php',
+       'ChronologyProtector' => __DIR__ . '/includes/libs/rdbms/ChronologyProtector.php',
        'ClassCollector' => __DIR__ . '/includes/utils/AutoloadGenerator.php',
        'CleanupAncientTables' => __DIR__ . '/maintenance/cleanupAncientTables.php',
        'CleanupBlocks' => __DIR__ . '/maintenance/cleanupBlocks.php',
@@ -281,53 +283,55 @@ $wgAutoloadLocalClasses = [
        'ConvertExtensionToRegistration' => __DIR__ . '/maintenance/convertExtensionToRegistration.php',
        'ConvertLinks' => __DIR__ . '/maintenance/convertLinks.php',
        'ConvertUserOptions' => __DIR__ . '/maintenance/convertUserOptions.php',
-       'ConvertableTimestamp' => __DIR__ . '/includes/libs/time/ConvertableTimestamp.php',
        'ConverterRule' => __DIR__ . '/languages/ConverterRule.php',
+       'ConvertibleTimestamp' => __DIR__ . '/includes/libs/time/ConvertibleTimestamp.php',
        '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',
+       'CryptRand' => __DIR__ . '/includes/libs/CryptRand.php',
        'CssContent' => __DIR__ . '/includes/content/CssContent.php',
        'CssContentHandler' => __DIR__ . '/includes/content/CssContentHandler.php',
        'CsvStatsOutput' => __DIR__ . '/maintenance/language/StatOutputs.php',
-       'CurlHttpRequest' => __DIR__ . '/includes/HttpFunctions.php',
+       'CurlHttpRequest' => __DIR__ . '/includes/http/CurlHttpRequest.php',
        'DBAccessBase' => __DIR__ . '/includes/dao/DBAccessBase.php',
-       'DBAccessError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
+       'DBAccessError' => __DIR__ . '/includes/libs/rdbms/exception/DBAccessError.php',
        'DBAccessObjectUtils' => __DIR__ . '/includes/dao/DBAccessObjectUtils.php',
        'DBConnRef' => __DIR__ . '/includes/libs/rdbms/database/DBConnRef.php',
-       'DBConnectionError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
+       'DBConnectionError' => __DIR__ . '/includes/libs/rdbms/exception/DBConnectionError.php',
        'DBError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
-       'DBExpectedError' => __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/DBError.php',
-       'DBReadOnlyError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
-       'DBReplicationWaitError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
+       'DBQueryError' => __DIR__ . '/includes/libs/rdbms/exception/DBQueryError.php',
+       'DBReadOnlyError' => __DIR__ . '/includes/libs/rdbms/exception/DBReadOnlyError.php',
+       'DBReplicationWaitError' => __DIR__ . '/includes/libs/rdbms/exception/DBReplicationWaitError.php',
        'DBSiteStore' => __DIR__ . '/includes/site/DBSiteStore.php',
-       'DBTransactionError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
-       'DBTransactionSizeError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
-       'DBUnexpectedError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
+       'DBTransactionError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionError.php',
+       'DBTransactionSizeError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionSizeError.php',
+       'DBUnexpectedError' => __DIR__ . '/includes/libs/rdbms/exception/DBUnexpectedError.php',
        'DataUpdate' => __DIR__ . '/includes/deferred/DataUpdate.php',
-       'Database' => __DIR__ . '/includes/db/Database.php',
-       'DatabaseBase' => __DIR__ . '/includes/db/Database.php',
+       'Database' => __DIR__ . '/includes/libs/rdbms/database/Database.php',
+       'DatabaseBase' => __DIR__ . '/includes/libs/rdbms/database/Database.php',
+       'DatabaseDomain' => __DIR__ . '/includes/libs/rdbms/database/DatabaseDomain.php',
        'DatabaseInstaller' => __DIR__ . '/includes/installer/DatabaseInstaller.php',
        'DatabaseLag' => __DIR__ . '/maintenance/lag.php',
        'DatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
        'DatabaseMssql' => __DIR__ . '/includes/db/DatabaseMssql.php',
-       'DatabaseMysql' => __DIR__ . '/includes/db/DatabaseMysql.php',
-       'DatabaseMysqlBase' => __DIR__ . '/includes/db/DatabaseMysqlBase.php',
-       'DatabaseMysqli' => __DIR__ . '/includes/db/DatabaseMysqli.php',
+       'DatabaseMysql' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMysql.php',
+       'DatabaseMysqlBase' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMysqlBase.php',
+       'DatabaseMysqli' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMysqli.php',
        'DatabaseOracle' => __DIR__ . '/includes/db/DatabaseOracle.php',
-       'DatabasePostgres' => __DIR__ . '/includes/db/DatabasePostgres.php',
-       'DatabaseSqlite' => __DIR__ . '/includes/db/DatabaseSqlite.php',
+       'DatabasePostgres' => __DIR__ . '/includes/libs/rdbms/database/DatabasePostgres.php',
+       'DatabaseSqlite' => __DIR__ . '/includes/libs/rdbms/database/DatabaseSqlite.php',
        'DatabaseUpdater' => __DIR__ . '/includes/installer/DatabaseUpdater.php',
        'DateFormats' => __DIR__ . '/maintenance/language/date-formats.php',
        'DateFormatter' => __DIR__ . '/includes/parser/DateFormatter.php',
@@ -342,7 +346,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',
@@ -357,7 +361,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',
@@ -431,13 +435,13 @@ $wgAutoloadLocalClasses = [
        'ExternalStoreHttp' => __DIR__ . '/includes/externalstore/ExternalStoreHttp.php',
        'ExternalStoreMedium' => __DIR__ . '/includes/externalstore/ExternalStoreMedium.php',
        'ExternalStoreMwstore' => __DIR__ . '/includes/externalstore/ExternalStoreMwstore.php',
-       'FSFile' => __DIR__ . '/includes/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',
-       'FSLockManager' => __DIR__ . '/includes/filebackend/lockmanager/FSLockManager.php',
+       'FSFile' => __DIR__ . '/includes/libs/filebackend/FSFile.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',
        'FakeConverter' => __DIR__ . '/languages/FakeConverter.php',
@@ -453,26 +457,26 @@ $wgAutoloadLocalClasses = [
        'Field' => __DIR__ . '/includes/libs/rdbms/field/Field.php',
        'File' => __DIR__ . '/includes/filerepo/file/File.php',
        'FileAwareNodeVisitor' => __DIR__ . '/maintenance/findDeprecated.php',
-       'FileBackend' => __DIR__ . '/includes/filebackend/FileBackend.php',
+       'FileBackend' => __DIR__ . '/includes/libs/filebackend/FileBackend.php',
        'FileBackendDBRepoWrapper' => __DIR__ . '/includes/filerepo/FileBackendDBRepoWrapper.php',
-       'FileBackendError' => __DIR__ . '/includes/filebackend/FileBackend.php',
-       'FileBackendException' => __DIR__ . '/includes/filebackend/FileBackend.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',
+       'FileContentHandler' => __DIR__ . '/includes/content/FileContentHandler.php',
        'FileContentsHasher' => __DIR__ . '/includes/utils/FileContentsHasher.php',
        'FileDeleteForm' => __DIR__ . '/includes/FileDeleteForm.php',
        'FileDependency' => __DIR__ . '/includes/cache/CacheDependency.php',
        'FileDuplicateSearchPage' => __DIR__ . '/includes/specials/SpecialFileDuplicateSearch.php',
-       'FileJournal' => __DIR__ . '/includes/filebackend/filejournal/FileJournal.php',
-       'FileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
-       'FileOpBatch' => __DIR__ . '/includes/filebackend/FileOpBatch.php',
+       'FileJournal' => __DIR__ . '/includes/libs/filebackend/filejournal/FileJournal.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 +530,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 +548,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 +562,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',
@@ -571,7 +578,7 @@ $wgAutoloadLocalClasses = [
        'Html' => __DIR__ . '/includes/Html.php',
        'HtmlArmor' => __DIR__ . '/includes/libs/HtmlArmor.php',
        'HtmlFormatter' => __DIR__ . '/includes/HtmlFormatter.php',
-       'Http' => __DIR__ . '/includes/HttpFunctions.php',
+       'Http' => __DIR__ . '/includes/http/Http.php',
        'HttpError' => __DIR__ . '/includes/exception/HttpError.php',
        'HttpStatus' => __DIR__ . '/includes/libs/HttpStatus.php',
        'IApiMessage' => __DIR__ . '/includes/api/ApiMessage.php',
@@ -583,9 +590,11 @@ $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',
-       'IP' => __DIR__ . '/includes/utils/IP.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',
        'IRCColourfulRCFeedFormatter' => __DIR__ . '/includes/rcfeed/IRCColourfulRCFeedFormatter.php',
@@ -657,10 +666,9 @@ $wgAutoloadLocalClasses = [
        'KkConverter' => __DIR__ . '/languages/classes/LanguageKk.php',
        'KuConverter' => __DIR__ . '/languages/classes/LanguageKu.php',
        'LBFactory' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactory.php',
-       'LBFactoryMW' => __DIR__ . '/includes/db/loadbalancer/LBFactoryMW.php',
-       'LBFactoryMulti' => __DIR__ . '/includes/db/loadbalancer/LBFactoryMulti.php',
-       'LBFactorySimple' => __DIR__ . '/includes/db/loadbalancer/LBFactorySimple.php',
-       'LBFactorySingle' => __DIR__ . '/includes/db/loadbalancer/LBFactorySingle.php',
+       'LBFactoryMulti' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactoryMulti.php',
+       'LBFactorySimple' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactorySimple.php',
+       'LBFactorySingle' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactorySingle.php',
        'LCStore' => __DIR__ . '/includes/cache/localisation/LCStore.php',
        'LCStoreCDB' => __DIR__ . '/includes/cache/localisation/LCStoreCDB.php',
        'LCStoreDB' => __DIR__ . '/includes/cache/localisation/LCStoreDB.php',
@@ -732,7 +740,7 @@ $wgAutoloadLocalClasses = [
        'ListVariants' => __DIR__ . '/maintenance/language/listVariants.php',
        'ListredirectsPage' => __DIR__ . '/includes/specials/SpecialListredirects.php',
        'LoadBalancer' => __DIR__ . '/includes/libs/rdbms/loadbalancer/LoadBalancer.php',
-       'LoadBalancerSingle' => __DIR__ . '/includes/db/loadbalancer/LBFactorySingle.php',
+       'LoadBalancerSingle' => __DIR__ . '/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php',
        'LoadMonitor' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitor.php',
        'LoadMonitorMySQL' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php',
        'LoadMonitorNull' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php',
@@ -746,7 +754,7 @@ $wgAutoloadLocalClasses = [
        'LocalSettingsGenerator' => __DIR__ . '/includes/installer/LocalSettingsGenerator.php',
        'LocalisationCache' => __DIR__ . '/includes/cache/localisation/LocalisationCache.php',
        'LocalisationCacheBulkLoad' => __DIR__ . '/includes/cache/localisation/LocalisationCacheBulkLoad.php',
-       'LockManager' => __DIR__ . '/includes/filebackend/lockmanager/LockManager.php',
+       'LockManager' => __DIR__ . '/includes/libs/lockmanager/LockManager.php',
        'LockManagerGroup' => __DIR__ . '/includes/filebackend/lockmanager/LockManagerGroup.php',
        'LogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
        'LogEntryBase' => __DIR__ . '/includes/logging/LogEntry.php',
@@ -765,15 +773,17 @@ $wgAutoloadLocalClasses = [
        'MWCallableUpdate' => __DIR__ . '/includes/deferred/MWCallableUpdate.php',
        'MWContentSerializationException' => __DIR__ . '/includes/content/ContentHandler.php',
        'MWCryptHKDF' => __DIR__ . '/includes/utils/MWCryptHKDF.php',
-       'MWCryptHash' => __DIR__ . '/includes/utils/MWCryptHash.php',
+       'MWCryptHash' => __DIR__ . '/includes/libs/MWCryptHash.php',
        'MWCryptRand' => __DIR__ . '/includes/utils/MWCryptRand.php',
        'MWDebug' => __DIR__ . '/includes/debug/MWDebug.php',
        'MWDocGen' => __DIR__ . '/maintenance/mwdocgen.php',
        'MWException' => __DIR__ . '/includes/exception/MWException.php',
        'MWExceptionHandler' => __DIR__ . '/includes/exception/MWExceptionHandler.php',
        'MWExceptionRenderer' => __DIR__ . '/includes/exception/MWExceptionRenderer.php',
+       'MWFileProps' => __DIR__ . '/includes/utils/MWFileProps.php',
        'MWGrants' => __DIR__ . '/includes/utils/MWGrants.php',
-       'MWHttpRequest' => __DIR__ . '/includes/HttpFunctions.php',
+       'MWHttpRequest' => __DIR__ . '/includes/http/MWHttpRequest.php',
+       'MWLBFactory' => __DIR__ . '/includes/db/MWLBFactory.php',
        'MWMemcached' => __DIR__ . '/includes/compat/MemcachedClientCompat.php',
        'MWMessagePack' => __DIR__ . '/includes/libs/MWMessagePack.php',
        'MWNamespace' => __DIR__ . '/includes/MWNamespace.php',
@@ -868,14 +878,14 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php',
        'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php',
        'MediaWiki\\Search\\ParserOutputSearchDataExtractor' => __DIR__ . '/includes/search/ParserOutputSearchDataExtractor.php',
-       'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/Services/CannotReplaceActiveServiceException.php',
-       'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . '/includes/Services/ContainerDisabledException.php',
-       'MediaWiki\\Services\\DestructibleService' => __DIR__ . '/includes/Services/DestructibleService.php',
-       'MediaWiki\\Services\\NoSuchServiceException' => __DIR__ . '/includes/Services/NoSuchServiceException.php',
-       'MediaWiki\\Services\\SalvageableService' => __DIR__ . '/includes/Services/SalvageableService.php',
-       'MediaWiki\\Services\\ServiceAlreadyDefinedException' => __DIR__ . '/includes/Services/ServiceAlreadyDefinedException.php',
-       'MediaWiki\\Services\\ServiceContainer' => __DIR__ . '/includes/Services/ServiceContainer.php',
-       'MediaWiki\\Services\\ServiceDisabledException' => __DIR__ . '/includes/Services/ServiceDisabledException.php',
+       'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/services/CannotReplaceActiveServiceException.php',
+       'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . '/includes/services/ContainerDisabledException.php',
+       'MediaWiki\\Services\\DestructibleService' => __DIR__ . '/includes/services/DestructibleService.php',
+       'MediaWiki\\Services\\NoSuchServiceException' => __DIR__ . '/includes/services/NoSuchServiceException.php',
+       'MediaWiki\\Services\\SalvageableService' => __DIR__ . '/includes/services/SalvageableService.php',
+       'MediaWiki\\Services\\ServiceAlreadyDefinedException' => __DIR__ . '/includes/services/ServiceAlreadyDefinedException.php',
+       'MediaWiki\\Services\\ServiceContainer' => __DIR__ . '/includes/services/ServiceContainer.php',
+       'MediaWiki\\Services\\ServiceDisabledException' => __DIR__ . '/includes/services/ServiceDisabledException.php',
        'MediaWiki\\Session\\BotPasswordSessionProvider' => __DIR__ . '/includes/session/BotPasswordSessionProvider.php',
        'MediaWiki\\Session\\CookieSessionProvider' => __DIR__ . '/includes/session/CookieSessionProvider.php',
        'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' => __DIR__ . '/includes/session/ImmutableSessionProviderWithCookie.php',
@@ -908,18 +918,19 @@ $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',
        'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php',
        'MemCachedClientforWiki' => __DIR__ . '/includes/compat/MemcachedClientCompat.php',
-       'MemcLockManager' => __DIR__ . '/includes/filebackend/lockmanager/MemcLockManager.php',
+       'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php',
        'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
        'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php',
        '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',
@@ -942,7 +953,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',
@@ -974,11 +985,11 @@ $wgAutoloadLocalClasses = [
        'NotRecursiveIterator' => __DIR__ . '/includes/utils/iterators/NotRecursiveIterator.php',
        'NukeNS' => __DIR__ . '/maintenance/nukeNS.php',
        'NukePage' => __DIR__ . '/maintenance/nukePage.php',
-       'NullFileJournal' => __DIR__ . '/includes/filebackend/filejournal/FileJournal.php',
-       'NullFileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
+       'NullFileJournal' => __DIR__ . '/includes/libs/filebackend/filejournal/NullFileJournal.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/filebackend/lockmanager/LockManager.php',
+       'NullLockManager' => __DIR__ . '/includes/libs/lockmanager/NullLockManager.php',
        'NullRepo' => __DIR__ . '/includes/filerepo/NullRepo.php',
        'NullStatsdDataFactory' => __DIR__ . '/includes/libs/stats/NullStatsdDataFactory.php',
        'NumericUppercaseCollation' => __DIR__ . '/includes/collation/NumericUppercaseCollation.php',
@@ -1048,7 +1059,7 @@ $wgAutoloadLocalClasses = [
        'Pbkdf2Password' => __DIR__ . '/includes/password/Pbkdf2Password.php',
        'PerRowAugmentor' => __DIR__ . '/includes/search/PerRowAugmentor.php',
        'PermissionsError' => __DIR__ . '/includes/exception/PermissionsError.php',
-       'PhpHttpRequest' => __DIR__ . '/includes/HttpFunctions.php',
+       'PhpHttpRequest' => __DIR__ . '/includes/http/PhpHttpRequest.php',
        'PhpXmlBugTester' => __DIR__ . '/includes/installer/PhpBugTests.php',
        'Pingback' => __DIR__ . '/includes/Pingback.php',
        'PoolCounter' => __DIR__ . '/includes/poolcounter/PoolCounter.php',
@@ -1068,9 +1079,9 @@ $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/db/DatabasePostgres.php',
+       'PostgresField' => __DIR__ . '/includes/libs/rdbms/field/PostgresField.php',
        'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php',
        'PostgresUpdater' => __DIR__ . '/includes/installer/PostgresUpdater.php',
        'Preferences' => __DIR__ . '/includes/Preferences.php',
@@ -1098,6 +1109,7 @@ $wgAutoloadLocalClasses = [
        'ProtectedPagesPager' => __DIR__ . '/includes/specials/SpecialProtectedpages.php',
        'ProtectedTitlesPager' => __DIR__ . '/includes/specials/pagers/ProtectedTitlesPager.php',
        'ProtectionForm' => __DIR__ . '/includes/ProtectionForm.php',
+       'ProxyLookup' => __DIR__ . '/includes/ProxyLookup.php',
        'PruneFileCache' => __DIR__ . '/maintenance/pruneFileCache.php',
        'PublishStashedFileJob' => __DIR__ . '/includes/jobqueue/jobs/PublishStashedFileJob.php',
        'PurgeAction' => __DIR__ . '/includes/actions/PurgeAction.php',
@@ -1109,7 +1121,7 @@ $wgAutoloadLocalClasses = [
        'PurgeParserCache' => __DIR__ . '/maintenance/purgeParserCache.php',
        'QueryPage' => __DIR__ . '/includes/specialpage/QueryPage.php',
        'QuickTemplate' => __DIR__ . '/includes/skins/QuickTemplate.php',
-       'QuorumLockManager' => __DIR__ . '/includes/filebackend/lockmanager/QuorumLockManager.php',
+       'QuorumLockManager' => __DIR__ . '/includes/libs/lockmanager/QuorumLockManager.php',
        'RCCacheEntry' => __DIR__ . '/includes/changes/RCCacheEntry.php',
        'RCCacheEntryFactory' => __DIR__ . '/includes/changes/RCCacheEntryFactory.php',
        'RCDatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
@@ -1135,10 +1147,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',
@@ -1223,11 +1235,11 @@ $wgAutoloadLocalClasses = [
        'SQLiteField' => __DIR__ . '/includes/libs/rdbms/field/SQLiteField.php',
        'SVGMetadataExtractor' => __DIR__ . '/includes/media/SVGMetadataExtractor.php',
        'SVGReader' => __DIR__ . '/includes/media/SVGMetadataExtractor.php',
-       'SamplingStatsdClient' => __DIR__ . '/includes/libs/SamplingStatsdClient.php',
+       'SamplingStatsdClient' => __DIR__ . '/includes/libs/stats/SamplingStatsdClient.php',
        'Sanitizer' => __DIR__ . '/includes/Sanitizer.php',
-       'SavepointPostgres' => __DIR__ . '/includes/db/DatabasePostgres.php',
-       'ScopedCallback' => __DIR__ . '/includes/libs/ScopedCallback.php',
-       'ScopedLock' => __DIR__ . '/includes/filebackend/lockmanager/ScopedLock.php',
+       'SavepointPostgres' => __DIR__ . '/includes/libs/rdbms/database/utils/SavepointPostgres.php',
+       'ScopedCallback' => __DIR__ . '/includes/compat/ScopedCallback.php',
+       'ScopedLock' => __DIR__ . '/includes/libs/lockmanager/ScopedLock.php',
        'SearchApi' => __DIR__ . '/includes/api/SearchApi.php',
        'SearchDatabase' => __DIR__ . '/includes/search/SearchDatabase.php',
        'SearchDump' => __DIR__ . '/maintenance/dumpIterator.php',
@@ -1380,7 +1392,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',
@@ -1390,18 +1402,18 @@ $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',
        'TableDiffFormatter' => __DIR__ . '/includes/diff/TableDiffFormatter.php',
        'TablePager' => __DIR__ . '/includes/pager/TablePager.php',
        'TagLogFormatter' => __DIR__ . '/includes/logging/TagLogFormatter.php',
-       'TempFSFile' => __DIR__ . '/includes/filebackend/TempFSFile.php',
+       'TempFSFile' => __DIR__ . '/includes/libs/filebackend/TempFSFile.php',
        'TempFileRepo' => __DIR__ . '/includes/filerepo/FileRepo.php',
        'TemplateParser' => __DIR__ . '/includes/TemplateParser.php',
        'TemplatesOnThisPageFormatter' => __DIR__ . '/includes/TemplatesOnThisPageFormatter.php',
@@ -1507,7 +1519,6 @@ $wgAutoloadLocalClasses = [
        'VirtualRESTService' => __DIR__ . '/includes/libs/virtualrest/VirtualRESTService.php',
        'VirtualRESTServiceClient' => __DIR__ . '/includes/libs/virtualrest/VirtualRESTServiceClient.php',
        'WANObjectCache' => __DIR__ . '/includes/libs/objectcache/WANObjectCache.php',
-       'WaitConditionLoop' => __DIR__ . '/includes/libs/WaitConditionLoop.php',
        'WantedCategoriesPage' => __DIR__ . '/includes/specials/SpecialWantedcategories.php',
        'WantedFilesPage' => __DIR__ . '/includes/specials/SpecialWantedfiles.php',
        'WantedPagesPage' => __DIR__ . '/includes/specials/SpecialWantedpages.php',
@@ -1560,9 +1571,9 @@ $wgAutoloadLocalClasses = [
        'XCFHandler' => __DIR__ . '/includes/media/XCF.php',
        'XCacheBagOStuff' => __DIR__ . '/includes/libs/objectcache/XCacheBagOStuff.php',
        'XMLRCFeedFormatter' => __DIR__ . '/includes/rcfeed/XMLRCFeedFormatter.php',
-       'XMPInfo' => __DIR__ . '/includes/media/XMPInfo.php',
-       'XMPReader' => __DIR__ . '/includes/media/XMP.php',
-       'XMPValidate' => __DIR__ . '/includes/media/XMPValidate.php',
+       'XMPInfo' => __DIR__ . '/includes/libs/xmp/XMPInfo.php',
+       'XMPReader' => __DIR__ . '/includes/libs/xmp/XMP.php',
+       'XMPValidate' => __DIR__ . '/includes/libs/xmp/XMPValidate.php',
        'Xhprof' => __DIR__ . '/includes/libs/Xhprof.php',
        'XhprofData' => __DIR__ . '/includes/libs/XhprofData.php',
        'Xml' => __DIR__ . '/includes/Xml.php',
index eedaa4e..884b64f 100644 (file)
@@ -16,7 +16,7 @@
                "wiki": "https://www.mediawiki.org/"
        },
        "require": {
-               "composer/semver": "1.4.1",
+               "composer/semver": "1.4.2",
                "cssjanus/cssjanus": "1.1.2",
                "ext-ctype": "*",
                "ext-iconv": "*",
@@ -25,7 +25,7 @@
                "ext-xml": "*",
                "liuggio/statsd-php-client": "1.0.18",
                "mediawiki/at-ease": "1.1.0",
-               "oojs/oojs-ui": "0.17.9",
+               "oojs/oojs-ui": "0.17.10",
                "oyejorge/less.php": "1.7.0.10",
                "php": ">=5.5.9",
                "psr/log": "1.0.0",
                "wikimedia/composer-merge-plugin": "1.3.1",
                "wikimedia/html-formatter": "1.0.1",
                "wikimedia/ip-set": "1.1.0",
-               "wikimedia/php-session-serializer": "1.0.3",
+               "wikimedia/php-session-serializer": "1.0.4",
                "wikimedia/relpath": "1.0.3",
                "wikimedia/running-stat": "1.1.0",
+               "wikimedia/scoped-callback": "1.0.0",
                "wikimedia/utfnormal": "1.0.3",
+               "wikimedia/wait-condition-loop": "1.0.1",
                "wikimedia/wrappedstring": "2.2.0",
                "zordius/lightncandy": "0.23"
        },
        "require-dev": {
+               "composer/spdx-licenses": "1.1.4",
                "jakub-onderka/php-parallel-lint": "0.9.2",
                "justinrainbow/json-schema": "~3.0",
                "mediawiki/mediawiki-codesniffer": "0.7.2",
index c010014..84a404a 100644 (file)
                },
                "license-name": {
                        "type": "string",
-                       "description": "Short identifier for the license under which the extension is released.",
-                       "enum": [
-                               "AFL-1.1",
-                               "AFL-1.2",
-                               "AFL-2.0",
-                               "AFL-2.1",
-                               "AFL-3.0",
-                               "APL-1.0",
-                               "Aladdin",
-                               "ANTLR-PD",
-                               "Apache-1.0",
-                               "Apache-1.1",
-                               "Apache-2.0",
-                               "APSL-1.0",
-                               "APSL-1.1",
-                               "APSL-1.2",
-                               "APSL-2.0",
-                               "Artistic-1.0",
-                               "Artistic-1.0-cl8",
-                               "Artistic-1.0-Perl",
-                               "Artistic-2.0",
-                               "AAL",
-                               "BitTorrent-1.0",
-                               "BitTorrent-1.1",
-                               "BSL-1.0",
-                               "BSD-2-Clause",
-                               "BSD-2-Clause-FreeBSD",
-                               "BSD-2-Clause-NetBSD",
-                               "BSD-3-Clause",
-                               "BSD-3-Clause-Clear",
-                               "BSD-4-Clause",
-                               "BSD-4-Clause-UC",
-                               "CECILL-1.0",
-                               "CECILL-1.1",
-                               "CECILL-2.0",
-                               "CECILL-B",
-                               "CECILL-C",
-                               "ClArtistic",
-                               "CNRI-Python",
-                               "CNRI-Python-GPL-Compatible",
-                               "CPOL-1.02",
-                               "CDDL-1.0",
-                               "CDDL-1.1",
-                               "CPAL-1.0",
-                               "CPL-1.0",
-                               "CATOSL-1.1",
-                               "Condor-1.1",
-                               "CC-BY-1.0",
-                               "CC-BY-2.0",
-                               "CC-BY-2.5",
-                               "CC-BY-3.0",
-                               "CC-BY-ND-1.0",
-                               "CC-BY-ND-2.0",
-                               "CC-BY-ND-2.5",
-                               "CC-BY-ND-3.0",
-                               "CC-BY-NC-1.0",
-                               "CC-BY-NC-2.0",
-                               "CC-BY-NC-2.5",
-                               "CC-BY-NC-3.0",
-                               "CC-BY-NC-ND-1.0",
-                               "CC-BY-NC-ND-2.0",
-                               "CC-BY-NC-ND-2.5",
-                               "CC-BY-NC-ND-3.0",
-                               "CC-BY-NC-SA-1.0",
-                               "CC-BY-NC-SA-2.0",
-                               "CC-BY-NC-SA-2.5",
-                               "CC-BY-NC-SA-3.0",
-                               "CC-BY-SA-1.0",
-                               "CC-BY-SA-2.0",
-                               "CC-BY-SA-2.5",
-                               "CC-BY-SA-3.0",
-                               "CC0-1.0",
-                               "CUA-OPL-1.0",
-                               "D-FSL-1.0",
-                               "WTFPL",
-                               "EPL-1.0",
-                               "eCos-2.0",
-                               "ECL-1.0",
-                               "ECL-2.0",
-                               "EFL-1.0",
-                               "EFL-2.0",
-                               "Entessa",
-                               "ErlPL-1.1",
-                               "EUDatagrid",
-                               "EUPL-1.0",
-                               "EUPL-1.1",
-                               "Fair",
-                               "Frameworx-1.0",
-                               "FTL",
-                               "AGPL-1.0",
-                               "AGPL-3.0",
-                               "GFDL-1.1",
-                               "GFDL-1.2",
-                               "GFDL-1.3",
-                               "GPL-1.0",
-                               "GPL-1.0+",
-                               "GPL-2.0",
-                               "GPL-2.0+",
-                               "GPL-2.0-with-autoconf-exception",
-                               "GPL-2.0-with-bison-exception",
-                               "GPL-2.0-with-classpath-exception",
-                               "GPL-2.0-with-font-exception",
-                               "GPL-2.0-with-GCC-exception",
-                               "GPL-3.0",
-                               "GPL-3.0+",
-                               "GPL-3.0-with-autoconf-exception",
-                               "GPL-3.0-with-GCC-exception",
-                               "LGPL-2.1",
-                               "LGPL-2.1+",
-                               "LGPL-3.0",
-                               "LGPL-3.0+",
-                               "LGPL-2.0",
-                               "LGPL-2.0+",
-                               "gSOAP-1.3b",
-                               "HPND",
-                               "IBM-pibs",
-                               "IPL-1.0",
-                               "Imlib2",
-                               "IJG",
-                               "Intel",
-                               "IPA",
-                               "ISC",
-                               "JSON",
-                               "LPPL-1.3a",
-                               "LPPL-1.0",
-                               "LPPL-1.1",
-                               "LPPL-1.2",
-                               "LPPL-1.3c",
-                               "Libpng",
-                               "LPL-1.02",
-                               "LPL-1.0",
-                               "MS-PL",
-                               "MS-RL",
-                               "MirOS",
-                               "MIT",
-                               "Motosoto",
-                               "MPL-1.0",
-                               "MPL-1.1",
-                               "MPL-2.0",
-                               "MPL-2.0-no-copyleft-exception",
-                               "Multics",
-                               "NASA-1.3",
-                               "Naumen",
-                               "NBPL-1.0",
-                               "NGPL",
-                               "NOSL",
-                               "NPL-1.0",
-                               "NPL-1.1",
-                               "Nokia",
-                               "NPOSL-3.0",
-                               "NTP",
-                               "OCLC-2.0",
-                               "ODbL-1.0",
-                               "PDDL-1.0",
-                               "OGTSL",
-                               "OLDAP-2.2.2",
-                               "OLDAP-1.1",
-                               "OLDAP-1.2",
-                               "OLDAP-1.3",
-                               "OLDAP-1.4",
-                               "OLDAP-2.0",
-                               "OLDAP-2.0.1",
-                               "OLDAP-2.1",
-                               "OLDAP-2.2",
-                               "OLDAP-2.2.1",
-                               "OLDAP-2.3",
-                               "OLDAP-2.4",
-                               "OLDAP-2.5",
-                               "OLDAP-2.6",
-                               "OLDAP-2.7",
-                               "OPL-1.0",
-                               "OSL-1.0",
-                               "OSL-2.0",
-                               "OSL-2.1",
-                               "OSL-3.0",
-                               "OLDAP-2.8",
-                               "OpenSSL",
-                               "PHP-3.0",
-                               "PHP-3.01",
-                               "PostgreSQL",
-                               "Python-2.0",
-                               "QPL-1.0",
-                               "RPSL-1.0",
-                               "RPL-1.1",
-                               "RPL-1.5",
-                               "RHeCos-1.1",
-                               "RSCPL",
-                               "Ruby",
-                               "SAX-PD",
-                               "SGI-B-1.0",
-                               "SGI-B-1.1",
-                               "SGI-B-2.0",
-                               "OFL-1.0",
-                               "OFL-1.1",
-                               "SimPL-2.0",
-                               "Sleepycat",
-                               "SMLNJ",
-                               "SugarCRM-1.1.3",
-                               "SISSL",
-                               "SISSL-1.2",
-                               "SPL-1.0",
-                               "Watcom-1.0",
-                               "NCSA",
-                               "VSL-1.0",
-                               "W3C",
-                               "WXwindows",
-                               "Xnet",
-                               "X11",
-                               "XFree86-1.1",
-                               "YPL-1.0",
-                               "YPL-1.1",
-                               "Zimbra-1.3",
-                               "Zlib",
-                               "ZPL-1.1",
-                               "ZPL-2.0",
-                               "ZPL-2.1",
-                               "Unlicense"
-                       ]
+                       "description": "SPDX identifier for the license under which the extension is released."
                },
                "requires": {
                        "type": "object",
                        "type": "array",
                        "description": "Parser test suite files to be run by parserTests.php when no specific filename is passed to it"
                },
+               "ServiceWiringFiles": {
+                       "type": "array",
+                       "description": "List of service wiring files to be loaded by the default instance of MediaWikiServices"
+               },
                "load_composer_autoloader": {
                        "type": "boolean",
                        "description": "Load the composer autoloader for this extension, if one is present"
index d707864..9499927 100644 (file)
                },
                "license-name": {
                        "type": "string",
-                       "description": "Short identifier for the license under which the extension is released.",
-                       "enum": [
-                               "AFL-1.1",
-                               "AFL-1.2",
-                               "AFL-2.0",
-                               "AFL-2.1",
-                               "AFL-3.0",
-                               "APL-1.0",
-                               "Aladdin",
-                               "ANTLR-PD",
-                               "Apache-1.0",
-                               "Apache-1.1",
-                               "Apache-2.0",
-                               "APSL-1.0",
-                               "APSL-1.1",
-                               "APSL-1.2",
-                               "APSL-2.0",
-                               "Artistic-1.0",
-                               "Artistic-1.0-cl8",
-                               "Artistic-1.0-Perl",
-                               "Artistic-2.0",
-                               "AAL",
-                               "BitTorrent-1.0",
-                               "BitTorrent-1.1",
-                               "BSL-1.0",
-                               "BSD-2-Clause",
-                               "BSD-2-Clause-FreeBSD",
-                               "BSD-2-Clause-NetBSD",
-                               "BSD-3-Clause",
-                               "BSD-3-Clause-Clear",
-                               "BSD-4-Clause",
-                               "BSD-4-Clause-UC",
-                               "CECILL-1.0",
-                               "CECILL-1.1",
-                               "CECILL-2.0",
-                               "CECILL-B",
-                               "CECILL-C",
-                               "ClArtistic",
-                               "CNRI-Python",
-                               "CNRI-Python-GPL-Compatible",
-                               "CPOL-1.02",
-                               "CDDL-1.0",
-                               "CDDL-1.1",
-                               "CPAL-1.0",
-                               "CPL-1.0",
-                               "CATOSL-1.1",
-                               "Condor-1.1",
-                               "CC-BY-1.0",
-                               "CC-BY-2.0",
-                               "CC-BY-2.5",
-                               "CC-BY-3.0",
-                               "CC-BY-ND-1.0",
-                               "CC-BY-ND-2.0",
-                               "CC-BY-ND-2.5",
-                               "CC-BY-ND-3.0",
-                               "CC-BY-NC-1.0",
-                               "CC-BY-NC-2.0",
-                               "CC-BY-NC-2.5",
-                               "CC-BY-NC-3.0",
-                               "CC-BY-NC-ND-1.0",
-                               "CC-BY-NC-ND-2.0",
-                               "CC-BY-NC-ND-2.5",
-                               "CC-BY-NC-ND-3.0",
-                               "CC-BY-NC-SA-1.0",
-                               "CC-BY-NC-SA-2.0",
-                               "CC-BY-NC-SA-2.5",
-                               "CC-BY-NC-SA-3.0",
-                               "CC-BY-SA-1.0",
-                               "CC-BY-SA-2.0",
-                               "CC-BY-SA-2.5",
-                               "CC-BY-SA-3.0",
-                               "CC0-1.0",
-                               "CUA-OPL-1.0",
-                               "D-FSL-1.0",
-                               "WTFPL",
-                               "EPL-1.0",
-                               "eCos-2.0",
-                               "ECL-1.0",
-                               "ECL-2.0",
-                               "EFL-1.0",
-                               "EFL-2.0",
-                               "Entessa",
-                               "ErlPL-1.1",
-                               "EUDatagrid",
-                               "EUPL-1.0",
-                               "EUPL-1.1",
-                               "Fair",
-                               "Frameworx-1.0",
-                               "FTL",
-                               "AGPL-1.0",
-                               "AGPL-3.0",
-                               "GFDL-1.1",
-                               "GFDL-1.2",
-                               "GFDL-1.3",
-                               "GPL-1.0",
-                               "GPL-1.0+",
-                               "GPL-2.0",
-                               "GPL-2.0+",
-                               "GPL-2.0-with-autoconf-exception",
-                               "GPL-2.0-with-bison-exception",
-                               "GPL-2.0-with-classpath-exception",
-                               "GPL-2.0-with-font-exception",
-                               "GPL-2.0-with-GCC-exception",
-                               "GPL-3.0",
-                               "GPL-3.0+",
-                               "GPL-3.0-with-autoconf-exception",
-                               "GPL-3.0-with-GCC-exception",
-                               "LGPL-2.1",
-                               "LGPL-2.1+",
-                               "LGPL-3.0",
-                               "LGPL-3.0+",
-                               "LGPL-2.0",
-                               "LGPL-2.0+",
-                               "gSOAP-1.3b",
-                               "HPND",
-                               "IBM-pibs",
-                               "IPL-1.0",
-                               "Imlib2",
-                               "IJG",
-                               "Intel",
-                               "IPA",
-                               "ISC",
-                               "JSON",
-                               "LPPL-1.3a",
-                               "LPPL-1.0",
-                               "LPPL-1.1",
-                               "LPPL-1.2",
-                               "LPPL-1.3c",
-                               "Libpng",
-                               "LPL-1.02",
-                               "LPL-1.0",
-                               "MS-PL",
-                               "MS-RL",
-                               "MirOS",
-                               "MIT",
-                               "Motosoto",
-                               "MPL-1.0",
-                               "MPL-1.1",
-                               "MPL-2.0",
-                               "MPL-2.0-no-copyleft-exception",
-                               "Multics",
-                               "NASA-1.3",
-                               "Naumen",
-                               "NBPL-1.0",
-                               "NGPL",
-                               "NOSL",
-                               "NPL-1.0",
-                               "NPL-1.1",
-                               "Nokia",
-                               "NPOSL-3.0",
-                               "NTP",
-                               "OCLC-2.0",
-                               "ODbL-1.0",
-                               "PDDL-1.0",
-                               "OGTSL",
-                               "OLDAP-2.2.2",
-                               "OLDAP-1.1",
-                               "OLDAP-1.2",
-                               "OLDAP-1.3",
-                               "OLDAP-1.4",
-                               "OLDAP-2.0",
-                               "OLDAP-2.0.1",
-                               "OLDAP-2.1",
-                               "OLDAP-2.2",
-                               "OLDAP-2.2.1",
-                               "OLDAP-2.3",
-                               "OLDAP-2.4",
-                               "OLDAP-2.5",
-                               "OLDAP-2.6",
-                               "OLDAP-2.7",
-                               "OPL-1.0",
-                               "OSL-1.0",
-                               "OSL-2.0",
-                               "OSL-2.1",
-                               "OSL-3.0",
-                               "OLDAP-2.8",
-                               "OpenSSL",
-                               "PHP-3.0",
-                               "PHP-3.01",
-                               "PostgreSQL",
-                               "Python-2.0",
-                               "QPL-1.0",
-                               "RPSL-1.0",
-                               "RPL-1.1",
-                               "RPL-1.5",
-                               "RHeCos-1.1",
-                               "RSCPL",
-                               "Ruby",
-                               "SAX-PD",
-                               "SGI-B-1.0",
-                               "SGI-B-1.1",
-                               "SGI-B-2.0",
-                               "OFL-1.0",
-                               "OFL-1.1",
-                               "SimPL-2.0",
-                               "Sleepycat",
-                               "SMLNJ",
-                               "SugarCRM-1.1.3",
-                               "SISSL",
-                               "SISSL-1.2",
-                               "SPL-1.0",
-                               "Watcom-1.0",
-                               "NCSA",
-                               "VSL-1.0",
-                               "W3C",
-                               "WXwindows",
-                               "Xnet",
-                               "X11",
-                               "XFree86-1.1",
-                               "YPL-1.0",
-                               "YPL-1.1",
-                               "Zimbra-1.3",
-                               "Zlib",
-                               "ZPL-1.1",
-                               "ZPL-2.0",
-                               "ZPL-2.1",
-                               "Unlicense"
-                       ]
+                       "description": "SPDX identifier for the license under which the extension is released."
                },
                "requires": {
                        "type": "object",
                        "type": "array",
                        "description": "Parser test suite files to be run by parserTests.php when no specific filename is passed to it"
                },
+               "ServiceWiringFiles": {
+                       "type": "array",
+                       "description": "List of service wiring files to be loaded by the default instance of MediaWikiServices"
+               },
                "load_composer_autoloader": {
                        "type": "boolean",
                        "description": "Load the composer autoloader for this extension, if one is present"
index ae0770b..cccd13d 100644 (file)
@@ -464,6 +464,41 @@ $moduleManager: ApiModuleManager Module manager instance
 action=query submodule. Use this to extend core API modules.
 &$module: Module object
 
+'ApiQueryBaseAfterQuery': Called for (some) API query modules after the
+database query has returned. An API query module wanting to use this hook
+should see the ApiQueryBase::select() and ApiQueryBase::processRow()
+documentation.
+$module: ApiQueryBase module in question
+$result: ResultWrapper|bool returned from the IDatabase::select()
+&$hookData: array that was passed to the 'ApiQueryBaseBeforeQuery' hook and
+ will be passed to the 'ApiQueryBaseProcessRow' hook, intended for inter-hook
+ communication.
+
+'ApiQueryBaseBeforeQuery': Called for (some) API query modules before a
+database query is made. WARNING: It would be very easy to misuse this hook and
+break the module! Any joins added *must* join on a unique key of the target
+table unless you really know what you're doing. An API query module wanting to
+use this hook should see the ApiQueryBase::select() and
+ApiQueryBase::processRow() documentation.
+$module: ApiQueryBase module in question
+&$tables: array of tables to be queried
+&$fields: array of columns to select
+&$conds: array of WHERE conditionals for query
+&$query_options: array of options for the database request
+&$join_conds: join conditions for the tables
+&$hookData: array that will be passed to the 'ApiQueryBaseAfterQuery' and
+ 'ApiQueryBaseProcessRow' hooks, intended for inter-hook communication.
+
+'ApiQueryBaseProcessRow': Called for (some) API query modules as each row of
+the database result is processed. Return false to stop processing the result
+set. An API query module wanting to use this hook should see the
+ApiQueryBase::select() and ApiQueryBase::processRow() documentation.
+$module: ApiQueryBase module in question
+$row: stdClass Database result row
+&$data: array to be included in the ApiResult.
+&$hookData: array that was be passed to the 'ApiQueryBaseBeforeQuery' and
+ 'ApiQueryBaseAfterQuery' hooks, intended for inter-hook communication.
+
 'APIQueryGeneratorAfterExecute': After calling the executeGenerator() method of
 an action=query submodule. Use this to extend core API modules.
 &$module: Module object
@@ -1019,6 +1054,18 @@ $user: user initiating the action
 uses are in active use.
 &$tags: list of all active tags. Append to this array.
 
+'ChangeTagsAfterUpdateTags': Called after tags have been updated with the
+ChangeTags::updateTags function. Params:
+$addedTags: tags effectively added in the update
+$removedTags: tags effectively removed in the update
+$prevTags: tags that were present prior to the update
+$rc_id: recentchanges table id
+$rev_id: revision table id
+$log_id: logging table id
+$params: tag params
+$rc: RecentChange being tagged when the tagging accompanies the action or null
+$user: User who performed the tagging when the tagging is subsequent to the action or null
+
 'Collation::factory': Called if $wgCategoryCollation is an unknown collation.
 $collationName: Name of the collation in question
 &$collationObject: Null. Replace with a subclass of the Collation class that
@@ -1974,6 +2021,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 19ba0a2..098d51c 100644 (file)
@@ -19,6 +19,9 @@
  *
  * @file
  */
+
+use MediaWiki\MediaWikiServices;
+
 class Block {
        /** @var string */
        public $mReason;
@@ -1120,6 +1123,7 @@ class Block {
                }
 
                $conds = [];
+               $proxyLookup = MediaWikiServices::getInstance()->getProxyLookup();
                foreach ( array_unique( $ipChain ) as $ipaddr ) {
                        # Discard invalid IP addresses. Since XFF can be spoofed and we do not
                        # necessarily trust the header given to us, make sure that we are only
@@ -1130,7 +1134,7 @@ class Block {
                                continue;
                        }
                        # Don't check trusted IPs (includes local squids which will be in every request)
-                       if ( IP::isTrustedProxy( $ipaddr ) ) {
+                       if ( $proxyLookup->isTrustedProxy( $ipaddr ) ) {
                                continue;
                        }
                        # Check both the original IP (to check against single blocks), as well as build
index a8e988f..c858dd7 100644 (file)
@@ -19,6 +19,7 @@
  *
  * @file
  */
+use MediaWiki\MediaWikiServices;
 
 class CategoryViewer extends ContextSource {
        /** @var int */
@@ -317,10 +318,21 @@ 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(),
+                                       [
+                                               'page_namespace',
+                                               'page_title',
+                                               '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 +350,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 3ab8829..2ae33b2 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 = [];
 
@@ -1835,13 +1844,6 @@ $wgDBmwschema = null;
  */
 $wgSQLiteDataDir = '';
 
-/**
- * Make all database connections secretly go to localhost. Fool the load balancer
- * thinking there is an arbitrarily large cluster of servers to connect to.
- * Useful for debugging.
- */
-$wgAllDBsAreLocalhost = false;
-
 /**
  * Shared database for multiple wikis. Commonly used for storing a user table
  * for single sign-on. The server for this database must be the same as for the
@@ -2092,7 +2094,7 @@ $wgExternalStores = [];
  * Create a cluster named 'cluster1' containing three servers:
  * @code
  * $wgExternalServers = [
- *     'cluster1' => [ 'srv28', 'srv29', 'srv30' ]
+ *     'cluster1' => <array in the same format as $wgDBservers>
  * ];
  * @endcode
  *
@@ -2209,7 +2211,7 @@ $wgCacheDirectory = false;
  *   - CACHE_NONE:       Do not cache
  *   - CACHE_DB:         Store cache objects in the DB
  *   - CACHE_MEMCACHED:  MemCached, must specify servers in $wgMemCachedServers
- *   - CACHE_ACCEL:      APC, XCache or WinCache
+ *   - CACHE_ACCEL:      APC, APCU, XCache or WinCache
  *   - (other):          A string may be used which identifies a cache
  *                       configuration in $wgObjectCaches.
  *
@@ -2286,6 +2288,7 @@ $wgObjectCaches = [
        ],
 
        'apc' => [ 'class' => 'APCBagOStuff', 'reportDupes' => false ],
+       'apcu' => [ 'class' => 'APCUBagOStuff', 'reportDupes' => false ],
        'xcache' => [ 'class' => 'XCacheBagOStuff', 'reportDupes' => false ],
        'wincache' => [ 'class' => 'WinCacheBagOStuff', 'reportDupes' => false ],
        'memcached-php' => [ 'class' => 'MemcachedPhpBagOStuff', 'loggroup' => 'memcached' ],
@@ -5434,11 +5437,30 @@ $wgDeleteRevisionsLimit = 0;
 $wgHideUserContribLimit = 1000;
 
 /**
- * Number of accounts each IP address may create, 0 to disable.
+ * Number of accounts each IP address may create per specified period(s).
+ *
+ * @par Example:
+ * @code
+ * $wgAccountCreationThrottle = [
+ *  // no more than 100 per month
+ *  [
+ *   'count' => 100,
+ *   'seconds' => 30*86400,
+ *  ],
+ *  // no more than 10 per day
+ *  [
+ *   'count' => 10,
+ *   'seconds' => 86400,
+ *  ],
+ * ];
+ * @endcode
  *
  * @warning Requires $wgMainCacheType to be enabled
  */
-$wgAccountCreationThrottle = 0;
+$wgAccountCreationThrottle = [ [
+       'count' => 0,
+       'seconds' => 86400,
+] ];
 
 /**
  * Edits matching these regular expressions in body text
@@ -5511,13 +5533,7 @@ $wgApplyIpBlocksToXff = false;
  * elapses.
  *
  * @par Example:
- * To set a generic maximum of 4 hits in 60 seconds:
- * @code
- *     $wgRateLimits = [ 4, 60 ];
- * @endcode
- *
- * @par Example:
- * You could also limit per action and then type of users.
+ * Limits per configured per action and then type of users.
  * @code
  *     $wgRateLimits = [
  *         'edit' => [
@@ -5526,8 +5542,20 @@ $wgApplyIpBlocksToXff = false;
  *             'newbie' => [ x, y ], // each new autoconfirmed accounts; overrides 'user'
  *             'ip' => [ x, y ], // each anon and recent account
  *             'subnet' => [ x, y ], // ... within a /24 subnet in IPv4 or /64 in IPv6
+ *             'groupName' => [ x, y ], // by group membership
  *         ]
- *     ]
+ *     ];
+ * @endcode
+ *
+ * @par Normally, the 'noratelimit' right allows a user to bypass any rate
+ * limit checks. This can be disabled on a per-action basis by setting the
+ * special '&can-bypass' key to false in that action's configuration.
+ * @code
+ *     $wgRateLimits = [
+ *         'some-action' => [
+ *             '&can-bypass' => false,
+ *             'user' => [ x, y ],
+ *     ];
  * @endcode
  *
  * @warning Requires that $wgMainCacheType is set to something persistent
@@ -6001,7 +6029,7 @@ $wgTrxProfilerLimits = [
        'JobRunner' => [
                'readQueryTime' => 30,
                'writeQueryTime' => 5,
-               'maxAffected' => 1000
+               'maxAffected' => 500 // ballpark of $wgUpdateRowsPerQuery
        ],
        // Command-line scripts
        'Maintenance' => [
index 077f39a..02930ea 100644 (file)
 # Obsolete aliases
 define( 'DB_SLAVE', -1 );
 
+/**@{
+ * Obsolete IDatabase::makeList() constants
+ * These are also available as Database class constants
+ */
+define( 'LIST_COMMA', IDatabase::LIST_COMMA );
+define( 'LIST_AND', IDatabase::LIST_AND );
+define( 'LIST_SET', IDatabase::LIST_SET );
+define( 'LIST_NAMES', IDatabase::LIST_NAMES );
+define( 'LIST_OR', IDatabase::LIST_OR );
+/**@}*/
+
 /**@{
  * Virtual namespaces; don't appear in the page database
  */
@@ -66,8 +77,13 @@ define( 'NS_CATEGORY_TALK', 15 );
  * When writing code that should be compatible with older MediaWiki
  * versions, either stick to the old names or define the new constants
  * yourself, if they're not defined already.
+ *
+ * @deprecated since 1.14
  */
 define( 'NS_IMAGE', NS_FILE );
+/**
+ * @deprecated since 1.14
+ */
 define( 'NS_IMAGE_TALK', NS_FILE_TALK );
 /**@}*/
 
index 606b4cd..8ca7105 100644 (file)
@@ -1613,8 +1613,8 @@ class EditPage {
        protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
                // Run old style post-section-merge edit filter
                if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged',
-                       [ $this, $content, &$this->hookError, $this->summary ] )
-               ) {
+                       [ $this, $content, &$this->hookError, $this->summary ]
+               ) {
                        # Error messages etc. could be handled within the hook...
                        $status->fatal( 'hookaborted' );
                        $status->value = self::AS_HOOK_ERROR;
@@ -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();
                }
@@ -3291,6 +3300,12 @@ HTML
                        'id' => $name,
                        'cols' => $wgUser->getIntOption( 'cols' ),
                        'rows' => $wgUser->getIntOption( 'rows' ),
+                       // The following classes can be used here:
+                       // * mw-editfont-default
+                       // * mw-editfont-monospace
+                       // * mw-editfont-sans-serif
+                       // * mw-editfont-serif
+                       'class' => 'mw-editfont-' . $wgUser->getOption( 'editfont' ),
                        // Avoid PHP notices when appending preferences
                        // (appending allows customAttribs['style'] to still work).
                        'style' => ''
@@ -3401,7 +3416,7 @@ HTML
                }
 
                if ( $newContent ) {
-                       ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
+                       ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ], '1.21' );
                        Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
 
                        $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
@@ -3487,7 +3502,7 @@ HTML
         * @param string $format Output format, valid values are any function of a Message object
         * @return string
         */
-       public static function getCopyrightWarning( $title, $format = 'plain' ) {
+       public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
                global $wgRightsText;
                if ( $wgRightsText ) {
                        $copywarnMsg = [ 'copyrightwarning',
@@ -3500,8 +3515,12 @@ HTML
                // Allow for site and per-namespace customization of contribution/copyright notice.
                Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
 
+               $msg = call_user_func_array( 'wfMessage', $copywarnMsg )->title( $title );
+               if ( $langcode ) {
+                       $msg->inLanguage( $langcode );
+               }
                return "<div id=\"editpage-copywarn\">\n" .
-                       call_user_func_array( 'wfMessage', $copywarnMsg )->title( $title )->$format() . "\n</div>";
+                       $msg->$format() . "\n</div>";
        }
 
        /**
@@ -3810,7 +3829,7 @@ HTML
                        }
 
                        $hook_args = [ $this, &$content ];
-                       ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
+                       ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args, '1.25' );
                        Hooks::run( 'EditPageGetPreviewContent', $hook_args );
 
                        $parserResult = $this->doPreviewParse( $content );
@@ -4136,7 +4155,7 @@ HTML
                        'name' => 'wpSave',
                        'tabindex' => ++$tabindex,
                ] + Linker::tooltipAndAccesskeyAttribs( 'save' );
-               $buttons['save'] = Html::submitButton( $buttonLabel, $attribs, [ 'mw-ui-constructive' ] );
+               $buttons['save'] = Html::submitButton( $buttonLabel, $attribs, [ 'mw-ui-progressive' ] );
 
                ++$tabindex; // use the same for preview and live preview
                $attribs = [
@@ -4254,7 +4273,7 @@ HTML
        protected function safeUnicodeOutput( $text ) {
                return $this->checkUnicodeCompliantBrowser()
                        ? $text
-                       : $this->makesafe( $text );
+                       : $this->makeSafe( $text );
        }
 
        /**
index 65638f2..e6223e8 100644 (file)
@@ -125,7 +125,7 @@ class FileDeleteForm {
                                        $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' )
                                        . '</div>' );
                        }
-                       if ( $status->ok ) {
+                       if ( $status->isOK() ) {
                                $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) );
                                $wgOut->addHTML( $this->prepareMessage( 'filedelete-success' ) );
                                // Return to the main page if we just deleted all versions of the
@@ -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 90bba53..2b6088e 100644 (file)
@@ -27,6 +27,7 @@ if ( !defined( 'MEDIAWIKI' ) ) {
 use Liuggio\StatsdClient\Sender\SocketSender;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\Session\SessionManager;
+use Wikimedia\ScopedCallback;
 
 // Hide compatibility functions from Doxygen
 /// @cond
@@ -2003,13 +2004,11 @@ require_once __DIR__ . '/libs/time/defines.php';
  * @return string|bool String / false The same date in the format specified in $outputtype or false
  */
 function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) {
-       try {
-               $timestamp = new MWTimestamp( $ts );
-               return $timestamp->getTimestamp( $outputtype );
-       } catch ( TimestampException $e ) {
+       $ret = MWTimestamp::convert( $outputtype, $ts );
+       if ( $ret === false ) {
                wfDebug( "wfTimestamp() fed bogus time value: TYPE=$outputtype; VALUE=$ts\n" );
-               return false;
        }
+       return $ret;
 }
 
 /**
@@ -2035,7 +2034,7 @@ function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) {
  */
 function wfTimestampNow() {
        # return NOW
-       return wfTimestamp( TS_MW, time() );
+       return MWTimestamp::now( TS_MW );
 }
 
 /**
@@ -2078,35 +2077,7 @@ function wfTempDir() {
                return $wgTmpDirectory;
        }
 
-       $tmpDir = array_map( "getenv", [ 'TMPDIR', 'TMP', 'TEMP' ] );
-       $tmpDir[] = sys_get_temp_dir();
-       $tmpDir[] = ini_get( 'upload_tmp_dir' );
-
-       foreach ( $tmpDir as $tmp ) {
-               if ( $tmp && file_exists( $tmp ) && is_dir( $tmp ) && is_writable( $tmp ) ) {
-                       return $tmp;
-               }
-       }
-
-       /**
-        * PHP on Windows will detect C:\Windows\Temp as not writable even though PHP can write to it
-        * so create a directory within that called 'mwtmp' with a suffix of the user running the
-        * current process.
-        * The user is included as if various scripts are run by different users they will likely
-        * not be able to access each others temporary files.
-        */
-       if ( wfIsWindows() ) {
-               $tmp = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'mwtmp' . '-' . get_current_user();
-               if ( !file_exists( $tmp ) ) {
-                       mkdir( $tmp );
-               }
-               if ( file_exists( $tmp ) && is_dir( $tmp ) && is_writable( $tmp ) ) {
-                       return $tmp;
-               }
-       }
-
-       throw new MWException( 'No writable temporary directory could be found. ' .
-               'Please set $wgTmpDirectory to a writable directory.' );
+       return TempFSFile::getUsableTempDirectory();
 }
 
 /**
@@ -2224,12 +2195,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
@@ -3097,7 +3067,7 @@ function wfSplitWikiID( $wiki ) {
  * @todo Replace calls to wfGetDB with calls to LoadBalancer::getConnection()
  *       on an injected instance of LoadBalancer.
  *
- * @return DatabaseBase
+ * @return Database
  */
 function wfGetDB( $db, $groups = [], $wiki = false ) {
        return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki );
index 8c01448..48b30c7 100644 (file)
  * @since 1.16
  */
 class Html {
-       // List of void elements from HTML5, section 8.1.2 as of 2011-08-12
+       // List of void elements from HTML5, section 8.1.2 as of 2016-09-19
        private static $voidElements = [
                'area',
                'base',
                'br',
                'col',
-               'command',
                'embed',
                'hr',
                'img',
@@ -156,8 +155,8 @@ class Html {
         *
         * @param string $contents The raw HTML contents of the element: *not*
         *   escaped!
-        * @param array $attrs Associative array of attributes, e.g., array(
-        *   'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for
+        * @param array $attrs Associative array of attributes, e.g., [
+        *   'href' => 'http://www.mediawiki.org/' ]. See expandAttributes() for
         *   further documentation.
         * @param string[] $modifiers classes to add to the button
         * @see http://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers
@@ -176,8 +175,8 @@ class Html {
         *
         * @param string $contents The raw HTML contents of the element: *not*
         *   escaped!
-        * @param array $attrs Associative array of attributes, e.g., array(
-        *   'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for
+        * @param array $attrs Associative array of attributes, e.g., [
+        *   'href' => 'http://www.mediawiki.org/' ]. See expandAttributes() for
         *   further documentation.
         * @param string[] $modifiers classes to add to the button
         * @see http://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers
@@ -200,8 +199,8 @@ class Html {
         * content model.
         *
         * @param string $element The element's name, e.g., 'a'
-        * @param array $attribs Associative array of attributes, e.g., array(
-        *   'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for
+        * @param array $attribs Associative array of attributes, e.g., [
+        *   'href' => 'http://www.mediawiki.org/' ]. See expandAttributes() for
         *   further documentation.
         * @param string $contents The raw HTML contents of the element: *not*
         *   escaped!
@@ -321,8 +320,8 @@ class Html {
         * to the input array (currently per the HTML 5 draft as of 2009-09-06).
         *
         * @param string $element Name of the element, e.g., 'a'
-        * @param array $attribs Associative array of attributes, e.g., array(
-        *   'href' => 'http://www.mediawiki.org/' ).  See expandAttributes() for
+        * @param array $attribs Associative array of attributes, e.g., [
+        *   'href' => 'http://www.mediawiki.org/' ].  See expandAttributes() for
         *   further documentation.
         * @return array An array of attributes functionally identical to $attribs
         */
@@ -339,7 +338,6 @@ class Html {
                                'height' => '150',
                                'width' => '300',
                        ],
-                       'command' => [ 'type' => 'command' ],
                        'form' => [
                                'action' => 'GET',
                                'autocomplete' => 'on',
@@ -432,8 +430,8 @@ class Html {
 
        /**
         * Given an associative array of element attributes, generate a string
-        * to stick after the element name in HTML output.  Like array( 'href' =>
-        * 'http://www.mediawiki.org/' ) becomes something like
+        * to stick after the element name in HTML output.  Like [ 'href' =>
+        * 'http://www.mediawiki.org/' ] becomes something like
         * ' href="http://www.mediawiki.org"'.  Again, this is like
         * Xml::expandAttributes(), but it implements some HTML-specific logic.
         *
@@ -445,25 +443,25 @@ class Html {
         *
         * @par Numerical array
         * @code
-        *     Html::element( 'em', array(
-        *         'class' => array( 'foo', 'bar' )
-        *     ) );
+        *     Html::element( 'em', [
+        *         'class' => [ 'foo', 'bar' ]
+        *     ] );
         *     // gives '<em class="foo bar"></em>'
         * @endcode
         *
         * @par Associative array
         * @code
-        *     Html::element( 'em', array(
-        *         'class' => array( 'foo', 'bar', 'foo' => false, 'quux' => true )
-        *     ) );
+        *     Html::element( 'em', [
+        *         'class' => [ 'foo', 'bar', 'foo' => false, 'quux' => true ]
+        *     ] );
         *     // gives '<em class="bar quux"></em>'
         * @endcode
         *
-        * @param array $attribs Associative array of attributes, e.g., array(
-        *   'href' => 'http://www.mediawiki.org/' ).  Values will be HTML-escaped.
+        * @param array $attribs Associative array of attributes, e.g., [
+        *   'href' => 'http://www.mediawiki.org/' ].  Values will be HTML-escaped.
         *   A value of false means to omit the attribute.  For boolean attributes,
-        *   you can omit the key, e.g., array( 'checked' ) instead of
-        *   array( 'checked' => 'checked' ) or such.
+        *   you can omit the key, e.g., [ 'checked' ] instead of
+        *   [ 'checked' => 'checked' ] or such.
         *
         * @throws MWException If an attribute that doesn't allow lists is set to an array
         * @return string HTML fragment that goes between element name and '>'
@@ -472,13 +470,13 @@ class Html {
        public static function expandAttributes( array $attribs ) {
                $ret = '';
                foreach ( $attribs as $key => $value ) {
-                       // Support intuitive array( 'checked' => true/false ) form
+                       // Support intuitive [ 'checked' => true/false ] form
                        if ( $value === false || is_null( $value ) ) {
                                continue;
                        }
 
-                       // For boolean attributes, support array( 'foo' ) instead of
-                       // requiring array( 'foo' => 'meaningless' ).
+                       // For boolean attributes, support [ 'foo' ] instead of
+                       // requiring [ 'foo' => 'meaningless' ].
                        if ( is_int( $key ) && in_array( strtolower( $value ), self::$boolAttribs ) ) {
                                $key = $value;
                        }
@@ -535,7 +533,7 @@ class Html {
                                                        }
                                                } elseif ( $v ) {
                                                        // If the value is truthy but not a string this is likely
-                                                       // an array( 'foo' => true ), falsy values don't add strings
+                                                       // an [ 'foo' => true ], falsy values don't add strings
                                                        $newValue[] = $k;
                                                }
                                        }
@@ -1011,11 +1009,11 @@ class Html {
         *
         * @par Example:
         * @code
-        *     Html::srcSet( array(
+        *     Html::srcSet( [
         *         '1x'   => 'standard.jpeg',
         *         '1.5x' => 'large.jpeg',
         *         '3x'   => 'extra-large.jpeg',
-        *     ) );
+        *     ] );
         *     // gives 'standard.jpeg 1x, large.jpeg 1.5x, extra-large.jpeg 2x'
         * @endcode
         *
diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php
deleted file mode 100644 (file)
index 2ca5e1b..0000000
+++ /dev/null
@@ -1,1122 +0,0 @@
-<?php
-/**
- * Various HTTP related functions.
- *
- * 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 HTTP
- */
-
-/**
- * @defgroup HTTP HTTP
- */
-
-use MediaWiki\Logger\LoggerFactory;
-
-/**
- * Various HTTP related functions
- * @ingroup HTTP
- */
-class Http {
-       static public $httpEngine = false;
-
-       /**
-        * Perform an HTTP request
-        *
-        * @param string $method HTTP method. Usually GET/POST
-        * @param string $url Full URL to act on. If protocol-relative, will be expanded to an http:// URL
-        * @param array $options Options to pass to MWHttpRequest object.
-        *      Possible keys for the array:
-        *    - timeout             Timeout length in seconds
-        *    - connectTimeout      Timeout for connection, in seconds (curl only)
-        *    - postData            An array of key-value pairs or a url-encoded form data
-        *    - proxy               The proxy to use.
-        *                          Otherwise it will use $wgHTTPProxy (if set)
-        *                          Otherwise it will use the environment variable "http_proxy" (if set)
-        *    - noProxy             Don't use any proxy at all. Takes precedence over proxy value(s).
-        *    - sslVerifyHost       Verify hostname against certificate
-        *    - sslVerifyCert       Verify SSL certificate
-        *    - caInfo              Provide CA information
-        *    - maxRedirects        Maximum number of redirects to follow (defaults to 5)
-        *    - followRedirects     Whether to follow redirects (defaults to false).
-        *                                  Note: this should only be used when the target URL is trusted,
-        *                                  to avoid attacks on intranet services accessible by HTTP.
-        *    - userAgent           A user agent, if you want to override the default
-        *                          MediaWiki/$wgVersion
-        * @param string $caller The method making this request, for profiling
-        * @return string|bool (bool)false on failure or a string on success
-        */
-       public static function request( $method, $url, $options = [], $caller = __METHOD__ ) {
-               wfDebug( "HTTP: $method: $url\n" );
-
-               $options['method'] = strtoupper( $method );
-
-               if ( !isset( $options['timeout'] ) ) {
-                       $options['timeout'] = 'default';
-               }
-               if ( !isset( $options['connectTimeout'] ) ) {
-                       $options['connectTimeout'] = 'default';
-               }
-
-               $req = MWHttpRequest::factory( $url, $options, $caller );
-               $status = $req->execute();
-
-               if ( $status->isOK() ) {
-                       return $req->getContent();
-               } else {
-                       $errors = $status->getErrorsByType( 'error' );
-                       $logger = LoggerFactory::getInstance( 'http' );
-                       $logger->warning( $status->getWikiText( false, false, 'en' ),
-                               [ 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ] );
-                       return false;
-               }
-       }
-
-       /**
-        * Simple wrapper for Http::request( 'GET' )
-        * @see Http::request()
-        * @since 1.25 Second parameter $timeout removed. Second parameter
-        * is now $options which can be given a 'timeout'
-        *
-        * @param string $url
-        * @param array $options
-        * @param string $caller The method making this request, for profiling
-        * @return string|bool false on error
-        */
-       public static function get( $url, $options = [], $caller = __METHOD__ ) {
-               $args = func_get_args();
-               if ( isset( $args[1] ) && ( is_string( $args[1] ) || is_numeric( $args[1] ) ) ) {
-                       // Second was used to be the timeout
-                       // And third parameter used to be $options
-                       wfWarn( "Second parameter should not be a timeout.", 2 );
-                       $options = isset( $args[2] ) && is_array( $args[2] ) ?
-                               $args[2] : [];
-                       $options['timeout'] = $args[1];
-                       $caller = __METHOD__;
-               }
-               return Http::request( 'GET', $url, $options, $caller );
-       }
-
-       /**
-        * Simple wrapper for Http::request( 'POST' )
-        * @see Http::request()
-        *
-        * @param string $url
-        * @param array $options
-        * @param string $caller The method making this request, for profiling
-        * @return string|bool false on error
-        */
-       public static function post( $url, $options = [], $caller = __METHOD__ ) {
-               return Http::request( 'POST', $url, $options, $caller );
-       }
-
-       /**
-        * A standard user-agent we can use for external requests.
-        * @return string
-        */
-       public static function userAgent() {
-               global $wgVersion;
-               return "MediaWiki/$wgVersion";
-       }
-
-       /**
-        * Checks that the given URI is a valid one. Hardcoding the
-        * protocols, because we only want protocols that both cURL
-        * and php support.
-        *
-        * file:// should not be allowed here for security purpose (r67684)
-        *
-        * @todo FIXME this is wildly inaccurate and fails to actually check most stuff
-        *
-        * @param string $uri URI to check for validity
-        * @return bool
-        */
-       public static function isValidURI( $uri ) {
-               return preg_match(
-                       '/^https?:\/\/[^\/\s]\S*$/D',
-                       $uri
-               );
-       }
-
-       /**
-        * Gets the relevant proxy from $wgHTTPProxy
-        *
-        * @return mixed The proxy address or an empty string if not set.
-        */
-       public static function getProxy() {
-               global $wgHTTPProxy;
-
-               if ( $wgHTTPProxy ) {
-                       return $wgHTTPProxy;
-               }
-
-               return "";
-       }
-}
-
-/**
- * This wrapper class will call out to curl (if available) or fallback
- * to regular PHP if necessary for handling internal HTTP requests.
- *
- * Renamed from HttpRequest to MWHttpRequest to avoid conflict with
- * PHP's HTTP extension.
- */
-class MWHttpRequest {
-       const SUPPORTS_FILE_POSTS = false;
-
-       protected $content;
-       protected $timeout = 'default';
-       protected $headersOnly = null;
-       protected $postData = null;
-       protected $proxy = null;
-       protected $noProxy = false;
-       protected $sslVerifyHost = true;
-       protected $sslVerifyCert = true;
-       protected $caInfo = null;
-       protected $method = "GET";
-       protected $reqHeaders = [];
-       protected $url;
-       protected $parsedUrl;
-       protected $callback;
-       protected $maxRedirects = 5;
-       protected $followRedirects = false;
-
-       /**
-        * @var CookieJar
-        */
-       protected $cookieJar;
-
-       protected $headerList = [];
-       protected $respVersion = "0.9";
-       protected $respStatus = "200 Ok";
-       protected $respHeaders = [];
-
-       public $status;
-
-       /**
-        * @var Profiler
-        */
-       protected $profiler;
-
-       /**
-        * @var string
-        */
-       protected $profileName;
-
-       /**
-        * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL
-        * @param array $options (optional) extra params to pass (see Http::request())
-        * @param string $caller The method making this request, for profiling
-        * @param Profiler $profiler An instance of the profiler for profiling, or null
-        */
-       protected function __construct(
-               $url, $options = [], $caller = __METHOD__, $profiler = null
-       ) {
-               global $wgHTTPTimeout, $wgHTTPConnectTimeout;
-
-               $this->url = wfExpandUrl( $url, PROTO_HTTP );
-               $this->parsedUrl = wfParseUrl( $this->url );
-
-               if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) {
-                       $this->status = Status::newFatal( 'http-invalid-url', $url );
-               } else {
-                       $this->status = Status::newGood( 100 ); // continue
-               }
-
-               if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) {
-                       $this->timeout = $options['timeout'];
-               } else {
-                       $this->timeout = $wgHTTPTimeout;
-               }
-               if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) {
-                       $this->connectTimeout = $options['connectTimeout'];
-               } else {
-                       $this->connectTimeout = $wgHTTPConnectTimeout;
-               }
-               if ( isset( $options['userAgent'] ) ) {
-                       $this->setUserAgent( $options['userAgent'] );
-               }
-
-               $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
-                               "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ];
-
-               foreach ( $members as $o ) {
-                       if ( isset( $options[$o] ) ) {
-                               // ensure that MWHttpRequest::method is always
-                               // uppercased. Bug 36137
-                               if ( $o == 'method' ) {
-                                       $options[$o] = strtoupper( $options[$o] );
-                               }
-                               $this->$o = $options[$o];
-                       }
-               }
-
-               if ( $this->noProxy ) {
-                       $this->proxy = ''; // noProxy takes precedence
-               }
-
-               // Profile based on what's calling us
-               $this->profiler = $profiler;
-               $this->profileName = $caller;
-       }
-
-       /**
-        * Simple function to test if we can make any sort of requests at all, using
-        * cURL or fopen()
-        * @return bool
-        */
-       public static function canMakeRequests() {
-               return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
-       }
-
-       /**
-        * Generate a new request object
-        * @param string $url Url to use
-        * @param array $options (optional) extra params to pass (see Http::request())
-        * @param string $caller The method making this request, for profiling
-        * @throws MWException
-        * @return CurlHttpRequest|PhpHttpRequest
-        * @see MWHttpRequest::__construct
-        */
-       public static function factory( $url, $options = null, $caller = __METHOD__ ) {
-               if ( !Http::$httpEngine ) {
-                       Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
-               } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
-                       throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' .
-                               ' Http::$httpEngine is set to "curl"' );
-               }
-
-               switch ( Http::$httpEngine ) {
-                       case 'curl':
-                               return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() );
-                       case 'php':
-                               if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
-                                       throw new MWException( __METHOD__ . ': allow_url_fopen ' .
-                                               'needs to be enabled for pure PHP http requests to ' .
-                                               'work. If possible, curl should be used instead. See ' .
-                                               'http://php.net/curl.'
-                                       );
-                               }
-                               return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() );
-                       default:
-                               throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' );
-               }
-       }
-
-       /**
-        * Get the body, or content, of the response to the request
-        *
-        * @return string
-        */
-       public function getContent() {
-               return $this->content;
-       }
-
-       /**
-        * Set the parameters of the request
-        *
-        * @param array $args
-        * @todo overload the args param
-        */
-       public function setData( $args ) {
-               $this->postData = $args;
-       }
-
-       /**
-        * Take care of setting up the proxy (do nothing if "noProxy" is set)
-        *
-        * @return void
-        */
-       public function proxySetup() {
-               // If there is an explicit proxy set and proxies are not disabled, then use it
-               if ( $this->proxy && !$this->noProxy ) {
-                       return;
-               }
-
-               // Otherwise, fallback to $wgHTTPProxy if this is not a machine
-               // local URL and proxies are not disabled
-               if ( self::isLocalURL( $this->url ) || $this->noProxy ) {
-                       $this->proxy = '';
-               } else {
-                       $this->proxy = Http::getProxy();
-               }
-       }
-
-       /**
-        * Check if the URL can be served by localhost
-        *
-        * @param string $url Full url to check
-        * @return bool
-        */
-       private static function isLocalURL( $url ) {
-               global $wgCommandLineMode, $wgLocalVirtualHosts;
-
-               if ( $wgCommandLineMode ) {
-                       return false;
-               }
-
-               // Extract host part
-               $matches = [];
-               if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) {
-                       $host = $matches[1];
-                       // Split up dotwise
-                       $domainParts = explode( '.', $host );
-                       // Check if this domain or any superdomain is listed as a local virtual host
-                       $domainParts = array_reverse( $domainParts );
-
-                       $domain = '';
-                       $countParts = count( $domainParts );
-                       for ( $i = 0; $i < $countParts; $i++ ) {
-                               $domainPart = $domainParts[$i];
-                               if ( $i == 0 ) {
-                                       $domain = $domainPart;
-                               } else {
-                                       $domain = $domainPart . '.' . $domain;
-                               }
-
-                               if ( in_array( $domain, $wgLocalVirtualHosts ) ) {
-                                       return true;
-                               }
-                       }
-               }
-
-               return false;
-       }
-
-       /**
-        * Set the user agent
-        * @param string $UA
-        */
-       public function setUserAgent( $UA ) {
-               $this->setHeader( 'User-Agent', $UA );
-       }
-
-       /**
-        * Set an arbitrary header
-        * @param string $name
-        * @param string $value
-        */
-       public function setHeader( $name, $value ) {
-               // I feel like I should normalize the case here...
-               $this->reqHeaders[$name] = $value;
-       }
-
-       /**
-        * Get an array of the headers
-        * @return array
-        */
-       public function getHeaderList() {
-               $list = [];
-
-               if ( $this->cookieJar ) {
-                       $this->reqHeaders['Cookie'] =
-                               $this->cookieJar->serializeToHttpRequest(
-                                       $this->parsedUrl['path'],
-                                       $this->parsedUrl['host']
-                               );
-               }
-
-               foreach ( $this->reqHeaders as $name => $value ) {
-                       $list[] = "$name: $value";
-               }
-
-               return $list;
-       }
-
-       /**
-        * Set a read callback to accept data read from the HTTP request.
-        * By default, data is appended to an internal buffer which can be
-        * retrieved through $req->getContent().
-        *
-        * To handle data as it comes in -- especially for large files that
-        * would not fit in memory -- you can instead set your own callback,
-        * in the form function($resource, $buffer) where the first parameter
-        * is the low-level resource being read (implementation specific),
-        * and the second parameter is the data buffer.
-        *
-        * You MUST return the number of bytes handled in the buffer; if fewer
-        * bytes are reported handled than were passed to you, the HTTP fetch
-        * will be aborted.
-        *
-        * @param callable $callback
-        * @throws MWException
-        */
-       public function setCallback( $callback ) {
-               if ( !is_callable( $callback ) ) {
-                       throw new MWException( 'Invalid MwHttpRequest callback' );
-               }
-               $this->callback = $callback;
-       }
-
-       /**
-        * A generic callback to read the body of the response from a remote
-        * server.
-        *
-        * @param resource $fh
-        * @param string $content
-        * @return int
-        */
-       public function read( $fh, $content ) {
-               $this->content .= $content;
-               return strlen( $content );
-       }
-
-       /**
-        * Take care of whatever is necessary to perform the URI request.
-        *
-        * @return Status
-        */
-       public function execute() {
-
-               $this->content = "";
-
-               if ( strtoupper( $this->method ) == "HEAD" ) {
-                       $this->headersOnly = true;
-               }
-
-               $this->proxySetup(); // set up any proxy as needed
-
-               if ( !$this->callback ) {
-                       $this->setCallback( [ $this, 'read' ] );
-               }
-
-               if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
-                       $this->setUserAgent( Http::userAgent() );
-               }
-
-       }
-
-       /**
-        * Parses the headers, including the HTTP status code and any
-        * Set-Cookie headers.  This function expects the headers to be
-        * found in an array in the member variable headerList.
-        */
-       protected function parseHeader() {
-
-               $lastname = "";
-
-               foreach ( $this->headerList as $header ) {
-                       if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
-                               $this->respVersion = $match[1];
-                               $this->respStatus = $match[2];
-                       } elseif ( preg_match( "#^[ \t]#", $header ) ) {
-                               $last = count( $this->respHeaders[$lastname] ) - 1;
-                               $this->respHeaders[$lastname][$last] .= "\r\n$header";
-                       } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
-                               $this->respHeaders[strtolower( $match[1] )][] = $match[2];
-                               $lastname = strtolower( $match[1] );
-                       }
-               }
-
-               $this->parseCookies();
-
-       }
-
-       /**
-        * Sets HTTPRequest status member to a fatal value with the error
-        * message if the returned integer value of the status code was
-        * not successful (< 300) or a redirect (>=300 and < 400).  (see
-        * RFC2616, section 10,
-        * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a
-        * list of status codes.)
-        */
-       protected function setStatus() {
-               if ( !$this->respHeaders ) {
-                       $this->parseHeader();
-               }
-
-               if ( (int)$this->respStatus > 399 ) {
-                       list( $code, $message ) = explode( " ", $this->respStatus, 2 );
-                       $this->status->fatal( "http-bad-status", $code, $message );
-               }
-       }
-
-       /**
-        * Get the integer value of the HTTP status code (e.g. 200 for "200 Ok")
-        * (see RFC2616, section 10, http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
-        * for a list of status codes.)
-        *
-        * @return int
-        */
-       public function getStatus() {
-               if ( !$this->respHeaders ) {
-                       $this->parseHeader();
-               }
-
-               return (int)$this->respStatus;
-       }
-
-       /**
-        * Returns true if the last status code was a redirect.
-        *
-        * @return bool
-        */
-       public function isRedirect() {
-               if ( !$this->respHeaders ) {
-                       $this->parseHeader();
-               }
-
-               $status = (int)$this->respStatus;
-
-               if ( $status >= 300 && $status <= 303 ) {
-                       return true;
-               }
-
-               return false;
-       }
-
-       /**
-        * Returns an associative array of response headers after the
-        * request has been executed.  Because some headers
-        * (e.g. Set-Cookie) can appear more than once the, each value of
-        * the associative array is an array of the values given.
-        *
-        * @return array
-        */
-       public function getResponseHeaders() {
-               if ( !$this->respHeaders ) {
-                       $this->parseHeader();
-               }
-
-               return $this->respHeaders;
-       }
-
-       /**
-        * Returns the value of the given response header.
-        *
-        * @param string $header
-        * @return string|null
-        */
-       public function getResponseHeader( $header ) {
-               if ( !$this->respHeaders ) {
-                       $this->parseHeader();
-               }
-
-               if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
-                       $v = $this->respHeaders[strtolower( $header )];
-                       return $v[count( $v ) - 1];
-               }
-
-               return null;
-       }
-
-       /**
-        * Tells the MWHttpRequest object to use this pre-loaded CookieJar.
-        *
-        * @param CookieJar $jar
-        */
-       public function setCookieJar( $jar ) {
-               $this->cookieJar = $jar;
-       }
-
-       /**
-        * Returns the cookie jar in use.
-        *
-        * @return CookieJar
-        */
-       public function getCookieJar() {
-               if ( !$this->respHeaders ) {
-                       $this->parseHeader();
-               }
-
-               return $this->cookieJar;
-       }
-
-       /**
-        * Sets a cookie. Used before a request to set up any individual
-        * cookies. Used internally after a request to parse the
-        * Set-Cookie headers.
-        * @see Cookie::set
-        * @param string $name
-        * @param mixed $value
-        * @param array $attr
-        */
-       public function setCookie( $name, $value = null, $attr = null ) {
-               if ( !$this->cookieJar ) {
-                       $this->cookieJar = new CookieJar;
-               }
-
-               $this->cookieJar->setCookie( $name, $value, $attr );
-       }
-
-       /**
-        * Parse the cookies in the response headers and store them in the cookie jar.
-        */
-       protected function parseCookies() {
-
-               if ( !$this->cookieJar ) {
-                       $this->cookieJar = new CookieJar;
-               }
-
-               if ( isset( $this->respHeaders['set-cookie'] ) ) {
-                       $url = parse_url( $this->getFinalUrl() );
-                       foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
-                               $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
-                       }
-               }
-
-       }
-
-       /**
-        * Returns the final URL after all redirections.
-        *
-        * Relative values of the "Location" header are incorrect as
-        * stated in RFC, however they do happen and modern browsers
-        * support them.  This function loops backwards through all
-        * locations in order to build the proper absolute URI - Marooned
-        * at wikia-inc.com
-        *
-        * Note that the multiple Location: headers are an artifact of
-        * CURL -- they shouldn't actually get returned this way. Rewrite
-        * this when bug 29232 is taken care of (high-level redirect
-        * handling rewrite).
-        *
-        * @return string
-        */
-       public function getFinalUrl() {
-               $headers = $this->getResponseHeaders();
-
-               // return full url (fix for incorrect but handled relative location)
-               if ( isset( $headers['location'] ) ) {
-                       $locations = $headers['location'];
-                       $domain = '';
-                       $foundRelativeURI = false;
-                       $countLocations = count( $locations );
-
-                       for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
-                               $url = parse_url( $locations[$i] );
-
-                               if ( isset( $url['host'] ) ) {
-                                       $domain = $url['scheme'] . '://' . $url['host'];
-                                       break; // found correct URI (with host)
-                               } else {
-                                       $foundRelativeURI = true;
-                               }
-                       }
-
-                       if ( $foundRelativeURI ) {
-                               if ( $domain ) {
-                                       return $domain . $locations[$countLocations - 1];
-                               } else {
-                                       $url = parse_url( $this->url );
-                                       if ( isset( $url['host'] ) ) {
-                                               return $url['scheme'] . '://' . $url['host'] .
-                                                       $locations[$countLocations - 1];
-                                       }
-                               }
-                       } else {
-                               return $locations[$countLocations - 1];
-                       }
-               }
-
-               return $this->url;
-       }
-
-       /**
-        * Returns true if the backend can follow redirects. Overridden by the
-        * child classes.
-        * @return bool
-        */
-       public function canFollowRedirects() {
-               return true;
-       }
-}
-
-/**
- * MWHttpRequest implemented using internal curl compiled into PHP
- */
-class CurlHttpRequest extends MWHttpRequest {
-       const SUPPORTS_FILE_POSTS = true;
-
-       protected $curlOptions = [];
-       protected $headerText = "";
-
-       /**
-        * @param resource $fh
-        * @param string $content
-        * @return int
-        */
-       protected function readHeader( $fh, $content ) {
-               $this->headerText .= $content;
-               return strlen( $content );
-       }
-
-       public function execute() {
-
-               parent::execute();
-
-               if ( !$this->status->isOK() ) {
-                       return $this->status;
-               }
-
-               $this->curlOptions[CURLOPT_PROXY] = $this->proxy;
-               $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout;
-
-               // Only supported in curl >= 7.16.2
-               if ( defined( 'CURLOPT_CONNECTTIMEOUT_MS' ) ) {
-                       $this->curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = $this->connectTimeout * 1000;
-               }
-
-               $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
-               $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
-               $this->curlOptions[CURLOPT_HEADERFUNCTION] = [ $this, "readHeader" ];
-               $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects;
-               $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression
-
-               $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent'];
-
-               $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0;
-               $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert;
-
-               if ( $this->caInfo ) {
-                       $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo;
-               }
-
-               if ( $this->headersOnly ) {
-                       $this->curlOptions[CURLOPT_NOBODY] = true;
-                       $this->curlOptions[CURLOPT_HEADER] = true;
-               } elseif ( $this->method == 'POST' ) {
-                       $this->curlOptions[CURLOPT_POST] = true;
-                       $postData = $this->postData;
-                       // Don't interpret POST parameters starting with '@' as file uploads, because this
-                       // makes it impossible to POST plain values starting with '@' (and causes security
-                       // issues potentially exposing the contents of local files).
-                       // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6,
-                       // but we support lower versions, and the option doesn't exist in HHVM 5.6.99.
-                       if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) {
-                               $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true;
-                       } elseif ( is_array( $postData ) ) {
-                               // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
-                               // is an array, but not if it's a string. So convert $req['body'] to a string
-                               // for safety.
-                               $postData = wfArrayToCgi( $postData );
-                       }
-                       $this->curlOptions[CURLOPT_POSTFIELDS] = $postData;
-
-                       // Suppress 'Expect: 100-continue' header, as some servers
-                       // will reject it with a 417 and Curl won't auto retry
-                       // with HTTP 1.0 fallback
-                       $this->reqHeaders['Expect'] = '';
-               } else {
-                       $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method;
-               }
-
-               $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList();
-
-               $curlHandle = curl_init( $this->url );
-
-               if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) {
-                       throw new MWException( "Error setting curl options." );
-               }
-
-               if ( $this->followRedirects && $this->canFollowRedirects() ) {
-                       MediaWiki\suppressWarnings();
-                       if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) {
-                               wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " .
-                                       "Probably open_basedir is set.\n" );
-                               // Continue the processing. If it were in curl_setopt_array,
-                               // processing would have halted on its entry
-                       }
-                       MediaWiki\restoreWarnings();
-               }
-
-               if ( $this->profiler ) {
-                       $profileSection = $this->profiler->scopedProfileIn(
-                               __METHOD__ . '-' . $this->profileName
-                       );
-               }
-
-               $curlRes = curl_exec( $curlHandle );
-               if ( curl_errno( $curlHandle ) == CURLE_OPERATION_TIMEOUTED ) {
-                       $this->status->fatal( 'http-timed-out', $this->url );
-               } elseif ( $curlRes === false ) {
-                       $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) );
-               } else {
-                       $this->headerList = explode( "\r\n", $this->headerText );
-               }
-
-               curl_close( $curlHandle );
-
-               if ( $this->profiler ) {
-                       $this->profiler->scopedProfileOut( $profileSection );
-               }
-
-               $this->parseHeader();
-               $this->setStatus();
-
-               return $this->status;
-       }
-
-       /**
-        * @return bool
-        */
-       public function canFollowRedirects() {
-               $curlVersionInfo = curl_version();
-               if ( $curlVersionInfo['version_number'] < 0x071304 ) {
-                       wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" );
-                       return false;
-               }
-
-               if ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
-                       if ( strval( ini_get( 'open_basedir' ) ) !== '' ) {
-                               wfDebug( "Cannot follow redirects when open_basedir is set\n" );
-                               return false;
-                       }
-               }
-
-               return true;
-       }
-}
-
-class PhpHttpRequest extends MWHttpRequest {
-
-       private $fopenErrors = [];
-
-       /**
-        * @param string $url
-        * @return string
-        */
-       protected function urlToTcp( $url ) {
-               $parsedUrl = parse_url( $url );
-
-               return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
-       }
-
-       /**
-        * Returns an array with a 'capath' or 'cafile' key
-        * that is suitable to be merged into the 'ssl' sub-array of
-        * a stream context options array.
-        * Uses the 'caInfo' option of the class if it is provided, otherwise uses the system
-        * default CA bundle if PHP supports that, or searches a few standard locations.
-        * @return array
-        * @throws DomainException
-        */
-       protected function getCertOptions() {
-               $certOptions = [];
-               $certLocations = [];
-               if ( $this->caInfo ) {
-                       $certLocations = [ 'manual' => $this->caInfo ];
-               } elseif ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
-                       // @codingStandardsIgnoreStart Generic.Files.LineLength
-                       // Default locations, based on
-                       // https://www.happyassassin.net/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/
-                       // PHP 5.5 and older doesn't have any defaults, so we try to guess ourselves.
-                       // PHP 5.6+ gets the CA location from OpenSSL as long as it is not set manually,
-                       // so we should leave capath/cafile empty there.
-                       // @codingStandardsIgnoreEnd
-                       $certLocations = array_filter( [
-                               getenv( 'SSL_CERT_DIR' ),
-                               getenv( 'SSL_CERT_PATH' ),
-                               '/etc/pki/tls/certs/ca-bundle.crt', # Fedora et al
-                               '/etc/ssl/certs',  # Debian et al
-                               '/etc/pki/tls/certs/ca-bundle.trust.crt',
-                               '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
-                               '/System/Library/OpenSSL', # OSX
-                       ] );
-               }
-
-               foreach ( $certLocations as $key => $cert ) {
-                       if ( is_dir( $cert ) ) {
-                               $certOptions['capath'] = $cert;
-                               break;
-                       } elseif ( is_file( $cert ) ) {
-                               $certOptions['cafile'] = $cert;
-                               break;
-                       } elseif ( $key === 'manual' ) {
-                               // fail more loudly if a cert path was manually configured and it is not valid
-                               throw new DomainException( "Invalid CA info passed: $cert" );
-                       }
-               }
-
-               return $certOptions;
-       }
-
-       /**
-        * Custom error handler for dealing with fopen() errors.
-        * fopen() tends to fire multiple errors in succession, and the last one
-        * is completely useless (something like "fopen: failed to open stream")
-        * so normal methods of handling errors programmatically
-        * like get_last_error() don't work.
-        */
-       public function errorHandler( $errno, $errstr ) {
-               $n = count( $this->fopenErrors ) + 1;
-               $this->fopenErrors += [ "errno$n" => $errno, "errstr$n" => $errstr ];
-       }
-
-       public function execute() {
-
-               parent::execute();
-
-               if ( is_array( $this->postData ) ) {
-                       $this->postData = wfArrayToCgi( $this->postData );
-               }
-
-               if ( $this->parsedUrl['scheme'] != 'http'
-                       && $this->parsedUrl['scheme'] != 'https' ) {
-                       $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
-               }
-
-               $this->reqHeaders['Accept'] = "*/*";
-               $this->reqHeaders['Connection'] = 'Close';
-               if ( $this->method == 'POST' ) {
-                       // Required for HTTP 1.0 POSTs
-                       $this->reqHeaders['Content-Length'] = strlen( $this->postData );
-                       if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
-                               $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded";
-                       }
-               }
-
-               // Set up PHP stream context
-               $options = [
-                       'http' => [
-                               'method' => $this->method,
-                               'header' => implode( "\r\n", $this->getHeaderList() ),
-                               'protocol_version' => '1.1',
-                               'max_redirects' => $this->followRedirects ? $this->maxRedirects : 0,
-                               'ignore_errors' => true,
-                               'timeout' => $this->timeout,
-                               // Curl options in case curlwrappers are installed
-                               'curl_verify_ssl_host' => $this->sslVerifyHost ? 2 : 0,
-                               'curl_verify_ssl_peer' => $this->sslVerifyCert,
-                       ],
-                       'ssl' => [
-                               'verify_peer' => $this->sslVerifyCert,
-                               'SNI_enabled' => true,
-                               'ciphers' => 'HIGH:!SSLv2:!SSLv3:-ADH:-kDH:-kECDH:-DSS',
-                               'disable_compression' => true,
-                       ],
-               ];
-
-               if ( $this->proxy ) {
-                       $options['http']['proxy'] = $this->urlToTcp( $this->proxy );
-                       $options['http']['request_fulluri'] = true;
-               }
-
-               if ( $this->postData ) {
-                       $options['http']['content'] = $this->postData;
-               }
-
-               if ( $this->sslVerifyHost ) {
-                       // PHP 5.6.0 deprecates CN_match, in favour of peer_name which
-                       // actually checks SubjectAltName properly.
-                       if ( version_compare( PHP_VERSION, '5.6.0', '>=' ) ) {
-                               $options['ssl']['peer_name'] = $this->parsedUrl['host'];
-                       } else {
-                               $options['ssl']['CN_match'] = $this->parsedUrl['host'];
-                       }
-               }
-
-               $options['ssl'] += $this->getCertOptions();
-
-               $context = stream_context_create( $options );
-
-               $this->headerList = [];
-               $reqCount = 0;
-               $url = $this->url;
-
-               $result = [];
-
-               if ( $this->profiler ) {
-                       $profileSection = $this->profiler->scopedProfileIn(
-                               __METHOD__ . '-' . $this->profileName
-                       );
-               }
-               do {
-                       $reqCount++;
-                       $this->fopenErrors = [];
-                       set_error_handler( [ $this, 'errorHandler' ] );
-                       $fh = fopen( $url, "r", false, $context );
-                       restore_error_handler();
-
-                       if ( !$fh ) {
-                               // HACK for instant commons.
-                               // If we are contacting (commons|upload).wikimedia.org
-                               // try again with CN_match for en.wikipedia.org
-                               // as php does not handle SubjectAltName properly
-                               // prior to "peer_name" option in php 5.6
-                               if ( isset( $options['ssl']['CN_match'] )
-                                       && ( $options['ssl']['CN_match'] === 'commons.wikimedia.org'
-                                               || $options['ssl']['CN_match'] === 'upload.wikimedia.org' )
-                               ) {
-                                       $options['ssl']['CN_match'] = 'en.wikipedia.org';
-                                       $context = stream_context_create( $options );
-                                       continue;
-                               }
-                               break;
-                       }
-
-                       $result = stream_get_meta_data( $fh );
-                       $this->headerList = $result['wrapper_data'];
-                       $this->parseHeader();
-
-                       if ( !$this->followRedirects ) {
-                               break;
-                       }
-
-                       # Handle manual redirection
-                       if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) {
-                               break;
-                       }
-                       # Check security of URL
-                       $url = $this->getResponseHeader( "Location" );
-
-                       if ( !Http::isValidURI( $url ) ) {
-                               wfDebug( __METHOD__ . ": insecure redirection\n" );
-                               break;
-                       }
-               } while ( true );
-               if ( $this->profiler ) {
-                       $this->profiler->scopedProfileOut( $profileSection );
-               }
-
-               $this->setStatus();
-
-               if ( $fh === false ) {
-                       if ( $this->fopenErrors ) {
-                               LoggerFactory::getInstance( 'http' )->warning( __CLASS__
-                                       . ': error opening connection: {errstr1}', $this->fopenErrors );
-                       }
-                       $this->status->fatal( 'http-request-error' );
-                       return $this->status;
-               }
-
-               if ( $result['timed_out'] ) {
-                       $this->status->fatal( 'http-timed-out', $this->url );
-                       return $this->status;
-               }
-
-               // If everything went OK, or we received some error code
-               // get the response body content.
-               if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) {
-                       while ( !feof( $fh ) ) {
-                               $buf = fread( $fh, 8192 );
-
-                               if ( $buf === false ) {
-                                       $this->status->fatal( 'http-read-error' );
-                                       break;
-                               }
-
-                               if ( strlen( $buf ) ) {
-                                       call_user_func( $this->callback, $fh, $buf );
-                               }
-                       }
-               }
-               fclose( $fh );
-
-               return $this->status;
-       }
-}
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 201e9b6..c1e5cc4 100644 (file)
@@ -28,7 +28,7 @@
  *
  * @since 1.20
  */
-class MWTimestamp extends ConvertableTimestamp {
+class MWTimestamp extends ConvertibleTimestamp {
        /**
         * Get a timestamp instance in GMT
         *
index 2fce08c..8cf009f 100644 (file)
@@ -529,11 +529,12 @@ class MediaWiki {
                        }
                } catch ( Exception $e ) {
                        $context = $this->context;
+                       $action = $context->getRequest()->getVal( 'action', 'view' );
                        if (
                                $e instanceof DBConnectionError &&
                                $context->hasTitle() &&
                                $context->getTitle()->canExist() &&
-                               $context->getRequest()->getVal( 'action', 'view' ) === 'view' &&
+                               in_array( $action, [ 'view', 'history' ], true ) &&
                                HTMLFileCache::useFileCache( $this->context, HTMLFileCache::MODE_OUTAGE )
                        ) {
                                // Try to use any (even stale) file during outages...
index f621867..f91bbae 100644 (file)
@@ -3,6 +3,7 @@ namespace MediaWiki;
 
 use Config;
 use ConfigFactory;
+use CryptRand;
 use EventRelayerGroup;
 use GenderCache;
 use GlobalVarConfig;
@@ -19,6 +20,7 @@ use MediaWiki\Services\ServiceContainer;
 use MediaWiki\Services\NoSuchServiceException;
 use MWException;
 use ObjectCache;
+use ProxyLookup;
 use SearchEngine;
 use SearchEngineConfig;
 use SearchEngineFactory;
@@ -521,6 +523,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'WatchedItemQueryService' );
        }
 
+       /**
+        * @since 1.28
+        * @return CryptRand
+        */
+       public function getCryptRand() {
+               return $this->getService( 'CryptRand' );
+       }
+
        /**
         * @since 1.28
         * @return MediaHandlerFactory
@@ -529,6 +539,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'MediaHandlerFactory' );
        }
 
+       /**
+        * @since 1.28
+        * @return ProxyLookup
+        */
+       public function getProxyLookup() {
+               return $this->getService( 'ProxyLookup' );
+       }
+
        /**
         * @since 1.28
         * @return GenderCache
@@ -580,6 +598,30 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'TitleParser' );
        }
 
+       /**
+        * @since 1.28
+        * @return \BagOStuff
+        */
+       public function getMainObjectStash() {
+               return $this->getService( 'MainObjectStash' );
+       }
+
+       /**
+        * @since 1.28
+        * @return \WANObjectCache
+        */
+       public function getMainWANObjectCache() {
+               return $this->getService( 'MainWANObjectCache' );
+       }
+
+       /**
+        * @since 1.28
+        * @return \BagOStuff
+        */
+       public function getLocalServerObjectCache() {
+               return $this->getService( 'LocalServerObjectCache' );
+       }
+
        /**
         * @since 1.28
         * @return VirtualRESTServiceClient
index dd1fd37..f797fe3 100644 (file)
@@ -42,7 +42,7 @@ class MergeHistory {
        /** @var Title Page to which history will be merged */
        protected $dest;
 
-       /** @var DatabaseBase Database that we are using */
+       /** @var IDatabase Database that we are using */
        protected $dbw;
 
        /** @var MWTimestamp Maximum timestamp that we can use (oldest timestamp of dest) */
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 a432d44..54d58d2 100644 (file)
@@ -863,10 +863,8 @@ class MimeMagic {
                        $mime = "application/x-opc+zip";
                        # TODO: remove the block below, as soon as improveTypeFromExtension is used everywhere
                        if ( $ext !== true && $ext !== false ) {
-                               /** This is the mode used by getPropsFromPath
-                                * These MIME's are stored in the database, where we don't really want
-                                * x-opc+zip, because we use it only for internal purposes
-                                */
+                               // These MIME's are stored in the database, where we don't really want
+                               // x-opc+zip, because we use it only for internal purposes
                                if ( $this->isMatchingExtension( $ext, $mime ) ) {
                                        /* A known file extension for an OPC file,
                                         * find the proper mime type for that file extension
@@ -1046,6 +1044,7 @@ class MimeMagic {
                        }
                }
 
+               $type = null;
                // Check for entry for full MIME type
                if ( $mime ) {
                        $type = $this->findMediaType( $mime );
index d17f234..8ee562a 100644 (file)
@@ -131,6 +131,14 @@ class MovePage {
                                ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
                                ContentHandler::getLocalizedName( $this->newTitle->getContentModel() )
                        );
+               } elseif (
+                       !ContentHandler::getForTitle( $this->oldTitle )->canBeUsedOn( $this->newTitle )
+               ) {
+                       $status->fatal(
+                               'content-not-allowed-here',
+                               ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
+                               $this->newTitle->getPrefixedText()
+                       );
                }
 
                // Image-specific checks
@@ -446,7 +454,7 @@ class MovePage {
                        $status = $newpage->doDeleteArticleReal(
                                $overwriteMessage,
                                /* $suppress */ false,
-                               $nt->getArticleId(),
+                               $nt->getArticleID(),
                                /* $commit */ false,
                                $errs,
                                $user
index ba14b99..a69c0e6 100644 (file)
@@ -67,13 +67,6 @@ class OutputPage extends ContextSource {
         */
        public $mBodytext = '';
 
-       /**
-        * Holds the debug lines that will be output as comments in page source if
-        * $wgDebugComments is enabled. See also $wgShowDebug.
-        * @deprecated since 1.20; use MWDebug class instead.
-        */
-       public $mDebugtext = '';
-
        /** @var string Stores contents of "<title>" tag */
        private $mHTMLtitle = '';
 
@@ -2222,6 +2215,8 @@ class OutputPage extends ContextSource {
         * @throws MWException
         */
        public function output( $return = false ) {
+               global $wgContLang;
+
                if ( $this->mDoNothing ) {
                        return $return ? '' : null;
                }
@@ -2268,7 +2263,7 @@ class OutputPage extends ContextSource {
                ob_start();
 
                $response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
-               $response->header( 'Content-language: ' . $config->get( 'ContLang' )->getHtmlCode() );
+               $response->header( 'Content-language: ' . $wgContLang->getHtmlCode() );
 
                // Avoid Internet Explorer "compatibility view" in IE 8-10, so that
                // jQuery etc. can work correctly.
@@ -2363,7 +2358,7 @@ class OutputPage extends ContextSource {
         * Output a standard error page
         *
         * showErrorPage( 'titlemsg', 'pagetextmsg' );
-        * showErrorPage( 'titlemsg', 'pagetextmsg', array( 'param1', 'param2' ) );
+        * showErrorPage( 'titlemsg', 'pagetextmsg', [ 'param1', 'param2' ] );
         * showErrorPage( 'titlemsg', $messageObject );
         * showErrorPage( $titleMessageObject, $messageObject );
         *
@@ -3057,8 +3052,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 ed06935..d09e1fb 100644 (file)
@@ -19,6 +19,7 @@
  *
  * @file
  */
+use Wikimedia\ScopedCallback;
 
 /**
  * Gives access to properties of a page.
index 49e596d..f6c4147 100644 (file)
@@ -57,35 +57,55 @@ abstract class PrefixSearch {
                if ( $search == '' ) {
                        return []; // Return empty result
                }
-               $namespaces = $this->validateNamespaces( $namespaces );
-
-               // Find a Title which is not an interwiki and is in NS_MAIN
-               $title = Title::newFromText( $search );
-               if ( $title && !$title->isExternal() ) {
-                       $ns = [ $title->getNamespace() ];
-                       $search = $title->getText();
-                       if ( $ns[0] == NS_MAIN ) {
-                               $ns = $namespaces; // no explicit prefix, use default namespaces
-                               Hooks::run( 'PrefixSearchExtractNamespace', [ &$ns, &$search ] );
-                       }
-                       return $this->searchBackend( $ns, $search, $limit, $offset );
-               }
 
-               // Is this a namespace prefix?
-               $title = Title::newFromText( $search . 'Dummy' );
-               if ( $title && $title->getText() == 'Dummy'
-                       && $title->getNamespace() != NS_MAIN
-                       && !$title->isExternal() )
-               {
-                       $namespaces = [ $title->getNamespace() ];
-                       $search = '';
+               $hasNamespace = $this->extractNamespace( $search );
+               if ( $hasNamespace ) {
+                       list( $namespace, $search ) = $hasNamespace;
+                       $namespaces = [ $namespace ];
                } else {
+                       $namespaces = $this->validateNamespaces( $namespaces );
                        Hooks::run( 'PrefixSearchExtractNamespace', [ &$namespaces, &$search ] );
                }
 
                return $this->searchBackend( $namespaces, $search, $limit, $offset );
        }
 
+       /**
+        * Figure out if given input contains an explicit namespace.
+        *
+        * @param string $input
+        * @return false|array Array of namespace and remaining text, or false if no namespace given.
+        */
+       protected function extractNamespace( $input ) {
+               if ( strpos( $input, ':' ) === false ) {
+                       return false;
+               }
+
+               // Namespace prefix only
+               $title = Title::newFromText( $input . 'Dummy' );
+               if (
+                       $title &&
+                       $title->getText() === 'Dummy' &&
+                       !$title->inNamespace( NS_MAIN ) &&
+                       !$title->isExternal()
+               ) {
+                       return [ $title->getNamespace(), '' ];
+               }
+
+               // Namespace prefix with additional input
+               $title = Title::newFromText( $input );
+               if (
+                       $title &&
+                       !$title->inNamespace( NS_MAIN ) &&
+                       !$title->isExternal()
+               ) {
+                       // getText provides correct capitalization
+                       return [ $title->getNamespace(), $title->getText() ];
+               }
+
+               return false;
+       }
+
        /**
         * Do a prefix search for all possible variants of the prefix
         * @param string $search
@@ -162,12 +182,20 @@ abstract class PrefixSearch {
                ) ) {
                        return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit, $offset ) );
                }
-               return $this->strings( $this->handleResultFromHook( $srchres, $namespaces, $search, $limit ) );
+               return $this->strings(
+                       $this->handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) );
        }
 
-       private function handleResultFromHook( $srchres, $namespaces, $search, $limit ) {
-               $rescorer = new SearchExactMatchRescorer();
-               return $rescorer->rescore( $search, $namespaces, $srchres, $limit );
+       private function handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) {
+               if ( $offset === 0 ) {
+                       // Only perform exact db match if offset === 0
+                       // This is still far from perfect but at least we avoid returning the
+                       // same title afain and again when the user is scrolling with a query
+                       // that matches a title in the db.
+                       $rescorer = new SearchExactMatchRescorer();
+                       $srchres = $rescorer->rescore( $search, $namespaces, $srchres, $limit );
+               }
+               return $srchres;
        }
 
        /**
@@ -254,43 +282,60 @@ abstract class PrefixSearch {
         * be automatically capitalized by Title::secureAndSpit()
         * later on depending on $wgCapitalLinks)
         *
-        * @param array $namespaces Namespaces to search in
+        * @param array|null $namespaces Namespaces to search in
         * @param string $search Term
         * @param int $limit Max number of items to return
         * @param int $offset Number of items to skip
-        * @return array Array of Title objects
+        * @return Title[] Array of Title objects
         */
        public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
-               $ns = array_shift( $namespaces ); // support only one namespace
-               if ( is_null( $ns ) || in_array( NS_MAIN, $namespaces ) ) {
-                       $ns = NS_MAIN; // if searching on many always default to main
+               // Backwards compatability with old code. Default to NS_MAIN if no namespaces provided.
+               if ( $namespaces === null ) {
+                       $namespaces = [];
                }
+               if ( !$namespaces ) {
+                       $namespaces[] = NS_MAIN;
+               }
+
+               // Construct suitable prefix for each namespace. They differ in cases where
+               // some namespaces always capitalize and some don't.
+               $prefixes = [];
+               foreach ( $namespaces as $namespace ) {
+                       // For now, if special is included, ignore the other namespaces
+                       if ( $namespace == NS_SPECIAL ) {
+                               return $this->specialSearch( $search, $limit, $offset );
+                       }
 
-               if ( $ns == NS_SPECIAL ) {
-                       return $this->specialSearch( $search, $limit, $offset );
+                       $title = Title::makeTitleSafe( $namespace, $search );
+                       // Why does the prefix default to empty?
+                       $prefix = $title ? $title->getDBkey() : '';
+                       $prefixes[$prefix][] = $namespace;
                }
 
-               $t = Title::newFromText( $search, $ns );
-               $prefix = $t ? $t->getDBkey() : '';
                $dbr = wfGetDB( DB_REPLICA );
-               $res = $dbr->select( 'page',
-                       [ 'page_id', 'page_namespace', 'page_title' ],
-                       [
-                               'page_namespace' => $ns,
-                               'page_title ' . $dbr->buildLike( $prefix, $dbr->anyString() )
-                       ],
-                       __METHOD__,
-                       [
-                               'LIMIT' => $limit,
-                               'ORDER BY' => 'page_title',
-                               'OFFSET' => $offset
-                       ]
-               );
-               $srchres = [];
-               foreach ( $res as $row ) {
-                       $srchres[] = Title::newFromRow( $row );
+               // Often there is only one prefix that applies to all requested namespaces,
+               // but sometimes there are two if some namespaces do not always capitalize.
+               $conds = [];
+               foreach ( $prefixes as $prefix => $namespaces ) {
+                       $condition = [
+                               'page_namespace' => $namespaces,
+                               'page_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
+                       ];
+                       $conds[] = $dbr->makeList( $condition, LIST_AND );
                }
-               return $srchres;
+
+               $table = 'page';
+               $fields = [ 'page_id', 'page_namespace', 'page_title' ];
+               $conds = $dbr->makeList( $conds, LIST_OR );
+               $options = [
+                       'LIMIT' => $limit,
+                       'ORDER BY' => [ 'page_title', 'page_namespace' ],
+                       'OFFSET' => $offset
+               ];
+
+               $res = $dbr->select( $table, $fields, $conds, __METHOD__, $options );
+
+               return iterator_to_array( TitleArray::newFromResult( $res ) );
        }
 
        /**
diff --git a/includes/ProxyLookup.php b/includes/ProxyLookup.php
new file mode 100644 (file)
index 0000000..3a3243a
--- /dev/null
@@ -0,0 +1,85 @@
+<?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
+ */
+
+use IPSet\IPSet;
+
+/**
+ * @since 1.28
+ */
+class ProxyLookup {
+
+       /**
+        * @var string[]
+        */
+       private $proxyServers;
+
+       /**
+        * @var string[]
+        */
+       private $proxyServersComplex;
+
+       /**
+        * @var IPSet|null
+        */
+       private $proxyIPSet;
+
+       /**
+        * @param string[] $proxyServers Simple list of IPs
+        * @param string[] $proxyServersComplex Complex list of IPs/ranges
+        */
+       public function __construct( $proxyServers, $proxyServersComplex ) {
+               $this->proxyServers = $proxyServers;
+               $this->proxyServersComplex = $proxyServersComplex;
+       }
+
+       /**
+        * Checks if an IP matches a proxy we've configured
+        *
+        * @param string $ip
+        * @return bool
+        */
+       public function isConfiguredProxy( $ip ) {
+               // Quick check of known singular proxy servers
+               if ( in_array( $ip, $this->proxyServers ) ) {
+                       return true;
+               }
+
+               // Check against addresses and CIDR nets in the complex list
+               if ( !$this->proxyIPSet ) {
+                       $this->proxyIPSet = new IPSet( $this->proxyServersComplex );
+               }
+               return $this->proxyIPSet->match( $ip );
+       }
+
+       /**
+        * Checks if an IP is a trusted proxy provider.
+        * Useful to tell if X-Forwarded-For data is possibly bogus.
+        * CDN cache servers for the site are whitelisted.
+        *
+        * @param string $ip
+        * @return bool
+        */
+       public function isTrustedProxy( $ip ) {
+               $trusted = $this->isConfiguredProxy( $ip );
+               Hooks::run( 'IsTrustedProxy', [ &$ip, &$trusted ] );
+               return $trusted;
+       }
+}
index 6acc528..208652f 100644 (file)
@@ -1046,11 +1046,10 @@ class Revision implements IDBAccessObject {
         *   to the $audience parameter
         *
         * @deprecated since 1.21, use getContent() instead
-        * @todo Replace usage in core
         * @return string
         */
        public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
 
                $content = $this->getContent( $audience, $user );
                return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
@@ -1396,6 +1395,11 @@ class Revision implements IDBAccessObject {
        public function insertOn( $dbw ) {
                global $wgDefaultExternalStore, $wgContentHandlerUseDB;
 
+               // We're inserting a new revision, so we have to use master anyway.
+               // If it's a null revision, it may have references to rows that
+               // are not in the replica yet (the text row).
+               $this->mQueryFlags |= self::READ_LATEST;
+
                // Not allowed to have rev_page equal to 0, false, etc.
                if ( !$this->mPage ) {
                        $title = $this->getTitle();
index 8f1fc99..4069658 100644 (file)
@@ -1015,6 +1015,7 @@ class Sanitizer {
                                | url\s*\(
                                | image\s*\(
                                | image-set\s*\(
+                               | attr\s*\([^)]+[\s,]+url
                        !ix', $value ) ) {
                        return '/* insecure input */';
                }
@@ -1867,7 +1868,7 @@ class Sanitizer {
                        list( /* $whole */, $protocol, $host, $rest ) = $matches;
 
                        // Characters that will be ignored in IDNs.
-                       // http://tools.ietf.org/html/3454#section-3.1
+                       // https://tools.ietf.org/html/rfc3454#section-3.1
                        // Strip them before further processing so blacklists and such work.
                        $strip = "/
                                \\s|          # general whitespace
index 4ab412e..42b75f0 100644 (file)
 
 use MediaWiki\Interwiki\ClassicInterwikiLookup;
 use MediaWiki\Linker\LinkRendererFactory;
+use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
 
 return [
        'DBLoadBalancerFactory' => function( MediaWikiServices $services ) {
-               $config = $services->getMainConfig()->get( 'LBFactoryConf' );
+               $mainConfig = $services->getMainConfig();
 
-               $class = LBFactoryMW::getLBFactoryClass( $config );
-               if ( !isset( $config['readOnlyReason'] ) ) {
-                       // TODO: replace the global wfConfiguredReadOnlyReason() with a service.
-                       $config['readOnlyReason'] = wfConfiguredReadOnlyReason();
-               }
+               $lbConf = MWLBFactory::applyDefaultConfig(
+                       $mainConfig->get( 'LBFactoryConf' ),
+                       $mainConfig
+               );
+               $class = MWLBFactory::getLBFactoryClass( $lbConf );
 
-               return new $class( $config );
+               return new $class( $lbConf );
        },
 
        'DBLoadBalancer' => function( MediaWikiServices $services ) {
@@ -158,12 +159,45 @@ return [
                return new WatchedItemQueryService( $services->getDBLoadBalancer() );
        },
 
+       'CryptRand' => function( MediaWikiServices $services ) {
+               $secretKey = $services->getMainConfig()->get( 'SecretKey' );
+               return new CryptRand(
+                       [
+                               // To try vary the system information of the state a bit more
+                               // by including the system's hostname into the state
+                               'wfHostname',
+                               // It's mostly worthless but throw the wiki's id into the data
+                               // for a little more variance
+                               'wfWikiID',
+                               // If we have a secret key set then throw it into the state as well
+                               function() use ( $secretKey ) {
+                                       return $secretKey ?: '';
+                               }
+                       ],
+                       // The config file is likely the most often edited file we know should
+                       // be around so include its stat info into the state.
+                       // The constant with its location will almost always be defined, as
+                       // WebStart.php defines MW_CONFIG_FILE to $IP/LocalSettings.php unless
+                       // being configured with MW_CONFIG_CALLBACK (e.g. the installer).
+                       defined( 'MW_CONFIG_FILE' ) ? [ MW_CONFIG_FILE ] : [],
+                       LoggerFactory::getInstance( 'CryptRand' )
+               );
+       },
+
        'MediaHandlerFactory' => function( MediaWikiServices $services ) {
                return new MediaHandlerFactory(
                        $services->getMainConfig()->get( 'MediaHandlers' )
                );
        },
 
+       'ProxyLookup' => function( MediaWikiServices $services ) {
+               $mainConfig = $services->getMainConfig();
+               return new ProxyLookup(
+                       $mainConfig->get( 'SquidServers' ),
+                       $mainConfig->get( 'SquidServersNoPurge' )
+               );
+       },
+
        'LinkCache' => function( MediaWikiServices $services ) {
                return new LinkCache(
                        $services->getTitleFormatter(),
@@ -210,6 +244,61 @@ return [
                return $services->getService( '_MediaWikiTitleCodec' );
        },
 
+       'MainObjectStash' => function( MediaWikiServices $services ) {
+               $mainConfig = $services->getMainConfig();
+
+               $id = $mainConfig->get( 'MainStash' );
+               if ( !isset( $mainConfig->get( 'ObjectCaches' )[$id] ) ) {
+                       throw new UnexpectedValueException(
+                               "Cache type \"$id\" is not present in \$wgObjectCaches." );
+               }
+
+               return \ObjectCache::newFromParams( $mainConfig->get( 'ObjectCaches' )[$id] );
+       },
+
+       'MainWANObjectCache' => function( MediaWikiServices $services ) {
+               $mainConfig = $services->getMainConfig();
+
+               $id = $mainConfig->get( 'MainWANCache' );
+               if ( !isset( $mainConfig->get( 'WANObjectCaches' )[$id] ) ) {
+                       throw new UnexpectedValueException(
+                               "WAN cache type \"$id\" is not present in \$wgWANObjectCaches." );
+               }
+
+               $params = $mainConfig->get( 'WANObjectCaches' )[$id];
+               $objectCacheId = $params['cacheId'];
+               if ( !isset( $mainConfig->get( 'ObjectCaches' )[$objectCacheId] ) ) {
+                       throw new UnexpectedValueException(
+                               "Cache type \"$objectCacheId\" is not present in \$wgObjectCaches." );
+               }
+               $params['store'] = $mainConfig->get( 'ObjectCaches' )[$objectCacheId];
+
+               return \ObjectCache::newWANCacheFromParams( $params );
+       },
+
+       'LocalServerObjectCache' => function( MediaWikiServices $services ) {
+               $mainConfig = $services->getMainConfig();
+
+               if ( function_exists( 'apc_fetch' ) ) {
+                       $id = 'apc';
+               } elseif ( function_exists( 'apcu_fetch' ) ) {
+                       $id = 'apcu';
+               } elseif ( function_exists( 'xcache_get' ) && wfIniGetBool( 'xcache.var_size' ) ) {
+                       $id = 'xcache';
+               } elseif ( function_exists( 'wincache_ucache_get' ) ) {
+                       $id = 'wincache';
+               } else {
+                       $id = CACHE_NONE;
+               }
+
+               if ( !isset( $mainConfig->get( 'ObjectCaches' )[$id] ) ) {
+                       throw new UnexpectedValueException(
+                               "Cache type \"$id\" is not present in \$wgObjectCaches." );
+               }
+
+               return \ObjectCache::newFromParams( $mainConfig->get( 'ObjectCaches' )[$id] );
+       },
+
        'VirtualRESTServiceClient' => function( MediaWikiServices $services ) {
                $config = $services->getMainConfig()->get( 'VirtualRestConfig' );
 
diff --git a/includes/Services/CannotReplaceActiveServiceException.php b/includes/Services/CannotReplaceActiveServiceException.php
deleted file mode 100644 (file)
index 4993073..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-namespace MediaWiki\Services;
-
-use Exception;
-use RuntimeException;
-
-/**
- * Exception thrown when trying to replace an already active service.
- *
- * 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
- *
- * @since 1.27
- */
-
-/**
- * Exception thrown when trying to replace an already active service.
- */
-class CannotReplaceActiveServiceException extends RuntimeException {
-
-       /**
-        * @param string $serviceName
-        * @param Exception|null $previous
-        */
-       public function __construct( $serviceName, Exception $previous = null ) {
-               parent::__construct( "Cannot replace an active service: $serviceName", 0, $previous );
-       }
-
-}
diff --git a/includes/Services/ContainerDisabledException.php b/includes/Services/ContainerDisabledException.php
deleted file mode 100644 (file)
index ede076d..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-namespace MediaWiki\Services;
-
-use Exception;
-use RuntimeException;
-
-/**
- * Exception thrown when trying to access a service on a disabled container or factory.
- *
- * 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
- *
- * @since 1.27
- */
-
-/**
- * Exception thrown when trying to access a service on a disabled container or factory.
- */
-class ContainerDisabledException extends RuntimeException {
-
-       /**
-        * @param Exception|null $previous
-        */
-       public function __construct( Exception $previous = null ) {
-               parent::__construct( 'Container disabled!', 0, $previous );
-       }
-
-}
diff --git a/includes/Services/DestructibleService.php b/includes/Services/DestructibleService.php
deleted file mode 100644 (file)
index 6ce9af2..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-namespace MediaWiki\Services;
-
-/**
- * Interface for destructible services.
- *
- * 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
- *
- * @since 1.27
- */
-
-/**
- * DestructibleService defines a standard interface for shutting down a service instance.
- * The intended use is for a service container to be able to shut down services that should
- * no longer be used, and allow such services to release any system resources.
- *
- * @note There is no expectation that services will be destroyed when the process (or web request)
- * terminates.
- */
-interface DestructibleService {
-
-       /**
-        * Notifies the service object that it should expect to no longer be used, and should release
-        * any system resources it may own. The behavior of all service methods becomes undefined after
-        * destroy() has been called. It is recommended that implementing classes should throw an
-        * exception when service methods are accessed after destroy() has been called.
-        */
-       public function destroy();
-
-}
diff --git a/includes/Services/NoSuchServiceException.php b/includes/Services/NoSuchServiceException.php
deleted file mode 100644 (file)
index 36e50d2..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-namespace MediaWiki\Services;
-
-use Exception;
-use RuntimeException;
-
-/**
- * Exception thrown when the requested service is not known.
- *
- * 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
- *
- * @since 1.27
- */
-
-/**
- * Exception thrown when the requested service is not known.
- */
-class NoSuchServiceException extends RuntimeException {
-
-       /**
-        * @param string $serviceName
-        * @param Exception|null $previous
-        */
-       public function __construct( $serviceName, Exception $previous = null ) {
-               parent::__construct( "No such service: $serviceName", 0, $previous );
-       }
-
-}
diff --git a/includes/Services/SalvageableService.php b/includes/Services/SalvageableService.php
deleted file mode 100644 (file)
index a613050..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-namespace MediaWiki\Services;
-
-/**
- * Interface for salvageable services.
- *
- * 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
- *
- * @since 1.28
- */
-
-/**
- * SalvageableService defines an interface for services that are able to salvage state from a
- * previous instance of the same class. The intent is to allow new service instances to re-use
- * resources that would be expensive to re-create, such as cached data or network connections.
- *
- * @note There is no expectation that services will be destroyed when the process (or web request)
- * terminates.
- */
-interface SalvageableService {
-
-       /**
-        * Re-uses state from $other. $other must not be used after being passed to salvage(),
-        * and should be considered to be destroyed.
-        *
-        * @note Implementations are responsible for determining what parts of $other can be re-used
-        * safely. In particular, implementations should check that the relevant configuration of
-        * $other is the same as in $this before re-using resources from $other.
-        *
-        * @note Implementations must take care to detach any re-used resources from the original
-        * service instance. If $other is destroyed later, resources that are now used by the
-        * new service instance must not be affected.
-        *
-        * @note If $other is a DestructibleService, implementations should make sure that $other
-        * is in destroyed state after salvage finished. This may be done by calling $other->destroy()
-        * after carefully detaching all relevant resources.
-        *
-        * @param SalvageableService $other The object to salvage state from. $other must have the
-        * exact same type as $this.
-        */
-       public function salvage( SalvageableService $other );
-
-}
diff --git a/includes/Services/ServiceAlreadyDefinedException.php b/includes/Services/ServiceAlreadyDefinedException.php
deleted file mode 100644 (file)
index c6344d3..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-namespace MediaWiki\Services;
-
-use Exception;
-use RuntimeException;
-
-/**
- * Exception thrown when a service was already defined, but the
- * caller expected it to not exist.
- *
- * 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
- *
- * @since 1.27
- */
-
-/**
- * Exception thrown when a service was already defined, but the
- * caller expected it to not exist.
- */
-class ServiceAlreadyDefinedException extends RuntimeException {
-
-       /**
-        * @param string $serviceName
-        * @param Exception|null $previous
-        */
-       public function __construct( $serviceName, Exception $previous = null ) {
-               parent::__construct( "Service already defined: $serviceName", 0, $previous );
-       }
-
-}
diff --git a/includes/Services/ServiceContainer.php b/includes/Services/ServiceContainer.php
deleted file mode 100644 (file)
index bad0ef9..0000000
+++ /dev/null
@@ -1,378 +0,0 @@
-<?php
-namespace MediaWiki\Services;
-
-use InvalidArgumentException;
-use RuntimeException;
-use Wikimedia\Assert\Assert;
-
-/**
- * Generic service container.
- *
- * 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
- *
- * @since 1.27
- */
-
-/**
- * ServiceContainer provides a generic service to manage named services using
- * lazy instantiation based on instantiator callback functions.
- *
- * Services managed by an instance of ServiceContainer may or may not implement
- * a common interface.
- *
- * @note When using ServiceContainer to manage a set of services, consider
- * creating a wrapper or a subclass that provides access to the services via
- * getter methods with more meaningful names and more specific return type
- * declarations.
- *
- * @see docs/injection.txt for an overview of using dependency injection in the
- *      MediaWiki code base.
- */
-class ServiceContainer implements DestructibleService {
-
-       /**
-        * @var object[]
-        */
-       private $services = [];
-
-       /**
-        * @var callable[]
-        */
-       private $serviceInstantiators = [];
-
-       /**
-        * @var boolean[] disabled status, per service name
-        */
-       private $disabled = [];
-
-       /**
-        * @var array
-        */
-       private $extraInstantiationParams;
-
-       /**
-        * @var boolean
-        */
-       private $destroyed = false;
-
-       /**
-        * @param array $extraInstantiationParams Any additional parameters to be passed to the
-        * instantiator function when creating a service. This is typically used to provide
-        * access to additional ServiceContainers or Config objects.
-        */
-       public function __construct( array $extraInstantiationParams = [] ) {
-               $this->extraInstantiationParams = $extraInstantiationParams;
-       }
-
-       /**
-        * Destroys all contained service instances that implement the DestructibleService
-        * interface. This will render all services obtained from this MediaWikiServices
-        * instance unusable. In particular, this will disable access to the storage backend
-        * via any of these services. Any future call to getService() will throw an exception.
-        *
-        * @see resetGlobalInstance()
-        */
-       public function destroy() {
-               foreach ( $this->getServiceNames() as $name ) {
-                       $service = $this->peekService( $name );
-                       if ( $service !== null && $service instanceof DestructibleService ) {
-                               $service->destroy();
-                       }
-               }
-
-               $this->destroyed = true;
-       }
-
-       /**
-        * @param array $wiringFiles A list of PHP files to load wiring information from.
-        * Each file is loaded using PHP's include mechanism. Each file is expected to
-        * return an associative array that maps service names to instantiator functions.
-        */
-       public function loadWiringFiles( array $wiringFiles ) {
-               foreach ( $wiringFiles as $file ) {
-                       // the wiring file is required to return an array of instantiators.
-                       $wiring = require $file;
-
-                       Assert::postcondition(
-                               is_array( $wiring ),
-                               "Wiring file $file is expected to return an array!"
-                       );
-
-                       $this->applyWiring( $wiring );
-               }
-       }
-
-       /**
-        * Registers multiple services (aka a "wiring").
-        *
-        * @param array $serviceInstantiators An associative array mapping service names to
-        *        instantiator functions.
-        */
-       public function applyWiring( array $serviceInstantiators ) {
-               Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' );
-
-               foreach ( $serviceInstantiators as $name => $instantiator ) {
-                       $this->defineService( $name, $instantiator );
-               }
-       }
-
-       /**
-        * Imports all wiring defined in $container. Wiring defined in $container
-        * will override any wiring already defined locally. However, already
-        * existing service instances will be preserved.
-        *
-        * @since 1.28
-        *
-        * @param ServiceContainer $container
-        * @param string[] $skip A list of service names to skip during import
-        */
-       public function importWiring( ServiceContainer $container, $skip = [] ) {
-               $newInstantiators = array_diff_key(
-                       $container->serviceInstantiators,
-                       array_flip( $skip )
-               );
-
-               $this->serviceInstantiators = array_merge(
-                       $this->serviceInstantiators,
-                       $newInstantiators
-               );
-       }
-
-       /**
-        * Returns true if a service is defined for $name, that is, if a call to getService( $name )
-        * would return a service instance.
-        *
-        * @param string $name
-        *
-        * @return bool
-        */
-       public function hasService( $name ) {
-               return isset( $this->serviceInstantiators[$name] );
-       }
-
-       /**
-        * Returns the service instance for $name only if that service has already been instantiated.
-        * This is intended for situations where services get destroyed/cleaned up, so we can
-        * avoid creating a service just to destroy it again.
-        *
-        * @note This is intended for internal use and for test fixtures.
-        * Application logic should use getService() instead.
-        *
-        * @see getService().
-        *
-        * @param string $name
-        *
-        * @return object|null The service instance, or null if the service has not yet been instantiated.
-        * @throws RuntimeException if $name does not refer to a known service.
-        */
-       public function peekService( $name ) {
-               if ( !$this->hasService( $name ) ) {
-                       throw new NoSuchServiceException( $name );
-               }
-
-               return isset( $this->services[$name] ) ? $this->services[$name] : null;
-       }
-
-       /**
-        * @return string[]
-        */
-       public function getServiceNames() {
-               return array_keys( $this->serviceInstantiators );
-       }
-
-       /**
-        * Define a new service. The service must not be known already.
-        *
-        * @see getService().
-        * @see replaceService().
-        *
-        * @param string $name The name of the service to register, for use with getService().
-        * @param callable $instantiator Callback that returns a service instance.
-        *        Will be called with this MediaWikiServices instance as the only parameter.
-        *        Any extra instantiation parameters provided to the constructor will be
-        *        passed as subsequent parameters when invoking the instantiator.
-        *
-        * @throws RuntimeException if there is already a service registered as $name.
-        */
-       public function defineService( $name, callable $instantiator ) {
-               Assert::parameterType( 'string', $name, '$name' );
-
-               if ( $this->hasService( $name ) ) {
-                       throw new ServiceAlreadyDefinedException( $name );
-               }
-
-               $this->serviceInstantiators[$name] = $instantiator;
-       }
-
-       /**
-        * Replace an already defined service.
-        *
-        * @see defineService().
-        *
-        * @note This causes any previously instantiated instance of the service to be discarded.
-        *
-        * @param string $name The name of the service to register.
-        * @param callable $instantiator Callback function that returns a service instance.
-        *        Will be called with this MediaWikiServices instance as the only parameter.
-        *        The instantiator must return a service compatible with the originally defined service.
-        *        Any extra instantiation parameters provided to the constructor will be
-        *        passed as subsequent parameters when invoking the instantiator.
-        *
-        * @throws RuntimeException if $name is not a known service.
-        */
-       public function redefineService( $name, callable $instantiator ) {
-               Assert::parameterType( 'string', $name, '$name' );
-
-               if ( !$this->hasService( $name ) ) {
-                       throw new NoSuchServiceException( $name );
-               }
-
-               if ( isset( $this->services[$name] ) ) {
-                       throw new CannotReplaceActiveServiceException( $name );
-               }
-
-               $this->serviceInstantiators[$name] = $instantiator;
-               unset( $this->disabled[$name] );
-       }
-
-       /**
-        * Disables a service.
-        *
-        * @note Attempts to call getService() for a disabled service will result
-        * in a DisabledServiceException. Calling peekService for a disabled service will
-        * return null. Disabled services are listed by getServiceNames(). A disabled service
-        * can be enabled again using redefineService().
-        *
-        * @note If the service was already active (that is, instantiated) when getting disabled,
-        * and the service instance implements DestructibleService, destroy() is called on the
-        * service instance.
-        *
-        * @see redefineService()
-        * @see resetService()
-        *
-        * @param string $name The name of the service to disable.
-        *
-        * @throws RuntimeException if $name is not a known service.
-        */
-       public function disableService( $name ) {
-               $this->resetService( $name );
-
-               $this->disabled[$name] = true;
-       }
-
-       /**
-        * Resets a service by dropping the service instance.
-        * If the service instances implements DestructibleService, destroy()
-        * is called on the service instance.
-        *
-        * @warning This is generally unsafe! Other services may still retain references
-        * to the stale service instance, leading to failures and inconsistencies. Subclasses
-        * may use this method to reset specific services under specific instances, but
-        * it should not be exposed to application logic.
-        *
-        * @note This is declared final so subclasses can not interfere with the expectations
-        * disableService() has when calling resetService().
-        *
-        * @see redefineService()
-        * @see disableService().
-        *
-        * @param string $name The name of the service to reset.
-        * @param bool $destroy Whether the service instance should be destroyed if it exists.
-        *        When set to false, any existing service instance will effectively be detached
-        *        from the container.
-        *
-        * @throws RuntimeException if $name is not a known service.
-        */
-       final protected function resetService( $name, $destroy = true ) {
-               Assert::parameterType( 'string', $name, '$name' );
-
-               $instance = $this->peekService( $name );
-
-               if ( $destroy && $instance instanceof DestructibleService )  {
-                       $instance->destroy();
-               }
-
-               unset( $this->services[$name] );
-               unset( $this->disabled[$name] );
-       }
-
-       /**
-        * Returns a service object of the kind associated with $name.
-        * Services instances are instantiated lazily, on demand.
-        * This method may or may not return the same service instance
-        * when called multiple times with the same $name.
-        *
-        * @note Rather than calling this method directly, it is recommended to provide
-        * getters with more meaningful names and more specific return types, using
-        * a subclass or wrapper.
-        *
-        * @see redefineService().
-        *
-        * @param string $name The service name
-        *
-        * @throws NoSuchServiceException if $name is not a known service.
-        * @throws ContainerDisabledException if this container has already been destroyed.
-        * @throws ServiceDisabledException if the requested service has been disabled.
-        *
-        * @return object The service instance
-        */
-       public function getService( $name ) {
-               if ( $this->destroyed ) {
-                       throw new ContainerDisabledException();
-               }
-
-               if ( isset( $this->disabled[$name] ) ) {
-                       throw new ServiceDisabledException( $name );
-               }
-
-               if ( !isset( $this->services[$name] ) ) {
-                       $this->services[$name] = $this->createService( $name );
-               }
-
-               return $this->services[$name];
-       }
-
-       /**
-        * @param string $name
-        *
-        * @throws InvalidArgumentException if $name is not a known service.
-        * @return object
-        */
-       private function createService( $name ) {
-               if ( isset( $this->serviceInstantiators[$name] ) ) {
-                       $service = call_user_func_array(
-                               $this->serviceInstantiators[$name],
-                               array_merge( [ $this ], $this->extraInstantiationParams )
-                       );
-                       // NOTE: when adding more wiring logic here, make sure copyWiring() is kept in sync!
-               } else {
-                       throw new NoSuchServiceException( $name );
-               }
-
-               return $service;
-       }
-
-       /**
-        * @param string $name
-        * @return bool Whether the service is disabled
-        * @since 1.28
-        */
-       public function isServiceDisabled( $name ) {
-               return isset( $this->disabled[$name] );
-       }
-}
diff --git a/includes/Services/ServiceDisabledException.php b/includes/Services/ServiceDisabledException.php
deleted file mode 100644 (file)
index ae15b7c..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-namespace MediaWiki\Services;
-
-use Exception;
-use RuntimeException;
-
-/**
- * Exception thrown when trying to access a disabled service.
- *
- * 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
- *
- * @since 1.27
- */
-
-/**
- * Exception thrown when trying to access a disabled service.
- */
-class ServiceDisabledException extends RuntimeException {
-
-       /**
-        * @param string $serviceName
-        * @param Exception|null $previous
-        */
-       public function __construct( $serviceName, Exception $previous = null ) {
-               parent::__construct( "Service disabled: $serviceName", 0, $previous );
-       }
-
-}
index ddf5b89..7cda14c 100644 (file)
@@ -504,8 +504,9 @@ if ( !class_exists( 'AutoLoader' ) ) {
 // Reset the global service locator, so any services that have already been created will be
 // re-created while taking into account any custom settings and extensions.
 MediaWikiServices::resetGlobalInstance( new GlobalVarConfig(), 'quick' );
-// Apply $wgSharedDB table aliases for the local LB (all non-foreign DB connections)
+
 if ( $wgSharedDB && $wgSharedTables ) {
+       // Apply $wgSharedDB table aliases for the local LB (all non-foreign DB connections)
        MediaWikiServices::getInstance()->getDBLoadBalancer()->setTableAliases(
                array_fill_keys(
                        $wgSharedTables,
@@ -661,6 +662,12 @@ if ( !$wgDBerrorLogTZ ) {
 
 // initialize the request object in $wgRequest
 $wgRequest = RequestContext::getMain()->getRequest(); // BackCompat
+// Set user IP/agent information for causal consistency purposes
+MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->setRequestInfo( [
+       'IPAddress' => $wgRequest->getIP(),
+       'UserAgent' => $wgRequest->getHeader( 'User-Agent' ),
+       'ChronologyProtection' => $wgRequest->getHeader( 'ChronologyProtection' )
+] );
 
 // Useful debug output
 if ( $wgCommandLineMode ) {
index e578873..07828fe 100644 (file)
  * developer of the calling code is reminded that the function can fail, and
  * so that a lack of error-handling will be explicit.
  */
-class Status {
-       /** @var StatusValue */
-       protected $sv;
-
-       /** @var mixed */
-       public $value;
-       /** @var array Map of (key => bool) to indicate success of each part of batch operations */
-       public $success = [];
-       /** @var int Counter for batch operations */
-       public $successCount = 0;
-       /** @var int Counter for batch operations */
-       public $failCount = 0;
-
+class Status extends StatusValue {
        /** @var callable */
        public $cleanCallback = false;
 
-       /**
-        * @param StatusValue $sv [optional]
-        */
-       public function __construct( StatusValue $sv = null ) {
-               $this->sv = ( $sv === null ) ? new StatusValue() : $sv;
-               // B/C field aliases
-               $this->value =& $this->sv->value;
-               $this->successCount =& $this->sv->successCount;
-               $this->failCount =& $this->sv->failCount;
-               $this->success =& $this->sv->success;
-       }
-
        /**
         * Succinct helper method to wrap a StatusValue
         *
@@ -77,99 +53,83 @@ class Status {
         * @return Status
         */
        public static function wrap( $sv ) {
-               return $sv instanceof Status ? $sv : new self( $sv );
-       }
+               if ( $sv instanceof static ) {
+                       return $sv;
+               }
 
-       /**
-        * Factory function for fatal errors
-        *
-        * @param string|Message $message Message name or object
-        * @return Status
-        */
-       public static function newFatal( $message /*, parameters...*/ ) {
-               return new self( call_user_func_array(
-                       [ 'StatusValue', 'newFatal' ], func_get_args()
-               ) );
+               $result = new static();
+               $result->ok =& $sv->ok;
+               $result->errors =& $sv->errors;
+               $result->value =& $sv->value;
+               $result->successCount =& $sv->successCount;
+               $result->failCount =& $sv->failCount;
+               $result->success =& $sv->success;
+
+               return $result;
        }
 
        /**
-        * Factory function for good results
+        * Backwards compatibility logic
         *
-        * @param mixed $value
-        * @return Status
+        * @param string $name
+        * @return mixed
+        * @throws RuntimeException
         */
-       public static function newGood( $value = null ) {
-               $sv = new StatusValue();
-               $sv->value = $value;
+       function __get( $name ) {
+               if ( $name === 'ok' ) {
+                       return $this->isOK();
+               } elseif ( $name === 'errors' ) {
+                       return $this->getErrors();
+               }
 
-               return new self( $sv );
+               throw new RuntimeException( "Cannot get '$name' property." );
        }
 
        /**
         * Change operation result
+        * Backwards compatibility logic
         *
-        * @param bool $ok Whether the operation completed
+        * @param string $name
         * @param mixed $value
+        * @throws RuntimeException
         */
-       public function setResult( $ok, $value = null ) {
-               $this->sv->setResult( $ok, $value );
-       }
-
-       /**
-        * Returns the wrapped StatusValue object
-        * @return StatusValue
-        * @since 1.27
-        */
-       public function getStatusValue() {
-               return $this->sv;
-       }
-
-       /**
-        * Returns whether the operation completed and didn't have any error or
-        * warnings
-        *
-        * @return bool
-        */
-       public function isGood() {
-               return $this->sv->isGood();
-       }
-
-       /**
-        * Returns whether the operation completed
-        *
-        * @return bool
-        */
-       public function isOK() {
-               return $this->sv->isOK();
+       function __set( $name, $value ) {
+               if ( $name === 'ok' ) {
+                       $this->setOK( $value );
+               } elseif ( !property_exists( $this, $name ) ) {
+                       // Caller is using undeclared ad-hoc properties
+                       $this->$name = $value;
+               } else {
+                       throw new RuntimeException( "Cannot set '$name' property." );
+               }
        }
 
        /**
-        * Add a new warning
+        * Splits this Status object into two new Status objects, one which contains only
+        * the error messages, and one that contains the warnings, only. The returned array is
+        * defined as:
+        * [
+        *     0 => object(Status) # the Status with error messages, only
+        *     1 => object(Status) # The Status with warning messages, only
+        * ]
         *
-        * @param string|Message $message Message name or object
+        * @return array
         */
-       public function warning( $message /*, parameters... */ ) {
-               call_user_func_array( [ $this->sv, 'warning' ], func_get_args() );
-       }
+       public function splitByErrorType() {
+               list( $errorsOnlyStatus, $warningsOnlyStatus ) = parent::splitByErrorType();
+               $errorsOnlyStatus->cleanCallback =
+                       $warningsOnlyStatus->cleanCallback = $this->cleanCallback;
 
-       /**
-        * Add an error, do not set fatal flag
-        * This can be used for non-fatal errors
-        *
-        * @param string|Message $message Message name or object
-        */
-       public function error( $message /*, parameters... */ ) {
-               call_user_func_array( [ $this->sv, 'error' ], func_get_args() );
+               return [ $errorsOnlyStatus, $warningsOnlyStatus ];
        }
 
        /**
-        * Add an error and set OK to false, indicating that the operation
-        * as a whole was fatal
-        *
-        * @param string|Message $message Message name or object
+        * Returns the wrapped StatusValue object
+        * @return StatusValue
+        * @since 1.27
         */
-       public function fatal( $message /*, parameters... */ ) {
-               call_user_func_array( [ $this->sv, 'fatal' ], func_get_args() );
+       public function getStatusValue() {
+               return $this;
        }
 
        /**
@@ -217,16 +177,16 @@ class Status {
        public function getWikiText( $shortContext = false, $longContext = false, $lang = null ) {
                $lang = $this->languageFromParam( $lang );
 
-               $rawErrors = $this->sv->getErrors();
+               $rawErrors = $this->getErrors();
                if ( count( $rawErrors ) == 0 ) {
-                       if ( $this->sv->isOK() ) {
-                               $this->sv->fatal( 'internalerror_info',
+                       if ( $this->isOK() ) {
+                               $this->fatal( 'internalerror_info',
                                        __METHOD__ . " called for a good result, this is incorrect\n" );
                        } else {
-                               $this->sv->fatal( 'internalerror_info',
+                               $this->fatal( 'internalerror_info',
                                        __METHOD__ . ": Invalid result object: no error text but not OK\n" );
                        }
-                       $rawErrors = $this->sv->getErrors(); // just added a fatal
+                       $rawErrors = $this->getErrors(); // just added a fatal
                }
                if ( count( $rawErrors ) == 1 ) {
                        $s = $this->getErrorMessage( $rawErrors[0], $lang )->plain();
@@ -265,24 +225,24 @@ class Status {
         *
         * If both parameters are missing, and there is only one error, no bullet will be added.
         *
-        * @param string|string[] $shortContext A message name or an array of message names.
-        * @param string|string[] $longContext A message name or an array of message names.
+        * @param string|string[]|bool $shortContext A message name or an array of message names.
+        * @param string|string[]|bool $longContext A message name or an array of message names.
         * @param string|Language $lang Language to use for processing messages
         * @return Message
         */
        public function getMessage( $shortContext = false, $longContext = false, $lang = null ) {
                $lang = $this->languageFromParam( $lang );
 
-               $rawErrors = $this->sv->getErrors();
+               $rawErrors = $this->getErrors();
                if ( count( $rawErrors ) == 0 ) {
-                       if ( $this->sv->isOK() ) {
-                               $this->sv->fatal( 'internalerror_info',
+                       if ( $this->isOK() ) {
+                               $this->fatal( 'internalerror_info',
                                        __METHOD__ . " called for a good result, this is incorrect\n" );
                        } else {
-                               $this->sv->fatal( 'internalerror_info',
+                               $this->fatal( 'internalerror_info',
                                        __METHOD__ . ": Invalid result object: no error text but not OK\n" );
                        }
-                       $rawErrors = $this->sv->getErrors(); // just added a fatal
+                       $rawErrors = $this->getErrors(); // just added a fatal
                }
                if ( count( $rawErrors ) == 1 ) {
                        $s = $this->getErrorMessage( $rawErrors[0], $lang );
@@ -313,11 +273,12 @@ class Status {
        }
 
        /**
-        * Return the message for a single error.
-        * @param mixed $error With an array & two values keyed by
-        * 'message' and 'params', use those keys-value pairs.
-        * Otherwise, if its an array, just use the first value as the
-        * message and the remaining items as the params.
+        * Return the message for a single error
+        *
+        * The code string can be used a message key with per-language versions.
+        * If $error is an array, the "params" field is a list of parameters for the message.
+        *
+        * @param array|string $error Code string or (key: code string, params: string[]) map
         * @param string|Language $lang Language to use for processing messages
         * @return Message
         */
@@ -333,8 +294,10 @@ class Status {
                                $msg = wfMessage( $msgName,
                                        array_map( 'wfEscapeWikiText', $this->cleanParams( $error ) ) );
                        }
-               } else {
+               } elseif ( is_string( $error ) ) {
                        $msg = wfMessage( $error );
+               } else {
+                       throw new UnexpectedValueException( "Got " . get_class( $error ) . " for key." );
                }
 
                $msg->inLanguage( $this->languageFromParam( $lang ) );
@@ -342,12 +305,11 @@ class Status {
        }
 
        /**
-        * Get the error message as HTML. This is done by parsing the wikitext error
-        * message.
-        * @param string $shortContext A short enclosing context message name, to
+        * Get the error message as HTML. This is done by parsing the wikitext error message
+        * @param string|bool $shortContext A short enclosing context message name, to
         *        be used when there is a single error
-        * @param string $longContext A long enclosing context message name, for a list
-        * @param string|Language $lang Language to use for processing messages
+        * @param string|bool $longContext A long enclosing context message name, for a list
+        * @param string|Language|null $lang Language to use for processing messages
         * @return string
         */
        public function getHTML( $shortContext = false, $longContext = false, $lang = null ) {
@@ -370,16 +332,6 @@ class Status {
                }, $errors );
        }
 
-       /**
-        * Merge another status object into this one
-        *
-        * @param Status $other Other Status object
-        * @param bool $overwriteValue Whether to override the "value" member
-        */
-       public function merge( $other, $overwriteValue = false ) {
-               $this->sv->merge( $other->sv, $overwriteValue );
-       }
-
        /**
         * Get the list of errors (but not warnings)
         *
@@ -413,7 +365,7 @@ class Status {
        protected function getStatusArray( $type = false ) {
                $result = [];
 
-               foreach ( $this->sv->getErrors() as $error ) {
+               foreach ( $this->getErrors() as $error ) {
                        if ( $type === false || $error['type'] === $type ) {
                                if ( $error['message'] instanceof MessageSpecifier ) {
                                        $result[] = array_merge(
@@ -431,92 +383,6 @@ class Status {
                return $result;
        }
 
-       /**
-        * Returns a list of status messages of the given type, with message and
-        * params left untouched, like a sane version of getStatusArray
-        *
-        * Each entry is a map of:
-        *   - message: string message key or MessageSpecifier
-        *   - params: array list of parameters
-        *
-        * @param string $type
-        * @return array
-        */
-       public function getErrorsByType( $type ) {
-               return $this->sv->getErrorsByType( $type );
-       }
-
-       /**
-        * Returns true if the specified message is present as a warning or error
-        *
-        * @param string|Message $message Message key or object to search for
-        *
-        * @return bool
-        */
-       public function hasMessage( $message ) {
-               return $this->sv->hasMessage( $message );
-       }
-
-       /**
-        * If the specified source message exists, replace it with the specified
-        * destination message, but keep the same parameters as in the original error.
-        *
-        * Note, due to the lack of tools for comparing Message objects, this
-        * function will not work when using a Message object as the search parameter.
-        *
-        * @param Message|string $source Message key or object to search for
-        * @param Message|string $dest Replacement message key or object
-        * @return bool Return true if the replacement was done, false otherwise.
-        */
-       public function replaceMessage( $source, $dest ) {
-               return $this->sv->replaceMessage( $source, $dest );
-       }
-
-       /**
-        * @return mixed
-        */
-       public function getValue() {
-               return $this->sv->getValue();
-       }
-
-       /**
-        * Backwards compatibility logic
-        *
-        * @param string $name
-        */
-       function __get( $name ) {
-               if ( $name === 'ok' ) {
-                       return $this->sv->isOK();
-               } elseif ( $name === 'errors' ) {
-                       return $this->sv->getErrors();
-               }
-               throw new Exception( "Cannot get '$name' property." );
-       }
-
-       /**
-        * Backwards compatibility logic
-        *
-        * @param string $name
-        * @param mixed $value
-        */
-       function __set( $name, $value ) {
-               if ( $name === 'ok' ) {
-                       $this->sv->setOK( $value );
-               } elseif ( !property_exists( $this, $name ) ) {
-                       // Caller is using undeclared ad-hoc properties
-                       $this->$name = $value;
-               } else {
-                       throw new Exception( "Cannot set '$name' property." );
-               }
-       }
-
-       /**
-        * @return string
-        */
-       public function __toString() {
-               return $this->sv->__toString();
-       }
-
        /**
         * Don't save the callback when serializing, because Closures can't be
         * serialized and we're going to clear it in __wakeup anyway.
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 20142a7..a1e6d5b 100644 (file)
@@ -186,10 +186,10 @@ class TemplateParser {
         * @code
         *     echo $templateParser->processTemplate(
         *         'ExampleTemplate',
-        *         array(
+        *         [
         *             'username' => $user->getName(),
         *             'message' => 'Hello!'
-        *         )
+        *         ]
         *     );
         * @endcode
         * @param string $templateName The name of the template
index 6d9ddd6..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
         *
@@ -1079,7 +1104,7 @@ class Title implements LinkTarget {
        /**
         * Returns true if the title is inside one of the specified namespaces.
         *
-        * @param int $namespaces,... The namespaces to check for
+        * @param int|int[] $namespaces,... The namespaces to check for
         * @return bool
         * @since 1.19
         */
index 4ac4dea..4802f72 100644 (file)
@@ -55,7 +55,7 @@ class WatchedItemQueryService {
        }
 
        /**
-        * @return DatabaseBase
+        * @return Database
         * @throws MWException
         */
        private function getConnection() {
@@ -63,10 +63,10 @@ class WatchedItemQueryService {
        }
 
        /**
-        * @param DatabaseBase $connection
+        * @param Database $connection
         * @throws MWException
         */
-       private function reuseConnection( DatabaseBase $connection ) {
+       private function reuseConnection( Database $connection ) {
                $this->loadBalancer->reuseConnection( $connection );
        }
 
@@ -337,7 +337,7 @@ class WatchedItemQueryService {
        }
 
        private function getWatchedItemsWithRCInfoQueryConds(
-               DatabaseBase $db,
+               Database $db,
                User $user,
                array $options
        ) {
@@ -445,7 +445,7 @@ class WatchedItemQueryService {
                return $conds;
        }
 
-       private function getStartEndConds( DatabaseBase $db, array $options ) {
+       private function getStartEndConds( Database $db, array $options ) {
                if ( !isset( $options['start'] ) && ! isset( $options['end'] ) ) {
                        return [];
                }
@@ -464,7 +464,7 @@ class WatchedItemQueryService {
                return $conds;
        }
 
-       private function getUserRelatedConds( DatabaseBase $db, User $user, array $options ) {
+       private function getUserRelatedConds( Database $db, User $user, array $options ) {
                if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
                        return [];
                }
@@ -491,7 +491,7 @@ class WatchedItemQueryService {
                return $conds;
        }
 
-       private function getExtraDeletedPageLogEntryRelatedCond( DatabaseBase $db, User $user ) {
+       private function getExtraDeletedPageLogEntryRelatedCond( Database $db, User $user ) {
                // LogPage::DELETED_ACTION hides the affected page, too. So hide those
                // entirely from the watchlist, or someone could guess the title.
                $bitmask = 0;
@@ -509,7 +509,7 @@ class WatchedItemQueryService {
                return '';
        }
 
-       private function getStartFromConds( DatabaseBase $db, array $options ) {
+       private function getStartFromConds( Database $db, array $options ) {
                $op = $options['dir'] === self::DIR_OLDER ? '<' : '>';
                list( $rcTimestamp, $rcId ) = $options['startFrom'];
                $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) );
@@ -529,7 +529,7 @@ class WatchedItemQueryService {
                );
        }
 
-       private function getWatchedItemsForUserQueryConds( DatabaseBase $db, User $user, array $options ) {
+       private function getWatchedItemsForUserQueryConds( Database $db, User $user, array $options ) {
                $conds = [ 'wl_user' => $user->getId() ];
                if ( $options['namespaceIds'] ) {
                        $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
@@ -563,12 +563,12 @@ class WatchedItemQueryService {
         * Creates a query condition part for getting only items before or after the given link target
         * (while ordering using $sort mode)
         *
-        * @param DatabaseBase $db
+        * @param Database $db
         * @param LinkTarget $target
         * @param string $op comparison operator to use in the conditions
         * @return string
         */
-       private function getFromUntilTargetConds( DatabaseBase $db, LinkTarget $target, $op ) {
+       private function getFromUntilTargetConds( Database $db, LinkTarget $target, $op ) {
                return $db->makeList(
                        [
                                "wl_namespace $op " . $target->getNamespace(),
index 9a74401..6c47cae 100644 (file)
@@ -3,6 +3,7 @@
 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
 use MediaWiki\Linker\LinkTarget;
 use Wikimedia\Assert\Assert;
+use Wikimedia\ScopedCallback;
 
 /**
  * Storage layer class for WatchedItems.
@@ -192,20 +193,11 @@ class WatchedItemStore implements StatsdAwareInterface {
        /**
         * @param int $dbIndex DB_MASTER or DB_REPLICA
         *
-        * @return DatabaseBase
+        * @return IDatabase
         * @throws MWException
         */
-       private function getConnection( $dbIndex ) {
-               return $this->loadBalancer->getConnection( $dbIndex, [ 'watchlist' ] );
-       }
-
-       /**
-        * @param DatabaseBase $connection
-        *
-        * @throws MWException
-        */
-       private function reuseConnection( $connection ) {
-               $this->loadBalancer->reuseConnection( $connection );
+       private function getConnectionRef( $dbIndex ) {
+               return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] );
        }
 
        /**
@@ -217,7 +209,7 @@ class WatchedItemStore implements StatsdAwareInterface {
         * @return int
         */
        public function countWatchedItems( User $user ) {
-               $dbr = $this->getConnection( DB_REPLICA );
+               $dbr = $this->getConnectionRef( DB_REPLICA );
                $return = (int)$dbr->selectField(
                        'watchlist',
                        'COUNT(*)',
@@ -226,7 +218,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        ],
                        __METHOD__
                );
-               $this->reuseConnection( $dbr );
 
                return $return;
        }
@@ -237,7 +228,7 @@ class WatchedItemStore implements StatsdAwareInterface {
         * @return int
         */
        public function countWatchers( LinkTarget $target ) {
-               $dbr = $this->getConnection( DB_REPLICA );
+               $dbr = $this->getConnectionRef( DB_REPLICA );
                $return = (int)$dbr->selectField(
                        'watchlist',
                        'COUNT(*)',
@@ -247,7 +238,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        ],
                        __METHOD__
                );
-               $this->reuseConnection( $dbr );
 
                return $return;
        }
@@ -263,7 +253,7 @@ class WatchedItemStore implements StatsdAwareInterface {
         * @throws MWException
         */
        public function countVisitingWatchers( LinkTarget $target, $threshold ) {
-               $dbr = $this->getConnection( DB_REPLICA );
+               $dbr = $this->getConnectionRef( DB_REPLICA );
                $visitingWatchers = (int)$dbr->selectField(
                        'watchlist',
                        'COUNT(*)',
@@ -276,7 +266,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        ],
                        __METHOD__
                );
-               $this->reuseConnection( $dbr );
 
                return $visitingWatchers;
        }
@@ -293,7 +282,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        public function countWatchersMultiple( array $targets, array $options = [] ) {
                $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
 
-               $dbr = $this->getConnection( DB_REPLICA );
+               $dbr = $this->getConnectionRef( DB_REPLICA );
 
                if ( array_key_exists( 'minimumWatchers', $options ) ) {
                        $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$options['minimumWatchers'];
@@ -308,8 +297,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        $dbOptions
                );
 
-               $this->reuseConnection( $dbr );
-
                $watchCounts = [];
                foreach ( $targets as $linkTarget ) {
                        $watchCounts[$linkTarget->getNamespace()][$linkTarget->getDBkey()] = 0;
@@ -341,7 +328,7 @@ class WatchedItemStore implements StatsdAwareInterface {
                array $targetsWithVisitThresholds,
                $minimumWatchers = null
        ) {
-               $dbr = $this->getConnection( DB_REPLICA );
+               $dbr = $this->getConnectionRef( DB_REPLICA );
 
                $conds = $this->getVisitingWatchersCondition( $dbr, $targetsWithVisitThresholds );
 
@@ -357,8 +344,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        $dbOptions
                );
 
-               $this->reuseConnection( $dbr );
-
                $watcherCounts = [];
                foreach ( $targetsWithVisitThresholds as list( $target ) ) {
                        /* @var LinkTarget $target */
@@ -452,14 +437,13 @@ class WatchedItemStore implements StatsdAwareInterface {
                        return false;
                }
 
-               $dbr = $this->getConnection( DB_REPLICA );
+               $dbr = $this->getConnectionRef( DB_REPLICA );
                $row = $dbr->selectRow(
                        'watchlist',
                        'wl_notificationtimestamp',
                        $this->dbCond( $user, $target ),
                        __METHOD__
                );
-               $this->reuseConnection( $dbr );
 
                if ( !$row ) {
                        return false;
@@ -499,7 +483,7 @@ class WatchedItemStore implements StatsdAwareInterface {
                                "wl_title {$options['sort']}"
                        ];
                }
-               $db = $this->getConnection( $options['forWrite'] ? DB_MASTER : DB_REPLICA );
+               $db = $this->getConnectionRef( $options['forWrite'] ? DB_MASTER : DB_REPLICA );
 
                $res = $db->select(
                        'watchlist',
@@ -508,7 +492,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        __METHOD__,
                        $dbOptions
                );
-               $this->reuseConnection( $db );
 
                $watchedItems = [];
                foreach ( $res as $row ) {
@@ -569,7 +552,7 @@ class WatchedItemStore implements StatsdAwareInterface {
                        return $timestamps;
                }
 
-               $dbr = $this->getConnection( DB_REPLICA );
+               $dbr = $this->getConnectionRef( DB_REPLICA );
 
                $lb = new LinkBatch( $targetsToLoad );
                $res = $dbr->select(
@@ -581,7 +564,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        ],
                        __METHOD__
                );
-               $this->reuseConnection( $dbr );
 
                foreach ( $res as $row ) {
                        $timestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp;
@@ -630,13 +612,12 @@ class WatchedItemStore implements StatsdAwareInterface {
                        $this->uncache( $user, $target );
                }
 
-               $dbw = $this->getConnection( DB_MASTER );
+               $dbw = $this->getConnectionRef( DB_MASTER );
                foreach ( array_chunk( $rows, 100 ) as $toInsert ) {
                        // Use INSERT IGNORE to avoid overwriting the notification timestamp
                        // if there's already an entry for this page
                        $dbw->insert( 'watchlist', $toInsert, __METHOD__, 'IGNORE' );
                }
-               $this->reuseConnection( $dbw );
 
                return true;
        }
@@ -660,7 +641,7 @@ class WatchedItemStore implements StatsdAwareInterface {
 
                $this->uncache( $user, $target );
 
-               $dbw = $this->getConnection( DB_MASTER );
+               $dbw = $this->getConnectionRef( DB_MASTER );
                $dbw->delete( 'watchlist',
                        [
                                'wl_user' => $user->getId(),
@@ -669,7 +650,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        ], __METHOD__
                );
                $success = (bool)$dbw->affectedRows();
-               $this->reuseConnection( $dbw );
 
                return $success;
        }
@@ -687,7 +667,7 @@ class WatchedItemStore implements StatsdAwareInterface {
                        return false;
                }
 
-               $dbw = $this->getConnection( DB_MASTER );
+               $dbw = $this->getConnectionRef( DB_MASTER );
 
                $conds = [ 'wl_user' => $user->getId() ];
                if ( $targets ) {
@@ -702,8 +682,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        __METHOD__
                );
 
-               $this->reuseConnection( $dbw );
-
                $this->uncacheUser( $user );
 
                return $success;
@@ -718,7 +696,7 @@ class WatchedItemStore implements StatsdAwareInterface {
         * @return int[] Array of user IDs the timestamp has been updated for
         */
        public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
-               $dbw = $this->getConnection( DB_MASTER );
+               $dbw = $this->getConnectionRef( DB_MASTER );
                $uids = $dbw->selectFieldValues(
                        'watchlist',
                        'wl_user',
@@ -730,7 +708,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        ],
                        __METHOD__
                );
-               $this->reuseConnection( $dbw );
 
                $watchers = array_map( 'intval', $uids );
                if ( $watchers ) {
@@ -740,7 +717,7 @@ class WatchedItemStore implements StatsdAwareInterface {
                                function () use ( $timestamp, $watchers, $target, $fname ) {
                                        global $wgUpdateRowsPerQuery;
 
-                                       $dbw = $this->getConnection( DB_MASTER );
+                                       $dbw = $this->getConnectionRef( DB_MASTER );
                                        $factory = wfGetLBFactory();
                                        $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
 
@@ -762,8 +739,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                                                }
                                        }
                                        $this->uncacheLinkTarget( $target );
-
-                                       $this->reuseConnection( $dbw );
                                },
                                DeferredUpdates::POSTSEND,
                                $dbw
@@ -885,7 +860,7 @@ class WatchedItemStore implements StatsdAwareInterface {
                        $queryOptions['LIMIT'] = $unreadLimit;
                }
 
-               $dbr = $this->getConnection( DB_REPLICA );
+               $dbr = $this->getConnectionRef( DB_REPLICA );
                $rowCount = $dbr->selectRowCount(
                        'watchlist',
                        '1',
@@ -896,7 +871,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                        __METHOD__,
                        $queryOptions
                );
-               $this->reuseConnection( $dbr );
 
                if ( !isset( $unreadLimit ) ) {
                        return $rowCount;
@@ -937,7 +911,7 @@ class WatchedItemStore implements StatsdAwareInterface {
         * @param LinkTarget $newTarget
         */
        public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) {
-               $dbw = $this->getConnection( DB_MASTER );
+               $dbw = $this->getConnectionRef( DB_MASTER );
 
                $result = $dbw->select(
                        'watchlist',
@@ -975,8 +949,6 @@ class WatchedItemStore implements StatsdAwareInterface {
                                __METHOD__
                        );
                }
-
-               $this->reuseConnection( $dbw );
        }
 
 }
index a5ae461..0065135 100644 (file)
@@ -23,6 +23,7 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Session\Session;
 use MediaWiki\Session\SessionId;
 use MediaWiki\Session\SessionManager;
@@ -1222,7 +1223,8 @@ HTML;
                # Append XFF
                $forwardedFor = $this->getHeader( 'X-Forwarded-For' );
                if ( $forwardedFor !== false ) {
-                       $isConfigured = IP::isConfiguredProxy( $ip );
+                       $proxyLookup = MediaWikiServices::getInstance()->getProxyLookup();
+                       $isConfigured = $proxyLookup->isConfiguredProxy( $ip );
                        $ipchain = array_map( 'trim', explode( ',', $forwardedFor ) );
                        $ipchain = array_reverse( $ipchain );
                        array_unshift( $ipchain, $ip );
@@ -1235,14 +1237,14 @@ HTML;
                        foreach ( $ipchain as $i => $curIP ) {
                                $curIP = IP::sanitizeIP( IP::canonicalize( $curIP ) );
                                if ( !$curIP || !isset( $ipchain[$i + 1] ) || $ipchain[$i + 1] === 'unknown'
-                                       || !IP::isTrustedProxy( $curIP )
+                                       || !$proxyLookup->isTrustedProxy( $curIP )
                                ) {
                                        break; // IP is not valid/trusted or does not point to anything
                                }
                                if (
                                        IP::isPublic( $ipchain[$i + 1] ) ||
                                        $wgUsePrivateIPs ||
-                                       IP::isConfiguredProxy( $curIP ) // bug 48919; treat IP as sane
+                                       $proxyLookup->isConfiguredProxy( $curIP ) // bug 48919; treat IP as sane
                                ) {
                                        // Follow the next IP according to the proxy
                                        $nextIP = IP::canonicalize( $ipchain[$i + 1] );
index 90b76e3..339b2e3 100644 (file)
@@ -39,7 +39,11 @@ class WebResponse {
         * @param null|int $http_response_code Forces the HTTP response code to the specified value.
         */
        public function header( $string, $replace = true, $http_response_code = null ) {
-               header( $string, $replace, $http_response_code );
+               if ( $http_response_code ) {
+                       header( $string, $replace, $http_response_code );
+               } else {
+                       header( $string, $replace );
+               }
        }
 
        /**
index 43f7217..b1bd098 100644 (file)
@@ -452,7 +452,7 @@ class Xml {
 
        /**
         * Convenience function to build an HTML submit button
-        * When $wgUseMediaWikiUIEverywhere is true it will default to a constructive button
+        * When $wgUseMediaWikiUIEverywhere is true it will default to a progressive button
         * @param string $value Label text for the button
         * @param array $attribs Optional custom attributes
         * @return string HTML
@@ -467,7 +467,7 @@ class Xml {
                // some submit forms
                // might need to be mw-ui-destructive (e.g. delete a page)
                if ( $wgUseMediaWikiUIEverywhere ) {
-                       $baseAttrs['class'] = 'mw-ui-button mw-ui-constructive';
+                       $baseAttrs['class'] = 'mw-ui-button mw-ui-progressive';
                }
                // Any custom attributes will take precendence of anything in baseAttrs e.g. override the class
                $attribs = $attribs + $baseAttrs;
index 41378fb..1e1bb39 100644 (file)
@@ -105,8 +105,7 @@ class HistoryAction extends FormlessAction {
                $config = $this->context->getConfig();
 
                # Fill in the file cache if not set already
-               $useFileCache = $config->get( 'UseFileCache' );
-               if ( $useFileCache && HTMLFileCache::useFileCache( $this->getContext() ) ) {
+               if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
                        $cache = new HTMLFileCache( $this->getTitle(), 'history' );
                        if ( !$cache->isCacheGood( /* Assume up to date */ ) ) {
                                ob_start( [ &$cache, 'saveToFileCache' ] );
@@ -140,6 +139,10 @@ class HistoryAction extends FormlessAction {
 
                // Fail nicely if article doesn't exist.
                if ( !$this->page->exists() ) {
+                       global $wgSend404Code;
+                       if ( $wgSend404Code ) {
+                               $out->setStatusCode( 404 );
+                       }
                        $out->addWikiMsg( 'nohistory' );
                        # show deletion/move log if there is an entry
                        LogEventsList::showLogExtract(
index 8e57f93..1a42ccc 100644 (file)
@@ -191,8 +191,6 @@ class ApiAuthManagerHelper {
         * @return array
         */
        public function formatAuthenticationResponse( AuthenticationResponse $res ) {
-               $params = $this->module->extractRequestParams();
-
                $ret = [
                        'status' => $res->status,
                ];
index f763e45..506ff73 100644 (file)
@@ -600,7 +600,7 @@ abstract class ApiBase extends ContextSource {
 
        /**
         * Gets a default replica DB connection object
-        * @return DatabaseBase
+        * @return Database
         */
        protected function getDB() {
                if ( !isset( $this->mSlaveDB ) ) {
@@ -2680,275 +2680,6 @@ abstract class ApiBase extends ContextSource {
                return false;
        }
 
-       /**
-        * Generates help message for this module, or false if there is no description
-        * @deprecated since 1.25
-        * @return string|bool
-        */
-       public function makeHelpMsg() {
-               wfDeprecated( __METHOD__, '1.25' );
-               static $lnPrfx = "\n  ";
-
-               $msg = $this->getFinalDescription();
-
-               if ( $msg !== false ) {
-
-                       if ( !is_array( $msg ) ) {
-                               $msg = [
-                                       $msg
-                               ];
-                       }
-                       $msg = $lnPrfx . implode( $lnPrfx, $msg ) . "\n";
-
-                       $msg .= $this->makeHelpArrayToString( $lnPrfx, false, $this->getHelpUrls() );
-
-                       if ( $this->isReadMode() ) {
-                               $msg .= "\nThis module requires read rights";
-                       }
-                       if ( $this->isWriteMode() ) {
-                               $msg .= "\nThis module requires write rights";
-                       }
-                       if ( $this->mustBePosted() ) {
-                               $msg .= "\nThis module only accepts POST requests";
-                       }
-                       if ( $this->isReadMode() || $this->isWriteMode() ||
-                               $this->mustBePosted()
-                       ) {
-                               $msg .= "\n";
-                       }
-
-                       // Parameters
-                       $paramsMsg = $this->makeHelpMsgParameters();
-                       if ( $paramsMsg !== false ) {
-                               $msg .= "Parameters:\n$paramsMsg";
-                       }
-
-                       $examples = $this->getExamples();
-                       if ( $examples ) {
-                               if ( !is_array( $examples ) ) {
-                                       $examples = [
-                                               $examples
-                                       ];
-                               }
-                               $msg .= 'Example' . ( count( $examples ) > 1 ? 's' : '' ) . ":\n";
-                               foreach ( $examples as $k => $v ) {
-                                       if ( is_numeric( $k ) ) {
-                                               $msg .= "  $v\n";
-                                       } else {
-                                               if ( is_array( $v ) ) {
-                                                       $msgExample = implode( "\n", array_map( [ $this, 'indentExampleText' ], $v ) );
-                                               } else {
-                                                       $msgExample = "  $v";
-                                               }
-                                               $msgExample .= ':';
-                                               $msg .= wordwrap( $msgExample, 100, "\n" ) . "\n    $k\n";
-                                       }
-                               }
-                       }
-               }
-
-               return $msg;
-       }
-
-       /**
-        * @deprecated since 1.25
-        * @param string $item
-        * @return string
-        */
-       private function indentExampleText( $item ) {
-               return '  ' . $item;
-       }
-
-       /**
-        * @deprecated since 1.25
-        * @param string $prefix Text to split output items
-        * @param string $title What is being output
-        * @param string|array $input
-        * @return string
-        */
-       protected function makeHelpArrayToString( $prefix, $title, $input ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               if ( $input === false ) {
-                       return '';
-               }
-               if ( !is_array( $input ) ) {
-                       $input = [ $input ];
-               }
-
-               if ( count( $input ) > 0 ) {
-                       if ( $title ) {
-                               $msg = $title . ( count( $input ) > 1 ? 's' : '' ) . ":\n  ";
-                       } else {
-                               $msg = '  ';
-                       }
-                       $msg .= implode( $prefix, $input ) . "\n";
-
-                       return $msg;
-               }
-
-               return '';
-       }
-
-       /**
-        * Generates the parameter descriptions for this module, to be displayed in the
-        * module's help.
-        * @deprecated since 1.25
-        * @return string|bool
-        */
-       public function makeHelpMsgParameters() {
-               wfDeprecated( __METHOD__, '1.25' );
-               $params = $this->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
-               if ( $params ) {
-                       $paramsDescription = $this->getFinalParamDescription();
-                       $msg = '';
-                       $paramPrefix = "\n" . str_repeat( ' ', 24 );
-                       $descWordwrap = "\n" . str_repeat( ' ', 28 );
-                       foreach ( $params as $paramName => $paramSettings ) {
-                               $desc = isset( $paramsDescription[$paramName] ) ? $paramsDescription[$paramName] : '';
-                               if ( is_array( $desc ) ) {
-                                       $desc = implode( $paramPrefix, $desc );
-                               }
-
-                               // handle shorthand
-                               if ( !is_array( $paramSettings ) ) {
-                                       $paramSettings = [
-                                               self::PARAM_DFLT => $paramSettings,
-                                       ];
-                               }
-
-                               // handle missing type
-                               if ( !isset( $paramSettings[ApiBase::PARAM_TYPE] ) ) {
-                                       $dflt = isset( $paramSettings[ApiBase::PARAM_DFLT] )
-                                               ? $paramSettings[ApiBase::PARAM_DFLT]
-                                               : null;
-                                       if ( is_bool( $dflt ) ) {
-                                               $paramSettings[ApiBase::PARAM_TYPE] = 'boolean';
-                                       } elseif ( is_string( $dflt ) || is_null( $dflt ) ) {
-                                               $paramSettings[ApiBase::PARAM_TYPE] = 'string';
-                                       } elseif ( is_int( $dflt ) ) {
-                                               $paramSettings[ApiBase::PARAM_TYPE] = 'integer';
-                                       }
-                               }
-
-                               if ( isset( $paramSettings[self::PARAM_DEPRECATED] )
-                                       && $paramSettings[self::PARAM_DEPRECATED]
-                               ) {
-                                       $desc = "DEPRECATED! $desc";
-                               }
-
-                               if ( isset( $paramSettings[self::PARAM_REQUIRED] )
-                                       && $paramSettings[self::PARAM_REQUIRED]
-                               ) {
-                                       $desc .= $paramPrefix . 'This parameter is required';
-                               }
-
-                               $type = isset( $paramSettings[self::PARAM_TYPE] )
-                                       ? $paramSettings[self::PARAM_TYPE]
-                                       : null;
-                               if ( isset( $type ) ) {
-                                       $hintPipeSeparated = true;
-                                       $multi = isset( $paramSettings[self::PARAM_ISMULTI] )
-                                               ? $paramSettings[self::PARAM_ISMULTI]
-                                               : false;
-                                       if ( $multi ) {
-                                               $prompt = 'Values (separate with \'|\'): ';
-                                       } else {
-                                               $prompt = 'One value: ';
-                                       }
-
-                                       if ( $type === 'submodule' ) {
-                                               if ( isset( $paramSettings[self::PARAM_SUBMODULE_MAP] ) ) {
-                                                       $type = array_keys( $paramSettings[self::PARAM_SUBMODULE_MAP] );
-                                               } else {
-                                                       $type = $this->getModuleManager()->getNames( $paramName );
-                                               }
-                                               sort( $type );
-                                       }
-                                       if ( is_array( $type ) ) {
-                                               $choices = [];
-                                               $nothingPrompt = '';
-                                               foreach ( $type as $t ) {
-                                                       if ( $t === '' ) {
-                                                               $nothingPrompt = 'Can be empty, or ';
-                                                       } else {
-                                                               $choices[] = $t;
-                                                       }
-                                               }
-                                               $desc .= $paramPrefix . $nothingPrompt . $prompt;
-                                               $choicesstring = implode( ', ', $choices );
-                                               $desc .= wordwrap( $choicesstring, 100, $descWordwrap );
-                                               $hintPipeSeparated = false;
-                                       } else {
-                                               switch ( $type ) {
-                                                       case 'namespace':
-                                                               // Special handling because namespaces are
-                                                               // type-limited, yet they are not given
-                                                               $desc .= $paramPrefix . $prompt;
-                                                               $desc .= wordwrap( implode( ', ', MWNamespace::getValidNamespaces() ),
-                                                                       100, $descWordwrap );
-                                                               $hintPipeSeparated = false;
-                                                               break;
-                                                       case 'limit':
-                                                               $desc .= $paramPrefix . "No more than {$paramSettings[self::PARAM_MAX]}";
-                                                               if ( isset( $paramSettings[self::PARAM_MAX2] ) ) {
-                                                                       $desc .= " ({$paramSettings[self::PARAM_MAX2]} for bots)";
-                                                               }
-                                                               $desc .= ' allowed';
-                                                               break;
-                                                       case 'integer':
-                                                               $s = $multi ? 's' : '';
-                                                               $hasMin = isset( $paramSettings[self::PARAM_MIN] );
-                                                               $hasMax = isset( $paramSettings[self::PARAM_MAX] );
-                                                               if ( $hasMin || $hasMax ) {
-                                                                       if ( !$hasMax ) {
-                                                                               $intRangeStr = "The value$s must be no less than " .
-                                                                                       "{$paramSettings[self::PARAM_MIN]}";
-                                                                       } elseif ( !$hasMin ) {
-                                                                               $intRangeStr = "The value$s must be no more than " .
-                                                                                       "{$paramSettings[self::PARAM_MAX]}";
-                                                                       } else {
-                                                                               $intRangeStr = "The value$s must be between " .
-                                                                                       "{$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}";
-                                                                       }
-
-                                                                       $desc .= $paramPrefix . $intRangeStr;
-                                                               }
-                                                               break;
-                                                       case 'upload':
-                                                               $desc .= $paramPrefix . 'Must be posted as a file upload using multipart/form-data';
-                                                               break;
-                                               }
-                                       }
-
-                                       if ( $multi ) {
-                                               if ( $hintPipeSeparated ) {
-                                                       $desc .= $paramPrefix . "Separate values with '|'";
-                                               }
-
-                                               $isArray = is_array( $type );
-                                               if ( !$isArray
-                                                       || $isArray && count( $type ) > self::LIMIT_SML1
-                                               ) {
-                                                       $desc .= $paramPrefix . 'Maximum number of values ' .
-                                                               self::LIMIT_SML1 . ' (' . self::LIMIT_SML2 . ' for bots)';
-                                               }
-                                       }
-                               }
-
-                               $default = isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null;
-                               if ( !is_null( $default ) && $default !== false ) {
-                                       $desc .= $paramPrefix . "Default: $default";
-                               }
-
-                               $msg .= sprintf( "  %-19s - %s\n", $this->encodeParamName( $paramName ), $desc );
-                       }
-
-                       return $msg;
-               }
-
-               return false;
-       }
-
        /**
         * @deprecated since 1.25, always returns empty string
         * @param IDatabase|bool $db
@@ -3014,16 +2745,6 @@ abstract class ApiBase extends ContextSource {
                return 0;
        }
 
-       /**
-        * Get the result data array (read-only)
-        * @deprecated since 1.25, use $this->getResult() methods instead
-        * @return array
-        */
-       public function getResultData() {
-               wfDeprecated( __METHOD__, '1.25' );
-               return $this->getResult()->getData();
-       }
-
        /**
         * Call wfTransactionalTimeLimit() if this request was POSTed
         * @since 1.26
index 407ae71..5a0edfc 100644 (file)
@@ -85,7 +85,6 @@ class ApiCSPReport extends ApiBase {
         */
        private function getFlags( $report ) {
                $reportOnly = $this->getParameter( 'reportonly' );
-               $userAgent = $this->getRequest()->getHeader( 'user-agent' );
                $source = $this->getParameter( 'source' );
                $falsePositives = $this->getConfig()->get( 'CSPFalsePositiveUrls' );
 
index 4ddbd04..13b3577 100644 (file)
 class ApiClearHasMsg extends ApiBase {
        public function execute() {
                $user = $this->getUser();
-               $user->setNewtalk( false );
+               if ( $this->getRequest()->wasPosted() ) {
+                       $user->setNewtalk( false );
+               } else {
+                       DeferredUpdates::addCallableUpdate( function () use ( $user ) {
+                               $user->setNewtalk( false );
+                       } );
+               }
                $this->getResult()->addValue( null, $this->getModuleName(), 'success' );
        }
 
index 6601fb7..19e2453 100644 (file)
@@ -31,6 +31,7 @@ class ApiContinuationManager {
 
        private $continuationData = [];
        private $generatorContinuationData = [];
+       private $generatorNonContinuationData = [];
 
        private $generatorParams = [];
        private $generatorDone = false;
@@ -142,6 +143,26 @@ class ApiContinuationManager {
                $this->continuationData[$name][$paramName] = $paramValue;
        }
 
+       /**
+        * Set the non-continuation parameter for the generator module
+        *
+        * In case the generator isn't going to be continued, this sets the fields
+        * to return.
+        *
+        * @since 1.28
+        * @param ApiBase $module
+        * @param string $paramName
+        * @param string|array $paramValue
+        */
+       public function addGeneratorNonContinueParam( ApiBase $module, $paramName, $paramValue ) {
+               $name = $module->getModuleName();
+               $paramName = $module->encodeParamName( $paramName );
+               if ( is_array( $paramValue ) ) {
+                       $paramValue = implode( '|', $paramValue );
+               }
+               $this->generatorNonContinuationData[$name][$paramName] = $paramValue;
+       }
+
        /**
         * Set the continuation parameter for the generator module
         * @param ApiBase $module
@@ -165,6 +186,15 @@ class ApiContinuationManager {
                return array_merge_recursive( $this->continuationData, $this->generatorContinuationData );
        }
 
+       /**
+        * Fetch raw non-continuation data
+        * @since 1.28
+        * @return array
+        */
+       public function getRawNonContinuation() {
+               return $this->generatorNonContinuationData;
+       }
+
        /**
         * Fetch continuation result data
         * @return array [ (array)$data, (bool)$batchcomplete ]
@@ -192,8 +222,13 @@ class ApiContinuationManager {
                        foreach ( $continuationData as $module => $kvp ) {
                                $data += $kvp;
                        }
-                       $data += $this->generatorParams;
-                       $generatorKeys = implode( '|', array_keys( $this->generatorParams ) );
+                       $generatorParams = [];
+                       foreach ( $this->generatorNonContinuationData as $kvp ) {
+                               $generatorParams += $kvp;
+                       }
+                       $generatorParams += $this->generatorParams;
+                       $data += $generatorParams;
+                       $generatorKeys = implode( '|', array_keys( $generatorParams ) );
                } elseif ( $this->generatorContinuationData ) {
                        // All the generator-using modules are complete, but the
                        // generator isn't. Continue the generator and restart the
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 c826bba..4c406a7 100644 (file)
@@ -231,6 +231,7 @@ abstract class ApiFormatBase extends ApiBase {
                                                        $out->getModuleScripts(),
                                                        $out->getModuleStyles()
                                                ) ) ),
+                                               'continue' => $this->getResult()->getResultData( 'continue' ),
                                                'time' => round( $time * 1000 ),
                                        ],
                                        false, FormatJson::ALL_OK
@@ -300,144 +301,6 @@ abstract class ApiFormatBase extends ApiBase {
                return 'https://www.mediawiki.org/wiki/API:Data_formats';
        }
 
-       /************************************************************************//**
-        * @name   Deprecated
-        * @{
-        */
-
-       /**
-        * Specify whether or not sequences like &amp;quot; should be unescaped
-        * to &quot; . This should only be set to true for the help message
-        * when rendered in the default (xmlfm) format. This is a temporary
-        * special-case fix that should be removed once the help has been
-        * reworked to use a fully HTML interface.
-        *
-        * @deprecated since 1.25
-        * @param bool $b Whether or not ampersands should be escaped.
-        */
-       public function setUnescapeAmps( $b ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               $this->mUnescapeAmps = $b;
-       }
-
-       /**
-        * Whether this formatter can format the help message in a nice way.
-        * By default, this returns the same as getIsHtml().
-        * When action=help is set explicitly, the help will always be shown
-        * @deprecated since 1.25
-        * @return bool
-        */
-       public function getWantsHelp() {
-               wfDeprecated( __METHOD__, '1.25' );
-               return $this->getIsHtml();
-       }
-
-       /**
-        * Sets whether the pretty-printer should format *bold*
-        * @deprecated since 1.25
-        * @param bool $help
-        */
-       public function setHelp( $help = true ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               $this->mHelp = $help;
-       }
-
-       /**
-        * Pretty-print various elements in HTML format, such as xml tags and
-        * URLs. This method also escapes characters like <
-        * @deprecated since 1.25
-        * @param string $text
-        * @return string
-        */
-       protected function formatHTML( $text ) {
-               wfDeprecated( __METHOD__, '1.25' );
-
-               // Escape everything first for full coverage
-               $text = htmlspecialchars( $text );
-
-               if ( $this->mFormat === 'XML' ) {
-                       // encode all comments or tags as safe blue strings
-                       $text = str_replace( '&lt;', '<span style="color:blue;">&lt;', $text );
-                       $text = str_replace( '&gt;', '&gt;</span>', $text );
-               }
-
-               // identify requests to api.php
-               $text = preg_replace( '#^(\s*)(api\.php\?[^ <\n\t]+)$#m', '\1<a href="\2">\2</a>', $text );
-               if ( $this->mHelp ) {
-                       // make lines inside * bold
-                       $text = preg_replace( '#^(\s*)(\*[^<>\n]+\*)(\s*)$#m', '$1<b>$2</b>$3', $text );
-               }
-
-               // Armor links (bug 61362)
-               $masked = [];
-               $text = preg_replace_callback( '#<a .*?</a>#', function ( $matches ) use ( &$masked ) {
-                       $sha = sha1( $matches[0] );
-                       $masked[$sha] = $matches[0];
-                       return "<$sha>";
-               }, $text );
-
-               // identify URLs
-               $protos = wfUrlProtocolsWithoutProtRel();
-               // This regex hacks around bug 13218 (&quot; included in the URL)
-               $text = preg_replace(
-                       "#(((?i)$protos).*?)(&quot;)?([ \\'\"<>\n]|&lt;|&gt;|&quot;)#",
-                       '<a href="\\1">\\1</a>\\3\\4',
-                       $text
-               );
-
-               // Unarmor links
-               $text = preg_replace_callback( '#<([0-9a-f]{40})>#', function ( $matches ) use ( &$masked ) {
-                       $sha = $matches[1];
-                       return isset( $masked[$sha] ) ? $masked[$sha] : $matches[0];
-               }, $text );
-
-               /**
-                * Temporary fix for bad links in help messages. As a special case,
-                * XML-escaped metachars are de-escaped one level in the help message
-                * for legibility. Should be removed once we have completed a fully-HTML
-                * version of the help message.
-                */
-               if ( $this->mUnescapeAmps ) {
-                       $text = preg_replace( '/&amp;(amp|quot|lt|gt);/', '&\1;', $text );
-               }
-
-               return $text;
-       }
-
-       /**
-        * @see ApiBase::getDescription
-        * @deprecated since 1.25
-        */
-       public function getDescription() {
-               return $this->getIsHtml() ? ' (pretty-print in HTML)' : '';
-       }
-
-       /**
-        * Set the flag to buffer the result instead of printing it.
-        * @deprecated since 1.25, output is always buffered
-        * @param bool $value
-        */
-       public function setBufferResult( $value ) {
-       }
-
-       /**
-        * Formerly indicated whether the formatter needed metadata from ApiResult.
-        *
-        * ApiResult previously (indirectly) used this to decide whether to add
-        * metadata or to ignore calls to metadata-setting methods, which
-        * unfortunately made several methods that should have been static have to
-        * be dynamic instead. Now ApiResult always stores metadata and formatters
-        * are required to ignore it or filter it out.
-        *
-        * @deprecated since 1.25
-        * @return bool Always true
-        */
-       public function getNeedsRawData() {
-               wfDeprecated( __METHOD__, '1.25' );
-               return true;
-       }
-
-       /**@}*/
 }
 
 /**
index 814450e..2e917e1 100644 (file)
@@ -56,15 +56,6 @@ class ApiFormatJson extends ApiFormatBase {
                return 'application/json';
        }
 
-       /**
-        * @deprecated since 1.25
-        */
-       public function getWantsHelp() {
-               wfDeprecated( __METHOD__, '1.25' );
-               // Help is always ugly in JSON
-               return false;
-       }
-
        public function execute() {
                $params = $this->extractRequestParams();
 
index a3bb6a2..02efd7b 100644 (file)
@@ -311,11 +311,11 @@ class ApiHelp extends ApiBase {
                                if ( count( $modules ) === 1 && $m === $modules[0] &&
                                        !( !empty( $options['submodules'] ) && $m->getModuleManager() )
                                ) {
-                                       $link = Html::element( 'b', null, $name );
+                                       $link = Html::element( 'b', [ 'dir' => 'ltr', 'lang' => 'en' ], $name );
                                } else {
                                        $link = SpecialPage::getTitleFor( 'ApiHelp', $m->getModulePath() )->getLocalURL();
                                        $link = Html::element( 'a',
-                                               [ 'href' => $link, 'class' => 'apihelp-linktrail' ],
+                                               [ 'href' => $link, 'class' => 'apihelp-linktrail', 'dir' => 'ltr', 'lang' => 'en' ],
                                                $name
                                        );
                                        $any = true;
@@ -350,7 +350,8 @@ class ApiHelp extends ApiBase {
                                if ( isset( $sourceInfo['namemsg'] ) ) {
                                        $extname = $context->msg( $sourceInfo['namemsg'] )->text();
                                } else {
-                                       $extname = $sourceInfo['name'];
+                                       // Probably English, so wrap it.
+                                       $extname = Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $sourceInfo['name'] );
                                }
                                $help['flags'] .= Html::rawElement( 'li', null,
                                        self::wrap(
@@ -361,7 +362,9 @@ class ApiHelp extends ApiBase {
 
                                $link = SpecialPage::getTitleFor( 'Version', 'License/' . $sourceInfo['name'] );
                                if ( isset( $sourceInfo['license-name'] ) ) {
-                                       $msg = $context->msg( 'api-help-license', $link, $sourceInfo['license-name'] );
+                                       $msg = $context->msg( 'api-help-license', $link,
+                                               Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $sourceInfo['license-name'] )
+                                       );
                                } elseif ( SpecialVersion::getExtLicenseFileName( dirname( $sourceInfo['path'] ) ) ) {
                                        $msg = $context->msg( 'api-help-license-noname', $link );
                                } else {
@@ -403,7 +406,7 @@ class ApiHelp extends ApiBase {
                                $help['help-urls'] .= Html::openElement( 'ul' );
                                foreach ( $urls as $url ) {
                                        $help['help-urls'] .= Html::rawElement( 'li', null,
-                                               Html::element( 'a', [ 'href' => $url ], $url )
+                                               Html::element( 'a', [ 'href' => $url, 'dir' => 'ltr' ], $url )
                                        );
                                }
                                $help['help-urls'] .= Html::closeElement( 'ul' );
@@ -432,8 +435,9 @@ class ApiHelp extends ApiBase {
                                                $settings = [ ApiBase::PARAM_DFLT => $settings ];
                                        }
 
-                                       $help['parameters'] .= Html::element( 'dt', null,
-                                               $module->encodeParamName( $name ) );
+                                       $help['parameters'] .= Html::rawElement( 'dt', null,
+                                               Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $module->encodeParamName( $name ) )
+                                       );
 
                                        // Add description
                                        $description = [];
@@ -488,8 +492,9 @@ class ApiHelp extends ApiBase {
                                                        $links = isset( $settings[ApiBase::PARAM_VALUE_LINKS] )
                                                                ? $settings[ApiBase::PARAM_VALUE_LINKS]
                                                                : [];
-                                                       $type = array_map( function ( $v ) use ( $links ) {
-                                                               $ret = wfEscapeWikiText( $v );
+                                                       $values = array_map( function ( $v ) use ( $links ) {
+                                                               // We can't know whether this contains LTR or RTL text.
+                                                               $ret = $v === '' ? $v : Html::element( 'span', [ 'dir' => 'auto' ], $v );
                                                                if ( isset( $links[$v] ) ) {
                                                                        $ret = "[[{$links[$v]}|$ret]]";
                                                                }
@@ -497,17 +502,17 @@ class ApiHelp extends ApiBase {
                                                        }, $type );
                                                        $i = array_search( '', $type, true );
                                                        if ( $i === false ) {
-                                                               $type = $context->getLanguage()->commaList( $type );
+                                                               $values = $context->getLanguage()->commaList( $values );
                                                        } else {
-                                                               unset( $type[$i] );
-                                                               $type = $context->msg( 'api-help-param-list-can-be-empty' )
-                                                                       ->numParams( count( $type ) )
-                                                                       ->params( $context->getLanguage()->commaList( $type ) )
+                                                               unset( $values[$i] );
+                                                               $values = $context->msg( 'api-help-param-list-can-be-empty' )
+                                                                       ->numParams( count( $values ) )
+                                                                       ->params( $context->getLanguage()->commaList( $values ) )
                                                                        ->parse();
                                                        }
                                                        $info[] = $context->msg( 'api-help-param-list' )
                                                                ->params( $multi ? 2 : 1 )
-                                                               ->params( $type )
+                                                               ->params( $values )
                                                                ->parse();
                                                        $hintPipeSeparated = false;
                                                } else {
@@ -527,7 +532,8 @@ class ApiHelp extends ApiBase {
                                                                                $prefix = $module->isMain()
                                                                                        ? '' : ( $module->getModulePath() . '+' );
                                                                                $submodules = array_map( function ( $name ) use ( $prefix ) {
-                                                                                       return "[[Special:ApiHelp/{$prefix}{$name}|{$name}]]";
+                                                                                       $text = Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $name );
+                                                                                       return "[[Special:ApiHelp/{$prefix}{$name}|{$text}]]";
                                                                                }, $submodules );
                                                                        }
                                                                        $count = count( $submodules );
@@ -650,8 +656,9 @@ class ApiHelp extends ApiBase {
                                                $info[] = $context->msg( 'api-help-param-default-empty' )
                                                        ->parse();
                                        } elseif ( $default !== null && $default !== false ) {
+                                               // We can't know whether this contains LTR or RTL text.
                                                $info[] = $context->msg( 'api-help-param-default' )
-                                                       ->params( wfEscapeWikiText( $default ) )
+                                                       ->params( Html::element( 'span', [ 'dir' => 'auto' ], $default ) )
                                                        ->parse();
                                        }
 
@@ -723,7 +730,7 @@ class ApiHelp extends ApiBase {
                                        $sandbox = SpecialPage::getTitleFor( 'ApiSandbox' )->getLocalURL() . '#' . $qs;
                                        $help['examples'] .= Html::rawElement( 'dt', null, $msg->parse() );
                                        $help['examples'] .= Html::rawElement( 'dd', null,
-                                               Html::element( 'a', [ 'href' => $link ], "api.php?$qs" ) . ' ' .
+                                               Html::element( 'a', [ 'href' => $link, 'dir' => 'ltr' ], "api.php?$qs" ) . ' ' .
                                                Html::rawElement( 'a', [ 'href' => $sandbox ],
                                                        $context->msg( 'api-help-open-in-apisandbox' )->parse() )
                                        );
index 573524a..45378ee 100644 (file)
@@ -64,7 +64,8 @@ class ApiHelpParamValueMessage extends Message {
         */
        public function fetchMessage() {
                if ( $this->message === null ) {
-                       $this->message = ";{$this->paramValue}:" . parent::fetchMessage();
+                       $this->message = ";<span dir=\"ltr\" lang=\"en\">{$this->paramValue}</span>:"
+                               . parent::fetchMessage();
                }
                return $this->message;
        }
index 2b99353..966bcbf 100644 (file)
@@ -108,7 +108,7 @@ class ApiImageRotate extends ApiBase {
                                continue;
                        }
                        $ext = strtolower( pathinfo( "$srcPath", PATHINFO_EXTENSION ) );
-                       $tmpFile = TempFSFile::factory( 'rotate_', $ext );
+                       $tmpFile = TempFSFile::factory( 'rotate_', $ext, wfTempDir() );
                        $dstPath = $tmpFile->getPath();
                        $err = $handler->rotate( $file, [
                                'srcPath' => $srcPath,
index 1f3c76a..c8f4460 100644 (file)
@@ -258,7 +258,6 @@ class ApiMain extends ApiBase {
                $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
                $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
                $this->mResult->setErrorFormatter( $this->mErrorFormatter );
-               $this->mResult->setMainForContinuation( $this );
                $this->mContinuationManager = null;
                $this->mEnableWrite = $enableWrite;
 
@@ -1299,7 +1298,7 @@ class ApiMain extends ApiBase {
                }
 
                if ( $module->isWriteMode()
-                       && in_array( 'bot', $this->getUser()->getGroups() )
+                       && $this->getUser()->isBot()
                        && wfGetLB()->getServerCount() > 1
                ) {
                        $this->checkBotReadOnly();
@@ -1362,6 +1361,15 @@ class ApiMain extends ApiBase {
                                        break;
                        }
                }
+               if ( isset( $params['assertuser'] ) ) {
+                       $assertUser = User::newFromName( $params['assertuser'], false );
+                       if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
+                               $this->dieUsage(
+                                       'Assertion that the user is "' . $params['assertuser'] . '" failed',
+                                       'assertnameduserfailed'
+                               );
+                       }
+               }
        }
 
        /**
@@ -1662,6 +1670,9 @@ class ApiMain extends ApiBase {
                        'assert' => [
                                ApiBase::PARAM_TYPE => [ 'user', 'bot' ]
                        ],
+                       'assertuser' => [
+                               ApiBase::PARAM_TYPE => 'user',
+                       ],
                        'requestid' => null,
                        'servedby' => false,
                        'curtimestamp' => false,
@@ -1816,119 +1827,6 @@ class ApiMain extends ApiBase {
                        $this->getRequest()->getHeader( 'User-agent' )
                );
        }
-
-       /************************************************************************//**
-        * @name   Deprecated
-        * @{
-        */
-
-       /**
-        * Sets whether the pretty-printer should format *bold* and $italics$
-        *
-        * @deprecated since 1.25
-        * @param bool $help
-        */
-       public function setHelp( $help = true ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               $this->mPrinter->setHelp( $help );
-       }
-
-       /**
-        * Override the parent to generate help messages for all available modules.
-        *
-        * @deprecated since 1.25
-        * @return string
-        */
-       public function makeHelpMsg() {
-               wfDeprecated( __METHOD__, '1.25' );
-
-               $this->setHelp();
-               $cacheHelpTimeout = $this->getConfig()->get( 'APICacheHelpTimeout' );
-
-               return ObjectCache::getMainWANInstance()->getWithSetCallback(
-                       wfMemcKey(
-                               'apihelp',
-                               $this->getModuleName(),
-                               str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) )
-                       ),
-                       $cacheHelpTimeout > 0 ? $cacheHelpTimeout : WANObjectCache::TTL_UNCACHEABLE,
-                       [ $this, 'reallyMakeHelpMsg' ]
-               );
-       }
-
-       /**
-        * @deprecated since 1.25
-        * @return mixed|string
-        */
-       public function reallyMakeHelpMsg() {
-               wfDeprecated( __METHOD__, '1.25' );
-               $this->setHelp();
-
-               // Use parent to make default message for the main module
-               $msg = parent::makeHelpMsg();
-
-               $asterisks = str_repeat( '*** ', 14 );
-               $msg .= "\n\n$asterisks Modules  $asterisks\n\n";
-
-               foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) {
-                       $module = $this->mModuleMgr->getModule( $name );
-                       $msg .= self::makeHelpMsgHeader( $module, 'action' );
-
-                       $msg2 = $module->makeHelpMsg();
-                       if ( $msg2 !== false ) {
-                               $msg .= $msg2;
-                       }
-                       $msg .= "\n";
-               }
-
-               $msg .= "\n$asterisks Permissions $asterisks\n\n";
-               foreach ( self::$mRights as $right => $rightMsg ) {
-                       $rightsMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )
-                               ->useDatabase( false )
-                               ->inLanguage( 'en' )
-                               ->text();
-                       $groups = User::getGroupsWithPermission( $right );
-                       $msg .= '* ' . $right . " *\n  $rightsMsg" .
-                               "\nGranted to:\n  " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
-               }
-
-               $msg .= "\n$asterisks Formats  $asterisks\n\n";
-               foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) {
-                       $module = $this->mModuleMgr->getModule( $name );
-                       $msg .= self::makeHelpMsgHeader( $module, 'format' );
-                       $msg2 = $module->makeHelpMsg();
-                       if ( $msg2 !== false ) {
-                               $msg .= $msg2;
-                       }
-                       $msg .= "\n";
-               }
-
-               $credits = $this->msg( 'api-credits' )->useDatabase( 'false' )->inLanguage( 'en' )->text();
-               $credits = str_replace( "\n", "\n   ", $credits );
-               $msg .= "\n*** Credits: ***\n   $credits\n";
-
-               return $msg;
-       }
-
-       /**
-        * @deprecated since 1.25
-        * @param ApiBase $module
-        * @param string $paramName What type of request is this? e.g. action,
-        *    query, list, prop, meta, format
-        * @return string
-        */
-       public static function makeHelpMsgHeader( $module, $paramName ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               $modulePrefix = $module->getModulePrefix();
-               if ( strval( $modulePrefix ) !== '' ) {
-                       $modulePrefix = "($modulePrefix) ";
-               }
-
-               return "* $paramName={$module->getModuleName()} $modulePrefix*";
-       }
-
-       /**@}*/
-
 }
 
 /**
index ed229cb..34c17c1 100644 (file)
@@ -1330,7 +1330,7 @@ class ApiPageSet extends ApiBase {
 
        /**
         * Get the database connection (read-only)
-        * @return DatabaseBase
+        * @return Database
         */
        protected function getDB() {
                return $this->mDbSource->getDB();
index 5eb86ab..2f53209 100644 (file)
@@ -168,7 +168,7 @@ class ApiQuery extends ApiBase {
         * @param string $name Name to assign to the database connection
         * @param int $db One of the DB_* constants
         * @param array $groups Query groups
-        * @return DatabaseBase
+        * @return Database
         */
        public function getNamedDB( $name, $db, $groups ) {
                if ( !array_key_exists( $name, $this->mNamedDB ) ) {
@@ -258,6 +258,11 @@ class ApiQuery extends ApiBase {
                // Write the continuation data into the result
                $this->setContinuationManager( null );
                if ( $this->mParams['rawcontinue'] ) {
+                       $data = $continuationManager->getRawNonContinuation();
+                       if ( $data ) {
+                               $this->getResult()->addValue( null, 'query-noncontinue', $data,
+                                       ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
+                       }
                        $data = $continuationManager->getRawContinuation();
                        if ( $data ) {
                                $this->getResult()->addValue( null, 'query-continue', $data,
@@ -495,61 +500,6 @@ class ApiQuery extends ApiBase {
                return $result;
        }
 
-       /**
-        * Override the parent to generate help messages for all available query modules.
-        * @deprecated since 1.25
-        * @return string
-        */
-       public function makeHelpMsg() {
-               wfDeprecated( __METHOD__, '1.25' );
-
-               // Use parent to make default message for the query module
-               $msg = parent::makeHelpMsg();
-
-               $querySeparator = str_repeat( '--- ', 12 );
-               $moduleSeparator = str_repeat( '*** ', 14 );
-               $msg .= "\n$querySeparator Query: Prop  $querySeparator\n\n";
-               $msg .= $this->makeHelpMsgHelper( 'prop' );
-               $msg .= "\n$querySeparator Query: List  $querySeparator\n\n";
-               $msg .= $this->makeHelpMsgHelper( 'list' );
-               $msg .= "\n$querySeparator Query: Meta  $querySeparator\n\n";
-               $msg .= $this->makeHelpMsgHelper( 'meta' );
-               $msg .= "\n\n$moduleSeparator Modules: continuation  $moduleSeparator\n\n";
-
-               return $msg;
-       }
-
-       /**
-        * For all modules of a given group, generate help messages and join them together
-        * @deprecated since 1.25
-        * @param string $group Module group
-        * @return string
-        */
-       private function makeHelpMsgHelper( $group ) {
-               $moduleDescriptions = [];
-
-               $moduleNames = $this->mModuleMgr->getNames( $group );
-               sort( $moduleNames );
-               foreach ( $moduleNames as $name ) {
-                       /**
-                        * @var $module ApiQueryBase
-                        */
-                       $module = $this->mModuleMgr->getModule( $name );
-
-                       $msg = ApiMain::makeHelpMsgHeader( $module, $group );
-                       $msg2 = $module->makeHelpMsg();
-                       if ( $msg2 !== false ) {
-                               $msg .= $msg2;
-                       }
-                       if ( $module instanceof ApiQueryGeneratorBase ) {
-                               $msg .= "Generator:\n  This module may be used as a generator\n";
-                       }
-                       $moduleDescriptions[] = $msg;
-               }
-
-               return implode( "\n", $moduleDescriptions );
-       }
-
        public function isReadMode() {
                // We need to make an exception for certain meta modules that should be
                // accessible even without the 'read' right. Restrict the exception as
index 6aeee68..553995c 100644 (file)
@@ -44,7 +44,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
         * which may not necessarily be the same as the local DB.
         *
         * TODO: allow querying non-local repos.
-        * @return DatabaseBase
+        * @return Database
         */
        protected function getDB() {
                return $this->mRepo->getSlaveDB();
index 060e3e1..b64b2c8 100644 (file)
@@ -166,12 +166,20 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
                $orderby[] = "rev_id $sort";
                $this->addOption( 'ORDER BY', $orderby );
 
-               $res = $this->select( __METHOD__ );
+               $hookData = [];
+               $res = $this->select( __METHOD__, [], $hookData );
                $pageMap = []; // Maps rev_page to array index
                $count = 0;
                $nextIndex = 0;
                $generated = [];
                foreach ( $res as $row ) {
+                       if ( $count === 0 && $resultPageSet !== null ) {
+                               // Set the non-continue since the list of all revisions is
+                               // prone to having entries added at the start frequently.
+                               $this->getContinuationManager()->addGeneratorNonContinueParam(
+                                       $this, 'continue', "$row->rev_timestamp|$row->rev_id"
+                               );
+                       }
                        if ( ++$count > $this->limit ) {
                                // We've had enough
                                $this->setContinueEnumParameter( 'continue', "$row->rev_timestamp|$row->rev_id" );
@@ -203,12 +211,12 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
                                        ];
                                        ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
                                        ApiQueryBase::addTitleInfo( $a, $title );
-                                       $fit = $result->addValue( [ 'query', $this->getModuleName() ], $index, $a );
+                                       $fit = $this->processRow( $row, $a['revisions'][0], $hookData ) &&
+                                               $result->addValue( [ 'query', $this->getModuleName() ], $index, $a );
                                } else {
                                        $index = $pageMap[$row->rev_page];
-                                       $fit = $result->addValue(
-                                               [ 'query', $this->getModuleName(), $index, 'revisions' ],
-                                               null, $rev );
+                                       $fit = $this->processRow( $row, $rev, $hookData ) &&
+                                               $result->addValue( [ 'query', $this->getModuleName(), $index, 'revisions' ], null, $rev );
                                }
                                if ( !$fit ) {
                                        $this->setContinueEnumParameter( 'continue', "$row->rev_timestamp|$row->rev_id" );
index b35eec2..bba5375 100644 (file)
@@ -103,7 +103,7 @@ abstract class ApiQueryBase extends ApiBase {
 
        /**
         * Get the Query database connection (read-only)
-        * @return DatabaseBase
+        * @return Database
         */
        protected function getDB() {
                if ( is_null( $this->mDb ) ) {
@@ -119,7 +119,7 @@ abstract class ApiQueryBase extends ApiBase {
         * @param string $name Name to assign to the database connection
         * @param int $db One of the DB_* constants
         * @param array $groups Query groups
-        * @return DatabaseBase
+        * @return Database
         */
        public function selectNamedDB( $name, $db, $groups ) {
                $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups );
@@ -347,9 +347,12 @@ abstract class ApiQueryBase extends ApiBase {
         *    'options' => ...,
         *    'join_conds' => ...
         *  ]
+        * @param array|null &$hookData If set, the ApiQueryBaseBeforeQuery and
+        *  ApiQueryBaseAfterQuery hooks will be called, and the
+        *  ApiQueryBaseProcessRow hook will be expected.
         * @return ResultWrapper
         */
-       protected function select( $method, $extraQuery = [] ) {
+       protected function select( $method, $extraQuery = [], array &$hookData = null ) {
 
                $tables = array_merge(
                        $this->tables,
@@ -372,11 +375,38 @@ abstract class ApiQueryBase extends ApiBase {
                        isset( $extraQuery['join_conds'] ) ? (array)$extraQuery['join_conds'] : []
                );
 
+               if ( $hookData !== null ) {
+                       Hooks::run( 'ApiQueryBaseBeforeQuery',
+                               [ $this, &$tables, &$fields, &$where, &$options, &$join_conds, &$hookData ]
+                       );
+               }
+
                $res = $this->getDB()->select( $tables, $fields, $where, $method, $options, $join_conds );
 
+               if ( $hookData !== null ) {
+                       Hooks::run( 'ApiQueryBaseAfterQuery', [ $this, $res, &$hookData ] );
+               }
+
                return $res;
        }
 
+       /**
+        * Call the ApiQueryBaseProcessRow hook
+        *
+        * Generally, a module that passed $hookData to self::select() will call
+        * this just before calling ApiResult::addValue(), and treat a false return
+        * here in the same way it treats a false return from addValue().
+        *
+        * @since 1.28
+        * @param object $row Database row
+        * @param array &$data Data to be added to the result
+        * @param array &$hookData Hook data from ApiQueryBase::select()
+        * @return bool Return false if row processing should end with continuation
+        */
+       protected function processRow( $row, array &$data, array &$hookData ) {
+               return Hooks::run( 'ApiQueryBaseProcessRow', [ $this, $row, &$data, &$hookData ] );
+       }
+
        /**
         * @param string $query
         * @param string $protocol
index 67fe0d6..f7b94c7 100644 (file)
@@ -45,6 +45,15 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase {
                $this->mGeneratorPageSet = $generatorPageSet;
        }
 
+       /**
+        * Indicate whether the module is in generator mode
+        * @since 1.28
+        * @return bool
+        */
+       public function isInGeneratorMode() {
+               return $this->mGeneratorPageSet !== null;
+       }
+
        /**
         * Get the PageSet object to work on.
         * If this module is generator, the pageSet object is different from other module's
index cc3ca60..8b11dc2 100644 (file)
@@ -361,9 +361,10 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                $this->token = $params['token'];
                $this->addOption( 'LIMIT', $params['limit'] + 1 );
 
+               $hookData = [];
                $count = 0;
                /* Perform the actual query. */
-               $res = $this->select( __METHOD__ );
+               $res = $this->select( __METHOD__, [], $hookData );
 
                $revids = [];
                $titles = [];
@@ -372,6 +373,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
 
                /* Iterate through the rows, adding data extracted from them to our query result. */
                foreach ( $res as $row ) {
+                       if ( $count === 0 && $resultPageSet !== null ) {
+                               // Set the non-continue since the list of recentchanges is
+                               // prone to having entries added at the start frequently.
+                               $this->getContinuationManager()->addGeneratorNonContinueParam(
+                                       $this, 'continue', "$row->rc_timestamp|$row->rc_id"
+                               );
+                       }
                        if ( ++$count > $params['limit'] ) {
                                // We've reached the one extra which shows that there are
                                // additional pages to be had. Stop here...
@@ -384,7 +392,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                                $vals = $this->extractRowInfo( $row );
 
                                /* Add that row's data to our final output. */
-                               $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
+                               $fit = $this->processRow( $row, $vals, $hookData ) &&
+                                       $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
                                if ( !$fit ) {
                                        $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
                                        break;
index b816f43..3259927 100644 (file)
@@ -313,7 +313,8 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
 
                $count = 0;
                $generated = [];
-               $res = $this->select( __METHOD__ );
+               $hookData = [];
+               $res = $this->select( __METHOD__, [], $hookData );
 
                foreach ( $res as $row ) {
                        if ( ++$count > $this->limit ) {
@@ -350,7 +351,8 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
                                        }
                                }
 
-                               $fit = $this->addPageSubItem( $row->rev_page, $rev, 'rev' );
+                               $fit = $this->processRow( $row, $rev, $hookData ) &&
+                                       $this->addPageSubItem( $row->rev_page, $rev, 'rev' );
                                if ( !$fit ) {
                                        if ( $enumRevMode ) {
                                                $this->setContinueEnumParameter( 'continue',
index f46b5d2..6be5198 100644 (file)
@@ -24,8 +24,6 @@
  * @file
  */
 
-use MediaWiki\MediaWikiServices;
-
 /**
  * Query module to perform full text search within wiki titles and content
  *
@@ -77,6 +75,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                // Create search engine instance and set options
                $search = $this->buildSearchEngine( $params );
                $search->setFeatureData( 'rewrite', (bool)$params['enablerewrites'] );
+               $search->setFeatureData( 'interwiki', (bool)$interwiki );
 
                $query = $search->transformSearchTerm( $query );
                $query = $search->replacePrefixes( $query );
@@ -235,7 +234,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                                                $vals = [
                                                        'namespace' => $result->getInterwikiNamespaceText(),
                                                        'title' => $title->getText(),
-                                                       'url' => $title->getFullUrl(),
+                                                       'url' => $title->getFullURL(),
                                                ];
 
                                                // Add item to results and see whether it fits
index f92a916..b85bec4 100644 (file)
@@ -111,8 +111,9 @@ class ApiQueryContributions extends ApiQueryBase {
 
                $this->prepareQuery();
 
+               $hookData = [];
                // Do the actual query.
-               $res = $this->select( __METHOD__ );
+               $res = $this->select( __METHOD__, [], $hookData );
 
                if ( $this->fld_sizediff ) {
                        $revIds = [];
@@ -139,7 +140,8 @@ class ApiQueryContributions extends ApiQueryBase {
                        }
 
                        $vals = $this->extractRowInfo( $row );
-                       $fit = $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
+                       $fit = $this->processRow( $row, $vals, $hookData ) &&
+                               $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
                        if ( !$fit ) {
                                $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
                                break;
index e308ba4..6e27fc8 100644 (file)
@@ -406,12 +406,12 @@ class ApiResult implements ApiSerializable {
                $arr = &$this->path( $path, ( $flags & ApiResult::ADD_ON_TOP ) ? 'prepend' : 'append' );
 
                if ( $this->checkingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) {
-                       // self::valueSize needs the validated value. Then flag
+                       // self::size needs the validated value. Then flag
                        // to not re-validate later.
                        $value = self::validateValue( $value );
                        $flags |= ApiResult::NO_VALIDATE;
 
-                       $newsize = $this->size + self::valueSize( $value );
+                       $newsize = $this->size + self::size( $value );
                        if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
                                /// @todo Add i18n message when replacing calls to ->setWarning()
                                $msg = new ApiRawMessage( 'This result was truncated because it would otherwise ' .
@@ -462,7 +462,7 @@ class ApiResult implements ApiSerializable {
                }
                $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
                if ( $this->checkingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) {
-                       $newsize = $this->size - self::valueSize( $ret );
+                       $newsize = $this->size - self::size( $ret );
                        $this->size = max( $newsize, 0 );
                }
                return $ret;
@@ -1085,17 +1085,15 @@ class ApiResult implements ApiSerializable {
        /**
         * Get the 'real' size of a result item. This means the strlen() of the item,
         * or the sum of the strlen()s of the elements if the item is an array.
-        * @note Once the deprecated public self::size is removed, we can rename
-        *       this back to a less awkward name.
         * @param mixed $value Validated value (see self::validateValue())
         * @return int
         */
-       private static function valueSize( $value ) {
+       private static function size( $value ) {
                $s = 0;
                if ( is_array( $value ) ) {
                        foreach ( $value as $k => $v ) {
                                if ( !self::isMetadataKey( $k ) ) {
-                                       $s += self::valueSize( $v );
+                                       $s += self::size( $v );
                                }
                        }
                } elseif ( is_scalar( $value ) ) {
@@ -1202,310 +1200,6 @@ class ApiResult implements ApiSerializable {
 
        /**@}*/
 
-       /************************************************************************//**
-        * @name   Deprecated
-        * @{
-        */
-
-       /**
-        * Formerly used to enable/disable "raw mode".
-        * @deprecated since 1.25, you shouldn't have been using it in the first place
-        * @since 1.23 $flag parameter added
-        * @param bool $flag Set the raw mode flag to this state
-        */
-       public function setRawMode( $flag = true ) {
-               wfDeprecated( __METHOD__, '1.25' );
-       }
-
-       /**
-        * Returns true, the equivalent of "raw mode" is always enabled now
-        * @deprecated since 1.25, you shouldn't have been using it in the first place
-        * @return bool
-        */
-       public function getIsRawMode() {
-               wfDeprecated( __METHOD__, '1.25' );
-               return true;
-       }
-
-       /**
-        * Get the result's internal data array (read-only)
-        * @deprecated since 1.25, use $this->getResultData() instead
-        * @return array
-        */
-       public function getData() {
-               wfDeprecated( __METHOD__, '1.25' );
-               return $this->getResultData( null, [
-                       'BC' => [],
-                       'Types' => [],
-                       'Strip' => 'all',
-               ] );
-       }
-
-       /**
-        * Disable size checking in addValue(). Don't use this unless you
-        * REALLY know what you're doing. Values added while size checking
-        * was disabled will not be counted (ever)
-        * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK
-        */
-       public function disableSizeCheck() {
-               wfDeprecated( __METHOD__, '1.24' );
-               $this->checkingSize = false;
-       }
-
-       /**
-        * Re-enable size checking in addValue()
-        * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK
-        */
-       public function enableSizeCheck() {
-               wfDeprecated( __METHOD__, '1.24' );
-               $this->checkingSize = true;
-       }
-
-       /**
-        * Alias for self::setValue()
-        *
-        * @since 1.21 int $flags replaced boolean $override
-        * @deprecated since 1.25, use self::setValue() instead
-        * @param array $arr To add $value to
-        * @param string $name Index of $arr to add $value at
-        * @param mixed $value
-        * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
-        *    This parameter used to be boolean, and the value of OVERRIDE=1 was
-        *    specifically chosen so that it would be backwards compatible with the
-        *    new method signature.
-        */
-       public static function setElement( &$arr, $name, $value, $flags = 0 ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               self::setValue( $arr, $name, $value, $flags );
-       }
-
-       /**
-        * Adds a content element to an array.
-        * Use this function instead of hardcoding the '*' element.
-        * @deprecated since 1.25, use self::setContentValue() instead
-        * @param array $arr To add the content element to
-        * @param mixed $value
-        * @param string $subElemName When present, content element is created
-        *  as a sub item of $arr. Use this parameter to create elements in
-        *  format "<elem>text</elem>" without attributes.
-        */
-       public static function setContent( &$arr, $value, $subElemName = null ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               if ( is_array( $value ) ) {
-                       throw new InvalidArgumentException( __METHOD__ . ': Bad parameter' );
-               }
-               if ( is_null( $subElemName ) ) {
-                       self::setContentValue( $arr, 'content', $value );
-               } else {
-                       if ( !isset( $arr[$subElemName] ) ) {
-                               $arr[$subElemName] = [];
-                       }
-                       self::setContentValue( $arr[$subElemName], 'content', $value );
-               }
-       }
-
-       /**
-        * Set indexed tag name on all subarrays of $arr
-        *
-        * Does not set the tag name for $arr itself.
-        *
-        * @deprecated since 1.25, use self::setIndexedTagNameRecursive() instead
-        * @param array $arr
-        * @param string $tag Tag name
-        */
-       public function setIndexedTagName_recursive( &$arr, $tag ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               if ( !is_array( $arr ) ) {
-                       return;
-               }
-               if ( !is_string( $tag ) ) {
-                       throw new InvalidArgumentException( 'Bad tag name' );
-               }
-               foreach ( $arr as $k => &$v ) {
-                       if ( !self::isMetadataKey( $k ) && is_array( $v ) ) {
-                               $v[self::META_INDEXED_TAG_NAME] = $tag;
-                               $this->setIndexedTagName_recursive( $v, $tag );
-                       }
-               }
-       }
-
-       /**
-        * Alias for self::addIndexedTagName()
-        * @deprecated since 1.25, use $this->addIndexedTagName() instead
-        * @param array $path Path to the array, like addValue()'s $path
-        * @param string $tag
-        */
-       public function setIndexedTagName_internal( $path, $tag ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               $this->addIndexedTagName( $path, $tag );
-       }
-
-       /**
-        * Alias for self::addParsedLimit()
-        * @deprecated since 1.25, use $this->addParsedLimit() instead
-        * @param string $moduleName
-        * @param int $limit
-        */
-       public function setParsedLimit( $moduleName, $limit ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               $this->addParsedLimit( $moduleName, $limit );
-       }
-
-       /**
-        * Set the ApiMain for use by $this->beginContinuation()
-        * @since 1.25
-        * @deprecated for backwards compatibility only, do not use
-        * @param ApiMain $main
-        */
-       public function setMainForContinuation( ApiMain $main ) {
-               $this->mainForContinuation = $main;
-       }
-
-       /**
-        * Parse a 'continue' parameter and return status information.
-        *
-        * This must be balanced by a call to endContinuation().
-        *
-        * @since 1.24
-        * @deprecated since 1.25, use ApiContinuationManager instead
-        * @param string|null $continue
-        * @param ApiBase[] $allModules
-        * @param array $generatedModules
-        * @return array
-        */
-       public function beginContinuation(
-               $continue, array $allModules = [], array $generatedModules = []
-       ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               if ( $this->mainForContinuation->getContinuationManager() ) {
-                       throw new UnexpectedValueException(
-                               __METHOD__ . ': Continuation already in progress from ' .
-                               $this->mainForContinuation->getContinuationManager()->getSource()
-                       );
-               }
-
-               // Ugh. If $continue doesn't match that in the request, temporarily
-               // replace the request when creating the ApiContinuationManager.
-               if ( $continue === null ) {
-                       $continue = '';
-               }
-               if ( $this->mainForContinuation->getVal( 'continue', '' ) !== $continue ) {
-                       $oldCtx = $this->mainForContinuation->getContext();
-                       $newCtx = new DerivativeContext( $oldCtx );
-                       $newCtx->setRequest( new DerivativeRequest(
-                               $oldCtx->getRequest(),
-                               [ 'continue' => $continue ] + $oldCtx->getRequest()->getValues(),
-                               $oldCtx->getRequest()->wasPosted()
-                       ) );
-                       $this->mainForContinuation->setContext( $newCtx );
-                       $reset = new ScopedCallback(
-                               [ $this->mainForContinuation, 'setContext' ],
-                               [ $oldCtx ]
-                       );
-               }
-               $manager = new ApiContinuationManager(
-                       $this->mainForContinuation, $allModules, $generatedModules
-               );
-               $reset = null;
-
-               $this->mainForContinuation->setContinuationManager( $manager );
-
-               return [
-                       $manager->isGeneratorDone(),
-                       $manager->getRunModules(),
-               ];
-       }
-
-       /**
-        * @since 1.24
-        * @deprecated since 1.25, use ApiContinuationManager instead
-        * @param ApiBase $module
-        * @param string $paramName
-        * @param string|array $paramValue
-        */
-       public function setContinueParam( ApiBase $module, $paramName, $paramValue ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               if ( $this->mainForContinuation->getContinuationManager() ) {
-                       $this->mainForContinuation->getContinuationManager()->addContinueParam(
-                               $module, $paramName, $paramValue
-                       );
-               }
-       }
-
-       /**
-        * @since 1.24
-        * @deprecated since 1.25, use ApiContinuationManager instead
-        * @param ApiBase $module
-        * @param string $paramName
-        * @param string|array $paramValue
-        */
-       public function setGeneratorContinueParam( ApiBase $module, $paramName, $paramValue ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               if ( $this->mainForContinuation->getContinuationManager() ) {
-                       $this->mainForContinuation->getContinuationManager()->addGeneratorContinueParam(
-                               $module, $paramName, $paramValue
-                       );
-               }
-       }
-
-       /**
-        * Close continuation, writing the data into the result
-        * @since 1.24
-        * @deprecated since 1.25, use ApiContinuationManager instead
-        * @param string $style 'standard' for the new style since 1.21, 'raw' for
-        *   the style used in 1.20 and earlier.
-        */
-       public function endContinuation( $style = 'standard' ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               if ( !$this->mainForContinuation->getContinuationManager() ) {
-                       return;
-               }
-
-               if ( $style === 'raw' ) {
-                       $data = $this->mainForContinuation->getContinuationManager()->getRawContinuation();
-                       if ( $data ) {
-                               $this->addValue( null, 'query-continue', $data, self::ADD_ON_TOP | self::NO_SIZE_CHECK );
-                       }
-               } else {
-                       $this->mainForContinuation->getContinuationManager()->setContinuationIntoResult( $this );
-               }
-       }
-
-       /**
-        * No-op, this is now checked on insert.
-        * @deprecated since 1.25
-        */
-       public function cleanUpUTF8() {
-               wfDeprecated( __METHOD__, '1.25' );
-       }
-
-       /**
-        * Get the 'real' size of a result item. This means the strlen() of the item,
-        * or the sum of the strlen()s of the elements if the item is an array.
-        * @deprecated since 1.25, no external users known and there doesn't seem
-        *  to be any case for such use over just checking the return value from the
-        *  add/set methods.
-        * @param mixed $value
-        * @return int
-        */
-       public static function size( $value ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               return self::valueSize( self::validateValue( $value ) );
-       }
-
-       /**
-        * Converts a Status object to an array suitable for addValue
-        * @deprecated since 1.25, use ApiErrorFormatter::arrayFromStatus()
-        * @param Status $status
-        * @param string $errorType
-        * @return array
-        */
-       public function convertStatusToArray( $status, $errorType = 'error' ) {
-               wfDeprecated( __METHOD__, '1.25' );
-               return $this->errorFormatter->arrayFromStatus( $status, $errorType );
-       }
-
-       /**@}*/
 }
 
 /**
index 8ae1192..fb9c4e6 100644 (file)
@@ -104,7 +104,8 @@ trait SearchApi {
                $searchEngine = MediaWikiServices::getInstance()->newSearchEngine();
                $params = [];
                foreach ( $configs as $paramName => $paramConfig ) {
-                       $profiles = $searchEngine->getProfiles( $paramConfig['profile-type'] );
+                       $profiles = $searchEngine->getProfiles( $paramConfig['profile-type'],
+                               $this->getContext()->getUser() );
                        if ( !$profiles ) {
                                continue;
                        }
@@ -188,4 +189,9 @@ trait SearchApi {
         *  containing 'help-message' and 'profile-type' keys.
         */
        abstract public function getSearchProfileParams();
+
+       /**
+        * @return IContextSource
+        */
+       abstract public function getContext();
 }
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 c7bfb23..3818ce9 100644 (file)
@@ -53,6 +53,7 @@
        "apihelp-createaccount-param-language": "Моўны код, які будзе выстаўлены ўдзельніку па змоўчаньні (неабавязкова, па змоўчаньні мова зьместу).",
        "apihelp-createaccount-example-pass": "Стварэньне ўдзельніка <kbd>testuser</kbd> з паролем <kbd>test123</kbd>.",
        "apihelp-createaccount-example-mail": "Стварэньне ўдзельніка <kbd>testmailuser</kbd> і адпраўка выпадковага паролю электроннай поштай.",
+       "apihelp-edit-param-text": "Зьмест старонкі.",
        "apihelp-query+transcludedin-paramvalue-prop-title": "Назва кожнай старонкі.",
        "apihelp-query+transcludedin-param-limit": "Колькі вяртаць.",
        "apihelp-query+userinfo-paramvalue-prop-acceptlang": "Дублюе загаловак <code>Accept-Language</code>, адасланы кліентам у структураваным фармаце."
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": "Записи в дневника."
index c93d8ba..a533710 100644 (file)
@@ -2,13 +2,23 @@
        "@metadata": {
                "authors": [
                        "Aftabuzzaman",
-                       "Bodhisattwa"
+                       "Bodhisattwa",
+                       "আজিজ"
                ]
        },
+       "apihelp-main-param-format": "আউটপুটের বিন্যাস",
+       "apihelp-main-param-requestid": "এখানে প্রদত্ত যেকোন মান প্রতিক্রিয়ায় অন্তর্ভুক্ত করা হবে। অনুরোধের পার্থক্য করতে ব্যবহার করা যেতে পারে।",
        "apihelp-block-description": "ব্যবহারকারীকে বাধা দিন।",
+       "apihelp-block-param-reason": "বাধার দানের কারণ।",
+       "apihelp-createaccount-description": "নতুন ব্যবহারকারীর অ্যাকাউন্ট তৈরি করুন",
        "apihelp-createaccount-param-name": "ব্যবহারকারী নাম।",
        "apihelp-delete-description": "একটি পাতা মুছে ফেলুন।",
        "apihelp-delete-example-simple": "<kbd>প্রধান পাতা</kbd> মুছে ফেলুন।",
+       "apihelp-edit-param-text": "পাতার বিষয়বস্তু।",
        "apihelp-edit-param-minor": "অনুল্লেখ্য সম্পাদনা।",
+       "apihelp-edit-param-createonly": "পাতাটি আগেই বিদ্যমান থাকলে সম্পদনা করবেন না।",
+       "apihelp-edit-param-contentmodel": "নতুন বিষয়বস্তুর, বিষয়বস্তু-মডেল।",
+       "apihelp-edit-example-edit": "একটি পাতা সম্পাদনা করুন",
+       "apihelp-edit-example-prepend": "একটি পৃষ্ঠার পূর্বে <kbd>_&#95;NOTOC_&#95;</kbd> লিখুন।",
        "apihelp-login-example-login": "প্রবেশ"
 }
index 0b86f10..b5179bb 100644 (file)
@@ -3,7 +3,8 @@
                "authors": [
                        "Gorizon",
                        "Mirzali",
-                       "Kumkumuk"
+                       "Kumkumuk",
+                       "Asmen"
                ]
        },
        "apihelp-main-param-action": "Performansa kamci aksiyon",
@@ -20,7 +21,7 @@
        "apihelp-disabled-description": "Eno modul aktiv niyo.",
        "apihelp-edit-description": "Vıraze û pelan bıvurne.",
        "apihelp-edit-param-text": "Zerreki pele",
-       "apihelp-edit-param-minor": "Vurnayışo qıckek.",
+       "apihelp-edit-param-minor": "Vurriyayışê werdiy",
        "apihelp-edit-param-notminor": "Vurnayışo qıckek niyo.",
        "apihelp-edit-param-bot": "Nê vurnayışi zey boti nişan ke.",
        "apihelp-edit-example-edit": "Şeker bıvurne",
@@ -35,7 +36,7 @@
        "apihelp-feedcontributions-param-feedformat": "Formata warikerdışi",
        "apihelp-feedcontributions-param-hideminor": "Vuryayışanê werdiyan bınımne",
        "apihelp-feedcontributions-param-showsizediff": "Goreyê ebati ferqê versiyoni bıasne.",
-       "apihelp-feedrecentchanges-param-hideminor": "Vurnayışanê qıckekan bınımne.",
+       "apihelp-feedrecentchanges-param-hideminor": "Vurriyayışanê werdiyan bınımne.",
        "apihelp-feedrecentchanges-param-hidebots": "Vurnayışanê botan bınımne.",
        "apihelp-feedrecentchanges-param-hideanons": "Vurnayışanê karberanê anoniman bınımne.",
        "apihelp-feedrecentchanges-param-hideliu": "Vurnayışanê karberanê qeydınan bınımne.",
index 40388f9..05f606d 100644 (file)
@@ -13,6 +13,7 @@
        "apihelp-main-param-smaxage": "Set the <code>s-maxage</code> HTTP cache control header to this many seconds. Errors are never cached.",
        "apihelp-main-param-maxage": "Set the <code>max-age</code> HTTP cache control header to this many seconds. Errors are never cached.",
        "apihelp-main-param-assert": "Verify the user is logged in if set to <kbd>user</kbd>, or has the bot user right if <kbd>bot</kbd>.",
+       "apihelp-main-param-assertuser": "Verify the current user is the named user.",
        "apihelp-main-param-requestid": "Any value given here will be included in the response. May be used to distinguish requests.",
        "apihelp-main-param-servedby": "Include the hostname that served the request in the results.",
        "apihelp-main-param-curtimestamp": "Include the current timestamp in the result.",
index ef9d7d1..d535a5d 100644 (file)
@@ -37,6 +37,7 @@
        "apihelp-main-param-smaxage": "Fixer l’entête HTTP de contrôle de cache <code>s-maxage</code> à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.",
        "apihelp-main-param-maxage": "Fixer l’entête HTTP de contrôle de cache <code>max-age</code> à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.",
        "apihelp-main-param-assert": "Vérifier si l’utilisateur est connecté si positionné à <kbd>user</kbd>, ou s'il a le droit d'un utilisateur robot si positionné à <kbd>bot</kbd>.",
+       "apihelp-main-param-assertuser": "Vérifier que l’utilisateur courant est l’utilisateur nommé.",
        "apihelp-main-param-requestid": "Toute valeur fournie ici sera incluse dans la réponse. Peut être utilisé pour distinguer des demandes.",
        "apihelp-main-param-servedby": "Inclure le nom d’hôte qui a renvoyé la requête dans les résultats.",
        "apihelp-main-param-curtimestamp": "Inclure l’horodatage actuel dans le résultat.",
index 79e36cf..9c8e67c 100644 (file)
@@ -20,6 +20,7 @@
        "apihelp-main-param-smaxage": "Fixar a cabeceira HTTP de control de caché <code>s-maxage</code> a esos segundos. Os erros nunca se gardan na caché.",
        "apihelp-main-param-maxage": "Fixar a cabeceira HTTP de control de caché <code>max-age</code> a esos segundos. Os erros nunca se gardan na caché.",
        "apihelp-main-param-assert": "Verificar se o usuario está conectado como <kbd>usuario</kbd> ou ten a marca de <kbd>bot</kbd>.",
+       "apihelp-main-param-assertuser": "Verificar que o usuario actual é o usuario nomeado.",
        "apihelp-main-param-requestid": "Calquera valor dado aquí será incluído na resposta. Pode usarse para distingir peticións.",
        "apihelp-main-param-servedby": "Inclúa o nome do servidor que servía a solicitude nos resultados.",
        "apihelp-main-param-curtimestamp": "Incluir a marca de tempo actual no resultado.",
index dd0d07b..73b4420 100644 (file)
@@ -21,6 +21,7 @@
        "apihelp-main-param-smaxage": "הגדרת כותרת בקרת מטמון HTTP‏ <code>s-maxage</code> למספר כזה של שניות.",
        "apihelp-main-param-maxage": "הגדרת כותרת בקרת מטמון HTTP‏ <code>max-age</code> למספר כזה של שניות.",
        "apihelp-main-param-assert": "לוודא שהמשתמש נכנס אם זה מוגדר ל־<kbd>user</kbd>, או שיש לו הרשאת בוט אם זה <kbd>bot</kbd>.",
+       "apihelp-main-param-assertuser": "לוודא שהמשתמש הנוכחי הוא המשתמש ששמו ניתן.",
        "apihelp-main-param-requestid": "כל ערך שיינתן כאן ייכלל בתשובה. אפשר להשתמש בזה כדי להבדיל בין בקשות.",
        "apihelp-main-param-servedby": "לכלול את שם המארח ששירת את הבקשה בתוצאות.",
        "apihelp-main-param-curtimestamp": "הכללת חותם־הזמן הנוכחי בתוצאה.",
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 0dfd5f5..c30110a 100644 (file)
                        "Darellur",
                        "The Polish",
                        "Matma Rex",
-                       "Sethakill"
+                       "Sethakill",
+                       "Woytecr"
                ]
        },
+       "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Dokumentacja]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista dyskusyjna]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Ogłoszenia dotyczące API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Błędy i propozycje]\n</div>\n<strong>Stan:</strong> Wszystkie funkcje opisane na tej stronie powinny działać, ale API nadal jest aktywnie rozwijane i mogą się zmienić w dowolnym czasie. Subskrybuj [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ listę dyskusyjną mediawiki-api-announce], aby móc na bieżąco dowiadywać się o aktualizacjach.\n\n<strong>Błędne żądania:</strong> Gdy zostanie wysłane błędne żądanie do API, zostanie wysłany w odpowiedzi nagłówek HTTP z kluczem \"MediaWiki-API-Error\" i zarówno jego wartość jak i wartość kodu błędu wysłanego w odpowiedzi będą miały taką samą wartość. Aby uzyskać więcej informacji, zobacz [[mw:API:Errors_and_warnings|API: Błędy i ostrzeżenia]].\n\n<strong>Testowanie:</strong> Aby łatwo testować żądania API, zobacz [[Special:ApiSandbox]].",
        "apihelp-main-param-action": "Wybierz akcję do wykonania.",
        "apihelp-main-param-format": "Format danych wyjściowych.",
        "apihelp-main-param-maxlag": "Maksymalne opóźnienie mogą być używane kiedy MediaWiki jest zainstalowana w klastrze zreplikowanej bazy danych. By zapisać działania powodujące większe opóźnienie replikacji, ten parametr może wymusić czekanie u klienta, dopóki opóźnienie replikacji jest mniejsze niż określona wartość. W przypadku nadmiernego opóźnienia, kod błędu <samp>maxlag</samp> jest zwracany z wiadomością jak <samp>Oczekiwanie na $host: $lag sekund opóźnienia</samp>.<br />Zobacz [[mw:Manual:Maxlag_parameter|Podręcznik:Parametr Maxlag]] by uzyskać więcej informacji.",
+       "apihelp-main-param-smaxage": "Ustaw nagłówek HTTP kontrolujący pamięć podręczną <code>s-maxage</code> na taką ilość sekund. Błędy nie będą nigdy przechowywane w pamięci podręcznej.",
+       "apihelp-main-param-maxage": "Ustaw nagłówek HTTP kontrolujący pamięć podręczną <code>maxage</code> na taką ilość sekund. Błędy nie będą nigdy przechowywane w pamięci podręcznej.",
        "apihelp-main-param-assert": "Zweryfikuj, czy użytkownik jest zalogowany, jeżeli wybrano <kbd>user</kbd>, lub czy ma uprawnienia bota, jeżeli wybrano <kbd>bot</kbd>.",
+       "apihelp-main-param-requestid": "Każda wartość tu podana będzie dołączana do odpowiedzi. Może być użyta do rozróżniania żądań.",
+       "apihelp-main-param-servedby": "Dołącz do odpowiedzi nazwę hosta, który obsłużył żądanie.",
        "apihelp-main-param-curtimestamp": "Dołącz obecny znacznik czasu do wyniku.",
+       "apihelp-main-param-uselang": "Język, w którym mają być pokazywane tłumaczenia wiadomości. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> z <kbd>siprop=languages</kbd> zwróci listę języków lub ustaw jako <kbd>user</kbd>, aby pobrać z preferencji zalogowanego użytkownika lub <kbd>content</kbd>, aby wykorzystać język zawartości tej wiki.",
        "apihelp-block-description": "Zablokuj użytkownika.",
        "apihelp-block-param-user": "Nazwa użytkownika, adres IP lub zakres adresów IP, które chcesz zablokować.",
        "apihelp-block-param-expiry": "Czas trwania. Może być względny (np. <kbd>5 months</kbd> or <kbd>2 weeks</kbd>) lub konkretny (np. <kbd>2014-09-18T12:34:56Z</kbd>). Jeśli jest ustawiony na <kbd>infinite</kbd>, <kbd>indefinite</kbd>, lub <kbd>never</kbd>, blokada nigdy nie wygaśnie.",
        "apihelp-block-param-allowusertalk": "Pozwala użytkownikowi edytować własną stronę dyskusji (zależy od <var>[[mw:Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).",
        "apihelp-block-param-reblock": "Jeżeli ten użytkownik jest już zablokowany, nadpisz blokadę.",
        "apihelp-block-param-watchuser": "Obserwuj stronę użytkownika i jego IP oraz ich strony dyskusji.",
-       "apihelp-block-example-ip-simple": "Zablokuj IP <kbd>192.0.2.5</kbd> na 3 dni za <kbd>Pierwszy atak</kbd>.",
-       "apihelp-block-example-user-complex": "Zablokuj użytkownika <kbd>Vandal</kbd> na zawsze za <kbd>Vandalism</kbd> i uniemożliwij utworzenie nowego konta oraz wysyłanie emaili.",
+       "apihelp-block-example-ip-simple": "Zablokuj IP <kbd>192.0.2.5</kbd> na 3 dni z powodem <kbd>First strike</kbd>.",
+       "apihelp-block-example-user-complex": "Zablokuj użytkownika <kbd>Vandal</kbd> na zawsze z powodem <kbd>Vandalism</kbd> i uniemożliw utworzenie nowego konta oraz wysyłanie emaili.",
+       "apihelp-changeauthenticationdata-description": "Zmień dane logowania bieżącego użytkownika.",
+       "apihelp-changeauthenticationdata-example-password": "Spróbuj zmienić hasło bieżącego użytkownika na <kbd>ExamplePassword</kbd>.",
+       "apihelp-checktoken-description": "Sprawdź poprawność tokenu z <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.",
        "apihelp-checktoken-param-type": "Typ tokenu do przetestowania.",
        "apihelp-checktoken-param-token": "Token do przetestowania.",
        "apihelp-checktoken-param-maxtokenage": "Maksymalny wiek tokenu, w sekundach.",
+       "apihelp-checktoken-example-simple": "Sprawdź poprawność tokenu <kbd>csrf</kbd>.",
+       "apihelp-clearhasmsg-description": "Czyści flagę <code>hasmsg</code> dla bieżącego użytkownika.",
+       "apihelp-clearhasmsg-example-1": "Wyczyść flagę <code>hasmsg</code> dla bieżącego użytkownika.",
        "apihelp-compare-param-fromtitle": "Pierwszy tytuł do porównania.",
        "apihelp-compare-param-fromid": "ID pierwszej strony do porównania.",
        "apihelp-compare-param-fromrev": "Pierwsza wersja do porównania.",
        "apihelp-delete-example-reason": "Usuń <kbd>Main Page</kbd> z powodem <kbd>Preparing for move</kbd>.",
        "apihelp-disabled-description": "Ten moduł został wyłączony.",
        "apihelp-edit-description": "Twórz i edytuj strony.",
+       "apihelp-edit-param-title": "Tytuł strony, którą edytować. Nie może być użyty równocześnie z <var>$1pageid</var>.",
+       "apihelp-edit-param-pageid": "ID strony, którą edytować. Nie może być używany równocześnie z <var>$1title</var>.",
        "apihelp-edit-param-section": "Numer sekcji. <kbd>0</kbd> dla górnej sekcji, <kbd>new</kbd> dla nowej sekcji.",
        "apihelp-edit-param-sectiontitle": "Tytuł nowej sekcji.",
        "apihelp-edit-param-text": "Zawartość strony.",
-       "apihelp-edit-param-tags": "Zmień tagi do przypisania do tej edycji.",
+       "apihelp-edit-param-summary": "Opis edycji. Także tytuł sekcji gdy użyto $1section=new, a nie ustawiono $1sectiontitle.",
+       "apihelp-edit-param-tags": "Znaczniki zmian, które przypisać do tej edycji.",
        "apihelp-edit-param-minor": "Drobna zmiana.",
-       "apihelp-edit-param-notminor": "Nie drobna zmiana.",
+       "apihelp-edit-param-notminor": "Nie oznaczaj tej zmiany jako drobną.",
        "apihelp-edit-param-bot": "Oznacz tę edycję jako edycję bota.",
+       "apihelp-edit-param-basetimestamp": "Czas wersji, która jest edytowana. Służy do wykrywania konfliktów edycji. Można pobrać poprzez [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].",
+       "apihelp-edit-param-starttimestamp": "Czas rozpoczęcia procesu edycji. Służy do wykrywania konfliktów edycji. Odpowiednia wartość może być pobrana za pomocą <var>[[Special:ApiHelp/main|curtimestamp]]</var> podczas rozpoczynania procesu edycji (np. podczas ładowania zawartości strony do edycji).",
+       "apihelp-edit-param-recreate": "Ignoruj błędy o usunięciu strony w międzyczasie.",
        "apihelp-edit-param-createonly": "Nie edytuj strony, jesli już istnieje.",
        "apihelp-edit-param-nocreate": "Zwróć błąd, jeśli strona nie istnieje.",
-       "apihelp-edit-param-watch": "Dodaj stronę do aktualnej listy obserwacji użytkownika.",
-       "apihelp-edit-param-unwatch": "Usuń stronę z aktualnej listy obserwacji użytkownika.",
+       "apihelp-edit-param-watch": "Dodaj stronę do listy obserwowanych bieżącego użytkownika.",
+       "apihelp-edit-param-unwatch": "Usuń stronę z listy obserwowanych bieżącego użytkownika.",
+       "apihelp-edit-param-md5": "Hash MD5 parametru $1text lub złączonych parametrów $1prependtext i $1appendtext. Jeżeli ustawiony, edycja nie zostanie zapisana dopóki hash nie będzie się zgadzać.",
+       "apihelp-edit-param-prependtext": "Tekst do dodania na początku strony. Zastępuje $1text.",
+       "apihelp-edit-param-appendtext": "Tekst do dodania na końcu strony. Zastępuje $1text.\n\nUżyj $1section=new zamiast tego parametru aby dodać nową sekcję.",
+       "apihelp-edit-param-undo": "Wycofaj tę wersję. Zastępuje $1text, $1prependtext i $1appendtext.",
+       "apihelp-edit-param-undoafter": "Wycofaj wszystkie wersje od $1undo do tej. Jeżeli nie ustawiono, wycofaj tylko jedną wersję.",
        "apihelp-edit-param-redirect": "Automatycznie rozwiązuj przekierowania.",
-       "apihelp-edit-example-edit": "Edytuj stronę",
+       "apihelp-edit-param-contentformat": "Format serializacji zawartości wprowadzonego tekstu.",
+       "apihelp-edit-param-contentmodel": "Model zawartości nowego tekstu.",
+       "apihelp-edit-param-token": "Token powinien być wysyłany jako ostatni parametr albo przynajmniej po parametrze $1text.",
+       "apihelp-edit-example-edit": "Edytuj stronę.",
+       "apihelp-edit-example-prepend": "Dopisz <kbd>_&#95;NOTOC_&#95;</kbd> na początku strony.",
        "apihelp-emailuser-description": "Wyślij e‐mail do użytkownika.",
        "apihelp-emailuser-param-target": "Użytkownik, do którego wysyłany jest e-mail.",
        "apihelp-emailuser-param-subject": "Nagłówek tematu.",
        "apihelp-mergehistory-description": "Łączenie historii edycji.",
        "apihelp-mergehistory-param-reason": "Powód łączenia historii.",
        "apihelp-move-description": "Przenieś stronę.",
+       "apihelp-move-param-to": "Tytuł na jaki zmienić nazwę strony.",
        "apihelp-move-param-reason": "Powód zmiany nazwy.",
        "apihelp-move-param-movetalk": "Zmień nazwę strony dyskusji, jeśli istnieje.",
        "apihelp-move-param-movesubpages": "Zmień nazwy podstron, jeśli możliwe.",
        "apihelp-move-param-noredirect": "Nie twórz przekierowania.",
+       "apihelp-move-param-watch": "Dodaj stronę i przekierowanie do listy obserwowanych bieżącego użytkownika.",
+       "apihelp-move-param-unwatch": "Usuń stronę i przekierowanie z listy obserwowanych bieżącego użytkownika.",
        "apihelp-move-param-ignorewarnings": "Ignoruj wszystkie ostrzeżenia.",
+       "apihelp-move-example-move": "Przenieś <kbd>Badtitle</kbd> na <kbd>Goodtitle</kbd> bez pozostawienia przekierowania.",
+       "apihelp-opensearch-description": "Przeszukaj wiki przy użyciu protokołu OpenSearch.",
        "apihelp-opensearch-param-search": "Wyszukaj tekst.",
        "apihelp-opensearch-param-limit": "Maksymalna liczba zwracanych wyników.",
        "apihelp-opensearch-param-namespace": "Przestrzenie nazw do przeszukania.",
+       "apihelp-opensearch-param-suggest": "Nie działa jeżeli <var>[[mw:Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]</var> ustawiono na false.",
+       "apihelp-opensearch-param-redirects": "Jak obsługiwać przekierowania:\n;return:Zwróć samo przekierowanie.\n;resolve:Zwróć stronę docelową. Może zwrócić mniej niż wyników określonych w $1limit.\nZ powodów historycznych, domyślnie jest to \"return\" dla $1format=json, a \"resolve\" dla innych formatów.",
        "apihelp-opensearch-param-format": "Format danych wyjściowych.",
+       "apihelp-opensearch-param-warningsaserror": "Jeżeli pojawią się ostrzeżenia związane z <kbd>format=json</kbd>, zwróć błąd API zamiast ignorowania ich.",
        "apihelp-opensearch-example-te": "Znajdź strony zaczynające się od <kbd>Te</kbd>.",
+       "apihelp-options-description": "Zmienia preferencje bieżącego użytkownika.\n\nMożna ustawiać tylko opcje zarejestrowane w rdzeniu, w zainstalowanych rozszerzeniach lub z kluczami o prefiksie <code>userjs-</code> (do wykorzystywania przez skrypty użytkowników).",
        "apihelp-options-param-reset": "Resetuj preferencje do domyślnych.",
+       "apihelp-options-param-resetkinds": "Lista typów opcji do zresetowania, jeżeli ustawiono opcję <var>$1reset</var>.",
+       "apihelp-options-param-change": "Lista zmian, w formacie nazwa=wartość (np. skin=vector). Wartość nie może zawierać znaku pionowej kreski. Jeżeli nie zostanie podana wartość (a nawet znak równości), np., optionname|otheroption|..., to opcja zostanie zresetowana do jej wartości domyślnej.",
        "apihelp-options-param-optionname": "Nazwa opcji, która powinna być ustawiona na wartość <var>$1optionvalue</var>.",
        "apihelp-options-param-optionvalue": "Wartość opcji, określona w <var>$1optionname</var>, może zawierać znaki pionowej kreski.",
        "apihelp-options-example-reset": "Resetuj wszystkie preferencje.",
+       "apihelp-options-example-change": "Zmień preferencje <kbd>skin</kbd> (skórka) i <kbd>hideminor</kbd> (ukryj drobne edycje).",
+       "apihelp-options-example-complex": "Zresetuj wszystkie preferencje, a następnie ustaw <kbd>skin</kbd> i <kbd>nickname</kbd>.",
        "apihelp-paraminfo-description": "Zdobądź informacje o modułach API.",
+       "apihelp-paraminfo-param-modules": "Lista nazw modułów (wartości parametrów <var>action</var> i <var>format</var> lub <kbd>main</kbd>). Można określić podmoduły za pomocą <kbd>+</kbd> lub wszystkie podmoduły, wpisując <kbd>+*</kbd>, lub wszystkie podmoduły rekursywnie <kbd>+**</kbd>.",
        "apihelp-paraminfo-param-helpformat": "Format tekstów pomocnicznych.",
+       "apihelp-paraminfo-param-querymodules": "Lista nazw modułów zapytań (wartość parametrów <var>prop</var>, <var>meta</var> lub <var>list</var>). Użyj <kbd>$1modules=query+foo</kbd> zamiast <kbd>$1querymodules=foo</kbd>.",
        "apihelp-parse-param-summary": "Powód do analizy.",
+       "apihelp-parse-param-prop": "Jakie porcje informacji otrzymać:",
+       "apihelp-parse-paramvalue-prop-text": "Przetworzony tekst z wikitekstu.",
+       "apihelp-parse-paramvalue-prop-langlinks": "Linki językowe z przetworzonego wikitekstu.",
+       "apihelp-parse-paramvalue-prop-categories": "Kategorie z przetworzonego wikitekstu.",
+       "apihelp-parse-paramvalue-prop-categorieshtml": "Wersja HTML listy kategorii.",
+       "apihelp-parse-paramvalue-prop-links": "Linki wewnętrzne z przetworzonego wikitekstu.",
+       "apihelp-parse-paramvalue-prop-templates": "Szablony z przetworzonego wikitekstu.",
+       "apihelp-parse-paramvalue-prop-images": "Zdjęcia z przetworzonego wikitekstu.",
+       "apihelp-parse-paramvalue-prop-externallinks": "Linki zewnętrzne z przetworzonego wikitekstu.",
+       "apihelp-parse-paramvalue-prop-sections": "Sekcje z przetworzonego wikitekstu.",
        "apihelp-parse-paramvalue-prop-wikitext": "Zwróć oryginalny wikitext, który został przeanalizowany.",
        "apihelp-parse-param-preview": "Analizuj w trybie podglądu.",
        "apihelp-parse-param-disabletoc": "Pomiń spis treści na wyjściu.",
        "apihelp-protect-example-protect": "Zabezpiecz stronę",
        "apihelp-protect-example-unprotect": "Odbezpiecz stronę ustawiając ograniczenia na <kbd>all</kbd> (czyli każdy może wykonać działanie).",
        "apihelp-protect-example-unprotect2": "Odbezpiecz stronę ustawiając brak ograniczeń.",
+       "apihelp-purge-description": "Wyczyść pamięć podręczną dla stron o podanych tytułach.\n\nWymaga wysłania jako żądanie POST jeżeli użytkownik jest niezalogowany.",
        "apihelp-purge-param-forcelinkupdate": "Uaktualnij tabele linków.",
+       "apihelp-purge-param-forcerecursivelinkupdate": "Uaktualnij tabele linków włącznie z linkami dotyczącymi każdej strony wykorzystywanej jako szablon na tej stronie.",
+       "apihelp-purge-example-simple": "Przemiel strony <kbd>Main Page</kbd> i <kbd>API</kbd>.",
        "apihelp-purge-example-generator": "Przeczyść pierwsze 10 stron w przestrzeni głównej.",
        "apihelp-query+allcategories-description": "Emuluj wszystkie kategorie.",
        "apihelp-query+allcategories-param-dir": "Kierunek sortowania.",
        "apihelp-query+allcategories-param-limit": "Liczba kategorii do zwórcenia.",
+       "apihelp-query+allcategories-param-prop": "Jakie właściwości otrzymać:",
+       "apihelp-query+allcategories-paramvalue-prop-size": "Dodaje liczbę stron w kategorii.",
+       "apihelp-query+allcategories-paramvalue-prop-hidden": "Oznacza kategorie ukryte za pomocą <code>_&#95;HIDDENCAT_&#95;</code>.",
+       "apihelp-query+allcategories-example-size": "Wymień kategorie z informacjami o liczbie stron w każdej z nich.",
+       "apihelp-query+alldeletedrevisions-description": "Wymień wszystkie usunięte wersje użytkownika lub z przestrzeni nazw.",
        "apihelp-query+alldeletedrevisions-paraminfo-useronly": "Może być użyte tylko z <var>$3user</var>.",
        "apihelp-query+alldeletedrevisions-paraminfo-nonuseronly": "Nie może być używane z <var>$3user</var>.",
        "apihelp-query+alldeletedrevisions-param-from": "Zacznij nasłuchiwanie na tym tytule.",
        "apihelp-query+alldeletedrevisions-param-to": "Skończ nasłuchiwanie na tym tytule.",
+       "apihelp-query+alldeletedrevisions-param-prefix": "Szukaj tytułów stron zaczynających się na tę wartość.",
        "apihelp-query+alldeletedrevisions-param-tag": "Pokazuj tylko zmiany oznaczone tym znacznikiem.",
        "apihelp-query+alldeletedrevisions-param-user": "Pokazuj tylko zmiany dokonane przez tego użytkownika.",
        "apihelp-query+alldeletedrevisions-param-excludeuser": "Nie pokazuj zmian dokonanych przez tego użytkownika.",
        "apihelp-query+alldeletedrevisions-param-namespace": "Listuj tylko strony z tej przestrzeni nazw.",
+       "apihelp-query+alldeletedrevisions-example-user": "Wymień ostatnie 50 usuniętych edycji przez użytkownika <kbd>Example</kbd>.",
+       "apihelp-query+alldeletedrevisions-example-ns-main": "Wymień ostatnie 50 usuniętych edycji z przestrzeni głównej.",
+       "apihelp-query+allfileusages-description": "Lista wykorzystania pliku, także dla nieistniejących.",
        "apihelp-query+allfileusages-param-limit": "Łączna liczba obiektów do zwrócenia.",
        "apihelp-query+allfileusages-example-unique": "Lista unikatowych tytułów plików.",
        "apihelp-query+allimages-param-sort": "Sortowanie według właściwości.",
        "apihelp-query+blocks-example-simple": "Listuj blokady.",
        "apihelp-query+categories-param-limit": "Liczba kategorii do zwrócenia.",
        "apihelp-query+categorymembers-description": "Wszystkie strony w danej kategorii.",
+       "apihelp-query+categorymembers-param-title": "Kategoria, której zawartość wymienić (wymagane). Musi zawierać prefiks <kbd>{{ns:category}}:</kbd>. Nie może być używany równocześnie z <var>$1pageid</var>.",
+       "apihelp-query+categorymembers-param-pageid": "ID strony kategorii, z której wymienić strony. Nie może być użyty równocześnie z <var>$1title</var>.",
+       "apihelp-query+categorymembers-param-prop": "Jakie informacje dołączyć:",
+       "apihelp-query+categorymembers-paramvalue-prop-ids": "Doda ID strony.",
+       "apihelp-query+categorymembers-paramvalue-prop-title": "Doda tytuł i identyfikator przestrzeni nazw strony.",
+       "apihelp-query+categorymembers-paramvalue-prop-sortkey": "Doda klucz sortowania obowiązujący w danej kategorii (ciąg szesnastkowy).",
+       "apihelp-query+categorymembers-paramvalue-prop-sortkeyprefix": "Doda klucz sortowania obowiązujący w danej kategorii (czytelna przez człowieka część klucza sortowania).",
+       "apihelp-query+categorymembers-paramvalue-prop-type": "Doda informacje o typie strony w kategorii (<samp>page</samp> (strona), <samp>subcat</samp> (podkategoria) lub <samp>file</samp> (plik)).",
        "apihelp-query+categorymembers-param-limit": "Maksymalna liczba zwracanych wyników.",
        "apihelp-query+categorymembers-param-sort": "Sortowanie według właściwości.",
        "apihelp-query+deletedrevisions-param-tag": "Pokazuj tylko zmiany oznaczone tym tagiem.",
        "apihelp-xml-param-xslt": "Jeśli określony, dodaje podaną stronę jako arkusz styli XSL. Powinna to być strona wiki w przestrzeni nazw MediaWiki, której nazwa kończy się na <code>.xsl</code>.",
        "apihelp-xmlfm-description": "Dane wyjściowe w formacie XML (prawidłowo wyświetlane w HTML).",
        "api-format-title": "Wynik MediaWiki API",
+       "api-pageset-param-titles": "Lista tytułów, z którymi pracować.",
+       "api-pageset-param-pageids": "Lista identyfikatorów stron, z którymi pracować.",
+       "api-pageset-param-revids": "Lista identyfikatorów wersji, z którymi pracować.",
+       "api-pageset-param-generator": "Pobierz listę stron, z którymi pracować poprzez wykonanie określonego modułu zapytań.\n\n<strong>Uwaga:</strong> Nazwy parametrów generatora muszą mieć dopisany prefiks \"g\", zobacz przykłady.",
+       "api-pageset-param-redirects-generator": "Automatycznie rozwiązuj przekierowania ze stron podanych w <var>$1titles</var>, <var>$1pageids</var>, oraz <var>$1revids</var>, a także ze stron zwróconych przez <var>$1generator</var>.",
+       "api-pageset-param-converttitles": "Konwertuj tytuły do innych wariantów, jeżeli trzeba. Będzie działać tylko wtedy, gdy język zawartości wiki będzie wspierał konwersje wariantów. Języki, które wspierają konwersję wariantów to m.in. $1.",
        "api-help-title": "Pomoc MediaWiki API",
        "api-help-lead": "To jest automatycznie wygenerowana strona dokumentacji MediaWiki API.\nDokumentacja i przykłady: https://www.mediawiki.org/wiki/API",
        "api-help-main-header": "Moduł główny",
        "api-help-param-deprecated": "Przestarzałe.",
        "api-help-param-required": "Ten parametr jest wymagany.",
        "api-help-datatypes-header": "Typy danych",
+       "api-help-param-type-integer": "Typ: {{PLURAL:$1|1=liczba całkowita|2=lista liczb całkowitych}}",
        "api-help-param-type-boolean": "Typ: wartość logiczna ([[Special:ApiHelp/main#main/datatypes|szczegóły]])",
        "api-help-param-type-timestamp": "Typ: {{PLURAL:$1|1=znacznik czasu|2=lista znaczników czasu}} ([[Special:ApiHelp/main#main/datatypes|dozwolone formaty]])",
        "api-help-param-type-user": "Typ: {{PLURAL:$1|1=nazwa użytkownika|2=lista nazw uzytkowników}}",
-       "api-help-param-list": "{{PLURAL:$1|1=Jedna z następujących wartość|2=Wartości (oddziel za pomocą <kbd>{{!}}</kbd>)}}: $2",
+       "api-help-param-list": "{{PLURAL:$1|1=Jedna z następujących wartości|2=Wartości (oddziel za pomocą <kbd>{{!}}</kbd> lub [[Special:ApiHelp/main#main/datatypes|alternatywy]])}}: $2",
        "api-help-param-limit": "Nie więcej niż $1 dozwolone.",
        "api-help-param-limit2": "Nie więcej niż $1 ($2 dla botów) dozwolone.",
        "api-help-param-integer-min": "{{PLURAL:$1|1=Wartość musi być nie mniejsza|2=Wartości muszą być nie mniejsze}} niż $2.",
        "api-help-param-integer-max": "{{PLURAL:$1|1=Wartość musi być nie większa|2=Wartości muszą być nie większe}} niż $3.",
        "api-help-param-integer-minmax": "{{PLURAL:$1|1=Wartość musi|2=Wartości muszą}} być pomiędzy $2 a $3.",
-       "api-help-param-multi-separate": "Oddziel wartości za pomocą <kbd>|</kbd>.",
+       "api-help-param-multi-separate": "Oddziel wartości za pomocą <kbd>|</kbd> lub [[Special:ApiHelp/main#main/datatypes|alternatywy]].",
        "api-help-param-multi-max": "Maksymalna liczba wartości to {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} dla botów).",
        "api-help-param-default": "Domyślnie: $1",
        "api-help-param-default-empty": "Domyślnie: <span class=\"apihelp-empty\">(puste)</span>",
        "api-help-param-token": "Token \"$1\" zdobyty z [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]",
+       "api-help-param-continue": "Gdy będzie dostępnych więcej wyników, użyj tego do ich kontynuowania.",
        "api-help-param-no-description": "<span class=\"apihelp-empty\">(bez opisu)</span>",
        "api-help-examples": "{{PLURAL:$1|Przykład|Przykłady}}:",
        "api-help-permissions": "{{PLURAL:$2|Uprawnienie|Uprawnienia}}:",
        "api-help-permissions-granted-to": "{{PLURAL:$1|Przydzielone dla}}: $2",
+       "api-help-right-apihighlimits": "Użyj wyższych limitów w zapytaniach API (dla zapytań powolnych: $1; dla zapytań szbkich: $2). Limity zapytań powolnych są także stosowane dla parametrów z podanymi wieloma wartościami.",
        "api-credits-header": "Twórcy",
        "api-credits": "Deweloperzy API:\n* Roan Kattouw (główny programista wrzesień 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (twórca, główny programista wrzesień 2006–wrzesień 2007)\n* Brad Jorsch (główny programista 2013–obecnie)\n\nProsimy wysyłać komentarze, sugestie i pytania do mediawiki-api@lists.wikimedia.org\nlub zgłoś błąd na https://phabricator.wikimedia.org/."
 }
index 40d5786..830a21e 100644 (file)
@@ -4,12 +4,14 @@
                        "Vitorvicentevalente",
                        "Fúlvio",
                        "Macofe",
-                       "Jkb8"
+                       "Jkb8",
+                       "Hamilton Abreu"
                ]
        },
        "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentação]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discussão]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anúncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Erros e solicitações]\n</div>\n<strong>Estado:</strong> Todas as funcionalidades mostradas nesta página deveriam estar a funcionar, mas a API ainda está em activo desenvolvimento, e pode ser alterada a qualquer momento. Inscreva-se na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discussão mediawiki-api-announce] para ser informado acerca das actualizações.\n\n<strong>Solicitações erradas:</strong> Quando solicitações erradas são enviadas à API, um cabeçalho em HTTP será enviado com a chave \"MediaWiki-API-Error\" e, em seguida, tanto o valor do cabeçalho quanto o código de erro retornado serão definidos com o mesmo valor. Para mais informação, consulte [[mw:API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Testes:</strong> Para facilitar os testes de solicitações à API, consulte [[Special:ApiSandbox]].",
        "apihelp-main-param-action": "Qual acção a executar.",
        "apihelp-main-param-format": "O formato de saída.",
+       "apihelp-main-param-origin": "Ao aceder à API usando um pedido AJAX entre domínios (CORS), coloque aqui o domínio de origem. Isto tem de ser incluído em todas as verificações prévias, e portanto tem de fazer parte da URI do pedido (e não do conteúdo do POST).\n\nPara pedidos autenticados, este valor tem de corresponder de forma exata a um dos cabeçalhos <code>Origin</code>, portanto tem de ser algo como <kbd>https://en.wikipedia.org</kbd> ou <kbd>https://meta.wikimedia.org</kbd>. Se este parâmetro não for igual ao cabeçalho <code>Origin</code>, será devolvida a resposta 403. Se este parâmetro for igual ao cabeçalho <code>Origin</code> e a origem for permitida (<i>white-listed</i>) os cabeçalhos <code>Access-Control-Allow-Origin</code> e <code>Access-Control-Allow-Credentials</code> serão preenchidos.\n\nPara pedidos não autenticados, especifique o valor <kbd>*</kbd>. Isto fará com que o cabeçalho <code>Access-Control-Allow-Origin</code>\nseja preenchido, mas <code>Access-Control-Allow-Credentials</code> terá o valor <code>false</code> e todos os dados específicos do utilizador serão restringidos.",
        "apihelp-block-description": "Bloquear um utilizador.",
        "apihelp-block-param-user": "Nome de utilizador(a), endereço ou gama de IP que pretende bloquear.",
        "apihelp-block-param-reason": "Motivo do bloqueio.",
@@ -32,7 +34,9 @@
        "apihelp-emailuser-description": "Enviar correio eletrónico a utilizador.",
        "apihelp-emailuser-param-subject": "Assunto.",
        "apihelp-emailuser-param-text": "Texto.",
+       "apihelp-expandtemplates-description": "Expande todas as predefinições em notação wiki.",
        "apihelp-expandtemplates-param-title": "Título da página.",
+       "apihelp-expandtemplates-example-simple": "Expandir a notação wiki <kbd><nowiki>{{Project:Sandbox}}</nowiki></kbd>.",
        "apihelp-feedcontributions-param-feedformat": "O formato do feed.",
        "apihelp-feedcontributions-param-deletedonly": "Mostrar apenas contribuições eliminadas.",
        "apihelp-feedcontributions-param-hideminor": "Ocultar edições menores.",
        "apihelp-query+info-description": "Obter informação básica da página.",
        "apihelp-query+recentchanges-example-simple": "Lista de mudanças recentes",
        "apihelp-query+search-param-enablerewrites": "Habilitar rescrever a pesquisa interna. Alguns motores de busca podem rescrever a consulta para outra que acha dará melhores resultados, como a corrigir erros de ortografia.",
+       "apihelp-query+watchlist-param-owner": "Usado com $1token para aceder à lista de páginas vigiadas de outro utilizador.",
+       "apihelp-query+watchlist-param-token": "Uma chave de segurança (disponível nas [[Special:Preferences#mw-prefsection-watchlist|preferências]] do utilizador) para permitir acesso à lista de páginas vigiadas de outro utilizador.",
+       "apihelp-query+watchlistraw-param-owner": "Usado com $1token para aceder à lista de páginas vigiadas de outro utilizador.",
+       "apihelp-query+watchlistraw-param-token": "Uma chave de segurança (disponível nas [[Special:Preferences#mw-prefsection-watchlist|preferências]] do utilizador) para permitir acesso à lista de páginas vigiadas de outro utilizador.",
        "apihelp-unblock-description": "Desbloquear um utilizador.",
        "apihelp-unblock-param-reason": "Motivo para o desbloqueio.",
        "apihelp-undelete-param-title": "Título da página a restaurar.",
index caa89b5..8deda75 100644 (file)
@@ -22,6 +22,7 @@
        "apihelp-main-param-smaxage": "{{doc-apihelp-param|main|smaxage}}",
        "apihelp-main-param-maxage": "{{doc-apihelp-param|main|maxage}}",
        "apihelp-main-param-assert": "{{doc-apihelp-param|main|assert}}",
+       "apihelp-main-param-assertuser": "{{doc-apihelp-param|main|assertuser}}",
        "apihelp-main-param-requestid": "{{doc-apihelp-param|main|requestid}}",
        "apihelp-main-param-servedby": "{{doc-apihelp-param|main|servedby}}",
        "apihelp-main-param-curtimestamp": "{{doc-apihelp-param|main|curtimestamp}}",
index 573748d..4117204 100644 (file)
@@ -31,6 +31,7 @@
        "apihelp-main-param-smaxage": "设置<code>s-maxage</code> HTTP缓存控制头至这些秒。错误不会缓存。",
        "apihelp-main-param-maxage": "设置<code>max-age</code> HTTP缓存控制头至这些秒。错误不会缓存。",
        "apihelp-main-param-assert": "如果设置为<kbd>user</kbd>就验证用户是否登录,或如果设置为<kbd>bot</kbd>就验证是否有机器人用户权限。",
+       "apihelp-main-param-assertuser": "验证当前用户是命名用户。",
        "apihelp-main-param-requestid": "任何在此提供的值将包含在响应中。可能可以用以区别请求。",
        "apihelp-main-param-servedby": "包含保存结果请求的主机名。",
        "apihelp-main-param-curtimestamp": "在结果中包括当前时间戳。",
        "api-help-permissions-granted-to": "{{PLURAL:$1|授予}}:$2",
        "api-help-right-apihighlimits": "在API查询中使用更高的上限(慢查询:$1;快查询:$2)。慢查询的限制也适用于多值参数。",
        "api-help-open-in-apisandbox": "<small>[在沙盒中打开]</small>",
-       "api-help-authmanager-general-usage": "使用此模块的一般程序是:\n# 通过<kbd>amirequestsfor=$4</kbd>取得来自<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>的可用字段,和来自<kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>的<kbd>$5</kbd>令牌。\n# 向用户显示字段,并获得其提交内容。\n# 发送至此模块,提供<var>$1returnurl</var>及任何相关字段。\n# 在响应中检查<samp>status</samp>。\n#* 如果您收到了<samp>PASS</samp>或<samp>FAIL</samp>,您已经完成。The operation either succeeded or it didn't.\n#* 如果您收到了<samp>UI</samp>,present the new fields to the user and obtain their submission. Then post to this module with <var>$1continue</var> and the relevant fields set, and repeat step 4.\n#* 如果您收到了<samp>REDIRECT</samp>,direct the user to the <samp>redirecttarget</samp> and wait for the return to <var>$1returnurl</var>. Then post to this module with <var>$1continue</var> and any fields passed to the return URL, and repeat step 4.\n#* 如果您收到了<samp>RESTART</samp>,that means the authentication worked but we don't have a linked user account. You might treat this as <samp>UI</samp> or as <samp>FAIL</samp>.",
+       "api-help-authmanager-general-usage": "使用此模块的一般程序是:\n# 通过<kbd>amirequestsfor=$4</kbd>取得来自<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>的可用字段,和来自<kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>的<kbd>$5</kbd>令牌。\n# 向用户显示字段,并获得其提交内容。\n# 发送至此模块,提供<var>$1returnurl</var>及任何相关字段。\n# 在响应中检查<samp>status</samp>。\n#* 如果您收到了<samp>PASS</samp>或<samp>FAIL</samp>,您已经完成。操作要么成功,要么不成功。\n#* 如果您收到了<samp>UI</samp>,present the new fields to the user and obtain their submission. Then post to this module with <var>$1continue</var> and the relevant fields set, and repeat step 4.\n#* 如果您收到了<samp>REDIRECT</samp>,direct the user to the <samp>redirecttarget</samp> and wait for the return to <var>$1returnurl</var>. Then post to this module with <var>$1continue</var> and any fields passed to the return URL, and repeat step 4.\n#* 如果您收到了<samp>RESTART</samp>,that means the authentication worked but we don't have a linked user account. You might treat this as <samp>UI</samp> or as <samp>FAIL</samp>.",
        "api-help-authmanagerhelper-request": "使用此身份验证请求,通过返回自<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>的<samp>id</samp>与<kbd>amirequestsfor=$1</kbd>。",
        "api-help-authmanagerhelper-messageformat": "返回消息使用的格式。",
        "api-help-authmanagerhelper-mergerequestfields": "合并用于所有身份验证请求的字段信息至一个数组中。",
index 51efe56..e223e16 100644 (file)
@@ -1694,9 +1694,9 @@ class AuthManager implements LoggerAwareInterface {
                                        $status = Status::newGood();
                                        $status->warning( 'userexists' );
                                } else {
-                                       $this->logger->error( __METHOD__ . ': {username} failed with message {message}', [
+                                       $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
                                                'username' => $username,
-                                               'message' => $status->getWikiText( null, null, 'en' )
+                                               'msg' => $status->getWikiText( null, null, 'en' )
                                        ] );
                                        $user->setId( 0 );
                                        $user->loadFromId();
index 0339e45..6684fb9 100644 (file)
@@ -81,6 +81,9 @@ class AuthenticationResponse {
        /** @var Message|null I18n message to display in case of UI or FAIL */
        public $message = null;
 
+       /** @var string Whether the $message is an error or warning message, for styling reasons */
+       public $messageType = 'warning';
+
        /**
         * @var string|null Local user name from authentication.
         * May be null if the authentication passed but no local user is known.
@@ -144,6 +147,7 @@ class AuthenticationResponse {
                $ret = new AuthenticationResponse;
                $ret->status = AuthenticationResponse::FAIL;
                $ret->message = $msg;
+               $ret->messageType = 'error';
                return $ret;
        }
 
@@ -172,18 +176,23 @@ class AuthenticationResponse {
        /**
         * @param AuthenticationRequest[] $reqs AuthenticationRequests needed to continue
         * @param Message $msg
+        * @param string $msgtype
         * @return AuthenticationResponse
         * @see AuthenticationResponse::UI
         */
-       public static function newUI( array $reqs, Message $msg ) {
+       public static function newUI( array $reqs, Message $msg, $msgtype = 'warning' ) {
                if ( !$reqs ) {
                        throw new \InvalidArgumentException( '$reqs may not be empty' );
                }
+               if ( $msgtype !== 'warning' && $msgtype !== 'error' ) {
+                       throw new \InvalidArgumentException( $msgtype . ' is not a valid message type.' );
+               }
 
                $ret = new AuthenticationResponse;
                $ret->status = AuthenticationResponse::UI;
                $ret->neededRequests = $reqs;
                $ret->message = $msg;
+               $ret->messageType = $msgtype;
                return $ret;
        }
 
index 055d7ea..d274e18 100644 (file)
@@ -81,6 +81,8 @@ class ButtonAuthenticationRequest extends AuthenticationRequest {
 
        /**
         * @codeCoverageIgnore
+        * @param array $data
+        * @return AuthenticationRequest|static
         */
        public static function __set_state( $data ) {
                if ( !isset( $data['label'] ) ) {
index beb11f4..7f121cd 100644 (file)
@@ -64,7 +64,8 @@ class ConfirmLinkSecondaryAuthenticationProvider extends AbstractSecondaryAuthen
                $req = new ConfirmLinkAuthenticationRequest( $maybeLink );
                return AuthenticationResponse::newUI(
                        [ $req ],
-                       wfMessage( 'authprovider-confirmlink-message' )
+                       wfMessage( 'authprovider-confirmlink-message' ),
+                       'warning'
                );
        }
 
@@ -150,7 +151,8 @@ class ConfirmLinkSecondaryAuthenticationProvider extends AbstractSecondaryAuthen
                                        'linkOk', wfMessage( 'ok' ), wfMessage( 'authprovider-confirmlink-ok-help' )
                                )
                        ],
-                       $combinedStatus->getMessage( 'authprovider-confirmlink-failed' )
+                       $combinedStatus->getMessage( 'authprovider-confirmlink-failed' ),
+                       'error'
                );
        }
 }
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 ],
index ddad54b..3db7e21 100644 (file)
@@ -70,6 +70,8 @@ class PasswordDomainAuthenticationRequest extends PasswordAuthenticationRequest
 
        /**
         * @codeCoverageIgnore
+        * @param array $data
+        * @return AuthenticationRequest|static
         */
        public static function __set_state( $data ) {
                $ret = new static( $data['domainList'] );
index dd97830..45ac3aa 100644 (file)
@@ -58,6 +58,7 @@ class ResetPasswordSecondaryAuthenticationProvider extends AbstractSecondaryAuth
 
        /**
         * Try to reset the password
+        * @param \User $user
         * @param AuthenticationRequest[] $reqs
         * @return AuthenticationResponse
         */
@@ -112,17 +113,17 @@ class ResetPasswordSecondaryAuthenticationProvider extends AbstractSecondaryAuth
 
                $req = AuthenticationRequest::getRequestByClass( $reqs, get_class( $needReq ) );
                if ( !$req || !array_key_exists( 'retype', $req->getFieldInfo() ) ) {
-                       return AuthenticationResponse::newUI( $needReqs, $data->msg );
+                       return AuthenticationResponse::newUI( $needReqs, $data->msg, 'warning' );
                }
 
                if ( $req->password !== $req->retype ) {
-                       return AuthenticationResponse::newUI( $needReqs, new \Message( 'badretype' ) );
+                       return AuthenticationResponse::newUI( $needReqs, new \Message( 'badretype' ), 'error' );
                }
 
                $req->username = $user->getName();
                $status = $this->manager->allowsAuthenticationDataChange( $req );
                if ( !$status->isGood() ) {
-                       return AuthenticationResponse::newUI( $needReqs, $status->getMessage() );
+                       return AuthenticationResponse::newUI( $needReqs, $status->getMessage(), 'error' );
                }
                $this->manager->changeAuthenticationData( $req );
 
index f16423d..9962fa3 100644 (file)
@@ -140,7 +140,7 @@ class TemporaryPasswordPrimaryAuthenticationProvider
                }
 
                $status = $this->checkPasswordValidity( $username, $req->password );
-               if ( !$status->isOk() ) {
+               if ( !$status->isOK() ) {
                        // Fatal, can't log in
                        return AuthenticationResponse::newFail( $status->getMessage() );
                }
index e2123ef..3f6a47d 100644 (file)
@@ -65,13 +65,19 @@ class ThrottlePreAuthenticationProvider extends AbstractPreAuthenticationProvide
        public function setConfig( Config $config ) {
                parent::setConfig( $config );
 
+               $accountCreationThrottle = $this->config->get( 'AccountCreationThrottle' );
+               // Handle old $wgAccountCreationThrottle format (number of attempts per 24 hours)
+               if ( !is_array( $accountCreationThrottle ) ) {
+                       $accountCreationThrottle = [ [
+                               'count' => $accountCreationThrottle,
+                               'seconds' => 86400,
+                       ] ];
+               }
+
                // @codeCoverageIgnoreStart
                $this->throttleSettings += [
                // @codeCoverageIgnoreEnd
-                       'accountCreationThrottle' => [ [
-                               'count' => $this->config->get( 'AccountCreationThrottle' ),
-                               'seconds' => 86400,
-                       ] ],
+                       'accountCreationThrottle' => $accountCreationThrottle,
                        'passwordAttemptThrottle' => $this->config->get( 'PasswordAttemptThrottle' ),
                ];
 
@@ -107,7 +113,9 @@ class ThrottlePreAuthenticationProvider extends AbstractPreAuthenticationProvide
 
                $result = $this->accountCreationThrottle->increase( null, $ip, __METHOD__ );
                if ( $result ) {
-                       return \StatusValue::newFatal( 'acct_creation_throttle_hit', $result['count'] );
+                       $message = wfMessage( 'acct_creation_throttle_hit' )->params( $result['count'] )
+                               ->durationParams( $result['wait'] );
+                       return \StatusValue::newFatal( $message );
                }
 
                return \StatusValue::newGood();
index 9dfabfd..9e6cf1e 100644 (file)
@@ -139,7 +139,7 @@ class BacklinkCache {
        /**
         * Get the replica DB connection to the database
         * When non existing, will initialize the connection.
-        * @return DatabaseBase
+        * @return Database
         */
        protected function getDB() {
                if ( !isset( $this->db ) ) {
index 360420b..e25f882 100644 (file)
@@ -157,12 +157,6 @@ abstract class FileCacheBase {
         * @return string Compressed text
         */
        public function saveText( $text ) {
-               global $wgUseFileCache;
-
-               if ( !$wgUseFileCache ) {
-                       return false;
-               }
-
                if ( $this->useGzip() ) {
                        $text = gzencode( $text );
                }
index 71011e0..ae8efa9 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup Cache
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Page view caching in the file system.
  * The only cacheable actions are "view" and "history". Also special pages
@@ -31,6 +33,7 @@
 class HTMLFileCache extends FileCacheBase {
        const MODE_NORMAL = 0; // normal cache mode
        const MODE_OUTAGE = 1; // fallback cache for DB outages
+       const MODE_REBUILD = 2; // background cache rebuild mode
 
        /**
         * Construct an HTMLFileCache object from a Title and an action
@@ -52,6 +55,7 @@ class HTMLFileCache extends FileCacheBase {
         */
        public function __construct( $title, $action ) {
                parent::__construct();
+
                $allowedTypes = self::cacheablePageActions();
                if ( !in_array( $action, $allowedTypes ) ) {
                        throw new MWException( 'Invalid file cache type given.' );
@@ -96,16 +100,15 @@ class HTMLFileCache extends FileCacheBase {
        /**
         * Check if pages can be cached for this request/user
         * @param IContextSource $context
-        * @param integer $mode One of the HTMLFileCache::MODE_* constants
+        * @param integer $mode One of the HTMLFileCache::MODE_* constants (since 1.28)
         * @return bool
         */
        public static function useFileCache( IContextSource $context, $mode = self::MODE_NORMAL ) {
-               global $wgUseFileCache, $wgDebugToolbar, $wgContLang;
+               $config = MediaWikiServices::getInstance()->getMainConfig();
 
-               if ( !$wgUseFileCache ) {
+               if ( !$config->get( 'UseFileCache' ) && $mode !== self::MODE_REBUILD ) {
                        return false;
-               }
-               if ( $wgDebugToolbar ) {
+               } elseif ( $config->get( 'DebugToolbar' ) ) {
                        wfDebug( "HTML file cache skipped. \$wgDebugToolbar on\n" );
 
                        return false;
@@ -133,7 +136,7 @@ class HTMLFileCache extends FileCacheBase {
                $ulang = $context->getLanguage();
 
                // Check that there are no other sources of variation
-               if ( $user->getId() || !$ulang->equals( $wgContLang ) ) {
+               if ( $user->getId() || $ulang->getCode() !== $config->get( 'LanguageCode' ) ) {
                        return false;
                }
 
@@ -154,7 +157,8 @@ class HTMLFileCache extends FileCacheBase {
         * @return void
         */
        public function loadFromFileCache( IContextSource $context, $mode = self::MODE_NORMAL ) {
-               global $wgMimeType, $wgContLang;
+               global $wgContLang;
+               $config = MediaWikiServices::getInstance()->getMainConfig();
 
                wfDebug( __METHOD__ . "()\n" );
                $filename = $this->cachePath();
@@ -165,8 +169,8 @@ class HTMLFileCache extends FileCacheBase {
                }
 
                $context->getOutput()->sendCacheControl();
-               header( "Content-Type: $wgMimeType; charset=UTF-8" );
-               header( 'Content-Language: ' . $wgContLang->getHtmlCode() );
+               header( "Content-Type: {$config->get( 'MimeType' )}; charset=UTF-8" );
+               header( "Content-Language: {$wgContLang->getHtmlCode()}" );
                if ( $this->useGzip() ) {
                        if ( wfClientAcceptsGzip() ) {
                                header( 'Content-Encoding: gzip' );
@@ -179,19 +183,24 @@ class HTMLFileCache extends FileCacheBase {
                } else {
                        readfile( $filename );
                }
+
                $context->getOutput()->disable(); // tell $wgOut that output is taken care of
        }
 
        /**
         * Save this cache object with the given text.
         * Use this as an ob_start() handler.
+        *
+        * Normally this is only registed as a handler if $wgUseFileCache is on.
+        * If can be explicitly called by rebuildFileCache.php when it takes over
+        * handling file caching itself, disabling any automatic handling the the
+        * process.
+        *
         * @param string $text
-        * @return bool Whether $wgUseFileCache is enabled
+        * @return string|bool The annotated $text or false on error
         */
        public function saveToFileCache( $text ) {
-               global $wgUseFileCache;
-
-               if ( !$wgUseFileCache || strlen( $text ) < 512 ) {
+               if ( strlen( $text ) < 512 ) {
                        // Disabled or empty/broken output (OOM and PHP errors)
                        return $text;
                }
@@ -234,9 +243,9 @@ class HTMLFileCache extends FileCacheBase {
         * @return bool Whether $wgUseFileCache is enabled
         */
        public static function clearFileCache( Title $title ) {
-               global $wgUseFileCache;
+               $config = MediaWikiServices::getInstance()->getMainConfig();
 
-               if ( !$wgUseFileCache ) {
+               if ( !$config->get( 'UseFileCache' ) ) {
                        return false;
                }
 
index 8a4d061..d773fff 100644 (file)
@@ -236,7 +236,7 @@ class LinkBatch {
         * Construct a WHERE clause which will match all the given titles.
         *
         * @param string $prefix The appropriate table's field name prefix ('page', 'pl', etc)
-        * @param IDatabase $db DatabaseBase object to use
+        * @param IDatabase $db DB object to use
         * @return string|bool String with SQL where clause fragment, or false if no items.
         */
        public function constructSet( $prefix, $db ) {
index e871855..f393acd 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  * @ingroup Cache
  */
+use MediaWiki\MediaWikiServices;
 
 /**
  * MediaWiki message cache structure version.
@@ -154,9 +155,9 @@ class MessageCache {
                $this->mExpiry = $expiry;
 
                if ( $wgUseLocalMessageCache ) {
-                       $this->localCache = ObjectCache::getLocalServerInstance( CACHE_NONE );
+                       $this->localCache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
                } else {
-                       $this->localCache = wfGetCache( CACHE_NONE );
+                       $this->localCache = new EmptyBagOStuff();
                }
 
                $this->wanCache = ObjectCache::getMainWANInstance();
index ca6ba48..15a00c7 100644 (file)
@@ -164,7 +164,7 @@ class ChangesFeed {
 
        /**
         * Generate the feed items given a row from the database, printing the feed.
-        * @param object $rows DatabaseBase resource with recentchanges rows
+        * @param object $rows IDatabase resource with recentchanges rows
         * @param ChannelFeed $feed
         */
        public static function generateFeed( $rows, &$feed ) {
@@ -178,7 +178,7 @@ class ChangesFeed {
 
        /**
         * Generate the feed items given a row from the database.
-        * @param object $rows DatabaseBase resource with recentchanges rows
+        * @param object $rows IDatabase resource with recentchanges rows
         * @return array
         */
        public static function buildItems( $rows ) {
index 590fd37..1262f2c 100644 (file)
@@ -90,6 +90,11 @@ class RecentChange {
         */
        public $counter = -1;
 
+       /**
+        * @var array List of tags to apply
+        */
+       private $tags = [];
+
        /**
         * @var array Array of change types
         */
@@ -297,7 +302,8 @@ class RecentChange {
                }
 
                # If our database is strict about IP addresses, use NULL instead of an empty string
-               if ( $dbw->strictIPs() && $this->mAttribs['rc_ip'] == '' ) {
+               $strictIPs = in_array( $dbw->getType(), [ 'oracle', 'postgres' ] ); // legacy
+               if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) {
                        unset( $this->mAttribs['rc_ip'] );
                }
 
@@ -312,7 +318,7 @@ class RecentChange {
                $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'recentchanges_rc_id_seq' );
 
                # # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
-               if ( $dbw->cascadingDeletes() && $this->mAttribs['rc_cur_id'] == 0 ) {
+               if ( $this->mAttribs['rc_cur_id'] == 0 ) {
                        unset( $this->mAttribs['rc_cur_id'] );
                }
 
@@ -325,6 +331,11 @@ class RecentChange {
                # Notify extensions
                Hooks::run( 'RecentChange_save', [ &$this ] );
 
+               if ( count( $this->tags ) ) {
+                       ChangeTags::addTags( $this->tags, $this->mAttribs['rc_id'],
+                               $this->mAttribs['rc_this_oldid'], $this->mAttribs['rc_logid'], null, $this );
+               }
+
                # Notify external application via UDP
                if ( !$noudp ) {
                        $this->notifyRCFeeds();
@@ -609,14 +620,11 @@ class RecentChange {
 
                DeferredUpdates::addCallableUpdate(
                        function () use ( $rc, $tags ) {
+                               $rc->addTags( $tags );
                                $rc->save();
                                if ( $rc->mAttribs['rc_patrolled'] ) {
                                        PatrolLog::record( $rc, true, $rc->getPerformer() );
                                }
-                               if ( count( $tags ) ) {
-                                       ChangeTags::addTags( $tags, $rc->mAttribs['rc_id'],
-                                               $rc->mAttribs['rc_this_oldid'], null, null );
-                               }
                        },
                        DeferredUpdates::POSTSEND,
                        wfGetDB( DB_MASTER )
@@ -685,14 +693,11 @@ class RecentChange {
 
                DeferredUpdates::addCallableUpdate(
                        function () use ( $rc, $tags ) {
+                               $rc->addTags( $tags );
                                $rc->save();
                                if ( $rc->mAttribs['rc_patrolled'] ) {
                                        PatrolLog::record( $rc, true, $rc->getPerformer() );
                                }
-                               if ( count( $tags ) ) {
-                                       ChangeTags::addTags( $tags, $rc->mAttribs['rc_id'],
-                                               $rc->mAttribs['rc_this_oldid'], null, null );
-                               }
                        },
                        DeferredUpdates::POSTSEND,
                        wfGetDB( DB_MASTER )
@@ -1025,4 +1030,20 @@ class RecentChange {
 
                return $unserializedParams;
        }
+
+       /**
+        * Tags to append to the recent change,
+        * and associated revision/log
+        *
+        * @since 1.28
+        *
+        * @param string|array $tags
+        */
+       public function addTags( $tags ) {
+               if ( is_string( $tags ) ) {
+                       $this->tags[] = $tags;
+               } else {
+                       $this->tags = array_merge( $tags, $this->tags );
+               }
+       }
 }
index 3ef9641..76dd754 100644 (file)
@@ -124,14 +124,16 @@ class ChangeTags {
         * @param int|null $rev_id The rev_id of the change to add the tags to
         * @param int|null $log_id The log_id of the change to add the tags to
         * @param string $params Params to put in the ct_params field of table 'change_tag'
+        * @param RecentChange|null $rc Recent change, in case the tagging accompanies the action
+        * (this should normally be the case)
         *
         * @throws MWException
         * @return bool False if no changes are made, otherwise true
         */
        public static function addTags( $tags, $rc_id = null, $rev_id = null,
-               $log_id = null, $params = null
+               $log_id = null, $params = null, RecentChange $rc = null
        ) {
-               $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params );
+               $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
                return (bool)$result[0];
        }
 
@@ -154,6 +156,9 @@ class ChangeTags {
         * Pass a variable whose value is null if the log_id is not relevant or unknown.
         * @param string $params Params to put in the ct_params field of table
         * 'change_tag' when adding tags
+        * @param RecentChange|null $rc Recent change being tagged, in case the tagging accompanies
+        * the action
+        * @param User|null $user Tagging user, in case the tagging is subsequent to the tagged action
         *
         * @throws MWException When $rc_id, $rev_id and $log_id are all null
         * @return array Index 0 is an array of tags actually added, index 1 is an
@@ -162,9 +167,9 @@ class ChangeTags {
         *
         * @since 1.25
         */
-       public static function updateTags(
-               $tagsToAdd, $tagsToRemove,
-               &$rc_id = null, &$rev_id = null, &$log_id = null, $params = null
+       public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
+               &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
+               User $user = null
        ) {
 
                $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
@@ -284,6 +289,10 @@ class ChangeTags {
                }
 
                self::purgeTagUsageCache();
+
+               Hooks::run( 'ChangeTagsAfterUpdateTags', [ $tagsToAdd, $tagsToRemove, $prevTags,
+                       $rc_id, $rev_id, $log_id, $params, $rc, $user ] );
+
                return [ $tagsToAdd, $tagsToRemove, $prevTags ];
        }
 
@@ -546,7 +555,7 @@ class ChangeTags {
 
                // do it!
                list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
-                       $tagsToRemove, $rc_id, $rev_id, $log_id, $params );
+                       $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
                if ( !$tagsAdded && !$tagsRemoved ) {
                        // no-op, don't log it
                        return Status::newGood( (object)[
@@ -571,7 +580,7 @@ class ChangeTags {
                        // This function is from revision deletion logic and has nothing to do with
                        // change tags, but it appears to be the only other place in core where we
                        // perform logged actions on log items.
-                       $logEntry->setTarget( RevDelLogList::suggestTarget( 0, [ $log_id ] ) );
+                       $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
                }
 
                if ( !$logEntry->getTarget() ) {
@@ -608,10 +617,10 @@ class ChangeTags {
         * Handles selecting tags, and filtering.
         * Needs $tables to be set up properly, so we can figure out which join conditions to use.
         *
-        * @param string|array $tables Table names, see DatabaseBase::select
-        * @param string|array $fields Fields used in query, see DatabaseBase::select
-        * @param string|array $conds Conditions used in query, see DatabaseBase::select
-        * @param array $join_conds Join conditions, see DatabaseBase::select
+        * @param string|array $tables Table names, see Database::select
+        * @param string|array $fields Fields used in query, see Database::select
+        * @param string|array $conds Conditions used in query, see Database::select
+        * @param array $join_conds Join conditions, see Database::select
         * @param array $options Options, see Database::select
         * @param bool|string $filter_tag Tag to select on
         *
@@ -656,21 +665,15 @@ class ChangeTags {
         * Build a text box to select a change tag
         *
         * @param string $selected Tag to select by default
-        * @param bool $fullForm Affects return value, see below
-        * @param Title $title Title object to send the form to. Used only if $fullForm is true.
         * @param bool $ooui Use an OOUI TextInputWidget as selector instead of a non-OOUI input field
         *        You need to call OutputPage::enableOOUI() yourself.
-        * @return string|array
-        *        - if $fullForm is false: an array of (label, selector).
-        *        - if $fullForm is true: HTML of entire form built around the selector.
+        * @return array an array of (label, selector)
         */
-       public static function buildTagFilterSelector( $selected = '',
-               $fullForm = false, Title $title = null, $ooui = false
-       ) {
+       public static function buildTagFilterSelector( $selected = '', $ooui = false ) {
                global $wgUseTagFilter;
 
                if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) {
-                       return $fullForm ? '' : [];
+                       return [];
                }
 
                $data = [
@@ -697,24 +700,7 @@ class ChangeTags {
                        );
                }
 
-               if ( !$fullForm ) {
-                       return $data;
-               }
-
-               $html = implode( '&#160;', $data );
-               $html .= "\n" .
-                       Xml::element(
-                               'input',
-                               [ 'type' => 'submit', 'value' => wfMessage( 'tag-filter-submit' )->text() ]
-                       );
-               $html .= "\n" . Html::hidden( 'title', $title->getPrefixedText() );
-               $html = Xml::tags(
-                       'form',
-                       [ 'action' => $title->getLocalURL(), 'class' => 'mw-tagfilter-form', 'method' => 'get' ],
-                       $html
-               );
-
-               return $html;
+               return $data;
        }
 
        /**
@@ -1038,7 +1024,7 @@ class ChangeTags {
                // let's not allow error results, as the actual tag deletion succeeded
                if ( !$status->isOK() ) {
                        wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
-                       $status->ok = true;
+                       $status->setOK( true );
                }
 
                // clear the memcache of defined tags
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 530fc76..9c0b96e 100644 (file)
@@ -94,10 +94,12 @@ class IcuCollation extends Collation {
                // Verified by native speakers
                'be' => [ "Ё" ],
                'be-tarask' => [ "Ё" ],
+               'bs' => [ "Č", "Ć", "Dž", "Đ", "Lj", "Nj", "Š", "Ž" ],
+               'cs' => [ "Č", "Ch", "Ř", "Š", "Ž" ],
                'cy' => [ "Ch", "Dd", "Ff", "Ng", "Ll", "Ph", "Rh", "Th" ],
                'en' => [],
-               // RTL, let's put each letter on a new line
                'fa' => [
+                       // RTL, let's put each letter on a new line
                        "آ",
                        "ء",
                        "ه",
@@ -106,15 +108,27 @@ class IcuCollation extends Collation {
                ],
                'fi' => [ "Å", "Ä", "Ö" ],
                'fr' => [],
+               'hr' => [ "Č", "Ć", "Dž", "Đ", "Lj", "Nj", "Š", "Ž" ],
+               'hsb' => [ "Č", "Dź", "Ě", "Ch", "Ł", "Ń", "Ř", "Š", "Ć", "Ž" ],
                'hu' => [ "Cs", "Dz", "Dzs", "Gy", "Ly", "Ny", "Ö", "Sz", "Ty", "Ü", "Zs" ],
                'is' => [ "Á", "Ð", "É", "Í", "Ó", "Ú", "Ý", "Þ", "Æ", "Ö", "Å" ],
                'it' => [],
+               'lt' => [ "Č", "Š", "Ž" ],
                'lv' => [ "Č", "Ģ", "Ķ", "Ļ", "Ņ", "Š", "Ž" ],
+               'mk' => [ "Ѓ", "Ќ" ],
+               'nl' => [],
                'pl' => [ "Ą", "Ć", "Ę", "Ł", "Ń", "Ó", "Ś", "Ź", "Ż" ],
                'pt' => [],
                'ru' => [],
+               'sk' => [ "Ä", "Č", "Ch", "Ô", "Š", "Ž" ],
+               'sr' => [],
                'sv' => [ "Å", "Ä", "Ö" ],
                'sv@collation=standard' => [ "Å", "Ä", "Ö" ],
+               'ta' => [
+                       "\xE0\xAE\x82", "ஃ", "க்ஷ", "க்", "ங்", "ச்", "ஞ்", "ட்", "ண்", "த்", "ந்",
+                       "ப்", "ம்", "ய்", "ர்", "ல்", "வ்", "ழ்", "ள்", "ற்", "ன்", "ஜ்", "ஶ்", "ஷ்",
+                       "ஸ்", "ஹ்", "க்ஷ்"
+               ],
                'uk' => [ "Ґ", "Ь" ],
                'vi' => [ "Ă", "Â", "Đ", "Ê", "Ô", "Ơ", "Ư" ],
                // Not verified, but likely correct
@@ -123,10 +137,8 @@ class IcuCollation extends Collation {
                'az' => [ "Ç", "Ə", "Ğ", "İ", "Ö", "Ş", "Ü" ],
                'bg' => [],
                'br' => [ "Ch", "C'h" ],
-               'bs' => [ "Č", "Ć", "Dž", "Đ", "Lj", "Nj", "Š", "Ž" ],
                'ca' => [],
                'co' => [],
-               'cs' => [ "Č", "Ch", "Ř", "Š", "Ž" ],
                'da' => [ "Æ", "Ø", "Å" ],
                'de' => [],
                'dsb' => [ "Č", "Ć", "Dź", "Ě", "Ch", "Ł", "Ń", "Ŕ", "Š", "Ś", "Ž", "Ź" ],
@@ -141,35 +153,23 @@ class IcuCollation extends Collation {
                'ga' => [],
                'gd' => [],
                'gl' => [ "Ch", "Ll", "Ñ" ],
-               'hr' => [ "Č", "Ć", "Dž", "Đ", "Lj", "Nj", "Š", "Ž" ],
-               'hsb' => [ "Č", "Dź", "Ě", "Ch", "Ł", "Ń", "Ř", "Š", "Ć", "Ž" ],
                'kk' => [ "Ү", "І" ],
                'kl' => [ "Æ", "Ø", "Å" ],
                'ku' => [ "Ç", "Ê", "Î", "Ş", "Û" ],
                'ky' => [ "Ё" ],
                'la' => [],
                'lb' => [],
-               'lt' => [ "Č", "Š", "Ž" ],
-               'mk' => [ "Ѓ", "Ќ" ],
                'mo' => [ "Ă", "Â", "Î", "Ş", "Ţ" ],
                'mt' => [ "Ċ", "Ġ", "Għ", "Ħ", "Ż" ],
-               'nl' => [],
                'no' => [ "Æ", "Ø", "Å" ],
                'oc' => [],
                'rm' => [],
                'ro' => [ "Ă", "Â", "Î", "Ş", "Ţ" ],
                'rup' => [ "Ă", "Â", "Î", "Ľ", "Ń", "Ş", "Ţ" ],
                'sco' => [],
-               'sk' => [ "Ä", "Č", "Ch", "Ô", "Š", "Ž" ],
                'sl' => [ "Č", "Š", "Ž" ],
                'smn' => [ "Á", "Č", "Đ", "Ŋ", "Š", "Ŧ", "Ž", "Æ", "Ø", "Å", "Ä", "Ö" ],
                'sq' => [ "Ç", "Dh", "Ë", "Gj", "Ll", "Nj", "Rr", "Sh", "Th", "Xh", "Zh" ],
-               'sr' => [],
-               'ta' => [
-                       "\xE0\xAE\x82", "ஃ", "க்ஷ", "க்", "ங்", "ச்", "ஞ்", "ட்", "ண்", "த்", "ந்",
-                       "ப்", "ம்", "ய்", "ர்", "ல்", "வ்", "ழ்", "ள்", "ற்", "ன்", "ஜ்", "ஶ்", "ஷ்",
-                       "ஸ்", "ஹ்", "க்ஷ்"
-               ],
                'tk' => [ "Ç", "Ä", "Ž", "Ň", "Ö", "Ş", "Ü", "Ý" ],
                'tl' => [ "Ñ", "Ng" ],
                'tr' => [ "Ç", "Ğ", "İ", "Ö", "Ş", "Ü" ],
diff --git a/includes/compat/ScopedCallback.php b/includes/compat/ScopedCallback.php
new file mode 100644 (file)
index 0000000..4fd4bc7
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Compatibility class for pre-namespace, pre-library class name
+ *
+ * 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
+ */
+
+/**
+ * @deprecated since 1.28 use Wikimedia\ScopedCallback
+ *
+ * @since 1.21
+ */
+class ScopedCallback extends Wikimedia\ScopedCallback {
+}
index 4e50c8e..5755918 100644 (file)
@@ -243,7 +243,7 @@ abstract class ContentHandler {
                }
 
                // Hook can force JS/CSS
-               Hooks::run( 'TitleIsCssOrJsPage', [ $title, &$isCodePage ], '1.25' );
+               Hooks::run( 'TitleIsCssOrJsPage', [ $title, &$isCodePage ], '1.21' );
 
                // Is this a user subpage containing code?
                $isCodeSubpage = NS_USER == $ns
@@ -258,7 +258,7 @@ abstract class ContentHandler {
                $isWikitext = $isWikitext && !$isCodePage && !$isCodeSubpage;
 
                // Hook can override $isWikitext
-               Hooks::run( 'TitleIsWikitextPage', [ $title, &$isWikitext ], '1.25' );
+               Hooks::run( 'TitleIsWikitextPage', [ $title, &$isWikitext ], '1.21' );
 
                if ( !$isWikitext ) {
                        switch ( $ext ) {
@@ -706,7 +706,7 @@ abstract class ContentHandler {
                if ( $title->getNamespace() == NS_MEDIAWIKI ) {
                        // Parse mediawiki messages with correct target language
                        list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
-                       $pageLang = wfGetLangObj( $lang );
+                       $pageLang = Language::factory( $lang );
                }
 
                Hooks::run( 'PageContentLanguage', [ $title, &$pageLang, $wgLang ] );
diff --git a/includes/content/FileContentHandler.php b/includes/content/FileContentHandler.php
new file mode 100644 (file)
index 0000000..26f1190
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Content handler for File: files
+ * TODO: this handler s not used directly now,
+ * but instead manually called by WikitextHandler.
+ * This should be fixed in the future.
+ */
+class FileContentHandler extends WikitextContentHandler  {
+
+       public function getFieldsForSearchIndex( SearchEngine $engine ) {
+               $fields['file_media_type'] =
+                       $engine->makeSearchFieldMapping( 'file_media_type', SearchIndexField::INDEX_TYPE_KEYWORD );
+               $fields['file_media_type']->setFlag( SearchIndexField::FLAG_CASEFOLD );
+               $fields['file_mime'] =
+                       $engine->makeSearchFieldMapping( 'file_mime', SearchIndexField::INDEX_TYPE_SHORT_TEXT );
+               $fields['file_mime']->setFlag( SearchIndexField::FLAG_CASEFOLD );
+               $fields['file_size'] =
+                       $engine->makeSearchFieldMapping( 'file_size', SearchIndexField::INDEX_TYPE_INTEGER );
+               $fields['file_width'] =
+                       $engine->makeSearchFieldMapping( 'file_width', SearchIndexField::INDEX_TYPE_INTEGER );
+               $fields['file_height'] =
+                       $engine->makeSearchFieldMapping( 'file_height', SearchIndexField::INDEX_TYPE_INTEGER );
+               $fields['file_bits'] =
+                       $engine->makeSearchFieldMapping( 'file_bits', SearchIndexField::INDEX_TYPE_INTEGER );
+               $fields['file_resolution'] =
+                       $engine->makeSearchFieldMapping( 'file_resolution', SearchIndexField::INDEX_TYPE_INTEGER );
+               $fields['file_text'] =
+                       $engine->makeSearchFieldMapping( 'file_text', SearchIndexField::INDEX_TYPE_TEXT );
+               return $fields;
+       }
+
+       public function getDataForSearchIndex( WikiPage $page, ParserOutput $parserOutput,
+                                              SearchEngine $engine ) {
+               $fields = [];
+
+               $title = $page->getTitle();
+               if ( NS_FILE != $title->getNamespace() ) {
+                       return [];
+               }
+               $file = wfLocalFile( $title );
+               if ( !$file || !$file->exists() ) {
+                       return [];
+               }
+
+               $handler = $file->getHandler();
+               if ( $handler ) {
+                       $fields['file_text'] = $handler->getEntireText( $file );
+               }
+               $fields['file_media_type'] = $file->getMediaType();
+               $fields['file_mime'] = $file->getMimeType();
+               $fields['file_size'] = $file->getSize();
+               $fields['file_width'] = $file->getWidth();
+               $fields['file_height'] = $file->getHeight();
+               $fields['file_bits'] = $file->getBitDepth();
+               $fields['file_resolution'] =
+                       (int)floor( sqrt( $fields['file_width'] * $fields['file_height'] ) );
+
+               return $fields;
+       }
+
+}
index 55c4ad5..fe12ff7 100644 (file)
@@ -1,7 +1,6 @@
 <?php
 
 use HtmlFormatter\HtmlFormatter;
-use MediaWiki\Logger\LoggerFactory;
 
 /**
  * Class allowing to explore structure of parsed wikitext.
index 978ac44..74b2f1a 100644 (file)
@@ -108,6 +108,14 @@ class WikitextContentHandler extends TextContentHandler {
                return true;
        }
 
+       /**
+        * Get file handler
+        * @return FileContentHandler
+        */
+       protected function getFileHandler() {
+               return new FileContentHandler();
+       }
+
        public function getFieldsForSearchIndex( SearchEngine $engine ) {
                $fields = parent::getFieldsForSearchIndex( $engine );
 
@@ -122,34 +130,12 @@ class WikitextContentHandler extends TextContentHandler {
                        $engine->makeSearchFieldMapping( 'opening_text', SearchIndexField::INDEX_TYPE_TEXT );
                $fields['opening_text']->setFlag( SearchIndexField::FLAG_SCORING |
                                                  SearchIndexField::FLAG_NO_HIGHLIGHT );
-
-               // FIXME: this really belongs in separate file handler but files
-               // do not have separate handler. Sadness.
-               $fields['file_text'] =
-                       $engine->makeSearchFieldMapping( 'file_text', SearchIndexField::INDEX_TYPE_TEXT );
+               // Until we have full first-class content handler for files, we invoke it explicitly here
+               $fields = array_merge( $fields, $this->getFileHandler()->getFieldsForSearchIndex( $engine ) );
 
                return $fields;
        }
 
-       /**
-        * Extract text of the file
-        * TODO: probably should go to file handler?
-        * @param Title $title
-        * @return string|null
-        */
-       protected function getFileText( Title $title ) {
-               $file = wfLocalFile( $title );
-               if ( $file && $file->exists() ) {
-                       $handler = $file->getHandler();
-                       if ( !$handler ) {
-                               return null;
-                       }
-                       return $handler->getEntireText( $file );
-               }
-
-               return null;
-       }
-
        public function getDataForSearchIndex( WikiPage $page, ParserOutput $parserOutput,
                                               SearchEngine $engine ) {
                $fields = parent::getDataForSearchIndex( $page, $parserOutput, $engine );
@@ -162,12 +148,10 @@ class WikitextContentHandler extends TextContentHandler {
                $fields['auxiliary_text'] = $structure->getAuxiliaryText();
                $fields['defaultsort'] = $structure->getDefaultSort();
 
-               $title = $page->getTitle();
-               if ( NS_FILE == $title->getNamespace() ) {
-                       $fileText = $this->getFileText( $title );
-                       if ( $fileText ) {
-                               $fields['file_text'] = $fileText;
-                       }
+               // Until we have full first-class content handler for files, we invoke it explicitly here
+               if ( NS_FILE == $page->getTitle()->getNamespace() ) {
+                       $fields = array_merge( $fields,
+                                       $this->getFileHandler()->getDataForSearchIndex( $page, $parserOutput, $engine ) );
                }
                return $fields;
        }
index b617871..829dd73 100644 (file)
@@ -19,7 +19,6 @@
  * @file
  */
 use Liuggio\StatsdClient\Factory\StatsdDataFactory;
-use MediaWiki\MediaWikiServices;
 
 /**
  * The simplest way of implementing IContextSource is to hold a RequestContext as a
index c87798e..a8cad9f 100644 (file)
@@ -160,7 +160,7 @@ class RequestContext implements IContextSource, MutableContext {
        /**
         * Set the Title object
         *
-        * @param Title $title
+        * @param Title|null $title
         */
        public function setTitle( Title $title = null ) {
                $this->title = $title;
index 01814c1..6a1bbd6 100644 (file)
@@ -53,7 +53,7 @@ abstract class DBAccessBase implements IDBAccessObject {
         * @param int $id Which connection to use
         * @param array $groups Query groups
         *
-        * @return DatabaseBase
+        * @return Database
         */
        protected function getConnection( $id, $groups = [] ) {
                $loadBalancer = wfGetLB( $this->wiki );
@@ -68,9 +68,9 @@ abstract class DBAccessBase implements IDBAccessObject {
         *
         * @since 1.21
         *
-        * @param DatabaseBase $db The database connection to release.
+        * @param Database $db The database connection to release.
         */
-       protected function releaseConnection( DatabaseBase $db ) {
+       protected function releaseConnection( Database $db ) {
                if ( $this->wiki !== false ) {
                        $loadBalancer = $this->getLoadBalancer();
                        $loadBalancer->reuseConnection( $db );
index ee82bdf..f1ccd2a 100644 (file)
@@ -23,6 +23,7 @@
  * @file
  * @ingroup Database
  */
+use MediaWiki\MediaWikiServices;
 
 class CloneDatabase {
        /** @var string Table prefix for cloning */
@@ -40,16 +41,19 @@ class CloneDatabase {
        /** @var bool Whether to use temporary tables or not */
        private $useTemporaryTables = true;
 
+       /** @var Database */
+       private $db;
+
        /**
         * Constructor
         *
-        * @param IDatabase $db A database subclass
+        * @param Database $db A database subclass
         * @param array $tablesToClone An array of tables to clone, unprefixed
         * @param string $newTablePrefix Prefix to assign to the tables
         * @param string $oldTablePrefix Prefix on current tables, if not $wgDBprefix
         * @param bool $dropCurrentTables
         */
-       public function __construct( IDatabase $db, array $tablesToClone,
+       public function __construct( Database $db, array $tablesToClone,
                $newTablePrefix, $oldTablePrefix = '', $dropCurrentTables = true
        ) {
                $this->db = $db;
@@ -130,14 +134,8 @@ class CloneDatabase {
        public static function changePrefix( $prefix ) {
                global $wgDBprefix;
 
-               $lbFactory = wfGetLBFactory();
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
                $lbFactory->setDomainPrefix( $prefix );
-               $lbFactory->forEachLB( function( LoadBalancer $lb ) use ( $prefix ) {
-                       $lb->setDomainPrefix( $prefix );
-                       $lb->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix ) {
-                               $db->tablePrefix( $prefix );
-                       } );
-               } );
                $wgDBprefix = $prefix;
        }
 }
diff --git a/includes/db/Database.php b/includes/db/Database.php
deleted file mode 100644 (file)
index e908824..0000000
+++ /dev/null
@@ -1,3708 +0,0 @@
-<?php
-/**
- * @defgroup Database Database
- *
- * 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
- */
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-
-/**
- * Database abstraction object
- * @ingroup Database
- */
-abstract class DatabaseBase implements IDatabase, 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 */
-       const DEADLOCK_DELAY_MIN = 500000;
-       /** Maximum time to wait before retry */
-       const DEADLOCK_DELAY_MAX = 1500000;
-
-       /** How long before it is worth doing a dummy query to test the connection */
-       const PING_TTL = 1.0;
-       const PING_QUERY = 'SELECT 1 AS ping';
-
-       const TINY_WRITE_SEC = .010;
-       const SLOW_WRITE_SEC = .500;
-       const SMALL_WRITE_ROWS = 100;
-
-       /** @var string SQL query */
-       protected $mLastQuery = '';
-       /** @var bool */
-       protected $mDoneWrites = false;
-       /** @var string|bool */
-       protected $mPHPError = false;
-       /** @var string */
-       protected $mServer;
-       /** @var string */
-       protected $mUser;
-       /** @var string */
-       protected $mPassword;
-       /** @var string */
-       protected $mDBname;
-       /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
-       protected $tableAliases = [];
-       /** @var bool Whether this PHP instance is for a CLI script */
-       protected $cliMode;
-       /** @var string Agent name for query profiling */
-       protected $agent;
-
-       /** @var BagOStuff APC cache */
-       protected $srvCache;
-       /** @var LoggerInterface */
-       protected $connLogger;
-       /** @var LoggerInterface */
-       protected $queryLogger;
-       /** @var callback Error logging callback */
-       protected $errorLogger;
-
-       /** @var resource Database connection */
-       protected $mConn = null;
-       /** @var bool */
-       protected $mOpened = false;
-
-       /** @var array[] List of (callable, method name) */
-       protected $mTrxIdleCallbacks = [];
-       /** @var array[] List of (callable, method name) */
-       protected $mTrxPreCommitCallbacks = [];
-       /** @var array[] List of (callable, method name) */
-       protected $mTrxEndCallbacks = [];
-       /** @var callable[] Map of (name => callable) */
-       protected $mTrxRecurringCallbacks = [];
-       /** @var bool Whether to suppress triggering of transaction end callbacks */
-       protected $mTrxEndCallbacksSuppressed = false;
-
-       /** @var string */
-       protected $mTablePrefix;
-       /** @var string */
-       protected $mSchema;
-       /** @var integer */
-       protected $mFlags;
-       /** @var array */
-       protected $mLBInfo = [];
-       /** @var bool|null */
-       protected $mDefaultBigSelects = null;
-       /** @var array|bool */
-       protected $mSchemaVars = false;
-       /** @var array */
-       protected $mSessionVars = [];
-       /** @var array|null */
-       protected $preparedArgs;
-       /** @var string|bool|null Stashed value of html_errors INI setting */
-       protected $htmlErrors;
-       /** @var string */
-       protected $delimiter = ';';
-
-       /**
-        * Either 1 if a transaction is active or 0 otherwise.
-        * The other Trx fields may not be meaningfull if this is 0.
-        *
-        * @var int
-        */
-       protected $mTrxLevel = 0;
-       /**
-        * Either a short hexidecimal string if a transaction is active or ""
-        *
-        * @var string
-        * @see DatabaseBase::mTrxLevel
-        */
-       protected $mTrxShortId = '';
-       /**
-        * The UNIX time that the transaction started. Callers can assume that if
-        * snapshot isolation is used, then the data is *at least* up to date to that
-        * point (possibly more up-to-date since the first SELECT defines the snapshot).
-        *
-        * @var float|null
-        * @see DatabaseBase::mTrxLevel
-        */
-       private $mTrxTimestamp = null;
-       /** @var float Lag estimate at the time of BEGIN */
-       private $mTrxReplicaLag = null;
-       /**
-        * Remembers the function name given for starting the most recent transaction via begin().
-        * Used to provide additional context for error reporting.
-        *
-        * @var string
-        * @see DatabaseBase::mTrxLevel
-        */
-       private $mTrxFname = null;
-       /**
-        * Record if possible write queries were done in the last transaction started
-        *
-        * @var bool
-        * @see DatabaseBase::mTrxLevel
-        */
-       private $mTrxDoneWrites = false;
-       /**
-        * Record if the current transaction was started implicitly due to DBO_TRX being set.
-        *
-        * @var bool
-        * @see DatabaseBase::mTrxLevel
-        */
-       private $mTrxAutomatic = false;
-       /**
-        * Array of levels of atomicity within transactions
-        *
-        * @var array
-        */
-       private $mTrxAtomicLevels = [];
-       /**
-        * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
-        *
-        * @var bool
-        */
-       private $mTrxAutomaticAtomic = false;
-       /**
-        * Track the write query callers of the current transaction
-        *
-        * @var string[]
-        */
-       private $mTrxWriteCallers = [];
-       /**
-        * @var float Seconds spent in write queries for the current transaction
-        */
-       private $mTrxWriteDuration = 0.0;
-       /**
-        * @var integer Number of write queries for the current transaction
-        */
-       private $mTrxWriteQueryCount = 0;
-       /**
-        * @var float Like mTrxWriteQueryCount but excludes lock-bound, easy to replicate, queries
-        */
-       private $mTrxWriteAdjDuration = 0.0;
-       /**
-        * @var integer Number of write queries counted in mTrxWriteAdjDuration
-        */
-       private $mTrxWriteAdjQueryCount = 0;
-       /**
-        * @var float RTT time estimate
-        */
-       private $mRTTEstimate = 0.0;
-
-       /** @var array Map of (name => 1) for locks obtained via lock() */
-       private $mNamedLocksHeld = [];
-
-       /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
-       private $lazyMasterHandle;
-
-       /**
-        * @since 1.21
-        * @var resource File handle for upgrade
-        */
-       protected $fileHandle = null;
-
-       /**
-        * @since 1.22
-        * @var string[] Process cache of VIEWs names in the database
-        */
-       protected $allViews = null;
-
-       /** @var float UNIX timestamp */
-       protected $lastPing = 0.0;
-
-       /** @var int[] Prior mFlags values */
-       private $priorFlags = [];
-
-       /** @var Profiler */
-       protected $profiler;
-       /** @var TransactionProfiler */
-       protected $trxProfiler;
-
-       /**
-        * Constructor.
-        *
-        * FIXME: It is possible to construct a Database object with no associated
-        * connection object, by specifying no parameters to __construct(). This
-        * feature is deprecated and should be removed.
-        *
-        * IDatabase classes should not be constructed directly in external
-        * code. DatabaseBase::factory() should be used instead.
-        *
-        * @param array $params Parameters passed from DatabaseBase::factory()
-        */
-       function __construct( array $params ) {
-               $server = $params['host'];
-               $user = $params['user'];
-               $password = $params['password'];
-               $dbName = $params['dbname'];
-               $flags = $params['flags'];
-
-               $this->mSchema = $params['schema'];
-               $this->mTablePrefix = $params['tablePrefix'];
-
-               $this->cliMode = isset( $params['cliMode'] )
-                       ? $params['cliMode']
-                       : ( PHP_SAPI === 'cli' );
-               $this->agent = isset( $params['agent'] )
-                       ? str_replace( '/', '-', $params['agent'] ) // escape for comment
-                       : '';
-
-               $this->mFlags = $flags;
-               if ( $this->mFlags & DBO_DEFAULT ) {
-                       if ( $this->cliMode ) {
-                               $this->mFlags &= ~DBO_TRX;
-                       } else {
-                               $this->mFlags |= DBO_TRX;
-                       }
-               }
-
-               $this->mSessionVars = $params['variables'];
-
-               $this->srvCache = isset( $params['srvCache'] )
-                       ? $params['srvCache']
-                       : new HashBagOStuff();
-
-               $this->profiler = isset( $params['profiler'] )
-                       ? $params['profiler']
-                       : Profiler::instance(); // @TODO: remove global state
-               $this->trxProfiler = isset( $params['trxProfiler'] )
-                       ? $params['trxProfiler']
-                       : new TransactionProfiler();
-               $this->connLogger = isset( $params['connLogger'] )
-                       ? $params['connLogger']
-                       : new \Psr\Log\NullLogger();
-               $this->queryLogger = isset( $params['queryLogger'] )
-                       ? $params['queryLogger']
-                       : new \Psr\Log\NullLogger();
-
-               if ( $user ) {
-                       $this->open( $server, $user, $password, $dbName );
-               }
-       }
-
-       /**
-        * Given a DB type, construct the name of the appropriate child class of
-        * IDatabase. This is designed to replace all of the manual stuff like:
-        *    $class = 'Database' . ucfirst( strtolower( $dbType ) );
-        * as well as validate against the canonical list of DB types we have
-        *
-        * This factory function is mostly useful for when you need to connect to a
-        * database other than the MediaWiki default (such as for external auth,
-        * an extension, et cetera). Do not use this to connect to the MediaWiki
-        * database. Example uses in core:
-        * @see LoadBalancer::reallyOpenConnection()
-        * @see ForeignDBRepo::getMasterDB()
-        * @see WebInstallerDBConnect::execute()
-        *
-        * @since 1.18
-        *
-        * @param string $dbType A possible DB type
-        * @param array $p An array of options to pass to the constructor.
-        *    Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
-        * @return IDatabase|null If the database driver or extension cannot be found
-        * @throws InvalidArgumentException If the database driver or extension cannot be found
-        */
-       final public static function factory( $dbType, $p = [] ) {
-               $canonicalDBTypes = [
-                       'mysql' => [ 'mysqli', 'mysql' ],
-                       'postgres' => [],
-                       'sqlite' => [],
-                       'oracle' => [],
-                       'mssql' => [],
-               ];
-
-               $driver = false;
-               $dbType = strtolower( $dbType );
-               if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
-                       $possibleDrivers = $canonicalDBTypes[$dbType];
-                       if ( !empty( $p['driver'] ) ) {
-                               if ( in_array( $p['driver'], $possibleDrivers ) ) {
-                                       $driver = $p['driver'];
-                               } else {
-                                       throw new InvalidArgumentException( __METHOD__ .
-                                               " type '$dbType' does not support driver '{$p['driver']}'" );
-                               }
-                       } else {
-                               foreach ( $possibleDrivers as $posDriver ) {
-                                       if ( extension_loaded( $posDriver ) ) {
-                                               $driver = $posDriver;
-                                               break;
-                                       }
-                               }
-                       }
-               } else {
-                       $driver = $dbType;
-               }
-               if ( $driver === false ) {
-                       throw new InvalidArgumentException( __METHOD__ .
-                               " no viable database extension found for type '$dbType'" );
-               }
-
-               // Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
-               // and everything else doesn't use a schema (e.g. null)
-               // Although postgres and oracle support schemas, we don't use them (yet)
-               // to maintain backwards compatibility
-               $defaultSchemas = [
-                       'mssql' => 'get from global',
-               ];
-
-               $class = 'Database' . ucfirst( $driver );
-               if ( class_exists( $class ) && is_subclass_of( $class, 'IDatabase' ) ) {
-                       // Resolve some defaults for b/c
-                       $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
-                       $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
-                       $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
-                       $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
-                       $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
-                       $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
-                       $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
-                       if ( !isset( $p['schema'] ) ) {
-                               $p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
-                       }
-                       $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
-
-                       $conn = new $class( $p );
-                       if ( isset( $p['connLogger'] ) ) {
-                               $conn->connLogger = $p['connLogger'];
-                       }
-                       if ( isset( $p['queryLogger'] ) ) {
-                               $conn->queryLogger = $p['queryLogger'];
-                       }
-                       if ( isset( $p['errorLogger'] ) ) {
-                               $conn->errorLogger = $p['errorLogger'];
-                       } else {
-                               $conn->errorLogger = function ( Exception $e ) {
-                                       trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
-                               };
-                       }
-               } else {
-                       $conn = null;
-               }
-
-               return $conn;
-       }
-
-       public function setLogger( LoggerInterface $logger ) {
-               $this->queryLogger = $logger;
-       }
-
-       public function getServerInfo() {
-               return $this->getServerVersion();
-       }
-
-       /**
-        * @return string Command delimiter used by this database engine
-        */
-       public function getDelimiter() {
-               return $this->delimiter;
-       }
-
-       /**
-        * Boolean, controls output of large amounts of debug information.
-        * @param bool|null $debug
-        *   - true to enable debugging
-        *   - false to disable debugging
-        *   - omitted or null to do nothing
-        *
-        * @return bool Previous value of the flag
-        * @deprecated since 1.28; use setFlag()
-        */
-       public function debug( $debug = null ) {
-               $res = $this->getFlag( DBO_DEBUG );
-               if ( $debug !== null ) {
-                       $debug ? $this->setFlag( DBO_DEBUG ) : $this->clearFlag( DBO_DEBUG );
-               }
-
-               return $res;
-       }
-
-       public function bufferResults( $buffer = null ) {
-               $res = !$this->getFlag( DBO_NOBUFFER );
-               if ( $buffer !== null ) {
-                       $buffer ? $this->clearFlag( DBO_NOBUFFER ) : $this->setFlag( DBO_NOBUFFER );
-               }
-
-               return $res;
-       }
-
-       /**
-        * Turns on (false) or off (true) the automatic generation and sending
-        * of a "we're sorry, but there has been a database error" page on
-        * database errors. Default is on (false). When turned off, the
-        * code should use lastErrno() and lastError() to handle the
-        * situation as appropriate.
-        *
-        * Do not use this function outside of the Database classes.
-        *
-        * @param null|bool $ignoreErrors
-        * @return bool The previous value of the flag.
-        */
-       protected function ignoreErrors( $ignoreErrors = null ) {
-               $res = $this->getFlag( DBO_IGNORE );
-               if ( $ignoreErrors !== null ) {
-                       $ignoreErrors ? $this->setFlag( DBO_IGNORE ) : $this->clearFlag( DBO_IGNORE );
-               }
-
-               return $res;
-       }
-
-       public function trxLevel() {
-               return $this->mTrxLevel;
-       }
-
-       public function trxTimestamp() {
-               return $this->mTrxLevel ? $this->mTrxTimestamp : null;
-       }
-
-       public function tablePrefix( $prefix = null ) {
-               $old = $this->mTablePrefix;
-               $this->mTablePrefix = $prefix;
-
-               return $old;
-       }
-
-       public function dbSchema( $schema = null ) {
-               $old = $this->mSchema;
-               $this->mSchema = $schema;
-
-               return $old;
-       }
-
-       /**
-        * Set the filehandle to copy write statements to.
-        *
-        * @param resource $fh File handle
-        */
-       public function setFileHandle( $fh ) {
-               $this->fileHandle = $fh;
-       }
-
-       public function getLBInfo( $name = null ) {
-               if ( is_null( $name ) ) {
-                       return $this->mLBInfo;
-               } else {
-                       if ( array_key_exists( $name, $this->mLBInfo ) ) {
-                               return $this->mLBInfo[$name];
-                       } else {
-                               return null;
-                       }
-               }
-       }
-
-       public function setLBInfo( $name, $value = null ) {
-               if ( is_null( $value ) ) {
-                       $this->mLBInfo = $name;
-               } else {
-                       $this->mLBInfo[$name] = $value;
-               }
-       }
-
-       public function setLazyMasterHandle( IDatabase $conn ) {
-               $this->lazyMasterHandle = $conn;
-       }
-
-       /**
-        * @return IDatabase|null
-        * @see setLazyMasterHandle()
-        * @since 1.27
-        */
-       public function getLazyMasterHandle() {
-               return $this->lazyMasterHandle;
-       }
-
-       /**
-        * @param TransactionProfiler $profiler
-        * @since 1.27
-        */
-       public function setTransactionProfiler( TransactionProfiler $profiler ) {
-               $this->trxProfiler = $profiler;
-       }
-
-       /**
-        * Returns true if this database supports (and uses) cascading deletes
-        *
-        * @return bool
-        */
-       public function cascadingDeletes() {
-               return false;
-       }
-
-       /**
-        * Returns true if this database supports (and uses) triggers (e.g. on the page table)
-        *
-        * @return bool
-        */
-       public function cleanupTriggers() {
-               return false;
-       }
-
-       /**
-        * Returns true if this database is strict about what can be put into an IP field.
-        * Specifically, it uses a NULL value instead of an empty string.
-        *
-        * @return bool
-        */
-       public function strictIPs() {
-               return false;
-       }
-
-       /**
-        * Returns true if this database uses timestamps rather than integers
-        *
-        * @return bool
-        */
-       public function realTimestamps() {
-               return false;
-       }
-
-       public function implicitGroupby() {
-               return true;
-       }
-
-       public function implicitOrderby() {
-               return true;
-       }
-
-       /**
-        * Returns true if this database can do a native search on IP columns
-        * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
-        *
-        * @return bool
-        */
-       public function searchableIPs() {
-               return false;
-       }
-
-       /**
-        * Returns true if this database can use functional indexes
-        *
-        * @return bool
-        */
-       public function functionalIndexes() {
-               return false;
-       }
-
-       public function lastQuery() {
-               return $this->mLastQuery;
-       }
-
-       public function doneWrites() {
-               return (bool)$this->mDoneWrites;
-       }
-
-       public function lastDoneWrites() {
-               return $this->mDoneWrites ?: false;
-       }
-
-       public function writesPending() {
-               return $this->mTrxLevel && $this->mTrxDoneWrites;
-       }
-
-       public function writesOrCallbacksPending() {
-               return $this->mTrxLevel && (
-                       $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
-               );
-       }
-
-       public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
-               if ( !$this->mTrxLevel ) {
-                       return false;
-               } elseif ( !$this->mTrxDoneWrites ) {
-                       return 0.0;
-               }
-
-               switch ( $type ) {
-                       case self::ESTIMATE_DB_APPLY:
-                               $this->ping( $rtt );
-                               $rttAdjTotal = $this->mTrxWriteAdjQueryCount * $rtt;
-                               $applyTime = max( $this->mTrxWriteAdjDuration - $rttAdjTotal, 0 );
-                               // For omitted queries, make them count as something at least
-                               $omitted = $this->mTrxWriteQueryCount - $this->mTrxWriteAdjQueryCount;
-                               $applyTime += self::TINY_WRITE_SEC * $omitted;
-
-                               return $applyTime;
-                       default: // everything
-                               return $this->mTrxWriteDuration;
-               }
-       }
-
-       public function pendingWriteCallers() {
-               return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
-       }
-
-       protected function pendingWriteAndCallbackCallers() {
-               if ( !$this->mTrxLevel ) {
-                       return [];
-               }
-
-               $fnames = $this->mTrxWriteCallers;
-               foreach ( [
-                       $this->mTrxIdleCallbacks,
-                       $this->mTrxPreCommitCallbacks,
-                       $this->mTrxEndCallbacks
-               ] as $callbacks ) {
-                       foreach ( $callbacks as $callback ) {
-                               $fnames[] = $callback[1];
-                       }
-               }
-
-               return $fnames;
-       }
-
-       public function isOpen() {
-               return $this->mOpened;
-       }
-
-       public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
-               if ( $remember === self::REMEMBER_PRIOR ) {
-                       array_push( $this->priorFlags, $this->mFlags );
-               }
-               $this->mFlags |= $flag;
-       }
-
-       public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
-               if ( $remember === self::REMEMBER_PRIOR ) {
-                       array_push( $this->priorFlags, $this->mFlags );
-               }
-               $this->mFlags &= ~$flag;
-       }
-
-       public function restoreFlags( $state = self::RESTORE_PRIOR ) {
-               if ( !$this->priorFlags ) {
-                       return;
-               }
-
-               if ( $state === self::RESTORE_INITIAL ) {
-                       $this->mFlags = reset( $this->priorFlags );
-                       $this->priorFlags = [];
-               } else {
-                       $this->mFlags = array_pop( $this->priorFlags );
-               }
-       }
-
-       public function getFlag( $flag ) {
-               return !!( $this->mFlags & $flag );
-       }
-
-       public function getProperty( $name ) {
-               return $this->$name;
-       }
-
-       public function getWikiID() {
-               if ( $this->mTablePrefix ) {
-                       return "{$this->mDBname}-{$this->mTablePrefix}";
-               } else {
-                       return $this->mDBname;
-               }
-       }
-
-       /**
-        * Get information about an index into an object
-        * @param string $table Table name
-        * @param string $index Index name
-        * @param string $fname Calling function name
-        * @return mixed Database-specific index description class or false if the index does not exist
-        */
-       abstract function indexInfo( $table, $index, $fname = __METHOD__ );
-
-       /**
-        * Wrapper for addslashes()
-        *
-        * @param string $s String to be slashed.
-        * @return string Slashed string.
-        */
-       abstract function strencode( $s );
-
-       /**
-        * Called by serialize. Throw an exception when DB connection is serialized.
-        * This causes problems on some database engines because the connection is
-        * not restored on unserialize.
-        */
-       public function __sleep() {
-               throw new RuntimeException( 'Database serialization may cause problems, since ' .
-                       'the connection is not restored on wakeup.' );
-       }
-
-       protected function installErrorHandler() {
-               $this->mPHPError = false;
-               $this->htmlErrors = ini_set( 'html_errors', '0' );
-               set_error_handler( [ $this, 'connectionerrorLogger' ] );
-       }
-
-       /**
-        * @return bool|string
-        */
-       protected function restoreErrorHandler() {
-               restore_error_handler();
-               if ( $this->htmlErrors !== false ) {
-                       ini_set( 'html_errors', $this->htmlErrors );
-               }
-               if ( $this->mPHPError ) {
-                       $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
-                       $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
-
-                       return $error;
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * @param int $errno
-        * @param string $errstr
-        */
-       public function connectionerrorLogger( $errno, $errstr ) {
-               $this->mPHPError = $errstr;
-       }
-
-       /**
-        * Create a log context to pass to PSR logging functions.
-        *
-        * @param array $extras Additional data to add to context
-        * @return array
-        */
-       protected function getLogContext( array $extras = [] ) {
-               return array_merge(
-                       [
-                               'db_server' => $this->mServer,
-                               'db_name' => $this->mDBname,
-                               'db_user' => $this->mUser,
-                       ],
-                       $extras
-               );
-       }
-
-       public function close() {
-               if ( $this->mConn ) {
-                       if ( $this->trxLevel() ) {
-                               $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
-                       }
-
-                       $closed = $this->closeConnection();
-                       $this->mConn = false;
-               } elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
-                       throw new RuntimeException( "Transaction callbacks still pending." );
-               } else {
-                       $closed = true;
-               }
-               $this->mOpened = false;
-
-               return $closed;
-       }
-
-       /**
-        * Make sure isOpen() returns true as a sanity check
-        *
-        * @throws DBUnexpectedError
-        */
-       protected function assertOpen() {
-               if ( !$this->isOpen() ) {
-                       throw new DBUnexpectedError( $this, "DB connection was already closed." );
-               }
-       }
-
-       /**
-        * Closes underlying database connection
-        * @since 1.20
-        * @return bool Whether connection was closed successfully
-        */
-       abstract protected function closeConnection();
-
-       function reportConnectionError( $error = 'Unknown error' ) {
-               $myError = $this->lastError();
-               if ( $myError ) {
-                       $error = $myError;
-               }
-
-               # New method
-               throw new DBConnectionError( $this, $error );
-       }
-
-       /**
-        * The DBMS-dependent part of query()
-        *
-        * @param string $sql SQL query.
-        * @return ResultWrapper|bool Result object to feed to fetchObject,
-        *   fetchRow, ...; or false on failure
-        */
-       abstract protected function doQuery( $sql );
-
-       /**
-        * Determine whether a query writes to the DB.
-        * Should return true if unsure.
-        *
-        * @param string $sql
-        * @return bool
-        */
-       protected function isWriteQuery( $sql ) {
-               return !preg_match(
-                       '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
-       }
-
-       /**
-        * @param $sql
-        * @return string|null
-        */
-       protected function getQueryVerb( $sql ) {
-               return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
-       }
-
-       /**
-        * Determine whether a SQL statement is sensitive to isolation level.
-        * A SQL statement is considered transactable if its result could vary
-        * depending on the transaction isolation level. Operational commands
-        * such as 'SET' and 'SHOW' are not considered to be transactable.
-        *
-        * @param string $sql
-        * @return bool
-        */
-       protected function isTransactableQuery( $sql ) {
-               $verb = $this->getQueryVerb( $sql );
-               return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
-       }
-
-       public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
-               $priorWritesPending = $this->writesOrCallbacksPending();
-               $this->mLastQuery = $sql;
-
-               $isWrite = $this->isWriteQuery( $sql );
-               if ( $isWrite ) {
-                       $reason = $this->getReadOnlyReason();
-                       if ( $reason !== false ) {
-                               throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
-                       }
-                       # Set a flag indicating that writes have been done
-                       $this->mDoneWrites = microtime( true );
-               }
-
-               // Add trace comment to the begin of the sql string, right after the operator.
-               // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
-               $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 )
-                       && $this->isTransactableQuery( $sql )
-               ) {
-                       $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
-                       $this->mTrxAutomatic = true;
-               }
-
-               # Keep track of whether the transaction has write queries pending
-               if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
-                       $this->mTrxDoneWrites = true;
-                       $this->trxProfiler->transactionWritingIn(
-                               $this->mServer, $this->mDBname, $this->mTrxShortId );
-               }
-
-               if ( $this->debug() ) {
-                       $this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
-               }
-
-               # Avoid fatals if close() was called
-               $this->assertOpen();
-
-               # Send the query to the server
-               $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
-
-               # Try reconnecting if the connection was lost
-               if ( false === $ret && $this->wasErrorReissuable() ) {
-                       $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
-                       # Stash the last error values before anything might clear them
-                       $lastError = $this->lastError();
-                       $lastErrno = $this->lastErrno();
-                       # Update state tracking to reflect transaction loss due to disconnection
-                       $this->handleTransactionLoss();
-                       if ( $this->reconnect() ) {
-                               $msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
-                               $this->connLogger->warning( $msg );
-                               $this->queryLogger->warning(
-                                       "$msg:\n" . ( new RuntimeException() )->getTraceAsString() );
-
-                               if ( !$recoverable ) {
-                                       # Callers may catch the exception and continue to use the DB
-                                       $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
-                               } else {
-                                       # Should be safe to silently retry the query
-                                       $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
-                               }
-                       } else {
-                               $msg = __METHOD__ . ": lost connection to {$this->getServer()} permanently";
-                               $this->connLogger->error( $msg );
-                       }
-               }
-
-               if ( false === $ret ) {
-                       # Deadlocks cause the entire transaction to abort, not just the statement.
-                       # http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
-                       # https://www.postgresql.org/docs/9.1/static/explicit-locking.html
-                       if ( $this->wasDeadlock() ) {
-                               if ( $this->explicitTrxActive() || $priorWritesPending ) {
-                                       $tempIgnore = false; // not recoverable
-                               }
-                               # Update state tracking to reflect transaction loss
-                               $this->handleTransactionLoss();
-                       }
-
-                       $this->reportQueryError(
-                               $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
-               }
-
-               $res = $this->resultObject( $ret );
-
-               return $res;
-       }
-
-       private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
-               $isMaster = !is_null( $this->getLBInfo( 'master' ) );
-               # generalizeSQL() will probably cut down the query to reasonable
-               # logging size most of the time. The substr is really just a sanity check.
-               if ( $isMaster ) {
-                       $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
-               } else {
-                       $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
-               }
-
-               # Include query transaction state
-               $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
-
-               $startTime = microtime( true );
-               $this->profiler->profileIn( $queryProf );
-               $ret = $this->doQuery( $commentedSql );
-               $this->profiler->profileOut( $queryProf );
-               $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
-
-               unset( $queryProfSection ); // profile out (if set)
-
-               if ( $ret !== false ) {
-                       $this->lastPing = $startTime;
-                       if ( $isWrite && $this->mTrxLevel ) {
-                               $this->updateTrxWriteQueryTime( $sql, $queryRuntime );
-                               $this->mTrxWriteCallers[] = $fname;
-                       }
-               }
-
-               if ( $sql === self::PING_QUERY ) {
-                       $this->mRTTEstimate = $queryRuntime;
-               }
-
-               $this->trxProfiler->recordQueryCompletion(
-                       $queryProf, $startTime, $isWrite, $this->affectedRows()
-               );
-               MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
-
-               return $ret;
-       }
-
-       /**
-        * Update the estimated run-time of a query, not counting large row lock times
-        *
-        * LoadBalancer can be set to rollback transactions that will create huge replication
-        * lag. It bases this estimate off of pendingWriteQueryDuration(). Certain simple
-        * queries, like inserting a row can take a long time due to row locking. This method
-        * uses some simple heuristics to discount those cases.
-        *
-        * @param string $sql A SQL write query
-        * @param float $runtime Total runtime, including RTT
-        */
-       private function updateTrxWriteQueryTime( $sql, $runtime ) {
-               // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
-               $indicativeOfReplicaRuntime = true;
-               if ( $runtime > self::SLOW_WRITE_SEC ) {
-                       $verb = $this->getQueryVerb( $sql );
-                       // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
-                       if ( $verb === 'INSERT' ) {
-                               $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
-                       } elseif ( $verb === 'REPLACE' ) {
-                               $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
-                       }
-               }
-
-               $this->mTrxWriteDuration += $runtime;
-               $this->mTrxWriteQueryCount += 1;
-               if ( $indicativeOfReplicaRuntime ) {
-                       $this->mTrxWriteAdjDuration += $runtime;
-                       $this->mTrxWriteAdjQueryCount += 1;
-               }
-       }
-
-       private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
-               # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
-               # Dropped connections also mean that named locks are automatically released.
-               # Only allow error suppression in autocommit mode or when the lost transaction
-               # didn't matter anyway (aside from DBO_TRX snapshot loss).
-               if ( $this->mNamedLocksHeld ) {
-                       return false; // possible critical section violation
-               } elseif ( $sql === 'COMMIT' ) {
-                       return !$priorWritesPending; // nothing written anyway? (T127428)
-               } elseif ( $sql === 'ROLLBACK' ) {
-                       return true; // transaction lost...which is also what was requested :)
-               } elseif ( $this->explicitTrxActive() ) {
-                       return false; // don't drop atomocity
-               } elseif ( $priorWritesPending ) {
-                       return false; // prior writes lost from implicit transaction
-               }
-
-               return true;
-       }
-
-       private function handleTransactionLoss() {
-               $this->mTrxLevel = 0;
-               $this->mTrxIdleCallbacks = []; // bug 65263
-               $this->mTrxPreCommitCallbacks = []; // bug 65263
-               try {
-                       // Handle callbacks in mTrxEndCallbacks
-                       $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
-                       $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
-                       return null;
-               } catch ( Exception $e ) {
-                       // Already logged; move on...
-                       return $e;
-               }
-       }
-
-       public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
-               if ( $this->ignoreErrors() || $tempIgnore ) {
-                       $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
-               } else {
-                       $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
-                       $this->queryLogger->error(
-                               "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
-                               $this->getLogContext( [
-                                       'method' => __METHOD__,
-                                       'errno' => $errno,
-                                       'error' => $error,
-                                       'sql1line' => $sql1line,
-                                       'fname' => $fname,
-                               ] )
-                       );
-                       $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
-                       throw new DBQueryError( $this, $error, $errno, $sql, $fname );
-               }
-       }
-
-       /**
-        * Intended to be compatible with the PEAR::DB wrapper functions.
-        * http://pear.php.net/manual/en/package.database.db.intro-execute.php
-        *
-        * ? = scalar value, quoted as necessary
-        * ! = raw SQL bit (a function for instance)
-        * & = filename; reads the file and inserts as a blob
-        *     (we don't use this though...)
-        *
-        * @param string $sql
-        * @param string $func
-        *
-        * @return array
-        */
-       protected function prepare( $sql, $func = __METHOD__ ) {
-               /* MySQL doesn't support prepared statements (yet), so just
-                * pack up the query for reference. We'll manually replace
-                * the bits later.
-                */
-               return [ 'query' => $sql, 'func' => $func ];
-       }
-
-       /**
-        * Free a prepared query, generated by prepare().
-        * @param string $prepared
-        */
-       protected function freePrepared( $prepared ) {
-               /* No-op by default */
-       }
-
-       /**
-        * Execute a prepared query with the various arguments
-        * @param string $prepared The prepared sql
-        * @param mixed $args Either an array here, or put scalars as varargs
-        *
-        * @return ResultWrapper
-        */
-       public function execute( $prepared, $args = null ) {
-               if ( !is_array( $args ) ) {
-                       # Pull the var args
-                       $args = func_get_args();
-                       array_shift( $args );
-               }
-
-               $sql = $this->fillPrepared( $prepared['query'], $args );
-
-               return $this->query( $sql, $prepared['func'] );
-       }
-
-       /**
-        * For faking prepared SQL statements on DBs that don't support it directly.
-        *
-        * @param string $preparedQuery A 'preparable' SQL statement
-        * @param array $args Array of Arguments to fill it with
-        * @return string Executable SQL
-        */
-       public function fillPrepared( $preparedQuery, $args ) {
-               reset( $args );
-               $this->preparedArgs =& $args;
-
-               return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
-                       [ &$this, 'fillPreparedArg' ], $preparedQuery );
-       }
-
-       /**
-        * preg_callback func for fillPrepared()
-        * The arguments should be in $this->preparedArgs and must not be touched
-        * while we're doing this.
-        *
-        * @param array $matches
-        * @throws DBUnexpectedError
-        * @return string
-        */
-       protected function fillPreparedArg( $matches ) {
-               switch ( $matches[1] ) {
-                       case '\\?':
-                               return '?';
-                       case '\\!':
-                               return '!';
-                       case '\\&':
-                               return '&';
-               }
-
-               list( /* $n */, $arg ) = each( $this->preparedArgs );
-
-               switch ( $matches[1] ) {
-                       case '?':
-                               return $this->addQuotes( $arg );
-                       case '!':
-                               return $arg;
-                       case '&':
-                               # return $this->addQuotes( file_get_contents( $arg ) );
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       '& mode is not implemented. If it\'s really needed, uncomment the line above.'
-                               );
-                       default:
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       'Received invalid match. This should never happen!'
-                               );
-               }
-       }
-
-       public function freeResult( $res ) {
-       }
-
-       public function selectField(
-               $table, $var, $cond = '', $fname = __METHOD__, $options = []
-       ) {
-               if ( $var === '*' ) { // sanity
-                       throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
-               }
-
-               if ( !is_array( $options ) ) {
-                       $options = [ $options ];
-               }
-
-               $options['LIMIT'] = 1;
-
-               $res = $this->select( $table, $var, $cond, $fname, $options );
-               if ( $res === false || !$this->numRows( $res ) ) {
-                       return false;
-               }
-
-               $row = $this->fetchRow( $res );
-
-               if ( $row !== false ) {
-                       return reset( $row );
-               } else {
-                       return false;
-               }
-       }
-
-       public function selectFieldValues(
-               $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
-       ) {
-               if ( $var === '*' ) { // sanity
-                       throw new DBUnexpectedError( $this, "Cannot use a * field" );
-               } elseif ( !is_string( $var ) ) { // sanity
-                       throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
-               }
-
-               if ( !is_array( $options ) ) {
-                       $options = [ $options ];
-               }
-
-               $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
-               if ( $res === false ) {
-                       return false;
-               }
-
-               $values = [];
-               foreach ( $res as $row ) {
-                       $values[] = $row->$var;
-               }
-
-               return $values;
-       }
-
-       /**
-        * Returns an optional USE INDEX clause to go after the table, and a
-        * string to go at the end of the query.
-        *
-        * @param array $options Associative array of options to be turned into
-        *   an SQL query, valid keys are listed in the function.
-        * @return array
-        * @see DatabaseBase::select()
-        */
-       public function makeSelectOptions( $options ) {
-               $preLimitTail = $postLimitTail = '';
-               $startOpts = '';
-
-               $noKeyOptions = [];
-
-               foreach ( $options as $key => $option ) {
-                       if ( is_numeric( $key ) ) {
-                               $noKeyOptions[$option] = true;
-                       }
-               }
-
-               $preLimitTail .= $this->makeGroupByWithHaving( $options );
-
-               $preLimitTail .= $this->makeOrderBy( $options );
-
-               // if (isset($options['LIMIT'])) {
-               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
-               //              isset($options['OFFSET']) ? $options['OFFSET']
-               //              : false);
-               // }
-
-               if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
-                       $postLimitTail .= ' FOR UPDATE';
-               }
-
-               if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
-                       $postLimitTail .= ' LOCK IN SHARE MODE';
-               }
-
-               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
-                       $startOpts .= 'DISTINCT';
-               }
-
-               # Various MySQL extensions
-               if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
-                       $startOpts .= ' /*! STRAIGHT_JOIN */';
-               }
-
-               if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
-                       $startOpts .= ' HIGH_PRIORITY';
-               }
-
-               if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
-                       $startOpts .= ' SQL_BIG_RESULT';
-               }
-
-               if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
-                       $startOpts .= ' SQL_BUFFER_RESULT';
-               }
-
-               if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
-                       $startOpts .= ' SQL_SMALL_RESULT';
-               }
-
-               if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
-                       $startOpts .= ' SQL_CALC_FOUND_ROWS';
-               }
-
-               if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
-                       $startOpts .= ' SQL_CACHE';
-               }
-
-               if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
-                       $startOpts .= ' SQL_NO_CACHE';
-               }
-
-               if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
-                       $useIndex = $this->useIndexClause( $options['USE INDEX'] );
-               } else {
-                       $useIndex = '';
-               }
-               if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
-                       $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
-               } else {
-                       $ignoreIndex = '';
-               }
-
-               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
-       }
-
-       /**
-        * Returns an optional GROUP BY with an optional HAVING
-        *
-        * @param array $options Associative array of options
-        * @return string
-        * @see DatabaseBase::select()
-        * @since 1.21
-        */
-       public function makeGroupByWithHaving( $options ) {
-               $sql = '';
-               if ( isset( $options['GROUP BY'] ) ) {
-                       $gb = is_array( $options['GROUP BY'] )
-                               ? implode( ',', $options['GROUP BY'] )
-                               : $options['GROUP BY'];
-                       $sql .= ' GROUP BY ' . $gb;
-               }
-               if ( isset( $options['HAVING'] ) ) {
-                       $having = is_array( $options['HAVING'] )
-                               ? $this->makeList( $options['HAVING'], LIST_AND )
-                               : $options['HAVING'];
-                       $sql .= ' HAVING ' . $having;
-               }
-
-               return $sql;
-       }
-
-       /**
-        * Returns an optional ORDER BY
-        *
-        * @param array $options Associative array of options
-        * @return string
-        * @see DatabaseBase::select()
-        * @since 1.21
-        */
-       public function makeOrderBy( $options ) {
-               if ( isset( $options['ORDER BY'] ) ) {
-                       $ob = is_array( $options['ORDER BY'] )
-                               ? implode( ',', $options['ORDER BY'] )
-                               : $options['ORDER BY'];
-
-                       return ' ORDER BY ' . $ob;
-               }
-
-               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 );
-
-               return $this->query( $sql, $fname );
-       }
-
-       public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
-               $options = [], $join_conds = []
-       ) {
-               if ( is_array( $vars ) ) {
-                       $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
-               }
-
-               $options = (array)$options;
-               $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
-                       ? $options['USE INDEX']
-                       : [];
-               $ignoreIndexes = ( isset( $options['IGNORE INDEX'] ) && is_array( $options['IGNORE INDEX'] ) )
-                       ? $options['IGNORE INDEX']
-                       : [];
-
-               if ( is_array( $table ) ) {
-                       $from = ' FROM ' .
-                               $this->tableNamesWithIndexClauseOrJOIN( $table, $useIndexes, $ignoreIndexes, $join_conds );
-               } elseif ( $table != '' ) {
-                       if ( $table[0] == ' ' ) {
-                               $from = ' FROM ' . $table;
-                       } else {
-                               $from = ' FROM ' .
-                                       $this->tableNamesWithIndexClauseOrJOIN( [ $table ], $useIndexes, $ignoreIndexes, [] );
-                       }
-               } else {
-                       $from = '';
-               }
-
-               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
-                       $this->makeSelectOptions( $options );
-
-               if ( !empty( $conds ) ) {
-                       if ( is_array( $conds ) ) {
-                               $conds = $this->makeList( $conds, LIST_AND );
-                       }
-                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex WHERE $conds $preLimitTail";
-               } else {
-                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
-               }
-
-               if ( isset( $options['LIMIT'] ) ) {
-                       $sql = $this->limitResult( $sql, $options['LIMIT'],
-                               isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
-               }
-               $sql = "$sql $postLimitTail";
-
-               if ( isset( $options['EXPLAIN'] ) ) {
-                       $sql = 'EXPLAIN ' . $sql;
-               }
-
-               return $sql;
-       }
-
-       public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
-               $options = [], $join_conds = []
-       ) {
-               $options = (array)$options;
-               $options['LIMIT'] = 1;
-               $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
-
-               if ( $res === false ) {
-                       return false;
-               }
-
-               if ( !$this->numRows( $res ) ) {
-                       return false;
-               }
-
-               $obj = $this->fetchObject( $res );
-
-               return $obj;
-       }
-
-       public function estimateRowCount(
-               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
-       ) {
-               $rows = 0;
-               $res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
-
-               if ( $res ) {
-                       $row = $this->fetchRow( $res );
-                       $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
-               }
-
-               return $rows;
-       }
-
-       public function selectRowCount(
-               $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
-       ) {
-               $rows = 0;
-               $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
-               $res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
-
-               if ( $res ) {
-                       $row = $this->fetchRow( $res );
-                       $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
-               }
-
-               return $rows;
-       }
-
-       /**
-        * Removes most variables from an SQL query and replaces them with X or N for numbers.
-        * It's only slightly flawed. Don't use for anything important.
-        *
-        * @param string $sql A SQL Query
-        *
-        * @return string
-        */
-       protected static function generalizeSQL( $sql ) {
-               # This does the same as the regexp below would do, but in such a way
-               # as to avoid crashing php on some large strings.
-               # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
-
-               $sql = str_replace( "\\\\", '', $sql );
-               $sql = str_replace( "\\'", '', $sql );
-               $sql = str_replace( "\\\"", '', $sql );
-               $sql = preg_replace( "/'.*'/s", "'X'", $sql );
-               $sql = preg_replace( '/".*"/s', "'X'", $sql );
-
-               # All newlines, tabs, etc replaced by single space
-               $sql = preg_replace( '/\s+/', ' ', $sql );
-
-               # All numbers => N,
-               # except the ones surrounded by characters, e.g. l10n
-               $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
-               $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
-
-               return $sql;
-       }
-
-       public function fieldExists( $table, $field, $fname = __METHOD__ ) {
-               $info = $this->fieldInfo( $table, $field );
-
-               return (bool)$info;
-       }
-
-       public function indexExists( $table, $index, $fname = __METHOD__ ) {
-               if ( !$this->tableExists( $table ) ) {
-                       return null;
-               }
-
-               $info = $this->indexInfo( $table, $index, $fname );
-               if ( is_null( $info ) ) {
-                       return null;
-               } else {
-                       return $info !== false;
-               }
-       }
-
-       public function tableExists( $table, $fname = __METHOD__ ) {
-               $table = $this->tableName( $table );
-               $old = $this->ignoreErrors( true );
-               $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
-               $this->ignoreErrors( $old );
-
-               return (bool)$res;
-       }
-
-       public function indexUnique( $table, $index ) {
-               $indexInfo = $this->indexInfo( $table, $index );
-
-               if ( !$indexInfo ) {
-                       return null;
-               }
-
-               return !$indexInfo[0]->Non_unique;
-       }
-
-       /**
-        * Helper for DatabaseBase::insert().
-        *
-        * @param array $options
-        * @return string
-        */
-       protected function makeInsertOptions( $options ) {
-               return implode( ' ', $options );
-       }
-
-       public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
-               # No rows to insert, easy just return now
-               if ( !count( $a ) ) {
-                       return true;
-               }
-
-               $table = $this->tableName( $table );
-
-               if ( !is_array( $options ) ) {
-                       $options = [ $options ];
-               }
-
-               $fh = null;
-               if ( isset( $options['fileHandle'] ) ) {
-                       $fh = $options['fileHandle'];
-               }
-               $options = $this->makeInsertOptions( $options );
-
-               if ( isset( $a[0] ) && is_array( $a[0] ) ) {
-                       $multi = true;
-                       $keys = array_keys( $a[0] );
-               } else {
-                       $multi = false;
-                       $keys = array_keys( $a );
-               }
-
-               $sql = 'INSERT ' . $options .
-                       " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
-
-               if ( $multi ) {
-                       $first = true;
-                       foreach ( $a as $row ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $sql .= ',';
-                               }
-                               $sql .= '(' . $this->makeList( $row ) . ')';
-                       }
-               } else {
-                       $sql .= '(' . $this->makeList( $a ) . ')';
-               }
-
-               if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
-                       return false;
-               } elseif ( $fh !== null ) {
-                       return true;
-               }
-
-               return (bool)$this->query( $sql, $fname );
-       }
-
-       /**
-        * Make UPDATE options array for DatabaseBase::makeUpdateOptions
-        *
-        * @param array $options
-        * @return array
-        */
-       protected function makeUpdateOptionsArray( $options ) {
-               if ( !is_array( $options ) ) {
-                       $options = [ $options ];
-               }
-
-               $opts = [];
-
-               if ( in_array( 'LOW_PRIORITY', $options ) ) {
-                       $opts[] = $this->lowPriorityOption();
-               }
-
-               if ( in_array( 'IGNORE', $options ) ) {
-                       $opts[] = 'IGNORE';
-               }
-
-               return $opts;
-       }
-
-       /**
-        * Make UPDATE options for the DatabaseBase::update function
-        *
-        * @param array $options The options passed to DatabaseBase::update
-        * @return string
-        */
-       protected function makeUpdateOptions( $options ) {
-               $opts = $this->makeUpdateOptionsArray( $options );
-
-               return implode( ' ', $opts );
-       }
-
-       function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
-               $table = $this->tableName( $table );
-               $opts = $this->makeUpdateOptions( $options );
-               $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
-
-               if ( $conds !== [] && $conds !== '*' ) {
-                       $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
-               }
-
-               return $this->query( $sql, $fname );
-       }
-
-       public function makeList( $a, $mode = LIST_COMMA ) {
-               if ( !is_array( $a ) ) {
-                       throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
-               }
-
-               $first = true;
-               $list = '';
-
-               foreach ( $a as $field => $value ) {
-                       if ( !$first ) {
-                               if ( $mode == LIST_AND ) {
-                                       $list .= ' AND ';
-                               } elseif ( $mode == LIST_OR ) {
-                                       $list .= ' OR ';
-                               } else {
-                                       $list .= ',';
-                               }
-                       } else {
-                               $first = false;
-                       }
-
-                       if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
-                               $list .= "($value)";
-                       } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
-                               $list .= "$value";
-                       } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
-                               // Remove null from array to be handled separately if found
-                               $includeNull = false;
-                               foreach ( array_keys( $value, null, true ) as $nullKey ) {
-                                       $includeNull = true;
-                                       unset( $value[$nullKey] );
-                               }
-                               if ( count( $value ) == 0 && !$includeNull ) {
-                                       throw new InvalidArgumentException( __METHOD__ . ": empty input for field $field" );
-                               } elseif ( count( $value ) == 0 ) {
-                                       // only check if $field is null
-                                       $list .= "$field IS NULL";
-                               } else {
-                                       // IN clause contains at least one valid element
-                                       if ( $includeNull ) {
-                                               // Group subconditions to ensure correct precedence
-                                               $list .= '(';
-                                       }
-                                       if ( count( $value ) == 1 ) {
-                                               // Special-case single values, as IN isn't terribly efficient
-                                               // Don't necessarily assume the single key is 0; we don't
-                                               // enforce linear numeric ordering on other arrays here.
-                                               $value = array_values( $value )[0];
-                                               $list .= $field . " = " . $this->addQuotes( $value );
-                                       } else {
-                                               $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
-                                       }
-                                       // if null present in array, append IS NULL
-                                       if ( $includeNull ) {
-                                               $list .= " OR $field IS NULL)";
-                                       }
-                               }
-                       } elseif ( $value === null ) {
-                               if ( $mode == LIST_AND || $mode == LIST_OR ) {
-                                       $list .= "$field IS ";
-                               } elseif ( $mode == LIST_SET ) {
-                                       $list .= "$field = ";
-                               }
-                               $list .= 'NULL';
-                       } else {
-                               if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
-                                       $list .= "$field = ";
-                               }
-                               $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
-                       }
-               }
-
-               return $list;
-       }
-
-       public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
-               $conds = [];
-
-               foreach ( $data as $base => $sub ) {
-                       if ( count( $sub ) ) {
-                               $conds[] = $this->makeList(
-                                       [ $baseKey => $base, $subKey => array_keys( $sub ) ],
-                                       LIST_AND );
-                       }
-               }
-
-               if ( $conds ) {
-                       return $this->makeList( $conds, LIST_OR );
-               } else {
-                       // Nothing to search for...
-                       return false;
-               }
-       }
-
-       /**
-        * Return aggregated value alias
-        *
-        * @param array $valuedata
-        * @param string $valuename
-        *
-        * @return string
-        */
-       public function aggregateValue( $valuedata, $valuename = 'value' ) {
-               return $valuename;
-       }
-
-       public function bitNot( $field ) {
-               return "(~$field)";
-       }
-
-       public function bitAnd( $fieldLeft, $fieldRight ) {
-               return "($fieldLeft & $fieldRight)";
-       }
-
-       public function bitOr( $fieldLeft, $fieldRight ) {
-               return "($fieldLeft | $fieldRight)";
-       }
-
-       public function buildConcat( $stringList ) {
-               return 'CONCAT(' . implode( ',', $stringList ) . ')';
-       }
-
-       public function buildGroupConcatField(
-               $delim, $table, $field, $conds = '', $join_conds = []
-       ) {
-               $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
-
-               return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
-       }
-
-       /**
-        * @param string $field Field or column to cast
-        * @return string
-        * @since 1.28
-        */
-       public function buildStringCast( $field ) {
-               return $field;
-       }
-
-       public function selectDB( $db ) {
-               # Stub. Shouldn't cause serious problems if it's not overridden, but
-               # if your database engine supports a concept similar to MySQL's
-               # databases you may as well.
-               $this->mDBname = $db;
-
-               return true;
-       }
-
-       public function getDBname() {
-               return $this->mDBname;
-       }
-
-       public function getServer() {
-               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
-               # use of `database`.table. But won't break things if someone wants
-               # to query a database table with a dot in the name.
-               if ( $this->isQuotedIdentifier( $name ) ) {
-                       return $name;
-               }
-
-               # Lets test for any bits of text that should never show up in a table
-               # name. Basically anything like JOIN or ON which are actually part of
-               # SQL queries, but may end up inside of the table value to combine
-               # sql. Such as how the API is doing.
-               # Note that we use a whitespace test rather than a \b test to avoid
-               # any remote case where a word like on may be inside of a table name
-               # surrounded by symbols which may be considered word breaks.
-               if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
-                       return $name;
-               }
-
-               # Split database and table into proper variables.
-               # We reverse the explode so that database.table and table both output
-               # the correct table.
-               $dbDetails = explode( '.', $name, 3 );
-               if ( count( $dbDetails ) == 3 ) {
-                       list( $database, $schema, $table ) = $dbDetails;
-                       # We don't want any prefix added in this case
-                       $prefix = '';
-               } elseif ( count( $dbDetails ) == 2 ) {
-                       list( $database, $table ) = $dbDetails;
-                       # We don't want any prefix added in this case
-                       # In dbs that support it, $database may actually be the schema
-                       # but that doesn't affect any of the functionality here
-                       $prefix = '';
-                       $schema = null;
-               } else {
-                       list( $table ) = $dbDetails;
-                       if ( isset( $this->tableAliases[$table] ) ) {
-                               $database = $this->tableAliases[$table]['dbname'];
-                               $schema = is_string( $this->tableAliases[$table]['schema'] )
-                                       ? $this->tableAliases[$table]['schema']
-                                       : $this->mSchema;
-                               $prefix = is_string( $this->tableAliases[$table]['prefix'] )
-                                       ? $this->tableAliases[$table]['prefix']
-                                       : $this->mTablePrefix;
-                       } else {
-                               $database = null;
-                               $schema = $this->mSchema; # Default schema
-                               $prefix = $this->mTablePrefix; # Default prefix
-                       }
-               }
-
-               # Quote $table and apply the prefix if not quoted.
-               # $tableName might be empty if this is called from Database::replaceVars()
-               $tableName = "{$prefix}{$table}";
-               if ( $format == 'quoted'
-                       && !$this->isQuotedIdentifier( $tableName ) && $tableName !== ''
-               ) {
-                       $tableName = $this->addIdentifierQuotes( $tableName );
-               }
-
-               # Quote $schema and merge it with the table name if needed
-               if ( strlen( $schema ) ) {
-                       if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
-                               $schema = $this->addIdentifierQuotes( $schema );
-                       }
-                       $tableName = $schema . '.' . $tableName;
-               }
-
-               # Quote $database and merge it with the table name if needed
-               if ( $database !== null ) {
-                       if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
-                               $database = $this->addIdentifierQuotes( $database );
-                       }
-                       $tableName = $database . '.' . $tableName;
-               }
-
-               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 = [];
-
-               foreach ( $inArray as $name ) {
-                       $retVal[$name] = $this->tableName( $name );
-               }
-
-               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 = [];
-
-               foreach ( $inArray as $name ) {
-                       $retVal[] = $this->tableName( $name );
-               }
-
-               return $retVal;
-       }
-
-       /**
-        * Get an aliased table name
-        * e.g. tableName AS newTableName
-        *
-        * @param string $name Table name, see tableName()
-        * @param string|bool $alias Alias (optional)
-        * @return string SQL name for aliased table. Will not alias a table to its own name
-        */
-       public function tableNameWithAlias( $name, $alias = false ) {
-               if ( !$alias || $alias == $name ) {
-                       return $this->tableName( $name );
-               } else {
-                       return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
-               }
-       }
-
-       /**
-        * Gets an array of aliased table names
-        *
-        * @param array $tables [ [alias] => table ]
-        * @return string[] See tableNameWithAlias()
-        */
-       public function tableNamesWithAlias( $tables ) {
-               $retval = [];
-               foreach ( $tables as $alias => $table ) {
-                       if ( is_numeric( $alias ) ) {
-                               $alias = $table;
-                       }
-                       $retval[] = $this->tableNameWithAlias( $table, $alias );
-               }
-
-               return $retval;
-       }
-
-       /**
-        * Get an aliased field name
-        * e.g. fieldName AS newFieldName
-        *
-        * @param string $name Field name
-        * @param string|bool $alias Alias (optional)
-        * @return string SQL name for aliased field. Will not alias a field to its own name
-        */
-       public function fieldNameWithAlias( $name, $alias = false ) {
-               if ( !$alias || (string)$alias === (string)$name ) {
-                       return $name;
-               } else {
-                       return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
-               }
-       }
-
-       /**
-        * Gets an array of aliased field names
-        *
-        * @param array $fields [ [alias] => field ]
-        * @return string[] See fieldNameWithAlias()
-        */
-       public function fieldNamesWithAlias( $fields ) {
-               $retval = [];
-               foreach ( $fields as $alias => $field ) {
-                       if ( is_numeric( $alias ) ) {
-                               $alias = $field;
-                       }
-                       $retval[] = $this->fieldNameWithAlias( $field, $alias );
-               }
-
-               return $retval;
-       }
-
-       /**
-        * Get the aliased table name clause for a FROM clause
-        * which might have a JOIN and/or USE INDEX or IGNORE INDEX clause
-        *
-        * @param array $tables ( [alias] => table )
-        * @param array $use_index Same as for select()
-        * @param array $ignore_index Same as for select()
-        * @param array $join_conds Same as for select()
-        * @return string
-        */
-       protected function tableNamesWithIndexClauseOrJOIN(
-               $tables, $use_index = [], $ignore_index = [], $join_conds = []
-       ) {
-               $ret = [];
-               $retJOIN = [];
-               $use_index = (array)$use_index;
-               $ignore_index = (array)$ignore_index;
-               $join_conds = (array)$join_conds;
-
-               foreach ( $tables as $alias => $table ) {
-                       if ( !is_string( $alias ) ) {
-                               // No alias? Set it equal to the table name
-                               $alias = $table;
-                       }
-                       // Is there a JOIN clause for this table?
-                       if ( isset( $join_conds[$alias] ) ) {
-                               list( $joinType, $conds ) = $join_conds[$alias];
-                               $tableClause = $joinType;
-                               $tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
-                               if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
-                                       $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
-                                       if ( $use != '' ) {
-                                               $tableClause .= ' ' . $use;
-                                       }
-                               }
-                               if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
-                                       $ignore = $this->ignoreIndexClause( implode( ',', (array)$ignore_index[$alias] ) );
-                                       if ( $ignore != '' ) {
-                                               $tableClause .= ' ' . $ignore;
-                                       }
-                               }
-                               $on = $this->makeList( (array)$conds, LIST_AND );
-                               if ( $on != '' ) {
-                                       $tableClause .= ' ON (' . $on . ')';
-                               }
-
-                               $retJOIN[] = $tableClause;
-                       } elseif ( isset( $use_index[$alias] ) ) {
-                               // Is there an INDEX clause for this table?
-                               $tableClause = $this->tableNameWithAlias( $table, $alias );
-                               $tableClause .= ' ' . $this->useIndexClause(
-                                       implode( ',', (array)$use_index[$alias] )
-                               );
-
-                               $ret[] = $tableClause;
-                       } elseif ( isset( $ignore_index[$alias] ) ) {
-                               // Is there an INDEX clause for this table?
-                               $tableClause = $this->tableNameWithAlias( $table, $alias );
-                               $tableClause .= ' ' . $this->ignoreIndexClause(
-                                       implode( ',', (array)$ignore_index[$alias] )
-                               );
-
-                               $ret[] = $tableClause;
-                       } else {
-                               $tableClause = $this->tableNameWithAlias( $table, $alias );
-
-                               $ret[] = $tableClause;
-                       }
-               }
-
-               // We can't separate explicit JOIN clauses with ',', use ' ' for those
-               $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
-               $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
-
-               // Compile our final table clause
-               return implode( ' ', [ $implicitJoins, $explicitJoins ] );
-       }
-
-       /**
-        * Get the name of an index in a given table.
-        *
-        * @param string $index
-        * @return string
-        */
-       protected function indexName( $index ) {
-               // Backwards-compatibility hack
-               $renamed = [
-                       'ar_usertext_timestamp' => 'usertext_timestamp',
-                       'un_user_id' => 'user_id',
-                       'un_user_ip' => 'user_ip',
-               ];
-
-               if ( isset( $renamed[$index] ) ) {
-                       return $renamed[$index];
-               } else {
-                       return $index;
-               }
-       }
-
-       public function addQuotes( $s ) {
-               if ( $s instanceof Blob ) {
-                       $s = $s->fetch();
-               }
-               if ( $s === null ) {
-                       return 'NULL';
-               } else {
-                       # This will also quote numeric values. This should be harmless,
-                       # and protects against weird problems that occur when they really
-                       # _are_ strings such as article titles and string->number->string
-                       # conversion is not 1:1.
-                       return "'" . $this->strencode( $s ) . "'";
-               }
-       }
-
-       /**
-        * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
-        * MySQL uses `backticks` while basically everything else uses double quotes.
-        * Since MySQL is the odd one out here the double quotes are our generic
-        * and we implement backticks in DatabaseMysql.
-        *
-        * @param string $s
-        * @return string
-        */
-       public function addIdentifierQuotes( $s ) {
-               return '"' . str_replace( '"', '""', $s ) . '"';
-       }
-
-       /**
-        * Returns if the given identifier looks quoted or not according to
-        * the database convention for quoting identifiers .
-        *
-        * @note Do not use this to determine if untrusted input is safe.
-        *   A malicious user can trick this function.
-        * @param string $name
-        * @return bool
-        */
-       public function isQuotedIdentifier( $name ) {
-               return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
-       }
-
-       /**
-        * @param string $s
-        * @return string
-        */
-       protected function escapeLikeInternal( $s ) {
-               return addcslashes( $s, '\%_' );
-       }
-
-       public function buildLike() {
-               $params = func_get_args();
-
-               if ( count( $params ) > 0 && is_array( $params[0] ) ) {
-                       $params = $params[0];
-               }
-
-               $s = '';
-
-               foreach ( $params as $value ) {
-                       if ( $value instanceof LikeMatch ) {
-                               $s .= $value->toString();
-                       } else {
-                               $s .= $this->escapeLikeInternal( $value );
-                       }
-               }
-
-               return " LIKE {$this->addQuotes( $s )} ";
-       }
-
-       public function anyChar() {
-               return new LikeMatch( '_' );
-       }
-
-       public function anyString() {
-               return new LikeMatch( '%' );
-       }
-
-       public function nextSequenceValue( $seqName ) {
-               return null;
-       }
-
-       /**
-        * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
-        * is only needed because a) MySQL must be as efficient as possible due to
-        * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
-        * which index to pick. Anyway, other databases might have different
-        * indexes on a given table. So don't bother overriding this unless you're
-        * MySQL.
-        * @param string $index
-        * @return string
-        */
-       public function useIndexClause( $index ) {
-               return '';
-       }
-
-       /**
-        * IGNORE INDEX clause. Unlikely to be useful for anything but MySQL. This
-        * is only needed because a) MySQL must be as efficient as possible due to
-        * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
-        * which index to pick. Anyway, other databases might have different
-        * indexes on a given table. So don't bother overriding this unless you're
-        * MySQL.
-        * @param string $index
-        * @return string
-        */
-       public function ignoreIndexClause( $index ) {
-               return '';
-       }
-
-       public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
-               $quotedTable = $this->tableName( $table );
-
-               if ( count( $rows ) == 0 ) {
-                       return;
-               }
-
-               # Single row case
-               if ( !is_array( reset( $rows ) ) ) {
-                       $rows = [ $rows ];
-               }
-
-               // @FXIME: this is not atomic, but a trx would break affectedRows()
-               foreach ( $rows as $row ) {
-                       # Delete rows which collide
-                       if ( $uniqueIndexes ) {
-                               $sql = "DELETE FROM $quotedTable WHERE ";
-                               $first = true;
-                               foreach ( $uniqueIndexes as $index ) {
-                                       if ( $first ) {
-                                               $first = false;
-                                               $sql .= '( ';
-                                       } else {
-                                               $sql .= ' ) OR ( ';
-                                       }
-                                       if ( is_array( $index ) ) {
-                                               $first2 = true;
-                                               foreach ( $index as $col ) {
-                                                       if ( $first2 ) {
-                                                               $first2 = false;
-                                                       } else {
-                                                               $sql .= ' AND ';
-                                                       }
-                                                       $sql .= $col . '=' . $this->addQuotes( $row[$col] );
-                                               }
-                                       } else {
-                                               $sql .= $index . '=' . $this->addQuotes( $row[$index] );
-                                       }
-                               }
-                               $sql .= ' )';
-                               $this->query( $sql, $fname );
-                       }
-
-                       # Now insert the row
-                       $this->insert( $table, $row, $fname );
-               }
-       }
-
-       /**
-        * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
-        * statement.
-        *
-        * @param string $table Table name
-        * @param array|string $rows Row(s) to insert
-        * @param string $fname Caller function name
-        *
-        * @return ResultWrapper
-        */
-       protected function nativeReplace( $table, $rows, $fname ) {
-               $table = $this->tableName( $table );
-
-               # Single row case
-               if ( !is_array( reset( $rows ) ) ) {
-                       $rows = [ $rows ];
-               }
-
-               $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
-               $first = true;
-
-               foreach ( $rows as $row ) {
-                       if ( $first ) {
-                               $first = false;
-                       } else {
-                               $sql .= ',';
-                       }
-
-                       $sql .= '(' . $this->makeList( $row ) . ')';
-               }
-
-               return $this->query( $sql, $fname );
-       }
-
-       public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
-               $fname = __METHOD__
-       ) {
-               if ( !count( $rows ) ) {
-                       return true; // nothing to do
-               }
-
-               if ( !is_array( reset( $rows ) ) ) {
-                       $rows = [ $rows ];
-               }
-
-               if ( count( $uniqueIndexes ) ) {
-                       $clauses = []; // list WHERE clauses that each identify a single row
-                       foreach ( $rows as $row ) {
-                               foreach ( $uniqueIndexes as $index ) {
-                                       $index = is_array( $index ) ? $index : [ $index ]; // columns
-                                       $rowKey = []; // unique key to this row
-                                       foreach ( $index as $column ) {
-                                               $rowKey[$column] = $row[$column];
-                                       }
-                                       $clauses[] = $this->makeList( $rowKey, LIST_AND );
-                               }
-                       }
-                       $where = [ $this->makeList( $clauses, LIST_OR ) ];
-               } else {
-                       $where = false;
-               }
-
-               $useTrx = !$this->mTrxLevel;
-               if ( $useTrx ) {
-                       $this->begin( $fname, self::TRANSACTION_INTERNAL );
-               }
-               try {
-                       # Update any existing conflicting row(s)
-                       if ( $where !== false ) {
-                               $ok = $this->update( $table, $set, $where, $fname );
-                       } else {
-                               $ok = true;
-                       }
-                       # Now insert any non-conflicting row(s)
-                       $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
-               } catch ( Exception $e ) {
-                       if ( $useTrx ) {
-                               $this->rollback( $fname, self::FLUSHING_INTERNAL );
-                       }
-                       throw $e;
-               }
-               if ( $useTrx ) {
-                       $this->commit( $fname, self::FLUSHING_INTERNAL );
-               }
-
-               return $ok;
-       }
-
-       public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
-               $fname = __METHOD__
-       ) {
-               if ( !$conds ) {
-                       throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
-               }
-
-               $delTable = $this->tableName( $delTable );
-               $joinTable = $this->tableName( $joinTable );
-               $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
-               if ( $conds != '*' ) {
-                       $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
-               }
-               $sql .= ')';
-
-               $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\";";
-               $res = $this->query( $sql, __METHOD__ );
-               $row = $this->fetchObject( $res );
-
-               $m = [];
-
-               if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
-                       $size = $m[1];
-               } else {
-                       $size = -1;
-               }
-
-               return $size;
-       }
-
-       /**
-        * A string to insert into queries to show that they're low-priority, like
-        * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
-        * string and nothing bad should happen.
-        *
-        * @return string Returns the text of the low priority option if it is
-        *   supported, or a blank string otherwise
-        */
-       public function lowPriorityOption() {
-               return '';
-       }
-
-       public function delete( $table, $conds, $fname = __METHOD__ ) {
-               if ( !$conds ) {
-                       throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
-               }
-
-               $table = $this->tableName( $table );
-               $sql = "DELETE FROM $table";
-
-               if ( $conds != '*' ) {
-                       if ( is_array( $conds ) ) {
-                               $conds = $this->makeList( $conds, LIST_AND );
-                       }
-                       $sql .= ' WHERE ' . $conds;
-               }
-
-               return $this->query( $sql, $fname );
-       }
-
-       public function insertSelect(
-               $destTable, $srcTable, $varMap, $conds,
-               $fname = __METHOD__, $insertOptions = [], $selectOptions = []
-       ) {
-               if ( $this->cliMode ) {
-                       // For massive migrations with downtime, we don't want to select everything
-                       // into memory and OOM, so do all this native on the server side if possible.
-                       return $this->nativeInsertSelect(
-                               $destTable,
-                               $srcTable,
-                               $varMap,
-                               $conds,
-                               $fname,
-                               $insertOptions,
-                               $selectOptions
-                       );
-               }
-
-               // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
-               // on only the master (without needing row-based-replication). It also makes it easy to
-               // know how big the INSERT is going to be.
-               $fields = [];
-               foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
-                       $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
-               }
-               $selectOptions[] = 'FOR UPDATE';
-               $res = $this->select( $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions );
-               if ( !$res ) {
-                       return false;
-               }
-
-               $rows = [];
-               foreach ( $res as $row ) {
-                       $rows[] = (array)$row;
-               }
-
-               return $this->insert( $destTable, $rows, $fname, $insertOptions );
-       }
-
-       public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
-               $fname = __METHOD__,
-               $insertOptions = [], $selectOptions = []
-       ) {
-               $destTable = $this->tableName( $destTable );
-
-               if ( !is_array( $insertOptions ) ) {
-                       $insertOptions = [ $insertOptions ];
-               }
-
-               $insertOptions = $this->makeInsertOptions( $insertOptions );
-
-               if ( !is_array( $selectOptions ) ) {
-                       $selectOptions = [ $selectOptions ];
-               }
-
-               list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions(
-                       $selectOptions );
-
-               if ( is_array( $srcTable ) ) {
-                       $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
-               } else {
-                       $srcTable = $this->tableName( $srcTable );
-               }
-
-               $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
-                       " SELECT $startOpts " . implode( ',', $varMap ) .
-                       " FROM $srcTable $useIndex $ignoreIndex ";
-
-               if ( $conds != '*' ) {
-                       if ( is_array( $conds ) ) {
-                               $conds = $this->makeList( $conds, LIST_AND );
-                       }
-                       $sql .= " WHERE $conds";
-               }
-
-               $sql .= " $tailOpts";
-
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * Construct a LIMIT query with optional offset. This is used for query
-        * pages. The SQL should be adjusted so that only the first $limit rows
-        * are returned. If $offset is provided as well, then the first $offset
-        * rows should be discarded, and the next $limit rows should be returned.
-        * If the result of the query is not ordered, then the rows to be returned
-        * are theoretically arbitrary.
-        *
-        * $sql is expected to be a SELECT, if that makes a difference.
-        *
-        * The version provided by default works in MySQL and SQLite. It will very
-        * likely need to be overridden for most other DBMSes.
-        *
-        * @param string $sql SQL query we will append the limit too
-        * @param int $limit The SQL limit
-        * @param int|bool $offset The SQL offset (default false)
-        * @throws DBUnexpectedError
-        * @return string
-        */
-       public function limitResult( $sql, $limit, $offset = false ) {
-               if ( !is_numeric( $limit ) ) {
-                       throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
-               }
-
-               return "$sql LIMIT "
-                       . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
-                       . "{$limit} ";
-       }
-
-       public function unionSupportsOrderAndLimit() {
-               return true; // True for almost every DB supported
-       }
-
-       public function unionQueries( $sqls, $all ) {
-               $glue = $all ? ') UNION ALL (' : ') UNION (';
-
-               return '(' . implode( $glue, $sqls ) . ')';
-       }
-
-       public function conditional( $cond, $trueVal, $falseVal ) {
-               if ( is_array( $cond ) ) {
-                       $cond = $this->makeList( $cond, LIST_AND );
-               }
-
-               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
-       }
-
-       public function strreplace( $orig, $old, $new ) {
-               return "REPLACE({$orig}, {$old}, {$new})";
-       }
-
-       public function getServerUptime() {
-               return 0;
-       }
-
-       public function wasDeadlock() {
-               return false;
-       }
-
-       public function wasLockTimeout() {
-               return false;
-       }
-
-       public function wasErrorReissuable() {
-               return false;
-       }
-
-       public function wasReadOnlyError() {
-               return false;
-       }
-
-       /**
-        * Determines if the given query error was a connection drop
-        * STUB
-        *
-        * @param integer|string $errno
-        * @return bool
-        */
-       public function wasConnectionError( $errno ) {
-               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 );
-               $tries = self::DEADLOCK_TRIES;
-
-               $this->begin( __METHOD__ );
-
-               $retVal = null;
-               /** @var Exception $e */
-               $e = null;
-               do {
-                       try {
-                               $retVal = call_user_func_array( $function, $args );
-                               break;
-                       } catch ( DBQueryError $e ) {
-                               if ( $this->wasDeadlock() ) {
-                                       // Retry after a randomized delay
-                                       usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
-                               } else {
-                                       // Throw the error back up
-                                       throw $e;
-                               }
-                       }
-               } while ( --$tries > 0 );
-
-               if ( $tries <= 0 ) {
-                       // Too many deadlocks; give up
-                       $this->rollback( __METHOD__ );
-                       throw $e;
-               } else {
-                       $this->commit( __METHOD__ );
-
-                       return $retVal;
-               }
-       }
-
-       public function masterPosWait( DBMasterPos $pos, $timeout ) {
-               # Real waits are implemented in the subclass.
-               return 0;
-       }
-
-       public function getSlavePos() {
-               # Stub
-               return false;
-       }
-
-       public function getMasterPos() {
-               # Stub
-               return false;
-       }
-
-       public function serverIsReadOnly() {
-               return false;
-       }
-
-       final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
-               if ( !$this->mTrxLevel ) {
-                       throw new DBUnexpectedError( $this, "No transaction is active." );
-               }
-               $this->mTrxEndCallbacks[] = [ $callback, $fname ];
-       }
-
-       final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
-               $this->mTrxIdleCallbacks[] = [ $callback, $fname ];
-               if ( !$this->mTrxLevel ) {
-                       $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
-               }
-       }
-
-       final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
-               if ( $this->mTrxLevel ) {
-                       $this->mTrxPreCommitCallbacks[] = [ $callback, $fname ];
-               } else {
-                       // If no transaction is active, then make one for this callback
-                       $this->startAtomic( __METHOD__ );
-                       try {
-                               call_user_func( $callback );
-                               $this->endAtomic( __METHOD__ );
-                       } catch ( Exception $e ) {
-                               $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
-                               throw $e;
-                       }
-               }
-       }
-
-       final public function setTransactionListener( $name, callable $callback = null ) {
-               if ( $callback ) {
-                       $this->mTrxRecurringCallbacks[$name] = $callback;
-               } else {
-                       unset( $this->mTrxRecurringCallbacks[$name] );
-               }
-       }
-
-       /**
-        * Whether to disable running of post-COMMIT/ROLLBACK callbacks
-        *
-        * This method should not be used outside of Database/LoadBalancer
-        *
-        * @param bool $suppress
-        * @since 1.28
-        */
-       final public function setTrxEndCallbackSuppression( $suppress ) {
-               $this->mTrxEndCallbacksSuppressed = $suppress;
-       }
-
-       /**
-        * Actually run and consume any "on transaction idle/resolution" callbacks.
-        *
-        * This method should not be used outside of Database/LoadBalancer
-        *
-        * @param integer $trigger IDatabase::TRIGGER_* constant
-        * @since 1.20
-        * @throws Exception
-        */
-       public function runOnTransactionIdleCallbacks( $trigger ) {
-               if ( $this->mTrxEndCallbacksSuppressed ) {
-                       return;
-               }
-
-               $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
-               /** @var Exception $e */
-               $e = null; // first exception
-               do { // callbacks may add callbacks :)
-                       $callbacks = array_merge(
-                               $this->mTrxIdleCallbacks,
-                               $this->mTrxEndCallbacks // include "transaction resolution" callbacks
-                       );
-                       $this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
-                       $this->mTrxEndCallbacks = []; // consumed (recursion guard)
-                       foreach ( $callbacks as $callback ) {
-                               try {
-                                       list( $phpCallback ) = $callback;
-                                       $this->clearFlag( DBO_TRX ); // make each query its own transaction
-                                       call_user_func_array( $phpCallback, [ $trigger ] );
-                                       if ( $autoTrx ) {
-                                               $this->setFlag( DBO_TRX ); // restore automatic begin()
-                                       } else {
-                                               $this->clearFlag( DBO_TRX ); // restore auto-commit
-                                       }
-                               } catch ( Exception $ex ) {
-                                       call_user_func( $this->errorLogger, $ex );
-                                       $e = $e ?: $ex;
-                                       // Some callbacks may use startAtomic/endAtomic, so make sure
-                                       // their transactions are ended so other callbacks don't fail
-                                       if ( $this->trxLevel() ) {
-                                               $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
-                                       }
-                               }
-                       }
-               } while ( count( $this->mTrxIdleCallbacks ) );
-
-               if ( $e instanceof Exception ) {
-                       throw $e; // re-throw any first exception
-               }
-       }
-
-       /**
-        * Actually run and consume any "on transaction pre-commit" callbacks.
-        *
-        * This method should not be used outside of Database/LoadBalancer
-        *
-        * @since 1.22
-        * @throws Exception
-        */
-       public function runOnTransactionPreCommitCallbacks() {
-               $e = null; // first exception
-               do { // callbacks may add callbacks :)
-                       $callbacks = $this->mTrxPreCommitCallbacks;
-                       $this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
-                       foreach ( $callbacks as $callback ) {
-                               try {
-                                       list( $phpCallback ) = $callback;
-                                       call_user_func( $phpCallback );
-                               } catch ( Exception $ex ) {
-                                       call_user_func( $this->errorLogger, $ex );
-                                       $e = $e ?: $ex;
-                               }
-                       }
-               } while ( count( $this->mTrxPreCommitCallbacks ) );
-
-               if ( $e instanceof Exception ) {
-                       throw $e; // re-throw any first exception
-               }
-       }
-
-       /**
-        * Actually run any "transaction listener" callbacks.
-        *
-        * This method should not be used outside of Database/LoadBalancer
-        *
-        * @param integer $trigger IDatabase::TRIGGER_* constant
-        * @throws Exception
-        * @since 1.20
-        */
-       public function runTransactionListenerCallbacks( $trigger ) {
-               if ( $this->mTrxEndCallbacksSuppressed ) {
-                       return;
-               }
-
-               /** @var Exception $e */
-               $e = null; // first exception
-
-               foreach ( $this->mTrxRecurringCallbacks as $phpCallback ) {
-                       try {
-                               $phpCallback( $trigger, $this );
-                       } catch ( Exception $ex ) {
-                               call_user_func( $this->errorLogger, $ex );
-                               $e = $e ?: $ex;
-                       }
-               }
-
-               if ( $e instanceof Exception ) {
-                       throw $e; // re-throw any first exception
-               }
-       }
-
-       final public function startAtomic( $fname = __METHOD__ ) {
-               if ( !$this->mTrxLevel ) {
-                       $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 ) ) {
-                               $this->mTrxAutomaticAtomic = true;
-                       }
-               }
-
-               $this->mTrxAtomicLevels[] = $fname;
-       }
-
-       final public function endAtomic( $fname = __METHOD__ ) {
-               if ( !$this->mTrxLevel ) {
-                       throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
-               }
-               if ( !$this->mTrxAtomicLevels ||
-                       array_pop( $this->mTrxAtomicLevels ) !== $fname
-               ) {
-                       throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
-               }
-
-               if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
-                       $this->commit( $fname, self::FLUSHING_INTERNAL );
-               }
-       }
-
-       final public function doAtomicSection( $fname, callable $callback ) {
-               $this->startAtomic( $fname );
-               try {
-                       $res = call_user_func_array( $callback, [ $this, $fname ] );
-               } catch ( Exception $e ) {
-                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
-                       throw $e;
-               }
-               $this->endAtomic( $fname );
-
-               return $res;
-       }
-
-       final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
-               // Protect against mismatched atomic section, transaction nesting, and snapshot loss
-               if ( $this->mTrxLevel ) {
-                       if ( $this->mTrxAtomicLevels ) {
-                               $levels = implode( ', ', $this->mTrxAtomicLevels );
-                               $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
-                               throw new DBUnexpectedError( $this, $msg );
-                       } elseif ( !$this->mTrxAutomatic ) {
-                               $msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
-                               throw new DBUnexpectedError( $this, $msg );
-                       } else {
-                               // @TODO: make this an exception at some point
-                               $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
-                               $this->queryLogger->error( $msg );
-                               return; // join the main transaction set
-                       }
-               } elseif ( $this->getFlag( 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 );
-                       return; // let any writes be in the main transaction
-               }
-
-               // Avoid fatals if close() was called
-               $this->assertOpen();
-
-               $this->doBegin( $fname );
-               $this->mTrxTimestamp = microtime( true );
-               $this->mTrxFname = $fname;
-               $this->mTrxDoneWrites = false;
-               $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
-               $this->mTrxAutomaticAtomic = false;
-               $this->mTrxAtomicLevels = [];
-               $this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
-               $this->mTrxWriteDuration = 0.0;
-               $this->mTrxWriteQueryCount = 0;
-               $this->mTrxWriteAdjDuration = 0.0;
-               $this->mTrxWriteAdjQueryCount = 0;
-               $this->mTrxWriteCallers = [];
-               // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
-               // Get an estimate of the replica DB lag before then, treating estimate staleness
-               // as lag itself just to be safe
-               $status = $this->getApproximateLagStatus();
-               $this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
-       }
-
-       /**
-        * Issues the BEGIN command to the database server.
-        *
-        * @see DatabaseBase::begin()
-        * @param string $fname
-        */
-       protected function doBegin( $fname ) {
-               $this->query( 'BEGIN', $fname );
-               $this->mTrxLevel = 1;
-       }
-
-       final public function commit( $fname = __METHOD__, $flush = '' ) {
-               if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
-                       // There are still atomic sections open. This cannot be ignored
-                       $levels = implode( ', ', $this->mTrxAtomicLevels );
-                       throw new DBUnexpectedError(
-                               $this,
-                               "$fname: Got COMMIT while atomic sections $levels are still open."
-                       );
-               }
-
-               if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
-                       if ( !$this->mTrxLevel ) {
-                               return; // nothing to do
-                       } elseif ( !$this->mTrxAutomatic ) {
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       "$fname: Flushing an explicit transaction, getting out of sync."
-                               );
-                       }
-               } else {
-                       if ( !$this->mTrxLevel ) {
-                               $this->queryLogger->error( "$fname: No transaction to commit, something got out of sync." );
-                               return; // nothing to do
-                       } elseif ( $this->mTrxAutomatic ) {
-                               // @TODO: make this an exception at some point
-                               $msg = "$fname: Explicit commit of implicit transaction.";
-                               $this->queryLogger->error( $msg );
-                               return; // wait for the main transaction set commit round
-                       }
-               }
-
-               // Avoid fatals if close() was called
-               $this->assertOpen();
-
-               $this->runOnTransactionPreCommitCallbacks();
-               $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
-               $this->doCommit( $fname );
-               if ( $this->mTrxDoneWrites ) {
-                       $this->mDoneWrites = microtime( true );
-                       $this->trxProfiler->transactionWritingOut(
-                               $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
-               }
-
-               $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
-               $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
-       }
-
-       /**
-        * Issues the COMMIT command to the database server.
-        *
-        * @see DatabaseBase::commit()
-        * @param string $fname
-        */
-       protected function doCommit( $fname ) {
-               if ( $this->mTrxLevel ) {
-                       $this->query( 'COMMIT', $fname );
-                       $this->mTrxLevel = 0;
-               }
-       }
-
-       final public function rollback( $fname = __METHOD__, $flush = '' ) {
-               if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
-                       if ( !$this->mTrxLevel ) {
-                               return; // nothing to do
-                       }
-               } else {
-                       if ( !$this->mTrxLevel ) {
-                               $this->queryLogger->error(
-                                       "$fname: No transaction to rollback, something got out of sync." );
-                               return; // nothing to do
-                       } elseif ( $this->getFlag( DBO_TRX ) ) {
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
-                               );
-                       }
-               }
-
-               // Avoid fatals if close() was called
-               $this->assertOpen();
-
-               $this->doRollback( $fname );
-               $this->mTrxAtomicLevels = [];
-               if ( $this->mTrxDoneWrites ) {
-                       $this->trxProfiler->transactionWritingOut(
-                               $this->mServer, $this->mDBname, $this->mTrxShortId );
-               }
-
-               $this->mTrxIdleCallbacks = []; // clear
-               $this->mTrxPreCommitCallbacks = []; // clear
-               $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
-               $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
-       }
-
-       /**
-        * Issues the ROLLBACK command to the database server.
-        *
-        * @see DatabaseBase::rollback()
-        * @param string $fname
-        */
-       protected function doRollback( $fname ) {
-               if ( $this->mTrxLevel ) {
-                       # Disconnects cause rollback anyway, so ignore those errors
-                       $ignoreErrors = true;
-                       $this->query( 'ROLLBACK', $fname, $ignoreErrors );
-                       $this->mTrxLevel = 0;
-               }
-       }
-
-       public function flushSnapshot( $fname = __METHOD__ ) {
-               if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
-                       // This only flushes transactions to clear snapshots, not to write data
-                       $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
-                       throw new DBUnexpectedError(
-                               $this,
-                               "$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
-                       );
-               }
-
-               $this->commit( $fname, self::FLUSHING_INTERNAL );
-       }
-
-       public function explicitTrxActive() {
-               return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
-       }
-
-       /**
-        * Creates a new table with structure copied from existing table
-        * Note that unlike most database abstraction functions, this function does not
-        * automatically append database prefix, because it works at a lower
-        * abstraction level.
-        * The table names passed to this function shall not be quoted (this
-        * function calls addIdentifierQuotes when needed).
-        *
-        * @param string $oldName Name of table whose structure should be copied
-        * @param string $newName Name of table to be created
-        * @param bool $temporary Whether the new table should be temporary
-        * @param string $fname Calling function name
-        * @throws RuntimeException
-        * @return bool True if operation was successful
-        */
-       public function duplicateTableStructure( $oldName, $newName, $temporary = false,
-               $fname = __METHOD__
-       ) {
-               throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
-       }
-
-       function listTables( $prefix = null, $fname = __METHOD__ ) {
-               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 ConvertableTimestamp( $ts );
-               // Let errors bubble up to avoid putting garbage in the DB
-               return $t->getTimestamp( TS_MW );
-       }
-
-       public function timestampOrNull( $ts = null ) {
-               if ( is_null( $ts ) ) {
-                       return null;
-               } else {
-                       return $this->timestamp( $ts );
-               }
-       }
-
-       /**
-        * Take the result from a query, and wrap it in a ResultWrapper if
-        * necessary. Boolean values are passed through as is, to indicate success
-        * of write queries or failure.
-        *
-        * Once upon a time, DatabaseBase::query() returned a bare MySQL result
-        * resource, and it was necessary to call this function to convert it to
-        * a wrapper. Nowadays, raw database objects are never exposed to external
-        * callers, so this is unnecessary in external code.
-        *
-        * @param bool|ResultWrapper|resource|object $result
-        * @return bool|ResultWrapper
-        */
-       protected function resultObject( $result ) {
-               if ( !$result ) {
-                       return false;
-               } elseif ( $result instanceof ResultWrapper ) {
-                       return $result;
-               } elseif ( $result === true ) {
-                       // Successful write query
-                       return $result;
-               } else {
-                       return new ResultWrapper( $this, $result );
-               }
-       }
-
-       public function ping( &$rtt = null ) {
-               // Avoid hitting the server if it was hit recently
-               if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
-                       if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
-                               $rtt = $this->mRTTEstimate;
-                               return true; // don't care about $rtt
-                       }
-               }
-
-               // This will reconnect if possible or return false if not
-               $this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
-               $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
-               $this->restoreFlags( self::RESTORE_PRIOR );
-
-               if ( $ok ) {
-                       $rtt = $this->mRTTEstimate;
-               }
-
-               return $ok;
-       }
-
-       /**
-        * @return bool
-        */
-       protected function reconnect() {
-               $this->closeConnection();
-               $this->mOpened = false;
-               $this->mConn = false;
-               try {
-                       $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
-                       $this->lastPing = microtime( true );
-                       $ok = true;
-               } catch ( DBConnectionError $e ) {
-                       $ok = false;
-               }
-
-               return $ok;
-       }
-
-       public function getSessionLagStatus() {
-               return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
-       }
-
-       /**
-        * Get the replica DB lag when the current transaction started
-        *
-        * This is useful when transactions might use snapshot isolation
-        * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
-        * is this lag plus transaction duration. If they don't, it is still
-        * safe to be pessimistic. This returns null if there is no transaction.
-        *
-        * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
-        * @since 1.27
-        */
-       public function getTransactionLagStatus() {
-               return $this->mTrxLevel
-                       ? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
-                       : null;
-       }
-
-       /**
-        * Get a replica DB lag estimate for this server
-        *
-        * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
-        * @since 1.27
-        */
-       public function getApproximateLagStatus() {
-               return [
-                       'lag'   => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
-                       'since' => microtime( true )
-               ];
-       }
-
-       /**
-        * Merge the result of getSessionLagStatus() for several DBs
-        * using the most pessimistic values to estimate the lag of
-        * any data derived from them in combination
-        *
-        * This is information is useful for caching modules
-        *
-        * @see WANObjectCache::set()
-        * @see WANObjectCache::getWithSetCallback()
-        *
-        * @param IDatabase $db1
-        * @param IDatabase ...
-        * @return array Map of values:
-        *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
-        *   - since: oldest UNIX timestamp of any of the DB lag estimates
-        *   - pending: whether any of the DBs have uncommitted changes
-        * @since 1.27
-        */
-       public static function getCacheSetOptions( IDatabase $db1 ) {
-               $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
-               foreach ( func_get_args() as $db ) {
-                       /** @var IDatabase $db */
-                       $status = $db->getSessionLagStatus();
-                       if ( $status['lag'] === false ) {
-                               $res['lag'] = false;
-                       } elseif ( $res['lag'] !== false ) {
-                               $res['lag'] = max( $res['lag'], $status['lag'] );
-                       }
-                       $res['since'] = min( $res['since'], $status['since'] );
-                       $res['pending'] = $res['pending'] ?: $db->writesPending();
-               }
-
-               return $res;
-       }
-
-       public function getLag() {
-               return 0;
-       }
-
-       function maxListLen() {
-               return 0;
-       }
-
-       public function encodeBlob( $b ) {
-               return $b;
-       }
-
-       public function decodeBlob( $b ) {
-               if ( $b instanceof Blob ) {
-                       $b = $b->fetch();
-               }
-               return $b;
-       }
-
-       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, $resultCallback = false, $fname = false, $inputCallback = false
-       ) {
-               MediaWiki\suppressWarnings();
-               $fp = fopen( $filename, 'r' );
-               MediaWiki\restoreWarnings();
-
-               if ( false === $fp ) {
-                       throw new RuntimeException( "Could not open \"{$filename}\".\n" );
-               }
-
-               if ( !$fname ) {
-                       $fname = __METHOD__ . "( $filename )";
-               }
-
-               try {
-                       $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
-               } catch ( Exception $e ) {
-                       fclose( $fp );
-                       throw $e;
-               }
-
-               fclose( $fp );
-
-               return $error;
-       }
-
-       public function setSchemaVars( $vars ) {
-               $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, $resultCallback = false,
-               $fname = __METHOD__, $inputCallback = false
-       ) {
-               $cmd = '';
-
-               while ( !feof( $fp ) ) {
-                       if ( $lineCallback ) {
-                               call_user_func( $lineCallback );
-                       }
-
-                       $line = trim( fgets( $fp ) );
-
-                       if ( $line == '' ) {
-                               continue;
-                       }
-
-                       if ( '-' == $line[0] && '-' == $line[1] ) {
-                               continue;
-                       }
-
-                       if ( $cmd != '' ) {
-                               $cmd .= ' ';
-                       }
-
-                       $done = $this->streamStatementEnd( $cmd, $line );
-
-                       $cmd .= "$line\n";
-
-                       if ( $done || feof( $fp ) ) {
-                               $cmd = $this->replaceVars( $cmd );
-
-                               if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
-                                       $res = $this->query( $cmd, $fname );
-
-                                       if ( $resultCallback ) {
-                                               call_user_func( $resultCallback, $res, $this );
-                                       }
-
-                                       if ( false === $res ) {
-                                               $err = $this->lastError();
-
-                                               return "Query \"{$cmd}\" failed with error code \"$err\".\n";
-                                       }
-                               }
-                               $cmd = '';
-                       }
-               }
-
-               return true;
-       }
-
-       /**
-        * 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 ) {
-               if ( $this->delimiter ) {
-                       $prev = $newLine;
-                       $newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
-                       if ( $newLine != $prev ) {
-                               return true;
-                       }
-               }
-
-               return false;
-       }
-
-       /**
-        * Database independent variable replacement. Replaces a set of variables
-        * in an SQL statement with their contents as given by $this->getSchemaVars().
-        *
-        * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
-        *
-        * - '{$var}' should be used for text and is passed through the database's
-        *   addQuotes method.
-        * - `{$var}` should be used for identifiers (e.g. table and database names).
-        *   It is passed through the database's addIdentifierQuotes method which
-        *   can be overridden if the database uses something other than backticks.
-        * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
-        *   database's tableName method.
-        * - / *i* / passes the name that follows through the database's indexName method.
-        * - In all other cases, / *$var* / is left unencoded. Except for table options,
-        *   its use should be avoided. In 1.24 and older, string encoding was applied.
-        *
-        * @param string $ins SQL statement to replace variables in
-        * @return string The new SQL statement with variables replaced
-        */
-       protected function replaceVars( $ins ) {
-               $vars = $this->getSchemaVars();
-               return preg_replace_callback(
-                       '!
-                               /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
-                               \'\{\$ (\w+) }\'                  | # 3. addQuotes
-                               `\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
-                               /\*\$ (\w+) \*/                     # 5. leave unencoded
-                       !x',
-                       function ( $m ) use ( $vars ) {
-                               // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
-                               // check for both nonexistent keys *and* the empty string.
-                               if ( isset( $m[1] ) && $m[1] !== '' ) {
-                                       if ( $m[1] === 'i' ) {
-                                               return $this->indexName( $m[2] );
-                                       } else {
-                                               return $this->tableName( $m[2] );
-                                       }
-                               } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
-                                       return $this->addQuotes( $vars[$m[3]] );
-                               } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
-                                       return $this->addIdentifierQuotes( $vars[$m[4]] );
-                               } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
-                                       return $vars[$m[5]];
-                               } else {
-                                       return $m[0];
-                               }
-                       },
-                       $ins
-               );
-       }
-
-       /**
-        * Get schema variables. If none have been set via setSchemaVars(), then
-        * use some defaults from the current object.
-        *
-        * @return array
-        */
-       protected function getSchemaVars() {
-               if ( $this->mSchemaVars ) {
-                       return $this->mSchemaVars;
-               } else {
-                       return $this->getDefaultSchemaVars();
-               }
-       }
-
-       /**
-        * Get schema variables to use if none have been set via setSchemaVars().
-        *
-        * Override this in derived classes to provide variables for tables.sql
-        * and SQL patch files.
-        *
-        * @return array
-        */
-       protected function getDefaultSchemaVars() {
-               return [];
-       }
-
-       public function lockIsFree( $lockName, $method ) {
-               return true;
-       }
-
-       public function lock( $lockName, $method, $timeout = 5 ) {
-               $this->mNamedLocksHeld[$lockName] = 1;
-
-               return true;
-       }
-
-       public function unlock( $lockName, $method ) {
-               unset( $this->mNamedLocksHeld[$lockName] );
-
-               return true;
-       }
-
-       public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
-               if ( $this->writesOrCallbacksPending() ) {
-                       // This only flushes transactions to clear snapshots, not to write data
-                       $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
-                       throw new DBUnexpectedError(
-                               $this,
-                               "$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
-                       );
-               }
-
-               if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
-                       return null;
-               }
-
-               $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
-                       if ( $this->trxLevel() ) {
-                               // There is a good chance an exception was thrown, causing any early return
-                               // from the caller. Let any error handler get a chance to issue rollback().
-                               // If there isn't one, let the error bubble up and trigger server-side rollback.
-                               $this->onTransactionResolution(
-                                       function () use ( $lockKey, $fname ) {
-                                               $this->unlock( $lockKey, $fname );
-                                       },
-                                       $fname
-                               );
-                       } else {
-                               $this->unlock( $lockKey, $fname );
-                       }
-               } );
-
-               $this->commit( $fname, self::FLUSHING_INTERNAL );
-
-               return $unlocker;
-       }
-
-       public function namedLocksEnqueue() {
-               return false;
-       }
-
-       /**
-        * Lock specific tables
-        *
-        * @param array $read Array of tables to lock for read access
-        * @param array $write Array of tables to lock for write access
-        * @param string $method Name of caller
-        * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
-        * @return bool
-        */
-       public function lockTables( $read, $write, $method, $lowPriority = true ) {
-               return true;
-       }
-
-       /**
-        * Unlock specific tables
-        *
-        * @param string $method The caller
-        * @return bool
-        */
-       public function unlockTables( $method ) {
-               return true;
-       }
-
-       /**
-        * Delete a table
-        * @param string $tableName
-        * @param string $fName
-        * @return bool|ResultWrapper
-        * @since 1.18
-        */
-       public function dropTable( $tableName, $fName = __METHOD__ ) {
-               if ( !$this->tableExists( $tableName, $fName ) ) {
-                       return false;
-               }
-               $sql = "DROP TABLE " . $this->tableName( $tableName );
-               if ( $this->cascadingDeletes() ) {
-                       $sql .= " CASCADE";
-               }
-
-               return $this->query( $sql, $fName );
-       }
-
-       /**
-        * Get search engine class. All subclasses of this need to implement this
-        * if they wish to use searching.
-        *
-        * @return string
-        */
-       public function getSearchEngine() {
-               return 'SearchEngineDummy';
-       }
-
-       public function getInfinity() {
-               return 'infinity';
-       }
-
-       public function encodeExpiry( $expiry ) {
-               return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
-                       ? $this->getInfinity()
-                       : $this->timestamp( $expiry );
-       }
-
-       public function decodeExpiry( $expiry, $format = TS_MW ) {
-               if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
-                       return 'infinity';
-               }
-
-               try {
-                       $t = new ConvertableTimestamp( $expiry );
-
-                       return $t->getTimestamp( $format );
-               } catch ( TimestampException $e ) {
-                       return false;
-               }
-       }
-
-       public function setBigSelects( $value = true ) {
-               // no-op
-       }
-
-       public function isReadOnly() {
-               return ( $this->getReadOnlyReason() !== false );
-       }
-
-       /**
-        * @return string|bool Reason this DB is read-only or false if it is not
-        */
-       protected function getReadOnlyReason() {
-               $reason = $this->getLBInfo( 'readOnlyReason' );
-
-               return is_string( $reason ) ? $reason : false;
-       }
-
-       public function setTableAliases( array $aliases ) {
-               $this->tableAliases = $aliases;
-       }
-
-       /**
-        * @since 1.19
-        * @return string
-        */
-       public function __toString() {
-               return (string)$this->mConn;
-       }
-
-       /**
-        * Run a few simple sanity checks
-        */
-       public function __destruct() {
-               if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
-                       trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
-               }
-
-               $danglingWriters = $this->pendingWriteAndCallbackCallers();
-               if ( $danglingWriters ) {
-                       $fnames = implode( ', ', $danglingWriters );
-                       trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
-               }
-       }
-}
-
-/**
- * @since 1.27
- */
-abstract class Database extends DatabaseBase {
-       // B/C until nothing type hints for DatabaseBase
-       // @TODO: finish renaming DatabaseBase => Database
-}
index 4ffafde..6d07216 100644 (file)
@@ -28,7 +28,7 @@
 /**
  * @ingroup Database
  */
-class DatabaseMssql extends Database {
+class DatabaseMssql extends DatabaseBase {
        protected $mInsertId = null;
        protected $mLastResult = null;
        protected $mAffectedRows = null;
@@ -42,22 +42,6 @@ class DatabaseMssql extends Database {
 
        protected $mPort;
 
-       public function cascadingDeletes() {
-               return true;
-       }
-
-       public function cleanupTriggers() {
-               return false;
-       }
-
-       public function strictIPs() {
-               return false;
-       }
-
-       public function realTimestamps() {
-               return false;
-       }
-
        public function implicitGroupby() {
                return false;
        }
@@ -66,10 +50,6 @@ class DatabaseMssql extends Database {
                return false;
        }
 
-       public function functionalIndexes() {
-               return true;
-       }
-
        public function unionSupportsOrderAndLimit() {
                return false;
        }
@@ -757,15 +737,15 @@ class DatabaseMssql extends Database {
         * UPDATE wrapper. Takes a condition array and a SET array.
         *
         * @param string $table Name of the table to UPDATE. This will be passed through
-        *                DatabaseBase::tableName().
+        *                Database::tableName().
         *
         * @param array $values An array of values to SET. For each array element,
         *                the key gives the field name, and the value gives the data
         *                to set that field to. The data will be quoted by
-        *                DatabaseBase::addQuotes().
+        *                Database::addQuotes().
         *
         * @param array $conds An array of conditions (WHERE). See
-        *                DatabaseBase::select() for the details of the format of
+        *                Database::select() for the details of the format of
         *                condition arrays. Use '*' to update all rows.
         *
         * @param string $fname The function name of the caller (from __METHOD__),
@@ -791,7 +771,7 @@ class DatabaseMssql extends Database {
 
                $this->mScrollableCursor = false;
                try {
-                       $ret = $this->query( $sql );
+                       $this->query( $sql );
                } catch ( Exception $e ) {
                        $this->mScrollableCursor = true;
                        throw $e;
@@ -806,7 +786,7 @@ class DatabaseMssql extends Database {
         * @param int $mode Constant
         *      - LIST_COMMA:          comma separated, no field names
         *      - LIST_AND:            ANDed WHERE clause (without the WHERE). See
-        *        the documentation for $conds in DatabaseBase::select().
+        *        the documentation for $conds in Database::select().
         *      - LIST_OR:             ORed WHERE clause (without the WHERE)
         *      - LIST_SET:            comma separated with field names, like a SET clause
         *      - LIST_NAMES:          comma separated field names
@@ -1105,8 +1085,8 @@ class DatabaseMssql extends Database {
        }
 
        /**
-        * @param string|Blob $s
-        * @return string
+        * @param string|int|null|bool|Blob $s
+        * @return string|int
         */
        public function addQuotes( $s ) {
                if ( $s instanceof MssqlBlob ) {
@@ -1265,13 +1245,6 @@ class DatabaseMssql extends Database {
                return $sql;
        }
 
-       /**
-        * @return string
-        */
-       public function getSearchEngine() {
-               return "SearchMssql";
-       }
-
        /**
         * Returns an associative array for fields that are of type varbinary, binary, or image
         * $table can be either a raw table name or passed through tableName() first
diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php
deleted file mode 100644 (file)
index 87330b0..0000000
+++ /dev/null
@@ -1,204 +0,0 @@
-<?php
-/**
- * This is the MySQL database abstraction layer.
- *
- * 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
- */
-
-/**
- * Database abstraction object for PHP extension mysql.
- *
- * @ingroup Database
- * @see Database
- */
-class DatabaseMysql extends DatabaseMysqlBase {
-       /**
-        * @param string $sql
-        * @return resource False on error
-        */
-       protected function doQuery( $sql ) {
-               $conn = $this->getBindingHandle();
-
-               if ( $this->bufferResults() ) {
-                       $ret = mysql_query( $sql, $conn );
-               } else {
-                       $ret = mysql_unbuffered_query( $sql, $conn );
-               }
-
-               return $ret;
-       }
-
-       /**
-        * @param string $realServer
-        * @return bool|resource MySQL Database connection or false on failure to connect
-        * @throws DBConnectionError
-        */
-       protected function mysqlConnect( $realServer ) {
-               # Avoid a suppressed fatal error, which is very hard to track down
-               if ( !extension_loaded( 'mysql' ) ) {
-                       throw new DBConnectionError(
-                               $this,
-                               "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n"
-                       );
-               }
-
-               $connFlags = 0;
-               if ( $this->mFlags & DBO_SSL ) {
-                       $connFlags |= MYSQL_CLIENT_SSL;
-               }
-               if ( $this->mFlags & DBO_COMPRESS ) {
-                       $connFlags |= MYSQL_CLIENT_COMPRESS;
-               }
-
-               if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
-                       $numAttempts = 2;
-               } else {
-                       $numAttempts = 1;
-               }
-
-               $conn = false;
-
-               # The kernel's default SYN retransmission period is far too slow for us,
-               # so we use a short timeout plus a manual retry. Retrying means that a small
-               # but finite rate of SYN packet loss won't cause user-visible errors.
-               for ( $i = 0; $i < $numAttempts && !$conn; $i++ ) {
-                       if ( $i > 1 ) {
-                               usleep( 1000 );
-                       }
-                       if ( $this->mFlags & DBO_PERSISTENT ) {
-                               $conn = mysql_pconnect( $realServer, $this->mUser, $this->mPassword, $connFlags );
-                       } else {
-                               # Create a new connection...
-                               $conn = mysql_connect( $realServer, $this->mUser, $this->mPassword, true, $connFlags );
-                       }
-               }
-
-               return $conn;
-       }
-
-       /**
-        * @param string $charset
-        * @return bool
-        */
-       protected function mysqlSetCharset( $charset ) {
-               $conn = $this->getBindingHandle();
-
-               if ( function_exists( 'mysql_set_charset' ) ) {
-                       return mysql_set_charset( $charset, $conn );
-               } else {
-                       return $this->query( 'SET NAMES ' . $charset, __METHOD__ );
-               }
-       }
-
-       /**
-        * @return bool
-        */
-       protected function closeConnection() {
-               $conn = $this->getBindingHandle();
-
-               return mysql_close( $conn );
-       }
-
-       /**
-        * @return int
-        */
-       function insertId() {
-               $conn = $this->getBindingHandle();
-
-               return mysql_insert_id( $conn );
-       }
-
-       /**
-        * @return int
-        */
-       function lastErrno() {
-               if ( $this->mConn ) {
-                       return mysql_errno( $this->mConn );
-               } else {
-                       return mysql_errno();
-               }
-       }
-
-       /**
-        * @return int
-        */
-       function affectedRows() {
-               $conn = $this->getBindingHandle();
-
-               return mysql_affected_rows( $conn );
-       }
-
-       /**
-        * @param string $db
-        * @return bool
-        */
-       function selectDB( $db ) {
-               $conn = $this->getBindingHandle();
-
-               $this->mDBname = $db;
-
-               return mysql_select_db( $db, $conn );
-       }
-
-       protected function mysqlFreeResult( $res ) {
-               return mysql_free_result( $res );
-       }
-
-       protected function mysqlFetchObject( $res ) {
-               return mysql_fetch_object( $res );
-       }
-
-       protected function mysqlFetchArray( $res ) {
-               return mysql_fetch_array( $res );
-       }
-
-       protected function mysqlNumRows( $res ) {
-               return mysql_num_rows( $res );
-       }
-
-       protected function mysqlNumFields( $res ) {
-               return mysql_num_fields( $res );
-       }
-
-       protected function mysqlFetchField( $res, $n ) {
-               return mysql_fetch_field( $res, $n );
-       }
-
-       protected function mysqlFieldName( $res, $n ) {
-               return mysql_field_name( $res, $n );
-       }
-
-       protected function mysqlFieldType( $res, $n ) {
-               return mysql_field_type( $res, $n );
-       }
-
-       protected function mysqlDataSeek( $res, $row ) {
-               return mysql_data_seek( $res, $row );
-       }
-
-       protected function mysqlError( $conn = null ) {
-               return ( $conn !== null ) ? mysql_error( $conn ) : mysql_error(); // avoid warning
-       }
-
-       protected function mysqlRealEscapeString( $s ) {
-               $conn = $this->getBindingHandle();
-
-               return mysql_real_escape_string( $s, $conn );
-       }
-}
diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php
deleted file mode 100644 (file)
index f8737a8..0000000
+++ /dev/null
@@ -1,1353 +0,0 @@
-<?php
-/**
- * This is the MySQL database abstraction layer.
- *
- * 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
- */
-
-/**
- * Database abstraction object for MySQL.
- * Defines methods independent on used MySQL extension.
- *
- * @ingroup Database
- * @since 1.22
- * @see Database
- */
-abstract class DatabaseMysqlBase extends Database {
-       /** @var MysqlMasterPos */
-       protected $lastKnownReplicaPos;
-       /** @var string Method to detect replica DB lag */
-       protected $lagDetectionMethod;
-       /** @var array Method to detect replica DB lag */
-       protected $lagDetectionOptions = [];
-       /** @var bool bool Whether to use GTID methods */
-       protected $useGTIDs = false;
-       /** @var string|null */
-       protected $sslKeyPath;
-       /** @var string|null */
-       protected $sslCertPath;
-       /** @var string|null */
-       protected $sslCAPath;
-       /** @var string[]|null */
-       protected $sslCiphers;
-       /** @var string|null */
-       private $serverVersion = null;
-
-       /**
-        * Additional $params include:
-        *   - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
-        *       pt-heartbeat assumes the table is at heartbeat.heartbeat
-        *       and uses UTC timestamps in the heartbeat.ts column.
-        *       (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html)
-        *   - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map to change
-        *       the default behavior. Normally, the heartbeat row with the server
-        *       ID of this server's master will be used. Set the "conds" field to
-        *       override the query conditions, e.g. ['shard' => 's1'].
-        *   - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible.
-        *   - sslKeyPath : path to key file [default: null]
-        *   - sslCertPath : path to certificate file [default: null]
-        *   - sslCAPath : parth to certificate authority PEM files [default: null]
-        *   - sslCiphers : array list of allowable ciphers [default: null]
-        * @param array $params
-        */
-       function __construct( array $params ) {
-               parent::__construct( $params );
-
-               $this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
-                       ? $params['lagDetectionMethod']
-                       : 'Seconds_Behind_Master';
-               $this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
-                       ? $params['lagDetectionOptions']
-                       : [];
-               $this->useGTIDs = !empty( $params['useGTIDs' ] );
-               foreach ( [ 'KeyPath', 'CertPath', 'CAPath', 'Ciphers' ] as $name ) {
-                       $var = "ssl{$name}";
-                       if ( isset( $params[$var] ) ) {
-                               $this->$var = $params[$var];
-                       }
-               }
-       }
-
-       /**
-        * @return string
-        */
-       function getType() {
-               return 'mysql';
-       }
-
-       /**
-        * @param string $server
-        * @param string $user
-        * @param string $password
-        * @param string $dbName
-        * @throws Exception|DBConnectionError
-        * @return bool
-        */
-       function open( $server, $user, $password, $dbName ) {
-               global $wgAllDBsAreLocalhost, $wgSQLMode;
-
-               # Close/unset connection handle
-               $this->close();
-
-               # Debugging hack -- fake cluster
-               $realServer = $wgAllDBsAreLocalhost ? 'localhost' : $server;
-               $this->mServer = $server;
-               $this->mUser = $user;
-               $this->mPassword = $password;
-               $this->mDBname = $dbName;
-
-               $this->installErrorHandler();
-               try {
-                       $this->mConn = $this->mysqlConnect( $realServer );
-               } catch ( Exception $ex ) {
-                       $this->restoreErrorHandler();
-                       throw $ex;
-               }
-               $error = $this->restoreErrorHandler();
-
-               # Always log connection errors
-               if ( !$this->mConn ) {
-                       if ( !$error ) {
-                               $error = $this->lastError();
-                       }
-                       wfLogDBError(
-                               "Error connecting to {db_server}: {error}",
-                               $this->getLogContext( [
-                                       'method' => __METHOD__,
-                                       'error' => $error,
-                               ] )
-                       );
-                       wfDebug( "DB connection error\n" .
-                               "Server: $server, User: $user, Password: " .
-                               substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
-
-                       $this->reportConnectionError( $error );
-               }
-
-               if ( $dbName != '' ) {
-                       MediaWiki\suppressWarnings();
-                       $success = $this->selectDB( $dbName );
-                       MediaWiki\restoreWarnings();
-                       if ( !$success ) {
-                               wfLogDBError(
-                                       "Error selecting database {db_name} on server {db_server}",
-                                       $this->getLogContext( [
-                                               'method' => __METHOD__,
-                                       ] )
-                               );
-                               wfDebug( "Error selecting database $dbName on server {$this->mServer} " .
-                                       "from client host " . wfHostname() . "\n" );
-
-                               $this->reportConnectionError( "Error selecting database $dbName" );
-                       }
-               }
-
-               // Tell the server what we're communicating with
-               if ( !$this->connectInitCharset() ) {
-                       $this->reportConnectionError( "Error setting character set" );
-               }
-
-               // Abstract over any insane MySQL defaults
-               $set = [ 'group_concat_max_len = 262144' ];
-               // Set SQL mode, default is turning them all off, can be overridden or skipped with null
-               if ( is_string( $wgSQLMode ) ) {
-                       $set[] = 'sql_mode = ' . $this->addQuotes( $wgSQLMode );
-               }
-               // Set any custom settings defined by site config
-               // (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html)
-               foreach ( $this->mSessionVars as $var => $val ) {
-                       // Escape strings but not numbers to avoid MySQL complaining
-                       if ( !is_int( $val ) && !is_float( $val ) ) {
-                               $val = $this->addQuotes( $val );
-                       }
-                       $set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val;
-               }
-
-               if ( $set ) {
-                       // Use doQuery() to avoid opening implicit transactions (DBO_TRX)
-                       $success = $this->doQuery( 'SET ' . implode( ', ', $set ) );
-                       if ( !$success ) {
-                               wfLogDBError(
-                                       'Error setting MySQL variables on server {db_server} (check $wgSQLMode)',
-                                       $this->getLogContext( [
-                                               'method' => __METHOD__,
-                                       ] )
-                               );
-                               $this->reportConnectionError(
-                                       'Error setting MySQL variables on server {db_server} (check $wgSQLMode)' );
-                       }
-               }
-
-               $this->mOpened = true;
-
-               return true;
-       }
-
-       /**
-        * Set the character set information right after connection
-        * @return bool
-        */
-       protected function connectInitCharset() {
-               global $wgDBmysql5;
-
-               if ( $wgDBmysql5 ) {
-                       // Tell the server we're communicating with it in UTF-8.
-                       // This may engage various charset conversions.
-                       return $this->mysqlSetCharset( 'utf8' );
-               } else {
-                       return $this->mysqlSetCharset( 'binary' );
-               }
-       }
-
-       /**
-        * Open a connection to a MySQL server
-        *
-        * @param string $realServer
-        * @return mixed Raw connection
-        * @throws DBConnectionError
-        */
-       abstract protected function mysqlConnect( $realServer );
-
-       /**
-        * Set the character set of the MySQL link
-        *
-        * @param string $charset
-        * @return bool
-        */
-       abstract protected function mysqlSetCharset( $charset );
-
-       /**
-        * @param ResultWrapper|resource $res
-        * @throws DBUnexpectedError
-        */
-       function freeResult( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               MediaWiki\suppressWarnings();
-               $ok = $this->mysqlFreeResult( $res );
-               MediaWiki\restoreWarnings();
-               if ( !$ok ) {
-                       throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
-               }
-       }
-
-       /**
-        * Free result memory
-        *
-        * @param resource $res Raw result
-        * @return bool
-        */
-       abstract protected function mysqlFreeResult( $res );
-
-       /**
-        * @param ResultWrapper|resource $res
-        * @return stdClass|bool
-        * @throws DBUnexpectedError
-        */
-       function fetchObject( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               MediaWiki\suppressWarnings();
-               $row = $this->mysqlFetchObject( $res );
-               MediaWiki\restoreWarnings();
-
-               $errno = $this->lastErrno();
-               // Unfortunately, mysql_fetch_object does not reset the last errno.
-               // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
-               // these are the only errors mysql_fetch_object can cause.
-               // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
-               if ( $errno == 2000 || $errno == 2013 ) {
-                       throw new DBUnexpectedError(
-                               $this,
-                               'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() )
-                       );
-               }
-
-               return $row;
-       }
-
-       /**
-        * Fetch a result row as an object
-        *
-        * @param resource $res Raw result
-        * @return stdClass
-        */
-       abstract protected function mysqlFetchObject( $res );
-
-       /**
-        * @param ResultWrapper|resource $res
-        * @return array|bool
-        * @throws DBUnexpectedError
-        */
-       function fetchRow( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               MediaWiki\suppressWarnings();
-               $row = $this->mysqlFetchArray( $res );
-               MediaWiki\restoreWarnings();
-
-               $errno = $this->lastErrno();
-               // Unfortunately, mysql_fetch_array does not reset the last errno.
-               // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
-               // these are the only errors mysql_fetch_array can cause.
-               // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
-               if ( $errno == 2000 || $errno == 2013 ) {
-                       throw new DBUnexpectedError(
-                               $this,
-                               'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() )
-                       );
-               }
-
-               return $row;
-       }
-
-       /**
-        * Fetch a result row as an associative and numeric array
-        *
-        * @param resource $res Raw result
-        * @return array
-        */
-       abstract protected function mysqlFetchArray( $res );
-
-       /**
-        * @throws DBUnexpectedError
-        * @param ResultWrapper|resource $res
-        * @return int
-        */
-       function numRows( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               MediaWiki\suppressWarnings();
-               $n = $this->mysqlNumRows( $res );
-               MediaWiki\restoreWarnings();
-
-               // Unfortunately, mysql_num_rows does not reset the last errno.
-               // We are not checking for any errors here, since
-               // these are no errors mysql_num_rows can cause.
-               // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
-               // See https://phabricator.wikimedia.org/T44430
-               return $n;
-       }
-
-       /**
-        * Get number of rows in result
-        *
-        * @param resource $res Raw result
-        * @return int
-        */
-       abstract protected function mysqlNumRows( $res );
-
-       /**
-        * @param ResultWrapper|resource $res
-        * @return int
-        */
-       function numFields( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-
-               return $this->mysqlNumFields( $res );
-       }
-
-       /**
-        * Get number of fields in result
-        *
-        * @param resource $res Raw result
-        * @return int
-        */
-       abstract protected function mysqlNumFields( $res );
-
-       /**
-        * @param ResultWrapper|resource $res
-        * @param int $n
-        * @return string
-        */
-       function fieldName( $res, $n ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-
-               return $this->mysqlFieldName( $res, $n );
-       }
-
-       /**
-        * Get the name of the specified field in a result
-        *
-        * @param ResultWrapper|resource $res
-        * @param int $n
-        * @return string
-        */
-       abstract protected function mysqlFieldName( $res, $n );
-
-       /**
-        * mysql_field_type() wrapper
-        * @param ResultWrapper|resource $res
-        * @param int $n
-        * @return string
-        */
-       public function fieldType( $res, $n ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-
-               return $this->mysqlFieldType( $res, $n );
-       }
-
-       /**
-        * Get the type of the specified field in a result
-        *
-        * @param ResultWrapper|resource $res
-        * @param int $n
-        * @return string
-        */
-       abstract protected function mysqlFieldType( $res, $n );
-
-       /**
-        * @param ResultWrapper|resource $res
-        * @param int $row
-        * @return bool
-        */
-       function dataSeek( $res, $row ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-
-               return $this->mysqlDataSeek( $res, $row );
-       }
-
-       /**
-        * Move internal result pointer
-        *
-        * @param ResultWrapper|resource $res
-        * @param int $row
-        * @return bool
-        */
-       abstract protected function mysqlDataSeek( $res, $row );
-
-       /**
-        * @return string
-        */
-       function lastError() {
-               if ( $this->mConn ) {
-                       # Even if it's non-zero, it can still be invalid
-                       MediaWiki\suppressWarnings();
-                       $error = $this->mysqlError( $this->mConn );
-                       if ( !$error ) {
-                               $error = $this->mysqlError();
-                       }
-                       MediaWiki\restoreWarnings();
-               } else {
-                       $error = $this->mysqlError();
-               }
-               if ( $error ) {
-                       $error .= ' (' . $this->mServer . ')';
-               }
-
-               return $error;
-       }
-
-       /**
-        * Returns the text of the error message from previous MySQL operation
-        *
-        * @param resource $conn Raw connection
-        * @return string
-        */
-       abstract protected function mysqlError( $conn = null );
-
-       /**
-        * @param string $table
-        * @param array $uniqueIndexes
-        * @param array $rows
-        * @param string $fname
-        * @return ResultWrapper
-        */
-       function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
-               return $this->nativeReplace( $table, $rows, $fname );
-       }
-
-       /**
-        * Estimate rows in dataset
-        * Returns estimated count, based on EXPLAIN output
-        * Takes same arguments as Database::select()
-        *
-        * @param string|array $table
-        * @param string|array $vars
-        * @param string|array $conds
-        * @param string $fname
-        * @param string|array $options
-        * @return bool|int
-        */
-       public function estimateRowCount( $table, $vars = '*', $conds = '',
-               $fname = __METHOD__, $options = []
-       ) {
-               $options['EXPLAIN'] = true;
-               $res = $this->select( $table, $vars, $conds, $fname, $options );
-               if ( $res === false ) {
-                       return false;
-               }
-               if ( !$this->numRows( $res ) ) {
-                       return 0;
-               }
-
-               $rows = 1;
-               foreach ( $res as $plan ) {
-                       $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
-               }
-
-               return (int)$rows;
-       }
-
-       /**
-        * @param string $table
-        * @param string $field
-        * @return bool|MySQLField
-        */
-       function fieldInfo( $table, $field ) {
-               $table = $this->tableName( $table );
-               $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
-               if ( !$res ) {
-                       return false;
-               }
-               $n = $this->mysqlNumFields( $res->result );
-               for ( $i = 0; $i < $n; $i++ ) {
-                       $meta = $this->mysqlFetchField( $res->result, $i );
-                       if ( $field == $meta->name ) {
-                               return new MySQLField( $meta );
-                       }
-               }
-
-               return false;
-       }
-
-       /**
-        * Get column information from a result
-        *
-        * @param resource $res Raw result
-        * @param int $n
-        * @return stdClass
-        */
-       abstract protected function mysqlFetchField( $res, $n );
-
-       /**
-        * Get information about an index into an object
-        * Returns false if the index does not exist
-        *
-        * @param string $table
-        * @param string $index
-        * @param string $fname
-        * @return bool|array|null False or null on failure
-        */
-       function indexInfo( $table, $index, $fname = __METHOD__ ) {
-               # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
-               # SHOW INDEX should work for 3.x and up:
-               # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
-               $table = $this->tableName( $table );
-               $index = $this->indexName( $index );
-
-               $sql = 'SHOW INDEX FROM ' . $table;
-               $res = $this->query( $sql, $fname );
-
-               if ( !$res ) {
-                       return null;
-               }
-
-               $result = [];
-
-               foreach ( $res as $row ) {
-                       if ( $row->Key_name == $index ) {
-                               $result[] = $row;
-                       }
-               }
-
-               return empty( $result ) ? false : $result;
-       }
-
-       /**
-        * @param string $s
-        * @return string
-        */
-       function strencode( $s ) {
-               return $this->mysqlRealEscapeString( $s );
-       }
-
-       /**
-        * @param string $s
-        * @return mixed
-        */
-       abstract protected function mysqlRealEscapeString( $s );
-
-       /**
-        * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
-        *
-        * @param string $s
-        * @return string
-        */
-       public function addIdentifierQuotes( $s ) {
-               // Characters in the range \u0001-\uFFFF are valid in a quoted identifier
-               // Remove NUL bytes and escape backticks by doubling
-               return '`' . str_replace( [ "\0", '`' ], [ '', '``' ], $s ) . '`';
-       }
-
-       /**
-        * @param string $name
-        * @return bool
-        */
-       public function isQuotedIdentifier( $name ) {
-               return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
-       }
-
-       function getLag() {
-               if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
-                       return $this->getLagFromPtHeartbeat();
-               } else {
-                       return $this->getLagFromSlaveStatus();
-               }
-       }
-
-       /**
-        * @return string
-        */
-       protected function getLagDetectionMethod() {
-               return $this->lagDetectionMethod;
-       }
-
-       /**
-        * @return bool|int
-        */
-       protected function getLagFromSlaveStatus() {
-               $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
-               $row = $res ? $res->fetchObject() : false;
-               if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
-                       return intval( $row->Seconds_Behind_Master );
-               }
-
-               return false;
-       }
-
-       /**
-        * @return bool|float
-        */
-       protected function getLagFromPtHeartbeat() {
-               $options = $this->lagDetectionOptions;
-
-               if ( isset( $options['conds'] ) ) {
-                       // Best method for multi-DC setups: use logical channel names
-                       $data = $this->getHeartbeatData( $options['conds'] );
-               } else {
-                       // Standard method: use master server ID (works with stock pt-heartbeat)
-                       $masterInfo = $this->getMasterServerInfo();
-                       if ( !$masterInfo ) {
-                               wfLogDBError(
-                                       "Unable to query master of {db_server} for server ID",
-                                       $this->getLogContext( [
-                                               'method' => __METHOD__
-                                       ] )
-                               );
-
-                               return false; // could not get master server ID
-                       }
-
-                       $conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
-                       $data = $this->getHeartbeatData( $conds );
-               }
-
-               list( $time, $nowUnix ) = $data;
-               if ( $time !== null ) {
-                       // @time is in ISO format like "2015-09-25T16:48:10.000510"
-                       $dateTime = new DateTime( $time, new DateTimeZone( 'UTC' ) );
-                       $timeUnix = (int)$dateTime->format( 'U' ) + $dateTime->format( 'u' ) / 1e6;
-
-                       return max( $nowUnix - $timeUnix, 0.0 );
-               }
-
-               wfLogDBError(
-                       "Unable to find pt-heartbeat row for {db_server}",
-                       $this->getLogContext( [
-                               'method' => __METHOD__
-                       ] )
-               );
-
-               return false;
-       }
-
-       protected function getMasterServerInfo() {
-               $cache = $this->srvCache;
-               $key = $cache->makeGlobalKey(
-                       'mysql',
-                       'master-info',
-                       // Using one key for all cluster replica DBs is preferable
-                       $this->getLBInfo( 'clusterMasterHost' ) ?: $this->getServer()
-               );
-
-               return $cache->getWithSetCallback(
-                       $key,
-                       $cache::TTL_INDEFINITE,
-                       function () use ( $cache, $key ) {
-                               // Get and leave a lock key in place for a short period
-                               if ( !$cache->lock( $key, 0, 10 ) ) {
-                                       return false; // avoid master connection spike slams
-                               }
-
-                               $conn = $this->getLazyMasterHandle();
-                               if ( !$conn ) {
-                                       return false; // something is misconfigured
-                               }
-
-                               // Connect to and query the master; catch errors to avoid outages
-                               try {
-                                       $res = $conn->query( 'SELECT @@server_id AS id', __METHOD__ );
-                                       $row = $res ? $res->fetchObject() : false;
-                                       $id = $row ? (int)$row->id : 0;
-                               } catch ( DBError $e ) {
-                                       $id = 0;
-                               }
-
-                               // Cache the ID if it was retrieved
-                               return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false;
-                       }
-               );
-       }
-
-       /**
-        * @param array $conds WHERE clause conditions to find a row
-        * @return array (heartbeat `ts` column value or null, UNIX timestamp) for the newest beat
-        * @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html
-        */
-       protected function getHeartbeatData( array $conds ) {
-               $whereSQL = $this->makeList( $conds, LIST_AND );
-               // Use ORDER BY for channel based queries since that field might not be UNIQUE.
-               // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
-               // percision field is not supported in MySQL <= 5.5.
-               $res = $this->query(
-                       "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1"
-               );
-               $row = $res ? $res->fetchObject() : false;
-
-               return [ $row ? $row->ts : null, microtime( true ) ];
-       }
-
-       public function getApproximateLagStatus() {
-               if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
-                       // Disable caching since this is fast enough and we don't wan't
-                       // to be *too* pessimistic by having both the cache TTL and the
-                       // pt-heartbeat interval count as lag in getSessionLagStatus()
-                       return parent::getApproximateLagStatus();
-               }
-
-               $key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServer() );
-               $approxLag = $this->srvCache->get( $key );
-               if ( !$approxLag ) {
-                       $approxLag = parent::getApproximateLagStatus();
-                       $this->srvCache->set( $key, $approxLag, 1 );
-               }
-
-               return $approxLag;
-       }
-
-       function masterPosWait( DBMasterPos $pos, $timeout ) {
-               if ( !( $pos instanceof MySQLMasterPos ) ) {
-                       throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" );
-               }
-
-               if ( $this->getLBInfo( 'is static' ) === true ) {
-                       return 0; // this is a copy of a read-only dataset with no master DB
-               } elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) {
-                       return 0; // already reached this point for sure
-               }
-
-               // Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
-               if ( $this->useGTIDs && $pos->gtids ) {
-                       // Wait on the GTID set (MariaDB only)
-                       $gtidArg = $this->addQuotes( implode( ',', $pos->gtids ) );
-                       $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
-               } else {
-                       // Wait on the binlog coordinates
-                       $encFile = $this->addQuotes( $pos->file );
-                       $encPos = intval( $pos->pos );
-                       $res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
-               }
-
-               $row = $res ? $this->fetchRow( $res ) : false;
-               if ( !$row ) {
-                       throw new DBExpectedError( $this, "Failed to query MASTER_POS_WAIT()" );
-               }
-
-               // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
-               $status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
-               if ( $status === null ) {
-                       // T126436: jobs programmed to wait on master positions might be referencing binlogs
-                       // 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();
-                       if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) {
-                               $this->lastKnownReplicaPos = $replicationPos;
-                               $status = 0;
-                       }
-               } elseif ( $status >= 0 ) {
-                       // Remember that this position was reached to save queries next time
-                       $this->lastKnownReplicaPos = $pos;
-               }
-
-               return $status;
-       }
-
-       /**
-        * Get the position of the master from SHOW SLAVE STATUS
-        *
-        * @return MySQLMasterPos|bool
-        */
-       function getSlavePos() {
-               $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
-               $row = $this->fetchObject( $res );
-
-               if ( $row ) {
-                       $pos = isset( $row->Exec_master_log_pos )
-                               ? $row->Exec_master_log_pos
-                               : $row->Exec_Master_Log_Pos;
-                       // Also fetch the last-applied GTID set (MariaDB)
-                       if ( $this->useGTIDs ) {
-                               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_slave_pos'", __METHOD__ );
-                               $gtidRow = $this->fetchObject( $res );
-                               $gtidSet = $gtidRow ? $gtidRow->Value : '';
-                       } else {
-                               $gtidSet = '';
-                       }
-
-                       return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos, $gtidSet );
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Get the position of the master from SHOW MASTER STATUS
-        *
-        * @return MySQLMasterPos|bool
-        */
-       function getMasterPos() {
-               $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
-               $row = $this->fetchObject( $res );
-
-               if ( $row ) {
-                       // Also fetch the last-written GTID set (MariaDB)
-                       if ( $this->useGTIDs ) {
-                               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_binlog_pos'", __METHOD__ );
-                               $gtidRow = $this->fetchObject( $res );
-                               $gtidSet = $gtidRow ? $gtidRow->Value : '';
-                       } else {
-                               $gtidSet = '';
-                       }
-
-                       return new MySQLMasterPos( $row->File, $row->Position, $gtidSet );
-               } else {
-                       return false;
-               }
-       }
-
-       public function serverIsReadOnly() {
-               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__ );
-               $row = $this->fetchObject( $res );
-
-               return $row ? ( strtolower( $row->Value ) === 'on' ) : false;
-       }
-
-       /**
-        * @param string $index
-        * @return string
-        */
-       function useIndexClause( $index ) {
-               return "FORCE INDEX (" . $this->indexName( $index ) . ")";
-       }
-
-       /**
-        * @param string $index
-        * @return string
-        */
-       function ignoreIndexClause( $index ) {
-               return "IGNORE INDEX (" . $this->indexName( $index ) . ")";
-       }
-
-       /**
-        * @return string
-        */
-       function lowPriorityOption() {
-               return 'LOW_PRIORITY';
-       }
-
-       /**
-        * @return string
-        */
-       public function getSoftwareLink() {
-               // MariaDB includes its name in its version string; this is how MariaDB's version of
-               // the mysql command-line client identifies MariaDB servers (see mariadb_connection()
-               // in libmysql/libmysql.c).
-               $version = $this->getServerVersion();
-               if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) {
-                       return '[{{int:version-db-mariadb-url}} MariaDB]';
-               }
-
-               // Percona Server's version suffix is not very distinctive, and @@version_comment
-               // doesn't give the necessary info for source builds, so assume the server is MySQL.
-               // (Even Percona's version of mysql doesn't try to make the distinction.)
-               return '[{{int:version-db-mysql-url}} MySQL]';
-       }
-
-       /**
-        * @return string
-        */
-       public function getServerVersion() {
-               // Not using mysql_get_server_info() or similar for consistency: in the handshake,
-               // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
-               // it off (see RPL_VERSION_HACK in include/mysql_com.h).
-               if ( $this->serverVersion === null ) {
-                       $this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ );
-               }
-               return $this->serverVersion;
-       }
-
-       /**
-        * @param array $options
-        */
-       public function setSessionOptions( array $options ) {
-               if ( isset( $options['connTimeout'] ) ) {
-                       $timeout = (int)$options['connTimeout'];
-                       $this->query( "SET net_read_timeout=$timeout" );
-                       $this->query( "SET net_write_timeout=$timeout" );
-               }
-       }
-
-       /**
-        * @param string $sql
-        * @param string $newLine
-        * @return bool
-        */
-       public function streamStatementEnd( &$sql, &$newLine ) {
-               if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
-                       preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
-                       $this->delimiter = $m[1];
-                       $newLine = '';
-               }
-
-               return parent::streamStatementEnd( $sql, $newLine );
-       }
-
-       /**
-        * Check to see if a named lock is available. This is non-blocking.
-        *
-        * @param string $lockName Name of lock to poll
-        * @param string $method Name of method calling us
-        * @return bool
-        * @since 1.20
-        */
-       public function lockIsFree( $lockName, $method ) {
-               $lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
-               $result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method );
-               $row = $this->fetchObject( $result );
-
-               return ( $row->lockstatus == 1 );
-       }
-
-       /**
-        * @param string $lockName
-        * @param string $method
-        * @param int $timeout
-        * @return bool
-        */
-       public function lock( $lockName, $method, $timeout = 5 ) {
-               $lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
-               $result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method );
-               $row = $this->fetchObject( $result );
-
-               if ( $row->lockstatus == 1 ) {
-                       parent::lock( $lockName, $method, $timeout ); // record
-                       return true;
-               }
-
-               wfDebug( __METHOD__ . " failed to acquire lock\n" );
-
-               return false;
-       }
-
-       /**
-        * FROM MYSQL DOCS:
-        * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
-        * @param string $lockName
-        * @param string $method
-        * @return bool
-        */
-       public function unlock( $lockName, $method ) {
-               $lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
-               $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method );
-               $row = $this->fetchObject( $result );
-
-               if ( $row->lockstatus == 1 ) {
-                       parent::unlock( $lockName, $method ); // record
-                       return true;
-               }
-
-               wfDebug( __METHOD__ . " failed to release lock\n" );
-
-               return false;
-       }
-
-       private function makeLockName( $lockName ) {
-               // http://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
-               // Newer version enforce a 64 char length limit.
-               return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
-       }
-
-       public function namedLocksEnqueue() {
-               return true;
-       }
-
-       /**
-        * @param array $read
-        * @param array $write
-        * @param string $method
-        * @param bool $lowPriority
-        * @return bool
-        */
-       public function lockTables( $read, $write, $method, $lowPriority = true ) {
-               $items = [];
-
-               foreach ( $write as $table ) {
-                       $tbl = $this->tableName( $table ) .
-                               ( $lowPriority ? ' LOW_PRIORITY' : '' ) .
-                               ' WRITE';
-                       $items[] = $tbl;
-               }
-               foreach ( $read as $table ) {
-                       $items[] = $this->tableName( $table ) . ' READ';
-               }
-               $sql = "LOCK TABLES " . implode( ',', $items );
-               $this->query( $sql, $method );
-
-               return true;
-       }
-
-       /**
-        * @param string $method
-        * @return bool
-        */
-       public function unlockTables( $method ) {
-               $this->query( "UNLOCK TABLES", $method );
-
-               return true;
-       }
-
-       /**
-        * Get search engine class. All subclasses of this
-        * need to implement this if they wish to use searching.
-        *
-        * @return string
-        */
-       public function getSearchEngine() {
-               return 'SearchMySQL';
-       }
-
-       /**
-        * @param bool $value
-        */
-       public function setBigSelects( $value = true ) {
-               if ( $value === 'default' ) {
-                       if ( $this->mDefaultBigSelects === null ) {
-                               # Function hasn't been called before so it must already be set to the default
-                               return;
-                       } else {
-                               $value = $this->mDefaultBigSelects;
-                       }
-               } elseif ( $this->mDefaultBigSelects === null ) {
-                       $this->mDefaultBigSelects =
-                               (bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
-               }
-               $encValue = $value ? '1' : '0';
-               $this->query( "SET sql_big_selects=$encValue", __METHOD__ );
-       }
-
-       /**
-        * DELETE where the condition is a join. MySql uses multi-table deletes.
-        * @param string $delTable
-        * @param string $joinTable
-        * @param string $delVar
-        * @param string $joinVar
-        * @param array|string $conds
-        * @param bool|string $fname
-        * @throws DBUnexpectedError
-        * @return bool|ResultWrapper
-        */
-       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ ) {
-               if ( !$conds ) {
-                       throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
-               }
-
-               $delTable = $this->tableName( $delTable );
-               $joinTable = $this->tableName( $joinTable );
-               $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
-
-               if ( $conds != '*' ) {
-                       $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
-               }
-
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * @param string $table
-        * @param array $rows
-        * @param array $uniqueIndexes
-        * @param array $set
-        * @param string $fname
-        * @return bool
-        */
-       public function upsert( $table, array $rows, array $uniqueIndexes,
-               array $set, $fname = __METHOD__
-       ) {
-               if ( !count( $rows ) ) {
-                       return true; // nothing to do
-               }
-
-               if ( !is_array( reset( $rows ) ) ) {
-                       $rows = [ $rows ];
-               }
-
-               $table = $this->tableName( $table );
-               $columns = array_keys( $rows[0] );
-
-               $sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES ';
-               $rowTuples = [];
-               foreach ( $rows as $row ) {
-                       $rowTuples[] = '(' . $this->makeList( $row ) . ')';
-               }
-               $sql .= implode( ',', $rowTuples );
-               $sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, LIST_SET );
-
-               return (bool)$this->query( $sql, $fname );
-       }
-
-       /**
-        * Determines how long the server has been up
-        *
-        * @return int
-        */
-       function getServerUptime() {
-               $vars = $this->getMysqlStatus( 'Uptime' );
-
-               return (int)$vars['Uptime'];
-       }
-
-       /**
-        * Determines if the last failure was due to a deadlock
-        *
-        * @return bool
-        */
-       function wasDeadlock() {
-               return $this->lastErrno() == 1213;
-       }
-
-       /**
-        * Determines if the last failure was due to a lock timeout
-        *
-        * @return bool
-        */
-       function wasLockTimeout() {
-               return $this->lastErrno() == 1205;
-       }
-
-       function wasErrorReissuable() {
-               return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
-       }
-
-       /**
-        * Determines if the last failure was due to the database being read-only.
-        *
-        * @return bool
-        */
-       function wasReadOnlyError() {
-               return $this->lastErrno() == 1223 ||
-                       ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
-       }
-
-       function wasConnectionError( $errno ) {
-               return $errno == 2013 || $errno == 2006;
-       }
-
-       /**
-        * Get the underlying binding handle, mConn
-        *
-        * Makes sure that mConn is set (disconnects and ping() failure can unset it).
-        * This catches broken callers than catch and ignore disconnection exceptions.
-        * Unlike checking isOpen(), this is safe to call inside of open().
-        *
-        * @return resource|object
-        * @throws DBUnexpectedError
-        * @since 1.26
-        */
-       protected function getBindingHandle() {
-               if ( !$this->mConn ) {
-                       throw new DBUnexpectedError(
-                               $this,
-                               'DB connection was already closed or the connection dropped.'
-                       );
-               }
-
-               return $this->mConn;
-       }
-
-       /**
-        * @param string $oldName
-        * @param string $newName
-        * @param bool $temporary
-        * @param string $fname
-        * @return bool
-        */
-       function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
-               $tmp = $temporary ? 'TEMPORARY ' : '';
-               $newName = $this->addIdentifierQuotes( $newName );
-               $oldName = $this->addIdentifierQuotes( $oldName );
-               $query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
-
-               return $this->query( $query, $fname );
-       }
-
-       /**
-        * List all tables on the database
-        *
-        * @param string $prefix Only show tables with this prefix, e.g. mw_
-        * @param string $fname Calling function name
-        * @return array
-        */
-       function listTables( $prefix = null, $fname = __METHOD__ ) {
-               $result = $this->query( "SHOW TABLES", $fname );
-
-               $endArray = [];
-
-               foreach ( $result as $table ) {
-                       $vars = get_object_vars( $table );
-                       $table = array_pop( $vars );
-
-                       if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
-                               $endArray[] = $table;
-                       }
-               }
-
-               return $endArray;
-       }
-
-       /**
-        * @param string $tableName
-        * @param string $fName
-        * @return bool|ResultWrapper
-        */
-       public function dropTable( $tableName, $fName = __METHOD__ ) {
-               if ( !$this->tableExists( $tableName, $fName ) ) {
-                       return false;
-               }
-
-               return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
-       }
-
-       /**
-        * @return array
-        */
-       protected function getDefaultSchemaVars() {
-               $vars = parent::getDefaultSchemaVars();
-               $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
-               $vars['wgDBTableOptions'] = str_replace(
-                       'CHARSET=mysql4',
-                       'CHARSET=binary',
-                       $vars['wgDBTableOptions']
-               );
-
-               return $vars;
-       }
-
-       /**
-        * Get status information from SHOW STATUS in an associative array
-        *
-        * @param string $which
-        * @return array
-        */
-       function getMysqlStatus( $which = "%" ) {
-               $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
-               $status = [];
-
-               foreach ( $res as $row ) {
-                       $status[$row->Variable_name] = $row->Value;
-               }
-
-               return $status;
-       }
-
-       /**
-        * Lists VIEWs in the database
-        *
-        * @param string $prefix Only show VIEWs with this prefix, eg.
-        * unit_test_, or $wgDBprefix. Default: null, would return all views.
-        * @param string $fname Name of calling function
-        * @return array
-        * @since 1.22
-        */
-       public function listViews( $prefix = null, $fname = __METHOD__ ) {
-
-               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] );
-                       }
-               }
-
-               if ( is_null( $prefix ) || $prefix === '' ) {
-                       return $this->allViews;
-               }
-
-               $filteredViews = [];
-               foreach ( $this->allViews as $viewName ) {
-                       // Does the name of this VIEW start with the table-prefix?
-                       if ( strpos( $viewName, $prefix ) === 0 ) {
-                               array_push( $filteredViews, $viewName );
-                       }
-               }
-
-               return $filteredViews;
-       }
-
-       /**
-        * Differentiates between a TABLE and a VIEW.
-        *
-        * @param string $name Name of the TABLE/VIEW to test
-        * @param string $prefix
-        * @return bool
-        * @since 1.22
-        */
-       public function isView( $name, $prefix = null ) {
-               return in_array( $name, $this->listViews( $prefix ) );
-       }
-}
-
diff --git a/includes/db/DatabaseMysqli.php b/includes/db/DatabaseMysqli.php
deleted file mode 100644 (file)
index e468601..0000000
+++ /dev/null
@@ -1,334 +0,0 @@
-<?php
-/**
- * This is the MySQLi database abstraction layer.
- *
- * 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
- */
-
-/**
- * Database abstraction object for PHP extension mysqli.
- *
- * @ingroup Database
- * @since 1.22
- * @see Database
- */
-class DatabaseMysqli extends DatabaseMysqlBase {
-       /** @var mysqli */
-       protected $mConn;
-
-       /**
-        * @param string $sql
-        * @return resource
-        */
-       protected function doQuery( $sql ) {
-               $conn = $this->getBindingHandle();
-
-               if ( $this->bufferResults() ) {
-                       $ret = $conn->query( $sql );
-               } else {
-                       $ret = $conn->query( $sql, MYSQLI_USE_RESULT );
-               }
-
-               return $ret;
-       }
-
-       /**
-        * @param string $realServer
-        * @return bool|mysqli
-        * @throws DBConnectionError
-        */
-       protected function mysqlConnect( $realServer ) {
-               global $wgDBmysql5;
-
-               # Avoid suppressed fatal error, which is very hard to track down
-               if ( !function_exists( 'mysqli_init' ) ) {
-                       throw new DBConnectionError( $this, "MySQLi functions missing,"
-                               . " have you compiled PHP with the --with-mysqli option?\n" );
-               }
-
-               // Other than mysql_connect, mysqli_real_connect expects an explicit port
-               // and socket parameters. So we need to parse the port and socket out of
-               // $realServer
-               $port = null;
-               $socket = null;
-               $hostAndPort = IP::splitHostAndPort( $realServer );
-               if ( $hostAndPort ) {
-                       $realServer = $hostAndPort[0];
-                       if ( $hostAndPort[1] ) {
-                               $port = $hostAndPort[1];
-                       }
-               } elseif ( substr_count( $realServer, ':' ) == 1 ) {
-                       // If we have a colon and something that's not a port number
-                       // inside the hostname, assume it's the socket location
-                       $hostAndSocket = explode( ':', $realServer );
-                       $realServer = $hostAndSocket[0];
-                       $socket = $hostAndSocket[1];
-               }
-
-               $mysqli = mysqli_init();
-
-               $connFlags = 0;
-               if ( $this->mFlags & DBO_SSL ) {
-                       $connFlags |= MYSQLI_CLIENT_SSL;
-                       $mysqli->ssl_set(
-                               $this->sslKeyPath,
-                               $this->sslCertPath,
-                               null,
-                               $this->sslCAPath,
-                               $this->sslCiphers
-                       );
-               }
-               if ( $this->mFlags & DBO_COMPRESS ) {
-                       $connFlags |= MYSQLI_CLIENT_COMPRESS;
-               }
-               if ( $this->mFlags & DBO_PERSISTENT ) {
-                       $realServer = 'p:' . $realServer;
-               }
-
-               if ( $wgDBmysql5 ) {
-                       // Tell the server we're communicating with it in UTF-8.
-                       // This may engage various charset conversions.
-                       $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'utf8' );
-               } else {
-                       $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'binary' );
-               }
-               $mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, 3 );
-
-               if ( $mysqli->real_connect( $realServer, $this->mUser,
-                       $this->mPassword, $this->mDBname, $port, $socket, $connFlags )
-               ) {
-                       return $mysqli;
-               }
-
-               return false;
-       }
-
-       protected function connectInitCharset() {
-               // already done in mysqlConnect()
-               return true;
-       }
-
-       /**
-        * @param string $charset
-        * @return bool
-        */
-       protected function mysqlSetCharset( $charset ) {
-               $conn = $this->getBindingHandle();
-
-               if ( method_exists( $conn, 'set_charset' ) ) {
-                       return $conn->set_charset( $charset );
-               } else {
-                       return $this->query( 'SET NAMES ' . $charset, __METHOD__ );
-               }
-       }
-
-       /**
-        * @return bool
-        */
-       protected function closeConnection() {
-               $conn = $this->getBindingHandle();
-
-               return $conn->close();
-       }
-
-       /**
-        * @return int
-        */
-       function insertId() {
-               $conn = $this->getBindingHandle();
-
-               return (int)$conn->insert_id;
-       }
-
-       /**
-        * @return int
-        */
-       function lastErrno() {
-               if ( $this->mConn ) {
-                       return $this->mConn->errno;
-               } else {
-                       return mysqli_connect_errno();
-               }
-       }
-
-       /**
-        * @return int
-        */
-       function affectedRows() {
-               $conn = $this->getBindingHandle();
-
-               return $conn->affected_rows;
-       }
-
-       /**
-        * @param string $db
-        * @return bool
-        */
-       function selectDB( $db ) {
-               $conn = $this->getBindingHandle();
-
-               $this->mDBname = $db;
-
-               return $conn->select_db( $db );
-       }
-
-       /**
-        * @param mysqli $res
-        * @return bool
-        */
-       protected function mysqlFreeResult( $res ) {
-               $res->free_result();
-
-               return true;
-       }
-
-       /**
-        * @param mysqli $res
-        * @return bool
-        */
-       protected function mysqlFetchObject( $res ) {
-               $object = $res->fetch_object();
-               if ( $object === null ) {
-                       return false;
-               }
-
-               return $object;
-       }
-
-       /**
-        * @param mysqli $res
-        * @return bool
-        */
-       protected function mysqlFetchArray( $res ) {
-               $array = $res->fetch_array();
-               if ( $array === null ) {
-                       return false;
-               }
-
-               return $array;
-       }
-
-       /**
-        * @param mysqli $res
-        * @return mixed
-        */
-       protected function mysqlNumRows( $res ) {
-               return $res->num_rows;
-       }
-
-       /**
-        * @param mysqli $res
-        * @return mixed
-        */
-       protected function mysqlNumFields( $res ) {
-               return $res->field_count;
-       }
-
-       /**
-        * @param mysqli $res
-        * @param int $n
-        * @return mixed
-        */
-       protected function mysqlFetchField( $res, $n ) {
-               $field = $res->fetch_field_direct( $n );
-
-               // Add missing properties to result (using flags property)
-               // which will be part of function mysql-fetch-field for backward compatibility
-               $field->not_null = $field->flags & MYSQLI_NOT_NULL_FLAG;
-               $field->primary_key = $field->flags & MYSQLI_PRI_KEY_FLAG;
-               $field->unique_key = $field->flags & MYSQLI_UNIQUE_KEY_FLAG;
-               $field->multiple_key = $field->flags & MYSQLI_MULTIPLE_KEY_FLAG;
-               $field->binary = $field->flags & MYSQLI_BINARY_FLAG;
-               $field->numeric = $field->flags & MYSQLI_NUM_FLAG;
-               $field->blob = $field->flags & MYSQLI_BLOB_FLAG;
-               $field->unsigned = $field->flags & MYSQLI_UNSIGNED_FLAG;
-               $field->zerofill = $field->flags & MYSQLI_ZEROFILL_FLAG;
-
-               return $field;
-       }
-
-       /**
-        * @param resource|ResultWrapper $res
-        * @param int $n
-        * @return mixed
-        */
-       protected function mysqlFieldName( $res, $n ) {
-               $field = $res->fetch_field_direct( $n );
-
-               return $field->name;
-       }
-
-       /**
-        * @param resource|ResultWrapper $res
-        * @param int $n
-        * @return mixed
-        */
-       protected function mysqlFieldType( $res, $n ) {
-               $field = $res->fetch_field_direct( $n );
-
-               return $field->type;
-       }
-
-       /**
-        * @param resource|ResultWrapper $res
-        * @param int $row
-        * @return mixed
-        */
-       protected function mysqlDataSeek( $res, $row ) {
-               return $res->data_seek( $row );
-       }
-
-       /**
-        * @param mysqli $conn Optional connection object
-        * @return string
-        */
-       protected function mysqlError( $conn = null ) {
-               if ( $conn === null ) {
-                       return mysqli_connect_error();
-               } else {
-                       return $conn->error;
-               }
-       }
-
-       /**
-        * Escapes special characters in a string for use in an SQL statement
-        * @param string $s
-        * @return string
-        */
-       protected function mysqlRealEscapeString( $s ) {
-               $conn = $this->getBindingHandle();
-
-               return $conn->real_escape_string( $s );
-       }
-
-       /**
-        * Give an id for the connection
-        *
-        * mysql driver used resource id, but mysqli objects cannot be cast to string.
-        * @return string
-        */
-       public function __toString() {
-               if ( $this->mConn instanceof mysqli ) {
-                       return (string)$this->mConn->thread_id;
-               } else {
-                       // mConn might be false or something.
-                       return (string)$this->mConn;
-               }
-       }
-}
index df311aa..561dadb 100644 (file)
@@ -131,7 +131,7 @@ class ORAResult {
 /**
  * @ingroup Database
  */
-class DatabaseOracle extends Database {
+class DatabaseOracle extends DatabaseBase {
        /** @var resource */
        protected $mLastResult = null;
 
@@ -176,22 +176,6 @@ class DatabaseOracle extends Database {
                return 'oracle';
        }
 
-       function cascadingDeletes() {
-               return true;
-       }
-
-       function cleanupTriggers() {
-               return true;
-       }
-
-       function strictIPs() {
-               return true;
-       }
-
-       function realTimestamps() {
-               return true;
-       }
-
        function implicitGroupby() {
                return false;
        }
@@ -200,10 +184,6 @@ class DatabaseOracle extends Database {
                return false;
        }
 
-       function searchableIPs() {
-               return true;
-       }
-
        /**
         * Usually aborts on failure
         * @param string $server
@@ -1517,10 +1497,6 @@ class DatabaseOracle extends Database {
                return 'CAST ( ' . $field . ' AS VARCHAR2 )';
        }
 
-       public function getSearchEngine() {
-               return 'SearchOracle';
-       }
-
        public function getInfinity() {
                return '31-12-2030 12:00:00.000000';
        }
diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php
deleted file mode 100644 (file)
index 590e1f4..0000000
+++ /dev/null
@@ -1,1638 +0,0 @@
-<?php
-/**
- * This is the Postgres database abstraction layer.
- *
- * 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
- */
-
-class PostgresField implements Field {
-       private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname,
-               $has_default, $default;
-
-       /**
-        * @param IDatabase $db
-        * @param string $table
-        * @param string $field
-        * @return null|PostgresField
-        */
-       static function fromText( $db, $table, $field ) {
-               $q = <<<SQL
-SELECT
- attnotnull, attlen, conname AS conname,
- atthasdef,
- adsrc,
- COALESCE(condeferred, 'f') AS deferred,
- COALESCE(condeferrable, 'f') AS deferrable,
- CASE WHEN typname = 'int2' THEN 'smallint'
-  WHEN typname = 'int4' THEN 'integer'
-  WHEN typname = 'int8' THEN 'bigint'
-  WHEN typname = 'bpchar' THEN 'char'
- ELSE typname END AS typname
-FROM pg_class c
-JOIN pg_namespace n ON (n.oid = c.relnamespace)
-JOIN pg_attribute a ON (a.attrelid = c.oid)
-JOIN pg_type t ON (t.oid = a.atttypid)
-LEFT JOIN pg_constraint o ON (o.conrelid = c.oid AND a.attnum = ANY(o.conkey) AND o.contype = 'f')
-LEFT JOIN pg_attrdef d on c.oid=d.adrelid and a.attnum=d.adnum
-WHERE relkind = 'r'
-AND nspname=%s
-AND relname=%s
-AND attname=%s;
-SQL;
-
-               $table = $db->tableName( $table, 'raw' );
-               $res = $db->query(
-                       sprintf( $q,
-                               $db->addQuotes( $db->getCoreSchema() ),
-                               $db->addQuotes( $table ),
-                               $db->addQuotes( $field )
-                       )
-               );
-               $row = $db->fetchObject( $res );
-               if ( !$row ) {
-                       return null;
-               }
-               $n = new PostgresField;
-               $n->type = $row->typname;
-               $n->nullable = ( $row->attnotnull == 'f' );
-               $n->name = $field;
-               $n->tablename = $table;
-               $n->max_length = $row->attlen;
-               $n->deferrable = ( $row->deferrable == 't' );
-               $n->deferred = ( $row->deferred == 't' );
-               $n->conname = $row->conname;
-               $n->has_default = ( $row->atthasdef === 't' );
-               $n->default = $row->adsrc;
-
-               return $n;
-       }
-
-       function name() {
-               return $this->name;
-       }
-
-       function tableName() {
-               return $this->tablename;
-       }
-
-       function type() {
-               return $this->type;
-       }
-
-       function isNullable() {
-               return $this->nullable;
-       }
-
-       function maxLength() {
-               return $this->max_length;
-       }
-
-       function is_deferrable() {
-               return $this->deferrable;
-       }
-
-       function is_deferred() {
-               return $this->deferred;
-       }
-
-       function conname() {
-               return $this->conname;
-       }
-
-       /**
-        * @since 1.19
-        * @return bool|mixed
-        */
-       function defaultValue() {
-               if ( $this->has_default ) {
-                       return $this->default;
-               } else {
-                       return false;
-               }
-       }
-}
-
-/**
- * Manage savepoints within a transaction
- * @ingroup Database
- * @since 1.19
- */
-class SavepointPostgres {
-       /** @var DatabasePostgres Establish a savepoint within a transaction */
-       protected $dbw;
-       protected $id;
-       protected $didbegin;
-
-       /**
-        * @param IDatabase $dbw
-        * @param int $id
-        */
-       public function __construct( $dbw, $id ) {
-               $this->dbw = $dbw;
-               $this->id = $id;
-               $this->didbegin = false;
-               /* If we are not in a transaction, we need to be for savepoint trickery */
-               if ( !$dbw->trxLevel() ) {
-                       $dbw->begin( "FOR SAVEPOINT", DatabasePostgres::TRANSACTION_INTERNAL );
-                       $this->didbegin = true;
-               }
-       }
-
-       public function __destruct() {
-               if ( $this->didbegin ) {
-                       $this->dbw->rollback();
-                       $this->didbegin = false;
-               }
-       }
-
-       public function commit() {
-               if ( $this->didbegin ) {
-                       $this->dbw->commit();
-                       $this->didbegin = false;
-               }
-       }
-
-       protected function query( $keyword, $msg_ok, $msg_failed ) {
-               if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) {
-               } else {
-                       wfDebug( sprintf( $msg_failed, $this->id ) );
-               }
-       }
-
-       public function savepoint() {
-               $this->query( "SAVEPOINT",
-                       "Transaction state: savepoint \"%s\" established.\n",
-                       "Transaction state: establishment of savepoint \"%s\" FAILED.\n"
-               );
-       }
-
-       public function release() {
-               $this->query( "RELEASE",
-                       "Transaction state: savepoint \"%s\" released.\n",
-                       "Transaction state: release of savepoint \"%s\" FAILED.\n"
-               );
-       }
-
-       public function rollback() {
-               $this->query( "ROLLBACK TO",
-                       "Transaction state: savepoint \"%s\" rolled back.\n",
-                       "Transaction state: rollback of savepoint \"%s\" FAILED.\n"
-               );
-       }
-
-       public function __toString() {
-               return (string)$this->id;
-       }
-}
-
-/**
- * @ingroup Database
- */
-class DatabasePostgres extends Database {
-       /** @var resource */
-       protected $mLastResult = null;
-
-       /** @var int The number of rows affected as an integer */
-       protected $mAffectedRows = null;
-
-       /** @var int */
-       private $mInsertId = null;
-
-       /** @var float|string */
-       private $numericVersion = null;
-
-       /** @var string Connect string to open a PostgreSQL connection */
-       private $connectString;
-
-       /** @var string */
-       private $mCoreSchema;
-
-       function getType() {
-               return 'postgres';
-       }
-
-       function cascadingDeletes() {
-               return true;
-       }
-
-       function cleanupTriggers() {
-               return true;
-       }
-
-       function strictIPs() {
-               return true;
-       }
-
-       function realTimestamps() {
-               return true;
-       }
-
-       function implicitGroupby() {
-               return false;
-       }
-
-       function implicitOrderby() {
-               return false;
-       }
-
-       function searchableIPs() {
-               return true;
-       }
-
-       function functionalIndexes() {
-               return true;
-       }
-
-       function hasConstraint( $name ) {
-               $sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
-                       "WHERE c.connamespace = n.oid AND conname = '" .
-                       pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" .
-                       pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'";
-               $res = $this->doQuery( $sql );
-
-               return $this->numRows( $res );
-       }
-
-       /**
-        * Usually aborts on failure
-        * @param string $server
-        * @param string $user
-        * @param string $password
-        * @param string $dbName
-        * @throws DBConnectionError|Exception
-        * @return resource|bool|null
-        */
-       function open( $server, $user, $password, $dbName ) {
-               # Test for Postgres support, to avoid suppressed fatal error
-               if ( !function_exists( 'pg_connect' ) ) {
-                       throw new DBConnectionError(
-                               $this,
-                               "Postgres functions missing, have you compiled PHP with the --with-pgsql\n" .
-                               "option? (Note: if you recently installed PHP, you may need to restart your\n" .
-                               "webserver and database)\n"
-                       );
-               }
-
-               global $wgDBport;
-
-               if ( !strlen( $user ) ) { # e.g. the class is being loaded
-                       return null;
-               }
-
-               $this->mServer = $server;
-               $port = $wgDBport;
-               $this->mUser = $user;
-               $this->mPassword = $password;
-               $this->mDBname = $dbName;
-
-               $connectVars = [
-                       'dbname' => $dbName,
-                       'user' => $user,
-                       'password' => $password
-               ];
-               if ( $server != false && $server != '' ) {
-                       $connectVars['host'] = $server;
-               }
-               if ( $port != false && $port != '' ) {
-                       $connectVars['port'] = $port;
-               }
-               if ( $this->mFlags & DBO_SSL ) {
-                       $connectVars['sslmode'] = 1;
-               }
-
-               $this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW );
-               $this->close();
-               $this->installErrorHandler();
-
-               try {
-                       $this->mConn = pg_connect( $this->connectString );
-               } catch ( Exception $ex ) {
-                       $this->restoreErrorHandler();
-                       throw $ex;
-               }
-
-               $phpError = $this->restoreErrorHandler();
-
-               if ( !$this->mConn ) {
-                       wfDebug( "DB connection error\n" );
-                       wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " .
-                               substr( $password, 0, 3 ) . "...\n" );
-                       wfDebug( $this->lastError() . "\n" );
-                       throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
-               }
-
-               $this->mOpened = true;
-
-               global $wgCommandLineMode;
-               # If called from the command-line (e.g. importDump), only show errors
-               if ( $wgCommandLineMode ) {
-                       $this->doQuery( "SET client_min_messages = 'ERROR'" );
-               }
-
-               $this->query( "SET client_encoding='UTF8'", __METHOD__ );
-               $this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
-               $this->query( "SET timezone = 'GMT'", __METHOD__ );
-               $this->query( "SET standard_conforming_strings = on", __METHOD__ );
-               if ( $this->getServerVersion() >= 9.0 ) {
-                       $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
-               }
-
-               global $wgDBmwschema;
-               $this->determineCoreSchema( $wgDBmwschema );
-
-               return $this->mConn;
-       }
-
-       /**
-        * Postgres doesn't support selectDB in the same way MySQL does. So if the
-        * DB name doesn't match the open connection, open a new one
-        * @param string $db
-        * @return bool
-        */
-       function selectDB( $db ) {
-               if ( $this->mDBname !== $db ) {
-                       return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
-               } else {
-                       return true;
-               }
-       }
-
-       function makeConnectionString( $vars ) {
-               $s = '';
-               foreach ( $vars as $name => $value ) {
-                       $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
-               }
-
-               return $s;
-       }
-
-       /**
-        * Closes a database connection, if it is open
-        * Returns success, true if already closed
-        * @return bool
-        */
-       protected function closeConnection() {
-               return pg_close( $this->mConn );
-       }
-
-       public function doQuery( $sql ) {
-               $sql = mb_convert_encoding( $sql, 'UTF-8' );
-               // Clear previously left over PQresult
-               while ( $res = pg_get_result( $this->mConn ) ) {
-                       pg_free_result( $res );
-               }
-               if ( pg_send_query( $this->mConn, $sql ) === false ) {
-                       throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
-               }
-               $this->mLastResult = pg_get_result( $this->mConn );
-               $this->mAffectedRows = null;
-               if ( pg_result_error( $this->mLastResult ) ) {
-                       return false;
-               }
-
-               return $this->mLastResult;
-       }
-
-       protected function dumpError() {
-               $diags = [
-                       PGSQL_DIAG_SEVERITY,
-                       PGSQL_DIAG_SQLSTATE,
-                       PGSQL_DIAG_MESSAGE_PRIMARY,
-                       PGSQL_DIAG_MESSAGE_DETAIL,
-                       PGSQL_DIAG_MESSAGE_HINT,
-                       PGSQL_DIAG_STATEMENT_POSITION,
-                       PGSQL_DIAG_INTERNAL_POSITION,
-                       PGSQL_DIAG_INTERNAL_QUERY,
-                       PGSQL_DIAG_CONTEXT,
-                       PGSQL_DIAG_SOURCE_FILE,
-                       PGSQL_DIAG_SOURCE_LINE,
-                       PGSQL_DIAG_SOURCE_FUNCTION
-               ];
-               foreach ( $diags as $d ) {
-                       wfDebug( sprintf( "PgSQL ERROR(%d): %s\n",
-                               $d, pg_result_error_field( $this->mLastResult, $d ) ) );
-               }
-       }
-
-       function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
-               if ( $tempIgnore ) {
-                       /* Check for constraint violation */
-                       if ( $errno === '23505' ) {
-                               parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
-
-                               return;
-                       }
-               }
-               /* Transaction stays in the ERROR state until rolled back */
-               if ( $this->mTrxLevel ) {
-                       $ignore = $this->ignoreErrors( true );
-                       $this->rollback( __METHOD__ );
-                       $this->ignoreErrors( $ignore );
-               }
-               parent::reportQueryError( $error, $errno, $sql, $fname, false );
-       }
-
-       function queryIgnore( $sql, $fname = __METHOD__ ) {
-               return $this->query( $sql, $fname, true );
-       }
-
-       /**
-        * @param stdClass|ResultWrapper $res
-        * @throws DBUnexpectedError
-        */
-       function freeResult( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               MediaWiki\suppressWarnings();
-               $ok = pg_free_result( $res );
-               MediaWiki\restoreWarnings();
-               if ( !$ok ) {
-                       throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
-               }
-       }
-
-       /**
-        * @param ResultWrapper|stdClass $res
-        * @return stdClass
-        * @throws DBUnexpectedError
-        */
-       function fetchObject( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               MediaWiki\suppressWarnings();
-               $row = pg_fetch_object( $res );
-               MediaWiki\restoreWarnings();
-               # @todo FIXME: HACK HACK HACK HACK debug
-
-               # @todo hashar: not sure if the following test really trigger if the object
-               #          fetching failed.
-               if ( pg_last_error( $this->mConn ) ) {
-                       throw new DBUnexpectedError(
-                               $this,
-                               'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
-                       );
-               }
-
-               return $row;
-       }
-
-       function fetchRow( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               MediaWiki\suppressWarnings();
-               $row = pg_fetch_array( $res );
-               MediaWiki\restoreWarnings();
-               if ( pg_last_error( $this->mConn ) ) {
-                       throw new DBUnexpectedError(
-                               $this,
-                               'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
-                       );
-               }
-
-               return $row;
-       }
-
-       function numRows( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               MediaWiki\suppressWarnings();
-               $n = pg_num_rows( $res );
-               MediaWiki\restoreWarnings();
-               if ( pg_last_error( $this->mConn ) ) {
-                       throw new DBUnexpectedError(
-                               $this,
-                               'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
-                       );
-               }
-
-               return $n;
-       }
-
-       function numFields( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-
-               return pg_num_fields( $res );
-       }
-
-       function fieldName( $res, $n ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-
-               return pg_field_name( $res, $n );
-       }
-
-       /**
-        * Return the result of the last call to nextSequenceValue();
-        * This must be called after nextSequenceValue().
-        *
-        * @return int|null
-        */
-       function insertId() {
-               return $this->mInsertId;
-       }
-
-       /**
-        * @param mixed $res
-        * @param int $row
-        * @return bool
-        */
-       function dataSeek( $res, $row ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-
-               return pg_result_seek( $res, $row );
-       }
-
-       function lastError() {
-               if ( $this->mConn ) {
-                       if ( $this->mLastResult ) {
-                               return pg_result_error( $this->mLastResult );
-                       } else {
-                               return pg_last_error();
-                       }
-               } else {
-                       return 'No database connection';
-               }
-       }
-
-       function lastErrno() {
-               if ( $this->mLastResult ) {
-                       return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
-               } else {
-                       return false;
-               }
-       }
-
-       function affectedRows() {
-               if ( !is_null( $this->mAffectedRows ) ) {
-                       // Forced result for simulated queries
-                       return $this->mAffectedRows;
-               }
-               if ( empty( $this->mLastResult ) ) {
-                       return 0;
-               }
-
-               return pg_affected_rows( $this->mLastResult );
-       }
-
-       /**
-        * Estimate rows in dataset
-        * Returns estimated count, based on EXPLAIN output
-        * This is not necessarily an accurate estimate, so use sparingly
-        * Returns -1 if count cannot be found
-        * Takes same arguments as Database::select()
-        *
-        * @param string $table
-        * @param string $vars
-        * @param string $conds
-        * @param string $fname
-        * @param array $options
-        * @return int
-        */
-       function estimateRowCount( $table, $vars = '*', $conds = '',
-               $fname = __METHOD__, $options = []
-       ) {
-               $options['EXPLAIN'] = true;
-               $res = $this->select( $table, $vars, $conds, $fname, $options );
-               $rows = -1;
-               if ( $res ) {
-                       $row = $this->fetchRow( $res );
-                       $count = [];
-                       if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
-                               $rows = (int)$count[1];
-                       }
-               }
-
-               return $rows;
-       }
-
-       /**
-        * Returns information about an index
-        * If errors are explicitly ignored, returns NULL on failure
-        *
-        * @param string $table
-        * @param string $index
-        * @param string $fname
-        * @return bool|null
-        */
-       function indexInfo( $table, $index, $fname = __METHOD__ ) {
-               $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
-               $res = $this->query( $sql, $fname );
-               if ( !$res ) {
-                       return null;
-               }
-               foreach ( $res as $row ) {
-                       if ( $row->indexname == $this->indexName( $index ) ) {
-                               return $row;
-                       }
-               }
-
-               return false;
-       }
-
-       /**
-        * Returns is of attributes used in index
-        *
-        * @since 1.19
-        * @param string $index
-        * @param bool|string $schema
-        * @return array
-        */
-       function indexAttributes( $index, $schema = false ) {
-               if ( $schema === false ) {
-                       $schema = $this->getCoreSchema();
-               }
-               /*
-                * A subquery would be not needed if we didn't care about the order
-                * of attributes, but we do
-                */
-               $sql = <<<__INDEXATTR__
-
-                       SELECT opcname,
-                               attname,
-                               i.indoption[s.g] as option,
-                               pg_am.amname
-                       FROM
-                               (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
-                                       FROM
-                                               pg_index isub
-                                       JOIN pg_class cis
-                                               ON cis.oid=isub.indexrelid
-                                       JOIN pg_namespace ns
-                                               ON cis.relnamespace = ns.oid
-                                       WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
-                               pg_attribute,
-                               pg_opclass opcls,
-                               pg_am,
-                               pg_class ci
-                               JOIN pg_index i
-                                       ON ci.oid=i.indexrelid
-                               JOIN pg_class ct
-                                       ON ct.oid = i.indrelid
-                               JOIN pg_namespace n
-                                       ON ci.relnamespace = n.oid
-                               WHERE
-                                       ci.relname='$index' AND n.nspname='$schema'
-                                       AND     attrelid = ct.oid
-                                       AND     i.indkey[s.g] = attnum
-                                       AND     i.indclass[s.g] = opcls.oid
-                                       AND     pg_am.oid = opcls.opcmethod
-__INDEXATTR__;
-               $res = $this->query( $sql, __METHOD__ );
-               $a = [];
-               if ( $res ) {
-                       foreach ( $res as $row ) {
-                               $a[] = [
-                                       $row->attname,
-                                       $row->opcname,
-                                       $row->amname,
-                                       $row->option ];
-                       }
-               } else {
-                       return null;
-               }
-
-               return $a;
-       }
-
-       function indexUnique( $table, $index, $fname = __METHOD__ ) {
-               $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" .
-                       " AND indexdef LIKE 'CREATE UNIQUE%(" .
-                       $this->strencode( $this->indexName( $index ) ) .
-                       ")'";
-               $res = $this->query( $sql, $fname );
-               if ( !$res ) {
-                       return null;
-               }
-
-               return $res->numRows() > 0;
-       }
-
-       /**
-        * Change the FOR UPDATE option as necessary based on the join conditions. Then pass
-        * to the parent function to get the actual SQL text.
-        *
-        * In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
-        * can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to do
-        * so causes a DB error. This wrapper checks which tables can be locked and adjusts it accordingly.
-        *
-        * MySQL uses "ORDER BY NULL" as an optimization hint, but that syntax is illegal in PostgreSQL.
-        * @see DatabaseBase::selectSQLText
-        */
-       function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
-               $options = [], $join_conds = []
-       ) {
-               if ( is_array( $options ) ) {
-                       $forUpdateKey = array_search( 'FOR UPDATE', $options, true );
-                       if ( $forUpdateKey !== false && $join_conds ) {
-                               unset( $options[$forUpdateKey] );
-
-                               foreach ( $join_conds as $table_cond => $join_cond ) {
-                                       if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
-                                               $options['FOR UPDATE'][] = $table_cond;
-                                       }
-                               }
-                       }
-
-                       if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
-                               unset( $options['ORDER BY'] );
-                       }
-               }
-
-               return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
-       }
-
-       /**
-        * INSERT wrapper, inserts an array into a table
-        *
-        * $args may be a single associative array, or an array of these with numeric keys,
-        * for multi-row insert (Postgres version 8.2 and above only).
-        *
-        * @param string $table Name of the table to insert to.
-        * @param array $args Items to insert into the table.
-        * @param string $fname Name of the function, for profiling
-        * @param array|string $options String or array. Valid options: IGNORE
-        * @return bool Success of insert operation. IGNORE always returns true.
-        */
-       function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
-               if ( !count( $args ) ) {
-                       return true;
-               }
-
-               $table = $this->tableName( $table );
-               if ( !isset( $this->numericVersion ) ) {
-                       $this->getServerVersion();
-               }
-
-               if ( !is_array( $options ) ) {
-                       $options = [ $options ];
-               }
-
-               if ( isset( $args[0] ) && is_array( $args[0] ) ) {
-                       $multi = true;
-                       $keys = array_keys( $args[0] );
-               } else {
-                       $multi = false;
-                       $keys = array_keys( $args );
-               }
-
-               // If IGNORE is set, we use savepoints to emulate mysql's behavior
-               $savepoint = null;
-               if ( in_array( 'IGNORE', $options ) ) {
-                       $savepoint = new SavepointPostgres( $this, 'mw' );
-                       $olde = error_reporting( 0 );
-                       // For future use, we may want to track the number of actual inserts
-                       // Right now, insert (all writes) simply return true/false
-                       $numrowsinserted = 0;
-               }
-
-               $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
-
-               if ( $multi ) {
-                       if ( $this->numericVersion >= 8.2 && !$savepoint ) {
-                               $first = true;
-                               foreach ( $args as $row ) {
-                                       if ( $first ) {
-                                               $first = false;
-                                       } else {
-                                               $sql .= ',';
-                                       }
-                                       $sql .= '(' . $this->makeList( $row ) . ')';
-                               }
-                               $res = (bool)$this->query( $sql, $fname, $savepoint );
-                       } else {
-                               $res = true;
-                               $origsql = $sql;
-                               foreach ( $args as $row ) {
-                                       $tempsql = $origsql;
-                                       $tempsql .= '(' . $this->makeList( $row ) . ')';
-
-                                       if ( $savepoint ) {
-                                               $savepoint->savepoint();
-                                       }
-
-                                       $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
-
-                                       if ( $savepoint ) {
-                                               $bar = pg_result_error( $this->mLastResult );
-                                               if ( $bar != false ) {
-                                                       $savepoint->rollback();
-                                               } else {
-                                                       $savepoint->release();
-                                                       $numrowsinserted++;
-                                               }
-                                       }
-
-                                       // If any of them fail, we fail overall for this function call
-                                       // Note that this will be ignored if IGNORE is set
-                                       if ( !$tempres ) {
-                                               $res = false;
-                                       }
-                               }
-                       }
-               } else {
-                       // Not multi, just a lone insert
-                       if ( $savepoint ) {
-                               $savepoint->savepoint();
-                       }
-
-                       $sql .= '(' . $this->makeList( $args ) . ')';
-                       $res = (bool)$this->query( $sql, $fname, $savepoint );
-                       if ( $savepoint ) {
-                               $bar = pg_result_error( $this->mLastResult );
-                               if ( $bar != false ) {
-                                       $savepoint->rollback();
-                               } else {
-                                       $savepoint->release();
-                                       $numrowsinserted++;
-                               }
-                       }
-               }
-               if ( $savepoint ) {
-                       error_reporting( $olde );
-                       $savepoint->commit();
-
-                       // Set the affected row count for the whole operation
-                       $this->mAffectedRows = $numrowsinserted;
-
-                       // IGNORE always returns true
-                       return true;
-               }
-
-               return $res;
-       }
-
-       /**
-        * INSERT SELECT wrapper
-        * $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ]
-        * Source items may be literals rather then field names, but strings should
-        * be quoted with Database::addQuotes()
-        * $conds may be "*" to copy the whole table
-        * srcTable may be an array of tables.
-        * @todo FIXME: Implement this a little better (seperate select/insert)?
-        *
-        * @param string $destTable
-        * @param array|string $srcTable
-        * @param array $varMap
-        * @param array $conds
-        * @param string $fname
-        * @param array $insertOptions
-        * @param array $selectOptions
-        * @return bool
-        */
-       function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
-               $insertOptions = [], $selectOptions = [] ) {
-               $destTable = $this->tableName( $destTable );
-
-               if ( !is_array( $insertOptions ) ) {
-                       $insertOptions = [ $insertOptions ];
-               }
-
-               /*
-                * If IGNORE is set, we use savepoints to emulate mysql's behavior
-                * Ignore LOW PRIORITY option, since it is MySQL-specific
-                */
-               $savepoint = null;
-               if ( in_array( 'IGNORE', $insertOptions ) ) {
-                       $savepoint = new SavepointPostgres( $this, 'mw' );
-                       $olde = error_reporting( 0 );
-                       $numrowsinserted = 0;
-                       $savepoint->savepoint();
-               }
-
-               if ( !is_array( $selectOptions ) ) {
-                       $selectOptions = [ $selectOptions ];
-               }
-               list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) =
-                       $this->makeSelectOptions( $selectOptions );
-               if ( is_array( $srcTable ) ) {
-                       $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
-               } else {
-                       $srcTable = $this->tableName( $srcTable );
-               }
-
-               $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
-                       " SELECT $startOpts " . implode( ',', $varMap ) .
-                       " FROM $srcTable $useIndex $ignoreIndex ";
-
-               if ( $conds != '*' ) {
-                       $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
-               }
-
-               $sql .= " $tailOpts";
-
-               $res = (bool)$this->query( $sql, $fname, $savepoint );
-               if ( $savepoint ) {
-                       $bar = pg_result_error( $this->mLastResult );
-                       if ( $bar != false ) {
-                               $savepoint->rollback();
-                       } else {
-                               $savepoint->release();
-                               $numrowsinserted++;
-                       }
-                       error_reporting( $olde );
-                       $savepoint->commit();
-
-                       // Set the affected row count for the whole operation
-                       $this->mAffectedRows = $numrowsinserted;
-
-                       // IGNORE always returns true
-                       return true;
-               }
-
-               return $res;
-       }
-
-       function tableName( $name, $format = 'quoted' ) {
-               # Replace reserved words with better ones
-               switch ( $name ) {
-                       case 'user':
-                               return $this->realTableName( 'mwuser', $format );
-                       case 'text':
-                               return $this->realTableName( 'pagecontent', $format );
-                       default:
-                               return $this->realTableName( $name, $format );
-               }
-       }
-
-       /* Don't cheat on installer */
-       function realTableName( $name, $format = 'quoted' ) {
-               return parent::tableName( $name, $format );
-       }
-
-       /**
-        * Return the next in a sequence, save the value for retrieval via insertId()
-        *
-        * @param string $seqName
-        * @return int|null
-        */
-       function nextSequenceValue( $seqName ) {
-               $safeseq = str_replace( "'", "''", $seqName );
-               $res = $this->query( "SELECT nextval('$safeseq')" );
-               $row = $this->fetchRow( $res );
-               $this->mInsertId = $row[0];
-
-               return $this->mInsertId;
-       }
-
-       /**
-        * Return the current value of a sequence. Assumes it has been nextval'ed in this session.
-        *
-        * @param string $seqName
-        * @return int
-        */
-       function currentSequenceValue( $seqName ) {
-               $safeseq = str_replace( "'", "''", $seqName );
-               $res = $this->query( "SELECT currval('$safeseq')" );
-               $row = $this->fetchRow( $res );
-               $currval = $row[0];
-
-               return $currval;
-       }
-
-       # Returns the size of a text field, or -1 for "unlimited"
-       function textFieldSize( $table, $field ) {
-               $table = $this->tableName( $table );
-               $sql = "SELECT t.typname as ftype,a.atttypmod as size
-                       FROM pg_class c, pg_attribute a, pg_type t
-                       WHERE relname='$table' AND a.attrelid=c.oid AND
-                               a.atttypid=t.oid and a.attname='$field'";
-               $res = $this->query( $sql );
-               $row = $this->fetchObject( $res );
-               if ( $row->ftype == 'varchar' ) {
-                       $size = $row->size - 4;
-               } else {
-                       $size = $row->size;
-               }
-
-               return $size;
-       }
-
-       function limitResult( $sql, $limit, $offset = false ) {
-               return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
-       }
-
-       function wasDeadlock() {
-               return $this->lastErrno() == '40P01';
-       }
-
-       function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
-               $newName = $this->addIdentifierQuotes( $newName );
-               $oldName = $this->addIdentifierQuotes( $oldName );
-
-               return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " .
-                       "(LIKE $oldName INCLUDING DEFAULTS)", $fname );
-       }
-
-       function listTables( $prefix = null, $fname = __METHOD__ ) {
-               $eschema = $this->addQuotes( $this->getCoreSchema() );
-               $result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
-               $endArray = [];
-
-               foreach ( $result as $table ) {
-                       $vars = get_object_vars( $table );
-                       $table = array_pop( $vars );
-                       if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
-                               $endArray[] = $table;
-                       }
-               }
-
-               return $endArray;
-       }
-
-       function timestamp( $ts = 0 ) {
-               return wfTimestamp( TS_POSTGRES, $ts );
-       }
-
-       /**
-        * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
-        * to http://www.php.net/manual/en/ref.pgsql.php
-        *
-        * Parsing a postgres array can be a tricky problem, he's my
-        * take on this, it handles multi-dimensional arrays plus
-        * escaping using a nasty regexp to determine the limits of each
-        * data-item.
-        *
-        * This should really be handled by PHP PostgreSQL module
-        *
-        * @since 1.19
-        * @param string $text Postgreql array returned in a text form like {a,b}
-        * @param string $output
-        * @param int $limit
-        * @param int $offset
-        * @return string
-        */
-       function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
-               if ( false === $limit ) {
-                       $limit = strlen( $text ) - 1;
-                       $output = [];
-               }
-               if ( '{}' == $text ) {
-                       return $output;
-               }
-               do {
-                       if ( '{' != $text[$offset] ) {
-                               preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
-                                       $text, $match, 0, $offset );
-                               $offset += strlen( $match[0] );
-                               $output[] = ( '"' != $match[1][0]
-                                       ? $match[1]
-                                       : stripcslashes( substr( $match[1], 1, -1 ) ) );
-                               if ( '},' == $match[3] ) {
-                                       return $output;
-                               }
-                       } else {
-                               $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 );
-                       }
-               } while ( $limit > $offset );
-
-               return $output;
-       }
-
-       /**
-        * Return aggregated value function call
-        * @param array $valuedata
-        * @param string $valuename
-        * @return array
-        */
-       public function aggregateValue( $valuedata, $valuename = 'value' ) {
-               return $valuedata;
-       }
-
-       /**
-        * @return string Wikitext of a link to the server software's web site
-        */
-       public function getSoftwareLink() {
-               return '[{{int:version-db-postgres-url}} PostgreSQL]';
-       }
-
-       /**
-        * Return current schema (executes SELECT current_schema())
-        * Needs transaction
-        *
-        * @since 1.19
-        * @return string Default schema for the current session
-        */
-       function getCurrentSchema() {
-               $res = $this->query( "SELECT current_schema()", __METHOD__ );
-               $row = $this->fetchRow( $res );
-
-               return $row[0];
-       }
-
-       /**
-        * Return list of schemas which are accessible without schema name
-        * This is list does not contain magic keywords like "$user"
-        * Needs transaction
-        *
-        * @see getSearchPath()
-        * @see setSearchPath()
-        * @since 1.19
-        * @return array List of actual schemas for the current sesson
-        */
-       function getSchemas() {
-               $res = $this->query( "SELECT current_schemas(false)", __METHOD__ );
-               $row = $this->fetchRow( $res );
-               $schemas = [];
-
-               /* PHP pgsql support does not support array type, "{a,b}" string is returned */
-
-               return $this->pg_array_parse( $row[0], $schemas );
-       }
-
-       /**
-        * Return search patch for schemas
-        * This is different from getSchemas() since it contain magic keywords
-        * (like "$user").
-        * Needs transaction
-        *
-        * @since 1.19
-        * @return array How to search for table names schemas for the current user
-        */
-       function getSearchPath() {
-               $res = $this->query( "SHOW search_path", __METHOD__ );
-               $row = $this->fetchRow( $res );
-
-               /* PostgreSQL returns SHOW values as strings */
-
-               return explode( ",", $row[0] );
-       }
-
-       /**
-        * Update search_path, values should already be sanitized
-        * Values may contain magic keywords like "$user"
-        * @since 1.19
-        *
-        * @param array $search_path List of schemas to be searched by default
-        */
-       function setSearchPath( $search_path ) {
-               $this->query( "SET search_path = " . implode( ", ", $search_path ) );
-       }
-
-       /**
-        * Determine default schema for MediaWiki core
-        * Adjust this session schema search path if desired schema exists
-        * and is not alread there.
-        *
-        * We need to have name of the core schema stored to be able
-        * to query database metadata.
-        *
-        * This will be also called by the installer after the schema is created
-        *
-        * @since 1.19
-        *
-        * @param string $desiredSchema
-        */
-       function determineCoreSchema( $desiredSchema ) {
-               $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
-               if ( $this->schemaExists( $desiredSchema ) ) {
-                       if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
-                               $this->mCoreSchema = $desiredSchema;
-                               wfDebug( "Schema \"" . $desiredSchema . "\" already in the search path\n" );
-                       } else {
-                               /**
-                                * Prepend our schema (e.g. 'mediawiki') in front
-                                * of the search path
-                                * Fixes bug 15816
-                                */
-                               $search_path = $this->getSearchPath();
-                               array_unshift( $search_path,
-                                       $this->addIdentifierQuotes( $desiredSchema ) );
-                               $this->setSearchPath( $search_path );
-                               $this->mCoreSchema = $desiredSchema;
-                               wfDebug( "Schema \"" . $desiredSchema . "\" added to the search path\n" );
-                       }
-               } else {
-                       $this->mCoreSchema = $this->getCurrentSchema();
-                       wfDebug( "Schema \"" . $desiredSchema . "\" not found, using current \"" .
-                               $this->mCoreSchema . "\"\n" );
-               }
-               /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
-               $this->commit( __METHOD__ );
-       }
-
-       /**
-        * Return schema name fore core MediaWiki tables
-        *
-        * @since 1.19
-        * @return string Core schema name
-        */
-       function getCoreSchema() {
-               return $this->mCoreSchema;
-       }
-
-       /**
-        * @return string Version information from the database
-        */
-       function getServerVersion() {
-               if ( !isset( $this->numericVersion ) ) {
-                       $versionInfo = pg_version( $this->mConn );
-                       if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
-                               // Old client, abort install
-                               $this->numericVersion = '7.3 or earlier';
-                       } elseif ( isset( $versionInfo['server'] ) ) {
-                               // Normal client
-                               $this->numericVersion = $versionInfo['server'];
-                       } else {
-                               // Bug 16937: broken pgsql extension from PHP<5.3
-                               $this->numericVersion = pg_parameter_status( $this->mConn, 'server_version' );
-                       }
-               }
-
-               return $this->numericVersion;
-       }
-
-       /**
-        * Query whether a given relation exists (in the given schema, or the
-        * default mw one if not given)
-        * @param string $table
-        * @param array|string $types
-        * @param bool|string $schema
-        * @return bool
-        */
-       function relationExists( $table, $types, $schema = false ) {
-               if ( !is_array( $types ) ) {
-                       $types = [ $types ];
-               }
-               if ( !$schema ) {
-                       $schema = $this->getCoreSchema();
-               }
-               $table = $this->realTableName( $table, 'raw' );
-               $etable = $this->addQuotes( $table );
-               $eschema = $this->addQuotes( $schema );
-               $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
-                       . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
-                       . "AND c.relkind IN ('" . implode( "','", $types ) . "')";
-               $res = $this->query( $sql );
-               $count = $res ? $res->numRows() : 0;
-
-               return (bool)$count;
-       }
-
-       /**
-        * For backward compatibility, this function checks both tables and
-        * views.
-        * @param string $table
-        * @param string $fname
-        * @param bool|string $schema
-        * @return bool
-        */
-       function tableExists( $table, $fname = __METHOD__, $schema = false ) {
-               return $this->relationExists( $table, [ 'r', 'v' ], $schema );
-       }
-
-       function sequenceExists( $sequence, $schema = false ) {
-               return $this->relationExists( $sequence, 'S', $schema );
-       }
-
-       function triggerExists( $table, $trigger ) {
-               $q = <<<SQL
-       SELECT 1 FROM pg_class, pg_namespace, pg_trigger
-               WHERE relnamespace=pg_namespace.oid AND relkind='r'
-                         AND tgrelid=pg_class.oid
-                         AND nspname=%s AND relname=%s AND tgname=%s
-SQL;
-               $res = $this->query(
-                       sprintf(
-                               $q,
-                               $this->addQuotes( $this->getCoreSchema() ),
-                               $this->addQuotes( $table ),
-                               $this->addQuotes( $trigger )
-                       )
-               );
-               if ( !$res ) {
-                       return null;
-               }
-               $rows = $res->numRows();
-
-               return $rows;
-       }
-
-       function ruleExists( $table, $rule ) {
-               $exists = $this->selectField( 'pg_rules', 'rulename',
-                       [
-                               'rulename' => $rule,
-                               'tablename' => $table,
-                               'schemaname' => $this->getCoreSchema()
-                       ]
-               );
-
-               return $exists === $rule;
-       }
-
-       function constraintExists( $table, $constraint ) {
-               $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
-                       "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
-                       $this->addQuotes( $this->getCoreSchema() ),
-                       $this->addQuotes( $table ),
-                       $this->addQuotes( $constraint )
-               );
-               $res = $this->query( $sql );
-               if ( !$res ) {
-                       return null;
-               }
-               $rows = $res->numRows();
-
-               return $rows;
-       }
-
-       /**
-        * Query whether a given schema exists. Returns true if it does, false if it doesn't.
-        * @param string $schema
-        * @return bool
-        */
-       function schemaExists( $schema ) {
-               $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
-                       [ 'nspname' => $schema ], __METHOD__ );
-
-               return (bool)$exists;
-       }
-
-       /**
-        * Returns true if a given role (i.e. user) exists, false otherwise.
-        * @param string $roleName
-        * @return bool
-        */
-       function roleExists( $roleName ) {
-               $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
-                       [ 'rolname' => $roleName ], __METHOD__ );
-
-               return (bool)$exists;
-       }
-
-       /**
-        * @var string $table
-        * @var string $field
-        * @return PostgresField|null
-        */
-       function fieldInfo( $table, $field ) {
-               return PostgresField::fromText( $this, $table, $field );
-       }
-
-       /**
-        * pg_field_type() wrapper
-        * @param ResultWrapper|resource $res ResultWrapper or PostgreSQL query result resource
-        * @param int $index Field number, starting from 0
-        * @return string
-        */
-       function fieldType( $res, $index ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-
-               return pg_field_type( $res, $index );
-       }
-
-       /**
-        * @param string $b
-        * @return Blob
-        */
-       function encodeBlob( $b ) {
-               return new PostgresBlob( pg_escape_bytea( $b ) );
-       }
-
-       function decodeBlob( $b ) {
-               if ( $b instanceof PostgresBlob ) {
-                       $b = $b->fetch();
-               } elseif ( $b instanceof Blob ) {
-                       return $b->fetch();
-               }
-
-               return pg_unescape_bytea( $b );
-       }
-
-       function strencode( $s ) {
-               // Should not be called by us
-
-               return pg_escape_string( $this->mConn, $s );
-       }
-
-       /**
-        * @param null|bool|Blob $s
-        * @return int|string
-        */
-       function addQuotes( $s ) {
-               if ( is_null( $s ) ) {
-                       return 'NULL';
-               } elseif ( is_bool( $s ) ) {
-                       return intval( $s );
-               } elseif ( $s instanceof Blob ) {
-                       if ( $s instanceof PostgresBlob ) {
-                               $s = $s->fetch();
-                       } else {
-                               $s = pg_escape_bytea( $this->mConn, $s->fetch() );
-                       }
-                       return "'$s'";
-               }
-
-               return "'" . pg_escape_string( $this->mConn, $s ) . "'";
-       }
-
-       /**
-        * Postgres specific version of replaceVars.
-        * Calls the parent version in Database.php
-        *
-        * @param string $ins SQL string, read from a stream (usually tables.sql)
-        * @return string SQL string
-        */
-       protected function replaceVars( $ins ) {
-               $ins = parent::replaceVars( $ins );
-
-               if ( $this->numericVersion >= 8.3 ) {
-                       // Thanks for not providing backwards-compatibility, 8.3
-                       $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
-               }
-
-               if ( $this->numericVersion <= 8.1 ) { // Our minimum version
-                       $ins = str_replace( 'USING gin', 'USING gist', $ins );
-               }
-
-               return $ins;
-       }
-
-       /**
-        * Various select options
-        *
-        * @param array $options An associative array of options to be turned into
-        *   an SQL query, valid keys are listed in the function.
-        * @return array
-        */
-       function makeSelectOptions( $options ) {
-               $preLimitTail = $postLimitTail = '';
-               $startOpts = $useIndex = $ignoreIndex = '';
-
-               $noKeyOptions = [];
-               foreach ( $options as $key => $option ) {
-                       if ( is_numeric( $key ) ) {
-                               $noKeyOptions[$option] = true;
-                       }
-               }
-
-               $preLimitTail .= $this->makeGroupByWithHaving( $options );
-
-               $preLimitTail .= $this->makeOrderBy( $options );
-
-               // if ( isset( $options['LIMIT'] ) ) {
-               //      $tailOpts .= $this->limitResult( '', $options['LIMIT'],
-               //              isset( $options['OFFSET'] ) ? $options['OFFSET']
-               //              : false );
-               // }
-
-               if ( isset( $options['FOR UPDATE'] ) ) {
-                       $postLimitTail .= ' FOR UPDATE OF ' .
-                               implode( ', ', array_map( [ &$this, 'tableName' ], $options['FOR UPDATE'] ) );
-               } elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
-                       $postLimitTail .= ' FOR UPDATE';
-               }
-
-               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
-                       $startOpts .= 'DISTINCT';
-               }
-
-               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
-       }
-
-       function getDBname() {
-               return $this->mDBname;
-       }
-
-       function getServer() {
-               return $this->mServer;
-       }
-
-       function buildConcat( $stringList ) {
-               return implode( ' || ', $stringList );
-       }
-
-       public function buildGroupConcatField(
-               $delimiter, $table, $field, $conds = '', $options = [], $join_conds = []
-       ) {
-               $fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delimiter ) . ')';
-
-               return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
-       }
-
-       /**
-        * @param string $field Field or column to cast
-        * @return string
-        * @since 1.28
-        */
-       public function buildStringCast( $field ) {
-               return $field . '::text';
-       }
-
-       public function getSearchEngine() {
-               return 'SearchPostgres';
-       }
-
-       public function streamStatementEnd( &$sql, &$newLine ) {
-               # Allow dollar quoting for function declarations
-               if ( substr( $newLine, 0, 4 ) == '$mw$' ) {
-                       if ( $this->delimiter ) {
-                               $this->delimiter = false;
-                       } else {
-                               $this->delimiter = ';';
-                       }
-               }
-
-               return parent::streamStatementEnd( $sql, $newLine );
-       }
-
-       /**
-        * Check to see if a named lock is available. This is non-blocking.
-        * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
-        *
-        * @param string $lockName Name of lock to poll
-        * @param string $method Name of method calling us
-        * @return bool
-        * @since 1.20
-        */
-       public function lockIsFree( $lockName, $method ) {
-               $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
-               $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
-                       WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
-               $row = $this->fetchObject( $result );
-
-               return ( $row->lockstatus === 't' );
-       }
-
-       /**
-        * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
-        * @param string $lockName
-        * @param string $method
-        * @param int $timeout
-        * @return bool
-        */
-       public function lock( $lockName, $method, $timeout = 5 ) {
-               $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
-               $loop = new WaitConditionLoop(
-                       function () use ( $lockName, $key, $timeout, $method ) {
-                               $res = $this->query( "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
-                               $row = $this->fetchObject( $res );
-                               if ( $row->lockstatus === 't' ) {
-                                       parent::lock( $lockName, $method, $timeout ); // record
-                                       return true;
-                               }
-
-                               return WaitConditionLoop::CONDITION_CONTINUE;
-                       },
-                       $timeout
-               );
-
-               return ( $loop->invoke() === $loop::CONDITION_REACHED );
-       }
-
-       /**
-        * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM
-        * PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
-        * @param string $lockName
-        * @param string $method
-        * @return bool
-        */
-       public function unlock( $lockName, $method ) {
-               $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
-               $result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
-               $row = $this->fetchObject( $result );
-
-               if ( $row->lockstatus === 't' ) {
-                       parent::unlock( $lockName, $method ); // record
-                       return true;
-               }
-
-               wfDebug( __METHOD__ . " failed to release lock\n" );
-
-               return false;
-       }
-
-       /**
-        * @param string $lockName
-        * @return string Integer
-        */
-       private function bigintFromLockName( $lockName ) {
-               return Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 );
-       }
-} // end DatabasePostgres class
diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php
deleted file mode 100644 (file)
index 0cbb496..0000000
+++ /dev/null
@@ -1,1051 +0,0 @@
-<?php
-/**
- * This is the SQLite database abstraction layer.
- * See maintenance/sqlite/README for development notes and other specific information
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * @ingroup Database
- */
-class DatabaseSqlite extends Database {
-       /** @var bool Whether full text is enabled */
-       private static $fulltextEnabled = null;
-
-       /** @var string Directory */
-       protected $dbDir;
-
-       /** @var string File name for SQLite database file */
-       protected $dbPath;
-
-       /** @var string Transaction mode */
-       protected $trxMode;
-
-       /** @var int The number of rows affected as an integer */
-       protected $mAffectedRows;
-
-       /** @var resource */
-       protected $mLastResult;
-
-       /** @var PDO */
-       protected $mConn;
-
-       /** @var FSLockManager (hopefully on the same server as the DB) */
-       protected $lockMgr;
-
-       /**
-        * Additional params include:
-        *   - dbDirectory : directory containing the DB and the lock file directory
-        *                   [defaults to $wgSQLiteDataDir]
-        *   - dbFilePath  : use this to force the path of the DB file
-        *   - trxMode     : one of (deferred, immediate, exclusive)
-        * @param array $p
-        */
-       function __construct( array $p ) {
-               global $wgSQLiteDataDir;
-
-               $this->dbDir = isset( $p['dbDirectory'] ) ? $p['dbDirectory'] : $wgSQLiteDataDir;
-
-               if ( isset( $p['dbFilePath'] ) ) {
-                       parent::__construct( $p );
-                       // Standalone .sqlite file mode.
-                       // Super doesn't open when $user is false, but we can work with $dbName,
-                       // which is derived from the file path in this case.
-                       $this->openFile( $p['dbFilePath'] );
-               } else {
-                       $this->mDBname = $p['dbname'];
-                       // Stock wiki mode using standard file names per DB.
-                       parent::__construct( $p );
-                       // Super doesn't open when $user is false, but we can work with $dbName
-                       if ( $p['dbname'] && !$this->isOpen() ) {
-                               if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) {
-                                       $done = [];
-                                       foreach ( $this->tableAliases as $params ) {
-                                               if ( isset( $done[$params['dbname']] ) ) {
-                                                       continue;
-                                               }
-                                               $this->attachDatabase( $params['dbname'] );
-                                               $done[$params['dbname']] = 1;
-                                       }
-                               }
-                       }
-               }
-
-               $this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
-               if ( $this->trxMode &&
-                       !in_array( $this->trxMode, [ 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ] )
-               ) {
-                       $this->trxMode = null;
-                       wfWarn( "Invalid SQLite transaction mode provided." );
-               }
-
-               $this->lockMgr = new FSLockManager( [ 'lockDirectory' => "{$this->dbDir}/locks" ] );
-       }
-
-       /**
-        * @param string $filename
-        * @param array $p Options map; supports:
-        *   - flags       : (same as __construct counterpart)
-        *   - trxMode     : (same as __construct counterpart)
-        *   - dbDirectory : (same as __construct counterpart)
-        * @return DatabaseSqlite
-        * @since 1.25
-        */
-       public static function newStandaloneInstance( $filename, array $p = [] ) {
-               $p['dbFilePath'] = $filename;
-               $p['schema'] = false;
-               $p['tablePrefix'] = '';
-
-               return DatabaseBase::factory( 'sqlite', $p );
-       }
-
-       /**
-        * @return string
-        */
-       function getType() {
-               return 'sqlite';
-       }
-
-       /**
-        * @todo Check if it should be true like parent class
-        *
-        * @return bool
-        */
-       function implicitGroupby() {
-               return false;
-       }
-
-       /** Open an SQLite database and return a resource handle to it
-        *  NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
-        *
-        * @param string $server
-        * @param string $user
-        * @param string $pass
-        * @param string $dbName
-        *
-        * @throws DBConnectionError
-        * @return PDO
-        */
-       function open( $server, $user, $pass, $dbName ) {
-               $this->close();
-               $fileName = self::generateFileName( $this->dbDir, $dbName );
-               if ( !is_readable( $fileName ) ) {
-                       $this->mConn = false;
-                       throw new DBConnectionError( $this, "SQLite database not accessible" );
-               }
-               $this->openFile( $fileName );
-
-               return $this->mConn;
-       }
-
-       /**
-        * Opens a database file
-        *
-        * @param string $fileName
-        * @throws DBConnectionError
-        * @return PDO|bool SQL connection or false if failed
-        */
-       protected function openFile( $fileName ) {
-               $err = false;
-
-               $this->dbPath = $fileName;
-               try {
-                       if ( $this->mFlags & DBO_PERSISTENT ) {
-                               $this->mConn = new PDO( "sqlite:$fileName", '', '',
-                                       [ PDO::ATTR_PERSISTENT => true ] );
-                       } else {
-                               $this->mConn = new PDO( "sqlite:$fileName", '', '' );
-                       }
-               } catch ( PDOException $e ) {
-                       $err = $e->getMessage();
-               }
-
-               if ( !$this->mConn ) {
-                       wfDebug( "DB connection error: $err\n" );
-                       throw new DBConnectionError( $this, $err );
-               }
-
-               $this->mOpened = !!$this->mConn;
-               if ( $this->mOpened ) {
-                       # Set error codes only, don't raise exceptions
-                       $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
-                       # Enforce LIKE to be case sensitive, just like MySQL
-                       $this->query( 'PRAGMA case_sensitive_like = 1' );
-
-                       return $this->mConn;
-               }
-
-               return false;
-       }
-
-       /**
-        * @return string SQLite DB file path
-        * @since 1.25
-        */
-       public function getDbFilePath() {
-               return $this->dbPath;
-       }
-
-       /**
-        * Does not actually close the connection, just destroys the reference for GC to do its work
-        * @return bool
-        */
-       protected function closeConnection() {
-               $this->mConn = null;
-
-               return true;
-       }
-
-       /**
-        * Generates a database file name. Explicitly public for installer.
-        * @param string $dir Directory where database resides
-        * @param string $dbName Database name
-        * @return string
-        */
-       public static function generateFileName( $dir, $dbName ) {
-               return "$dir/$dbName.sqlite";
-       }
-
-       /**
-        * Check if the searchindext table is FTS enabled.
-        * @return bool False if not enabled.
-        */
-       function checkForEnabledSearch() {
-               if ( self::$fulltextEnabled === null ) {
-                       self::$fulltextEnabled = false;
-                       $table = $this->tableName( 'searchindex' );
-                       $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
-                       if ( $res ) {
-                               $row = $res->fetchRow();
-                               self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
-                       }
-               }
-
-               return self::$fulltextEnabled;
-       }
-
-       /**
-        * Returns version of currently supported SQLite fulltext search module or false if none present.
-        * @return string
-        */
-       static function getFulltextSearchModule() {
-               static $cachedResult = null;
-               if ( $cachedResult !== null ) {
-                       return $cachedResult;
-               }
-               $cachedResult = false;
-               $table = 'dummy_search_test';
-
-               $db = self::newStandaloneInstance( ':memory:' );
-               if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
-                       $cachedResult = 'FTS3';
-               }
-               $db->close();
-
-               return $cachedResult;
-       }
-
-       /**
-        * Attaches external database to our connection, see http://sqlite.org/lang_attach.html
-        * for details.
-        *
-        * @param string $name Database name to be used in queries like
-        *   SELECT foo FROM dbname.table
-        * @param bool|string $file Database file name. If omitted, will be generated
-        *   using $name and configured data directory
-        * @param string $fname Calling function name
-        * @return ResultWrapper
-        */
-       function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
-               if ( !$file ) {
-                       $file = self::generateFileName( $this->dbDir, $name );
-               }
-               $file = $this->addQuotes( $file );
-
-               return $this->query( "ATTACH DATABASE $file AS $name", $fname );
-       }
-
-       function isWriteQuery( $sql ) {
-               return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
-       }
-
-       /**
-        * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
-        *
-        * @param string $sql
-        * @return bool|ResultWrapper
-        */
-       protected function doQuery( $sql ) {
-               $res = $this->mConn->query( $sql );
-               if ( $res === false ) {
-                       return false;
-               } else {
-                       $r = $res instanceof ResultWrapper ? $res->result : $res;
-                       $this->mAffectedRows = $r->rowCount();
-                       $res = new ResultWrapper( $this, $r->fetchAll() );
-               }
-
-               return $res;
-       }
-
-       /**
-        * @param ResultWrapper|mixed $res
-        */
-       function freeResult( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res->result = null;
-               } else {
-                       $res = null;
-               }
-       }
-
-       /**
-        * @param ResultWrapper|array $res
-        * @return stdClass|bool
-        */
-       function fetchObject( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $r =& $res->result;
-               } else {
-                       $r =& $res;
-               }
-
-               $cur = current( $r );
-               if ( is_array( $cur ) ) {
-                       next( $r );
-                       $obj = new stdClass;
-                       foreach ( $cur as $k => $v ) {
-                               if ( !is_numeric( $k ) ) {
-                                       $obj->$k = $v;
-                               }
-                       }
-
-                       return $obj;
-               }
-
-               return false;
-       }
-
-       /**
-        * @param ResultWrapper|mixed $res
-        * @return array|bool
-        */
-       function fetchRow( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $r =& $res->result;
-               } else {
-                       $r =& $res;
-               }
-               $cur = current( $r );
-               if ( is_array( $cur ) ) {
-                       next( $r );
-
-                       return $cur;
-               }
-
-               return false;
-       }
-
-       /**
-        * The PDO::Statement class implements the array interface so count() will work
-        *
-        * @param ResultWrapper|array $res
-        * @return int
-        */
-       function numRows( $res ) {
-               $r = $res instanceof ResultWrapper ? $res->result : $res;
-
-               return count( $r );
-       }
-
-       /**
-        * @param ResultWrapper $res
-        * @return int
-        */
-       function numFields( $res ) {
-               $r = $res instanceof ResultWrapper ? $res->result : $res;
-               if ( is_array( $r ) && count( $r ) > 0 ) {
-                       // The size of the result array is twice the number of fields. (Bug: 65578)
-                       return count( $r[0] ) / 2;
-               } else {
-                       // If the result is empty return 0
-                       return 0;
-               }
-       }
-
-       /**
-        * @param ResultWrapper $res
-        * @param int $n
-        * @return bool
-        */
-       function fieldName( $res, $n ) {
-               $r = $res instanceof ResultWrapper ? $res->result : $res;
-               if ( is_array( $r ) ) {
-                       $keys = array_keys( $r[0] );
-
-                       return $keys[$n];
-               }
-
-               return false;
-       }
-
-       /**
-        * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
-        *
-        * @param string $name
-        * @param string $format
-        * @return string
-        */
-       function tableName( $name, $format = 'quoted' ) {
-               // table names starting with sqlite_ are reserved
-               if ( strpos( $name, 'sqlite_' ) === 0 ) {
-                       return $name;
-               }
-
-               return str_replace( '"', '', parent::tableName( $name, $format ) );
-       }
-
-       /**
-        * Index names have DB scope
-        *
-        * @param string $index
-        * @return string
-        */
-       protected function indexName( $index ) {
-               return $index;
-       }
-
-       /**
-        * This must be called after nextSequenceVal
-        *
-        * @return int
-        */
-       function insertId() {
-               // PDO::lastInsertId yields a string :(
-               return intval( $this->mConn->lastInsertId() );
-       }
-
-       /**
-        * @param ResultWrapper|array $res
-        * @param int $row
-        */
-       function dataSeek( $res, $row ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $r =& $res->result;
-               } else {
-                       $r =& $res;
-               }
-               reset( $r );
-               if ( $row > 0 ) {
-                       for ( $i = 0; $i < $row; $i++ ) {
-                               next( $r );
-                       }
-               }
-       }
-
-       /**
-        * @return string
-        */
-       function lastError() {
-               if ( !is_object( $this->mConn ) ) {
-                       return "Cannot return last error, no db connection";
-               }
-               $e = $this->mConn->errorInfo();
-
-               return isset( $e[2] ) ? $e[2] : '';
-       }
-
-       /**
-        * @return string
-        */
-       function lastErrno() {
-               if ( !is_object( $this->mConn ) ) {
-                       return "Cannot return last error, no db connection";
-               } else {
-                       $info = $this->mConn->errorInfo();
-
-                       return $info[1];
-               }
-       }
-
-       /**
-        * @return int
-        */
-       function affectedRows() {
-               return $this->mAffectedRows;
-       }
-
-       /**
-        * Returns information about an index
-        * Returns false if the index does not exist
-        * - if errors are explicitly ignored, returns NULL on failure
-        *
-        * @param string $table
-        * @param string $index
-        * @param string $fname
-        * @return array
-        */
-       function indexInfo( $table, $index, $fname = __METHOD__ ) {
-               $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
-               $res = $this->query( $sql, $fname );
-               if ( !$res ) {
-                       return null;
-               }
-               if ( $res->numRows() == 0 ) {
-                       return false;
-               }
-               $info = [];
-               foreach ( $res as $row ) {
-                       $info[] = $row->name;
-               }
-
-               return $info;
-       }
-
-       /**
-        * @param string $table
-        * @param string $index
-        * @param string $fname
-        * @return bool|null
-        */
-       function indexUnique( $table, $index, $fname = __METHOD__ ) {
-               $row = $this->selectRow( 'sqlite_master', '*',
-                       [
-                               'type' => 'index',
-                               'name' => $this->indexName( $index ),
-                       ], $fname );
-               if ( !$row || !isset( $row->sql ) ) {
-                       return null;
-               }
-
-               // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
-               $indexPos = strpos( $row->sql, 'INDEX' );
-               if ( $indexPos === false ) {
-                       return null;
-               }
-               $firstPart = substr( $row->sql, 0, $indexPos );
-               $options = explode( ' ', $firstPart );
-
-               return in_array( 'UNIQUE', $options );
-       }
-
-       /**
-        * Filter the options used in SELECT statements
-        *
-        * @param array $options
-        * @return array
-        */
-       function makeSelectOptions( $options ) {
-               foreach ( $options as $k => $v ) {
-                       if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
-                               $options[$k] = '';
-                       }
-               }
-
-               return parent::makeSelectOptions( $options );
-       }
-
-       /**
-        * @param array $options
-        * @return string
-        */
-       protected function makeUpdateOptionsArray( $options ) {
-               $options = parent::makeUpdateOptionsArray( $options );
-               $options = self::fixIgnore( $options );
-
-               return $options;
-       }
-
-       /**
-        * @param array $options
-        * @return array
-        */
-       static function fixIgnore( $options ) {
-               # SQLite uses OR IGNORE not just IGNORE
-               foreach ( $options as $k => $v ) {
-                       if ( $v == 'IGNORE' ) {
-                               $options[$k] = 'OR IGNORE';
-                       }
-               }
-
-               return $options;
-       }
-
-       /**
-        * @param array $options
-        * @return string
-        */
-       function makeInsertOptions( $options ) {
-               $options = self::fixIgnore( $options );
-
-               return parent::makeInsertOptions( $options );
-       }
-
-       /**
-        * Based on generic method (parent) with some prior SQLite-sepcific adjustments
-        * @param string $table
-        * @param array $a
-        * @param string $fname
-        * @param array $options
-        * @return bool
-        */
-       function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
-               if ( !count( $a ) ) {
-                       return true;
-               }
-
-               # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
-               if ( isset( $a[0] ) && is_array( $a[0] ) ) {
-                       $ret = true;
-                       foreach ( $a as $v ) {
-                               if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) {
-                                       $ret = false;
-                               }
-                       }
-               } else {
-                       $ret = parent::insert( $table, $a, "$fname/single-row", $options );
-               }
-
-               return $ret;
-       }
-
-       /**
-        * @param string $table
-        * @param array $uniqueIndexes Unused
-        * @param string|array $rows
-        * @param string $fname
-        * @return bool|ResultWrapper
-        */
-       function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
-               if ( !count( $rows ) ) {
-                       return true;
-               }
-
-               # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
-               if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
-                       $ret = true;
-                       foreach ( $rows as $v ) {
-                               if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) {
-                                       $ret = false;
-                               }
-                       }
-               } else {
-                       $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" );
-               }
-
-               return $ret;
-       }
-
-       /**
-        * Returns the size of a text field, or -1 for "unlimited"
-        * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though.
-        *
-        * @param string $table
-        * @param string $field
-        * @return int
-        */
-       function textFieldSize( $table, $field ) {
-               return -1;
-       }
-
-       /**
-        * @return bool
-        */
-       function unionSupportsOrderAndLimit() {
-               return false;
-       }
-
-       /**
-        * @param string $sqls
-        * @param bool $all Whether to "UNION ALL" or not
-        * @return string
-        */
-       function unionQueries( $sqls, $all ) {
-               $glue = $all ? ' UNION ALL ' : ' UNION ';
-
-               return implode( $glue, $sqls );
-       }
-
-       /**
-        * @return bool
-        */
-       function wasDeadlock() {
-               return $this->lastErrno() == 5; // SQLITE_BUSY
-       }
-
-       /**
-        * @return bool
-        */
-       function wasErrorReissuable() {
-               return $this->lastErrno() == 17; // SQLITE_SCHEMA;
-       }
-
-       /**
-        * @return bool
-        */
-       function wasReadOnlyError() {
-               return $this->lastErrno() == 8; // SQLITE_READONLY;
-       }
-
-       /**
-        * @return string Wikitext of a link to the server software's web site
-        */
-       public function getSoftwareLink() {
-               return "[{{int:version-db-sqlite-url}} SQLite]";
-       }
-
-       /**
-        * @return string Version information from the database
-        */
-       function getServerVersion() {
-               $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
-
-               return $ver;
-       }
-
-       /**
-        * @return string User-friendly database information
-        */
-       public function getServerInfo() {
-               return wfMessage( self::getFulltextSearchModule()
-                       ? 'sqlite-has-fts'
-                       : 'sqlite-no-fts', $this->getServerVersion() )->text();
-       }
-
-       /**
-        * Get information about a given field
-        * Returns false if the field does not exist.
-        *
-        * @param string $table
-        * @param string $field
-        * @return SQLiteField|bool False on failure
-        */
-       function fieldInfo( $table, $field ) {
-               $tableName = $this->tableName( $table );
-               $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
-               $res = $this->query( $sql, __METHOD__ );
-               foreach ( $res as $row ) {
-                       if ( $row->name == $field ) {
-                               return new SQLiteField( $row, $tableName );
-                       }
-               }
-
-               return false;
-       }
-
-       protected function doBegin( $fname = '' ) {
-               if ( $this->trxMode ) {
-                       $this->query( "BEGIN {$this->trxMode}", $fname );
-               } else {
-                       $this->query( 'BEGIN', $fname );
-               }
-               $this->mTrxLevel = 1;
-       }
-
-       /**
-        * @param string $s
-        * @return string
-        */
-       function strencode( $s ) {
-               return substr( $this->addQuotes( $s ), 1, -1 );
-       }
-
-       /**
-        * @param string $b
-        * @return Blob
-        */
-       function encodeBlob( $b ) {
-               return new Blob( $b );
-       }
-
-       /**
-        * @param Blob|string $b
-        * @return string
-        */
-       function decodeBlob( $b ) {
-               if ( $b instanceof Blob ) {
-                       $b = $b->fetch();
-               }
-
-               return $b;
-       }
-
-       /**
-        * @param Blob|string $s
-        * @return string
-        */
-       function addQuotes( $s ) {
-               if ( $s instanceof Blob ) {
-                       return "x'" . bin2hex( $s->fetch() ) . "'";
-               } elseif ( is_bool( $s ) ) {
-                       return (int)$s;
-               } elseif ( strpos( $s, "\0" ) !== false ) {
-                       // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
-                       // This is a known limitation of SQLite's mprintf function which PDO
-                       // should work around, but doesn't. I have reported this to php.net as bug #63419:
-                       // https://bugs.php.net/bug.php?id=63419
-                       // There was already a similar report for SQLite3::escapeString, bug #62361:
-                       // https://bugs.php.net/bug.php?id=62361
-                       // There is an additional bug regarding sorting this data after insert
-                       // on older versions of sqlite shipped with ubuntu 12.04
-                       // https://phabricator.wikimedia.org/T74367
-                       wfDebugLog(
-                               __CLASS__,
-                               __FUNCTION__ .
-                                       ': Quoting value containing null byte. ' .
-                                       'For consistency all binary data should have been ' .
-                                       'first processed with self::encodeBlob()'
-                       );
-                       return "x'" . bin2hex( $s ) . "'";
-               } else {
-                       return $this->mConn->quote( $s );
-               }
-       }
-
-       /**
-        * @return string
-        */
-       function buildLike() {
-               $params = func_get_args();
-               if ( count( $params ) > 0 && is_array( $params[0] ) ) {
-                       $params = $params[0];
-               }
-
-               return parent::buildLike( $params ) . "ESCAPE '\' ";
-       }
-
-       /**
-        * @param string $field Field or column to cast
-        * @return string
-        * @since 1.28
-        */
-       public function buildStringCast( $field ) {
-               return 'CAST ( ' . $field . ' AS TEXT )';
-       }
-
-       /**
-        * @return string
-        */
-       public function getSearchEngine() {
-               return "SearchSqlite";
-       }
-
-       /**
-        * No-op version of deadlockLoop
-        *
-        * @return mixed
-        */
-       public function deadlockLoop( /*...*/ ) {
-               $args = func_get_args();
-               $function = array_shift( $args );
-
-               return call_user_func_array( $function, $args );
-       }
-
-       /**
-        * @param string $s
-        * @return string
-        */
-       protected function replaceVars( $s ) {
-               $s = parent::replaceVars( $s );
-               if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
-                       // CREATE TABLE hacks to allow schema file sharing with MySQL
-
-                       // binary/varbinary column type -> blob
-                       $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
-                       // no such thing as unsigned
-                       $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
-                       // INT -> INTEGER
-                       $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
-                       // floating point types -> REAL
-                       $s = preg_replace(
-                               '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i',
-                               'REAL',
-                               $s
-                       );
-                       // varchar -> TEXT
-                       $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
-                       // TEXT normalization
-                       $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
-                       // BLOB normalization
-                       $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
-                       // BOOL -> INTEGER
-                       $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
-                       // DATETIME -> TEXT
-                       $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
-                       // No ENUM type
-                       $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
-                       // binary collation type -> nothing
-                       $s = preg_replace( '/\bbinary\b/i', '', $s );
-                       // auto_increment -> autoincrement
-                       $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
-                       // No explicit options
-                       $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
-                       // AUTOINCREMENT should immedidately follow PRIMARY KEY
-                       $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
-               } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
-                       // No truncated indexes
-                       $s = preg_replace( '/\(\d+\)/', '', $s );
-                       // No FULLTEXT
-                       $s = preg_replace( '/\bfulltext\b/i', '', $s );
-               } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
-                       // DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
-                       $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
-               } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) {
-                       // INSERT IGNORE --> INSERT OR IGNORE
-                       $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s );
-               }
-
-               return $s;
-       }
-
-       public function lock( $lockName, $method, $timeout = 5 ) {
-               if ( !is_dir( "{$this->dbDir}/locks" ) ) { // create dir as needed
-                       if ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) ) {
-                               throw new DBError( $this, "Cannot create directory \"{$this->dbDir}/locks\"." );
-                       }
-               }
-
-               return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK();
-       }
-
-       public function unlock( $lockName, $method ) {
-               return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isOK();
-       }
-
-       /**
-        * Build a concatenation list to feed into a SQL query
-        *
-        * @param string[] $stringList
-        * @return string
-        */
-       function buildConcat( $stringList ) {
-               return '(' . implode( ') || (', $stringList ) . ')';
-       }
-
-       public function buildGroupConcatField(
-               $delim, $table, $field, $conds = '', $join_conds = []
-       ) {
-               $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
-
-               return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
-       }
-
-       /**
-        * @param string $oldName
-        * @param string $newName
-        * @param bool $temporary
-        * @param string $fname
-        * @return bool|ResultWrapper
-        * @throws RuntimeException
-        */
-       function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
-               $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" .
-                       $this->addQuotes( $oldName ) . " AND type='table'", $fname );
-               $obj = $this->fetchObject( $res );
-               if ( !$obj ) {
-                       throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
-               }
-               $sql = $obj->sql;
-               $sql = preg_replace(
-                       '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/',
-                       $this->addIdentifierQuotes( $newName ),
-                       $sql,
-                       1
-               );
-               if ( $temporary ) {
-                       if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
-                               wfDebug( "Table $oldName is virtual, can't create a temporary duplicate.\n" );
-                       } else {
-                               $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
-                       }
-               }
-
-               $res = $this->query( $sql, $fname );
-
-               // Take over indexes
-               $indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );
-               foreach ( $indexList as $index ) {
-                       if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
-                               continue;
-                       }
-
-                       if ( $index->unique ) {
-                               $sql = 'CREATE UNIQUE INDEX';
-                       } else {
-                               $sql = 'CREATE INDEX';
-                       }
-                       // Try to come up with a new index name, given indexes have database scope in SQLite
-                       $indexName = $newName . '_' . $index->name;
-                       $sql .= ' ' . $indexName . ' ON ' . $newName;
-
-                       $indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' );
-                       $fields = [];
-                       foreach ( $indexInfo as $indexInfoRow ) {
-                               $fields[$indexInfoRow->seqno] = $indexInfoRow->name;
-                       }
-
-                       $sql .= '(' . implode( ',', $fields ) . ')';
-
-                       $this->query( $sql );
-               }
-
-               return $res;
-       }
-
-       /**
-        * List all tables on the database
-        *
-        * @param string $prefix Only show tables with this prefix, e.g. mw_
-        * @param string $fname Calling function name
-        *
-        * @return array
-        */
-       function listTables( $prefix = null, $fname = __METHOD__ ) {
-               $result = $this->select(
-                       'sqlite_master',
-                       'name',
-                       "type='table'"
-               );
-
-               $endArray = [];
-
-               foreach ( $result as $table ) {
-                       $vars = get_object_vars( $table );
-                       $table = array_pop( $vars );
-
-                       if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
-                               if ( strpos( $table, 'sqlite_' ) !== 0 ) {
-                                       $endArray[] = $table;
-                               }
-                       }
-               }
-
-               return $endArray;
-       }
-
-       /**
-        * @return string
-        */
-       public function __toString() {
-               return 'SQLite ' . (string)$this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
-       }
-
-} // end DatabaseSqlite class
diff --git a/includes/db/MWLBFactory.php b/includes/db/MWLBFactory.php
new file mode 100644 (file)
index 0000000..bfdce39
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Generator 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
+ */
+
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+
+/**
+ * MediaWiki-specific class for generating database load balancers
+ * @ingroup Database
+ */
+abstract class MWLBFactory {
+       /**
+        * @param array $lbConf Config for LBFactory::__construct()
+        * @param Config $mainConfig Main config object from MediaWikiServices
+        * @return array
+        */
+       public static function applyDefaultConfig( array $lbConf, Config $mainConfig ) {
+               global $wgCommandLineMode;
+
+               $lbConf += [
+                       'localDomain' => new DatabaseDomain(
+                               $mainConfig->get( 'DBname' ),
+                               null,
+                               $mainConfig->get( 'DBprefix' )
+                       ),
+                       'profiler' => Profiler::instance(),
+                       'trxProfiler' => Profiler::instance()->getTransactionProfiler(),
+                       'replLogger' => LoggerFactory::getInstance( 'DBReplication' ),
+                       'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ),
+                       'connLogger' => LoggerFactory::getInstance( 'DBConnection' ),
+                       'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ),
+                       'errorLogger' => [ MWExceptionHandler::class, 'logException' ],
+                       'cliMode' => $wgCommandLineMode,
+                       'hostname' => wfHostname(),
+                       // TODO: replace the global wfConfiguredReadOnlyReason() with a service.
+                       'readOnlyReason' => wfConfiguredReadOnlyReason(),
+               ];
+
+               if ( $lbConf['class'] === 'LBFactorySimple' ) {
+                       if ( isset( $lbConf['servers'] ) ) {
+                               // Server array is already explicitly configured; leave alone
+                       } elseif ( is_array( $mainConfig->get( 'DBservers' ) ) ) {
+                               foreach ( $mainConfig->get( 'DBservers' ) as $i => $server ) {
+                                       if ( $server['type'] === 'sqlite' ) {
+                                               $server += [ 'dbDirectory' => $mainConfig->get( 'SQLiteDataDir' ) ];
+                                       } elseif ( $server['type'] === 'postgres' ) {
+                                               $server += [ 'port' => $mainConfig->get( 'DBport' ) ];
+                                       }
+                                       $lbConf['servers'][$i] = $server + [
+                                               'schema' => $mainConfig->get( 'DBmwschema' ),
+                                               'tablePrefix' => $mainConfig->get( 'DBprefix' ),
+                                               'flags' => DBO_DEFAULT,
+                                               'sqlMode' => $mainConfig->get( 'SQLMode' ),
+                                               'utf8Mode' => $mainConfig->get( 'DBmysql5' )
+                                       ];
+                               }
+                       } else {
+                               $flags = DBO_DEFAULT;
+                               $flags |= $mainConfig->get( 'DebugDumpSql' ) ? DBO_DEBUG : 0;
+                               $flags |= $mainConfig->get( 'DBssl' ) ? DBO_SSL : 0;
+                               $flags |= $mainConfig->get( 'DBcompress' ) ? DBO_COMPRESS : 0;
+                               $server = [
+                                       'host' => $mainConfig->get( 'DBserver' ),
+                                       'user' => $mainConfig->get( 'DBuser' ),
+                                       'password' => $mainConfig->get( 'DBpassword' ),
+                                       'dbname' => $mainConfig->get( 'DBname' ),
+                                       'schema' => $mainConfig->get( 'DBmwschema' ),
+                                       'tablePrefix' => $mainConfig->get( 'DBprefix' ),
+                                       'type' => $mainConfig->get( 'DBtype' ),
+                                       'load' => 1,
+                                       'flags' => $flags,
+                                       'sqlMode' => $mainConfig->get( 'SQLMode' ),
+                                       'utf8Mode' => $mainConfig->get( 'DBmysql5' )
+                               ];
+                               if ( $server['type'] === 'sqlite' ) {
+                                       $server[ 'dbDirectory'] = $mainConfig->get( 'SQLiteDataDir' );
+                               } elseif ( $server['type'] === 'postgres' ) {
+                                       $server['port'] = $mainConfig->get( 'DBport' );
+                               }
+                               $lbConf['servers'] = [ $server ];
+                       }
+                       if ( !isset( $lbConf['externalClusters'] ) ) {
+                               $lbConf['externalClusters'] = $mainConfig->get( 'ExternalServers' );
+                       }
+               } elseif ( $lbConf['class'] === 'LBFactoryMulti' ) {
+                       if ( isset( $lbConf['serverTemplate'] ) ) {
+                               $lbConf['serverTemplate']['schema'] = $mainConfig->get( 'DBmwschema' );
+                               $lbConf['serverTemplate']['sqlMode'] = $mainConfig->get( 'SQLMode' );
+                               $lbConf['serverTemplate']['utf8Mode'] = $mainConfig->get( 'DBmysql5' );
+                       }
+               }
+
+               // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
+               $sCache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
+               if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) {
+                       $lbConf['srvCache'] = $sCache;
+               }
+               $cCache = ObjectCache::getLocalClusterInstance();
+               if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) {
+                       $lbConf['memCache'] = $cCache;
+               }
+               $wCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+               if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
+                       $lbConf['wanCache'] = $wCache;
+               }
+
+               return $lbConf;
+       }
+
+       /**
+        * Returns the LBFactory class to use and the load balancer configuration.
+        *
+        * @todo instead of this, use a ServiceContainer for managing the different implementations.
+        *
+        * @param array $config (e.g. $wgLBFactoryConf)
+        * @return string Class name
+        */
+       public static function getLBFactoryClass( array $config ) {
+               // For configuration backward compatibility after removing
+               // underscores from class names in MediaWiki 1.23.
+               $bcClasses = [
+                       'LBFactory_Simple' => 'LBFactorySimple',
+                       'LBFactory_Single' => 'LBFactorySingle',
+                       'LBFactory_Multi' => 'LBFactoryMulti'
+               ];
+
+               $class = $config['class'];
+
+               if ( isset( $bcClasses[$class] ) ) {
+                       $class = $bcClasses[$class];
+                       wfDeprecated(
+                               '$wgLBFactoryConf must be updated. See RELEASE-NOTES for details',
+                               '1.23'
+                       );
+               }
+
+               return $class;
+       }
+}
diff --git a/includes/db/loadbalancer/LBFactoryMW.php b/includes/db/loadbalancer/LBFactoryMW.php
deleted file mode 100644 (file)
index 33c48a5..0000000
+++ /dev/null
@@ -1,154 +0,0 @@
-<?php
-/**
- * Generator 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
- */
-
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Services\DestructibleService;
-use MediaWiki\Logger\LoggerFactory;
-
-/**
- * Legacy MediaWiki-specific class for generating database load balancers
- * @ingroup Database
- */
-abstract class LBFactoryMW extends LBFactory implements DestructibleService {
-       /** @noinspection PhpMissingParentConstructorInspection */
-       /**
-        * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
-        * @param array $conf
-        * @TODO: inject objects via dependency framework
-        */
-       public function __construct( array $conf ) {
-               global $wgCommandLineMode;
-
-               $defaults = [
-                       'domain' => wfWikiID(),
-                       'hostname' => wfHostname(),
-                       'trxProfiler' => Profiler::instance()->getTransactionProfiler(),
-                       'replLogger' => LoggerFactory::getInstance( 'DBReplication' ),
-                       'queryLogger' => LoggerFactory::getInstance( 'wfLogDBError' ),
-                       'connLogger' => LoggerFactory::getInstance( 'wfLogDBError' ),
-                       'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ),
-                       'errorLogger' => [ MWExceptionHandler::class, 'logException' ]
-               ];
-               // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
-               $sCache = ObjectCache::getLocalServerInstance();
-               if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) {
-                       $defaults['srvCache'] = $sCache;
-               }
-               $cCache = ObjectCache::getLocalClusterInstance();
-               if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) {
-                       $defaults['memCache'] = $cCache;
-               }
-               $wCache = ObjectCache::getMainWANInstance();
-               if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
-                       $defaults['wanCache'] = $wCache;
-               }
-
-               $this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
-               $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : $wgCommandLineMode;
-
-               parent::__construct( $conf + $defaults );
-       }
-
-       /**
-        * Returns the LBFactory class to use and the load balancer configuration.
-        *
-        * @todo instead of this, use a ServiceContainer for managing the different implementations.
-        *
-        * @param array $config (e.g. $wgLBFactoryConf)
-        * @return string Class name
-        */
-       public static function getLBFactoryClass( array $config ) {
-               // For configuration backward compatibility after removing
-               // underscores from class names in MediaWiki 1.23.
-               $bcClasses = [
-                       'LBFactory_Simple' => 'LBFactorySimple',
-                       'LBFactory_Single' => 'LBFactorySingle',
-                       'LBFactory_Multi' => 'LBFactoryMulti'
-               ];
-
-               $class = $config['class'];
-
-               if ( isset( $bcClasses[$class] ) ) {
-                       $class = $bcClasses[$class];
-                       wfDeprecated(
-                               '$wgLBFactoryConf must be updated. See RELEASE-NOTES for details',
-                               '1.23'
-                       );
-               }
-
-               return $class;
-       }
-
-       /**
-        * @return bool
-        * @since 1.27
-        * @deprecated Since 1.28; use laggedReplicaUsed()
-        */
-       public function laggedSlaveUsed() {
-               return $this->laggedReplicaUsed();
-       }
-
-       protected function newChronologyProtector() {
-               $request = RequestContext::getMain()->getRequest();
-               $chronProt = new ChronologyProtector(
-                       ObjectCache::getMainStashInstance(),
-                       [
-                               'ip' => $request->getIP(),
-                               'agent' => $request->getHeader( 'User-Agent' ),
-                       ],
-                       $request->getFloat( 'cpPosTime', $request->getCookie( 'cpPosTime', '' ) )
-               );
-               if ( PHP_SAPI === 'cli' ) {
-                       $chronProt->setEnabled( false );
-               } elseif ( $request->getHeader( 'ChronologyProtection' ) === 'false' ) {
-                       // Request opted out of using position wait logic. This is useful for requests
-                       // done by the job queue or background ETL that do not have a meaningful session.
-                       $chronProt->setWaitEnabled( false );
-               }
-
-               return $chronProt;
-       }
-
-       /**
-        * 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 ( LoadBalancer $lb ) use ( &$usedCluster ) {
-                       $usedCluster |= ( $lb->getServerCount() > 1 );
-               } );
-
-               if ( !$usedCluster ) {
-                       return $url; // no master/replica clusters touched
-               }
-
-               return wfAppendQuery( $url, [ 'cpPosTime' => $time ] );
-       }
-}
diff --git a/includes/db/loadbalancer/LBFactoryMulti.php b/includes/db/loadbalancer/LBFactoryMulti.php
deleted file mode 100644 (file)
index 95bc8f4..0000000
+++ /dev/null
@@ -1,422 +0,0 @@
-<?php
-/**
- * Advanced generator of database load balancing objects for wiki farms.
- *
- * 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
- */
-
-/**
- * A multi-wiki, multi-master factory for Wikimedia and similar installations.
- * Ignores the old configuration globals.
- *
- * Template override precedence (highest => lowest):
- *   - templateOverridesByServer
- *   - masterTemplateOverrides
- *   - templateOverridesBySection/templateOverridesByCluster
- *   - externalTemplateOverrides
- *   - serverTemplate
- * Overrides only work on top level keys (so nested values will not be merged).
- *
- * Configuration:
- *     sectionsByDB                A map of database names to section names.
- *
- *     sectionLoads                A 2-d map. For each section, gives a map of server names to
- *                                 load ratios. For example:
- *                                 [
- *                                     'section1' => [
- *                                         'db1' => 100,
- *                                         'db2' => 100
- *                                     ]
- *                                 ]
- *
- *     serverTemplate              A server info associative array as documented for $wgDBservers.
- *                                 The host, hostName and load entries will be overridden.
- *
- *     groupLoadsBySection         A 3-d map giving server load ratios for each section and group.
- *                                 For example:
- *                                 [
- *                                     'section1' => [
- *                                         'group1' => [
- *                                             'db1' => 100,
- *                                             'db2' => 100
- *                                         ]
- *                                     ]
- *                                 ]
- *
- *     groupLoadsByDB              A 3-d map giving server load ratios by DB name.
- *
- *     hostsByName                 A map of hostname to IP address.
- *
- *     externalLoads               A map of external storage cluster name to server load map.
- *
- *     externalTemplateOverrides   A set of server info keys overriding serverTemplate for external
- *                                 storage.
- *
- *     templateOverridesByServer   A 2-d map overriding serverTemplate and
- *                                 externalTemplateOverrides on a server-by-server basis. Applies
- *                                 to both core and external storage.
- *     templateOverridesBySection  A 2-d map overriding the server info by section.
- *     templateOverridesByCluster  A 2-d map overriding the server info by external storage cluster.
- *
- *     masterTemplateOverrides     An override array for all master servers.
- *
- *     loadMonitorClass            Name of the LoadMonitor class to always use.
- *
- *     readOnlyBySection           A map of section name to read-only message.
- *                                 Missing or false for read/write.
- *
- * @ingroup Database
- */
-class LBFactoryMulti extends LBFactoryMW {
-       /** @var array A map of database names to section names */
-       private $sectionsByDB;
-
-       /**
-        * @var array A 2-d map. For each section, gives a map of server names to
-        * load ratios
-        */
-       private $sectionLoads;
-
-       /**
-        * @var array A server info associative array as documented for
-        * $wgDBservers. The host, hostName and load entries will be
-        * overridden
-        */
-       private $serverTemplate;
-
-       // Optional settings
-
-       /** @var array A 3-d map giving server load ratios for each section and group */
-       private $groupLoadsBySection = [];
-
-       /** @var array A 3-d map giving server load ratios by DB name */
-       private $groupLoadsByDB = [];
-
-       /** @var array A map of hostname to IP address */
-       private $hostsByName = [];
-
-       /** @var array A map of external storage cluster name to server load map */
-       private $externalLoads = [];
-
-       /**
-        * @var array A set of server info keys overriding serverTemplate for
-        * external storage
-        */
-       private $externalTemplateOverrides;
-
-       /**
-        * @var array A 2-d map overriding serverTemplate and
-        * externalTemplateOverrides on a server-by-server basis. Applies to both
-        * core and external storage
-        */
-       private $templateOverridesByServer;
-
-       /** @var array A 2-d map overriding the server info by section */
-       private $templateOverridesBySection;
-
-       /** @var array A 2-d map overriding the server info by external storage cluster */
-       private $templateOverridesByCluster;
-
-       /** @var array An override array for all master servers */
-       private $masterTemplateOverrides;
-
-       /**
-        * @var array|bool A map of section name to read-only message. Missing or
-        * false for read/write
-        */
-       private $readOnlyBySection = [];
-
-       // Other stuff
-
-       /** @var array Load balancer factory configuration */
-       private $conf;
-
-       /** @var LoadBalancer[] */
-       private $mainLBs = [];
-
-       /** @var LoadBalancer[] */
-       private $extLBs = [];
-
-       /** @var string */
-       private $loadMonitorClass;
-
-       /** @var string */
-       private $lastWiki;
-
-       /** @var string */
-       private $lastSection;
-
-       /**
-        * @param array $conf
-        * @throws InvalidArgumentException
-        */
-       public function __construct( array $conf ) {
-               parent::__construct( $conf );
-
-               $this->conf = $conf;
-               $required = [ 'sectionsByDB', 'sectionLoads', 'serverTemplate' ];
-               $optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
-                       'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
-                       'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides',
-                       'readOnlyBySection', 'loadMonitorClass' ];
-
-               foreach ( $required as $key ) {
-                       if ( !isset( $conf[$key] ) ) {
-                               throw new InvalidArgumentException( __CLASS__ . ": $key is required in configuration" );
-                       }
-                       $this->$key = $conf[$key];
-               }
-
-               foreach ( $optional as $key ) {
-                       if ( isset( $conf[$key] ) ) {
-                               $this->$key = $conf[$key];
-                       }
-               }
-       }
-
-       /**
-        * @param bool|string $wiki
-        * @return string
-        */
-       private function getSectionForWiki( $wiki = false ) {
-               if ( $this->lastWiki === $wiki ) {
-                       return $this->lastSection;
-               }
-               list( $dbName, ) = $this->getDBNameAndPrefix( $wiki );
-               if ( isset( $this->sectionsByDB[$dbName] ) ) {
-                       $section = $this->sectionsByDB[$dbName];
-               } else {
-                       $section = 'DEFAULT';
-               }
-               $this->lastSection = $section;
-               $this->lastWiki = $wiki;
-
-               return $section;
-       }
-
-       /**
-        * @param bool|string $wiki
-        * @return LoadBalancer
-        */
-       public function newMainLB( $wiki = false ) {
-               list( $dbName, ) = $this->getDBNameAndPrefix( $wiki );
-               $section = $this->getSectionForWiki( $wiki );
-               if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
-                       $groupLoads = $this->groupLoadsByDB[$dbName];
-               } else {
-                       $groupLoads = [];
-               }
-
-               if ( isset( $this->groupLoadsBySection[$section] ) ) {
-                       $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] );
-               }
-
-               $readOnlyReason = $this->readOnlyReason;
-               // Use the LB-specific read-only reason if everything isn't already read-only
-               if ( $readOnlyReason === false && isset( $this->readOnlyBySection[$section] ) ) {
-                       $readOnlyReason = $this->readOnlyBySection[$section];
-               }
-
-               $template = $this->serverTemplate;
-               if ( isset( $this->templateOverridesBySection[$section] ) ) {
-                       $template = $this->templateOverridesBySection[$section] + $template;
-               }
-
-               return $this->newLoadBalancer(
-                       $template,
-                       $this->sectionLoads[$section],
-                       $groupLoads,
-                       $readOnlyReason
-               );
-       }
-
-       /**
-        * @param bool|string $wiki
-        * @return LoadBalancer
-        */
-       public function getMainLB( $wiki = false ) {
-               $section = $this->getSectionForWiki( $wiki );
-               if ( !isset( $this->mainLBs[$section] ) ) {
-                       $lb = $this->newMainLB( $wiki );
-                       $this->chronProt->initLB( $lb );
-                       $this->mainLBs[$section] = $lb;
-               }
-
-               return $this->mainLBs[$section];
-       }
-
-       /**
-        * @param string $cluster
-        * @param bool|string $wiki
-        * @throws InvalidArgumentException
-        * @return LoadBalancer
-        */
-       protected function newExternalLB( $cluster, $wiki = false ) {
-               if ( !isset( $this->externalLoads[$cluster] ) ) {
-                       throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
-               }
-               $template = $this->serverTemplate;
-               if ( isset( $this->externalTemplateOverrides ) ) {
-                       $template = $this->externalTemplateOverrides + $template;
-               }
-               if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
-                       $template = $this->templateOverridesByCluster[$cluster] + $template;
-               }
-
-               return $this->newLoadBalancer(
-                       $template,
-                       $this->externalLoads[$cluster],
-                       [],
-                       $this->readOnlyReason
-               );
-       }
-
-       /**
-        * @param string $cluster External storage cluster, or false for core
-        * @param bool|string $wiki Wiki ID, or false for the current wiki
-        * @return LoadBalancer
-        */
-       public function getExternalLB( $cluster, $wiki = false ) {
-               if ( !isset( $this->extLBs[$cluster] ) ) {
-                       $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
-                       $this->chronProt->initLB( $this->extLBs[$cluster] );
-               }
-
-               return $this->extLBs[$cluster];
-       }
-
-       /**
-        * Make a new load balancer object based on template and load array
-        *
-        * @param array $template
-        * @param array $loads
-        * @param array $groupLoads
-        * @param string|bool $readOnlyReason
-        * @return LoadBalancer
-        */
-       private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) {
-               $lb = new LoadBalancer( array_merge(
-                       $this->baseLoadBalancerParams(),
-                       [
-                               'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
-                               'loadMonitor' => $this->loadMonitorClass,
-                               'readOnlyReason' => $readOnlyReason
-                       ]
-               ) );
-               $this->initLoadBalancer( $lb );
-
-               return $lb;
-       }
-
-       /**
-        * Make a server array as expected by LoadBalancer::__construct, using a template and load array
-        *
-        * @param array $template
-        * @param array $loads
-        * @param array $groupLoads
-        * @return array
-        */
-       private function makeServerArray( $template, $loads, $groupLoads ) {
-               $servers = [];
-               $master = true;
-               $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
-               foreach ( $groupLoadsByServer as $server => $stuff ) {
-                       if ( !isset( $loads[$server] ) ) {
-                               $loads[$server] = 0;
-                       }
-               }
-               foreach ( $loads as $serverName => $load ) {
-                       $serverInfo = $template;
-                       if ( $master ) {
-                               $serverInfo['master'] = true;
-                               if ( isset( $this->masterTemplateOverrides ) ) {
-                                       $serverInfo = $this->masterTemplateOverrides + $serverInfo;
-                               }
-                               $master = false;
-                       } else {
-                               $serverInfo['replica'] = true;
-                       }
-                       if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
-                               $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
-                       }
-                       if ( isset( $groupLoadsByServer[$serverName] ) ) {
-                               $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
-                       }
-                       if ( isset( $this->hostsByName[$serverName] ) ) {
-                               $serverInfo['host'] = $this->hostsByName[$serverName];
-                       } else {
-                               $serverInfo['host'] = $serverName;
-                       }
-                       $serverInfo['hostName'] = $serverName;
-                       $serverInfo['load'] = $load;
-                       $serverInfo += [ 'flags' => DBO_DEFAULT ];
-
-                       $servers[] = $serverInfo;
-               }
-
-               return $servers;
-       }
-
-       /**
-        * Take a group load array indexed by group then server, and reindex it by server then group
-        * @param array $groupLoads
-        * @return array
-        */
-       private function reindexGroupLoads( $groupLoads ) {
-               $reindexed = [];
-               foreach ( $groupLoads as $group => $loads ) {
-                       foreach ( $loads as $server => $load ) {
-                               $reindexed[$server][$group] = $load;
-                       }
-               }
-
-               return $reindexed;
-       }
-
-       /**
-        * Get the database name and prefix based on the wiki ID
-        * @param bool|string $wiki
-        * @return array
-        */
-       private function getDBNameAndPrefix( $wiki = false ) {
-               if ( $wiki === false ) {
-                       global $wgDBname, $wgDBprefix;
-
-                       return [ $wgDBname, $wgDBprefix ];
-               } else {
-                       return wfSplitWikiID( $wiki );
-               }
-       }
-
-       /**
-        * 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 = [] ) {
-               foreach ( $this->mainLBs as $lb ) {
-                       call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
-               }
-               foreach ( $this->extLBs as $lb ) {
-                       call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
-               }
-       }
-}
diff --git a/includes/db/loadbalancer/LBFactorySimple.php b/includes/db/loadbalancer/LBFactorySimple.php
deleted file mode 100644 (file)
index 09533eb..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-<?php
-/**
- * Generator 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
- */
-
-/**
- * A simple single-master LBFactory that gets its configuration from the b/c globals
- */
-class LBFactorySimple extends LBFactoryMW {
-       /** @var LoadBalancer */
-       private $mainLB;
-       /** @var LoadBalancer[] */
-       private $extLBs = [];
-
-       /** @var string */
-       private $loadMonitorClass;
-
-       public function __construct( array $conf ) {
-               parent::__construct( $conf );
-
-               $this->loadMonitorClass = isset( $conf['loadMonitorClass'] )
-                       ? $conf['loadMonitorClass']
-                       : null;
-       }
-
-       /**
-        * @param bool|string $wiki
-        * @return LoadBalancer
-        */
-       public function newMainLB( $wiki = false ) {
-               global $wgDBservers, $wgDBprefix, $wgDBmwschema;
-
-               if ( is_array( $wgDBservers ) ) {
-                       $servers = $wgDBservers;
-                       foreach ( $servers as $i => &$server ) {
-                               if ( $i == 0 ) {
-                                       $server['master'] = true;
-                               } else {
-                                       $server['replica'] = true;
-                               }
-                               $server += [
-                                       'schema' => $wgDBmwschema,
-                                       'tablePrefix' => $wgDBprefix,
-                                       'flags' => DBO_DEFAULT
-                               ];
-                       }
-               } else {
-                       global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
-                       global $wgDBssl, $wgDBcompress;
-
-                       $flags = DBO_DEFAULT;
-                       if ( $wgDebugDumpSql ) {
-                               $flags |= DBO_DEBUG;
-                       }
-                       if ( $wgDBssl ) {
-                               $flags |= DBO_SSL;
-                       }
-                       if ( $wgDBcompress ) {
-                               $flags |= DBO_COMPRESS;
-                       }
-
-                       $servers = [ [
-                               'host' => $wgDBserver,
-                               'user' => $wgDBuser,
-                               'password' => $wgDBpassword,
-                               'dbname' => $wgDBname,
-                               'schema' => $wgDBmwschema,
-                               'tablePrefix' => $wgDBprefix,
-                               'type' => $wgDBtype,
-                               'load' => 1,
-                               'flags' => $flags,
-                               'master' => true
-                       ] ];
-               }
-
-               return $this->newLoadBalancer( $servers );
-       }
-
-       /**
-        * @param bool|string $wiki
-        * @return LoadBalancer
-        */
-       public function getMainLB( $wiki = false ) {
-               if ( !isset( $this->mainLB ) ) {
-                       $this->mainLB = $this->newMainLB( $wiki );
-                       $this->chronProt->initLB( $this->mainLB );
-               }
-
-               return $this->mainLB;
-       }
-
-       /**
-        * @param string $cluster
-        * @param bool|string $wiki
-        * @return LoadBalancer
-        * @throws InvalidArgumentException
-        */
-       protected function newExternalLB( $cluster, $wiki = false ) {
-               global $wgExternalServers;
-               if ( !isset( $wgExternalServers[$cluster] ) ) {
-                       throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
-               }
-
-               return $this->newLoadBalancer( $wgExternalServers[$cluster] );
-       }
-
-       /**
-        * @param string $cluster
-        * @param bool|string $wiki
-        * @return array
-        */
-       public function getExternalLB( $cluster, $wiki = false ) {
-               if ( !isset( $this->extLBs[$cluster] ) ) {
-                       $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
-                       $this->chronProt->initLB( $this->extLBs[$cluster] );
-               }
-
-               return $this->extLBs[$cluster];
-       }
-
-       private function newLoadBalancer( array $servers ) {
-               $lb = new LoadBalancer( array_merge(
-                       $this->baseLoadBalancerParams(),
-                       [
-                               'servers' => $servers,
-                               'loadMonitor' => $this->loadMonitorClass,
-                       ]
-               ) );
-               $this->initLoadBalancer( $lb );
-
-               return $lb;
-       }
-
-       /**
-        * 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 = [] ) {
-               if ( isset( $this->mainLB ) ) {
-                       call_user_func_array( $callback, array_merge( [ $this->mainLB ], $params ) );
-               }
-               foreach ( $this->extLBs as $lb ) {
-                       call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
-               }
-       }
-}
diff --git a/includes/db/loadbalancer/LBFactorySingle.php b/includes/db/loadbalancer/LBFactorySingle.php
deleted file mode 100644 (file)
index 3937dfd..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-<?php
-/**
- * Simple generator of database connections that always returns the same object.
- *
- * 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 LBFactory class that always returns a single database object.
- */
-class LBFactorySingle extends LBFactory {
-       /** @var LoadBalancerSingle */
-       private $lb;
-
-       /**
-        * @param array $conf An associative array with one member:
-        *  - connection: The IDatabase connection object
-        */
-       public function __construct( array $conf ) {
-               parent::__construct( $conf );
-
-               $this->lb = new LoadBalancerSingle( array_merge( $this->baseLoadBalancerParams(), $conf ) );
-       }
-
-       /**
-        * @param bool|string $wiki
-        * @return LoadBalancerSingle
-        */
-       public function newMainLB( $wiki = false ) {
-               return $this->lb;
-       }
-
-       /**
-        * @param bool|string $wiki
-        * @return LoadBalancerSingle
-        */
-       public function getMainLB( $wiki = false ) {
-               return $this->lb;
-       }
-
-       /**
-        * @param string $cluster External storage cluster, or false for core
-        * @param bool|string $wiki Wiki ID, or false for the current wiki
-        * @return LoadBalancerSingle
-        */
-       protected function newExternalLB( $cluster, $wiki = false ) {
-               return $this->lb;
-       }
-
-       /**
-        * @param string $cluster External storage cluster, or false for core
-        * @param bool|string $wiki Wiki ID, or false for the current wiki
-        * @return LoadBalancerSingle
-        */
-       public function getExternalLB( $cluster, $wiki = false ) {
-               return $this->lb;
-       }
-
-       /**
-        * @param string|callable $callback
-        * @param array $params
-        */
-       public function forEachLB( $callback, array $params = [] ) {
-               call_user_func_array( $callback, array_merge( [ $this->lb ], $params ) );
-       }
-}
-
-/**
- * Helper class for LBFactorySingle.
- */
-class LoadBalancerSingle extends LoadBalancer {
-       /** @var IDatabase */
-       private $db;
-
-       /**
-        * @param array $params
-        */
-       public function __construct( array $params ) {
-               $this->db = $params['connection'];
-
-               parent::__construct( [
-                       'servers' => [
-                               [
-                                       'type' => $this->db->getType(),
-                                       'host' => $this->db->getServer(),
-                                       'dbname' => $this->db->getDBname(),
-                                       'load' => 1,
-                               ]
-                       ],
-                       'trxProfiler' => isset( $params['trxProfiler'] ) ? $params['trxProfiler'] : null,
-                       'srvCache' => isset( $params['srvCache'] ) ? $params['srvCache'] : null,
-                       'wanCache' => isset( $params['wanCache'] ) ? $params['wanCache'] : null
-               ] );
-
-               if ( isset( $params['readOnlyReason'] ) ) {
-                       $this->db->setLBInfo( 'readOnlyReason', $params['readOnlyReason'] );
-               }
-       }
-
-       /**
-        *
-        * @param string $server
-        * @param bool $dbNameOverride
-        *
-        * @return IDatabase
-        */
-       protected function reallyOpenConnection( $server, $dbNameOverride = false ) {
-               return $this->db;
-       }
-}
index 526b4ab..4d7c84d 100644 (file)
@@ -70,6 +70,14 @@ class LegacyLogger extends AbstractLogger {
                LogLevel::EMERGENCY => 600,
        ];
 
+       /**
+        * @var array
+        */
+       protected static $dbChannels = [
+               'DBQuery' => true,
+               'DBConnection' => true
+       ];
+
        /**
         * @param string $channel
         */
@@ -83,11 +91,31 @@ class LegacyLogger extends AbstractLogger {
         * @param string|int $level
         * @param string $message
         * @param array $context
+        * @return null
         */
        public function log( $level, $message, array $context = [] ) {
-               if ( self::shouldEmit( $this->channel, $message, $level, $context ) ) {
-                       $text = self::format( $this->channel, $message, $context );
-                       $destination = self::destination( $this->channel, $message, $context );
+               if ( is_string( $level ) ) {
+                       $level = self::$levelMapping[$level];
+               }
+               if ( $this->channel === 'DBQuery' && isset( $context['method'] )
+                       && isset( $context['master'] ) && isset( $context['runtime'] )
+               ) {
+                       MWDebug::query( $message, $context['method'], $context['master'], $context['runtime'] );
+                       return; // only send profiling data to MWDebug profiling
+               }
+
+               if ( isset( self::$dbChannels[$this->channel] )
+                       && $level >= self::$levelMapping[LogLevel::ERROR]
+               ) {
+                       // Format and write DB errors to the legacy locations
+                       $effectiveChannel = 'wfLogDBError';
+               } else {
+                       $effectiveChannel = $this->channel;
+               }
+
+               if ( self::shouldEmit( $effectiveChannel, $message, $level, $context ) ) {
+                       $text = self::format( $effectiveChannel, $message, $context );
+                       $destination = self::destination( $effectiveChannel, $message, $context );
                        self::emit( $text, $destination );
                }
                if ( !isset( $context['private'] ) || !$context['private'] ) {
@@ -101,7 +129,7 @@ class LegacyLogger extends AbstractLogger {
         *
         * @param string $channel
         * @param string $message
-        * @param string|int $level \Psr\Log\LogEvent constant or Monlog level int
+        * @param string|int $level \Psr\Log\LogEvent constant or Monolog level int
         * @param array $context
         * @return bool True if message should be sent to disk/network, false
         * otherwise
@@ -109,6 +137,10 @@ class LegacyLogger extends AbstractLogger {
        public static function shouldEmit( $channel, $message, $level, $context ) {
                global $wgDebugLogFile, $wgDBerrorLog, $wgDebugLogGroups;
 
+               if ( is_string( $level ) ) {
+                       $level = self::$levelMapping[$level];
+               }
+
                if ( $channel === 'wfLogDBError' ) {
                        // wfLogDBError messages are emitted if a database log location is
                        // specfied.
@@ -136,9 +168,6 @@ class LegacyLogger extends AbstractLogger {
                                }
 
                                if ( isset( $logConfig['level'] ) ) {
-                                       if ( is_string( $level ) ) {
-                                               $level = self::$levelMapping[$level];
-                                       }
                                        $shouldEmit = $level >= self::$levelMapping[$logConfig['level']];
                                }
                        } else {
@@ -298,6 +327,7 @@ class LegacyLogger extends AbstractLogger {
         * @param string $channel
         * @param string $message
         * @param array $context
+        * @return null
         */
        protected static function formatAsWfDebugLog( $channel, $message, $context ) {
                $time = wfTimestamp( TS_DB );
@@ -432,7 +462,6 @@ class LegacyLogger extends AbstractLogger {
        *
        * @param string $text
        * @param string $file Filename
-       * @throws MWException
        */
        public static function emit( $text, $file ) {
                if ( substr( $file, 0, 4 ) == 'udp:' ) {
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 4159166..93b3ef6 100644 (file)
@@ -108,87 +108,81 @@ class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
                        }
                }
 
-               // If using cascading deletes, we can skip some explicit deletes
-               if ( !$dbw->cascadingDeletes() ) {
-                       // Delete outgoing links
-                       $this->batchDeleteByPK(
-                               'pagelinks',
-                               [ 'pl_from' => $id ],
-                               [ 'pl_from', 'pl_namespace', 'pl_title' ],
-                               $batchSize
-                       );
-                       $this->batchDeleteByPK(
-                               'imagelinks',
-                               [ 'il_from' => $id ],
-                               [ 'il_from', 'il_to' ],
-                               $batchSize
-                       );
-                       $this->batchDeleteByPK(
-                               'categorylinks',
-                               [ 'cl_from' => $id ],
-                               [ 'cl_from', 'cl_to' ],
-                               $batchSize
-                       );
-                       $this->batchDeleteByPK(
-                               'templatelinks',
-                               [ 'tl_from' => $id ],
-                               [ 'tl_from', 'tl_namespace', 'tl_title' ],
-                               $batchSize
-                       );
-                       $this->batchDeleteByPK(
-                               'externallinks',
-                               [ 'el_from' => $id ],
-                               [ 'el_id' ],
-                               $batchSize
-                       );
-                       $this->batchDeleteByPK(
-                               'langlinks',
-                               [ 'll_from' => $id ],
-                               [ 'll_from', 'll_lang' ],
-                               $batchSize
-                       );
-                       $this->batchDeleteByPK(
-                               'iwlinks',
-                               [ 'iwl_from' => $id ],
-                               [ 'iwl_from', 'iwl_prefix', 'iwl_title' ],
-                               $batchSize
-                       );
-                       // Delete any redirect entry or page props entries
-                       $dbw->delete( 'redirect', [ 'rd_from' => $id ], __METHOD__ );
-                       $dbw->delete( 'page_props', [ 'pp_page' => $id ], __METHOD__ );
-               }
+               $this->batchDeleteByPK(
+                       'pagelinks',
+                       [ 'pl_from' => $id ],
+                       [ 'pl_from', 'pl_namespace', 'pl_title' ],
+                       $batchSize
+               );
+               $this->batchDeleteByPK(
+                       'imagelinks',
+                       [ 'il_from' => $id ],
+                       [ 'il_from', 'il_to' ],
+                       $batchSize
+               );
+               $this->batchDeleteByPK(
+                       'categorylinks',
+                       [ 'cl_from' => $id ],
+                       [ 'cl_from', 'cl_to' ],
+                       $batchSize
+               );
+               $this->batchDeleteByPK(
+                       'templatelinks',
+                       [ 'tl_from' => $id ],
+                       [ 'tl_from', 'tl_namespace', 'tl_title' ],
+                       $batchSize
+               );
+               $this->batchDeleteByPK(
+                       'externallinks',
+                       [ 'el_from' => $id ],
+                       [ 'el_id' ],
+                       $batchSize
+               );
+               $this->batchDeleteByPK(
+                       'langlinks',
+                       [ 'll_from' => $id ],
+                       [ 'll_from', 'll_lang' ],
+                       $batchSize
+               );
+               $this->batchDeleteByPK(
+                       'iwlinks',
+                       [ 'iwl_from' => $id ],
+                       [ 'iwl_from', 'iwl_prefix', 'iwl_title' ],
+                       $batchSize
+               );
 
-               // If using cleanup triggers, we can skip some manual deletes
-               if ( !$dbw->cleanupTriggers() ) {
-                       // Find recentchanges entries to clean up...
-                       $rcIdsForTitle = $dbw->selectFieldValues(
-                               'recentchanges',
-                               'rc_id',
-                               [
-                                       'rc_type != ' . RC_LOG,
-                                       'rc_namespace' => $title->getNamespace(),
-                                       'rc_title' => $title->getDBkey(),
-                                       'rc_timestamp < ' .
-                                               $dbw->addQuotes( $dbw->timestamp( $this->timestamp ) )
-                               ],
-                               __METHOD__
-                       );
-                       $rcIdsForPage = $dbw->selectFieldValues(
-                               'recentchanges',
-                               'rc_id',
-                               [ 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ],
-                               __METHOD__
-                       );
+               // Delete any redirect entry or page props entries
+               $dbw->delete( 'redirect', [ 'rd_from' => $id ], __METHOD__ );
+               $dbw->delete( 'page_props', [ 'pp_page' => $id ], __METHOD__ );
+
+               // Find recentchanges entries to clean up...
+               $rcIdsForTitle = $dbw->selectFieldValues(
+                       'recentchanges',
+                       'rc_id',
+                       [
+                               'rc_type != ' . RC_LOG,
+                               'rc_namespace' => $title->getNamespace(),
+                               'rc_title' => $title->getDBkey(),
+                               'rc_timestamp < ' .
+                                       $dbw->addQuotes( $dbw->timestamp( $this->timestamp ) )
+                       ],
+                       __METHOD__
+               );
+               $rcIdsForPage = $dbw->selectFieldValues(
+                       'recentchanges',
+                       'rc_id',
+                       [ 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ],
+                       __METHOD__
+               );
 
-                       // T98706: delete by PK to avoid lock contention with RC delete log insertions
-                       $rcIdBatches = array_chunk( array_merge( $rcIdsForTitle, $rcIdsForPage ), $batchSize );
-                       foreach ( $rcIdBatches as $rcIdBatch ) {
-                               $dbw->delete( 'recentchanges', [ 'rc_id' => $rcIdBatch ], __METHOD__ );
-                               if ( count( $rcIdBatches ) > 1 ) {
-                                       $lbFactory->commitAndWaitForReplication(
-                                               __METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
-                                       );
-                               }
+               // T98706: delete by PK to avoid lock contention with RC delete log insertions
+               $rcIdBatches = array_chunk( array_merge( $rcIdsForTitle, $rcIdsForPage ), $batchSize );
+               foreach ( $rcIdBatches as $rcIdBatch ) {
+                       $dbw->delete( 'recentchanges', [ 'rc_id' => $rcIdBatch ], __METHOD__ );
+                       if ( count( $rcIdBatches ) > 1 ) {
+                               $lbFactory->commitAndWaitForReplication(
+                                       __METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
+                               );
                        }
                }
 
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__
                );
index 1537535..cd644cb 100644 (file)
@@ -609,7 +609,7 @@ class DifferenceEngine extends ContextSource {
                                // This needs to be synchronised with Article::showCssOrJsPage(), which sucks
                                // Give hooks a chance to customise the output
                                // @todo standardize this crap into one function
-                               if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
+                               if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', [ $this->mNewContent, $this->mNewPage, $out ], '1.24' ) ) {
                                        // NOTE: deprecated hook, B/C only
                                        // use the content object's own rendering
                                        $cnt = $this->mNewRev->getContent();
@@ -620,7 +620,11 @@ class DifferenceEngine extends ContextSource {
                                }
                        } elseif ( !Hooks::run( 'ArticleContentViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
                                // Handled by extension
-                       } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', [ $this->mNewContent, $this->mNewPage, $out ] ) ) {
+                       } elseif ( !ContentHandler::runLegacyHooks(
+                               'ArticleViewCustom',
+                               [ $this->mNewContent, $this->mNewPage, $out ],
+                               '1.21'
+                       ) ) {
                                // NOTE: deprecated hook, B/C only
                                // Handled by extension
                        } else {
index 5496cb6..e958c94 100644 (file)
@@ -199,7 +199,26 @@ class MWException extends Exception {
         * It will be either HTML or plain text based on isCommandLine().
         */
        public function report() {
-               MWExceptionRenderer::output( $this, MWExceptionRenderer::AS_PRETTY );
+               global $wgMimeType;
+
+               if ( defined( 'MW_API' ) ) {
+                       // Unhandled API exception, we can't be sure that format printer is alive
+                       self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $this ) );
+                       wfHttpError( 500, 'Internal Server Error', $this->getText() );
+               } elseif ( self::isCommandLine() ) {
+                       $message = $this->getText();
+                       // T17602: STDERR may not be available
+                       if ( defined( 'STDERR' ) ) {
+                               fwrite( STDERR, $message );
+                       } else {
+                               echo $message;
+                       }
+               } else {
+                       self::statusHeader( 500 );
+                       self::header( "Content-Type: $wgMimeType; charset=utf-8" );
+
+                       $this->reportHTML();
+               }
        }
 
        /**
index 8359846..4a1f190 100644 (file)
@@ -62,12 +62,19 @@ class MWExceptionHandler {
        protected static function report( $e ) {
                try {
                        // Try and show the exception prettily, with the normal skin infrastructure
-                       MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY );
+                       if ( $e instanceof MWException ) {
+                               // Delegate to MWException until all subclasses are handled by
+                               // MWExceptionRenderer and MWException::report() has been
+                               // removed.
+                               $e->report();
+                       } else {
+                               MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY );
+                       }
                } catch ( Exception $e2 ) {
                        // Exception occurred from within exception handler
                        // Show a simpler message for the original exception,
                        // don't try to invoke report()
-                       MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY, $e2 );
+                       MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW, $e2 );
                }
        }
 
index bb7a01f..8fdc417 100644 (file)
@@ -28,18 +28,13 @@ class MWExceptionRenderer {
        const AS_PRETTY = 2; // show as HTML
 
        /**
-        * @param Exception $e Original exception
+        * @param Exception|Throwable $e Original exception
         * @param integer $mode MWExceptionExposer::AS_* constant
-        * @param Exception|null $eNew New exception from attempting to show the first
+        * @param Exception|Throwable|null $eNew New exception from attempting to show the first
         */
-       public static function output( Exception $e, $mode, Exception $eNew = null ) {
+       public static function output( $e, $mode, $eNew = null ) {
                global $wgMimeType;
 
-               if ( $e instanceof DBConnectionError ) {
-                       self::reportOutageHTML( $e );
-                       return;
-               }
-
                if ( defined( 'MW_API' ) ) {
                        // Unhandled API exception, we can't be sure that format printer is alive
                        self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $e ) );
@@ -47,9 +42,13 @@ class MWExceptionRenderer {
                } elseif ( self::isCommandLine() ) {
                        self::printError( self::getText( $e ) );
                } elseif ( $mode === self::AS_PRETTY ) {
-                       self::statusHeader( 500 );
-                       self::header( "Content-Type: $wgMimeType; charset=utf-8" );
-                       self::reportHTML( $e );
+                       if ( $e instanceof DBConnectionError ) {
+                               self::reportOutageHTML( $e );
+                       } else {
+                               self::statusHeader( 500 );
+                               self::header( "Content-Type: $wgMimeType; charset=utf-8" );
+                               self::reportHTML( $e );
+                       }
                } else {
                        if ( $eNew ) {
                                $message = "MediaWiki internal error.\n\n";
@@ -88,12 +87,12 @@ class MWExceptionRenderer {
         *
         * Called by MWException for b/c
         *
-        * @param Exception $e
+        * @param Exception|Throwable $e
         * @param string $name Class name of the exception
         * @param array $args Arguments to pass to the callback functions
         * @return string|null String to output or null if any hook has been called
         */
-       public static function runHooks( Exception $e, $name, $args = [] ) {
+       public static function runHooks( $e, $name, $args = [] ) {
                global $wgExceptionHooks;
 
                if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) {
@@ -129,10 +128,10 @@ class MWExceptionRenderer {
        }
 
        /**
-        * @param Exception $e
+        * @param Exception|Throwable $e
         * @return bool Should the exception use $wgOut to output the error?
         */
-       private static function useOutputPage( Exception $e ) {
+       private static function useOutputPage( $e ) {
                // Can the extension use the Message class/wfMessage to get i18n-ed messages?
                foreach ( $e->getTrace() as $frame ) {
                        if ( isset( $frame['class'] ) && $frame['class'] === 'LocalisationCache' ) {
@@ -150,9 +149,9 @@ class MWExceptionRenderer {
        /**
         * Output the exception report using HTML
         *
-        * @param Exception $e
+        * @param Exception|Throwable $e
         */
-       private static function reportHTML( Exception $e ) {
+       private static function reportHTML( $e ) {
                global $wgOut, $wgSitename;
 
                if ( self::useOutputPage( $e ) ) {
@@ -172,7 +171,7 @@ class MWExceptionRenderer {
                        } else {
                                // Show any custom GUI message before the details
                                if ( $e instanceof MessageSpecifier ) {
-                                       $wgOut->addHtml( Message::newFromSpecifier( $e )->escaped() );
+                                       $wgOut->addHTML( Message::newFromSpecifier( $e )->escaped() );
                                }
                                $wgOut->addHTML( self::getHTML( $e ) );
                        }
@@ -206,10 +205,10 @@ class MWExceptionRenderer {
         * backtrace to the error, otherwise show a message to ask to set it to true
         * to show that information.
         *
-        * @param Exception $e
+        * @param Exception|Throwable $e
         * @return string Html to output
         */
-       public static function getHTML( Exception $e ) {
+       public static function getHTML( $e ) {
                if ( self::showBackTrace( $e ) ) {
                        $html = "<div class=\"errorbox\"><p>" .
                                nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $e ) ) ) .
@@ -254,10 +253,10 @@ class MWExceptionRenderer {
        }
 
        /**
-        * @param Exception $e
+        * @param Exception|Throwable $e
         * @return string
         */
-       private function getText( Exception $e ) {
+       private static function getText( $e ) {
                if ( self::showBackTrace( $e ) ) {
                        return MWExceptionHandler::getLogMessage( $e ) .
                                "\nBacktrace:\n" .
@@ -269,10 +268,10 @@ class MWExceptionRenderer {
        }
 
        /**
-        * @param Exception $e
+        * @param Exception|Throwable $e
         * @return bool
         */
-       private static function showBackTrace( Exception $e ) {
+       private static function showBackTrace( $e ) {
                global $wgShowExceptionDetails, $wgShowDBErrorBacktrace;
 
                return (
@@ -324,9 +323,9 @@ class MWExceptionRenderer {
        }
 
        /**
-        * @param Exception $e
+        * @param Exception|Throwable $e
         */
-       private static function reportOutageHTML( Exception $e ) {
+       private static function reportOutageHTML( $e ) {
                global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors;
 
                $sorry = htmlspecialchars( self::msg(
index 2eae279..7e93299 100644 (file)
@@ -130,7 +130,7 @@ class ExternalStoreDB extends ExternalStoreMedium {
                        wfDebug( "writable external store\n" );
                }
 
-               $db = $lb->getConnection( DB_REPLICA, [], $wiki );
+               $db = $lb->getConnectionRef( DB_REPLICA, [], $wiki );
                $db->clearFlag( DBO_TRX ); // sanity
 
                return $db;
@@ -146,7 +146,7 @@ class ExternalStoreDB extends ExternalStoreMedium {
                $wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
                $lb = $this->getLoadBalancer( $cluster );
 
-               $db = $lb->getConnection( DB_MASTER, [], $wiki );
+               $db = $lb->getConnectionRef( DB_MASTER, [], $wiki );
                $db->clearFlag( DBO_TRX ); // sanity
 
                return $db;
diff --git a/includes/filebackend/FSFile.php b/includes/filebackend/FSFile.php
deleted file mode 100644 (file)
index 8aa11b6..0000000
+++ /dev/null
@@ -1,280 +0,0 @@
-<?php
-/**
- * Non-directory file on the file system.
- *
- * 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
- */
-
-/**
- * Class representing a non-directory file on the file system
- *
- * @ingroup FileBackend
- */
-class FSFile {
-       /** @var string Path to file */
-       protected $path;
-
-       /** @var string File SHA-1 in base 36 */
-       protected $sha1Base36;
-
-       /**
-        * Sets up the file object
-        *
-        * @param string $path Path to temporary file on local disk
-        */
-       public function __construct( $path ) {
-               $this->path = $path;
-       }
-
-       /**
-        * Returns the file system path
-        *
-        * @return string
-        */
-       public function getPath() {
-               return $this->path;
-       }
-
-       /**
-        * Checks if the file exists
-        *
-        * @return bool
-        */
-       public function exists() {
-               return is_file( $this->path );
-       }
-
-       /**
-        * Get the file size in bytes
-        *
-        * @return int|bool
-        */
-       public function getSize() {
-               return filesize( $this->path );
-       }
-
-       /**
-        * Get the file's last-modified timestamp
-        *
-        * @return string|bool TS_MW timestamp or false on failure
-        */
-       public function getTimestamp() {
-               MediaWiki\suppressWarnings();
-               $timestamp = filemtime( $this->path );
-               MediaWiki\restoreWarnings();
-               if ( $timestamp !== false ) {
-                       $timestamp = wfTimestamp( TS_MW, $timestamp );
-               }
-
-               return $timestamp;
-       }
-
-       /**
-        * Guess the MIME type from the file contents alone
-        *
-        * @return string
-        */
-       public function getMimeType() {
-               return MimeMagic::singleton()->guessMimeType( $this->path, false );
-       }
-
-       /**
-        * Get an associative array containing information about
-        * a file with the given storage path.
-        *
-        * Resulting array fields include:
-        *   - fileExists
-        *   - size (filesize in bytes)
-        *   - mime (as major/minor)
-        *   - media_type (value to be used with the MEDIATYPE_xxx constants)
-        *   - metadata (handler specific)
-        *   - sha1 (in base 36)
-        *   - width
-        *   - height
-        *   - bits (bitrate)
-        *   - file-mime
-        *   - major_mime
-        *   - minor_mime
-        *
-        * @param string|bool $ext The file extension, or true to extract it from the filename.
-        *             Set it to false to ignore the extension.
-        * @return array
-        */
-       public function getProps( $ext = true ) {
-               wfDebug( __METHOD__ . ": Getting file info for $this->path\n" );
-
-               $info = self::placeholderProps();
-               $info['fileExists'] = $this->exists();
-
-               if ( $info['fileExists'] ) {
-                       $magic = MimeMagic::singleton();
-
-                       # get the file extension
-                       if ( $ext === true ) {
-                               $ext = self::extensionFromPath( $this->path );
-                       }
-
-                       # MIME type according to file contents
-                       $info['file-mime'] = $this->getMimeType();
-                       # logical MIME type
-                       $info['mime'] = $magic->improveTypeFromExtension( $info['file-mime'], $ext );
-
-                       list( $info['major_mime'], $info['minor_mime'] ) = File::splitMime( $info['mime'] );
-                       $info['media_type'] = $magic->getMediaType( $this->path, $info['mime'] );
-
-                       # Get size in bytes
-                       $info['size'] = $this->getSize();
-
-                       # Height, width and metadata
-                       $handler = MediaHandler::getHandler( $info['mime'] );
-                       if ( $handler ) {
-                               $tempImage = (object)[]; // XXX (hack for File object)
-                               $info['metadata'] = $handler->getMetadata( $tempImage, $this->path );
-                               $gis = $handler->getImageSize( $tempImage, $this->path, $info['metadata'] );
-                               if ( is_array( $gis ) ) {
-                                       $info = $this->extractImageSizeInfo( $gis ) + $info;
-                               }
-                       }
-                       $info['sha1'] = $this->getSha1Base36();
-
-                       wfDebug( __METHOD__ . ": $this->path loaded, {$info['size']} bytes, {$info['mime']}.\n" );
-               } else {
-                       wfDebug( __METHOD__ . ": $this->path NOT FOUND!\n" );
-               }
-
-               return $info;
-       }
-
-       /**
-        * Placeholder file properties to use for files that don't exist
-        *
-        * Resulting array fields include:
-        *   - fileExists
-        *   - mime (as major/minor)
-        *   - media_type (value to be used with the MEDIATYPE_xxx constants)
-        *   - metadata (handler specific)
-        *   - sha1 (in base 36)
-        *   - width
-        *   - height
-        *   - bits (bitrate)
-        *
-        * @return array
-        */
-       public static function placeholderProps() {
-               $info = [];
-               $info['fileExists'] = false;
-               $info['mime'] = null;
-               $info['media_type'] = MEDIATYPE_UNKNOWN;
-               $info['metadata'] = '';
-               $info['sha1'] = '';
-               $info['width'] = 0;
-               $info['height'] = 0;
-               $info['bits'] = 0;
-
-               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.
-        *
-        * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
-        * fairly neatly.
-        *
-        * @param bool $recache
-        * @return bool|string False on failure
-        */
-       public function getSha1Base36( $recache = false ) {
-               if ( $this->sha1Base36 !== null && !$recache ) {
-                       return $this->sha1Base36;
-               }
-
-               MediaWiki\suppressWarnings();
-               $this->sha1Base36 = sha1_file( $this->path );
-               MediaWiki\restoreWarnings();
-
-               if ( $this->sha1Base36 !== false ) {
-                       $this->sha1Base36 = Wikimedia\base_convert( $this->sha1Base36, 16, 36, 31 );
-               }
-
-               return $this->sha1Base36;
-       }
-
-       /**
-        * Get the final file extension from a file system path
-        *
-        * @param string $path
-        * @return string
-        */
-       public static function extensionFromPath( $path ) {
-               $i = strrpos( $path, '.' );
-
-               return strtolower( $i ? substr( $path, $i + 1 ) : '' );
-       }
-
-       /**
-        * Get an associative array containing information about a file in the local filesystem.
-        *
-        * @param string $path Absolute local filesystem path
-        * @param string|bool $ext The file extension, or true to extract it from the filename.
-        *   Set it to false to ignore the extension.
-        * @return array
-        */
-       public static function getPropsFromPath( $path, $ext = true ) {
-               $fsFile = new self( $path );
-
-               return $fsFile->getProps( $ext );
-       }
-
-       /**
-        * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
-        * encoding, zero padded to 31 digits.
-        *
-        * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
-        * fairly neatly.
-        *
-        * @param string $path
-        * @return bool|string False on failure
-        */
-       public static function getSha1Base36FromPath( $path ) {
-               $fsFile = new self( $path );
-
-               return $fsFile->getSha1Base36();
-       }
-}
diff --git a/includes/filebackend/FSFileBackend.php b/includes/filebackend/FSFileBackend.php
deleted file mode 100644 (file)
index efe78ee..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.
- *
- * Status 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 = Status::newGood();
-
-               $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' );
-                       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, Status $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 = Status::newGood();
-
-               $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, Status $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 = Status::newGood();
-
-               $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, Status $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 = Status::newGood();
-
-               $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, Status $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 = Status::newGood();
-
-               $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, Status $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 Status
-        */
-       protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
-               $status = Status::newGood();
-               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 = Status::newGood();
-               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 = Status::newGood();
-               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 = Status::newGood();
-               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 );
-                               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 Status[]
-        */
-       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 = Status::newGood();
-                       $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;
-                       }
-               }
-       }
-}
diff --git a/includes/filebackend/FileBackend.php b/includes/filebackend/FileBackend.php
deleted file mode 100644 (file)
index ed36a1f..0000000
+++ /dev/null
@@ -1,1551 +0,0 @@
-<?php
-/**
- * @defgroup FileBackend File backend
- *
- * File backend is used to interact with file storage systems,
- * such as the local file system, NFS, or cloud storage systems.
- */
-
-/**
- * Base class for all file 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 Base class for all file backend classes (including multi-write backends).
- *
- * This class defines the methods as abstract that subclasses must implement.
- * 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 "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.
- * For legacy reasons, the FSFileBackend class allows manually setting the paths of
- * containers to ones that do not respect the "wiki 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
- * directory files; a regular file cannot have the same name as a directory. Other
- * backends with virtual directories may not have this limitation. Callers should
- * store files in such a way that no files and directories are under the same path.
- *
- * In general, this class allows for callers to access storage through the same
- * interface, without regard to the underlying storage system. However, calling code
- * must follow certain patterns and be aware of certain things to ensure compatibility:
- *   - a) Always call prepare() on the parent directory before trying to put a file there;
- *        key/value stores only need the container to exist first, but filesystems need
- *        all the parent directories to exist first (prepare() is aware of all this)
- *   - b) Always call clean() on a directory when it might become empty to avoid empty
- *        directory buildup on filesystems; key/value stores never have empty directories,
- *        so doing this helps preserve consistency in both cases
- *   - c) Likewise, do not rely on the existence of empty directories for anything;
- *        calling directoryExists() on a path that prepare() was previously called on
- *        will return false for key/value stores if there are no files under that path
- *   - d) Never alter the resulting FSFile returned from getLocalReference(), as it could
- *        either be a copy of the source file in /tmp or the original source file itself
- *   - e) Use a file layout that results in never attempting to store files over directories
- *        or directories over files; key/value stores allow this but filesystems do not
- *   - f) Use ASCII file names (e.g. base32, IDs, hashes) to avoid Unicode issues in Windows
- *   - g) Do not assume that move operations are atomic (difficult with key/value stores)
- *   - h) Do not assume that file stat or read operations always have immediate consistency;
- *        various methods have a "latest" flag that should always be used if up-to-date
- *        information is required (this trades performance for correctness as needed)
- *   - i) Do not assume that directory listings have immediate consistency
- *
- * Methods of subclasses should avoid throwing exceptions at all costs.
- * As a corollary, external dependencies should be kept to a minimum.
- *
- * @ingroup FileBackend
- * @since 1.19
- */
-abstract class FileBackend {
-       /** @var string Unique backend name */
-       protected $name;
-
-       /** @var string Unique wiki name */
-       protected $wikiId;
-
-       /** @var string Read-only explanation message */
-       protected $readOnly;
-
-       /** @var string When to do operations in parallel */
-       protected $parallelize;
-
-       /** @var int How many operations can be done in parallel */
-       protected $concurrency;
-
-       /** @var LockManager */
-       protected $lockManager;
-
-       /** @var FileJournal */
-       protected $fileJournal;
-
-       /** Bitfield flags for supported features */
-       const ATTR_HEADERS = 1; // files can be tagged with standard HTTP headers
-       const ATTR_METADATA = 2; // files can be stored with metadata key/values
-       const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
-
-       /**
-        * Create a new backend instance from configuration.
-        * 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.
-        *   - lockManager : LockManager object to use for any file locking.
-        *                   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.
-        *   - parallelize : When to do file operations in parallel (when possible).
-        *                   Allowed values are "implicit", "explicit" and "off".
-        *   - concurrency : How many file operations can be done in parallel.
-        * @throws FileBackendException
-        */
-       public function __construct( array $config ) {
-               $this->name = $config['name'];
-               $this->wikiId = $config['wikiId']; // e.g. "my_wiki-en_"
-               if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) {
-                       throw new FileBackendException( "Backend name '{$this->name}' is invalid." );
-               } elseif ( !is_string( $this->wikiId ) ) {
-                       throw new FileBackendException( "Backend wiki ID not provided for '{$this->name}'." );
-               }
-               $this->lockManager = isset( $config['lockManager'] )
-                       ? $config['lockManager']
-                       : new NullLockManager( [] );
-               $this->fileJournal = isset( $config['fileJournal'] )
-                       ? $config['fileJournal']
-                       : FileJournal::factory( [ 'class' => 'NullFileJournal' ], $this->name );
-               $this->readOnly = isset( $config['readOnly'] )
-                       ? (string)$config['readOnly']
-                       : '';
-               $this->parallelize = isset( $config['parallelize'] )
-                       ? (string)$config['parallelize']
-                       : 'off';
-               $this->concurrency = isset( $config['concurrency'] )
-                       ? (int)$config['concurrency']
-                       : 50;
-       }
-
-       /**
-        * Get the unique backend name.
-        * We may have multiple different backends of the same type.
-        * For example, we can have two Swift backends using different proxies.
-        *
-        * @return string
-        */
-       final public function getName() {
-               return $this->name;
-       }
-
-       /**
-        * Get the wiki identifier used for this backend (possibly empty).
-        * Note that this might *not* be in the same format as wfWikiID().
-        *
-        * @return string
-        * @since 1.20
-        */
-       final public function getWikiId() {
-               return $this->wikiId;
-       }
-
-       /**
-        * Check if this backend is read-only
-        *
-        * @return bool
-        */
-       final public function isReadOnly() {
-               return ( $this->readOnly != '' );
-       }
-
-       /**
-        * Get an explanatory message if this backend is read-only
-        *
-        * @return string|bool Returns false if the backend is not read-only
-        */
-       final public function getReadOnlyReason() {
-               return ( $this->readOnly != '' ) ? $this->readOnly : false;
-       }
-
-       /**
-        * Get the a bitfield of extra features supported by the backend medium
-        *
-        * @return int Bitfield of FileBackend::ATTR_* flags
-        * @since 1.23
-        */
-       public function getFeatures() {
-               return self::ATTR_UNICODE_PATHS;
-       }
-
-       /**
-        * Check if the backend medium supports a field of extra features
-        *
-        * @param int $bitfield Bitfield of FileBackend::ATTR_* flags
-        * @return bool
-        * @since 1.23
-        */
-       final public function hasFeatures( $bitfield ) {
-               return ( $this->getFeatures() & $bitfield ) === $bitfield;
-       }
-
-       /**
-        * This is the main entry point into the backend for write operations.
-        * Callers supply an ordered list of operations to perform as a transaction.
-        * Files will be locked, the stat cache cleared, and then the operations attempted.
-        * If any serious errors occur, all attempted operations will be rolled back.
-        *
-        * $ops is an array of arrays. The outer array holds a list of operations.
-        * Each inner array is a set of key value pairs that specify an operation.
-        *
-        * Supported operations and their parameters. The supported actions are:
-        *  - create
-        *  - store
-        *  - copy
-        *  - move
-        *  - delete
-        *  - describe (since 1.21)
-        *  - null
-        *
-        * FSFile/TempFSFile object support was added in 1.27.
-        *
-        * a) Create a new file in storage with the contents of a string
-        * @code
-        *     [
-        *         'op'                  => 'create',
-        *         'dst'                 => <storage path>,
-        *         'content'             => <string of new file contents>,
-        *         'overwrite'           => <boolean>,
-        *         'overwriteSame'       => <boolean>,
-        *         'headers'             => <HTTP header name/value map> # since 1.21
-        *     ]
-        * @endcode
-        *
-        * b) Copy a file system file into storage
-        * @code
-        *     [
-        *         'op'                  => 'store',
-        *         'src'                 => <file system path, FSFile, or TempFSFile>,
-        *         'dst'                 => <storage path>,
-        *         'overwrite'           => <boolean>,
-        *         'overwriteSame'       => <boolean>,
-        *         'headers'             => <HTTP header name/value map> # since 1.21
-        *     ]
-        * @endcode
-        *
-        * c) Copy a file within storage
-        * @code
-        *     [
-        *         'op'                  => 'copy',
-        *         'src'                 => <storage path>,
-        *         'dst'                 => <storage path>,
-        *         'overwrite'           => <boolean>,
-        *         'overwriteSame'       => <boolean>,
-        *         'ignoreMissingSource' => <boolean>, # since 1.21
-        *         'headers'             => <HTTP header name/value map> # since 1.21
-        *     ]
-        * @endcode
-        *
-        * d) Move a file within storage
-        * @code
-        *     [
-        *         'op'                  => 'move',
-        *         'src'                 => <storage path>,
-        *         'dst'                 => <storage path>,
-        *         'overwrite'           => <boolean>,
-        *         'overwriteSame'       => <boolean>,
-        *         'ignoreMissingSource' => <boolean>, # since 1.21
-        *         'headers'             => <HTTP header name/value map> # since 1.21
-        *     ]
-        * @endcode
-        *
-        * e) Delete a file within storage
-        * @code
-        *     [
-        *         'op'                  => 'delete',
-        *         'src'                 => <storage path>,
-        *         'ignoreMissingSource' => <boolean>
-        *     ]
-        * @endcode
-        *
-        * f) Update metadata for a file within storage
-        * @code
-        *     [
-        *         'op'                  => 'describe',
-        *         'src'                 => <storage path>,
-        *         'headers'             => <HTTP header name/value map>
-        *     ]
-        * @endcode
-        *
-        * g) Do nothing (no-op)
-        * @code
-        *     [
-        *         'op'                  => 'null',
-        *     ]
-        * @endcode
-        *
-        * Boolean flags for operations (operation-specific):
-        *   - ignoreMissingSource : The operation will simply succeed and do
-        *                           nothing if the source file does not exist.
-        *   - overwrite           : Any destination file will be overwritten.
-        *   - overwriteSame       : If a file already exists at the destination with the
-        *                           same contents, then do nothing to the destination file
-        *                           instead of giving an error. This does not compare headers.
-        *                           This option is ignored if 'overwrite' is already provided.
-        *   - headers             : If supplied, the result of merging these headers with any
-        *                           existing source file headers (replacing conflicting ones)
-        *                           will be set as the destination file headers. Headers are
-        *                           deleted if their value is set to the empty string. When a
-        *                           file has headers they are included in responses to GET and
-        *                           HEAD requests to the backing store for that file.
-        *                           Header values should be no larger than 255 bytes, except for
-        *                           Content-Disposition. The system might ignore or truncate any
-        *                           headers that are too long to store (exact limits will vary).
-        *                           Backends that don't support metadata ignore this. (since 1.21)
-        *
-        * $opts is an associative of boolean flags, including:
-        *   - force               : Operation precondition errors no longer trigger an abort.
-        *                           Any remaining operations are still attempted. Unexpected
-        *                           failures may still cause remaining operations to be aborted.
-        *   - nonLocking          : No locks are acquired for the operations.
-        *                           This can increase performance for non-critical writes.
-        *                           This has no effect unless the 'force' flag is set.
-        *   - nonJournaled        : Don't log this operation batch in the file journal.
-        *                           This limits the ability of recovery scripts.
-        *   - parallelize         : Try to do operations in parallel when possible.
-        *   - bypassReadOnly      : Allow writes in read-only mode. (since 1.20)
-        *   - preserveCache       : Don't clear the process cache before checking files.
-        *                           This should only be used if all entries in the process
-        *                           cache were added after the files were already locked. (since 1.20)
-        *
-        * @remarks Remarks on locking:
-        * File system paths given to operations should refer to files that are
-        * already locked or otherwise safe from modification from other processes.
-        * Normally these files will be new temp files, which should be adequate.
-        *
-        * @par Return value:
-        *
-        * This returns a Status, which contains all warnings and fatals that occurred
-        * during the operation. The 'failCount', 'successCount', and 'success' members
-        * will reflect each operation attempted.
-        *
-        * The status will be "OK" unless:
-        *   - a) unexpected operation errors occurred (network partitions, disk full...)
-        *   - b) significant operation errors occurred and 'force' was not set
-        *
-        * @param array $ops List of operations to execute in order
-        * @param array $opts Batch operation options
-        * @return Status
-        */
-       final public function doOperations( array $ops, array $opts = [] ) {
-               if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
-                       return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
-               }
-               if ( !count( $ops ) ) {
-                       return Status::newGood(); // nothing to do
-               }
-
-               $ops = $this->resolveFSFileObjects( $ops );
-               if ( empty( $opts['force'] ) ) { // sanity
-                       unset( $opts['nonLocking'] );
-               }
-
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
-
-               return $this->doOperationsInternal( $ops, $opts );
-       }
-
-       /**
-        * @see FileBackend::doOperations()
-        * @param array $ops
-        * @param array $opts
-        */
-       abstract protected function doOperationsInternal( array $ops, array $opts );
-
-       /**
-        * Same as doOperations() except it takes a single operation.
-        * If you are doing a batch of operations that should either
-        * all succeed or all fail, then use that function instead.
-        *
-        * @see FileBackend::doOperations()
-        *
-        * @param array $op Operation
-        * @param array $opts Operation options
-        * @return Status
-        */
-       final public function doOperation( array $op, array $opts = [] ) {
-               return $this->doOperations( [ $op ], $opts );
-       }
-
-       /**
-        * Performs a single create operation.
-        * This sets $params['op'] to 'create' and passes it to doOperation().
-        *
-        * @see FileBackend::doOperation()
-        *
-        * @param array $params Operation parameters
-        * @param array $opts Operation options
-        * @return Status
-        */
-       final public function create( array $params, array $opts = [] ) {
-               return $this->doOperation( [ 'op' => 'create' ] + $params, $opts );
-       }
-
-       /**
-        * Performs a single store operation.
-        * This sets $params['op'] to 'store' and passes it to doOperation().
-        *
-        * @see FileBackend::doOperation()
-        *
-        * @param array $params Operation parameters
-        * @param array $opts Operation options
-        * @return Status
-        */
-       final public function store( array $params, array $opts = [] ) {
-               return $this->doOperation( [ 'op' => 'store' ] + $params, $opts );
-       }
-
-       /**
-        * Performs a single copy operation.
-        * This sets $params['op'] to 'copy' and passes it to doOperation().
-        *
-        * @see FileBackend::doOperation()
-        *
-        * @param array $params Operation parameters
-        * @param array $opts Operation options
-        * @return Status
-        */
-       final public function copy( array $params, array $opts = [] ) {
-               return $this->doOperation( [ 'op' => 'copy' ] + $params, $opts );
-       }
-
-       /**
-        * Performs a single move operation.
-        * This sets $params['op'] to 'move' and passes it to doOperation().
-        *
-        * @see FileBackend::doOperation()
-        *
-        * @param array $params Operation parameters
-        * @param array $opts Operation options
-        * @return Status
-        */
-       final public function move( array $params, array $opts = [] ) {
-               return $this->doOperation( [ 'op' => 'move' ] + $params, $opts );
-       }
-
-       /**
-        * Performs a single delete operation.
-        * This sets $params['op'] to 'delete' and passes it to doOperation().
-        *
-        * @see FileBackend::doOperation()
-        *
-        * @param array $params Operation parameters
-        * @param array $opts Operation options
-        * @return Status
-        */
-       final public function delete( array $params, array $opts = [] ) {
-               return $this->doOperation( [ 'op' => 'delete' ] + $params, $opts );
-       }
-
-       /**
-        * Performs a single describe operation.
-        * This sets $params['op'] to 'describe' and passes it to doOperation().
-        *
-        * @see FileBackend::doOperation()
-        *
-        * @param array $params Operation parameters
-        * @param array $opts Operation options
-        * @return Status
-        * @since 1.21
-        */
-       final public function describe( array $params, array $opts = [] ) {
-               return $this->doOperation( [ 'op' => 'describe' ] + $params, $opts );
-       }
-
-       /**
-        * Perform a set of independent file operations on some files.
-        *
-        * This does no locking, nor journaling, and possibly no stat calls.
-        * Any destination files that already exist will be overwritten.
-        * This should *only* be used on non-original files, like cache files.
-        *
-        * Supported operations and their parameters:
-        *  - create
-        *  - store
-        *  - copy
-        *  - move
-        *  - delete
-        *  - describe (since 1.21)
-        *  - null
-        *
-        * FSFile/TempFSFile object support was added in 1.27.
-        *
-        * a) Create a new file in storage with the contents of a string
-        * @code
-        *     [
-        *         'op'                  => 'create',
-        *         'dst'                 => <storage path>,
-        *         'content'             => <string of new file contents>,
-        *         'headers'             => <HTTP header name/value map> # since 1.21
-        *     ]
-        * @endcode
-        *
-        * b) Copy a file system file into storage
-        * @code
-        *     [
-        *         'op'                  => 'store',
-        *         'src'                 => <file system path, FSFile, or TempFSFile>,
-        *         'dst'                 => <storage path>,
-        *         'headers'             => <HTTP header name/value map> # since 1.21
-        *     ]
-        * @endcode
-        *
-        * c) Copy a file within storage
-        * @code
-        *     [
-        *         'op'                  => 'copy',
-        *         'src'                 => <storage path>,
-        *         'dst'                 => <storage path>,
-        *         'ignoreMissingSource' => <boolean>, # since 1.21
-        *         'headers'             => <HTTP header name/value map> # since 1.21
-        *     ]
-        * @endcode
-        *
-        * d) Move a file within storage
-        * @code
-        *     [
-        *         'op'                  => 'move',
-        *         'src'                 => <storage path>,
-        *         'dst'                 => <storage path>,
-        *         'ignoreMissingSource' => <boolean>, # since 1.21
-        *         'headers'             => <HTTP header name/value map> # since 1.21
-        *     ]
-        * @endcode
-        *
-        * e) Delete a file within storage
-        * @code
-        *     [
-        *         'op'                  => 'delete',
-        *         'src'                 => <storage path>,
-        *         'ignoreMissingSource' => <boolean>
-        *     ]
-        * @endcode
-        *
-        * f) Update metadata for a file within storage
-        * @code
-        *     [
-        *         'op'                  => 'describe',
-        *         'src'                 => <storage path>,
-        *         'headers'             => <HTTP header name/value map>
-        *     ]
-        * @endcode
-        *
-        * g) Do nothing (no-op)
-        * @code
-        *     [
-        *         'op'                  => 'null',
-        *     ]
-        * @endcode
-        *
-        * @par Boolean flags for operations (operation-specific):
-        *   - ignoreMissingSource : The operation will simply succeed and do
-        *                           nothing if the source file does not exist.
-        *   - headers             : If supplied with a header name/value map, the backend will
-        *                           reply with these headers when GETs/HEADs of the destination
-        *                           file are made. Header values should be smaller than 256 bytes.
-        *                           Content-Disposition headers can be longer, though the system
-        *                           might ignore or truncate ones that are too long to store.
-        *                           Existing headers will remain, but these will replace any
-        *                           conflicting previous headers, and headers will be removed
-        *                           if they are set to an empty string.
-        *                           Backends that don't support metadata ignore this. (since 1.21)
-        *
-        * $opts is an associative of boolean flags, including:
-        *   - bypassReadOnly      : Allow writes in read-only mode (since 1.20)
-        *
-        * @par Return value:
-        * This returns a Status, which contains all warnings and fatals that occurred
-        * during the operation. The 'failCount', 'successCount', and 'success' members
-        * will reflect each operation attempted for the given files. The status will be
-        * considered "OK" as long as no fatal errors occurred.
-        *
-        * @param array $ops Set of operations to execute
-        * @param array $opts Batch operation options
-        * @return Status
-        * @since 1.20
-        */
-       final public function doQuickOperations( array $ops, array $opts = [] ) {
-               if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
-                       return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
-               }
-               if ( !count( $ops ) ) {
-                       return Status::newGood(); // nothing to do
-               }
-
-               $ops = $this->resolveFSFileObjects( $ops );
-               foreach ( $ops as &$op ) {
-                       $op['overwrite'] = true; // avoids RTTs in key/value stores
-               }
-
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
-
-               return $this->doQuickOperationsInternal( $ops );
-       }
-
-       /**
-        * @see FileBackend::doQuickOperations()
-        * @param array $ops
-        * @since 1.20
-        */
-       abstract protected function doQuickOperationsInternal( array $ops );
-
-       /**
-        * Same as doQuickOperations() except it takes a single operation.
-        * If you are doing a batch of operations, then use that function instead.
-        *
-        * @see FileBackend::doQuickOperations()
-        *
-        * @param array $op Operation
-        * @return Status
-        * @since 1.20
-        */
-       final public function doQuickOperation( array $op ) {
-               return $this->doQuickOperations( [ $op ] );
-       }
-
-       /**
-        * Performs a single quick create operation.
-        * This sets $params['op'] to 'create' and passes it to doQuickOperation().
-        *
-        * @see FileBackend::doQuickOperation()
-        *
-        * @param array $params Operation parameters
-        * @return Status
-        * @since 1.20
-        */
-       final public function quickCreate( array $params ) {
-               return $this->doQuickOperation( [ 'op' => 'create' ] + $params );
-       }
-
-       /**
-        * Performs a single quick store operation.
-        * This sets $params['op'] to 'store' and passes it to doQuickOperation().
-        *
-        * @see FileBackend::doQuickOperation()
-        *
-        * @param array $params Operation parameters
-        * @return Status
-        * @since 1.20
-        */
-       final public function quickStore( array $params ) {
-               return $this->doQuickOperation( [ 'op' => 'store' ] + $params );
-       }
-
-       /**
-        * Performs a single quick copy operation.
-        * This sets $params['op'] to 'copy' and passes it to doQuickOperation().
-        *
-        * @see FileBackend::doQuickOperation()
-        *
-        * @param array $params Operation parameters
-        * @return Status
-        * @since 1.20
-        */
-       final public function quickCopy( array $params ) {
-               return $this->doQuickOperation( [ 'op' => 'copy' ] + $params );
-       }
-
-       /**
-        * Performs a single quick move operation.
-        * This sets $params['op'] to 'move' and passes it to doQuickOperation().
-        *
-        * @see FileBackend::doQuickOperation()
-        *
-        * @param array $params Operation parameters
-        * @return Status
-        * @since 1.20
-        */
-       final public function quickMove( array $params ) {
-               return $this->doQuickOperation( [ 'op' => 'move' ] + $params );
-       }
-
-       /**
-        * Performs a single quick delete operation.
-        * This sets $params['op'] to 'delete' and passes it to doQuickOperation().
-        *
-        * @see FileBackend::doQuickOperation()
-        *
-        * @param array $params Operation parameters
-        * @return Status
-        * @since 1.20
-        */
-       final public function quickDelete( array $params ) {
-               return $this->doQuickOperation( [ 'op' => 'delete' ] + $params );
-       }
-
-       /**
-        * Performs a single quick describe operation.
-        * This sets $params['op'] to 'describe' and passes it to doQuickOperation().
-        *
-        * @see FileBackend::doQuickOperation()
-        *
-        * @param array $params Operation parameters
-        * @return Status
-        * @since 1.21
-        */
-       final public function quickDescribe( array $params ) {
-               return $this->doQuickOperation( [ 'op' => 'describe' ] + $params );
-       }
-
-       /**
-        * Concatenate a list of storage files into a single file system file.
-        * The target path should refer to a file that is already locked or
-        * otherwise safe from modification from other processes. Normally,
-        * the file will be a new temp file, which should be adequate.
-        *
-        * @param array $params Operation parameters, include:
-        *   - srcs        : ordered source storage paths (e.g. chunk1, chunk2, ...)
-        *   - dst         : file system path to 0-byte temp file
-        *   - parallelize : try to do operations in parallel when possible
-        * @return Status
-        */
-       abstract public function concatenate( array $params );
-
-       /**
-        * Prepare a storage directory for usage.
-        * This will create any required containers and parent directories.
-        * Backends using key/value stores only need to create the container.
-        *
-        * The 'noAccess' and 'noListing' parameters works the same as in secure(),
-        * except they are only applied *if* the directory/container had to be created.
-        * These flags should always be set for directories that have private files.
-        * However, setting them is not guaranteed to actually do anything.
-        * Additional server configuration may be needed to achieve the desired effect.
-        *
-        * @param array $params Parameters include:
-        *   - dir            : storage directory
-        *   - noAccess       : try to deny file access (since 1.20)
-        *   - noListing      : try to deny file listing (since 1.20)
-        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
-        * @return Status
-        */
-       final public function prepare( array $params ) {
-               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
-                       return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
-               }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
-               return $this->doPrepare( $params );
-       }
-
-       /**
-        * @see FileBackend::prepare()
-        * @param array $params
-        */
-       abstract protected function doPrepare( array $params );
-
-       /**
-        * Take measures to block web access to a storage directory and
-        * the container it belongs to. FS backends might add .htaccess
-        * files whereas key/value store backends might revoke container
-        * access to the storage user representing end-users in web requests.
-        *
-        * This is not guaranteed to actually make files or listings publically hidden.
-        * Additional server configuration may be needed to achieve the desired effect.
-        *
-        * @param array $params Parameters include:
-        *   - dir            : storage directory
-        *   - noAccess       : try to deny file access
-        *   - noListing      : try to deny file listing
-        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
-        * @return Status
-        */
-       final public function secure( array $params ) {
-               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
-                       return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
-               }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
-               return $this->doSecure( $params );
-       }
-
-       /**
-        * @see FileBackend::secure()
-        * @param array $params
-        */
-       abstract protected function doSecure( array $params );
-
-       /**
-        * Remove measures to block web access to a storage directory and
-        * the container it belongs to. FS backends might remove .htaccess
-        * files whereas key/value store backends might grant container
-        * access to the storage user representing end-users in web requests.
-        * This essentially can undo the result of secure() calls.
-        *
-        * This is not guaranteed to actually make files or listings publically viewable.
-        * Additional server configuration may be needed to achieve the desired effect.
-        *
-        * @param array $params Parameters include:
-        *   - dir            : storage directory
-        *   - access         : try to allow file access
-        *   - listing        : try to allow file listing
-        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
-        * @return Status
-        * @since 1.20
-        */
-       final public function publish( array $params ) {
-               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
-                       return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
-               }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
-               return $this->doPublish( $params );
-       }
-
-       /**
-        * @see FileBackend::publish()
-        * @param array $params
-        */
-       abstract protected function doPublish( array $params );
-
-       /**
-        * Delete a storage directory if it is empty.
-        * Backends using key/value stores may do nothing unless the directory
-        * is that of an empty container, in which case it will be deleted.
-        *
-        * @param array $params Parameters include:
-        *   - dir            : storage directory
-        *   - recursive      : recursively delete empty subdirectories first (since 1.20)
-        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
-        * @return Status
-        */
-       final public function clean( array $params ) {
-               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
-                       return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
-               }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
-               return $this->doClean( $params );
-       }
-
-       /**
-        * @see FileBackend::clean()
-        * @param array $params
-        */
-       abstract protected function doClean( array $params );
-
-       /**
-        * Enter file operation scope.
-        * This just makes PHP ignore user aborts/disconnects until the return
-        * value leaves scope. This returns null and does nothing in CLI mode.
-        *
-        * @return ScopedCallback|null
-        */
-       final protected function getScopedPHPBehaviorForOps() {
-               if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
-                       $old = ignore_user_abort( true ); // avoid half-finished operations
-                       return new ScopedCallback( function () use ( $old ) {
-                               ignore_user_abort( $old );
-                       } );
-               }
-
-               return null;
-       }
-
-       /**
-        * Check if a file exists at a storage path in the backend.
-        * This returns false if only a directory exists at the path.
-        *
-        * @param array $params Parameters include:
-        *   - src    : source storage path
-        *   - latest : use the latest available data
-        * @return bool|null Returns null on failure
-        */
-       abstract public function fileExists( array $params );
-
-       /**
-        * Get the last-modified timestamp of the file at a storage path.
-        *
-        * @param array $params Parameters include:
-        *   - src    : source storage path
-        *   - latest : use the latest available data
-        * @return string|bool TS_MW timestamp or false on failure
-        */
-       abstract public function getFileTimestamp( array $params );
-
-       /**
-        * Get the contents of a file at a storage path in the backend.
-        * This should be avoided for potentially large files.
-        *
-        * @param array $params Parameters include:
-        *   - src    : source storage path
-        *   - latest : use the latest available data
-        * @return string|bool Returns false on failure
-        */
-       final public function getFileContents( array $params ) {
-               $contents = $this->getFileContentsMulti(
-                       [ 'srcs' => [ $params['src'] ] ] + $params );
-
-               return $contents[$params['src']];
-       }
-
-       /**
-        * Like getFileContents() except it takes an array of storage paths
-        * and returns a map of storage paths to strings (or null on failure).
-        * The map keys (paths) are in the same order as the provided list of paths.
-        *
-        * @see FileBackend::getFileContents()
-        *
-        * @param array $params Parameters include:
-        *   - srcs        : list of source storage paths
-        *   - latest      : use the latest available data
-        *   - parallelize : try to do operations in parallel when possible
-        * @return array Map of (path name => string or false on failure)
-        * @since 1.20
-        */
-       abstract public function getFileContentsMulti( array $params );
-
-       /**
-        * Get metadata about a file at a storage path in the backend.
-        * If the file does not exist, then this returns false.
-        * Otherwise, the result is an associative array that includes:
-        *   - headers  : map of HTTP headers used for GET/HEAD requests (name => value)
-        *   - metadata : map of file metadata (name => value)
-        * Metadata keys and headers names will be returned in all lower-case.
-        * Additional values may be included for internal use only.
-        *
-        * Use FileBackend::hasFeatures() to check how well this is supported.
-        *
-        * @param array $params
-        * $params include:
-        *   - src    : source storage path
-        *   - latest : use the latest available data
-        * @return array|bool Returns false on failure
-        * @since 1.23
-        */
-       abstract public function getFileXAttributes( array $params );
-
-       /**
-        * Get the size (bytes) of a file at a storage path in the backend.
-        *
-        * @param array $params Parameters include:
-        *   - src    : source storage path
-        *   - latest : use the latest available data
-        * @return int|bool Returns false on failure
-        */
-       abstract public function getFileSize( array $params );
-
-       /**
-        * Get quick information about a file at a storage path in the backend.
-        * If the file does not exist, then this returns false.
-        * Otherwise, the result is an associative array that includes:
-        *   - mtime  : the last-modified timestamp (TS_MW)
-        *   - size   : the file size (bytes)
-        * Additional values may be included for internal use only.
-        *
-        * @param array $params Parameters include:
-        *   - src    : source storage path
-        *   - latest : use the latest available data
-        * @return array|bool|null Returns null on failure
-        */
-       abstract public function getFileStat( array $params );
-
-       /**
-        * Get a SHA-1 hash of the file at a storage path in the backend.
-        *
-        * @param array $params Parameters include:
-        *   - src    : source storage path
-        *   - latest : use the latest available data
-        * @return string|bool Hash string or false on failure
-        */
-       abstract public function getFileSha1Base36( array $params );
-
-       /**
-        * Get the properties of the file at a storage path in the backend.
-        * This gives the result of FSFile::getProps() on a local copy of the file.
-        *
-        * @param array $params Parameters include:
-        *   - src    : source storage path
-        *   - latest : use the latest available data
-        * @return array Returns FSFile::placeholderProps() on failure
-        */
-       abstract public function getFileProps( array $params );
-
-       /**
-        * Stream the file at a storage path in the backend.
-        *
-        * If the file does not exists, an HTTP 404 error will be given.
-        * Appropriate HTTP headers (Status, Content-Type, Content-Length)
-        * will be sent if streaming began, while none will be sent otherwise.
-        * Implementations should flush the output buffer before sending data.
-        *
-        * @param array $params Parameters include:
-        *   - src      : source storage path
-        *   - headers  : list of additional HTTP headers to send if the file exists
-        *   - options  : HTTP request header map with lower case keys (since 1.28). Supports:
-        *                range             : format is "bytes=(\d*-\d*)"
-        *                if-modified-since : format is an HTTP date
-        *   - headless : only include the body (and headers from "headers") (since 1.28)
-        *   - latest   : use the latest available data
-        *   - allowOB  : preserve any output buffers (since 1.28)
-        * @return Status
-        */
-       abstract public function streamFile( array $params );
-
-       /**
-        * Returns a file system file, identical to the file at a storage path.
-        * The file returned is either:
-        *   - a) A local copy of the file at a storage path in the backend.
-        *        The temporary copy will have the same extension as the source.
-        *   - b) An original of the file at a storage path in the backend.
-        * Temporary files may be purged when the file object falls out of scope.
-        *
-        * Write operations should *never* be done on this file as some backends
-        * may do internal tracking or may be instances of FileBackendMultiWrite.
-        * In that latter case, there are copies of the file that must stay in sync.
-        * Additionally, further calls to this function may return the same file.
-        *
-        * @param array $params Parameters include:
-        *   - src    : source storage path
-        *   - latest : use the latest available data
-        * @return FSFile|null Returns null on failure
-        */
-       final public function getLocalReference( array $params ) {
-               $fsFiles = $this->getLocalReferenceMulti(
-                       [ 'srcs' => [ $params['src'] ] ] + $params );
-
-               return $fsFiles[$params['src']];
-       }
-
-       /**
-        * Like getLocalReference() except it takes an array of storage paths
-        * and returns a map of storage paths to FSFile objects (or null on failure).
-        * The map keys (paths) are in the same order as the provided list of paths.
-        *
-        * @see FileBackend::getLocalReference()
-        *
-        * @param array $params Parameters include:
-        *   - srcs        : list of source storage paths
-        *   - latest      : use the latest available data
-        *   - parallelize : try to do operations in parallel when possible
-        * @return array Map of (path name => FSFile or null on failure)
-        * @since 1.20
-        */
-       abstract public function getLocalReferenceMulti( array $params );
-
-       /**
-        * Get a local copy on disk of the file at a storage path in the backend.
-        * The temporary copy will have the same file extension as the source.
-        * Temporary files may be purged when the file object falls out of scope.
-        *
-        * @param array $params Parameters include:
-        *   - src    : source storage path
-        *   - latest : use the latest available data
-        * @return TempFSFile|null Returns null on failure
-        */
-       final public function getLocalCopy( array $params ) {
-               $tmpFiles = $this->getLocalCopyMulti(
-                       [ 'srcs' => [ $params['src'] ] ] + $params );
-
-               return $tmpFiles[$params['src']];
-       }
-
-       /**
-        * Like getLocalCopy() except it takes an array of storage paths and
-        * returns a map of storage paths to TempFSFile objects (or null on failure).
-        * The map keys (paths) are in the same order as the provided list of paths.
-        *
-        * @see FileBackend::getLocalCopy()
-        *
-        * @param array $params Parameters include:
-        *   - srcs        : list of source storage paths
-        *   - latest      : use the latest available data
-        *   - parallelize : try to do operations in parallel when possible
-        * @return array Map of (path name => TempFSFile or null on failure)
-        * @since 1.20
-        */
-       abstract public function getLocalCopyMulti( array $params );
-
-       /**
-        * Return an HTTP URL to a given file that requires no authentication to use.
-        * The URL may be pre-authenticated (via some token in the URL) and temporary.
-        * This will return null if the backend cannot make an HTTP URL for the file.
-        *
-        * This is useful for key/value stores when using scripts that seek around
-        * large files and those scripts (and the backend) support HTTP Range headers.
-        * Otherwise, one would need to use getLocalReference(), which involves loading
-        * the entire file on to local disk.
-        *
-        * @param array $params Parameters include:
-        *   - src : source storage path
-        *   - ttl : lifetime (seconds) if pre-authenticated; default is 1 day
-        * @return string|null
-        * @since 1.21
-        */
-       abstract public function getFileHttpUrl( array $params );
-
-       /**
-        * Check if a directory exists at a given storage path.
-        * Backends using key/value stores will check if the path is a
-        * virtual directory, meaning there are files under the given directory.
-        *
-        * Storage backends with eventual consistency might return stale data.
-        *
-        * @param array $params Parameters include:
-        *   - dir : storage directory
-        * @return bool|null Returns null on failure
-        * @since 1.20
-        */
-       abstract public function directoryExists( array $params );
-
-       /**
-        * Get an iterator to list *all* directories under a storage directory.
-        * If the directory is of the form "mwstore://backend/container",
-        * then all directories in the container will be listed.
-        * If the directory is of form "mwstore://backend/container/dir",
-        * then all directories directly under that directory will be listed.
-        * Results will be storage directories relative to the given directory.
-        *
-        * Storage backends with eventual consistency might return stale data.
-        *
-        * Failures during iteration can result in FileBackendError exceptions (since 1.22).
-        *
-        * @param array $params Parameters include:
-        *   - dir     : storage directory
-        *   - topOnly : only return direct child dirs of the directory
-        * @return Traversable|array|null Returns null on failure
-        * @since 1.20
-        */
-       abstract public function getDirectoryList( array $params );
-
-       /**
-        * Same as FileBackend::getDirectoryList() except only lists
-        * directories that are immediately under the given directory.
-        *
-        * Storage backends with eventual consistency might return stale data.
-        *
-        * Failures during iteration can result in FileBackendError exceptions (since 1.22).
-        *
-        * @param array $params Parameters include:
-        *   - dir : storage directory
-        * @return Traversable|array|null Returns null on failure
-        * @since 1.20
-        */
-       final public function getTopDirectoryList( array $params ) {
-               return $this->getDirectoryList( [ 'topOnly' => true ] + $params );
-       }
-
-       /**
-        * Get an iterator to list *all* stored files under a storage directory.
-        * If the directory is of the form "mwstore://backend/container",
-        * then all files in the container will be listed.
-        * If the directory is of form "mwstore://backend/container/dir",
-        * then all files under that directory will be listed.
-        * Results will be storage paths relative to the given directory.
-        *
-        * Storage backends with eventual consistency might return stale data.
-        *
-        * Failures during iteration can result in FileBackendError exceptions (since 1.22).
-        *
-        * @param array $params Parameters include:
-        *   - dir        : storage directory
-        *   - topOnly    : only return direct child files of the directory (since 1.20)
-        *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
-        * @return Traversable|array|null Returns null on failure
-        */
-       abstract public function getFileList( array $params );
-
-       /**
-        * Same as FileBackend::getFileList() except only lists
-        * files that are immediately under the given directory.
-        *
-        * Storage backends with eventual consistency might return stale data.
-        *
-        * Failures during iteration can result in FileBackendError exceptions (since 1.22).
-        *
-        * @param array $params Parameters include:
-        *   - dir        : storage directory
-        *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
-        * @return Traversable|array|null Returns null on failure
-        * @since 1.20
-        */
-       final public function getTopFileList( array $params ) {
-               return $this->getFileList( [ 'topOnly' => true ] + $params );
-       }
-
-       /**
-        * Preload persistent file stat cache and property cache into in-process cache.
-        * This should be used when stat calls will be made on a known list of a many files.
-        *
-        * @see FileBackend::getFileStat()
-        *
-        * @param array $paths Storage paths
-        */
-       abstract public function preloadCache( array $paths );
-
-       /**
-        * Invalidate any in-process file stat and property cache.
-        * If $paths is given, then only the cache for those files will be cleared.
-        *
-        * @see FileBackend::getFileStat()
-        *
-        * @param array $paths Storage paths (optional)
-        */
-       abstract public function clearCache( array $paths = null );
-
-       /**
-        * Preload file stat information (concurrently if possible) into in-process cache.
-        *
-        * This should be used when stat calls will be made on a known list of a many files.
-        * This does not make use of the persistent file stat cache.
-        *
-        * @see FileBackend::getFileStat()
-        *
-        * @param array $params Parameters include:
-        *   - srcs        : list of source storage paths
-        *   - latest      : use the latest available data
-        * @return bool All requests proceeded without I/O errors (since 1.24)
-        * @since 1.23
-        */
-       abstract public function preloadFileStat( array $params );
-
-       /**
-        * Lock the files at the given storage paths in the backend.
-        * This will either lock all the files or none (on failure).
-        *
-        * Callers should consider using getScopedFileLocks() instead.
-        *
-        * @param array $paths Storage paths
-        * @param int $type LockManager::LOCK_* constant
-        * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
-        * @return Status
-        */
-       final public function lockFiles( array $paths, $type, $timeout = 0 ) {
-               $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
-
-               return $this->lockManager->lock( $paths, $type, $timeout );
-       }
-
-       /**
-        * Unlock the files at the given storage paths in the backend.
-        *
-        * @param array $paths Storage paths
-        * @param int $type LockManager::LOCK_* constant
-        * @return Status
-        */
-       final public function unlockFiles( array $paths, $type ) {
-               $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
-
-               return $this->lockManager->unlock( $paths, $type );
-       }
-
-       /**
-        * Lock the files at the given storage paths in the backend.
-        * This will either lock all the files or none (on failure).
-        * On failure, the status object will be updated with errors.
-        *
-        * Once the return value goes out scope, the locks will be released and
-        * the status updated. Unlock fatals will not change the status "OK" value.
-        *
-        * @see ScopedLock::factory()
-        *
-        * @param array $paths List of storage paths or map of lock types to path lists
-        * @param int|string $type LockManager::LOCK_* constant or "mixed"
-        * @param Status $status Status to update on lock/unlock
-        * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
-        * @return ScopedLock|null Returns null on failure
-        */
-       final public function getScopedFileLocks( array $paths, $type, Status $status, $timeout = 0 ) {
-               if ( $type === 'mixed' ) {
-                       foreach ( $paths as &$typePaths ) {
-                               $typePaths = array_map( 'FileBackend::normalizeStoragePath', $typePaths );
-                       }
-               } else {
-                       $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
-               }
-
-               return ScopedLock::factory( $this->lockManager, $paths, $type, $status, $timeout );
-       }
-
-       /**
-        * Get an array of scoped locks needed for a batch of file operations.
-        *
-        * Normally, FileBackend::doOperations() handles locking, unless
-        * the 'nonLocking' param is passed in. This function is useful if you
-        * want the files to be locked for a broader scope than just when the
-        * files are changing. For example, if you need to update DB metadata,
-        * you may want to keep the files locked until finished.
-        *
-        * @see FileBackend::doOperations()
-        *
-        * @param array $ops List of file operations to FileBackend::doOperations()
-        * @param Status $status Status to update on lock/unlock
-        * @return ScopedLock|null
-        * @since 1.20
-        */
-       abstract public function getScopedLocksForOps( array $ops, Status $status );
-
-       /**
-        * Get the root storage path of this backend.
-        * All container paths are "subdirectories" of this path.
-        *
-        * @return string Storage path
-        * @since 1.20
-        */
-       final public function getRootStoragePath() {
-               return "mwstore://{$this->name}";
-       }
-
-       /**
-        * Get the storage path for the given container for this backend
-        *
-        * @param string $container Container name
-        * @return string Storage path
-        * @since 1.21
-        */
-       final public function getContainerStoragePath( $container ) {
-               return $this->getRootStoragePath() . "/{$container}";
-       }
-
-       /**
-        * Get the file journal object for this backend
-        *
-        * @return FileJournal
-        */
-       final public function getJournal() {
-               return $this->fileJournal;
-       }
-
-       /**
-        * Convert FSFile 'src' paths to string paths (with an 'srcRef' field set to the FSFile)
-        *
-        * The 'srcRef' field keeps any TempFSFile objects in scope for the backend to have it
-        * around as long it needs (which may vary greatly depending on configuration)
-        *
-        * @param array $ops File operation batch for FileBaclend::doOperations()
-        * @return array File operation batch
-        */
-       protected function resolveFSFileObjects( array $ops ) {
-               foreach ( $ops as &$op ) {
-                       $src = isset( $op['src'] ) ? $op['src'] : null;
-                       if ( $src instanceof FSFile ) {
-                               $op['srcRef'] = $src;
-                               $op['src'] = $src->getPath();
-                       }
-               }
-               unset( $op );
-
-               return $ops;
-       }
-
-       /**
-        * Check if a given path is a "mwstore://" path.
-        * This does not do any further validation or any existence checks.
-        *
-        * @param string $path
-        * @return bool
-        */
-       final public static function isStoragePath( $path ) {
-               return ( strpos( $path, 'mwstore://' ) === 0 );
-       }
-
-       /**
-        * Split a storage path into a backend name, a container name,
-        * and a relative file path. The relative path may be the empty string.
-        * This does not do any path normalization or traversal checks.
-        *
-        * @param string $storagePath
-        * @return array (backend, container, rel object) or (null, null, null)
-        */
-       final public static function splitStoragePath( $storagePath ) {
-               if ( self::isStoragePath( $storagePath ) ) {
-                       // Remove the "mwstore://" prefix and split the path
-                       $parts = explode( '/', substr( $storagePath, 10 ), 3 );
-                       if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
-                               if ( count( $parts ) == 3 ) {
-                                       return $parts; // e.g. "backend/container/path"
-                               } else {
-                                       return [ $parts[0], $parts[1], '' ]; // e.g. "backend/container"
-                               }
-                       }
-               }
-
-               return [ null, null, null ];
-       }
-
-       /**
-        * Normalize a storage path by cleaning up directory separators.
-        * Returns null if the path is not of the format of a valid storage path.
-        *
-        * @param string $storagePath
-        * @return string|null
-        */
-       final public static function normalizeStoragePath( $storagePath ) {
-               list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
-               if ( $relPath !== null ) { // must be for this backend
-                       $relPath = self::normalizeContainerPath( $relPath );
-                       if ( $relPath !== null ) {
-                               return ( $relPath != '' )
-                                       ? "mwstore://{$backend}/{$container}/{$relPath}"
-                                       : "mwstore://{$backend}/{$container}";
-                       }
-               }
-
-               return null;
-       }
-
-       /**
-        * Get the parent storage directory of a storage path.
-        * This returns a path like "mwstore://backend/container",
-        * "mwstore://backend/container/...", or null if there is no parent.
-        *
-        * @param string $storagePath
-        * @return string|null
-        */
-       final public static function parentStoragePath( $storagePath ) {
-               $storagePath = dirname( $storagePath );
-               list( , , $rel ) = self::splitStoragePath( $storagePath );
-
-               return ( $rel === null ) ? null : $storagePath;
-       }
-
-       /**
-        * Get the final extension from a storage or FS path
-        *
-        * @param string $path
-        * @param string $case One of (rawcase, uppercase, lowercase) (since 1.24)
-        * @return string
-        */
-       final public static function extensionFromPath( $path, $case = 'lowercase' ) {
-               $i = strrpos( $path, '.' );
-               $ext = $i ? substr( $path, $i + 1 ) : '';
-
-               if ( $case === 'lowercase' ) {
-                       $ext = strtolower( $ext );
-               } elseif ( $case === 'uppercase' ) {
-                       $ext = strtoupper( $ext );
-               }
-
-               return $ext;
-       }
-
-       /**
-        * Check if a relative path has no directory traversals
-        *
-        * @param string $path
-        * @return bool
-        * @since 1.20
-        */
-       final public static function isPathTraversalFree( $path ) {
-               return ( self::normalizeContainerPath( $path ) !== null );
-       }
-
-       /**
-        * Build a Content-Disposition header value per RFC 6266.
-        *
-        * @param string $type One of (attachment, inline)
-        * @param string $filename Suggested file name (should not contain slashes)
-        * @throws FileBackendError
-        * @return string
-        * @since 1.20
-        */
-       final public static function makeContentDisposition( $type, $filename = '' ) {
-               $parts = [];
-
-               $type = strtolower( $type );
-               if ( !in_array( $type, [ 'inline', 'attachment' ] ) ) {
-                       throw new FileBackendError( "Invalid Content-Disposition type '$type'." );
-               }
-               $parts[] = $type;
-
-               if ( strlen( $filename ) ) {
-                       $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
-               }
-
-               return implode( ';', $parts );
-       }
-
-       /**
-        * Validate and normalize a relative storage path.
-        * Null is returned if the path involves directory traversal.
-        * Traversal is insecure for FS backends and broken for others.
-        *
-        * This uses the same traversal protection as Title::secureAndSplit().
-        *
-        * @param string $path Storage path relative to a container
-        * @return string|null
-        */
-       final protected static function normalizeContainerPath( $path ) {
-               // Normalize directory separators
-               $path = strtr( $path, '\\', '/' );
-               // Collapse any consecutive directory separators
-               $path = preg_replace( '![/]{2,}!', '/', $path );
-               // Remove any leading directory separator
-               $path = ltrim( $path, '/' );
-               // Use the same traversal protection as Title::secureAndSplit()
-               if ( strpos( $path, '.' ) !== false ) {
-                       if (
-                               $path === '.' ||
-                               $path === '..' ||
-                               strpos( $path, './' ) === 0 ||
-                               strpos( $path, '../' ) === 0 ||
-                               strpos( $path, '/./' ) !== false ||
-                               strpos( $path, '/../' ) !== false
-                       ) {
-                               return null;
-                       }
-               }
-
-               return $path;
-       }
-}
-
-/**
- * Generic file backend exception for checked and unexpected (e.g. config) exceptions
- *
- * @ingroup FileBackend
- * @since 1.23
- */
-class FileBackendException extends Exception {
-}
-
-/**
- * File backend exception for checked exceptions (e.g. I/O errors)
- *
- * @ingroup FileBackend
- * @since 1.22
- */
-class FileBackendError extends FileBackendException {
-}
index 57461a4..e65a594 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup FileBackend
  * @author Aaron Schulz
  */
+use \MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 
 /**
  * Class to handle file backend registration
@@ -61,7 +63,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 +88,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 +100,8 @@ class FileBackendGroup {
                                        "{$repoName}-deleted" => $deletedDir,
                                        "{$repoName}-temp" => "{$directory}/temp"
                                ],
-                               'fileMode' => $fileMode,
+                               'fileMode' => isset( $info['fileMode'] ) ? $info['fileMode'] : 0644,
+                               'directoryMode' => $wgDirectoryMode,
                        ];
                }
 
@@ -114,18 +114,18 @@ class FileBackendGroup {
         *
         * @param array $configs
         * @param string|null $readOnlyReason
-        * @throws FileBackendException
+        * @throws InvalidArgumentException
         */
        protected function register( array $configs, $readOnlyReason = null ) {
                foreach ( $configs as $config ) {
                        if ( !isset( $config['name'] ) ) {
-                               throw new FileBackendException( "Cannot register a backend with no name." );
+                               throw new InvalidArgumentException( "Cannot register a backend with no name." );
                        }
                        $name = $config['name'];
                        if ( isset( $this->backends[$name] ) ) {
-                               throw new FileBackendException( "Backend with name `{$name}` already registered." );
+                               throw new LogicException( "Backend with name `{$name}` already registered." );
                        } elseif ( !isset( $config['class'] ) ) {
-                               throw new FileBackendException( "Backend with name `{$name}` has no class." );
+                               throw new InvalidArgumentException( "Backend with name `{$name}` has no class." );
                        }
                        $class = $config['class'];
 
@@ -147,26 +147,23 @@ class FileBackendGroup {
         *
         * @param string $name
         * @return FileBackend
-        * @throws FileBackendException
+        * @throws InvalidArgumentException
         */
        public function get( $name ) {
-               if ( !isset( $this->backends[$name] ) ) {
-                       throw new FileBackendException( "No backend defined with the name `$name`." );
-               }
                // Lazy-load the actual backend instance
                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['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 = $this->config( $name );
+
+                       $class = $config['class'];
+                       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 );
                }
@@ -178,16 +175,36 @@ class FileBackendGroup {
         * Get the config array for a backend object with a given name
         *
         * @param string $name
-        * @return array
-        * @throws FileBackendException
+        * @return array Parameters to FileBackend::__construct()
+        * @throws InvalidArgumentException
         */
        public function config( $name ) {
                if ( !isset( $this->backends[$name] ) ) {
-                       throw new FileBackendException( "No backend defined with the name `$name`." );
+                       throw new InvalidArgumentException( "No backend defined with the name `$name`." );
                }
                $class = $this->backends[$name]['class'];
 
-               return [ 'class' => $class ] + $this->backends[$name]['config'];
+               $config = $this->backends[$name]['config'];
+               $config['class'] = $class;
+               $config += [ // set defaults
+                       'wikiId' => wfWikiID(), // e.g. "my_wiki-en_"
+                       'mimeCallback' => [ $this, 'guessMimeInternal' ],
+                       'obResetFunc' => 'wfResetOutputBuffers',
+                       'streamMimeFunc' => [ 'StreamFile', 'contentTypeFromPath' ],
+                       'tmpDirectory' => wfTempDir(),
+                       'statusWrapper' => [ 'Status', 'wrap' ],
+                       'wanCache' => MediaWikiServices::getInstance()->getMainWANObjectCache(),
+                       'srvCache' => ObjectCache::getLocalServerInstance( 'hash' ),
+                       'logger' => LoggerFactory::getInstance( 'FileOperation' ),
+                       'profiler' => Profiler::instance()
+               ];
+               $config['lockManager'] =
+                       LockManagerGroup::singleton( $config['wikiId'] )->get( $config['lockManager'] );
+               $config['fileJournal'] = isset( $config['fileJournal'] )
+                       ? FileJournal::factory( $config['fileJournal'], $name )
+                       : FileJournal::factory( [ 'class' => 'NullFileJournal' ], $name );
+
+               return $config;
        }
 
        /**
@@ -221,7 +238,7 @@ class FileBackendGroup {
                if ( !$type && $fsPath ) {
                        $type = $magic->guessMimeType( $fsPath, false );
                } elseif ( !$type && strlen( $content ) ) {
-                       $tmpFile = TempFSFile::factory( 'mime_' );
+                       $tmpFile = TempFSFile::factory( 'mime_', '', wfTempDir() );
                        file_put_contents( $tmpFile->getPath(), $content );
                        $type = $magic->guessMimeType( $tmpFile->getPath(), false );
                }
diff --git a/includes/filebackend/FileBackendMultiWrite.php b/includes/filebackend/FileBackendMultiWrite.php
deleted file mode 100644 (file)
index 3b20048..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 FileBackendError( "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 FileBackendError( '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 FileBackendError( '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 FileBackendError( 'No master backend defined.' );
-               }
-               if ( $this->readIndex < 0 ) {
-                       $this->readIndex = $this->masterIndex; // default
-               }
-       }
-
-       final protected function doOperationsInternal( array $ops, array $opts ) {
-               $status = Status::newGood();
-
-               $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 Status
-        */
-       public function consistencyCheck( array $paths ) {
-               $status = Status::newGood();
-               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 Status
-        */
-       public function accessibilityCheck( array $paths ) {
-               $status = Status::newGood();
-               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 Status
-        */
-       public function resyncFiles( array $paths, $resyncMode = true ) {
-               $status = Status::newGood();
-
-               $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 = Status::newGood();
-               // Do the operations on the master backend; setting Status 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 Status
-        */
-       protected function doDirectoryOp( $method, array $params ) {
-               $status = Status::newGood();
-
-               $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, Status $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 bc4d81d..0000000
+++ /dev/null
@@ -1,1971 +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       : Status will be returned immediately if supported.
-        *                   If the status 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 Status
-        */
-       final public function createInternal( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
-                       $status = Status::newFatal( '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 Status
-        */
-       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       : Status will be returned immediately if supported.
-        *                   If the status 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 Status
-        */
-       final public function storeInternal( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
-                       $status = Status::newFatal( '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 Status
-        */
-       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               : Status will be returned immediately if supported.
-        *                           If the status 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 Status
-        */
-       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 Status
-        */
-       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               : Status will be returned immediately if supported.
-        *                           If the status is OK, then its value field will be
-        *                           set to a FileBackendStoreOpHandle object.
-        *
-        * @param array $params
-        * @return Status
-        */
-       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 Status
-        */
-       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               : Status will be returned immediately if supported.
-        *                           If the status 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 Status
-        */
-       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 Status
-        */
-       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         : Status will be returned immediately if supported.
-        *                     If the status is OK, then its value field will be
-        *                     set to a FileBackendStoreOpHandle object.
-        *
-        * @param array $params
-        * @return Status
-        */
-       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 = Status::newGood(); // nothing to do
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see FileBackendStore::describeInternal()
-        * @param array $params
-        * @return Status
-        */
-       protected function doDescribeInternal( array $params ) {
-               return Status::newGood();
-       }
-
-       /**
-        * No-op file operation that does nothing.
-        * Do not call this function from places outside FileBackend and FileOp.
-        *
-        * @param array $params
-        * @return Status
-        */
-       final public function nullInternal( array $params ) {
-               return Status::newGood();
-       }
-
-       final public function concatenate( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
-
-               // 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 Status
-        */
-       protected function doConcatenate( array $params ) {
-               $status = Status::newGood();
-               $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 = Status::newGood();
-
-               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 Status
-        */
-       protected function doPrepareInternal( $container, $dir, array $params ) {
-               return Status::newGood();
-       }
-
-       final protected function doSecure( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
-
-               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 Status
-        */
-       protected function doSecureInternal( $container, $dir, array $params ) {
-               return Status::newGood();
-       }
-
-       final protected function doPublish( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
-
-               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 Status
-        */
-       protected function doPublishInternal( $container, $dir, array $params ) {
-               return Status::newGood();
-       }
-
-       final protected function doClean( array $params ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
-
-               // 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 Status
-        */
-       protected function doCleanInternal( $container, $dir, array $params ) {
-               return Status::newGood();
-       }
-
-       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 = Status::newGood();
-
-               // 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 Status
-        */
-       protected function doStreamFile( array $params ) {
-               $status = Status::newGood();
-
-               $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 array 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, Status $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 = Status::newGood();
-
-               // 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 = Status::newFatal( '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 status 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 = Status::newGood();
-
-               // 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
-
-               $statuses = []; // array of (index => Status)
-               $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 Status object fields will correspond
-        * to the order in which the handles where given.
-        *
-        * @param FileBackendStoreOpHandle[] $fileOpHandles
-        *
-        * @throws FileBackendError
-        * @return array Map of Status objects
-        */
-       final public function executeOpHandlesInternal( array $fileOpHandles ) {
-               $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-
-               foreach ( $fileOpHandles as $fileOpHandle ) {
-                       if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
-                               throw new FileBackendError( "Given a non-FileBackendStoreOpHandle object." );
-                       } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
-                               throw new FileBackendError( "Given 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 Status[] List of corresponding Status objects
-        */
-       protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
-               if ( count( $fileOpHandles ) ) {
-                       throw new FileBackendError( "This backend supports no 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 Status 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 56a4073..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 FileBackendError( "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 status object is returned.
-        *
-        * @param array $predicates
-        * @return Status
-        */
-       final public function precheck( array &$predicates ) {
-               if ( $this->state !== self::STATE_NEW ) {
-                       return Status::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 Status
-        */
-       protected function doPrecheck( array &$predicates ) {
-               return Status::newGood();
-       }
-
-       /**
-        * Attempt the operation
-        *
-        * @return Status
-        */
-       final public function attempt() {
-               if ( $this->state !== self::STATE_CHECKED ) {
-                       return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
-               } elseif ( $this->failed ) { // failed precheck
-                       return Status::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 = Status::newGood();
-               }
-
-               return $status;
-       }
-
-       /**
-        * @return Status
-        */
-       protected function doAttempt() {
-               return Status::newGood();
-       }
-
-       /**
-        * Attempt the operation in the background
-        *
-        * @return Status
-        */
-       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 status will be returned if there is no chance it can be overwritten.
-        *
-        * @param array $predicates
-        * @return Status
-        */
-       protected function precheckDestExistence( array $predicates ) {
-               $status = Status::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 = Status::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 Status::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 = Status::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 Status::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 = Status::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 = Status::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 = Status::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 = Status::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 = Status::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 = Status::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 78209d8..0000000
+++ /dev/null
@@ -1,202 +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 Status 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 Status
-        */
-       public static function attempt( array $performOps, array $opts, FileJournal $journal ) {
-               $status = Status::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() ) {
-                               return $subStatus; // 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 Status $status
-        */
-       protected static function runParallelBatches( array $pPerformOps, Status $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 Status[] $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 Status, 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 e2c1ede..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 = Status::newGood();
-
-               $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 = Status::newGood();
-
-               $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 = Status::newGood();
-
-               $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 = Status::newGood();
-
-               $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 );
-                               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 2adf934..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.
- *
- * Status 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 = Status::newGood();
-
-               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, Status $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 = Status::newGood();
-
-               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, Status $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 = Status::newGood();
-
-               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, Status $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 = Status::newGood();
-
-               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, Status $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 = Status::newGood();
-
-               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, Status $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 = Status::newGood();
-
-               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, Status $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 = Status::newGood();
-
-               // (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 = Status::newGood();
-               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 = Status::newGood();
-
-               $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 = Status::newGood();
-
-               // 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 = Status::newGood();
-               /** @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 = Status::newGood();
-
-               $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 );
-                       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 Status[]
-        */
-       protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
-               $statuses = [];
-
-               $auth = $this->getAuthentication();
-               if ( !$auth ) {
-                       foreach ( $fileOpHandles as $index => $fileOpHandle ) {
-                               $statuses[$index] = Status::newFatal( '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] = Status::newGood();
-               }
-
-               // 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 Status
-        */
-       protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
-               $status = Status::newGood();
-               $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 Status
-        */
-       protected function createContainer( $container, array $params ) {
-               $status = Status::newGood();
-
-               $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 Status
-        */
-       protected function deleteContainer( $container, array $params ) {
-               $status = Status::newGood();
-
-               $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 Status With the list as value
-        */
-       private function objectListing(
-               $fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
-       ) {
-               $status = Status::newGood();
-
-               $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 Status object to have a fatal error.
-        *
-        * @param Status|null $status
-        * @param string $func
-        * @param array $params
-        * @param string $err Error string
-        * @param int $code HTTP status
-        * @param string $desc HTTP status description
-        */
-       public function onError( $status, $func, array $params, $err = '', $code = 0, $desc = '' ) {
-               if ( $status instanceof Status ) {
-                       $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/TempFSFile.php b/includes/filebackend/TempFSFile.php
deleted file mode 100644 (file)
index f572840..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-<?php
-/**
- * Location holder of files stored temporarily
- *
- * 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
- */
-
-/**
- * This class is used to hold the location and do limited manipulation
- * of files stored temporarily (this will be whatever wfTempDir() returns)
- *
- * @ingroup FileBackend
- */
-class TempFSFile extends FSFile {
-       /** @var bool Garbage collect the temp file */
-       protected $canDelete = false;
-
-       /** @var array Map of (path => 1) for paths to delete on shutdown */
-       protected static $pathsCollect = null;
-
-       public function __construct( $path ) {
-               parent::__construct( $path );
-
-               if ( self::$pathsCollect === null ) {
-                       self::$pathsCollect = [];
-                       register_shutdown_function( [ __CLASS__, 'purgeAllOnShutdown' ] );
-               }
-       }
-
-       /**
-        * Make a new temporary file on the file system.
-        * Temporary files may be purged when the file object falls out of scope.
-        *
-        * @param string $prefix
-        * @param string $extension
-        * @return TempFSFile|null
-        */
-       public static function factory( $prefix, $extension = '' ) {
-               $ext = ( $extension != '' ) ? ".{$extension}" : '';
-
-               $attempts = 5;
-               while ( $attempts-- ) {
-                       $path = wfTempDir() . '/' . $prefix . wfRandomString( 12 ) . $ext;
-                       MediaWiki\suppressWarnings();
-                       $newFileHandle = fopen( $path, 'x' );
-                       MediaWiki\restoreWarnings();
-                       if ( $newFileHandle ) {
-                               fclose( $newFileHandle );
-                               $tmpFile = new self( $path );
-                               $tmpFile->autocollect();
-                               // Safely instantiated, end loop.
-                               return $tmpFile;
-                       }
-               }
-
-               // Give up
-               return null;
-       }
-
-       /**
-        * Purge this file off the file system
-        *
-        * @return bool Success
-        */
-       public function purge() {
-               $this->canDelete = false; // done
-               MediaWiki\suppressWarnings();
-               $ok = unlink( $this->path );
-               MediaWiki\restoreWarnings();
-
-               unset( self::$pathsCollect[$this->path] );
-
-               return $ok;
-       }
-
-       /**
-        * Clean up the temporary file only after an object goes out of scope
-        *
-        * @param object $object
-        * @return TempFSFile This object
-        */
-       public function bind( $object ) {
-               if ( is_object( $object ) ) {
-                       if ( !isset( $object->tempFSFileReferences ) ) {
-                               // Init first since $object might use __get() and return only a copy variable
-                               $object->tempFSFileReferences = [];
-                       }
-                       $object->tempFSFileReferences[] = $this;
-               }
-
-               return $this;
-       }
-
-       /**
-        * Set flag to not clean up after the temporary file
-        *
-        * @return TempFSFile This object
-        */
-       public function preserve() {
-               $this->canDelete = false;
-
-               unset( self::$pathsCollect[$this->path] );
-
-               return $this;
-       }
-
-       /**
-        * Set flag clean up after the temporary file
-        *
-        * @return TempFSFile This object
-        */
-       public function autocollect() {
-               $this->canDelete = true;
-
-               self::$pathsCollect[$this->path] = 1;
-
-               return $this;
-       }
-
-       /**
-        * Try to make sure that all files are purged on error
-        *
-        * This method should only be called internally
-        */
-       public static function purgeAllOnShutdown() {
-               foreach ( self::$pathsCollect as $path ) {
-                       MediaWiki\suppressWarnings();
-                       unlink( $path );
-                       MediaWiki\restoreWarnings();
-               }
-       }
-
-       /**
-        * Cleans up after the temporary file by deleting it
-        */
-       function __destruct() {
-               if ( $this->canDelete ) {
-                       $this->purge();
-               }
-       }
-}
index 7efb3a1..2e06c40 100644 (file)
@@ -48,10 +48,10 @@ class DBFileJournal extends FileJournal {
         * @see FileJournal::logChangeBatch()
         * @param array $entries
         * @param string $batchId
-        * @return Status
+        * @return StatusValue
         */
        protected function doLogChangeBatch( array $entries, $batchId ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
 
                try {
                        $dbw = $this->getMasterDB();
@@ -151,11 +151,11 @@ class DBFileJournal extends FileJournal {
 
        /**
         * @see FileJournal::purgeOldLogs()
-        * @return Status
+        * @return StatusValue
         * @throws DBError
         */
        protected function doPurgeOldLogs() {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
                if ( $this->ttlDays <= 0 ) {
                        return $status; // nothing to do
                }
diff --git a/includes/filebackend/filejournal/FileJournal.php b/includes/filebackend/filejournal/FileJournal.php
deleted file mode 100644 (file)
index b84e195..0000000
+++ /dev/null
@@ -1,251 +0,0 @@
-<?php
-/**
- * @defgroup FileJournal File journal
- * @ingroup FileBackend
- */
-
-/**
- * File operation journaling.
- *
- * 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 FileJournal
- * @author Aaron Schulz
- */
-
-/**
- * @brief Class for handling file operation journaling.
- *
- * Subclasses should avoid throwing exceptions at all costs.
- *
- * @ingroup FileJournal
- * @since 1.20
- */
-abstract class FileJournal {
-       /** @var string */
-       protected $backend;
-
-       /** @var int */
-       protected $ttlDays;
-
-       /**
-        * Construct a new instance from configuration.
-        *
-        * @param array $config Includes:
-        *     'ttlDays' : days to keep log entries around (false means "forever")
-        */
-       protected function __construct( array $config ) {
-               $this->ttlDays = isset( $config['ttlDays'] ) ? $config['ttlDays'] : false;
-       }
-
-       /**
-        * Create an appropriate FileJournal object from config
-        *
-        * @param array $config
-        * @param string $backend A registered file backend name
-        * @throws Exception
-        * @return FileJournal
-        */
-       final public static function factory( array $config, $backend ) {
-               $class = $config['class'];
-               $jrn = new $class( $config );
-               if ( !$jrn instanceof self ) {
-                       throw new Exception( "Class given is not an instance of FileJournal." );
-               }
-               $jrn->backend = $backend;
-
-               return $jrn;
-       }
-
-       /**
-        * Get a statistically unique ID string
-        *
-        * @return string <9 char TS_MW timestamp in base 36><22 random base 36 chars>
-        */
-       final public function getTimestampedUUID() {
-               $s = '';
-               for ( $i = 0; $i < 5; $i++ ) {
-                       $s .= mt_rand( 0, 2147483647 );
-               }
-               $s = Wikimedia\base_convert( sha1( $s ), 16, 36, 31 );
-
-               return substr( Wikimedia\base_convert( wfTimestamp( TS_MW ), 10, 36, 9 ) . $s, 0, 31 );
-       }
-
-       /**
-        * Log changes made by a batch file operation.
-        *
-        * @param array $entries List of file operations (each an array of parameters) which contain:
-        *     op      : Basic operation name (create, update, delete)
-        *     path    : The storage path of the file
-        *     newSha1 : The final base 36 SHA-1 of the file
-        *   Note that 'false' should be used as the SHA-1 for non-existing files.
-        * @param string $batchId UUID string that identifies the operation batch
-        * @return Status
-        */
-       final public function logChangeBatch( array $entries, $batchId ) {
-               if ( !count( $entries ) ) {
-                       return Status::newGood();
-               }
-
-               return $this->doLogChangeBatch( $entries, $batchId );
-       }
-
-       /**
-        * @see FileJournal::logChangeBatch()
-        *
-        * @param array $entries List of file operations (each an array of parameters)
-        * @param string $batchId UUID string that identifies the operation batch
-        * @return Status
-        */
-       abstract protected function doLogChangeBatch( array $entries, $batchId );
-
-       /**
-        * Get the position ID of the latest journal entry
-        *
-        * @return int|bool
-        */
-       final public function getCurrentPosition() {
-               return $this->doGetCurrentPosition();
-       }
-
-       /**
-        * @see FileJournal::getCurrentPosition()
-        * @return int|bool
-        */
-       abstract protected function doGetCurrentPosition();
-
-       /**
-        * Get the position ID of the latest journal entry at some point in time
-        *
-        * @param int|string $time Timestamp
-        * @return int|bool
-        */
-       final public function getPositionAtTime( $time ) {
-               return $this->doGetPositionAtTime( $time );
-       }
-
-       /**
-        * @see FileJournal::getPositionAtTime()
-        * @param int|string $time Timestamp
-        * @return int|bool
-        */
-       abstract protected function doGetPositionAtTime( $time );
-
-       /**
-        * Get an array of file change log entries.
-        * A starting change ID and/or limit can be specified.
-        *
-        * @param int $start Starting change ID or null
-        * @param int $limit Maximum number of items to return
-        * @param string &$next Updated to the ID of the next entry.
-        * @return array List of associative arrays, each having:
-        *     id         : unique, monotonic, ID for this change
-        *     batch_uuid : UUID for an operation batch
-        *     backend    : the backend name
-        *     op         : primitive operation (create,update,delete,null)
-        *     path       : affected storage path
-        *     new_sha1   : base 36 sha1 of the new file had the operation succeeded
-        *     timestamp  : TS_MW timestamp of the batch change
-        *   Also, $next is updated to the ID of the next entry.
-        */
-       final public function getChangeEntries( $start = null, $limit = 0, &$next = null ) {
-               $entries = $this->doGetChangeEntries( $start, $limit ? $limit + 1 : 0 );
-               if ( $limit && count( $entries ) > $limit ) {
-                       $last = array_pop( $entries ); // remove the extra entry
-                       $next = $last['id']; // update for next call
-               } else {
-                       $next = null; // end of list
-               }
-
-               return $entries;
-       }
-
-       /**
-        * @see FileJournal::getChangeEntries()
-        * @param int $start
-        * @param int $limit
-        * @return array
-        */
-       abstract protected function doGetChangeEntries( $start, $limit );
-
-       /**
-        * Purge any old log entries
-        *
-        * @return Status
-        */
-       final public function purgeOldLogs() {
-               return $this->doPurgeOldLogs();
-       }
-
-       /**
-        * @see FileJournal::purgeOldLogs()
-        * @return Status
-        */
-       abstract protected function doPurgeOldLogs();
-}
-
-/**
- * Simple version of FileJournal that does nothing
- * @since 1.20
- */
-class NullFileJournal extends FileJournal {
-       /**
-        * @see FileJournal::doLogChangeBatch()
-        * @param array $entries
-        * @param string $batchId
-        * @return Status
-        */
-       protected function doLogChangeBatch( array $entries, $batchId ) {
-               return Status::newGood();
-       }
-
-       /**
-        * @see FileJournal::doGetCurrentPosition()
-        * @return int|bool
-        */
-       protected function doGetCurrentPosition() {
-               return false;
-       }
-
-       /**
-        * @see FileJournal::doGetPositionAtTime()
-        * @param int|string $time Timestamp
-        * @return int|bool
-        */
-       protected function doGetPositionAtTime( $time ) {
-               return false;
-       }
-
-       /**
-        * @see FileJournal::doGetChangeEntries()
-        * @param int $start
-        * @param int $limit
-        * @return array
-        */
-       protected function doGetChangeEntries( $start, $limit ) {
-               return [];
-       }
-
-       /**
-        * @see FileJournal::doPurgeOldLogs()
-        * @return Status
-        */
-       protected function doPurgeOldLogs() {
-               return Status::newGood();
-       }
-}
diff --git a/includes/filebackend/lockmanager/DBLockManager.php b/includes/filebackend/lockmanager/DBLockManager.php
deleted file mode 100644 (file)
index cccf71a..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 = Status::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 Status::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();
-               }
-       }
-}
diff --git a/includes/filebackend/lockmanager/FSLockManager.php b/includes/filebackend/lockmanager/FSLockManager.php
deleted file mode 100644 (file)
index 2b660ec..0000000
+++ /dev/null
@@ -1,248 +0,0 @@
-<?php
-/**
- * Simple version of LockManager based on using FS lock files.
- *
- * 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
- */
-
-/**
- * Simple version of LockManager based on using FS lock files.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * This should work fine for small sites running off one server.
- * Do not use this with 'lockDirectory' set to an NFS mount unless the
- * NFS client is at least version 2.6.12. Otherwise, the BSD flock()
- * locks will be ignored; see http://nfs.sourceforge.net/#section_d.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-class FSLockManager extends LockManager {
-       /** @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 $lockDir; // global dir for all servers
-
-       /** @var array Map of (locked key => lock file handle) */
-       protected $handles = [];
-
-       /**
-        * Construct a new instance from configuration.
-        *
-        * @param array $config Includes:
-        *   - lockDirectory : Directory containing the lock files
-        */
-       function __construct( array $config ) {
-               parent::__construct( $config );
-
-               $this->lockDir = $config['lockDirectory'];
-       }
-
-       /**
-        * @see LockManager::doLock()
-        * @param array $paths
-        * @param int $type
-        * @return Status
-        */
-       protected function doLock( array $paths, $type ) {
-               $status = Status::newGood();
-
-               $lockedPaths = []; // files locked in this attempt
-               foreach ( $paths as $path ) {
-                       $status->merge( $this->doSingleLock( $path, $type ) );
-                       if ( $status->isOK() ) {
-                               $lockedPaths[] = $path;
-                       } else {
-                               // Abort and unlock everything
-                               $status->merge( $this->doUnlock( $lockedPaths, $type ) );
-
-                               return $status;
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see LockManager::doUnlock()
-        * @param array $paths
-        * @param int $type
-        * @return Status
-        */
-       protected function doUnlock( array $paths, $type ) {
-               $status = Status::newGood();
-
-               foreach ( $paths as $path ) {
-                       $status->merge( $this->doSingleUnlock( $path, $type ) );
-               }
-
-               return $status;
-       }
-
-       /**
-        * Lock a single resource key
-        *
-        * @param string $path
-        * @param int $type
-        * @return Status
-        */
-       protected function doSingleLock( $path, $type ) {
-               $status = Status::newGood();
-
-               if ( isset( $this->locksHeld[$path][$type] ) ) {
-                       ++$this->locksHeld[$path][$type];
-               } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
-                       $this->locksHeld[$path][$type] = 1;
-               } else {
-                       if ( isset( $this->handles[$path] ) ) {
-                               $handle = $this->handles[$path];
-                       } else {
-                               MediaWiki\suppressWarnings();
-                               $handle = fopen( $this->getLockPath( $path ), 'a+' );
-                               MediaWiki\restoreWarnings();
-                               if ( !$handle ) { // lock dir missing?
-                                       wfMkdirParents( $this->lockDir );
-                                       $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
-                               }
-                       }
-                       if ( $handle ) {
-                               // Either a shared or exclusive lock
-                               $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX;
-                               if ( flock( $handle, $lock | LOCK_NB ) ) {
-                                       // Record this lock as active
-                                       $this->locksHeld[$path][$type] = 1;
-                                       $this->handles[$path] = $handle;
-                               } else {
-                                       fclose( $handle );
-                                       $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                               }
-                       } else {
-                               $status->fatal( 'lockmanager-fail-openlock', $path );
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * Unlock a single resource key
-        *
-        * @param string $path
-        * @param int $type
-        * @return Status
-        */
-       protected function doSingleUnlock( $path, $type ) {
-               $status = Status::newGood();
-
-               if ( !isset( $this->locksHeld[$path] ) ) {
-                       $status->warning( 'lockmanager-notlocked', $path );
-               } elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
-                       $status->warning( 'lockmanager-notlocked', $path );
-               } else {
-                       $handlesToClose = [];
-                       --$this->locksHeld[$path][$type];
-                       if ( $this->locksHeld[$path][$type] <= 0 ) {
-                               unset( $this->locksHeld[$path][$type] );
-                       }
-                       if ( !count( $this->locksHeld[$path] ) ) {
-                               unset( $this->locksHeld[$path] ); // no locks on this path
-                               if ( isset( $this->handles[$path] ) ) {
-                                       $handlesToClose[] = $this->handles[$path];
-                                       unset( $this->handles[$path] );
-                               }
-                       }
-                       // Unlock handles to release locks and delete
-                       // any lock files that end up with no locks on them...
-                       if ( wfIsWindows() ) {
-                               // Windows: for any process, including this one,
-                               // calling unlink() on a locked file will fail
-                               $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
-                               $status->merge( $this->pruneKeyLockFiles( $path ) );
-                       } else {
-                               // Unix: unlink() can be used on files currently open by this
-                               // process and we must do so in order to avoid race conditions
-                               $status->merge( $this->pruneKeyLockFiles( $path ) );
-                               $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @param string $path
-        * @param array $handlesToClose
-        * @return Status
-        */
-       private function closeLockHandles( $path, array $handlesToClose ) {
-               $status = Status::newGood();
-               foreach ( $handlesToClose as $handle ) {
-                       if ( !flock( $handle, LOCK_UN ) ) {
-                               $status->fatal( 'lockmanager-fail-releaselock', $path );
-                       }
-                       if ( !fclose( $handle ) ) {
-                               $status->warning( 'lockmanager-fail-closelock', $path );
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * @param string $path
-        * @return Status
-        */
-       private function pruneKeyLockFiles( $path ) {
-               $status = Status::newGood();
-               if ( !isset( $this->locksHeld[$path] ) ) {
-                       # No locks are held for the lock file anymore
-                       if ( !unlink( $this->getLockPath( $path ) ) ) {
-                               $status->warning( 'lockmanager-fail-deletelock', $path );
-                       }
-                       unset( $this->handles[$path] );
-               }
-
-               return $status;
-       }
-
-       /**
-        * Get the path to the lock file for a key
-        * @param string $path
-        * @return string
-        */
-       protected function getLockPath( $path ) {
-               return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock";
-       }
-
-       /**
-        * Make sure remaining locks get cleared for sanity
-        */
-       function __destruct() {
-               while ( count( $this->locksHeld ) ) {
-                       foreach ( $this->locksHeld as $path => $locks ) {
-                               $this->doSingleUnlock( $path, self::LOCK_EX );
-                               $this->doSingleUnlock( $path, self::LOCK_SH );
-                       }
-               }
-       }
-}
diff --git a/includes/filebackend/lockmanager/LockManager.php b/includes/filebackend/lockmanager/LockManager.php
deleted file mode 100644 (file)
index a3cb3b1..0000000
+++ /dev/null
@@ -1,258 +0,0 @@
-<?php
-/**
- * @defgroup LockManager Lock management
- * @ingroup FileBackend
- */
-
-/**
- * Resource locking handling.
- *
- * 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
- * @author Aaron Schulz
- */
-
-/**
- * @brief Class for handling resource locking.
- *
- * Locks on resource keys can either be shared or exclusive.
- *
- * Implementations must keep track of what is locked by this proccess
- * in-memory and support nested locking calls (using reference counting).
- * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op.
- * Locks should either be non-blocking or have low wait timeouts.
- *
- * Subclasses should avoid throwing exceptions at all costs.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-abstract class LockManager {
-       /** @var array Mapping of lock types to the type actually used */
-       protected $lockTypeMap = [
-               self::LOCK_SH => self::LOCK_SH,
-               self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH
-               self::LOCK_EX => self::LOCK_EX
-       ];
-
-       /** @var array Map of (resource path => lock type => count) */
-       protected $locksHeld = [];
-
-       protected $domain; // string; domain (usually wiki ID)
-       protected $lockTTL; // integer; maximum time locks can be held
-
-       /** 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)
-       const LOCK_EX = 3; // exclusive lock (for writes)
-
-       /**
-        * Construct a new instance from configuration
-        *
-        * @param array $config Parameters include:
-        *   - domain  : Domain (usually wiki ID) that all resources are relative to [optional]
-        *   - lockTTL : Age (in seconds) at which resource locks should expire.
-        *               This only applies if locks are not tied to a connection/process.
-        */
-       public function __construct( array $config ) {
-               $this->domain = isset( $config['domain'] ) ? $config['domain'] : wfWikiID();
-               if ( isset( $config['lockTTL'] ) ) {
-                       $this->lockTTL = max( 5, $config['lockTTL'] );
-               } elseif ( PHP_SAPI === 'cli' ) {
-                       $this->lockTTL = 3600;
-               } else {
-                       $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
-                       $this->lockTTL = max( 5 * 60, 2 * (int)$met );
-               }
-       }
-
-       /**
-        * Lock the resources at the given abstract paths
-        *
-        * @param array $paths List of resource names
-        * @param int $type LockManager::LOCK_* constant
-        * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
-        * @return Status
-        */
-       final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) {
-               return $this->lockByType( [ $type => $paths ], $timeout );
-       }
-
-       /**
-        * Lock the resources at the given abstract paths
-        *
-        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
-        * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
-        * @return Status
-        * @since 1.22
-        */
-       final public function lockByType( array $pathsByType, $timeout = 0 ) {
-               $pathsByType = $this->normalizePathsByType( $pathsByType );
-
-               $status = null;
-               $loop = new WaitConditionLoop(
-                       function () use ( &$status, $pathsByType ) {
-                               $status = $this->doLockByType( $pathsByType );
-
-                               return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE;
-                       },
-                       $timeout
-               );
-               $loop->invoke();
-
-               return $status;
-       }
-
-       /**
-        * Unlock the resources at the given abstract paths
-        *
-        * @param array $paths List of paths
-        * @param int $type LockManager::LOCK_* constant
-        * @return Status
-        */
-       final public function unlock( array $paths, $type = self::LOCK_EX ) {
-               return $this->unlockByType( [ $type => $paths ] );
-       }
-
-       /**
-        * Unlock the resources at the given abstract paths
-        *
-        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
-        * @return Status
-        * @since 1.22
-        */
-       final public function unlockByType( array $pathsByType ) {
-               $pathsByType = $this->normalizePathsByType( $pathsByType );
-               $status = $this->doUnlockByType( $pathsByType );
-
-               return $status;
-       }
-
-       /**
-        * Get the base 36 SHA-1 of a string, padded to 31 digits.
-        * Before hashing, the path will be prefixed with the domain ID.
-        * This should be used interally for lock key or file names.
-        *
-        * @param string $path
-        * @return string
-        */
-       final protected function sha1Base36Absolute( $path ) {
-               return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 );
-       }
-
-       /**
-        * Get the base 16 SHA-1 of a string, padded to 31 digits.
-        * Before hashing, the path will be prefixed with the domain ID.
-        * This should be used interally for lock key or file names.
-        *
-        * @param string $path
-        * @return string
-        */
-       final protected function sha1Base16Absolute( $path ) {
-               return sha1( "{$this->domain}:{$path}" );
-       }
-
-       /**
-        * Normalize the $paths array by converting LOCK_UW locks into the
-        * appropriate type and removing any duplicated paths for each lock type.
-        *
-        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
-        * @return array
-        * @since 1.22
-        */
-       final protected function normalizePathsByType( array $pathsByType ) {
-               $res = [];
-               foreach ( $pathsByType as $type => $paths ) {
-                       $res[$this->lockTypeMap[$type]] = array_unique( $paths );
-               }
-
-               return $res;
-       }
-
-       /**
-        * @see LockManager::lockByType()
-        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
-        * @return Status
-        * @since 1.22
-        */
-       protected function doLockByType( array $pathsByType ) {
-               $status = Status::newGood();
-               $lockedByType = []; // map of (type => paths)
-               foreach ( $pathsByType as $type => $paths ) {
-                       $status->merge( $this->doLock( $paths, $type ) );
-                       if ( $status->isOK() ) {
-                               $lockedByType[$type] = $paths;
-                       } else {
-                               // Release the subset of locks that were acquired
-                               foreach ( $lockedByType as $lType => $lPaths ) {
-                                       $status->merge( $this->doUnlock( $lPaths, $lType ) );
-                               }
-                               break;
-                       }
-               }
-
-               return $status;
-       }
-
-       /**
-        * Lock resources with the given keys and lock type
-        *
-        * @param array $paths List of paths
-        * @param int $type LockManager::LOCK_* constant
-        * @return Status
-        */
-       abstract protected function doLock( array $paths, $type );
-
-       /**
-        * @see LockManager::unlockByType()
-        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
-        * @return Status
-        * @since 1.22
-        */
-       protected function doUnlockByType( array $pathsByType ) {
-               $status = Status::newGood();
-               foreach ( $pathsByType as $type => $paths ) {
-                       $status->merge( $this->doUnlock( $paths, $type ) );
-               }
-
-               return $status;
-       }
-
-       /**
-        * Unlock resources with the given keys and lock type
-        *
-        * @param array $paths List of paths
-        * @param int $type LockManager::LOCK_* constant
-        * @return Status
-        */
-       abstract protected function doUnlock( array $paths, $type );
-}
-
-/**
- * Simple version of LockManager that does nothing
- * @since 1.19
- */
-class NullLockManager extends LockManager {
-       protected function doLock( array $paths, $type ) {
-               return Status::newGood();
-       }
-
-       protected function doUnlock( array $paths, $type ) {
-               return Status::newGood();
-       }
-}
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 );
                }
 
diff --git a/includes/filebackend/lockmanager/MemcLockManager.php b/includes/filebackend/lockmanager/MemcLockManager.php
deleted file mode 100644 (file)
index 2f17e27..0000000
+++ /dev/null
@@ -1,386 +0,0 @@
-<?php
-/**
- * Version of LockManager based on using memcached 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 memcached servers.
- *
- * Version of LockManager based on using memcached 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 memcached.
- * A majority of peers must agree for a lock to be acquired.
- *
- * @ingroup LockManager
- * @since 1.20
- */
-class MemcLockManager 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 array Map server names to MemcachedBagOStuff objects */
-       protected $bagOStuffs = [];
-
-       /** @var array (server name => bool) */
-       protected $serversUp = [];
-
-       /** @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.
-        *   - memcConfig   : Configuration array for ObjectCache::newFromParams. [optional]
-        *                    If set, this must use one of the memcached classes.
-        * @throws Exception
-        */
-       public function __construct( array $config ) {
-               parent::__construct( $config );
-
-               // Sanitize srvsByBucket config to prevent PHP errors
-               $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
-               $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
-
-               $memcConfig = isset( $config['memcConfig'] )
-                       ? $config['memcConfig']
-                       : [ 'class' => 'MemcachedPhpBagOStuff' ];
-
-               foreach ( $config['lockServers'] as $name => $address ) {
-                       $params = [ 'servers' => [ $address ] ] + $memcConfig;
-                       $cache = ObjectCache::newFromParams( $params );
-                       if ( $cache instanceof MemcachedBagOStuff ) {
-                               $this->bagOStuffs[$name] = $cache;
-                       } else {
-                               throw new Exception(
-                                       'Only MemcachedBagOStuff classes are supported by MemcLockManager.' );
-                       }
-               }
-
-               $this->session = wfRandomString( 32 );
-       }
-
-       // @todo Change this code to work in one batch
-       protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = Status::newGood();
-
-               $lockedPaths = [];
-               foreach ( $pathsByType as $type => $paths ) {
-                       $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
-                       if ( $status->isOK() ) {
-                               $lockedPaths[$type] = isset( $lockedPaths[$type] )
-                                       ? array_merge( $lockedPaths[$type], $paths )
-                                       : $paths;
-                       } else {
-                               foreach ( $lockedPaths as $lType => $lPaths ) {
-                                       $status->merge( $this->doFreeLocksOnServer( $lockSrv, $lPaths, $lType ) );
-                               }
-                               break;
-                       }
-               }
-
-               return $status;
-       }
-
-       // @todo Change this code to work in one batch
-       protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = Status::newGood();
-
-               foreach ( $pathsByType as $type => $paths ) {
-                       $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) );
-               }
-
-               return $status;
-       }
-
-       /**
-        * @see QuorumLockManager::getLocksOnServer()
-        * @param string $lockSrv
-        * @param array $paths
-        * @param string $type
-        * @return Status
-        */
-       protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
-               $status = Status::newGood();
-
-               $memc = $this->getCache( $lockSrv );
-               $keys = array_map( [ $this, 'recordKeyForPath' ], $paths ); // lock records
-
-               // Lock all of the active lock record keys...
-               if ( !$this->acquireMutexes( $memc, $keys ) ) {
-                       foreach ( $paths as $path ) {
-                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                       }
-
-                       return $status;
-               }
-
-               // Fetch all the existing lock records...
-               $lockRecords = $memc->getMulti( $keys );
-
-               $now = time();
-               // Check if the requested locks conflict with existing ones...
-               foreach ( $paths as $path ) {
-                       $locksKey = $this->recordKeyForPath( $path );
-                       $locksHeld = isset( $lockRecords[$locksKey] )
-                               ? self::sanitizeLockArray( $lockRecords[$locksKey] )
-                               : self::newLockArray(); // init
-                       foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) {
-                               if ( $expiry < $now ) { // stale?
-                                       unset( $locksHeld[self::LOCK_EX][$session] );
-                               } elseif ( $session !== $this->session ) {
-                                       $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                               }
-                       }
-                       if ( $type === self::LOCK_EX ) {
-                               foreach ( $locksHeld[self::LOCK_SH] as $session => $expiry ) {
-                                       if ( $expiry < $now ) { // stale?
-                                               unset( $locksHeld[self::LOCK_SH][$session] );
-                                       } elseif ( $session !== $this->session ) {
-                                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                                       }
-                               }
-                       }
-                       if ( $status->isOK() ) {
-                               // Register the session in the lock record array
-                               $locksHeld[$type][$this->session] = $now + $this->lockTTL;
-                               // We will update this record if none of the other locks conflict
-                               $lockRecords[$locksKey] = $locksHeld;
-                       }
-               }
-
-               // If there were no lock conflicts, update all the lock records...
-               if ( $status->isOK() ) {
-                       foreach ( $paths as $path ) {
-                               $locksKey = $this->recordKeyForPath( $path );
-                               $locksHeld = $lockRecords[$locksKey];
-                               $ok = $memc->set( $locksKey, $locksHeld, 7 * 86400 );
-                               if ( !$ok ) {
-                                       $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                               } else {
-                                       wfDebug( __METHOD__ . ": acquired lock on key $locksKey.\n" );
-                               }
-                       }
-               }
-
-               // Unlock all of the active lock record keys...
-               $this->releaseMutexes( $memc, $keys );
-
-               return $status;
-       }
-
-       /**
-        * @see QuorumLockManager::freeLocksOnServer()
-        * @param string $lockSrv
-        * @param array $paths
-        * @param string $type
-        * @return Status
-        */
-       protected function doFreeLocksOnServer( $lockSrv, array $paths, $type ) {
-               $status = Status::newGood();
-
-               $memc = $this->getCache( $lockSrv );
-               $keys = array_map( [ $this, 'recordKeyForPath' ], $paths ); // lock records
-
-               // Lock all of the active lock record keys...
-               if ( !$this->acquireMutexes( $memc, $keys ) ) {
-                       foreach ( $paths as $path ) {
-                               $status->fatal( 'lockmanager-fail-releaselock', $path );
-                       }
-
-                       return $status;
-               }
-
-               // Fetch all the existing lock records...
-               $lockRecords = $memc->getMulti( $keys );
-
-               // Remove the requested locks from all records...
-               foreach ( $paths as $path ) {
-                       $locksKey = $this->recordKeyForPath( $path ); // lock record
-                       if ( !isset( $lockRecords[$locksKey] ) ) {
-                               $status->warning( 'lockmanager-fail-releaselock', $path );
-                               continue; // nothing to do
-                       }
-                       $locksHeld = self::sanitizeLockArray( $lockRecords[$locksKey] );
-                       if ( isset( $locksHeld[$type][$this->session] ) ) {
-                               unset( $locksHeld[$type][$this->session] ); // unregister this session
-                               if ( $locksHeld === self::newLockArray() ) {
-                                       $ok = $memc->delete( $locksKey );
-                               } else {
-                                       $ok = $memc->set( $locksKey, $locksHeld );
-                               }
-                               if ( !$ok ) {
-                                       $status->fatal( 'lockmanager-fail-releaselock', $path );
-                               }
-                       } else {
-                               $status->warning( 'lockmanager-fail-releaselock', $path );
-                       }
-                       wfDebug( __METHOD__ . ": released lock on key $locksKey.\n" );
-               }
-
-               // Unlock all of the active lock record keys...
-               $this->releaseMutexes( $memc, $keys );
-
-               return $status;
-       }
-
-       /**
-        * @see QuorumLockManager::releaseAllLocks()
-        * @return Status
-        */
-       protected function releaseAllLocks() {
-               return Status::newGood(); // not supported
-       }
-
-       /**
-        * @see QuorumLockManager::isServerUp()
-        * @param string $lockSrv
-        * @return bool
-        */
-       protected function isServerUp( $lockSrv ) {
-               return (bool)$this->getCache( $lockSrv );
-       }
-
-       /**
-        * Get the MemcachedBagOStuff object for a $lockSrv
-        *
-        * @param string $lockSrv Server name
-        * @return MemcachedBagOStuff|null
-        */
-       protected function getCache( $lockSrv ) {
-               /** @var BagOStuff $memc */
-               $memc = null;
-               if ( isset( $this->bagOStuffs[$lockSrv] ) ) {
-                       $memc = $this->bagOStuffs[$lockSrv];
-                       if ( !isset( $this->serversUp[$lockSrv] ) ) {
-                               $this->serversUp[$lockSrv] = $memc->set( __CLASS__ . ':ping', 1, 1 );
-                               if ( !$this->serversUp[$lockSrv] ) {
-                                       trigger_error( __METHOD__ . ": Could not contact $lockSrv.", E_USER_WARNING );
-                               }
-                       }
-                       if ( !$this->serversUp[$lockSrv] ) {
-                               return null; // server appears to be down
-                       }
-               }
-
-               return $memc;
-       }
-
-       /**
-        * @param string $path
-        * @return string
-        */
-       protected function recordKeyForPath( $path ) {
-               return implode( ':', [ __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ] );
-       }
-
-       /**
-        * @return array An empty lock structure for a key
-        */
-       protected static function newLockArray() {
-               return [ self::LOCK_SH => [], self::LOCK_EX => [] ];
-       }
-
-       /**
-        * @param array $a
-        * @return array An empty lock structure for a key
-        */
-       protected static function sanitizeLockArray( $a ) {
-               if ( is_array( $a ) && isset( $a[self::LOCK_EX] ) && isset( $a[self::LOCK_SH] ) ) {
-                       return $a;
-               } else {
-                       trigger_error( __METHOD__ . ": reset invalid lock array.", E_USER_WARNING );
-
-                       return self::newLockArray();
-               }
-       }
-
-       /**
-        * @param MemcachedBagOStuff $memc
-        * @param array $keys List of keys to acquire
-        * @return bool
-        */
-       protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
-               $lockedKeys = [];
-
-               // Acquire the keys in lexicographical order, to avoid deadlock problems.
-               // If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has.
-               sort( $keys );
-
-               // Try to quickly loop to acquire the keys, but back off after a few rounds.
-               // This reduces memcached spam, especially in the rare case where a server acquires
-               // some lock keys and dies without releasing them. Lock keys expire after a few minutes.
-               $loop = new WaitConditionLoop(
-                       function () use ( $memc, $keys, &$lockedKeys ) {
-                               foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
-                                       if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
-                                               $lockedKeys[] = $key;
-                                       }
-                               }
-
-                               return array_diff( $keys, $lockedKeys )
-                                       ? WaitConditionLoop::CONDITION_CONTINUE
-                                       : true;
-                       },
-                       3.0 // timeout
-               );
-               $loop->invoke();
-
-               if ( count( $lockedKeys ) != count( $keys ) ) {
-                       $this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked
-                       return false;
-               }
-
-               return true;
-       }
-
-       /**
-        * @param MemcachedBagOStuff $memc
-        * @param array $keys List of acquired keys
-        */
-       protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) {
-               foreach ( $keys as $key ) {
-                       $memc->delete( "$key:mutex" );
-               }
-       }
-
-       /**
-        * Make sure remaining locks get cleared for sanity
-        */
-       function __destruct() {
-               while ( count( $this->locksHeld ) ) {
-                       foreach ( $this->locksHeld as $path => $locks ) {
-                               $this->doUnlock( [ $path ], self::LOCK_EX );
-                               $this->doUnlock( [ $path ], self::LOCK_SH );
-                       }
-               }
-       }
-}
index 0536091..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 ) {
@@ -35,10 +40,10 @@ class MySqlLockManager extends DBLockManager {
         * @param string $lockSrv
         * @param array $paths
         * @param string $type
-        * @return Status
+        * @return StatusValue
         */
        protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
 
                $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
 
@@ -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__
                                );
@@ -105,10 +117,10 @@ class MySqlLockManager extends DBLockManager {
 
        /**
         * @see QuorumLockManager::releaseAllLocks()
-        * @return Status
+        * @return StatusValue
         */
        protected function releaseAllLocks() {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
 
                foreach ( $this->conns as $lockDb => $db ) {
                        if ( $db->trxLevel() ) { // in transaction
diff --git a/includes/filebackend/lockmanager/PostgreSqlLockManager.php b/includes/filebackend/lockmanager/PostgreSqlLockManager.php
deleted file mode 100644 (file)
index d55b5ae..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 = Status::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 Status
-        */
-       protected function releaseAllLocks() {
-               $status = Status::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/QuorumLockManager.php b/includes/filebackend/lockmanager/QuorumLockManager.php
deleted file mode 100644 (file)
index 108b846..0000000
+++ /dev/null
@@ -1,248 +0,0 @@
-<?php
-/**
- * Version of LockManager that uses a quorum from peer servers for 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 that uses a quorum from peer servers for locks.
- * The resource space can also be sharded into separate peer groups.
- *
- * @ingroup LockManager
- * @since 1.20
- */
-abstract class QuorumLockManager extends LockManager {
-       /** @var array Map of bucket indexes to peer server lists */
-       protected $srvsByBucket = []; // (bucket index => (lsrv1, lsrv2, ...))
-
-       /** @var array Map of degraded buckets */
-       protected $degradedBuckets = []; // (buckey index => UNIX timestamp)
-
-       final protected function doLock( array $paths, $type ) {
-               return $this->doLockByType( [ $type => $paths ] );
-       }
-
-       final protected function doUnlock( array $paths, $type ) {
-               return $this->doUnlockByType( [ $type => $paths ] );
-       }
-
-       protected function doLockByType( array $pathsByType ) {
-               $status = Status::newGood();
-
-               $pathsToLock = []; // (bucket => type => paths)
-               // Get locks that need to be acquired (buckets => locks)...
-               foreach ( $pathsByType as $type => $paths ) {
-                       foreach ( $paths as $path ) {
-                               if ( isset( $this->locksHeld[$path][$type] ) ) {
-                                       ++$this->locksHeld[$path][$type];
-                               } else {
-                                       $bucket = $this->getBucketFromPath( $path );
-                                       $pathsToLock[$bucket][$type][] = $path;
-                               }
-                       }
-               }
-
-               $lockedPaths = []; // files locked in this attempt (type => paths)
-               // Attempt to acquire these locks...
-               foreach ( $pathsToLock as $bucket => $pathsToLockByType ) {
-                       // Try to acquire the locks for this bucket
-                       $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) );
-                       if ( !$status->isOK() ) {
-                               $status->merge( $this->doUnlockByType( $lockedPaths ) );
-
-                               return $status;
-                       }
-                       // Record these locks as active
-                       foreach ( $pathsToLockByType as $type => $paths ) {
-                               foreach ( $paths as $path ) {
-                                       $this->locksHeld[$path][$type] = 1; // locked
-                                       // Keep track of what locks were made in this attempt
-                                       $lockedPaths[$type][] = $path;
-                               }
-                       }
-               }
-
-               return $status;
-       }
-
-       protected function doUnlockByType( array $pathsByType ) {
-               $status = Status::newGood();
-
-               $pathsToUnlock = []; // (bucket => type => paths)
-               foreach ( $pathsByType as $type => $paths ) {
-                       foreach ( $paths as $path ) {
-                               if ( !isset( $this->locksHeld[$path][$type] ) ) {
-                                       $status->warning( 'lockmanager-notlocked', $path );
-                               } else {
-                                       --$this->locksHeld[$path][$type];
-                                       // Reference count the locks held and release locks when zero
-                                       if ( $this->locksHeld[$path][$type] <= 0 ) {
-                                               unset( $this->locksHeld[$path][$type] );
-                                               $bucket = $this->getBucketFromPath( $path );
-                                               $pathsToUnlock[$bucket][$type][] = $path;
-                                       }
-                                       if ( !count( $this->locksHeld[$path] ) ) {
-                                               unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
-                                       }
-                               }
-                       }
-               }
-
-               // Remove these specific locks if possible, or at least release
-               // all locks once this process is currently not holding any locks.
-               foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) {
-                       $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) );
-               }
-               if ( !count( $this->locksHeld ) ) {
-                       $status->merge( $this->releaseAllLocks() );
-                       $this->degradedBuckets = []; // safe to retry the normal quorum
-               }
-
-               return $status;
-       }
-
-       /**
-        * Attempt to acquire locks with the peers for a bucket.
-        * This is all or nothing; if any key is locked then this totally fails.
-        *
-        * @param int $bucket
-        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
-        * @return Status
-        */
-       final protected function doLockingRequestBucket( $bucket, array $pathsByType ) {
-               $status = Status::newGood();
-
-               $yesVotes = 0; // locks made on trustable servers
-               $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
-               $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
-               // Get votes for each peer, in order, until we have enough...
-               foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
-                       if ( !$this->isServerUp( $lockSrv ) ) {
-                               --$votesLeft;
-                               $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
-                               $this->degradedBuckets[$bucket] = time();
-                               continue; // server down?
-                       }
-                       // Attempt to acquire the lock on this peer
-                       $status->merge( $this->getLocksOnServer( $lockSrv, $pathsByType ) );
-                       if ( !$status->isOK() ) {
-                               return $status; // vetoed; resource locked
-                       }
-                       ++$yesVotes; // success for this peer
-                       if ( $yesVotes >= $quorum ) {
-                               return $status; // lock obtained
-                       }
-                       --$votesLeft;
-                       $votesNeeded = $quorum - $yesVotes;
-                       if ( $votesNeeded > $votesLeft ) {
-                               break; // short-circuit
-                       }
-               }
-               // At this point, we must not have met the quorum
-               $status->setResult( false );
-
-               return $status;
-       }
-
-       /**
-        * Attempt to release locks with the peers for a bucket
-        *
-        * @param int $bucket
-        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
-        * @return Status
-        */
-       final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) {
-               $status = Status::newGood();
-
-               $yesVotes = 0; // locks freed on trustable servers
-               $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
-               $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
-               $isDegraded = isset( $this->degradedBuckets[$bucket] ); // not the normal quorum?
-               foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
-                       if ( !$this->isServerUp( $lockSrv ) ) {
-                               $status->warning( 'lockmanager-fail-svr-release', $lockSrv );
-                       } else {
-                               // Attempt to release the lock on this peer
-                               $status->merge( $this->freeLocksOnServer( $lockSrv, $pathsByType ) );
-                               ++$yesVotes; // success for this peer
-                               // Normally the first peers form the quorum, and the others are ignored.
-                               // Ignore them in this case, but not when an alternative quorum was used.
-                               if ( $yesVotes >= $quorum && !$isDegraded ) {
-                                       break; // lock released
-                               }
-                       }
-               }
-               // Set a bad status if the quorum was not met.
-               // Assumes the same "up" servers as during the acquire step.
-               $status->setResult( $yesVotes >= $quorum );
-
-               return $status;
-       }
-
-       /**
-        * Get the bucket for resource path.
-        * This should avoid throwing any exceptions.
-        *
-        * @param string $path
-        * @return int
-        */
-       protected function getBucketFromPath( $path ) {
-               $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
-               return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket );
-       }
-
-       /**
-        * Check if a lock server is up.
-        * This should process cache results to reduce RTT.
-        *
-        * @param string $lockSrv
-        * @return bool
-        */
-       abstract protected function isServerUp( $lockSrv );
-
-       /**
-        * Get a connection to a lock server and acquire locks
-        *
-        * @param string $lockSrv
-        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
-        * @return Status
-        */
-       abstract protected function getLocksOnServer( $lockSrv, array $pathsByType );
-
-       /**
-        * Get a connection to a lock server and release locks on $paths.
-        *
-        * Subclasses must effectively implement this or releaseAllLocks().
-        *
-        * @param string $lockSrv
-        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
-        * @return Status
-        */
-       abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType );
-
-       /**
-        * Release all locks that this session is holding.
-        *
-        * Subclasses must effectively implement this or freeLocksOnServer().
-        *
-        * @return Status
-        */
-       abstract protected function releaseAllLocks();
-}
diff --git a/includes/filebackend/lockmanager/RedisLockManager.php b/includes/filebackend/lockmanager/RedisLockManager.php
deleted file mode 100644 (file)
index 4121ecb..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 = Status::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 = Status::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 Status::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 );
-               }
-       }
-}
diff --git a/includes/filebackend/lockmanager/ScopedLock.php b/includes/filebackend/lockmanager/ScopedLock.php
deleted file mode 100644 (file)
index e1a600c..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-/**
- * Resource locking handling.
- *
- * 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
- * @author Aaron Schulz
- */
-
-/**
- * Self-releasing locks
- *
- * LockManager helper class to handle scoped locks, which
- * release when an object is destroyed or goes out of scope.
- *
- * @ingroup LockManager
- * @since 1.19
- */
-class ScopedLock {
-       /** @var LockManager */
-       protected $manager;
-
-       /** @var Status */
-       protected $status;
-
-       /** @var array Map of lock types to resource paths */
-       protected $pathsByType;
-
-       /**
-        * @param LockManager $manager
-        * @param array $pathsByType Map of lock types to path lists
-        * @param Status $status
-        */
-       protected function __construct( LockManager $manager, array $pathsByType, Status $status ) {
-               $this->manager = $manager;
-               $this->pathsByType = $pathsByType;
-               $this->status = $status;
-       }
-
-       /**
-        * Get a ScopedLock object representing a lock on resource paths.
-        * Any locks are released once this object goes out of scope.
-        * The status object is updated with any errors or warnings.
-        *
-        * @param LockManager $manager
-        * @param array $paths List of storage paths or map of lock types to path lists
-        * @param int|string $type LockManager::LOCK_* constant or "mixed" and $paths
-        *   can be a map of types to paths (since 1.22). Otherwise $type should be an
-        *   integer and $paths should be a list of paths.
-        * @param Status $status
-        * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.22)
-        * @return ScopedLock|null Returns null on failure
-        */
-       public static function factory(
-               LockManager $manager, array $paths, $type, Status $status, $timeout = 0
-       ) {
-               $pathsByType = is_integer( $type ) ? [ $type => $paths ] : $paths;
-               $lockStatus = $manager->lockByType( $pathsByType, $timeout );
-               $status->merge( $lockStatus );
-               if ( $lockStatus->isOK() ) {
-                       return new self( $manager, $pathsByType, $status );
-               }
-
-               return null;
-       }
-
-       /**
-        * Release a scoped lock and set any errors in the attatched Status object.
-        * This is useful for early release of locks before function scope is destroyed.
-        * This is the same as setting the lock object to null.
-        *
-        * @param ScopedLock $lock
-        * @since 1.21
-        */
-       public static function release( ScopedLock &$lock = null ) {
-               $lock = null;
-       }
-
-       /**
-        * Release the locks when this goes out of scope
-        */
-       function __destruct() {
-               $wasOk = $this->status->isOK();
-               $this->status->merge( $this->manager->unlockByType( $this->pathsByType ) );
-               if ( $wasOk ) {
-                       // Make sure status is OK, despite any unlockFiles() fatals
-                       $this->status->setResult( true, $this->status->value );
-               }
-       }
-}
index b24354d..d06acf2 100644 (file)
@@ -66,6 +66,7 @@ class FSRepo extends FileRepo {
                                        "{$repoName}-deleted" => $deletedDir
                                ],
                                'fileMode' => $fileMode,
+                               'tmpDirectory' => wfTempDir()
                        ] );
                        // Update repo config to use this backend
                        $info['backend'] = $backend;
index 596dbde..5bc60a0 100644 (file)
@@ -50,8 +50,10 @@ class FileBackendDBRepoWrapper extends FileBackend {
        protected $dbs;
 
        public function __construct( array $config ) {
-               $config['name'] = $config['backend']->getName();
-               $config['wikiId'] = $config['backend']->getWikiId();
+               /** @var FileBackend $backend */
+               $backend = $config['backend'];
+               $config['name'] = $backend->getName();
+               $config['wikiId'] = $backend->getWikiId();
                parent::__construct( $config );
                $this->backend = $config['backend'];
                $this->repoName = $config['repoName'];
@@ -256,7 +258,7 @@ class FileBackendDBRepoWrapper extends FileBackend {
                return $this->translateSrcParams( __FUNCTION__, $params );
        }
 
-       public function getScopedLocksForOps( array $ops, Status $status ) {
+       public function getScopedLocksForOps( array $ops, StatusValue $status ) {
                return $this->backend->getScopedLocksForOps( $ops, $status );
        }
 
index b8b1cf6..66dab99 100644 (file)
@@ -393,7 +393,7 @@ class FileRepo {
                        if ( $this->oldFileFactory ) {
                                return call_user_func( $this->oldFileFactory, $title, $this, $time );
                        } else {
-                               return false;
+                               return null;
                        }
                } else {
                        return call_user_func( $this->fileFactory, $title, $this );
@@ -818,14 +818,14 @@ class FileRepo {
         *   self::OVERWRITE_SAME    Overwrite the file if the destination exists and has the
         *                           same contents as the source
         *   self::SKIP_LOCKING      Skip any file locking when doing the store
-        * @return FileRepoStatus
+        * @return Status
         */
        public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
                $this->assertWritableRepo(); // fail out if read-only
 
                $status = $this->storeBatch( [ [ $srcPath, $dstZone, $dstRel ] ], $flags );
                if ( $status->successCount == 0 ) {
-                       $status->ok = false;
+                       $status->setOK( false );
                }
 
                return $status;
@@ -841,7 +841,7 @@ class FileRepo {
         *                           same contents as the source
         *   self::SKIP_LOCKING      Skip any file locking when doing the store
         * @throws MWException
-        * @return FileRepoStatus
+        * @return Status
         */
        public function storeBatch( array $triplets, $flags = 0 ) {
                $this->assertWritableRepo(); // fail out if read-only
@@ -912,7 +912,7 @@ class FileRepo {
         * @param array $files List of files to delete
         * @param int $flags Bitwise combination of the following flags:
         *   self::SKIP_LOCKING      Skip any file locking when doing the deletions
-        * @return FileRepoStatus
+        * @return Status
         */
        public function cleanupBatch( array $files, $flags = 0 ) {
                $this->assertWritableRepo(); // fail out if read-only
@@ -952,7 +952,7 @@ class FileRepo {
         * @param array|string|null $options An array consisting of a key named headers
         *   listing extra headers. If a string, taken as content-disposition header.
         *   (Support for array of options new in 1.23)
-        * @return FileRepoStatus
+        * @return Status
         */
        final public function quickImport( $src, $dst, $options = null ) {
                return $this->quickImportBatch( [ [ $src, $dst, $options ] ] );
@@ -964,7 +964,7 @@ class FileRepo {
         * This is intended for purging thumbnails.
         *
         * @param string $path Virtual URL or storage path
-        * @return FileRepoStatus
+        * @return Status
         */
        final public function quickPurge( $path ) {
                return $this->quickPurgeBatch( [ $path ] );
@@ -995,7 +995,7 @@ class FileRepo {
         * When "headers" are given they are used as HTTP headers if supported.
         *
         * @param array $triples List of (source path or FSFile, destination path, disposition)
-        * @return FileRepoStatus
+        * @return Status
         */
        public function quickImportBatch( array $triples ) {
                $status = $this->newGood();
@@ -1040,7 +1040,7 @@ class FileRepo {
         * This does no locking nor journaling and is intended for purging thumbnails.
         *
         * @param array $paths List of virtual URLs or storage paths
-        * @return FileRepoStatus
+        * @return Status
         */
        public function quickPurgeBatch( array $paths ) {
                $status = $this->newGood();
@@ -1065,7 +1065,7 @@ class FileRepo {
         * @param string $originalName The base name of the file as specified
         *   by the user. The file extension will be maintained.
         * @param string $srcPath The current location of the file.
-        * @return FileRepoStatus Object with the URL in the value.
+        * @return Status Object with the URL in the value.
         */
        public function storeTemp( $originalName, $srcPath ) {
                $this->assertWritableRepo(); // fail out if read-only
@@ -1107,7 +1107,7 @@ class FileRepo {
         * @param string $dstPath Target file system path
         * @param int $flags Bitwise combination of the following flags:
         *   self::DELETE_SOURCE     Delete the source files on success
-        * @return FileRepoStatus
+        * @return Status
         */
        public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) {
                $this->assertWritableRepo(); // fail out if read-only
@@ -1156,7 +1156,7 @@ class FileRepo {
         * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
         *   that the source file should be deleted if possible
         * @param array $options Optional additional parameters
-        * @return FileRepoStatus
+        * @return Status
         */
        public function publish(
                $src, $dstRel, $archiveRel, $flags = 0, array $options = []
@@ -1166,7 +1166,7 @@ class FileRepo {
                $status = $this->publishBatch(
                        [ [ $src, $dstRel, $archiveRel, $options ] ], $flags );
                if ( $status->successCount == 0 ) {
-                       $status->ok = false;
+                       $status->setOK( false );
                }
                if ( isset( $status->value[0] ) ) {
                        $status->value = $status->value[0];
@@ -1185,7 +1185,7 @@ class FileRepo {
         * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
         *   that the source files should be deleted if possible
         * @throws MWException
-        * @return FileRepoStatus
+        * @return Status
         */
        public function publishBatch( array $ntuples, $flags = 0 ) {
                $this->assertWritableRepo(); // fail out if read-only
@@ -1322,7 +1322,10 @@ class FileRepo {
                        $params = [ 'noAccess' => true, 'noListing' => true ] + $params;
                }
 
-               return $this->backend->prepare( $params );
+               $status = $this->newGood();
+               $status->merge( $this->backend->prepare( $params ) );
+
+               return $status;
        }
 
        /**
@@ -1380,7 +1383,7 @@ class FileRepo {
         * @param mixed $srcRel Relative path for the file to be deleted
         * @param mixed $archiveRel Relative path for the archive location.
         *   Relative to a private archive directory.
-        * @return FileRepoStatus
+        * @return Status
         */
        public function delete( $srcRel, $archiveRel ) {
                $this->assertWritableRepo(); // fail out if read-only
@@ -1403,7 +1406,7 @@ class FileRepo {
         *   public root in the first element, and the archive file path relative
         *   to the deleted zone root in the second element.
         * @throws MWException
-        * @return FileRepoStatus
+        * @return Status
         */
        public function deleteBatch( array $sourceDestPairs ) {
                $this->assertWritableRepo(); // fail out if read-only
@@ -1539,9 +1542,15 @@ class FileRepo {
         * @return array
         */
        public function getFileProps( $virtualUrl ) {
-               $path = $this->resolveToStoragePath( $virtualUrl );
+               $fsFile = $this->getLocalReference( $virtualUrl );
+               $mwProps = new MWFileProps( MimeMagic::singleton() );
+               if ( $fsFile ) {
+                       $props = $mwProps->getPropsFromPath( $fsFile->getPath(), true );
+               } else {
+                       $props = $mwProps->newPlaceholderProps();
+               }
 
-               return $this->backend->getFileProps( [ 'src' => $path ] );
+               return $props;
        }
 
        /**
@@ -1593,7 +1602,10 @@ class FileRepo {
                $path = $this->resolveToStoragePath( $virtualUrl );
                $params = [ 'src' => $path, 'headers' => $headers, 'options' => $optHeaders ];
 
-               return $this->backend->streamFile( $params );
+               $status = $this->newGood();
+               $status->merge( $this->backend->streamFile( $params ) );
+
+               return $status;
        }
 
        /**
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 001800f..7fb7a0e 100644 (file)
@@ -51,9 +51,12 @@ class ForeignDBRepo extends LocalRepo {
        /** @var bool */
        protected $hasSharedCache;
 
-       # Other stuff
+       /** @var IDatabase */
        protected $dbConn;
+
+       /** @var callable */
        protected $fileFactory = [ 'ForeignDBFile', 'newFromTitle' ];
+       /** @var callable */
        protected $fileFromRowFactory = [ 'ForeignDBFile', 'newFromRow' ];
 
        /**
@@ -106,7 +109,7 @@ class ForeignDBRepo extends LocalRepo {
                ];
 
                return function ( $index ) use ( $type, $params ) {
-                       return DatabaseBase::factory( $type, $params );
+                       return Database::factory( $type, $params );
                };
        }
 
index f8b1ed9..129d55a 100644 (file)
@@ -42,6 +42,9 @@ class ForeignDBViaLBRepo extends LocalRepo {
        /** @var array */
        protected $fileFromRowFactory = [ 'ForeignDBFile', 'newFromRow' ];
 
+       /** @var bool */
+       protected $hasSharedCache;
+
        /**
         * @param array|null $info
         */
@@ -56,23 +59,22 @@ class ForeignDBViaLBRepo extends LocalRepo {
         * @return IDatabase
         */
        function getMasterDB() {
-               return wfGetDB( DB_MASTER, [], $this->wiki );
+               return wfGetLB( $this->wiki )->getConnectionRef( DB_MASTER, [], $this->wiki );
        }
 
        /**
         * @return IDatabase
         */
        function getSlaveDB() {
-               return wfGetDB( DB_REPLICA, [], $this->wiki );
+               return wfGetLB( $this->wiki )->getConnectionRef( DB_REPLICA, [], $this->wiki );
        }
 
        /**
         * @return Closure
         */
        protected function getDBFactory() {
-               $wiki = $this->wiki;
-               return function( $index ) use ( $wiki ) {
-                       return wfGetDB( $index, [], $wiki );
+               return function( $index ) {
+                       return wfGetLB( $this->wiki )->getConnectionRef( $index, [], $this->wiki );
                };
        }
 
index 7b40a7b..c195241 100644 (file)
  * @ingroup FileRepo
  */
 class LocalRepo extends FileRepo {
-       /** @var array */
+       /** @var callable */
        protected $fileFactory = [ 'LocalFile', 'newFromTitle' ];
-
-       /** @var array */
+       /** @var callable */
        protected $fileFactoryKey = [ 'LocalFile', 'newFromKey' ];
-
-       /** @var array */
+       /** @var callable */
        protected $fileFromRowFactory = [ 'LocalFile', 'newFromRow' ];
-
-       /** @var array */
+       /** @var callable */
        protected $oldFileFromRowFactory = [ 'OldLocalFile', 'newFromRow' ];
-
-       /** @var array */
+       /** @var callable */
        protected $oldFileFactory = [ 'OldLocalFile', 'newFromTitle' ];
-
-       /** @var array */
+       /** @var callable */
        protected $oldFileFactoryKey = [ 'OldLocalFile', 'newFromKey' ];
 
        function __construct( array $info = null ) {
                parent::__construct( $info );
 
-               $this->hasSha1Storage = isset( $info['storageLayout'] ) && $info['storageLayout'] === 'sha1';
+               $this->hasSha1Storage = isset( $info['storageLayout'] )
+                       && $info['storageLayout'] === 'sha1';
 
                if ( $this->hasSha1Storage() ) {
                        $this->backend = new FileBackendDBRepoWrapper( [
@@ -93,7 +89,7 @@ class LocalRepo extends FileRepo {
         *
         * @param array $storageKeys
         *
-        * @return FileRepoStatus
+        * @return Status
         */
        function cleanupDeletedBatch( array $storageKeys ) {
                if ( $this->hasSha1Storage() ) {
@@ -454,7 +450,7 @@ class LocalRepo extends FileRepo {
 
        /**
         * Get a connection to the replica DB
-        * @return DatabaseBase
+        * @return IDatabase
         */
        function getSlaveDB() {
                return wfGetDB( DB_REPLICA );
@@ -462,7 +458,7 @@ class LocalRepo extends FileRepo {
 
        /**
         * Get a connection to the master DB
-        * @return DatabaseBase
+        * @return IDatabase
         */
        function getMasterDB() {
                return wfGetDB( DB_MASTER );
@@ -562,7 +558,7 @@ class LocalRepo extends FileRepo {
         *
         * @param string $function
         * @param array $args
-        * @return FileRepoStatus
+        * @return Status
         */
        protected function skipWriteOperationIfSha1( $function, array $args ) {
                $this->assertWritableRepo(); // fail out if read-only
index d515b05..d47624f 100644 (file)
@@ -135,17 +135,18 @@ class RepoGroup {
                }
 
                # Check the cache
+               $dbkey = $title->getDBkey();
                if ( empty( $options['ignoreRedirect'] )
                        && empty( $options['private'] )
                        && empty( $options['bypassCache'] )
                ) {
                        $time = isset( $options['time'] ) ? $options['time'] : '';
-                       $dbkey = $title->getDBkey();
                        if ( $this->cache->has( $dbkey, $time, 60 ) ) {
                                return $this->cache->get( $dbkey, $time );
                        }
                        $useCache = true;
                } else {
+                       $time = false;
                        $useCache = false;
                }
 
@@ -451,7 +452,9 @@ class RepoGroup {
 
                        return $repo->getFileProps( $fileName );
                } else {
-                       return FSFile::getPropsFromPath( $fileName );
+                       $mwProps = new MWFileProps( MimeMagic::singleton() );
+
+                       return $mwProps->getPropsFromPath( $fileName, true );
                }
        }
 
index d1e683a..921e129 100644 (file)
@@ -425,6 +425,7 @@ class ArchivedFile {
         */
        function pageCount() {
                if ( !isset( $this->pageCount ) ) {
+                       // @FIXME: callers expect File objects
                        if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
                                $this->pageCount = $this->handler->pageCount( $this );
                        } else {
index 425a08c..c1d5573 100644 (file)
@@ -1328,7 +1328,7 @@ abstract class File implements IDBAccessObject {
         */
        protected function makeTransformTmpFile( $thumbPath ) {
                $thumbExt = FileBackend::extensionFromPath( $thumbPath );
-               return TempFSFile::factory( 'transform_', $thumbExt );
+               return TempFSFile::factory( 'transform_', $thumbExt, wfTempDir() );
        }
 
        /**
@@ -1805,7 +1805,7 @@ abstract class File implements IDBAccessObject {
         * @param int $flags A bitwise combination of:
         *   File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
         * @param array $options Optional additional parameters
-        * @return FileRepoStatus On success, the value member contains the
+        * @return Status On success, the value member contains the
         *   archive name, or an empty string if it was a new file.
         *
         * STUB
@@ -1905,7 +1905,7 @@ abstract class File implements IDBAccessObject {
         * and logging are caller's responsibility
         *
         * @param Title $target New file name
-        * @return FileRepoStatus
+        * @return Status
         */
        function move( $target ) {
                $this->readOnlyError();
@@ -1922,7 +1922,7 @@ abstract class File implements IDBAccessObject {
         * @param string $reason
         * @param bool $suppress Hide content from sysops?
         * @param User|null $user
-        * @return FileRepoStatus
+        * @return Status
         * STUB
         * Overridden by LocalFile
         */
index f6752d8..43b6855 100644 (file)
  * @ingroup FileAbstraction
  */
 class ForeignAPIFile extends File {
+       /** @var bool */
        private $mExists;
+       /** @var array */
+       private $mInfo = [];
 
        protected $repoClass = 'ForeignApiRepo';
 
@@ -244,7 +247,7 @@ class ForeignAPIFile extends File {
        public function getUser( $type = 'text' ) {
                if ( $type == 'text' ) {
                        return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null;
-               } elseif ( $type == 'id' ) {
+               } else {
                        return 0; // What makes sense here, for a remote user?
                }
        }
@@ -344,9 +347,6 @@ class ForeignAPIFile extends File {
                return $files;
        }
 
-       /**
-        * @see File::purgeCache()
-        */
        function purgeCache( $options = [] ) {
                $this->purgeThumbnails( $options );
                $this->purgeDescriptionPage();
index cf0045e..c041dea 100644 (file)
@@ -57,7 +57,7 @@ class ForeignDBFile extends LocalFile {
         * @param string $srcPath
         * @param int $flags
         * @param array $options
-        * @return FileRepoStatus
+        * @return Status
         * @throws MWException
         */
        function publish( $srcPath, $flags = 0, array $options = [] ) {
@@ -84,7 +84,7 @@ class ForeignDBFile extends LocalFile {
        /**
         * @param array $versions
         * @param bool $unsuppress
-        * @return FileRepoStatus
+        * @return Status
         * @throws MWException
         */
        function restore( $versions = [], $unsuppress = false ) {
@@ -95,7 +95,7 @@ class ForeignDBFile extends LocalFile {
         * @param string $reason
         * @param bool $suppress
         * @param User|null $user
-        * @return FileRepoStatus
+        * @return Status
         * @throws MWException
         */
        function delete( $reason, $suppress = false, $user = null ) {
@@ -104,7 +104,7 @@ class ForeignDBFile extends LocalFile {
 
        /**
         * @param Title $target
-        * @return FileRepoStatus
+        * @return Status
         * @throws MWException
         */
        function move( $target ) {
index 618272c..9df9360 100644 (file)
@@ -1160,7 +1160,7 @@ class LocalFile extends File {
         * @param User|null $user User object or null to use $wgUser
         * @param string[] $tags Change tags to add to the log entry and page revision.
         *   (This doesn't check $user's permissions.)
-        * @return FileRepoStatus On success, the value member contains the
+        * @return Status On success, the value member contains the
         *     archive name, or an empty string if it was a new file.
         */
        function upload( $src, $comment, $pageText, $flags = 0, $props = false,
@@ -1179,7 +1179,8 @@ class LocalFile extends File {
                        ) {
                                $props = $this->repo->getFileProps( $srcPath );
                        } else {
-                               $props = FSFile::getPropsFromPath( $srcPath );
+                               $mwProps = new MWFileProps( MimeMagic::singleton() );
+                               $props = $mwProps->getPropsFromPath( $srcPath, true );
                        }
                }
 
@@ -1480,8 +1481,10 @@ class LocalFile extends File {
                                                );
 
                                                if ( isset( $status->value['revision'] ) ) {
+                                                       /** @var $rev Revision */
+                                                       $rev = $status->value['revision'];
                                                        // Associate new page revision id
-                                                       $logEntry->setAssociatedRevId( $status->value['revision']->getId() );
+                                                       $logEntry->setAssociatedRevId( $rev->getId() );
                                                }
                                                // This relies on the resetArticleID() call in WikiPage::insertOn(),
                                                // which is triggered on $descTitle by doEditContent() above.
@@ -1579,7 +1582,7 @@ class LocalFile extends File {
         * @param int $flags A bitwise combination of:
         *     File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
         * @param array $options Optional additional parameters
-        * @return FileRepoStatus On success, the value member contains the
+        * @return Status On success, the value member contains the
         *     archive name, or an empty string if it was a new file.
         */
        function publish( $src, $flags = 0, array $options = [] ) {
@@ -1598,7 +1601,7 @@ class LocalFile extends File {
         * @param int $flags A bitwise combination of:
         *     File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
         * @param array $options Optional additional parameters
-        * @return FileRepoStatus On success, the value member contains the
+        * @return Status On success, the value member contains the
         *     archive name, or an empty string if it was a new file.
         */
        function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
@@ -1660,7 +1663,7 @@ class LocalFile extends File {
         * and logging are caller's responsibility
         *
         * @param Title $target New file name
-        * @return FileRepoStatus
+        * @return Status
         */
        function move( $target ) {
                if ( $this->getRepo()->getReadOnlyReason() !== false ) {
@@ -1719,7 +1722,7 @@ class LocalFile extends File {
         * @param string $reason
         * @param bool $suppress
         * @param User|null $user
-        * @return FileRepoStatus
+        * @return Status
         */
        function delete( $reason, $suppress = false, $user = null ) {
                if ( $this->getRepo()->getReadOnlyReason() !== false ) {
@@ -1777,7 +1780,7 @@ class LocalFile extends File {
         * @param bool $suppress
         * @param User|null $user
         * @throws MWException Exception on database or file store failure
-        * @return FileRepoStatus
+        * @return Status
         */
        function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
                if ( $this->getRepo()->getReadOnlyReason() !== false ) {
@@ -1813,7 +1816,7 @@ class LocalFile extends File {
         * @param array $versions Set of record ids of deleted items to restore,
         *   or empty to restore all revisions.
         * @param bool $unsuppress
-        * @return FileRepoStatus
+        * @return Status
         */
        function restore( $versions = [], $unsuppress = false ) {
                if ( $this->getRepo()->getReadOnlyReason() !== false ) {
@@ -2343,7 +2346,7 @@ class LocalFileDeleteBatch {
 
        /**
         * Run the transaction
-        * @return FileRepoStatus
+        * @return Status
         */
        public function execute() {
                $repo = $this->file->getRepo();
@@ -2491,7 +2494,7 @@ class LocalFileRestoreBatch {
         * rows and there's no need to keep the image row locked while it's acquiring those locks
         * The caller may have its own transaction open.
         * So we save the batch and let the caller call cleanup()
-        * @return FileRepoStatus
+        * @return Status
         */
        public function execute() {
                /** @var Language */
@@ -2692,7 +2695,7 @@ class LocalFileRestoreBatch {
                                // Even if some files could be copied, fail entirely as that is the
                                // easiest thing to do without data loss
                                $this->cleanupFailedBatch( $storeStatus, $storeBatch );
-                               $status->ok = false;
+                               $status->setOK( false );
                                $this->file->unlock();
 
                                return $status;
@@ -2792,7 +2795,7 @@ class LocalFileRestoreBatch {
        /**
         * Delete unused files in the deleted zone.
         * This should be called from outside the transaction in which execute() was called.
-        * @return FileRepoStatus
+        * @return Status
         */
        public function cleanup() {
                if ( !$this->cleanupBatch ) {
@@ -2849,7 +2852,7 @@ class LocalFileMoveBatch {
 
        protected $archive;
 
-       /** @var DatabaseBase */
+       /** @var IDatabase */
        protected $db;
 
        /**
@@ -2927,7 +2930,7 @@ class LocalFileMoveBatch {
 
        /**
         * Perform the move.
-        * @return FileRepoStatus
+        * @return Status
         */
        public function execute() {
                $repo = $this->file->repo;
@@ -2952,7 +2955,7 @@ class LocalFileMoveBatch {
                if ( !$statusDb->isGood() ) {
                        $destFile->unlock();
                        $this->file->unlock();
-                       $statusDb->ok = false;
+                       $statusDb->setOK( false );
 
                        return $statusDb;
                }
@@ -2971,7 +2974,7 @@ class LocalFileMoveBatch {
                                $this->file->unlock();
                                wfDebugLog( 'imagemove', "Error in moving files: "
                                        . $statusMove->getWikiText( false, false, 'en' ) );
-                               $statusMove->ok = false;
+                               $statusMove->setOK( false );
 
                                return $statusMove;
                        }
@@ -2999,7 +3002,7 @@ class LocalFileMoveBatch {
         * Verify the database updates and return a new FileRepoStatus indicating how
         * many rows would be updated.
         *
-        * @return FileRepoStatus
+        * @return Status
         */
        protected function verifyDBUpdates() {
                $repo = $this->file->repo;
index 31e62ec..a17ca6e 100644 (file)
@@ -332,7 +332,7 @@ class OldLocalFile extends LocalFile {
         * @param string $timestamp
         * @param string $comment
         * @param User $user
-        * @return FileRepoStatus
+        * @return Status
         */
        function uploadOld( $srcPath, $archiveName, $timestamp, $comment, $user ) {
                $this->lock();
index f6527b8..0f889da 100644 (file)
@@ -238,8 +238,8 @@ class TraditionalImageGallery extends ImageGalleryBase {
        }
 
        /**
-        * How much padding such the thumb have between image and inner div that
-        * that contains the border. This is both for verical and horizontal
+        * How much padding the thumb has between the image and the inner div
+        * that contains the border. This is for both vertical and horizontal
         * padding. (However, it is cut in half in the vertical direction).
         * @return int
         */
index 3c88594..1f4d99e 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.
@@ -173,7 +176,7 @@ class HTMLForm extends ContextSource {
        protected $mFieldTree;
        protected $mShowReset = false;
        protected $mShowSubmit = true;
-       protected $mSubmitFlags = [ 'constructive', 'primary' ];
+       protected $mSubmitFlags = [ 'primary', 'progressive' ];
        protected $mShowCancel = false;
        protected $mCancelTarget;
 
@@ -1014,7 +1017,8 @@ class HTMLForm extends ContextSource {
                $this->getOutput()->addModuleStyles( 'mediawiki.htmlform.styles' );
 
                $html = ''
-                       . $this->getErrors( $submitResult )
+                       . $this->getErrorsOrWarnings( $submitResult, 'error' )
+                       . $this->getErrorsOrWarnings( $submitResult, 'warning' )
                        . $this->getHeaderText()
                        . $this->getBody()
                        . $this->getHiddenFields()
@@ -1230,23 +1234,46 @@ class HTMLForm extends ContextSource {
         *
         * @param string|array|Status $errors
         *
+        * @deprecated since 1.28, use getErrorsOrWarnings() instead
+        *
         * @return string
         */
        public function getErrors( $errors ) {
-               if ( $errors instanceof Status ) {
-                       if ( $errors->isOK() ) {
-                               $errorstr = '';
+               wfDeprecated( __METHOD__ );
+               return $this->getErrorsOrWarnings( $errors, 'error' );
+       }
+
+       /**
+        * Returns a formatted list of errors or warnings from the given elements.
+        *
+        * @param string|array|Status $elements The set of errors/warnings to process.
+        * @param string $elementsType Should warnings or errors be returned.  This is meant
+        *      for Status objects, all other valid types are always considered as errors.
+        * @return string
+        */
+       public function getErrorsOrWarnings( $elements, $elementsType ) {
+               if ( !in_array( $elementsType, [ 'error', 'warning' ], true ) ) {
+                       throw new DomainException( $elementsType . ' is not a valid type.' );
+               }
+               $elementstr = false;
+               if ( $elements instanceof Status ) {
+                       list( $errorStatus, $warningStatus ) = $elements->splitByErrorType();
+                       $status = $elementsType === 'error' ? $errorStatus : $warningStatus;
+                       if ( $status->isGood() ) {
+                               $elementstr = '';
                        } else {
-                               $errorstr = $this->getOutput()->parse( $errors->getWikiText() );
+                               $elementstr = $this->getOutput()->parse(
+                                       $status->getWikiText()
+                               );
                        }
-               } elseif ( is_array( $errors ) ) {
-                       $errorstr = $this->formatErrors( $errors );
-               } else {
-                       $errorstr = $errors;
+               } elseif ( is_array( $elements ) && $elementsType === 'error' ) {
+                       $elementstr = $this->formatErrors( $elements );
+               } elseif ( $elementsType === 'error' ) {
+                       $elementstr = $elements;
                }
 
-               return $errorstr
-                       ? Html::rawElement( 'div', [ 'class' => 'error' ], $errorstr )
+               return $elementstr
+                       ? Html::rawElement( 'div', [ 'class' => $elementsType ], $elementstr )
                        : '';
        }
 
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;
index 0b22727..6fbf15b 100644 (file)
@@ -26,6 +26,7 @@
  */
 class OOUIHTMLForm extends HTMLForm {
        private $oouiErrors;
+       private $oouiWarnings;
 
        public function __construct( $descriptor, $context = null, $messagePrefix = '' ) {
                parent::__construct( $descriptor, $context, $messagePrefix );
@@ -185,28 +186,34 @@ class OOUIHTMLForm extends HTMLForm {
        }
 
        /**
-        * @param string|array|Status $err
+        * @param string|array|Status $elements
+        * @param string $elementsType
         * @return string
         */
-       function getErrors( $err ) {
-               if ( !$err ) {
+       function getErrorsOrWarnings( $elements, $elementsType ) {
+               if ( !in_array( $elementsType, [ 'error', 'warning' ] ) ) {
+                       throw new DomainException( $elementsType . ' is not a valid type.' );
+               }
+               if ( !$elements ) {
                        $errors = [];
-               } elseif ( $err instanceof Status ) {
-                       if ( $err->isOK() ) {
+               } elseif ( $elements instanceof Status ) {
+                       if ( $elements->isGood() ) {
                                $errors = [];
                        } else {
-                               $errors = $err->getErrorsByType( 'error' );
+                               $errors = $elements->getErrorsByType( $elementsType );
                                foreach ( $errors as &$error ) {
-                                       // Input:  array( 'message' => 'foo', 'errors' => array( 'a', 'b', 'c' ) )
-                                       // Output: array( 'foo', 'a', 'b', 'c' )
+                                       // Input:  [ 'message' => 'foo', 'errors' => [ 'a', 'b', 'c' ] ]
+                                       // Output: [ 'foo', 'a', 'b', 'c' ]
                                        $error = array_merge( [ $error['message'] ], $error['params'] );
                                }
                        }
-               } else {
-                       $errors = $err;
+               } elseif ( $elementsType === 'errors' )  {
+                       $errors = $elements;
                        if ( !is_array( $errors ) ) {
                                $errors = [ $errors ];
                        }
+               } else {
+                       $errors = [];
                }
 
                foreach ( $errors as &$error ) {
@@ -215,7 +222,11 @@ class OOUIHTMLForm extends HTMLForm {
                }
 
                // Used in getBody()
-               $this->oouiErrors = $errors;
+               if ( $elementsType === 'error' ) {
+                       $this->oouiErrors = $errors;
+               } else {
+                       $this->oouiWarnings = $errors;
+               }
                return '';
        }
 
@@ -236,7 +247,10 @@ class OOUIHTMLForm extends HTMLForm {
                        if ( $this->oouiErrors ) {
                                $classes[] = 'mw-htmlform-ooui-header-errors';
                        }
-                       if ( $this->mHeader || $this->oouiErrors ) {
+                       if ( $this->oouiWarnings ) {
+                               $classes[] = 'mw-htmlform-ooui-header-warnings';
+                       }
+                       if ( $this->mHeader || $this->oouiErrors || $this->oouiWarnings ) {
                                // if there's no header, don't create an (empty) LabelWidget, simply use a placeholder
                                if ( $this->mHeader ) {
                                        $element = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( $this->mHeader ) ] );
@@ -249,6 +263,7 @@ class OOUIHTMLForm extends HTMLForm {
                                                [
                                                        'align' => 'top',
                                                        'errors' => $this->oouiErrors,
+                                                       'notices' => $this->oouiWarnings,
                                                        'classes' => $classes,
                                                ]
                                        )
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 cb98549..0c33ad9 100644 (file)
@@ -7,7 +7,7 @@
 class HTMLSubmitField extends HTMLButtonField {
        protected $buttonType = 'submit';
 
-       protected $mFlags = [ 'primary', 'constructive' ];
+       protected $mFlags = [ 'primary', 'progressive' ];
 
        public function skipLoadData( $request ) {
                return !$request->getCheck( $this->mName );
diff --git a/includes/http/CurlHttpRequest.php b/includes/http/CurlHttpRequest.php
new file mode 100644 (file)
index 0000000..f58c3a9
--- /dev/null
@@ -0,0 +1,165 @@
+<?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
+ */
+
+/**
+ * MWHttpRequest implemented using internal curl compiled into PHP
+ */
+class CurlHttpRequest extends MWHttpRequest {
+       const SUPPORTS_FILE_POSTS = true;
+
+       protected $curlOptions = [];
+       protected $headerText = "";
+
+       /**
+        * @param resource $fh
+        * @param string $content
+        * @return int
+        */
+       protected function readHeader( $fh, $content ) {
+               $this->headerText .= $content;
+               return strlen( $content );
+       }
+
+       public function execute() {
+
+               parent::execute();
+
+               if ( !$this->status->isOK() ) {
+                       return $this->status;
+               }
+
+               $this->curlOptions[CURLOPT_PROXY] = $this->proxy;
+               $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout;
+
+               // Only supported in curl >= 7.16.2
+               if ( defined( 'CURLOPT_CONNECTTIMEOUT_MS' ) ) {
+                       $this->curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = $this->connectTimeout * 1000;
+               }
+
+               $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
+               $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
+               $this->curlOptions[CURLOPT_HEADERFUNCTION] = [ $this, "readHeader" ];
+               $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects;
+               $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression
+
+               $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent'];
+
+               $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0;
+               $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert;
+
+               if ( $this->caInfo ) {
+                       $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo;
+               }
+
+               if ( $this->headersOnly ) {
+                       $this->curlOptions[CURLOPT_NOBODY] = true;
+                       $this->curlOptions[CURLOPT_HEADER] = true;
+               } elseif ( $this->method == 'POST' ) {
+                       $this->curlOptions[CURLOPT_POST] = true;
+                       $postData = $this->postData;
+                       // Don't interpret POST parameters starting with '@' as file uploads, because this
+                       // makes it impossible to POST plain values starting with '@' (and causes security
+                       // issues potentially exposing the contents of local files).
+                       // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6,
+                       // but we support lower versions, and the option doesn't exist in HHVM 5.6.99.
+                       if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) {
+                               $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true;
+                       } elseif ( is_array( $postData ) ) {
+                               // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
+                               // is an array, but not if it's a string. So convert $req['body'] to a string
+                               // for safety.
+                               $postData = wfArrayToCgi( $postData );
+                       }
+                       $this->curlOptions[CURLOPT_POSTFIELDS] = $postData;
+
+                       // Suppress 'Expect: 100-continue' header, as some servers
+                       // will reject it with a 417 and Curl won't auto retry
+                       // with HTTP 1.0 fallback
+                       $this->reqHeaders['Expect'] = '';
+               } else {
+                       $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method;
+               }
+
+               $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList();
+
+               $curlHandle = curl_init( $this->url );
+
+               if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) {
+                       throw new MWException( "Error setting curl options." );
+               }
+
+               if ( $this->followRedirects && $this->canFollowRedirects() ) {
+                       MediaWiki\suppressWarnings();
+                       if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) {
+                               $this->logger->debug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " .
+                                       "Probably open_basedir is set.\n" );
+                               // Continue the processing. If it were in curl_setopt_array,
+                               // processing would have halted on its entry
+                       }
+                       MediaWiki\restoreWarnings();
+               }
+
+               if ( $this->profiler ) {
+                       $profileSection = $this->profiler->scopedProfileIn(
+                               __METHOD__ . '-' . $this->profileName
+                       );
+               }
+
+               $curlRes = curl_exec( $curlHandle );
+               if ( curl_errno( $curlHandle ) == CURLE_OPERATION_TIMEOUTED ) {
+                       $this->status->fatal( 'http-timed-out', $this->url );
+               } elseif ( $curlRes === false ) {
+                       $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) );
+               } else {
+                       $this->headerList = explode( "\r\n", $this->headerText );
+               }
+
+               curl_close( $curlHandle );
+
+               if ( $this->profiler ) {
+                       $this->profiler->scopedProfileOut( $profileSection );
+               }
+
+               $this->parseHeader();
+               $this->setStatus();
+
+               return $this->status;
+       }
+
+       /**
+        * @return bool
+        */
+       public function canFollowRedirects() {
+               $curlVersionInfo = curl_version();
+               if ( $curlVersionInfo['version_number'] < 0x071304 ) {
+                       $this->logger->debug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" );
+                       return false;
+               }
+
+               if ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
+                       if ( strval( ini_get( 'open_basedir' ) ) !== '' ) {
+                               $this->logger->debug( "Cannot follow redirects when open_basedir is set\n" );
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+}
diff --git a/includes/http/Http.php b/includes/http/Http.php
new file mode 100644 (file)
index 0000000..43ae2d0
--- /dev/null
@@ -0,0 +1,163 @@
+<?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
+ */
+
+use MediaWiki\Logger\LoggerFactory;
+
+/**
+ * Various HTTP related functions
+ * @ingroup HTTP
+ */
+class Http {
+       static public $httpEngine = false;
+
+       /**
+        * Perform an HTTP request
+        *
+        * @param string $method HTTP method. Usually GET/POST
+        * @param string $url Full URL to act on. If protocol-relative, will be expanded to an http:// URL
+        * @param array $options Options to pass to MWHttpRequest object.
+        *      Possible keys for the array:
+        *    - timeout             Timeout length in seconds
+        *    - connectTimeout      Timeout for connection, in seconds (curl only)
+        *    - postData            An array of key-value pairs or a url-encoded form data
+        *    - proxy               The proxy to use.
+        *                          Otherwise it will use $wgHTTPProxy (if set)
+        *                          Otherwise it will use the environment variable "http_proxy" (if set)
+        *    - noProxy             Don't use any proxy at all. Takes precedence over proxy value(s).
+        *    - sslVerifyHost       Verify hostname against certificate
+        *    - sslVerifyCert       Verify SSL certificate
+        *    - caInfo              Provide CA information
+        *    - maxRedirects        Maximum number of redirects to follow (defaults to 5)
+        *    - followRedirects     Whether to follow redirects (defaults to false).
+        *                                  Note: this should only be used when the target URL is trusted,
+        *                                  to avoid attacks on intranet services accessible by HTTP.
+        *    - userAgent           A user agent, if you want to override the default
+        *                          MediaWiki/$wgVersion
+        *    - logger              A \Psr\Logger\LoggerInterface instance for debug logging
+        * @param string $caller The method making this request, for profiling
+        * @return string|bool (bool)false on failure or a string on success
+        */
+       public static function request( $method, $url, $options = [], $caller = __METHOD__ ) {
+               wfDebug( "HTTP: $method: $url\n" );
+
+               $options['method'] = strtoupper( $method );
+
+               if ( !isset( $options['timeout'] ) ) {
+                       $options['timeout'] = 'default';
+               }
+               if ( !isset( $options['connectTimeout'] ) ) {
+                       $options['connectTimeout'] = 'default';
+               }
+
+               $req = MWHttpRequest::factory( $url, $options, $caller );
+               $status = $req->execute();
+
+               if ( $status->isOK() ) {
+                       return $req->getContent();
+               } else {
+                       $errors = $status->getErrorsByType( 'error' );
+                       $logger = LoggerFactory::getInstance( 'http' );
+                       $logger->warning( $status->getWikiText( false, false, 'en' ),
+                               [ 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ] );
+                       return false;
+               }
+       }
+
+       /**
+        * Simple wrapper for Http::request( 'GET' )
+        * @see Http::request()
+        * @since 1.25 Second parameter $timeout removed. Second parameter
+        * is now $options which can be given a 'timeout'
+        *
+        * @param string $url
+        * @param array $options
+        * @param string $caller The method making this request, for profiling
+        * @return string|bool false on error
+        */
+       public static function get( $url, $options = [], $caller = __METHOD__ ) {
+               $args = func_get_args();
+               if ( isset( $args[1] ) && ( is_string( $args[1] ) || is_numeric( $args[1] ) ) ) {
+                       // Second was used to be the timeout
+                       // And third parameter used to be $options
+                       wfWarn( "Second parameter should not be a timeout.", 2 );
+                       $options = isset( $args[2] ) && is_array( $args[2] ) ?
+                               $args[2] : [];
+                       $options['timeout'] = $args[1];
+                       $caller = __METHOD__;
+               }
+               return Http::request( 'GET', $url, $options, $caller );
+       }
+
+       /**
+        * Simple wrapper for Http::request( 'POST' )
+        * @see Http::request()
+        *
+        * @param string $url
+        * @param array $options
+        * @param string $caller The method making this request, for profiling
+        * @return string|bool false on error
+        */
+       public static function post( $url, $options = [], $caller = __METHOD__ ) {
+               return Http::request( 'POST', $url, $options, $caller );
+       }
+
+       /**
+        * A standard user-agent we can use for external requests.
+        * @return string
+        */
+       public static function userAgent() {
+               global $wgVersion;
+               return "MediaWiki/$wgVersion";
+       }
+
+       /**
+        * Checks that the given URI is a valid one. Hardcoding the
+        * protocols, because we only want protocols that both cURL
+        * and php support.
+        *
+        * file:// should not be allowed here for security purpose (r67684)
+        *
+        * @todo FIXME this is wildly inaccurate and fails to actually check most stuff
+        *
+        * @param string $uri URI to check for validity
+        * @return bool
+        */
+       public static function isValidURI( $uri ) {
+               return preg_match(
+                       '/^https?:\/\/[^\/\s]\S*$/D',
+                       $uri
+               );
+       }
+
+       /**
+        * Gets the relevant proxy from $wgHTTPProxy
+        *
+        * @return mixed The proxy address or an empty string if not set.
+        */
+       public static function getProxy() {
+               global $wgHTTPProxy;
+
+               if ( $wgHTTPProxy ) {
+                       return $wgHTTPProxy;
+               }
+
+               return "";
+       }
+}
diff --git a/includes/http/MWHttpRequest.php b/includes/http/MWHttpRequest.php
new file mode 100644 (file)
index 0000000..458854a
--- /dev/null
@@ -0,0 +1,618 @@
+<?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
+ */
+
+use MediaWiki\Logger\LoggerFactory;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * This wrapper class will call out to curl (if available) or fallback
+ * to regular PHP if necessary for handling internal HTTP requests.
+ *
+ * Renamed from HttpRequest to MWHttpRequest to avoid conflict with
+ * PHP's HTTP extension.
+ */
+class MWHttpRequest implements LoggerAwareInterface {
+       const SUPPORTS_FILE_POSTS = false;
+
+       protected $content;
+       protected $timeout = 'default';
+       protected $headersOnly = null;
+       protected $postData = null;
+       protected $proxy = null;
+       protected $noProxy = false;
+       protected $sslVerifyHost = true;
+       protected $sslVerifyCert = true;
+       protected $caInfo = null;
+       protected $method = "GET";
+       protected $reqHeaders = [];
+       protected $url;
+       protected $parsedUrl;
+       protected $callback;
+       protected $maxRedirects = 5;
+       protected $followRedirects = false;
+
+       /**
+        * @var CookieJar
+        */
+       protected $cookieJar;
+
+       protected $headerList = [];
+       protected $respVersion = "0.9";
+       protected $respStatus = "200 Ok";
+       protected $respHeaders = [];
+
+       public $status;
+
+       /**
+        * @var Profiler
+        */
+       protected $profiler;
+
+       /**
+        * @var string
+        */
+       protected $profileName;
+
+       /**
+        * @var LoggerInterface;
+        */
+       protected $logger;
+
+       /**
+        * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL
+        * @param array $options (optional) extra params to pass (see Http::request())
+        * @param string $caller The method making this request, for profiling
+        * @param Profiler $profiler An instance of the profiler for profiling, or null
+        */
+       protected function __construct(
+               $url, $options = [], $caller = __METHOD__, $profiler = null
+       ) {
+               global $wgHTTPTimeout, $wgHTTPConnectTimeout;
+
+               $this->url = wfExpandUrl( $url, PROTO_HTTP );
+               $this->parsedUrl = wfParseUrl( $this->url );
+
+               if ( isset( $options['logger'] ) ) {
+                       $this->logger = $options['logger'];
+               } else {
+                       $this->logger = new NullLogger();
+               }
+
+               if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) {
+                       $this->status = Status::newFatal( 'http-invalid-url', $url );
+               } else {
+                       $this->status = Status::newGood( 100 ); // continue
+               }
+
+               if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) {
+                       $this->timeout = $options['timeout'];
+               } else {
+                       $this->timeout = $wgHTTPTimeout;
+               }
+               if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) {
+                       $this->connectTimeout = $options['connectTimeout'];
+               } else {
+                       $this->connectTimeout = $wgHTTPConnectTimeout;
+               }
+               if ( isset( $options['userAgent'] ) ) {
+                       $this->setUserAgent( $options['userAgent'] );
+               }
+
+               $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
+                               "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ];
+
+               foreach ( $members as $o ) {
+                       if ( isset( $options[$o] ) ) {
+                               // ensure that MWHttpRequest::method is always
+                               // uppercased. Bug 36137
+                               if ( $o == 'method' ) {
+                                       $options[$o] = strtoupper( $options[$o] );
+                               }
+                               $this->$o = $options[$o];
+                       }
+               }
+
+               if ( $this->noProxy ) {
+                       $this->proxy = ''; // noProxy takes precedence
+               }
+
+               // Profile based on what's calling us
+               $this->profiler = $profiler;
+               $this->profileName = $caller;
+       }
+
+       /**
+        * @param LoggerInterface $logger
+        */
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * Simple function to test if we can make any sort of requests at all, using
+        * cURL or fopen()
+        * @return bool
+        */
+       public static function canMakeRequests() {
+               return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
+       }
+
+       /**
+        * Generate a new request object
+        * @param string $url Url to use
+        * @param array $options (optional) extra params to pass (see Http::request())
+        * @param string $caller The method making this request, for profiling
+        * @throws MWException
+        * @return CurlHttpRequest|PhpHttpRequest
+        * @see MWHttpRequest::__construct
+        */
+       public static function factory( $url, $options = null, $caller = __METHOD__ ) {
+               if ( !Http::$httpEngine ) {
+                       Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
+               } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
+                       throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' .
+                               ' Http::$httpEngine is set to "curl"' );
+               }
+
+               if ( !is_array( $options ) ) {
+                       $options = [];
+               }
+
+               if ( !isset( $options['logger'] ) ) {
+                       $options['logger'] = LoggerFactory::getInstance( 'http' );
+               }
+
+               switch ( Http::$httpEngine ) {
+                       case 'curl':
+                               return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() );
+                       case 'php':
+                               if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
+                                       throw new MWException( __METHOD__ . ': allow_url_fopen ' .
+                                               'needs to be enabled for pure PHP http requests to ' .
+                                               'work. If possible, curl should be used instead. See ' .
+                                               'http://php.net/curl.'
+                                       );
+                               }
+                               return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() );
+                       default:
+                               throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' );
+               }
+       }
+
+       /**
+        * Get the body, or content, of the response to the request
+        *
+        * @return string
+        */
+       public function getContent() {
+               return $this->content;
+       }
+
+       /**
+        * Set the parameters of the request
+        *
+        * @param array $args
+        * @todo overload the args param
+        */
+       public function setData( $args ) {
+               $this->postData = $args;
+       }
+
+       /**
+        * Take care of setting up the proxy (do nothing if "noProxy" is set)
+        *
+        * @return void
+        */
+       public function proxySetup() {
+               // If there is an explicit proxy set and proxies are not disabled, then use it
+               if ( $this->proxy && !$this->noProxy ) {
+                       return;
+               }
+
+               // Otherwise, fallback to $wgHTTPProxy if this is not a machine
+               // local URL and proxies are not disabled
+               if ( self::isLocalURL( $this->url ) || $this->noProxy ) {
+                       $this->proxy = '';
+               } else {
+                       $this->proxy = Http::getProxy();
+               }
+       }
+
+       /**
+        * Check if the URL can be served by localhost
+        *
+        * @param string $url Full url to check
+        * @return bool
+        */
+       private static function isLocalURL( $url ) {
+               global $wgCommandLineMode, $wgLocalVirtualHosts;
+
+               if ( $wgCommandLineMode ) {
+                       return false;
+               }
+
+               // Extract host part
+               $matches = [];
+               if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) {
+                       $host = $matches[1];
+                       // Split up dotwise
+                       $domainParts = explode( '.', $host );
+                       // Check if this domain or any superdomain is listed as a local virtual host
+                       $domainParts = array_reverse( $domainParts );
+
+                       $domain = '';
+                       $countParts = count( $domainParts );
+                       for ( $i = 0; $i < $countParts; $i++ ) {
+                               $domainPart = $domainParts[$i];
+                               if ( $i == 0 ) {
+                                       $domain = $domainPart;
+                               } else {
+                                       $domain = $domainPart . '.' . $domain;
+                               }
+
+                               if ( in_array( $domain, $wgLocalVirtualHosts ) ) {
+                                       return true;
+                               }
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Set the user agent
+        * @param string $UA
+        */
+       public function setUserAgent( $UA ) {
+               $this->setHeader( 'User-Agent', $UA );
+       }
+
+       /**
+        * Set an arbitrary header
+        * @param string $name
+        * @param string $value
+        */
+       public function setHeader( $name, $value ) {
+               // I feel like I should normalize the case here...
+               $this->reqHeaders[$name] = $value;
+       }
+
+       /**
+        * Get an array of the headers
+        * @return array
+        */
+       public function getHeaderList() {
+               $list = [];
+
+               if ( $this->cookieJar ) {
+                       $this->reqHeaders['Cookie'] =
+                               $this->cookieJar->serializeToHttpRequest(
+                                       $this->parsedUrl['path'],
+                                       $this->parsedUrl['host']
+                               );
+               }
+
+               foreach ( $this->reqHeaders as $name => $value ) {
+                       $list[] = "$name: $value";
+               }
+
+               return $list;
+       }
+
+       /**
+        * Set a read callback to accept data read from the HTTP request.
+        * By default, data is appended to an internal buffer which can be
+        * retrieved through $req->getContent().
+        *
+        * To handle data as it comes in -- especially for large files that
+        * would not fit in memory -- you can instead set your own callback,
+        * in the form function($resource, $buffer) where the first parameter
+        * is the low-level resource being read (implementation specific),
+        * and the second parameter is the data buffer.
+        *
+        * You MUST return the number of bytes handled in the buffer; if fewer
+        * bytes are reported handled than were passed to you, the HTTP fetch
+        * will be aborted.
+        *
+        * @param callable $callback
+        * @throws MWException
+        */
+       public function setCallback( $callback ) {
+               if ( !is_callable( $callback ) ) {
+                       throw new MWException( 'Invalid MwHttpRequest callback' );
+               }
+               $this->callback = $callback;
+       }
+
+       /**
+        * A generic callback to read the body of the response from a remote
+        * server.
+        *
+        * @param resource $fh
+        * @param string $content
+        * @return int
+        */
+       public function read( $fh, $content ) {
+               $this->content .= $content;
+               return strlen( $content );
+       }
+
+       /**
+        * Take care of whatever is necessary to perform the URI request.
+        *
+        * @return Status
+        */
+       public function execute() {
+
+               $this->content = "";
+
+               if ( strtoupper( $this->method ) == "HEAD" ) {
+                       $this->headersOnly = true;
+               }
+
+               $this->proxySetup(); // set up any proxy as needed
+
+               if ( !$this->callback ) {
+                       $this->setCallback( [ $this, 'read' ] );
+               }
+
+               if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
+                       $this->setUserAgent( Http::userAgent() );
+               }
+
+       }
+
+       /**
+        * Parses the headers, including the HTTP status code and any
+        * Set-Cookie headers.  This function expects the headers to be
+        * found in an array in the member variable headerList.
+        */
+       protected function parseHeader() {
+
+               $lastname = "";
+
+               foreach ( $this->headerList as $header ) {
+                       if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
+                               $this->respVersion = $match[1];
+                               $this->respStatus = $match[2];
+                       } elseif ( preg_match( "#^[ \t]#", $header ) ) {
+                               $last = count( $this->respHeaders[$lastname] ) - 1;
+                               $this->respHeaders[$lastname][$last] .= "\r\n$header";
+                       } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
+                               $this->respHeaders[strtolower( $match[1] )][] = $match[2];
+                               $lastname = strtolower( $match[1] );
+                       }
+               }
+
+               $this->parseCookies();
+
+       }
+
+       /**
+        * Sets HTTPRequest status member to a fatal value with the error
+        * message if the returned integer value of the status code was
+        * not successful (< 300) or a redirect (>=300 and < 400).  (see
+        * RFC2616, section 10,
+        * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a
+        * list of status codes.)
+        */
+       protected function setStatus() {
+               if ( !$this->respHeaders ) {
+                       $this->parseHeader();
+               }
+
+               if ( (int)$this->respStatus > 399 ) {
+                       list( $code, $message ) = explode( " ", $this->respStatus, 2 );
+                       $this->status->fatal( "http-bad-status", $code, $message );
+               }
+       }
+
+       /**
+        * Get the integer value of the HTTP status code (e.g. 200 for "200 Ok")
+        * (see RFC2616, section 10, http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+        * for a list of status codes.)
+        *
+        * @return int
+        */
+       public function getStatus() {
+               if ( !$this->respHeaders ) {
+                       $this->parseHeader();
+               }
+
+               return (int)$this->respStatus;
+       }
+
+       /**
+        * Returns true if the last status code was a redirect.
+        *
+        * @return bool
+        */
+       public function isRedirect() {
+               if ( !$this->respHeaders ) {
+                       $this->parseHeader();
+               }
+
+               $status = (int)$this->respStatus;
+
+               if ( $status >= 300 && $status <= 303 ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Returns an associative array of response headers after the
+        * request has been executed.  Because some headers
+        * (e.g. Set-Cookie) can appear more than once the, each value of
+        * the associative array is an array of the values given.
+        *
+        * @return array
+        */
+       public function getResponseHeaders() {
+               if ( !$this->respHeaders ) {
+                       $this->parseHeader();
+               }
+
+               return $this->respHeaders;
+       }
+
+       /**
+        * Returns the value of the given response header.
+        *
+        * @param string $header
+        * @return string|null
+        */
+       public function getResponseHeader( $header ) {
+               if ( !$this->respHeaders ) {
+                       $this->parseHeader();
+               }
+
+               if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
+                       $v = $this->respHeaders[strtolower( $header )];
+                       return $v[count( $v ) - 1];
+               }
+
+               return null;
+       }
+
+       /**
+        * Tells the MWHttpRequest object to use this pre-loaded CookieJar.
+        *
+        * @param CookieJar $jar
+        */
+       public function setCookieJar( $jar ) {
+               $this->cookieJar = $jar;
+       }
+
+       /**
+        * Returns the cookie jar in use.
+        *
+        * @return CookieJar
+        */
+       public function getCookieJar() {
+               if ( !$this->respHeaders ) {
+                       $this->parseHeader();
+               }
+
+               return $this->cookieJar;
+       }
+
+       /**
+        * Sets a cookie. Used before a request to set up any individual
+        * cookies. Used internally after a request to parse the
+        * Set-Cookie headers.
+        * @see Cookie::set
+        * @param string $name
+        * @param mixed $value
+        * @param array $attr
+        */
+       public function setCookie( $name, $value = null, $attr = null ) {
+               if ( !$this->cookieJar ) {
+                       $this->cookieJar = new CookieJar;
+               }
+
+               $this->cookieJar->setCookie( $name, $value, $attr );
+       }
+
+       /**
+        * Parse the cookies in the response headers and store them in the cookie jar.
+        */
+       protected function parseCookies() {
+
+               if ( !$this->cookieJar ) {
+                       $this->cookieJar = new CookieJar;
+               }
+
+               if ( isset( $this->respHeaders['set-cookie'] ) ) {
+                       $url = parse_url( $this->getFinalUrl() );
+                       foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
+                               $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
+                       }
+               }
+
+       }
+
+       /**
+        * Returns the final URL after all redirections.
+        *
+        * Relative values of the "Location" header are incorrect as
+        * stated in RFC, however they do happen and modern browsers
+        * support them.  This function loops backwards through all
+        * locations in order to build the proper absolute URI - Marooned
+        * at wikia-inc.com
+        *
+        * Note that the multiple Location: headers are an artifact of
+        * CURL -- they shouldn't actually get returned this way. Rewrite
+        * this when bug 29232 is taken care of (high-level redirect
+        * handling rewrite).
+        *
+        * @return string
+        */
+       public function getFinalUrl() {
+               $headers = $this->getResponseHeaders();
+
+               // return full url (fix for incorrect but handled relative location)
+               if ( isset( $headers['location'] ) ) {
+                       $locations = $headers['location'];
+                       $domain = '';
+                       $foundRelativeURI = false;
+                       $countLocations = count( $locations );
+
+                       for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
+                               $url = parse_url( $locations[$i] );
+
+                               if ( isset( $url['host'] ) ) {
+                                       $domain = $url['scheme'] . '://' . $url['host'];
+                                       break; // found correct URI (with host)
+                               } else {
+                                       $foundRelativeURI = true;
+                               }
+                       }
+
+                       if ( $foundRelativeURI ) {
+                               if ( $domain ) {
+                                       return $domain . $locations[$countLocations - 1];
+                               } else {
+                                       $url = parse_url( $this->url );
+                                       if ( isset( $url['host'] ) ) {
+                                               return $url['scheme'] . '://' . $url['host'] .
+                                                       $locations[$countLocations - 1];
+                                       }
+                               }
+                       } else {
+                               return $locations[$countLocations - 1];
+                       }
+               }
+
+               return $this->url;
+       }
+
+       /**
+        * Returns true if the backend can follow redirects. Overridden by the
+        * child classes.
+        * @return bool
+        */
+       public function canFollowRedirects() {
+               return true;
+       }
+}
diff --git a/includes/http/PhpHttpRequest.php b/includes/http/PhpHttpRequest.php
new file mode 100644 (file)
index 0000000..2af000f
--- /dev/null
@@ -0,0 +1,258 @@
+<?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
+ */
+
+class PhpHttpRequest extends MWHttpRequest {
+
+       private $fopenErrors = [];
+
+       /**
+        * @param string $url
+        * @return string
+        */
+       protected function urlToTcp( $url ) {
+               $parsedUrl = parse_url( $url );
+
+               return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
+       }
+
+       /**
+        * Returns an array with a 'capath' or 'cafile' key
+        * that is suitable to be merged into the 'ssl' sub-array of
+        * a stream context options array.
+        * Uses the 'caInfo' option of the class if it is provided, otherwise uses the system
+        * default CA bundle if PHP supports that, or searches a few standard locations.
+        * @return array
+        * @throws DomainException
+        */
+       protected function getCertOptions() {
+               $certOptions = [];
+               $certLocations = [];
+               if ( $this->caInfo ) {
+                       $certLocations = [ 'manual' => $this->caInfo ];
+               } elseif ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
+                       // @codingStandardsIgnoreStart Generic.Files.LineLength
+                       // Default locations, based on
+                       // https://www.happyassassin.net/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/
+                       // PHP 5.5 and older doesn't have any defaults, so we try to guess ourselves.
+                       // PHP 5.6+ gets the CA location from OpenSSL as long as it is not set manually,
+                       // so we should leave capath/cafile empty there.
+                       // @codingStandardsIgnoreEnd
+                       $certLocations = array_filter( [
+                               getenv( 'SSL_CERT_DIR' ),
+                               getenv( 'SSL_CERT_PATH' ),
+                               '/etc/pki/tls/certs/ca-bundle.crt', # Fedora et al
+                               '/etc/ssl/certs',  # Debian et al
+                               '/etc/pki/tls/certs/ca-bundle.trust.crt',
+                               '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
+                               '/System/Library/OpenSSL', # OSX
+                       ] );
+               }
+
+               foreach ( $certLocations as $key => $cert ) {
+                       if ( is_dir( $cert ) ) {
+                               $certOptions['capath'] = $cert;
+                               break;
+                       } elseif ( is_file( $cert ) ) {
+                               $certOptions['cafile'] = $cert;
+                               break;
+                       } elseif ( $key === 'manual' ) {
+                               // fail more loudly if a cert path was manually configured and it is not valid
+                               throw new DomainException( "Invalid CA info passed: $cert" );
+                       }
+               }
+
+               return $certOptions;
+       }
+
+       /**
+        * Custom error handler for dealing with fopen() errors.
+        * fopen() tends to fire multiple errors in succession, and the last one
+        * is completely useless (something like "fopen: failed to open stream")
+        * so normal methods of handling errors programmatically
+        * like get_last_error() don't work.
+        */
+       public function errorHandler( $errno, $errstr ) {
+               $n = count( $this->fopenErrors ) + 1;
+               $this->fopenErrors += [ "errno$n" => $errno, "errstr$n" => $errstr ];
+       }
+
+       public function execute() {
+
+               parent::execute();
+
+               if ( is_array( $this->postData ) ) {
+                       $this->postData = wfArrayToCgi( $this->postData );
+               }
+
+               if ( $this->parsedUrl['scheme'] != 'http'
+                       && $this->parsedUrl['scheme'] != 'https' ) {
+                       $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
+               }
+
+               $this->reqHeaders['Accept'] = "*/*";
+               $this->reqHeaders['Connection'] = 'Close';
+               if ( $this->method == 'POST' ) {
+                       // Required for HTTP 1.0 POSTs
+                       $this->reqHeaders['Content-Length'] = strlen( $this->postData );
+                       if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
+                               $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded";
+                       }
+               }
+
+               // Set up PHP stream context
+               $options = [
+                       'http' => [
+                               'method' => $this->method,
+                               'header' => implode( "\r\n", $this->getHeaderList() ),
+                               'protocol_version' => '1.1',
+                               'max_redirects' => $this->followRedirects ? $this->maxRedirects : 0,
+                               'ignore_errors' => true,
+                               'timeout' => $this->timeout,
+                               // Curl options in case curlwrappers are installed
+                               'curl_verify_ssl_host' => $this->sslVerifyHost ? 2 : 0,
+                               'curl_verify_ssl_peer' => $this->sslVerifyCert,
+                       ],
+                       'ssl' => [
+                               'verify_peer' => $this->sslVerifyCert,
+                               'SNI_enabled' => true,
+                               'ciphers' => 'HIGH:!SSLv2:!SSLv3:-ADH:-kDH:-kECDH:-DSS',
+                               'disable_compression' => true,
+                       ],
+               ];
+
+               if ( $this->proxy ) {
+                       $options['http']['proxy'] = $this->urlToTcp( $this->proxy );
+                       $options['http']['request_fulluri'] = true;
+               }
+
+               if ( $this->postData ) {
+                       $options['http']['content'] = $this->postData;
+               }
+
+               if ( $this->sslVerifyHost ) {
+                       // PHP 5.6.0 deprecates CN_match, in favour of peer_name which
+                       // actually checks SubjectAltName properly.
+                       if ( version_compare( PHP_VERSION, '5.6.0', '>=' ) ) {
+                               $options['ssl']['peer_name'] = $this->parsedUrl['host'];
+                       } else {
+                               $options['ssl']['CN_match'] = $this->parsedUrl['host'];
+                       }
+               }
+
+               $options['ssl'] += $this->getCertOptions();
+
+               $context = stream_context_create( $options );
+
+               $this->headerList = [];
+               $reqCount = 0;
+               $url = $this->url;
+
+               $result = [];
+
+               if ( $this->profiler ) {
+                       $profileSection = $this->profiler->scopedProfileIn(
+                               __METHOD__ . '-' . $this->profileName
+                       );
+               }
+               do {
+                       $reqCount++;
+                       $this->fopenErrors = [];
+                       set_error_handler( [ $this, 'errorHandler' ] );
+                       $fh = fopen( $url, "r", false, $context );
+                       restore_error_handler();
+
+                       if ( !$fh ) {
+                               // HACK for instant commons.
+                               // If we are contacting (commons|upload).wikimedia.org
+                               // try again with CN_match for en.wikipedia.org
+                               // as php does not handle SubjectAltName properly
+                               // prior to "peer_name" option in php 5.6
+                               if ( isset( $options['ssl']['CN_match'] )
+                                       && ( $options['ssl']['CN_match'] === 'commons.wikimedia.org'
+                                               || $options['ssl']['CN_match'] === 'upload.wikimedia.org' )
+                               ) {
+                                       $options['ssl']['CN_match'] = 'en.wikipedia.org';
+                                       $context = stream_context_create( $options );
+                                       continue;
+                               }
+                               break;
+                       }
+
+                       $result = stream_get_meta_data( $fh );
+                       $this->headerList = $result['wrapper_data'];
+                       $this->parseHeader();
+
+                       if ( !$this->followRedirects ) {
+                               break;
+                       }
+
+                       # Handle manual redirection
+                       if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) {
+                               break;
+                       }
+                       # Check security of URL
+                       $url = $this->getResponseHeader( "Location" );
+
+                       if ( !Http::isValidURI( $url ) ) {
+                               $this->logger->debug( __METHOD__ . ": insecure redirection\n" );
+                               break;
+                       }
+               } while ( true );
+               if ( $this->profiler ) {
+                       $this->profiler->scopedProfileOut( $profileSection );
+               }
+
+               $this->setStatus();
+
+               if ( $fh === false ) {
+                       if ( $this->fopenErrors ) {
+                               $this->logger->warning( __CLASS__
+                                       . ': error opening connection: {errstr1}', $this->fopenErrors );
+                       }
+                       $this->status->fatal( 'http-request-error' );
+                       return $this->status;
+               }
+
+               if ( $result['timed_out'] ) {
+                       $this->status->fatal( 'http-timed-out', $this->url );
+                       return $this->status;
+               }
+
+               // If everything went OK, or we received some error code
+               // get the response body content.
+               if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) {
+                       while ( !feof( $fh ) ) {
+                               $buf = fread( $fh, 8192 );
+
+                               if ( $buf === false ) {
+                                       $this->status->fatal( 'http-read-error' );
+                                       break;
+                               }
+
+                               if ( strlen( $buf ) ) {
+                                       call_user_func( $this->callback, $fh, $buf );
+                               }
+                       }
+               }
+               fclose( $fh );
+
+               return $this->status;
+       }
+}
index d78d61a..1e866f3 100644 (file)
@@ -319,7 +319,7 @@ class WikiRevision {
         * @deprecated Since 1.21, use getContent() instead.
         */
        function getText() {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
 
                return $this->text;
        }
index ded2bd8..331d1a1 100644 (file)
@@ -41,7 +41,7 @@ abstract class DatabaseInstaller {
        /**
         * The database connection.
         *
-        * @var DatabaseBase
+        * @var Database
         */
        public $db = null;
 
@@ -167,7 +167,7 @@ abstract class DatabaseInstaller {
         *
         * @param string $sourceFileMethod
         * @param string $stepName
-        * @param string $archiveTableMustNotExist
+        * @param bool $archiveTableMustNotExist
         * @return Status
         */
        private function stepApplySourceFile(
@@ -334,8 +334,7 @@ abstract class DatabaseInstaller {
 
                $connection = $status->value;
                $services->redefineService( 'DBLoadBalancerFactory', function() use ( $connection ) {
-                       return new LBFactorySingle( [
-                               'connection' => $connection ] );
+                       return LBFactorySingle::newFromConnection( $connection );
                } );
 
        }
@@ -354,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 0e4b098..ff87e9f 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  * @ingroup Deployment
  */
+use MediaWiki\MediaWikiServices;
 
 require_once __DIR__ . '/../../maintenance/Maintenance.php';
 
@@ -56,7 +57,7 @@ abstract class DatabaseUpdater {
        /**
         * Handle to the database subclass
         *
-        * @var DatabaseBase
+        * @var Database
         */
        protected $db;
 
@@ -76,6 +77,7 @@ abstract class DatabaseUpdater {
                PopulateBacklinkNamespace::class,
                FixDefaultJsonContentPages::class,
                CleanupEmptyCategories::class,
+               AddRFCAndPMIDInterwiki::class,
        ];
 
        /**
@@ -100,11 +102,11 @@ abstract class DatabaseUpdater {
        /**
         * Constructor
         *
-        * @param DatabaseBase $db To perform updates on
+        * @param Database $db To perform updates on
         * @param bool $shared Whether to perform updates on shared tables
         * @param Maintenance $maintenance Maintenance object which created us
         */
-       protected function __construct( DatabaseBase &$db, $shared, Maintenance $maintenance = null ) {
+       protected function __construct( Database &$db, $shared, Maintenance $maintenance = null ) {
                $this->db = $db;
                $this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files
                $this->shared = $shared;
@@ -170,14 +172,14 @@ abstract class DatabaseUpdater {
        }
 
        /**
-        * @param DatabaseBase $db
+        * @param Database $db
         * @param bool $shared
         * @param Maintenance $maintenance
         *
         * @throws MWException
         * @return DatabaseUpdater
         */
-       public static function newForDB( &$db, $shared = false, $maintenance = null ) {
+       public static function newForDB( Database $db, $shared = false, $maintenance = null ) {
                $type = $db->getType();
                if ( in_array( $type, Installer::getDBTypes() ) ) {
                        $class = ucfirst( $type ) . 'Updater';
@@ -191,7 +193,7 @@ abstract class DatabaseUpdater {
        /**
         * Get a database connection to run updates
         *
-        * @return DatabaseBase
+        * @return Database
         */
        public function getDB() {
                return $this->db;
@@ -402,6 +404,20 @@ abstract class DatabaseUpdater {
                }
        }
 
+       /**
+        * Get appropriate schema variables in the current database connection.
+        *
+        * This should be called after any request data has been imported, but before
+        * any write operations to the database. The result should be passed to the DB
+        * setSchemaVars() method.
+        *
+        * @return array
+        * @since 1.28
+        */
+       public function getSchemaVars() {
+               return []; // DB-type specific
+       }
+
        /**
         * Do all the updates
         *
@@ -410,6 +426,8 @@ abstract class DatabaseUpdater {
        public function doUpdates( $what = [ 'core', 'extensions', 'stats' ] ) {
                global $wgVersion;
 
+               $this->db->setSchemaVars( $this->getSchemaVars() );
+
                $what = array_flip( $what );
                $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
                if ( isset( $what['core'] ) ) {
@@ -440,6 +458,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 ) {
@@ -454,7 +474,7 @@ abstract class DatabaseUpdater {
                        flush();
                        if ( $ret !== false ) {
                                $updatesDone[] = $origParams;
-                               wfGetLBFactory()->waitForReplication();
+                               $lbFactory->waitForReplication();
                        } else {
                                $updatesSkipped[] = [ $func, $params, $origParams ];
                        }
index eafb9d4..03f9974 100644 (file)
@@ -244,6 +244,7 @@ abstract class Installer {
        protected $objectCaches = [
                'xcache' => 'xcache_get',
                'apc' => 'apc_fetch',
+               'apcu' => 'apcu_fetch',
                'wincache' => 'wincache_ucache_get'
        ];
 
index 62cd883..c5ec72b 100644 (file)
@@ -182,7 +182,7 @@ class MssqlInstaller extends DatabaseInstaller {
                        return $status;
                }
                /**
-                * @var $conn DatabaseBase
+                * @var $conn Database
                 */
                $conn = $status->value;
 
@@ -212,7 +212,7 @@ class MssqlInstaller extends DatabaseInstaller {
                }
 
                try {
-                       $db = DatabaseBase::factory( 'mssql', [
+                       $db = Database::factory( 'mssql', [
                                'host' => $this->getVar( 'wgDBserver' ),
                                'user' => $user,
                                'password' => $password,
@@ -240,7 +240,7 @@ class MssqlInstaller extends DatabaseInstaller {
                        return;
                }
                /**
-                * @var $conn DatabaseBase
+                * @var $conn Database
                 */
                $conn = $status->value;
                $conn->selectDB( $this->getVar( 'wgDBname' ) );
@@ -261,7 +261,7 @@ class MssqlInstaller extends DatabaseInstaller {
                if ( !$status->isOK() ) {
                        return false;
                }
-               /** @var $conn DatabaseBase */
+               /** @var $conn Database */
                $conn = $status->value;
 
                // We need the server-level ALTER ANY LOGIN permission to create new accounts
@@ -457,7 +457,7 @@ class MssqlInstaller extends DatabaseInstaller {
                        }
 
                        try {
-                               DatabaseBase::factory( 'mssql', [
+                               Database::factory( 'mssql', [
                                        'host' => $this->getVar( 'wgDBserver' ),
                                        'user' => $user,
                                        'password' => $password,
@@ -491,7 +491,7 @@ class MssqlInstaller extends DatabaseInstaller {
                if ( !$status->isOK() ) {
                        return $status;
                }
-               /** @var DatabaseBase $conn */
+               /** @var Database $conn */
                $conn = $status->value;
                $dbName = $this->getVar( 'wgDBname' );
                $schemaName = $this->getVar( 'wgDBmwschema' );
index 770d3bf..1175e9e 100644 (file)
@@ -92,6 +92,8 @@ class MssqlUpdater extends DatabaseUpdater {
                        // 1.28
                        [ 'addIndex', 'recentchanges', 'rc_name_type_patrolled_timestamp',
                                'patch-add-rc_name_type_patrolled_timestamp_index.sql' ],
+                       [ 'addField', 'change_tag', 'ct_id', 'patch-change_tag-ct_id.sql' ],
+                       [ 'addField', 'tag_summary', 'ts_id', 'patch-tag_summary-ts_id.sql' ],
                ];
        }
 
index 1bd3f51..5ff47e9 100644 (file)
@@ -124,7 +124,7 @@ class MysqlInstaller extends DatabaseInstaller {
                        return $status;
                }
                /**
-                * @var $conn DatabaseBase
+                * @var $conn Database
                 */
                $conn = $status->value;
 
@@ -143,7 +143,7 @@ class MysqlInstaller extends DatabaseInstaller {
        public function openConnection() {
                $status = Status::newGood();
                try {
-                       $db = DatabaseBase::factory( 'mysql', [
+                       $db = Database::factory( 'mysql', [
                                'host' => $this->getVar( 'wgDBserver' ),
                                'user' => $this->getVar( '_InstallUser' ),
                                'password' => $this->getVar( '_InstallPassword' ),
@@ -168,7 +168,7 @@ class MysqlInstaller extends DatabaseInstaller {
                        return;
                }
                /**
-                * @var $conn DatabaseBase
+                * @var $conn Database
                 */
                $conn = $status->value;
                $conn->selectDB( $this->getVar( 'wgDBname' ) );
@@ -226,7 +226,7 @@ class MysqlInstaller extends DatabaseInstaller {
                $status = $this->getConnection();
 
                /**
-                * @var $conn DatabaseBase
+                * @var $conn Database
                 */
                $conn = $status->value;
 
@@ -261,7 +261,7 @@ class MysqlInstaller extends DatabaseInstaller {
                if ( !$status->isOK() ) {
                        return false;
                }
-               /** @var $conn DatabaseBase */
+               /** @var $conn Database */
                $conn = $status->value;
 
                // Get current account name
@@ -427,7 +427,7 @@ class MysqlInstaller extends DatabaseInstaller {
                if ( !$create ) {
                        // Test the web account
                        try {
-                               DatabaseBase::factory( 'mysql', [
+                               Database::factory( 'mysql', [
                                        'host' => $this->getVar( 'wgDBserver' ),
                                        'user' => $this->getVar( 'wgDBuser' ),
                                        'password' => $this->getVar( 'wgDBpassword' ),
@@ -471,7 +471,7 @@ class MysqlInstaller extends DatabaseInstaller {
                if ( !$status->isOK() ) {
                        return $status;
                }
-               /** @var DatabaseBase $conn */
+               /** @var Database $conn */
                $conn = $status->value;
                $dbName = $this->getVar( 'wgDBname' );
                if ( !$conn->selectDB( $dbName ) ) {
@@ -509,7 +509,7 @@ class MysqlInstaller extends DatabaseInstaller {
                if ( $this->getVar( '_CreateDBAccount' ) ) {
                        // Before we blindly try to create a user that already has access,
                        try { // first attempt to connect to the database
-                               DatabaseBase::factory( 'mysql', [
+                               Database::factory( 'mysql', [
                                        'host' => $server,
                                        'user' => $dbUser,
                                        'password' => $password,
index 65af086..497f273 100644 (file)
@@ -288,6 +288,8 @@ class MysqlUpdater extends DatabaseUpdater {
                                'patch-add-rc_name_type_patrolled_timestamp_index.sql' ],
                        [ 'doRevisionPageRevIndexNonUnique' ],
                        [ 'doNonUniquePlTlIl' ],
+                       [ 'addField', 'change_tag', 'ct_id', 'patch-change_tag-ct_id.sql' ],
+                       [ 'addField', 'tag_summary', 'ts_id', 'patch-tag_summary-ts_id.sql' ],
                ];
        }
 
@@ -1122,4 +1124,18 @@ class MysqlUpdater extends DatabaseUpdater {
                        'Making rev_page_id index non-unique'
                );
        }
+
+       public function getSchemaVars() {
+               global $wgDBTableOptions;
+
+               $vars = [];
+               $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $wgDBTableOptions );
+               $vars['wgDBTableOptions'] = str_replace(
+                       'CHARSET=mysql4',
+                       'CHARSET=binary',
+                       $vars['wgDBTableOptions']
+               );
+
+               return $vars;
+       }
 }
index 8610834..b8fc4e7 100644 (file)
@@ -150,7 +150,7 @@ class OracleInstaller extends DatabaseInstaller {
                }
 
                /**
-                * @var $conn DatabaseBase
+                * @var $conn Database
                 */
                $conn = $status->value;
 
index 8075aac..e1e0d0f 100644 (file)
@@ -116,6 +116,8 @@ class OracleUpdater extends DatabaseUpdater {
                        // 1.28
                        [ 'addIndex', 'recentchanges', 'rc_name_type_patrolled_timestamp',
                                'patch-add-rc_name_type_patrolled_timestamp_index.sql' ],
+                       [ 'addField', 'change_tag', 'ct_id', 'patch-change_tag-ct_id.sql' ],
+                       [ 'addField', 'tag_summary', 'ts_id', 'patch-tag_summary-ts_id.sql' ],
 
                        // KEEP THIS AT THE BOTTOM!!
                        [ 'doRebuildDuplicateFunction' ],
index 0728415..33e1a1f 100644 (file)
@@ -114,7 +114,7 @@ class PostgresInstaller extends DatabaseInstaller {
                        return $status;
                }
                /**
-                * @var $conn DatabaseBase
+                * @var $conn Database
                 */
                $conn = $status->value;
 
@@ -154,7 +154,7 @@ class PostgresInstaller extends DatabaseInstaller {
        protected function openConnectionWithParams( $user, $password, $dbName, $schema ) {
                $status = Status::newGood();
                try {
-                       $db = DatabaseBase::factory( 'postgres', [
+                       $db = Database::factory( 'postgres', [
                                'host' => $this->getVar( 'wgDBserver' ),
                                'user' => $user,
                                'password' => $password,
@@ -181,7 +181,7 @@ class PostgresInstaller extends DatabaseInstaller {
 
                if ( $status->isOK() ) {
                        /**
-                        * @var $conn DatabaseBase
+                        * @var $conn Database
                         */
                        $conn = $status->value;
                        $conn->clearFlag( DBO_TRX );
@@ -233,7 +233,7 @@ class PostgresInstaller extends DatabaseInstaller {
                                $status = $this->openPgConnection( 'create-schema' );
                                if ( $status->isOK() ) {
                                        /**
-                                        * @var $conn DatabaseBase
+                                        * @var $conn Database
                                         */
                                        $conn = $status->value;
                                        $safeRole = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
@@ -264,7 +264,7 @@ class PostgresInstaller extends DatabaseInstaller {
                                        'password' => $password,
                                        'dbname' => $db
                                ];
-                               $conn = DatabaseBase::factory( 'postgres', $p );
+                               $conn = Database::factory( 'postgres', $p );
                        } catch ( DBConnectionError $error ) {
                                $conn = false;
                                $status->fatal( 'config-pg-test-error', $db,
@@ -287,7 +287,7 @@ class PostgresInstaller extends DatabaseInstaller {
                        return false;
                }
                /**
-                * @var $conn DatabaseBase
+                * @var $conn Database
                 */
                $conn = $status->value;
                $superuser = $this->getVar( '_InstallUser' );
@@ -413,7 +413,7 @@ class PostgresInstaller extends DatabaseInstaller {
 
        /**
         * Recursive helper for canCreateObjectsForWebUser().
-        * @param DatabaseBase $conn
+        * @param Database $conn
         * @param int $targetMember Role ID of the member to look for
         * @param int $group Role ID of the group to look for
         * @param int $maxDepth Maximum recursive search depth
@@ -588,7 +588,7 @@ class PostgresInstaller extends DatabaseInstaller {
                }
 
                /**
-                * @var $conn DatabaseBase
+                * @var $conn Database
                 */
                $conn = $status->value;
 
@@ -638,7 +638,7 @@ class PostgresInstaller extends DatabaseInstaller {
                        return $status;
                }
                /**
-                * @var $conn DatabaseBase
+                * @var $conn Database
                 */
                $conn = $status->value;
 
index be94d91..f3d2860 100644 (file)
@@ -68,6 +68,8 @@ class PostgresUpdater extends DatabaseUpdater {
                        [ 'addSequence', 'archive', false, 'archive_ar_id_seq' ],
                        [ 'addSequence', 'externallinks', false, 'externallinks_el_id_seq' ],
                        [ 'addSequence', 'watchlist', false, 'watchlist_wl_id_seq' ],
+                       [ 'addSequence', 'change_tag', false, 'change_tag_ct_id_seq' ],
+                       [ 'addSequence', 'tag_summary', false, 'tag_summary_ts_id_seq' ],
 
                        # new tables
                        [ 'addTable', 'category', 'patch-category.sql' ],
@@ -437,6 +439,10 @@ class PostgresUpdater extends DatabaseUpdater {
                        // 1.28
                        [ 'addPgIndex', 'recentchanges', 'rc_name_type_patrolled_timestamp',
                                '( rc_namespace, rc_type, rc_patrolled, rc_timestamp )' ],
+                       [ 'addPgField', 'change_tag', 'ct_id',
+                               "INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('change_tag_ct_id_seq')" ],
+                       [ 'addPgField', 'tag_summary', 'ts_id',
+                               "INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('tag_summary_ts_id_seq')" ],
                ];
        }
 
index d59c162..c5c4a7c 100644 (file)
@@ -179,16 +179,12 @@ class SqliteInstaller extends DatabaseInstaller {
         * @return Status
         */
        public function openConnection() {
-               global $wgSQLiteDataDir;
-
                $status = Status::newGood();
                $dir = $this->getVar( 'wgSQLiteDataDir' );
                $dbName = $this->getVar( 'wgDBname' );
                try {
                        # @todo FIXME: Need more sensible constructor parameters, e.g. single associative array
-                       # Setting globals kind of sucks
-                       $wgSQLiteDataDir = $dir;
-                       $db = DatabaseBase::factory( 'sqlite', [ 'dbname' => $dbName ] );
+                       $db = Database::factory( 'sqlite', [ 'dbname' => $dbName, 'dbDirectory' => $dir ] );
                        $status->value = $db;
                } catch ( DBConnectionError $e ) {
                        $status->fatal( 'config-sqlite-connection-error', $e->getMessage() );
@@ -243,10 +239,7 @@ class SqliteInstaller extends DatabaseInstaller {
 
                # Create the global cache DB
                try {
-                       global $wgSQLiteDataDir;
-                       # @todo FIXME: setting globals kind of sucks
-                       $wgSQLiteDataDir = $dir;
-                       $conn = DatabaseBase::factory( 'sqlite', [ 'dbname' => "wikicache" ] );
+                       $conn = Database::factory( 'sqlite', [ 'dbname' => 'wikicache', 'dbDirectory' => $dir ] );
                        # @todo: don't duplicate objectcache definition, though it's very simple
                        $sql =
 <<<EOT
@@ -268,6 +261,11 @@ EOT;
                return $this->getConnection();
        }
 
+       /**
+        * @param $dir
+        * @param $db
+        * @return Status
+        */
        protected function makeStubDBFile( $dir, $db ) {
                $file = DatabaseSqlite::generateFileName( $dir, $db );
                if ( file_exists( $file ) ) {
@@ -326,6 +324,7 @@ EOT;
                'type' => 'sqlite',
                'dbname' => 'wikicache',
                'tablePrefix' => '',
+               'dbDirectory' => \$wgSQLiteDataDir,
                'flags' => 0
        ]
 ];";
index 1c6e6eb..388c034 100644 (file)
@@ -156,6 +156,8 @@ class SqliteUpdater extends DatabaseUpdater {
                        // 1.28
                        [ 'addIndex', 'recentchanges', 'rc_name_type_patrolled_timestamp',
                                'patch-add-rc_name_type_patrolled_timestamp_index.sql' ],
+                       [ 'addField', 'change_tag', 'ct_id', 'patch-change_tag-ct_id.sql' ],
+                       [ 'addField', 'tag_summary', 'ts_id', 'patch-tag_summary-ts_id.sql' ],
                ];
        }
 
index 77cd689..eb84cc6 100644 (file)
@@ -65,6 +65,7 @@
        "config-memory-bad": "'''Папярэджаньне:''' памер PHP <code>memory_limit</code> складае $1.\nВерагодна, гэта вельмі мала.\nУсталяваньне можа быць няўдалым!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] усталяваны",
        "config-apc": "[http://www.php.net/apc APC] усталяваны",
+       "config-apcu": "[http://www.php.net/apcu APCu] ўсталяваны",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] усталяваны",
        "config-no-cache-apcu": "<strong>Папярэджаньне:</strong> ня знойдзеныя [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ці [http://www.iis.net/download/WinCacheForPhp WinCache]. Кэшаваньне аб’ектаў адключанае.",
        "config-mod-security": "'''Папярэджаньне''': на Вашым ўэб-сэрверы ўключаны [http://modsecurity.org/ mod_security]. У выпадку няслушнай наладцы, ён можа стаць прычынай праблемаў для MediaWiki ці іншага праграмнага забесьпячэньня, якое дазваляе ўдзельнікам дасылаць на сэрвэр любы зьмест.\nГлядзіце [http://modsecurity.org/documentation/ дакумэнтацыю mod_security] ці зьвярніцеся ў падтрымку Вашага хосту, калі ў Вас узьнікаюць выпадковыя праблемы.",
        "config-cache-options": "Налады кэшаваньня аб’ектаў:",
        "config-cache-help": "Кэшаваньне аб’ектаў павялічвае хуткасьць працы MediaWiki праз кэшаваньне зьвестак, якія часта выкарыстоўваюцца.\nВельмі рэкамэндуем уключыць гэта для сярэдніх і буйных сайтаў, таксама будзе карысна для дробных сайтаў.",
        "config-cache-none": "Без кэшаваньня (ніякія магчымасьці не страчваюцца, але хуткасьць працы буйных сайтаў можа зьнізіцца)",
-       "config-cache-accel": "Кэшаваньне аб’ектаў PHP (APC, XCache ці WinCache)",
+       "config-cache-accel": "Кэшаваньне аб’ектаў PHP (APC, APCu, XCache ці WinCache)",
        "config-cache-memcached": "Выкарыстоўваць Memcached (патрабуе дадатковай канфігурацыі)",
        "config-memcached-servers": "Сэрвэры memcached:",
        "config-memcached-help": "Сьпіс IP-адрасоў, якія будуць выкарыстоўвацца Memcached.\nАдрасы павінны быць у асобным радку з пазначэньнем порту, які будзе выкарыстоўвацца. Напрыклад:\n 127.0.0.1:11211\n 192.168.1.25:1234",
index 4e34c7d..8b1ca18 100644 (file)
@@ -43,8 +43,8 @@
        "config-page-existingwiki": "Съществуващо уики",
        "config-help-restart": "Необходимо е потвърждение за изтриване на всички въведени и съхранени данни и започване отначало на процеса по инсталация.",
        "config-restart": "Да, започване отначало",
-       "config-welcome": "=== Ð\9fÑ\80овеÑ\80ка Ð½Ð° Ñ\81Ñ\80едаÑ\82а ===\nЩе Ð±Ñ\8aдаÑ\82 Ð¸Ð·Ð²Ñ\8aÑ\80Ñ\88ени Ð¾Ñ\81новни Ð¿Ñ\80овеÑ\80ки, ÐºÐ¾Ð¸Ñ\82о Ð´Ð° Ñ\83Ñ\81Ñ\82ановÑ\8fÑ\82 Ð´Ð°Ð»Ð¸ Ñ\81Ñ\80едаÑ\82а Ðµ Ð¿Ð¾Ð´Ñ\85одÑ\8fÑ\89а за инсталиране на МедияУики.\nАко е необходима помощ по време на инсталацията, резултатите от направените проверки трябва също да бъдат предоставени.",
-       "config-copyright": "=== Авторски права и Условия ===\n\n$1\n\nТази програма е свободен софтуер, който може да се променя и/или разпространява според Общия публичен лиценз на GNU, както е публикуван от Free Software Foundation във версия на Лиценза 2 или по-късна версия.\n\nТази програма се разпространява с надеждата, че ще е полезна, но '''без каквито и да е гаранции'''; без дори косвена гаранция за '''продаваемост''' или '''прогодност за конкретна употреба'''.\nЗа повече подробности се препоръчва преглеждането на Общия публичен лиценз на GNU.\n\nКъм програмата трябва да е приложено <doclink href=Copying>копие на Общия публичен лиценз на GNU</doclink>; ако не, можете да пишете на Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. или да [http://www.gnu.org/copyleft/gpl.html го прочетете онлайн].",
+       "config-welcome": "=== Ð\9fÑ\80овеÑ\80ка Ð½Ð° Ñ\83Ñ\81ловиÑ\8fÑ\82а ===\nЩе Ð±Ñ\8aдаÑ\82 Ð¸Ð·Ð²Ñ\8aÑ\80Ñ\88ени Ð¾Ñ\81новни Ð¿Ñ\80овеÑ\80ки, ÐºÐ¾Ð¸Ñ\82о Ð´Ð° Ñ\83Ñ\81Ñ\82ановÑ\8fÑ\82 Ð´Ð°Ð»Ð¸ Ñ\83Ñ\81ловиÑ\8fÑ\82а Ñ\81а Ð¿Ð¾Ð´Ñ\85одÑ\8fÑ\89и за инсталиране на МедияУики.\nАко е необходима помощ по време на инсталацията, резултатите от направените проверки трябва също да бъдат предоставени.",
+       "config-copyright": "=== Авторски права и условия ===\n\n$1\n\nТази програма е свободен софтуер, който може да се променя и/или разпространява според Общия публичен лиценз на GNU, както е публикуван от Free Software Foundation във версия на Лиценза 2 или по-късна версия.\n\nТази програма се разпространява с надеждата, че ще е полезна, но <strong>без каквито и да е гаранции</strong>; без дори косвена гаранция за <strong>продаваемост</strong>  или <strong>прогодност за конкретна употреба</strong> .\nЗа повече подробности се препоръчва преглеждането на Общия публичен лиценз на GNU.\n\nКъм програмата трябва да е приложено <doclink href=Copying>копие на Общия публичен лиценз на GNU</doclink>; ако не, можете да пишете на Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, или да [http://www.gnu.org/copyleft/gpl.html го прочетете онлайн].",
        "config-sidebar": "* [https://www.mediawiki.org Сайт на МедияУики]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Наръчник на потребителя]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Наръчник на администратора]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ ЧЗВ]\n----\n* <doclink href=Readme>Документация</doclink>\n* <doclink href=ReleaseNotes>Бележки за версията</doclink>\n* <doclink href=Copying>Авторски права</doclink>\n* <doclink href=UpgradeDoc>Обновяване</doclink>",
        "config-env-good": "Средата беше проверена.\nИнсталирането на МедияУики е възможно.",
        "config-env-bad": "Средата беше проверена.\nНе е възможна инсталация на МедияУики.",
@@ -59,7 +59,7 @@
        "config-pcre-old": "<strong>Фатална грешка:</strong> Изисква се PCRE версия $1 или по-нова.\nИзпълнимият файл на PHP е свързан с PCRE версия $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/Повече информация за PCRE].",
        "config-pcre-no-utf8": "'''Фатално''': Модулът PCRE на PHP изглежда е компилиран без поддръжка на PCRE_UTF8.\nЗа да функционира правилно, МедияУики изисква поддръжка на UTF-8.",
        "config-memory-raised": "<code>memory_limit</code> на PHP е $1, увеличаване до $2.",
-       "config-memory-bad": "'''Предупреждение:''' <code>memory_limit</code> на PHP е $1.\nСтойността вероятно е твърде ниска.\nВъзможно е инсталацията да се провали!",
+       "config-memory-bad": "<strong>Предупреждение:<strong> <code>memory_limit</code> на PHP е $1.\nСтойността вероятно е твърде ниска.\nВъзможно е инсталацията да се провали!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] е инсталиран",
        "config-apc": "[http://www.php.net/apc APC] е инсталиран",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] е инсталиран",
        "config-admin-help": "Въвежда се предпочитаното потребителско име, например \"Иванчо Иванчев\".\nТова ще е потребителското име, което администраторът ще използва за влизане в уикито.",
        "config-admin-name-blank": "Необходимо е да бъде въведено потребителско име на администратора.",
        "config-admin-name-invalid": "Посоченото потребителско име \"<nowiki>$1</nowiki>\" е невалидно.\nНеобходимо е да се посочи друго.",
-       "config-admin-password-blank": "Ð\9dеовÑ\85одимо Ðµ Ð´Ð° Ñ\81е Ð²Ñ\8aведе парола за администраторската сметка.",
+       "config-admin-password-blank": "Ð\92Ñ\8aведеÑ\82е парола за администраторската сметка.",
        "config-admin-password-mismatch": "Двете въведени пароли не съвпадат.",
        "config-admin-email": "Адрес за електронна поща:",
        "config-admin-email-help": "Въвеждането на адрес за е-поща позволява получаване на е-писма от другите потребители на уикито, възстановяване на изгубена или забравена парола, оповестяване при промени в страниците от списъка за наблюдение. Това поле може да бъде оставено празно.",
        "config-help": "помощ",
        "config-nofile": "Файлът „$1“ не може да бъде открит. Да не е бил изтрит?",
        "config-extension-link": "Знаете ли, че това уики поддържа [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions разширения]?\n\nМожете да разгледате [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category разширенията по категория] или [https://www.mediawiki.org/wiki/Extension_Matrix Матрицата на разширенията] за пълен списък на разширенията.",
-       "mainpagetext": "<strong>УикиÑ\82о беше успешно инсталирано.</strong>",
+       "mainpagetext": "<strong>Ð\9cедиÑ\8fУики беше успешно инсталирано.</strong>",
        "mainpagedocfooter": "Разгледайте [https://meta.wikimedia.org/wiki/Help:Contents ръководството] за подробна информация относно използването на уики софтуера.\n\n== Първи стъпки ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Настройки за конфигуриране]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ ЧЗВ за МедияУики]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Пощенски списък относно нови версии на МедияУики]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Локализиране на МедияУики]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Научете как да се справяте със спама във вашето уики]"
 }
index fb23d01..6926650 100644 (file)
@@ -6,7 +6,8 @@
                        "Aftab1995",
                        "Tauhid16",
                        "Aftabuzzaman",
-                       "Hasive"
+                       "Hasive",
+                       "আজিজ"
                ]
        },
        "config-desc": "মিডিয়াউইকির জন্য ইন্সটলার",
@@ -94,7 +95,9 @@
        "config-admin-name": "আপনার ব্যবহারকারী নাম:",
        "config-admin-password": "পাসওয়ার্ড:",
        "config-admin-password-confirm": "পাসওয়ার্ড আবারও প্রবেশ করান:",
+       "config-admin-help": "এখানে আপনার পছন্দের ব্যবহারকারী নাম লিখুন, উদাহরণস্বরূপ \"আজিজ\"। এই নামটি উইকিতে প্রবেশের সময় ব্যবহার করতে হবে।",
        "config-admin-name-blank": "একটি প্রশাসক ব্যবহারকারী নাম প্রবেশ করান",
+       "config-admin-name-invalid": "উল্লেখিত ব্যবহারকারী নাম \"<nowiki>$1</nowiki>\" অবৈধ। একটি ভিন্ন নাম উল্লেখ করুন।",
        "config-admin-password-blank": "প্রশাসক অ্যাকাউন্টের জন্য পাসওয়ার্ড প্রবেশ করান।",
        "config-admin-password-mismatch": "আপনি যে দুটি পাসওয়ার্ড দিয়েছেন তারা পরস্পর মেলেনি।",
        "config-admin-email": "ইমেইল ঠিকানা:",
index 68497c5..c696650 100644 (file)
@@ -66,6 +66,7 @@
        "config-memory-bad": "<strong>Upozornění:</strong> <code>memory_limit</code> je v PHP nastaven na $1.\nTo je pravděpodobně příliš málo.\nInstalace může selhat!",
        "config-xcache": "Je nainstalována [http://xcache.lighttpd.net/ XCache]",
        "config-apc": "Je nainstalováno [http://www.php.net/apc APC]",
+       "config-apcu": "Je nainstalováno [http://www.php.net/apcu APCu]",
        "config-wincache": "Je nainstalována [http://www.iis.net/download/WinCacheForPhp WinCache]",
        "config-no-cache-apcu": "<strong>Upozornění:</strong> Nebylo nalezeno [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache], ani [http://www.iis.net/download/WinCacheForPhp WinCache].\nKešování objektů bude vypnuto.",
        "config-mod-security": "<strong>Upozornění:</strong> váš webový server má zapnuto [http://modsecurity.org/ mod_security]/mod_security2. Mnoho běžných konfigurací bude způsobovat potíže MediaWiki a dalším programům, které umožňují ukládat libovolný obsah.\nPokud je to možné, mělo by se to vypnout. Jinak se v případě, že narazíte na náhodné chyby, podívejte do [http://modsecurity.org/documentation/ dokumentace mod_security] nebo kontaktujte technickou podporu vašeho poskytovatele.",
        "config-cache-options": "Nastavení cachování objektů:",
        "config-cache-help": "Cachování objektů se používá pro vylepšení rychlosti MediaWiki tím, že se cachují často používaná data.\nStředním až velkým serverům se jeho zapnutí důrazně doporučuje, i menší servery pocítí jeho výhody.",
        "config-cache-none": "Bez cachování (o žádnou funkcionalitu nepřijdete, na větších wiki však může dojít ke zhoršení rychlosti)",
-       "config-cache-accel": "Cachování PHP objektů (APC, XCache nebo WinCache)",
+       "config-cache-accel": "Cachování PHP objektů (APC, APCu, XCache nebo WinCache)",
        "config-cache-memcached": "Použít Memcached (vyžaduje další nastavení a konfiguraci)",
        "config-memcached-servers": "Servery Memcached:",
        "config-memcached-help": "Seznam IP adres, které se mají používat pro Memcached.\nUveďte jednu na řádek spolu s portem. Například:\n 127.0.0.1:11211\n 192.168.1.25:1234",
index e704141..2783bca 100644 (file)
@@ -74,6 +74,7 @@
        "config-memory-bad": "'''Warnung:''' Der PHP-Parameter <code>memory_limit</code> beträgt $1.\nDieser Wert ist wahrscheinlich zu niedrig.\nDer Installationsvorgang könnte eventuell scheitern!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] ist installiert",
        "config-apc": "[http://www.php.net/apc APC] ist installiert",
+       "config-apcu": "[http://www.php.net/apcu APCu] ist installiert",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] ist installiert",
        "config-no-cache-apcu": "<strong>Warnung:</strong> [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] oder [http://www.iis.net/download/WinCacheForPhp WinCache] konnten nicht gefunden werden.\nDer Objektcache ist nicht aktiviert.",
        "config-mod-security": "'''Warnung:''' Auf dem Webserver wurde [http://modsecurity.org/ ModSecurity] aktiviert. Sofern falsch konfiguriert, kann dies zu Problemen mit MediaWiki sowie anderer Software auf dem Server führen und es Benutzern ermöglichen, beliebige Inhalte im Wiki einzustellen.\nFür weitere Informationen empfehlen wir die [http://modsecurity.org/documentation/ Dokumentation zu ModSecurity] oder den Kontakt zum Hoster, sofern Fehler auftreten.",
        "config-cache-options": "Einstellungen für die Zwischenspeicherung von Objekten:",
        "config-cache-help": "Das Objektcaching wird dazu genutzt, die Geschwindigkeit von MediaWiki zu verbessern, indem häufig genutzte Daten zwischengespeichert werden.\nEs wird sehr empfohlen, es für mittelgroße bis große Wikis zu nutzen, aber auch für kleine Wikis ergeben sich erkennbare Geschwindigkeitsverbesserungen.",
        "config-cache-none": "Kein Objektcaching (es wird keine Funktion entfernt, allerdings kann dies die Leistungsfähigkeit größerer Wikis negativ beeinflussen)",
-       "config-cache-accel": "Objektcaching von PHP (APC, XCache oder WinCache)",
+       "config-cache-accel": "Objektcaching von PHP (APC, APCu, XCache oder WinCache)",
        "config-cache-memcached": "Memcached Cacheserver (erfordert einen zusätzlichen Installationsvorgang mitsamt Konfiguration)",
        "config-memcached-servers": "Memcached Cacheserver",
        "config-memcached-help": "Liste der für Memcached nutzbaren IP-Adressen.\nEs sollte eine je Zeile mitsamt des vorgesehenen Ports angegeben werden. Beispiele:\n127.0.0.1:11211 oder\n192.168.1.25:1234 usw.",
index 6fa270a..c80d54e 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",
        "config-page-complete": "Temamyayo",
        "config-page-restart": "Barkerdışi fına ser kı",
        "config-page-readme": "Mı bıwane",
+       "config-page-releasenotes": "Notë versiyoni",
        "config-page-copying": "Kopyayeno",
        "config-page-upgradedoc": "Berzkerdış",
+       "config-page-existingwiki": "Mewcud wiki",
        "config-restart": "E, fına dest pekê",
        "config-sidebar": "* [https://www.mediawiki.org MediaWiki keye]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Şınasiya Karberi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Şınasiya İdarekaran]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Peşti]\n----\n* <doclink href=Readme>Mı buwanê</doclink>\n* <doclink href=ReleaseNotes>Notê elaqeyıni</doclink>\n* <doclink href=Copying>Kopyakerdış</doclink>\n* <doclink href=UpgradeDoc>Zêdekerdış</doclink>",
        "config-env-php": "PHP $1 i biyo saz.",
        "config-db-type": "Database tipe:",
        "config-db-host": "Database host:",
        "config-db-host-oracle": "Database TNS:",
+       "config-db-wiki-settings": "Ena wikiyer akernë",
        "config-db-name": "Database name:",
+       "config-db-name-oracle": "Şemaya hardata:",
        "config-db-username": "Database nameykarberi:",
        "config-db-password": "Database parola :",
        "config-db-port": "Portê database:",
+       "config-oracle-def-ts": "Hesıbyaye caytabloy:",
+       "config-oracle-temp-ts": "İdareten caytabloy:",
+       "config-type-mysql": "MySQL (yana hewlın)",
        "config-type-mssql": "Microsoft SQL Server",
        "config-header-mysql": "Eyarê MySQL",
+       "config-header-sqlite": "SQLite sazi",
+       "config-header-oracle": "Orqcle sazi",
+       "config-header-mssql": "Sazë Microsoft SQL Serveri",
+       "config-missing-db-name": "\"{{int:config-db-name}}\"nrë jew erc dekerdış gerek keno.",
+       "config-missing-db-host": "\"{{int:config-db-host}}\" rë jew erc gerek keno",
+       "config-missing-db-server-oracle": "\"{{int:config-db-host-oracle}}\" rë jew erc gerek keno",
+       "config-mysql-engine": "Motorë depok kerdışi",
        "config-mysql-innodb": "InnoDB",
        "config-mysql-myisam": "MyISAM",
        "config-mysql-binary": "Dılet",
        "config-mysql-utf8": "UTF-8",
+       "config-mssql-sqlauth": "SQL Server araştnayış",
+       "config-mssql-windowsauth": "Windows kamiye araştnayış",
        "config-site-name": "Namey wiki:",
        "config-site-name-blank": "Yew nameyê sita cıkewe.",
        "config-project-namespace": "Wareyê nameyê proceyi:",
index 3f3032b..6a6c0ff 100644 (file)
@@ -57,6 +57,7 @@
        "config-memory-bad": "<strong>Warning:</strong> PHP's <code>memory_limit</code> is $1.\nThis is probably too low.\nThe installation may fail!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] is installed",
        "config-apc": "[http://www.php.net/apc APC] is installed",
+       "config-apcu": "[http://www.php.net/apcu APCu] is installed",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] is installed",
        "config-no-cache-apcu": "<strong>Warning:</strong> Could not find [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] or [http://www.iis.net/download/WinCacheForPhp WinCache].\nObject caching is not enabled.",
        "config-mod-security": "<strong>Warning:</strong> Your web server has [http://modsecurity.org/ mod_security]/mod_security2 enabled. Many common configurations of this will cause problems for MediaWiki and other software that allows users to post arbitrary content.\nIf possible, this should be disabled. Otherwise, refer to [http://modsecurity.org/documentation/ mod_security documentation] or contact your host's support if you encounter random errors.",
        "config-cache-options": "Settings for object caching:",
        "config-cache-help": "Object caching is used to improve the speed of MediaWiki by caching frequently used data.\nMedium to large sites are highly encouraged to enable this, and small sites will see benefits as well.",
        "config-cache-none": "No caching (no functionality is removed, but speed may be impacted on larger wiki sites)",
-       "config-cache-accel": "PHP object caching (APC, XCache or WinCache)",
+       "config-cache-accel": "PHP object caching (APC, APCu, XCache or WinCache)",
        "config-cache-memcached": "Use Memcached (requires additional setup and configuration)",
        "config-memcached-servers": "Memcached servers:",
        "config-memcached-help": "List of IP addresses to use for Memcached.\nShould specify one per line and specify the port to be used. For example:\n 127.0.0.1:11211\n 192.168.1.25:1234",
index 09f3a40..afcd9ed 100644 (file)
        "config-mysql-innodb": "InnoDB",
        "config-mysql-myisam": "MyISAM",
        "config-mysql-myisam-dep": "<strong>Advertencia:</strong> has seleccionado MyISAM como motor de almacenamiento de MySQL, el cual no está recomendado para usarse con MediaWiki, porque:\n* apenas soporta concurrencia debido al bloqueo de tablas\n* es más propenso a la corrupción que otros motores\n* el código MediaWiki no siempre controla MyISAM como debiera\n\nSi tu instalación de MySQL soporta InnoDB, es muy recomendable que lo elijas en su lugar.\nSi tu instalación de MySQL no soporta InnoDB, quizás es el momento de una actualización.",
-       "config-mysql-only-myisam-dep": "<strong>Advertencia:</strong> solo se ha encontrado el motor de almacenamiento MyISAM para MySQL en esta máquina, y no se recomienda su uso con MediaWiki, porque:\n* apenas soporta concurrencia debido al bloqueo de tablas\n* es más propenso a la corrupción que otros motores\n* el código MediaWiki no siempre controla MyISAM como debiera\n\nTu instalación de MySQL no soporta InnoDB, quizás es el momento de una actualización.",
+       "config-mysql-only-myisam-dep": "<strong>Advertencia:</strong> solo se ha encontrado el motor de almacenamiento MyISAM para MySQL en esta máquina, y no se recomienda su uso con MediaWiki, porque:\n* apenas admite la concurrencia debido al bloqueo de tablas\n* es más propenso a daños que otros motores\n* el código de MediaWiki no siempre controla MyISAM como debería\n\nTu instalación de MySQL no admite InnoDB; quizás es el momento de una actualización.",
        "config-mysql-engine-help": "<strong>InnoDB</strong> es casi siempre la mejor opción, dado que soporta bien los accesos simultáneos.\n\n<strong>MyISAM</strong> puede ser más rápido en instalaciones con usuario único o de sólo lectura.\nLas bases de datos MyISAM tienden a corromperse más a menudo que las bases de datos InnoDB.",
        "config-mysql-charset": "Conjunto de caracteres de la base de datos:",
        "config-mysql-binary": "Binario",
index 3e7f8f3..95224e9 100644 (file)
@@ -85,6 +85,7 @@
        "config-memory-bad": "'''Attention :''' Le paramètre <code>memory_limit</code> de PHP est à $1.\nCette valeur est probablement trop faible.\nIl est possible que l’installation échoue !",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] est installé",
        "config-apc": "[http://www.php.net/apc APC] est installé",
+       "config-apcu": "[http://www.php.net/apcu APCu] est installé",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] est installé",
        "config-no-cache-apcu": "'''Attention :''' Impossible de trouver [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ou [http://www.iis.net/download/WinCacheForPhp WinCache].\nLa mise en cache d'objets n'est pas activée.",
        "config-mod-security": "'''Attention''': Votre serveur web a [http://modsecurity.org/ mod_security] activé. S'il est mal configuré, cela peut poser des problèmes à MediaWiki ou à d'autres applications qui permettent aux utilisateurs de publier un contenu quelconque.\nReportez-vous à [http://modsecurity.org/documentation/ la documentation de mod_security] ou contactez le support de votre hébergeur si vous rencontrez des erreurs aléatoires.",
        "config-cache-options": "Paramètres pour la mise en cache des objets:",
        "config-cache-help": "La mise en cache des objets améliore la vitesse de MediaWiki en mettant en cache les données fréquemment utilisées.\nLes sites de taille moyenne à grande sont fortement encouragés à l'activer. Les petits sites y verront également des avantages.",
        "config-cache-none": "Pas de mise en cache (aucune fonctionnalité n'a été supprimée, mais la vitesse peut changer sur les wikis importants)",
-       "config-cache-accel": "Mise en cache des objets PHP (APC, XCache ou WinCache)",
+       "config-cache-accel": "Mise en cache des objets PHP (APC, APCu, XCache ou WinCache)",
        "config-cache-memcached": "Utiliser Memcached (nécessite une installation et une configuration supplémentaires)",
        "config-memcached-servers": "serveurs pour Memcached :",
        "config-memcached-help": "Liste des adresses IP à utiliser pour Memcached.\nUne par ligne, en indiquant le port à utiliser. Par exemple :\n  127.0.0.1:11211\n  192.168.1.25:1234",
index e9e757a..64ad12a 100644 (file)
@@ -64,6 +64,7 @@
        "config-memory-bad": "<strong>Atención:<strong> O parámetro <code>memory_limit</code> do PHP é $1.\nProbablemente é un valor baixo de máis.\nA instalación pode fallar!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] está instalado",
        "config-apc": "[http://www.php.net/apc APC] está instalado",
+       "config-apcu": "[http://www.php.net/apcu APCu] está instalado",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] está instalado",
        "config-no-cache-apcu": "<strong>Advertencia:</strong> Non se puido atopar [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] ou [http://www.iis.net/download/WinCacheForPhp WinCache].\nA caché de obxectos non está activada.",
        "config-mod-security": "<strong>Atención:</strong> O seu servidor web ten o [http://modsecurity.org/ mod_security] activado. Se estivese mal configurado, pode causar problemas a MediaWiki ou calquera outro software que permita aos usuarios publicar contidos arbitrarios.\nOlle a [http://modsecurity.org/documentation/ documentación do mod_security] ou póñase en contacto co soporte do seu servidor se atopa erros aleatorios.",
        "config-cache-options": "Configuración da caché de obxectos:",
        "config-cache-help": "A caché de obxectos emprégase para mellorar a velocidade de MediaWiki mediante a memorización de datos usados con frecuencia.\nÉ amplamente recomendable a súa activación nos sitios de tamaño medio e grande; os sitios pequenos obterán tamén beneficios.",
        "config-cache-none": "Sen caché (non se elimina ningunha funcionalidade, pero pode afectar á velocidade en wikis grandes)",
-       "config-cache-accel": "Caché de obxectos do PHP (APC, XCache ou WinCache)",
+       "config-cache-accel": "Caché de obxectos do PHP (APC, APCu, XCache ou WinCache)",
        "config-cache-memcached": "Empregar o Memcached (necesita unha instalación e configuración adicional)",
        "config-memcached-servers": "Servidores da memoria caché:",
        "config-memcached-help": "Lista de enderezos IP para Memcached.\nDebe especificarse un por liña, así como o porto a usar. Por exemplo:\n 127.0.0.1:11211\n 192.168.1.25:1234",
index 6ed2722..0c3b2b4 100644 (file)
@@ -18,7 +18,9 @@
                        "C.R.",
                        "Macofe",
                        "Matteocng",
-                       "Einreiher"
+                       "Einreiher",
+                       "Tosky",
+                       "Selven"
                ]
        },
        "config-desc": "Programma di installazione per MediaWiki",
@@ -76,6 +78,7 @@
        "config-memory-bad": "''Attenzione:''' Il valore di <code>memory_limit</code> di PHP è $1.\nProbabilmente è troppo basso.\nL'installazione potrebbe non riuscire!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] è installato",
        "config-apc": "[http://www.php.net/apc APC] è installato",
+       "config-apcu": "[http://www.php.net/apc APC] è installato",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] è installato",
        "config-no-cache-apcu": "'''Attenzione:''' [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] o [http://www.iis.net/download/WinCacheForPhp WinCache] non sono stati trovati.\nLa caching degli oggetti non è attivata.",
        "config-mod-security": "<strong>Attenzione:</strong> Il tuo server web ha il [http://modsecurity.org/ mod_security] abilitato. Se non correttamente configurato, può creare problemi a MediaWiki o ad altro software che permette agli utenti di pubblicare contenuto.\nFai riferimento alla [http://modsecurity.org/documentation/ documentazione sul mod_security] o contatta il supporto tecnico del tuo provider di hosting se si verificano errori.",
        "config-subscribe-help": "Si tratta di una mailing list a basso traffico dedicata agli annunci di nuove versioni, compresi importanti segnalazioni riguardanti la sicurezza.\nÈ consigliato iscriversi e aggiornare la propria installazione di MediaWiki quando una nuova versione viene resa pubblica.",
        "config-subscribe-noemail": "Hai provato ad iscriverti alla mailing list dedicata agli annunci delle nuove versioni senza fornire un indirizzo email.\nInserire un indirizzo email se si desidera effettuare l'iscrizione alla mailing list.",
        "config-pingback": "Condividi i dati su questa installazione con gli sviluppatori di MediaWiki.",
-       "config-almost-done": "Hai quasi finito!\nAdesso puoi saltare la rimanente parte della configurazione e semplicemente installare la wiki.",
+       "config-pingback-help": "Se si seleziona questa opzione, MediaWiki contatterà periodicamente https://www.mediawiki.org con i dati base su questa istanza MediaWiki. In questa categoria di dati rientrano, per esempio, il tipo di sistema, la versione di PHP e database di backend scelto. La Wikimedia Foundation condivide questi dati con gli sviluppatori Mediawiki per aiutarla a guidare i futuri sforzi di sviluppo. Per il tuo sistema saranno inviati i seguenti dati:\n<pre>$1</pre>",
+       "config-almost-done": "Hai quasi finito!\nAdesso puoi saltare la rimanente parte della configurazione e semplicemente installare il wiki.",
        "config-optional-continue": "Fammi altre domande.",
        "config-optional-skip": "Sono già stanco, installa solo il wiki.",
        "config-profile": "Profilo dei diritti utente:",
        "config-cache-options": "Impostazioni per la cache di oggetti:",
        "config-cache-help": "La memorizzazione di oggetti nella cache è utilizzata per migliorare la velocità di MediaWiki attraverso l'allocazione nella cache dei dati utilizzati di frequente.\nPer siti di dimensioni medie e grandi, è caldamente consigliato attivare la cache, ma anche per piccoli siti se ne vedranno i benefici.",
        "config-cache-none": "Nessuna memorizzazione in cache (nessuna funzionalità viene impedita, ma sui siti wiki più grandi la velocità potrebbe risentirne)",
-       "config-cache-accel": "Mettere in cache oggetti PHP (APC, XCache o WinCache)",
+       "config-cache-accel": "Mettere in cache oggetti PHP (APC, APCu, XCache o WinCache)",
        "config-cache-memcached": "Usa Memcached (richiede ulteriori attività di installazione e configurazione)",
        "config-memcached-servers": "Server di memcached:",
        "config-memcached-help": "Elenco di indirizzi IP da utilizzare per Memcached.\nDovresti specificarne uno per riga e indicare la porta da utilizzare. Per esempio:\n 127.0.0.1:11211\n 192.168.1.25:1234",
        "config-install-extension-tables": "Creazione delle tabelle per le estensioni attivate",
        "config-install-mainpage-failed": "Impossibile inserire la pagina principale: $1",
        "config-install-done": "<strong>Complimenti!</strong>\nHai installato MediaWiki.\n\nIl programma di installazione ha generato un file <code>LocalSettings.php</code> che contiene tutte le impostazioni.\n\nDevi scaricarlo ed inserirlo nella directory base del tuo wiki (la stessa dove è presente index.php). Il download dovrebbe partire automaticamente.\n\nSe il download non si avvia, o se è stato annullato, puoi riavviarlo cliccando sul collegamento di seguito:\n\n$3\n\n<strong>Nota:</strong> se esci ora dall'installazione senza scaricare il file di configurazione che è stato generato, questo poi non sarà più disponibile in seguito.\n\nQuando hai fatto, puoi <strong>[$2 entrare nel tuo wiki]</strong>.",
+       "config-install-done-path": "<strong>Complimenti!</strong>\nHai installato MediaWiki.\n\nIl programma di installazione ha generato un file <code>LocalSettings.php</code> che contiene tutte le impostazioni.\n\nDevi scaricarlo ed inserirlo in <code>$4</code>. Il download dovrebbe partire automaticamente.\n\nSe il download non si avvia, o se è stato annullato, puoi riavviarlo cliccando sul collegamento seguente:\n\n$3\n\n<strong>Nota:</strong> se esci ora dall'installazione senza scaricare il file di configurazione che è stato generato, questo poi non sarà più disponibile in seguito.\n\nQuando hai fatto, puoi <strong>[$2 entrare nel tuo wiki]</strong>.",
        "config-download-localsettings": "Scarica <code>LocalSettings.php</code>",
        "config-help": "aiuto",
        "config-help-tooltip": "fai clic per espandere",
index 91df060..d0490b4 100644 (file)
@@ -50,6 +50,7 @@
        "config-memory-bad": "'''Opgepasst:''' De Parameter <code>memory_limit</code> vu PHP ass $1.\nDat ass wahrscheinlech ze niddreg.\nD'Installatioun kéint net funktionéieren.",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] ass installéiert",
        "config-apc": "[http://www.php.net/apc APC] ass installéiert",
+       "config-apcu": "[http://www.php.net/apcu APCu] ass installéiert.",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] ass installéiert",
        "config-diff3-bad": "GNU diff3 gouf net fonnt.",
        "config-git": "D'Software Git fir d'Kontroll vu Versioune gouf fonnt: <code>$1</code>.",
index 7fe3a64..23f3de1 100644 (file)
@@ -26,6 +26,8 @@
        "config-page-copying": "Kopē",
        "config-restart": "Jā, restartēt",
        "config-env-php": "PHP $1 ir uzstādīts.",
+       "config-env-hhvm": "HHVM $1 ir uzstādīts.",
+       "config-apcu": "[http://www.php.net/apcu APCu] ir uzstādīts",
        "config-diff3-bad": "GNU diff3 nav atrasts.",
        "config-db-name": "Datubāzes nosaukums:",
        "config-db-username": "Datubāzes lietotājvārds:",
        "config-email-settings": "E-pasta iestatījumi",
        "config-logo": "Logo URL:",
        "config-cc-again": "Izvēlies vēlreiz...",
+       "config-memcached-servers": "Memcached serveri:",
        "config-extensions": "Paplašinājumi",
        "config-install-step-done": "Gatavs",
+       "config-install-user": "Veido datu bāzes lietotāju",
        "config-help": "palīdzība",
        "config-help-tooltip": "uzspiediet, lai izvērstu",
        "mainpagetext": "<strong>MediaWiki veiksmīgi instalēts.</strong>",
index 8b8e0fb..5e3fc0d 100644 (file)
@@ -21,9 +21,9 @@
        "config-page-dbsettings": "डाटाबेस कुंजी",
        "config-page-name": "नाम",
        "config-page-options": "विकल्प",
-       "config-page-install": "सà¥\8dथापित à¤\95रà¥\81",
+       "config-page-install": "सà¥\8dथापित à¤\95रà¥\80",
        "config-page-complete": "पूर्ण!",
-       "config-page-restart": "स्थापनाके पुनारम्भ करु",
+       "config-page-restart": "स्थापनाक पुनारम्भ करी",
        "config-page-readme": "पढू",
        "config-page-releasenotes": "रिलीज नोट्स",
        "config-page-copying": "अनुकरण",
index f8b66c1..649d9ce 100644 (file)
@@ -61,6 +61,7 @@
        "config-memory-bad": "'''Предупредување:''' <code>memory_limit</code> за PHP изнесува $1.\nОва е веројатно премалку.\nВоспоставката може да не успее!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] е воспоставен",
        "config-apc": "[http://www.php.net/apc APC] е воспоставен",
+       "config-apcu": "[http://www.php.net/apcu APCu] е воспоставен",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] е воспоставен",
        "config-no-cache-apcu": "<strong>Предупредување:</strong> Не можев да го најдам [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] или [http://www.iis.net/download/WinCacheForPhp WinCache].\nМеѓускладирањето на објекти не е овозможено",
        "config-mod-security": "'''Предупредување''': на вашиот опслужувач има овозможено [http://modsecurity.org/ mod_security]. Ако не е поставено како што треба, ова може да предизвика проблеми кај МедијаВики и други програми што им овозможуваат на корисниците да објавуваат произволни содржини.\nПогледнете ја [http://modsecurity.org/documentation/ mod_security документацијата] или обратете се кај домаќинот ако наидете на случајни грешки.",
        "config-cache-options": "Нагодувања за меѓускладирање на објекти:",
        "config-cache-help": "Меѓускладирањето на објекти се користи за зголемување на брзината на МедијаВики со меѓускладирање на често употребуваните податоци.\nОва многу се препорачува на средни до големи викија, но од тоа ќе имаат полза и малите викија.",
        "config-cache-none": "Без меѓускладирање (не се остранува ниедна функција, но може да влијае на брзината кај поголеми викија)",
-       "config-cache-accel": "Меѓускладирање на PHP-објекти (APC, XCache или WinCache)",
+       "config-cache-accel": "Меѓускладирање на PHP-објекти (APC, APCu, XCache или WinCache)",
        "config-cache-memcached": "Користи Memcached (бара дополнително поставување и нагодување)",
        "config-memcached-servers": "Memcached-опслужувачи:",
        "config-memcached-help": "Список на IP-адреси за употреба во Memcached.\nТреба да се наведе по една во секој ред, како и портата што ќе се користи. На пример:\n 127.0.0.1:11211\n 192.168.1.25:1234",
index 29bbb71..0e5d40a 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.",
@@ -65,6 +66,7 @@
        "config-memory-bad": "'''Advarsel:''' PHPs <code>memory_limit</code> er $1.\nDette er sannsynligvis for lavt.\nInstallasjonen kan mislykkes!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] er innstallert",
        "config-apc": "[http://www.php.net/apc APC] er innstallert",
+       "config-apcu": "[http://www.php.net/apcu APCu] er installert",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] er installert",
        "config-no-cache-apcu": "<strong>Advarsel:</strong> Kunne ikke finne [http://www.php.net/apc APC], [http://xcache.lighttpd.net/ XCache] eller [http://www.iis.net/download/WinCacheForPhp WinCache].\nObjekthurtiglagring er ikke aktivert.",
        "config-mod-security": "'''Advarsel''': Din web-tjener har [http://modsecurity.org/ mod_security] påslått. Hvis denne er feilinnstilt, kan det gi problemer for MediaWiki eller annen programvare som tillater brukere å poste vilkårlig innhold.\nSjekk [http://modsecurity.org/documentation/ mod_security-dokumentasjonen] eller ta kontakt med din nettleverandør hvis du opplever tilfeldige feil.",
        "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-cache-options": "Innstillinger for objekt-mellomlagring:",
        "config-cache-help": "Objekt-mellomlagring brukes for å forbedre hastigheten for MediaWiki. Ofte forekommende data lagres for gjenbruk.\nMiddels til store nettsteder bør absolutt aktivisere mellomlagring, med også små nettsteder kan ha nytte av dette.",
        "config-cache-none": "Ingen mellomlagring (ingen funksjonalitet mistes, men hastigheten kan bli dårlig for store wikier-nettsteder)",
-       "config-cache-accel": "Mellomlagring av PHP-objekter (APC, XCache or WinCache)",
+       "config-cache-accel": "Mellomlagring av PHP-objekter (APC, APCu, XCache eller WinCache)",
        "config-cache-memcached": "Bruk Memcached (krever tilleggsoppsett og -konfigurering)",
        "config-memcached-servers": "Memcached-servere:",
        "config-memcached-help": "Liste av IP-adresser for bruk fra Memcached.\nDet bør angis en per linje sammen med porten som brukes. For eksempel:\n 127.0.0.1:11211\n 192.168.1.25:1234",
        "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 7c50e82..ce9ff81 100644 (file)
@@ -82,5 +82,5 @@
        "config-help": "सहायता",
        "config-help-tooltip": "विस्तार गर्न क्लीक गर्नुहोस्",
        "mainpagetext": "'''मीडिया सफलतापूर्वक कम्प्यूटरमा स्थापित भयो ।'''",
-       "mainpagedocfooter": " विकी अनुप्रयोग कसरी प्रयोग गर्ने भन्ने जानकारीको लागि  [https://meta.wikimedia.org/wiki/Help:Contents प्रयोगकर्ता सहायता] हेर्नुहोस्\n\n== सुरू गर्नको लागि  ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings विन्यास सेटिङ्ग सूची]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ मेडियाविकि सामान्य प्रश्नका उत्तरहरु]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce मेडियाविकि सुचना मेलिङ्ग सूची]"
+       "mainpagedocfooter": " विकी अनुप्रयोग कसरी प्रयोग गर्ने भन्ने जानकारीको लागि  [https://meta.wikimedia.org/wiki/Help:Contents प्रयोगकर्ता सहायता] हेर्नुहोस्\n\n विकी अनुप्रयोग कसरी प्रयोग गर्ने भन्ने जानकारीको लागि  [https://meta.wikimedia.org/wiki/Help:Contents प्रयोगकर्ता सहायता] हेर्नुहोस्\n\n== सुरू गर्नको लागि  ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings विन्यास सेटिङ्ग सूची]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ मेडियाविकि सामान्य प्रश्नका उत्तरहरू]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce मेडियाविकि सूचना मेलिङ्ग सूची]"
 }
index 78649ce..0a77232 100644 (file)
@@ -57,7 +57,7 @@
        "config-restart": "Ja, opnieuw starten",
        "config-welcome": "=== Controle omgeving ===\nEr worden een aantal basiscontroles uitgevoerd met als doel vast te stellen of deze omgeving geschikt is voor een installatie van MediaWiki.\nLever deze gegevens aan als u ondersteuning vraagt bij de installatie.",
        "config-copyright": "=== Auteursrechten en voorwaarden ===\n\n$1\n\nDit programma is vrije software. U mag het verder verspreiden en/of aanpassen in overeenstemming met de voorwaarden van de GNU General Public License zoals uitgegeven door de Free Software Foundation; ofwel versie 2 van de Licentie of - naar uw keuze - enige latere versie.\n\nDit programma wordt verspreid in de hoop dat het nuttig is, maar '''zonder enige garantie''', zelfs zonder de impliciete garantie van '''verkoopbaarheid''' of '''geschiktheid voor een bepaald doel'''.\nZie de GNU General Public License voor meer informatie.\n\nSamen met dit programma hoort u een <doclink href=Copying>exemplaar van de GNU General Public License</doclink> ontvangen te hebben; zo niet, schrijf dan aan de Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, Verenigde Staten. Of [http://www.gnu.org/copyleft/gpl.html lees de licentie online].",
-       "config-sidebar": "* [https://www.mediawiki.org MediaWiki thuispagina]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Gebruikershandleiding] (Engelstalig)\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Beheerdershandleiding] (Engelstalig)\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Veelgestelde vragen] (Engelstalig)\n----\n* <doclink href=Readme>Leesmij</doclink> (Engelstalig)\n* <doclink href=ReleaseNotes>Release notes</doclink> (Engelstalig)\n* <doclink href=Copying>Kopiëren</doclink> (Engelstalig)\n* <doclink href=UpgradeDoc>Versie bijwerken</doclink> (Engelstalig)",
+       "config-sidebar": "* [https://www.mediawiki.org MediaWiki-thuispagina]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Gebruikershandleiding] (Engelstalig)\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Beheerdershandleiding] (Engelstalig)\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Veelgestelde vragen] (Engelstalig)\n----\n* <doclink href=Readme>Leesmij</doclink> (Engelstalig)\n* <doclink href=ReleaseNotes>Release notes</doclink> (Engelstalig)\n* <doclink href=Copying>Kopiëren</doclink> (Engelstalig)\n* <doclink href=UpgradeDoc>Versie bijwerken</doclink> (Engelstalig)",
        "config-env-good": "De omgeving is gecontroleerd.\nU kunt MediaWiki installeren.",
        "config-env-bad": "De omgeving is gecontroleerd.\nU kunt MediaWiki niet installeren.",
        "config-env-php": "PHP $1 is op dit moment geïnstalleerd.",
        "config-ns-site-name": "Zelfde als de wiki: $1",
        "config-ns-other": "Andere (geef aan welke)",
        "config-ns-other-default": "MijnWiki",
-       "config-project-namespace-help": "In het kielzog van Wikipedia beheren veel wiki's hun beleidspagina's apart van hun inhoudelijke pagina's in een \"'''projectnaamruimte'''\".\nAlle paginanamen in deze naamruimte beginnen met een bepaald voorvoegsel dat u hier kunt opgeven.\nDit voorvoegsel wordt meestal afgeleid van de naam van de wiki, maar het kan geen bijzondere tekens bevatten als \"#\" of \":\".",
+       "config-project-namespace-help": "In het kielzog van Wikipedia beheren veel wiki's hun beleidspagina's apart van hun inhoudelijke pagina's in een '''projectnaamruimte'''.\nAlle paginanamen in deze naamruimte beginnen met een bepaald voorvoegsel dat u hier kunt opgeven.\nDit voorvoegsel wordt meestal afgeleid van de naam van de wiki, maar het kan geen bijzondere tekens bevatten als \"#\" of \":\".",
        "config-ns-invalid": "De opgegeven naamruimte \"<nowiki>$1</nowiki>\" is ongeldig.\nGeef een andere naamruimte op.",
        "config-ns-conflict": "De opgegeven naamruimte \"<nowiki>$1</nowiki>\" conflicteert met een standaard naamruimte in MediaWiki.\nGeef een andere naam op voor de projectnaamruimte.",
        "config-admin-box": "Beheerdersgebruiker",
        "config-subscribe": "Abonneren op de [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce mailinglijst releaseaankondigen].",
        "config-subscribe-help": "Dit is een mailinglijst met een laag volume voor aankondigingen van nieuwe versies, inclusief belangrijke aankondigingen met betrekking tot beveiliging.\nAbonneer uzelf erop en werk uw MediaWiki-installatie bij als er nieuwe versies uitkomen.",
        "config-subscribe-noemail": "U hebt geprobeerd zich te abonneren op de mailinglijst voor release-aankondigingen zonder een e-mailadres op te geven.\nGeef een e-mailadres op als u zich wilt abonneren op de mailinglijst.",
+       "config-pingback": "Gegevens over deze installatie delen met MediaWiki-ontwikkelaars.",
        "config-almost-done": "U bent bijna klaar!\nAls u wilt kunt u de overige instellingen overslaan en de wiki nu installeren.",
        "config-optional-continue": "Stel me meer vragen.",
        "config-optional-skip": "Laat maar zitten, installeer gewoon de wiki.",
        "config-logo": "URL voor logo:",
        "config-logo-help": "Het standaarduiterlijk van MediaWiki bevat ruimte voor een logo van 135x160 pixels boven het menu.\nUpload een afbeelding met de juiste afmetingen en voer de URL hier in.\n\nU kunt <code>$wgStylePath</code> of <code>$wgScriptPath</code> gebruiken als uw logo relatief is aan een van deze paden.\n\nAls u geen logo wilt gebruiken, kunt u dit veld leeg laten.",
        "config-instantcommons": "Instant Commons inschakelen",
-       "config-instantcommons-help": "[https://www.mediawiki.org/wiki/InstantCommons Instant Commons] is functie die het mogelijk maakt om afbeeldingen, geluidsbestanden en andere mediabestanden te gebruiken van de website [https://commons.wikimedia.org/ Wikimedia Commons].\nHiervoor heeft MediaWiki toegang nodig tot Internet.\n\nMeer informatie over deze functie en hoe deze in te stellen voor andere wiki's dan Wikimedia Commons is te vinden in de [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos handleiding].",
+       "config-instantcommons-help": "[https://www.mediawiki.org/wiki/InstantCommons Instant Commons] is functie die het mogelijk maakt om afbeeldingen, geluidsbestanden en andere mediabestanden te gebruiken van de website [https://commons.wikimedia.org/ Wikimedia Commons].\nHiervoor heeft MediaWiki toegang nodig tot internet.\n\nMeer informatie over deze functie en hoe deze in te stellen voor andere wiki's dan Wikimedia Commons is te vinden in de [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos handleiding].",
        "config-cc-error": "De licentiekiezer van Creative Commons heeft geen resultaat opgeleverd.\nVoer de licentie handmatig in.",
        "config-cc-again": "Opnieuw kiezen...",
        "config-cc-not-chosen": "Kies de Creative Commonslicentie die u wilt gebruiken en klik op \"proceed\".",
index 8d54c98..434201d 100644 (file)
@@ -71,7 +71,7 @@
        "config-unicode-using-intl": "Korzystanie z [http://pecl.php.net/intl rozszerzenia intl PECL] do normalizacji Unicode.",
        "config-unicode-pure-php-warning": "<strong>Uwaga:<strong> [http://pecl.php.net/intl Rozszerzenie intl PECL] do obsługi normalizacji Unicode nie jest dostępne. Użyta zostanie mało wydajna zwykła implementacja w PHP.\nJeśli prowadzisz stronę o dużym natężeniu ruchu, powinieneś zapoznać się z informacjami o [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalizacji Unicode].",
        "config-unicode-update-warning": "<strong>Uwaga:</strong> zainstalowana wersja normalizacji Unicode korzysta z nieaktualnej biblioteki [http://site.icu-project.org/ projektu ICU].\nPowinieneś [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations wykonać aktualizację], jeśli chcesz korzystać w pełni z Unicode.",
-       "config-no-db": "Nie można odnaleźć właściwego sterownika bazy danych! Musisz zainstalować sterownik bazy danych dla PHP.\nMożna użyć {{PLURAL:$2|następującego typu bazy|następujących typów baz} danych: $1.\n\nJeśli skompilowałeś PHP samodzielnie, skonfiguruj je ponownie z włączonym klientem bazy danych, na przykład za pomocą polecenia <code>./configure --with-mysqli</code>.\nJeśli zainstalowałeś PHP jako pakiet Debiana lub Ubuntu, musisz również zainstalować np. moduł <code>php5-mysql</code>.",
+       "config-no-db": "Nie można odnaleźć właściwego sterownika bazy danych! Musisz zainstalować sterownik bazy danych dla PHP.\nMożna użyć {{PLURAL:$2|następującego typu bazy|następujących typów baz}} danych: $1.\n\nJeśli skompilowałeś PHP samodzielnie, skonfiguruj go ponownie z włączonym klientem bazy danych, na przykład za pomocą polecenia <code>./configure --with-mysqli</code>.\nJeśli zainstalowałeś PHP jako pakiet Debiana lub Ubuntu, musisz również zainstalować np. moduł <code>php5-mysql</code>.",
        "config-outdated-sqlite": "'''Ostrzeżenie''': masz SQLite  $1, która jest niższa od minimalnej wymaganej wersji  $2 . SQLite będzie niedostępne.",
        "config-no-fts3": "'''Uwaga''' – SQLite został skompilowany bez [//sqlite.org/fts3.html modułu FTS3] – funkcje wyszukiwania nie będą dostępne.",
        "config-pcre-old": "<strong>Błąd krytyczny:</strong> Wymagany jest PCRE w wersji $1 lub nowszej.\nTwój plik wykonywalny PHP jest powiązany z wersją PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Więcej informacji].",
@@ -80,6 +80,7 @@
        "config-memory-bad": "'''Uwaga:''' PHP <code>memory_limit</code> jest ustawione na $1.\nTo jest prawdopodobnie zbyt mało.\nInstalacja może się nie udać!",
        "config-xcache": "[Http://trac.lighttpd.net/xcache/ XCache] jest zainstalowany",
        "config-apc": "[Http://www.php.net/apc APC] jest zainstalowany",
+       "config-apcu": "[http://www.php.net/apcu APCu] jest zainstalowany",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] jest zainstalowany",
        "config-no-cache-apcu": "<strong>Ostrzeżenie:</strong> Nie można znaleźć [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] lub [http://www.iis.net/download/WinCacheForPhp WinCache].\nPamięć podręczna obiektów nie zostanie włączona.",
        "config-mod-security": "''' Ostrzeżenie ''': Serwer sieci web ma włączone [http://modsecurity.org/ mod_security]. Jeśli jest niepoprawnie skonfigurowane, może być przyczyną problemów MediaWiki lub innego oprogramowania, które pozwala użytkownikom na wysyłanie dowolnej zawartości.\nSprawdź w [http://modsecurity.org/documentation/ dokumentacji mod_security] lub skontaktuj się z obsługa hosta, jeśli wystąpią losowe błędy.",
        "config-cc-not-chosen": "Wybierz, którą chcesz licencję Creative Commons i kliknij „proceed”.",
        "config-advanced-settings": "Konfiguracja zaawansowana",
        "config-cache-options": "Ustawienia buforowania obiektów:",
-       "config-cache-help": "Buforowanie obiekto jest używane aby przyspieszyć MediaWiki przez trzymanie w pamięci podręcznej często używanych danych.\nŚrednie oraz duże witryny są wysoce zachęcane by je włączyć, a małe witryny także dostrzegą korzyści.",
+       "config-cache-help": "Buforowanie obiektów jest używane do przyspieszenia MediaWiki przez trzymanie w pamięci podręcznej często używanych danych.\nŚrednie oraz duże witryny są wysoce zachęcane by je włączyć, ale małe witryny również dostrzegą korzyści.",
        "config-cache-none": "Brak buforowania (wszystkie funkcje będą działać, ale mogą wystąpić kłopoty z wydajnością na dużych witrynach wiki)",
-       "config-cache-accel": "Buforowania obiektów PHP (APC, XCache lub WinCache)",
+       "config-cache-accel": "Buforowania obiektów PHP (APC, APCu, XCache lub WinCache)",
        "config-cache-memcached": "Użyj Memcached (wymaga dodatkowej instalacji i konfiguracji)",
        "config-memcached-servers": "Serwery Memcached:",
        "config-memcached-help": "Lista adresów IP do wykorzystania przez Memcached.\nAdresy powinny być umieszczane po jednym w linii i określać również wykorzystywany port. Na przykład:\n 127.0.0.1:11211\n 192.168.1.25:1234",
        "config-install-begin": "Po naciśnięciu \"{{int:config-continue}}\", rozpocznie się instalacja MediaWiki.\nJeśli nadal chcesz dokonać zmian, naciśnij \"{{int:config-back}}\".",
        "config-install-step-done": "gotowe",
        "config-install-step-failed": "nieudane",
-       "config-install-extensions": "Włącznie z rozszerzeniami",
+       "config-install-extensions": "Dołączanie rozszerzeń",
        "config-install-database": "Konfigurowanie bazy danych",
        "config-install-schema": "Tworzenie schematu",
        "config-install-pg-schema-not-exist": "Schemat PostgreSQL nie istnieje.",
        "config-help": "pomoc",
        "config-help-tooltip": "kliknij, aby rozwinąć",
        "config-nofile": "Nie udało się odnaleźć pliku \"$1\". Czy nie został usunięty?",
-       "config-extension-link": "Czy wiesz, że twoja wiki obsługuje [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions/pl rozszerzenia]?\n\nMożesz przejrzeć [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category rozszerzenia według kategorii] lub [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] aby zobaczyć pełną listę rozszerzeń.",
+       "config-extension-link": "Czy wiesz, że twoja wiki obsługuje [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions rozszerzenia]?\n\nMożesz przejrzeć [https://www.mediawiki.org/wiki/Category:Extensions_by_category rozszerzenia według kategorii] lub [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix], aby zobaczyć pełną listę rozszerzeń.",
        "mainpagetext": "<strong>Instalacja MediaWiki powiodła się.</strong>",
-       "mainpagedocfooter": "Zobacz [https://meta.wikimedia.org/wiki/Help:Contents przewodnik użytkownika], aby uzyskać informacje o działaniu oprogramowania wiki.\n\n== Na początek ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings/pl Lista ustawień konfiguracyjnych]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/pl MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Komunikaty o nowych wersjach MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Przetłumacz MediaWiki na swój język]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam \nDowiedz się, jak walczyć ze spamem na swojej wiki]"
+       "mainpagedocfooter": "Zobacz [https://meta.wikimedia.org/wiki/Help:Contents/pl przewodnik użytkownika], aby uzyskać informacje o działaniu oprogramowania wiki.\n\n== Na początek ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista ustawień konfiguracyjnych]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Komunikaty o nowych wersjach MediaWiki (lista dyskusyjna)]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Przetłumacz MediaWiki na swój język]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Dowiedz się, jak walczyć ze spamem na swojej wiki]"
 }
index 49f242a..38e4976 100644 (file)
@@ -74,7 +74,9 @@
        "config-memory-bad": "'''Aviso:''' A configuração <code>memory_limit</code> do PHP é $1.\nIsto é provavelmente demasiado baixo.\nA instalação poderá falhar!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] instalada",
        "config-apc": "[http://www.php.net/apc APC] instalada",
+       "config-apcu": "[http://www.php.net/apcu APCu] está instalado",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] instalada",
+       "config-no-cache-apcu": "<strong>Aviso:</strong> Não foram encontrados o [http://www.php.net/apcu APCu], o [http://xcache.lighttpd.net/ XCache] ou o [http://www.iis.net/download/WinCacheForPhp WinCache].\nA cache de objetos não está ativa.",
        "config-mod-security": "'''Aviso''': O seu servidor de internet tem o [http://modsecurity.org/ mod_security] ativado. Se este estiver mal configurado, pode causar problemas ao MediaWiki ou a outros programas, permitindo que os utilizadores publiquem conteúdos arbitrários.\nConsulte a [http://modsecurity.org/documentation/ mod_security documentação] ou peça apoio ao fornecedor do alojamento do seu servidor se encontrar erros aleatórios.",
        "config-diff3-bad": "O GNU diff3 não foi encontrado.",
        "config-git": "Foi encontrado o software de controlo de versões Git: <code>$1</code>.",
        "config-subscribe": "Subscreva a [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce lista de divulgação de anúncios de lançamento].",
        "config-subscribe-help": "Esta é uma lista de divulgação de baixo volume para anúncios de lançamento de versões novas, incluindo anúncios de segurança importantes.\nDeve subscrevê-la e atualizar a sua instalação MediaWiki quando são lançadas versões novas.",
        "config-subscribe-noemail": "Tentou subscrever a lista de divulgação dos anúncios de novas versões, sem fornecer um endereço de correio electrónico.\nPara subscrever esta lista de divulgação tem de fornecer um endereço de correio electrónico.",
+       "config-pingback": "Partilhar dados sobre esta instalação com os programadores do MediaWiki.",
+       "config-pingback-help": "Se selecionar esta opção, o MediaWiki fará periodicamente um <i>ping</i> a https://www.mediawiki.org com dados básicos acerca desta instância do MediaWiki. Estes dados incluem, por exemplo, o tipo de sistema, a versão do PHP e a base de dados que escolheu. A Wikimedia Foundation partilha estes dados com os programadores do MediaWiki, para ajudar a guiar o esforço de desenvolvimento futuro. Para o seu sistema, serão enviados os seguintes dados:\n<pre>$1</pre>",
        "config-almost-done": "Está quase a terminar!\nAgora pode saltar as configurações restantes e instalar já a wiki.",
        "config-optional-continue": "Faz-me mais perguntas.",
        "config-optional-skip": "Já estou aborrecido, instala lá a wiki.",
        "config-cache-options": "Configuração da cache de objetos:",
        "config-cache-help": "A cache de objetos é usada para melhorar o desempenho do MediaWiki. Armazena dados usados com frequência.\nSites de tamanho médio ou grande são altamente encorajados a ativar esta funcionalidade e os sites pequenos também terão alguns benefícios em fazê-lo.",
        "config-cache-none": "Sem cache (não é removida nenhuma funcionalidade, mas a velocidade de operação pode ser afectada nas wikis grandes)",
-       "config-cache-accel": "Cache de objetos do PHP (APC, XCache ou WinCache)",
+       "config-cache-accel": "Cache de objetos do PHP (APC, APCu, XCache ou WinCache)",
        "config-cache-memcached": "Usar Memcached (requer instalação e configurações adicionais)",
        "config-memcached-servers": "Servidores Memcached:",
        "config-memcached-help": "Lista de endereços IP que serão usados para o Memcached.\nDeve-se colocar um por linha e indicar a porta a utilizar. Por exemplo:\n 127.0.0.1:11211\n 192.168.1.25:1234",
        "config-skins": "Temas",
        "config-skins-help": "Os temas listados abaixo foram detetados no seu diretório <code>./skins</code>. Deverá ativar pelo menos um e escolher qual o escolhido por padrão.",
        "config-skins-use-as-default": "Usar este tema como padrão",
+       "config-skins-missing": "Não foi encontrado nenhum tema; o MediaWiki usará um tema de recurso até instalar temas adequados.",
        "config-skins-must-enable-some": "Deve escolher pelo menos um tema para ativar.",
        "config-skins-must-enable-default": "O tema escolhido como padrão deve ser ativado.",
        "config-install-alreadydone": "'''Aviso:''' Parece que já instalou o MediaWiki e está a tentar instalá-lo novamente.\nPasse para a próxima página, por favor.",
        "config-install-stats": "A inicializar as estatísticas",
        "config-install-keys": "A gerar as chaves secretas",
        "config-insecure-keys": "'''Aviso:''' {{PLURAL:$2|A chave segura|As chaves seguras}} ($1) {{PLURAL:$2|gerada durante a instalação não é completamente segura|geradas durante a instalação não são completamente seguras}}. Considere a possibilidade de {{PLURAL:$2|alterá-la|alterá-las}} manualmente.",
+       "config-install-updates": "Evitar executar atualizações desnecessárias",
+       "config-install-updates-failed": "<strong>Erro:</strong> A inserção de chaves de atualização nas tabelas falhou com o seguinte erro: $1",
        "config-install-sysop": "A criar a conta de administrador",
        "config-install-subscribe-fail": "Não foi possível subscrever a lista mediawiki-announce: $1",
        "config-install-subscribe-notpossible": "cURL não está instalado e <code>allow_url_fopen</code> não está disponível.",
index 833e7d6..d7e86be 100644 (file)
@@ -75,6 +75,7 @@
        "config-memory-bad": "Parameters:\n* $1 is the configured <code>memory_limit</code>.",
        "config-xcache": "Message indicates if this program is available",
        "config-apc": "Message indicates if this program is available",
+       "config-apcu": "Message indicates if this program is available",
        "config-wincache": "Message indicates if this program is available",
        "config-no-cache-apcu": "Status message in the MediaWiki installer environment checks.",
        "config-mod-security": "Status message in the MediaWiki installer environment checks.",
index 53b4c9e..bead2f1 100644 (file)
@@ -79,6 +79,7 @@
        "config-memory-bad": "'''Внимание:''' размер PHP <code>memory_limit</code> составляет $1.\nВероятно, этого слишком мало.\nУстановка может потерпеть неудачу!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] установлен",
        "config-apc": "[http://www.php.net/apc APC] установлен",
+       "config-apcu": "[http://www.php.net/apcu APCu] установлен",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] установлен",
        "config-no-cache-apcu": "'''Внимание:''' Не найдены [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] или [http://www.iis.net/download/WinCacheForPhp WinCache].\nКэширование объектов будет отключено.",
        "config-mod-security": "<strong>Внимание</strong>: На вашем веб-сервере включен [http://modsecurity.org/ mod_security]/mod_security2. Многие его стандартные настройки могут вызывать проблемы для MediaWiki или другого ПО, позволяющего пользователям отправлять на сервер произвольный контент.\nОбратитесь к [http://modsecurity.org/documentation/ документации mod_security] или в службу поддержки вашего хостинг-провайдера, если вы сталкиваетесь со случайными ошибками.",
        "config-cache-options": "Параметры кэширования объектов:",
        "config-cache-help": "Кэширование объектов используется для повышения скорости MediaWiki путем кэширования часто используемых данных.\nДля средних и больших сайтов кеширование настоятельно рекомендуется включать, а для небольших сайтов кеширование может показать преимущество.",
        "config-cache-none": "Без кэширования (никакой функционал не теряется, но крупные вики-сайты могут работать медленнее)",
-       "config-cache-accel": "PHP кэширование объектов (APC, XCache или WinCache)",
+       "config-cache-accel": "Кэширование PHP-объектов (APC, APCu, XCache или WinCache)",
        "config-cache-memcached": "Использовать Memcached (требует дополнительной настройки)",
        "config-memcached-servers": "Сервера Memcached:",
        "config-memcached-help": "Список IP-адресов, используемых Memcached.\nПеречислите по одному адресу на строку с указанием портов. Например:\n 127.0.0.1:11211\n 192.168.1.25:1234",
index 33fa590..21a3297 100644 (file)
@@ -2,11 +2,12 @@
        "@metadata": {
                "authors": [
                        "Sindhu",
-                       "Aursani"
+                       "Aursani",
+                       "Mehtab ahmed"
                ]
        },
        "config-information": "معلومات",
-       "config-localsettings-badkey": "توهان جي ڏنل ڪنجي غيردرست آهي.",
+       "config-localsettings-badkey": "توهان جي سنواري ڏنل ڪنجي غيردرست آهي.",
        "config-your-language": "توهان جي ٻولي:",
        "config-wiki-language": "وڪِي ٻولي:",
        "config-back": "پوئتي ←",
@@ -26,6 +27,5 @@
        "config-page-existingwiki": "موجوده وڪِي",
        "config-restart": "ها، وري کان شروع ڪريو",
        "config-env-php": "PHP $1 تنصيب ٿي چڪو",
-       "config-env-hhvm": "HHVM $1 تنصيب ٿي چڪو.",
-       "config-xml-bad": "PHP جو XML ماڊيول کٽل آهي. ذريعات‌وڪيءَ کي ان ماڊيول ۾  فنڪشنس گھربل آهن ۽ اها موجوده ترتيب يا ڪنفيگيوريشن ۾ ڪم نہ ڪندي. \nتوهان کي گھرجي تہ php-xml RPM پيڪيج تنصيب ڪريو."
+       "config-env-hhvm": "HHVM $1 تنصيب ٿي چڪو."
 }
index 41be1ac..d6ee081 100644 (file)
        "config-cc-again": "Izberi ponovno ...",
        "config-cc-not-chosen": "Izberite licenco Creative Commons, ki jo želite uporabiti, in kliknite »proceed«.",
        "config-advanced-settings": "Napredna konfiguracija",
-       "config-cache-accel": "Predpomnjenje predmetov PHP (APC, XCache ali WinCache)",
+       "config-cache-accel": "Predpomnjenje predmetov PHP (APC, APCu, XCache ali WinCache)",
        "config-cache-memcached": "Uporabi Memcached (zahteva dodatno namestitev in konfiguracijo)",
        "config-memcached-servers": "Strežniki Memcached:",
        "config-memcache-badip": "Vnesli ste neveljaven IP-naslov za Memcached: $1",
index 20ebf9b..45c5a7d 100644 (file)
@@ -67,6 +67,7 @@
        "config-memory-bad": "''' Varning:''' PHP:s <code>memory_limit</code> är $1.\nDetta är förmodligen för lågt.\nInstallationen kan misslyckas!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] är installerat",
        "config-apc": "[http://www.php.net/apc APC] är installerat",
+       "config-apcu": "[http://www.php.net/apcu APCu] är installerat",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] är installerat",
        "config-no-cache-apcu": "'''Varning:''' Kunde inte hitta [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] eller [http://www.iis.net/download/WinCacheForPhp WinCache].\nCachelagring av objekt är inte aktiverat.",
        "config-mod-security": "'''Varning:''' Din webbserver har [http://modsecurity.org/ mod_security] aktiverat. Om felaktigt konfigurerat kan den skapa problem för MediaWiki eller annan programvara som tillåter användaren att posta godtyckligt innehåll.\nTitta på [http://modsecurity.org/documentation/ mod_security-dokumentationen] eller kontakta din värd om du påträffar slumpmässiga fel.",
        "config-cache-options": "Inställningar för cachelagring av objekt:",
        "config-cache-help": "Cachelagring av objekt används för att förbättra hastigheten på MediaWiki genom att cachelagra data som används ofta.\nMedelstora till stora webbplatser är starkt uppmuntrade att aktivera detta, och små webbplatser kommer även att se fördelar.",
        "config-cache-none": "Ingen cachelagring (ingen funktionalitet tas bort, men hastighet kan påverkas på större wiki-webbplatser)",
-       "config-cache-accel": "Cachelagring av PHP-objekt (APC, XCache eller WinCache)",
+       "config-cache-accel": "Cachelagring av PHP-objekt (APC, APCu, XCache eller WinCache)",
        "config-cache-memcached": "Använda Memcached (kräver ytterligare inställningar och konfiguration)",
        "config-memcached-servers": "Memcached-servrar:",
        "config-memcached-help": "Lista över IP-adresser som ska användas för Memcached.\nBör ange en per rad och specificera den port som ska användas. Till exempel:\n 127.0.0.1:11211\n 192.168.1.25:1234",
index d541696..1410ae7 100644 (file)
@@ -80,6 +80,7 @@
        "config-memory-bad": "<strong>警告:</strong>PHP的内存使用上限<code>memory_limit</code>为$1。\n该设定可能过低,并导致安装失败!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache]已安装",
        "config-apc": "[http://www.php.net/apc APC]已安装",
+       "config-apcu": "[http://www.php.net/apcu APCu]已安装",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache]已安装",
        "config-no-cache-apcu": "<strong>警告:</strong>找不到[http://www.php.net/apcu APCu]、[http://xcache.lighttpd.net/ XCache]或[http://www.iis.net/download/WinCacheForPhp WinCache]。\n对象缓存未启用。",
        "config-mod-security": "<strong>警告:</strong>您的web服务器已启用[http://modsecurity.org/ mod_security]/mod_security2。它的很多常见配置可能导致MediaWiki及其他软件允许用户发布任意内容的问题。如果可能,这应当被禁用。否则,当您遭遇随机错误时,请参考[http://modsecurity.org/documentation/ mod_security 文档]或联络您的主机支持。",
        "config-cache-options": "对象缓存设置:",
        "config-cache-help": "对象缓存可通过缓存频繁使用的数据来提高MediaWiki的速度。高度推荐中到大型的网站启用该功能,小型网站亦能从其中受益。",
        "config-cache-none": "无缓存(不影响功能,但对较大型的wiki网站会有速度影响)",
-       "config-cache-accel": "PHP对象缓存(APC、XCache或WinCache)",
+       "config-cache-accel": "PHP对象缓存(APC、APCu、XCache或WinCache)",
        "config-cache-memcached": "使用Memcached(需要另外安装并配置)",
        "config-memcached-servers": "Memcached服务器:",
        "config-memcached-help": "用于Memcached的IP地址列表。请保持每行一条,并指定要使用的端口。例如:\n127.0.0.1:11211\n192.168.1.25:1234",
index ab21779..2f2e934 100644 (file)
        "config-cache-options": "物件快取設定:",
        "config-cache-help": "物件快取是用來增進 MediaWiki 速度的一項功能,透過快取經常使用的資料。\n中型到大型的網站我們會建議開啟這個選項,對小型的網站也有一定程度的效果。",
        "config-cache-none": "不快取 (不會影響功能,但在大型 Wiki 網站可能會有處理速度的問題)",
-       "config-cache-accel": "使用 PHP 物件快取 (APC、XCache 或 WinCache)",
+       "config-cache-accel": "使用 PHP 物件快取 (APC、APCu、XCache 或 WinCache)",
        "config-cache-memcached": "使用 Memcached (需要額外安裝與設定)",
        "config-memcached-servers": "Memcached 伺服器:",
        "config-memcached-help": "請列出 Memcached 伺服器的 IP 位址。\n每一行只指定一個位置並且要註明使用的埠號,例如:\n 127.0.0.1:11211\n 192.168.1.25:1234",
index bbd0ddb..f814cee 100644 (file)
@@ -301,7 +301,9 @@ abstract class Job implements IJobSpecification {
        }
 
        /**
-        * @param callable $callback
+        * @param callable $callback A function with one parameter, the success status, which will be
+        *   false if the job failed or it succeeded but the DB changes could not be committed or
+        *   any deferred updates threw an exception. (This parameter was added in 1.28.)
         * @since 1.27
         */
        protected function addTeardownCallback( $callback ) {
@@ -310,12 +312,12 @@ abstract class Job implements IJobSpecification {
 
        /**
         * Do any final cleanup after run(), deferred updates, and all DB commits happen
-        *
+        * @param bool $status Whether the job, its deferred updates, and DB commit all succeeded
         * @since 1.27
         */
-       public function teardown() {
+       public function teardown( $status ) {
                foreach ( $this->teardownCallbacks as $callback ) {
-                       call_user_func( $callback );
+                       call_user_func( $callback, $status );
                }
        }
 
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 ed3aa9a..84ded8d 100644 (file)
@@ -283,7 +283,7 @@ class JobRunner implements LoggerAwareInterface {
                }
                // Always attempt to call teardown() even if Job throws exception.
                try {
-                       $job->teardown();
+                       $job->teardown( $status );
                } catch ( Exception $e ) {
                        MWExceptionHandler::logException( $e );
                }
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;
                        }
diff --git a/includes/libs/CryptRand.php b/includes/libs/CryptRand.php
new file mode 100644 (file)
index 0000000..6d18c81
--- /dev/null
@@ -0,0 +1,389 @@
+<?php
+/**
+ * A cryptographic random generator class used for generating secret keys
+ *
+ * This is based in part on Drupal code as well as what we used in our own code
+ * prior to introduction of this class.
+ *
+ * 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
+ *
+ * @author Daniel Friesen
+ * @file
+ */
+use Psr\Log\LoggerInterface;
+
+class CryptRand {
+       /**
+        * Minimum number of iterations we want to make in our drift calculations.
+        */
+       const MIN_ITERATIONS = 1000;
+
+       /**
+        * Number of milliseconds we want to spend generating each separate byte
+        * of the final generated bytes.
+        * This is used in combination with the hash length to determine the duration
+        * we should spend doing drift calculations.
+        */
+       const MSEC_PER_BYTE = 0.5;
+
+       /**
+        * A boolean indicating whether the previous random generation was done using
+        * cryptographically strong random number generator or not.
+        */
+       protected $strong = null;
+
+       /**
+        * List of functions to call to generate some random state
+        *
+        * @var callable[]
+        */
+       protected $randomFuncs = [];
+
+       /**
+        * List of files to generate some random state from
+        *
+        * @var string[]
+        */
+       protected $randomFiles = [];
+
+       /**
+        * @var LoggerInterface
+        */
+       protected $logger;
+
+       public function __construct( array $randomFuncs, array $randomFiles, LoggerInterface $logger ) {
+               $this->randomFuncs = $randomFuncs;
+               $this->randomFiles = $randomFiles;
+               $this->logger = $logger;
+       }
+
+       /**
+        * Initialize an initial random state based off of whatever we can find
+        * @return string
+        */
+       protected function initialRandomState() {
+               // $_SERVER contains a variety of unstable user and system specific information
+               // It'll vary a little with each page, and vary even more with separate users
+               // It'll also vary slightly across different machines
+               $state = serialize( $_SERVER );
+
+               // Try to gather a little entropy from the different php rand sources
+               $state .= rand() . uniqid( mt_rand(), true );
+
+               // Include some information about the filesystem's current state in the random state
+               $files = $this->randomFiles;
+
+               // We know this file is here so grab some info about ourselves
+               $files[] = __FILE__;
+
+               // We must also have a parent folder, and with the usual file structure, a grandparent
+               $files[] = __DIR__;
+               $files[] = dirname( __DIR__ );
+
+               foreach ( $files as $file ) {
+                       MediaWiki\suppressWarnings();
+                       $stat = stat( $file );
+                       MediaWiki\restoreWarnings();
+                       if ( $stat ) {
+                               // stat() duplicates data into numeric and string keys so kill off all the numeric ones
+                               foreach ( $stat as $k => $v ) {
+                                       if ( is_numeric( $k ) ) {
+                                               unset( $k );
+                                       }
+                               }
+                               // The absolute filename itself will differ from install to install so don't leave it out
+                               $path = realpath( $file );
+                               if ( $path !== false ) {
+                                       $state .= $path;
+                               } else {
+                                       $state .= $file;
+                               }
+                               $state .= implode( '', $stat );
+                       } else {
+                               // The fact that the file isn't there is worth at least a
+                               // minuscule amount of entropy.
+                               $state .= '0';
+                       }
+               }
+
+               // Try and make this a little more unstable by including the varying process
+               // id of the php process we are running inside of if we are able to access it
+               if ( function_exists( 'getmypid' ) ) {
+                       $state .= getmypid();
+               }
+
+               // If available try to increase the instability of the data by throwing in
+               // the precise amount of memory that we happen to be using at the moment.
+               if ( function_exists( 'memory_get_usage' ) ) {
+                       $state .= memory_get_usage( true );
+               }
+
+               foreach ( $this->randomFuncs as $randomFunc ) {
+                       $state .= call_user_func( $randomFunc );
+               }
+
+               return $state;
+       }
+
+       /**
+        * Randomly hash data while mixing in clock drift data for randomness
+        *
+        * @param string $data The data to randomly hash.
+        * @return string The hashed bytes
+        * @author Tim Starling
+        */
+       protected function driftHash( $data ) {
+               // Minimum number of iterations (to avoid slow operations causing the
+               // loop to gather little entropy)
+               $minIterations = self::MIN_ITERATIONS;
+               // Duration of time to spend doing calculations (in seconds)
+               $duration = ( self::MSEC_PER_BYTE / 1000 ) * MWCryptHash::hashLength();
+               // Create a buffer to use to trigger memory operations
+               $bufLength = 10000000;
+               $buffer = str_repeat( ' ', $bufLength );
+               $bufPos = 0;
+
+               // Iterate for $duration seconds or at least $minIterations number of iterations
+               $iterations = 0;
+               $startTime = microtime( true );
+               $currentTime = $startTime;
+               while ( $iterations < $minIterations || $currentTime - $startTime < $duration ) {
+                       // Trigger some memory writing to trigger some bus activity
+                       // This may create variance in the time between iterations
+                       $bufPos = ( $bufPos + 13 ) % $bufLength;
+                       $buffer[$bufPos] = ' ';
+                       // Add the drift between this iteration and the last in as entropy
+                       $nextTime = microtime( true );
+                       $delta = (int)( ( $nextTime - $currentTime ) * 1000000 );
+                       $data .= $delta;
+                       // Every 100 iterations hash the data and entropy
+                       if ( $iterations % 100 === 0 ) {
+                               $data = sha1( $data );
+                       }
+                       $currentTime = $nextTime;
+                       $iterations++;
+               }
+               $timeTaken = $currentTime - $startTime;
+               $data = MWCryptHash::hash( $data );
+
+               $this->logger->debug( "Clock drift calculation " .
+                       "(time-taken=" . ( $timeTaken * 1000 ) . "ms, " .
+                       "iterations=$iterations, " .
+                       "time-per-iteration=" . ( $timeTaken / $iterations * 1e6 ) . "us)\n" );
+
+               return $data;
+       }
+
+       /**
+        * Return a rolling random state initially build using data from unstable sources
+        * @return string A new weak random state
+        */
+       protected function randomState() {
+               static $state = null;
+               if ( is_null( $state ) ) {
+                       // Initialize the state with whatever unstable data we can find
+                       // It's important that this data is hashed right afterwards to prevent
+                       // it from being leaked into the output stream
+                       $state = MWCryptHash::hash( $this->initialRandomState() );
+               }
+               // Generate a new random state based on the initial random state or previous
+               // random state by combining it with clock drift
+               $state = $this->driftHash( $state );
+
+               return $state;
+       }
+
+       /**
+        * Return a boolean indicating whether or not the source used for cryptographic
+        * random bytes generation in the previously run generate* call
+        * was cryptographically strong.
+        *
+        * @return bool Returns true if the source was strong, false if not.
+        */
+       public function wasStrong() {
+               if ( is_null( $this->strong ) ) {
+                       throw new RuntimeException( __METHOD__ . ' called before generation of random data' );
+               }
+
+               return $this->strong;
+       }
+
+       /**
+        * Generate a run of (ideally) cryptographically random data and return
+        * it in raw binary form.
+        * You can use CryptRand::wasStrong() if you wish to know if the source used
+        * was cryptographically strong.
+        *
+        * @param int $bytes The number of bytes of random data to generate
+        * @param bool $forceStrong Pass true if you want generate to prefer cryptographically
+        *                          strong sources of entropy even if reading from them may steal
+        *                          more entropy from the system than optimal.
+        * @return string Raw binary random data
+        */
+       public function generate( $bytes, $forceStrong = false ) {
+
+               $this->logger->debug( "Generating cryptographic random bytes for\n" );
+
+               $bytes = floor( $bytes );
+               static $buffer = '';
+               if ( is_null( $this->strong ) ) {
+                       // Set strength to false initially until we know what source data is coming from
+                       $this->strong = true;
+               }
+
+               if ( strlen( $buffer ) < $bytes ) {
+                       // If available make use of mcrypt_create_iv URANDOM source to generate randomness
+                       // On unix-like systems this reads from /dev/urandom but does it without any buffering
+                       // and bypasses openbasedir restrictions, so it's preferable to reading directly
+                       // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate
+                       // entropy so this is also preferable to just trying to read urandom because it may work
+                       // on Windows systems as well.
+                       if ( function_exists( 'mcrypt_create_iv' ) ) {
+                               $rem = $bytes - strlen( $buffer );
+                               $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM );
+                               if ( $iv === false ) {
+                                       $this->logger->debug( "mcrypt_create_iv returned false.\n" );
+                               } else {
+                                       $buffer .= $iv;
+                                       $this->logger->debug( "mcrypt_create_iv generated " . strlen( $iv ) .
+                                               " bytes of randomness.\n" );
+                               }
+                       }
+               }
+
+               if ( strlen( $buffer ) < $bytes ) {
+                       if ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
+                               $rem = $bytes - strlen( $buffer );
+                               $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong );
+                               if ( $openssl_bytes === false ) {
+                                       $this->logger->debug( "openssl_random_pseudo_bytes returned false.\n" );
+                               } else {
+                                       $buffer .= $openssl_bytes;
+                                       $this->logger->debug( "openssl_random_pseudo_bytes generated " .
+                                               strlen( $openssl_bytes ) . " bytes of " .
+                                               ( $openssl_strong ? "strong" : "weak" ) . " randomness.\n" );
+                               }
+                               if ( strlen( $buffer ) >= $bytes ) {
+                                       // openssl tells us if the random source was strong, if some of our data was generated
+                                       // using it use it's say on whether the randomness is strong
+                                       $this->strong = !!$openssl_strong;
+                               }
+                       }
+               }
+
+               // Only read from urandom if we can control the buffer size or were passed forceStrong
+               if ( strlen( $buffer ) < $bytes &&
+                       ( function_exists( 'stream_set_read_buffer' ) || $forceStrong )
+               ) {
+                       $rem = $bytes - strlen( $buffer );
+                       if ( !function_exists( 'stream_set_read_buffer' ) && $forceStrong ) {
+                               $this->logger->debug( "Was forced to read from /dev/urandom " .
+                                       "without control over the buffer size.\n" );
+                       }
+                       // /dev/urandom is generally considered the best possible commonly
+                       // available random source, and is available on most *nix systems.
+                       MediaWiki\suppressWarnings();
+                       $urandom = fopen( "/dev/urandom", "rb" );
+                       MediaWiki\restoreWarnings();
+
+                       // Attempt to read all our random data from urandom
+                       // php's fread always does buffered reads based on the stream's chunk_size
+                       // so in reality it will usually read more than the amount of data we're
+                       // asked for and not storing that risks depleting the system's random pool.
+                       // If stream_set_read_buffer is available set the chunk_size to the amount
+                       // of data we need. Otherwise read 8k, php's default chunk_size.
+                       if ( $urandom ) {
+                               // php's default chunk_size is 8k
+                               $chunk_size = 1024 * 8;
+                               if ( function_exists( 'stream_set_read_buffer' ) ) {
+                                       // If possible set the chunk_size to the amount of data we need
+                                       stream_set_read_buffer( $urandom, $rem );
+                                       $chunk_size = $rem;
+                               }
+                               $random_bytes = fread( $urandom, max( $chunk_size, $rem ) );
+                               $buffer .= $random_bytes;
+                               fclose( $urandom );
+                               $this->logger->debug( "/dev/urandom generated " . strlen( $random_bytes ) .
+                                       " bytes of randomness.\n" );
+
+                               if ( strlen( $buffer ) >= $bytes ) {
+                                       // urandom is always strong, set to true if all our data was generated using it
+                                       $this->strong = true;
+                               }
+                       } else {
+                               $this->logger->debug( "/dev/urandom could not be opened.\n" );
+                       }
+               }
+
+               // If we cannot use or generate enough data from a secure source
+               // use this loop to generate a good set of pseudo random data.
+               // This works by initializing a random state using a pile of unstable data
+               // and continually shoving it through a hash along with a variable salt.
+               // We hash the random state with more salt to avoid the state from leaking
+               // out and being used to predict the /randomness/ that follows.
+               if ( strlen( $buffer ) < $bytes ) {
+                       $this->logger->debug( __METHOD__ .
+                               ": Falling back to using a pseudo random state to generate randomness.\n" );
+               }
+               while ( strlen( $buffer ) < $bytes ) {
+                       $buffer .= MWCryptHash::hmac( $this->randomState(), strval( mt_rand() ) );
+                       // This code is never really cryptographically strong, if we use it
+                       // at all, then set strong to false.
+                       $this->strong = false;
+               }
+
+               // Once the buffer has been filled up with enough random data to fulfill
+               // the request shift off enough data to handle the request and leave the
+               // unused portion left inside the buffer for the next request for random data
+               $generated = substr( $buffer, 0, $bytes );
+               $buffer = substr( $buffer, $bytes );
+
+               $this->logger->debug( strlen( $buffer ) .
+                       " bytes of randomness leftover in the buffer.\n" );
+
+               return $generated;
+       }
+
+       /**
+        * Generate a run of (ideally) cryptographically random data and return
+        * it in hexadecimal string format.
+        * You can use CryptRand::wasStrong() if you wish to know if the source used
+        * was cryptographically strong.
+        *
+        * @param int $chars The number of hex chars of random data to generate
+        * @param bool $forceStrong Pass true if you want generate to prefer cryptographically
+        *                          strong sources of entropy even if reading from them may steal
+        *                          more entropy from the system than optimal.
+        * @return string Hexadecimal random data
+        */
+       public function generateHex( $chars, $forceStrong = false ) {
+               // hex strings are 2x the length of raw binary so we divide the length in half
+               // odd numbers will result in a .5 that leads the generate() being 1 character
+               // short, so we use ceil() to ensure that we always have enough bytes
+               $bytes = ceil( $chars / 2 );
+               // Generate the data and then convert it to a hex string
+               $hex = bin2hex( $this->generate( $bytes, $forceStrong ) );
+
+               // A bit of paranoia here, the caller asked for a specific length of string
+               // here, and it's possible (eg when given an odd number) that we may actually
+               // have at least 1 char more than they asked for. Just in case they made this
+               // call intending to insert it into a database that does truncation we don't
+               // want to give them too much and end up with their database and their live
+               // code having two different values because part of what we gave them is truncated
+               // hence, we strip out any run of characters longer than what we were asked for.
+               return substr( $hex, 0, $chars );
+       }
+}
diff --git a/includes/libs/IP.php b/includes/libs/IP.php
new file mode 100644 (file)
index 0000000..21203a4
--- /dev/null
@@ -0,0 +1,744 @@
+<?php
+/**
+ * Functions and constants to play with IP addresses and ranges
+ *
+ * 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 Antoine Musso "<hashar at free dot fr>", Aaron Schulz
+ */
+
+use IPSet\IPSet;
+
+// Some regex definition to "play" with IP address and IP address blocks
+
+// An IPv4 address is made of 4 bytes from x00 to xFF which is d0 to d255
+define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])' );
+define( 'RE_IP_ADD', RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE );
+// An IPv4 block is an IP address and a prefix (d1 to d32)
+define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)' );
+define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX );
+
+// An IPv6 address is made up of 8 words (each x0000 to xFFFF).
+// However, the "::" abbreviation can be used on consecutive x0000 words.
+define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' );
+define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)' );
+define( 'RE_IPV6_ADD',
+       '(?:' . // starts with "::" (including "::")
+               ':(?::|(?::' . RE_IPV6_WORD . '){1,7})' .
+       '|' . // ends with "::" (except "::")
+               RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,6}::' .
+       '|' . // contains one "::" in the middle (the ^ makes the test fail if none found)
+               RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)' .
+       '|' . // contains no "::"
+               RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' .
+       ')'
+);
+// An IPv6 block is an IP address and a prefix (d1 to d128)
+define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX );
+// For IPv6 canonicalization (NOT for strict validation; these are quite lax!)
+define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' );
+define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' );
+
+// This might be useful for regexps used elsewhere, matches any IPv4 or IPv6 address or network
+define( 'IP_ADDRESS_STRING',
+       '(?:' .
+               RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?' . // IPv4
+       '|' .
+               RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?' . // IPv6
+       ')'
+);
+
+/**
+ * A collection of public static functions to play with IP address
+ * and IP blocks.
+ */
+class IP {
+       /** @var IPSet */
+       private static $proxyIpSet = null;
+
+       /**
+        * Determine if a string is as valid IP address or network (CIDR prefix).
+        * SIIT IPv4-translated addresses are rejected.
+        * @note canonicalize() tries to convert translated addresses to IPv4.
+        *
+        * @param string $ip Possible IP address
+        * @return bool
+        */
+       public static function isIPAddress( $ip ) {
+               return (bool)preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip );
+       }
+
+       /**
+        * Given a string, determine if it as valid IP in IPv6 only.
+        * @note Unlike isValid(), this looks for networks too.
+        *
+        * @param string $ip Possible IP address
+        * @return bool
+        */
+       public static function isIPv6( $ip ) {
+               return (bool)preg_match( '/^' . RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?$/', $ip );
+       }
+
+       /**
+        * Given a string, determine if it as valid IP in IPv4 only.
+        * @note Unlike isValid(), this looks for networks too.
+        *
+        * @param string $ip Possible IP address
+        * @return bool
+        */
+       public static function isIPv4( $ip ) {
+               return (bool)preg_match( '/^' . RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?$/', $ip );
+       }
+
+       /**
+        * Validate an IP address. Ranges are NOT considered valid.
+        * SIIT IPv4-translated addresses are rejected.
+        * @note canonicalize() tries to convert translated addresses to IPv4.
+        *
+        * @param string $ip
+        * @return bool True if it is valid
+        */
+       public static function isValid( $ip ) {
+               return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip )
+                       || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip ) );
+       }
+
+       /**
+        * Validate an IP Block (valid address WITH a valid prefix).
+        * SIIT IPv4-translated addresses are rejected.
+        * @note canonicalize() tries to convert translated addresses to IPv4.
+        *
+        * @param string $ipblock
+        * @return bool True if it is valid
+        */
+       public static function isValidBlock( $ipblock ) {
+               return ( preg_match( '/^' . RE_IPV6_BLOCK . '$/', $ipblock )
+                       || preg_match( '/^' . RE_IP_BLOCK . '$/', $ipblock ) );
+       }
+
+       /**
+        * Convert an IP into a verbose, uppercase, normalized form.
+        * Both IPv4 and IPv6 addresses are trimmed. Additionally,
+        * IPv6 addresses in octet notation are expanded to 8 words;
+        * IPv4 addresses have leading zeros, in each octet, removed.
+        *
+        * @param string $ip IP address in quad or octet form (CIDR or not).
+        * @return string
+        */
+       public static function sanitizeIP( $ip ) {
+               $ip = trim( $ip );
+               if ( $ip === '' ) {
+                       return null;
+               }
+               /* If not an IP, just return trimmed value, since sanitizeIP() is called
+                * in a number of contexts where usernames are supplied as input.
+                */
+               if ( !self::isIPAddress( $ip ) ) {
+                       return $ip;
+               }
+               if ( self::isIPv4( $ip ) ) {
+                       // Remove leading 0's from octet representation of IPv4 address
+                       $ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip );
+                       return $ip;
+               }
+               // Remove any whitespaces, convert to upper case
+               $ip = strtoupper( $ip );
+               // Expand zero abbreviations
+               $abbrevPos = strpos( $ip, '::' );
+               if ( $abbrevPos !== false ) {
+                       // We know this is valid IPv6. Find the last index of the
+                       // address before any CIDR number (e.g. "a:b:c::/24").
+                       $CIDRStart = strpos( $ip, "/" );
+                       $addressEnd = ( $CIDRStart !== false )
+                               ? $CIDRStart - 1
+                               : strlen( $ip ) - 1;
+                       // If the '::' is at the beginning...
+                       if ( $abbrevPos == 0 ) {
+                               $repeat = '0:';
+                               $extra = ( $ip == '::' ) ? '0' : ''; // for the address '::'
+                               $pad = 9; // 7+2 (due to '::')
+                       // If the '::' is at the end...
+                       } elseif ( $abbrevPos == ( $addressEnd - 1 ) ) {
+                               $repeat = ':0';
+                               $extra = '';
+                               $pad = 9; // 7+2 (due to '::')
+                       // If the '::' is in the middle...
+                       } else {
+                               $repeat = ':0';
+                               $extra = ':';
+                               $pad = 8; // 6+2 (due to '::')
+                       }
+                       $ip = str_replace( '::',
+                               str_repeat( $repeat, $pad - substr_count( $ip, ':' ) ) . $extra,
+                               $ip
+                       );
+               }
+               // Remove leading zeros from each bloc as needed
+               $ip = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip );
+
+               return $ip;
+       }
+
+       /**
+        * Prettify an IP for display to end users.
+        * This will make it more compact and lower-case.
+        *
+        * @param string $ip
+        * @return string
+        */
+       public static function prettifyIP( $ip ) {
+               $ip = self::sanitizeIP( $ip ); // normalize (removes '::')
+               if ( self::isIPv6( $ip ) ) {
+                       // Split IP into an address and a CIDR
+                       if ( strpos( $ip, '/' ) !== false ) {
+                               list( $ip, $cidr ) = explode( '/', $ip, 2 );
+                       } else {
+                               list( $ip, $cidr ) = [ $ip, '' ];
+                       }
+                       // Get the largest slice of words with multiple zeros
+                       $offset = 0;
+                       $longest = $longestPos = false;
+                       while ( preg_match(
+                               '!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset
+                       ) ) {
+                               list( $match, $pos ) = $m[0]; // full match
+                               if ( strlen( $match ) > strlen( $longest ) ) {
+                                       $longest = $match;
+                                       $longestPos = $pos;
+                               }
+                               $offset = ( $pos + strlen( $match ) ); // advance
+                       }
+                       if ( $longest !== false ) {
+                               // Replace this portion of the string with the '::' abbreviation
+                               $ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) );
+                       }
+                       // Add any CIDR back on
+                       if ( $cidr !== '' ) {
+                               $ip = "{$ip}/{$cidr}";
+                       }
+                       // Convert to lower case to make it more readable
+                       $ip = strtolower( $ip );
+               }
+
+               return $ip;
+       }
+
+       /**
+        * Given a host/port string, like one might find in the host part of a URL
+        * per RFC 2732, split the hostname part and the port part and return an
+        * array with an element for each. If there is no port part, the array will
+        * have false in place of the port. If the string was invalid in some way,
+        * false is returned.
+        *
+        * This was easy with IPv4 and was generally done in an ad-hoc way, but
+        * with IPv6 it's somewhat more complicated due to the need to parse the
+        * square brackets and colons.
+        *
+        * A bare IPv6 address is accepted despite the lack of square brackets.
+        *
+        * @param string $both The string with the host and port
+        * @return array|false Array normally, false on certain failures
+        */
+       public static function splitHostAndPort( $both ) {
+               if ( substr( $both, 0, 1 ) === '[' ) {
+                       if ( preg_match( '/^\[(' . RE_IPV6_ADD . ')\](?::(?P<port>\d+))?$/', $both, $m ) ) {
+                               if ( isset( $m['port'] ) ) {
+                                       return [ $m[1], intval( $m['port'] ) ];
+                               } else {
+                                       return [ $m[1], false ];
+                               }
+                       } else {
+                               // Square bracket found but no IPv6
+                               return false;
+                       }
+               }
+               $numColons = substr_count( $both, ':' );
+               if ( $numColons >= 2 ) {
+                       // Is it a bare IPv6 address?
+                       if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) {
+                               return [ $both, false ];
+                       } else {
+                               // Not valid IPv6, but too many colons for anything else
+                               return false;
+                       }
+               }
+               if ( $numColons >= 1 ) {
+                       // Host:port?
+                       $bits = explode( ':', $both );
+                       if ( preg_match( '/^\d+/', $bits[1] ) ) {
+                               return [ $bits[0], intval( $bits[1] ) ];
+                       } else {
+                               // Not a valid port
+                               return false;
+                       }
+               }
+
+               // Plain hostname
+               return [ $both, false ];
+       }
+
+       /**
+        * Given a host name and a port, combine them into host/port string like
+        * you might find in a URL. If the host contains a colon, wrap it in square
+        * brackets like in RFC 2732. If the port matches the default port, omit
+        * the port specification
+        *
+        * @param string $host
+        * @param int $port
+        * @param bool|int $defaultPort
+        * @return string
+        */
+       public static function combineHostAndPort( $host, $port, $defaultPort = false ) {
+               if ( strpos( $host, ':' ) !== false ) {
+                       $host = "[$host]";
+               }
+               if ( $defaultPort !== false && $port == $defaultPort ) {
+                       return $host;
+               } else {
+                       return "$host:$port";
+               }
+       }
+
+       /**
+        * Convert an IPv4 or IPv6 hexadecimal representation back to readable format
+        *
+        * @param string $hex Number, with "v6-" prefix if it is IPv6
+        * @return string Quad-dotted (IPv4) or octet notation (IPv6)
+        */
+       public static function formatHex( $hex ) {
+               if ( substr( $hex, 0, 3 ) == 'v6-' ) { // IPv6
+                       return self::hexToOctet( substr( $hex, 3 ) );
+               } else { // IPv4
+                       return self::hexToQuad( $hex );
+               }
+       }
+
+       /**
+        * Converts a hexadecimal number to an IPv6 address in octet notation
+        *
+        * @param string $ip_hex Pure hex (no v6- prefix)
+        * @return string (of format a:b:c:d:e:f:g:h)
+        */
+       public static function hexToOctet( $ip_hex ) {
+               // Pad hex to 32 chars (128 bits)
+               $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT );
+               // Separate into 8 words
+               $ip_oct = substr( $ip_hex, 0, 4 );
+               for ( $n = 1; $n < 8; $n++ ) {
+                       $ip_oct .= ':' . substr( $ip_hex, 4 * $n, 4 );
+               }
+               // NO leading zeroes
+               $ip_oct = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip_oct );
+
+               return $ip_oct;
+       }
+
+       /**
+        * Converts a hexadecimal number to an IPv4 address in quad-dotted notation
+        *
+        * @param string $ip_hex Pure hex
+        * @return string (of format a.b.c.d)
+        */
+       public static function hexToQuad( $ip_hex ) {
+               // Pad hex to 8 chars (32 bits)
+               $ip_hex = str_pad( strtoupper( $ip_hex ), 8, '0', STR_PAD_LEFT );
+               // Separate into four quads
+               $s = '';
+               for ( $i = 0; $i < 4; $i++ ) {
+                       if ( $s !== '' ) {
+                               $s .= '.';
+                       }
+                       $s .= base_convert( substr( $ip_hex, $i * 2, 2 ), 16, 10 );
+               }
+
+               return $s;
+       }
+
+       /**
+        * Determine if an IP address really is an IP address, and if it is public,
+        * i.e. not RFC 1918 or similar
+        *
+        * @param string $ip
+        * @return bool
+        */
+       public static function isPublic( $ip ) {
+               static $privateSet = null;
+               if ( !$privateSet ) {
+                       $privateSet = new IPSet( [
+                               '10.0.0.0/8', # RFC 1918 (private)
+                               '172.16.0.0/12', # RFC 1918 (private)
+                               '192.168.0.0/16', # RFC 1918 (private)
+                               '0.0.0.0/8', # this network
+                               '127.0.0.0/8', # loopback
+                               'fc00::/7', # RFC 4193 (local)
+                               '0:0:0:0:0:0:0:1', # loopback
+                               '169.254.0.0/16', # link-local
+                               'fe80::/10', # link-local
+                       ] );
+               }
+               return !$privateSet->match( $ip );
+       }
+
+       /**
+        * Return a zero-padded upper case hexadecimal representation of an IP address.
+        *
+        * Hexadecimal addresses are used because they can easily be extended to
+        * IPv6 support. To separate the ranges, the return value from this
+        * function for an IPv6 address will be prefixed with "v6-", a non-
+        * hexadecimal string which sorts after the IPv4 addresses.
+        *
+        * @param string $ip Quad dotted/octet IP address.
+        * @return string|bool False on failure
+        */
+       public static function toHex( $ip ) {
+               if ( self::isIPv6( $ip ) ) {
+                       $n = 'v6-' . self::IPv6ToRawHex( $ip );
+               } elseif ( self::isIPv4( $ip ) ) {
+                       // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08),
+                       // also double/triple 0 needs to be changed to just a single 0 for ip2long.
+                       $ip = self::sanitizeIP( $ip );
+                       $n = ip2long( $ip );
+                       if ( $n < 0 ) {
+                               $n += pow( 2, 32 );
+                               # On 32-bit platforms (and on Windows), 2^32 does not fit into an int,
+                               # so $n becomes a float. We convert it to string instead.
+                               if ( is_float( $n ) ) {
+                                       $n = (string)$n;
+                               }
+                       }
+                       if ( $n !== false ) {
+                               # Floating points can handle the conversion; faster than Wikimedia\base_convert()
+                               $n = strtoupper( str_pad( base_convert( $n, 10, 16 ), 8, '0', STR_PAD_LEFT ) );
+                       }
+               } else {
+                       $n = false;
+               }
+
+               return $n;
+       }
+
+       /**
+        * Given an IPv6 address in octet notation, returns a pure hex string.
+        *
+        * @param string $ip Octet ipv6 IP address.
+        * @return string|bool Pure hex (uppercase); false on failure
+        */
+       private static function IPv6ToRawHex( $ip ) {
+               $ip = self::sanitizeIP( $ip );
+               if ( !$ip ) {
+                       return false;
+               }
+               $r_ip = '';
+               foreach ( explode( ':', $ip ) as $v ) {
+                       $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT );
+               }
+
+               return $r_ip;
+       }
+
+       /**
+        * Convert a network specification in CIDR notation
+        * to an integer network and a number of bits
+        *
+        * @param string $range IP with CIDR prefix
+        * @return array(int or string, int)
+        */
+       public static function parseCIDR( $range ) {
+               if ( self::isIPv6( $range ) ) {
+                       return self::parseCIDR6( $range );
+               }
+               $parts = explode( '/', $range, 2 );
+               if ( count( $parts ) != 2 ) {
+                       return [ false, false ];
+               }
+               list( $network, $bits ) = $parts;
+               $network = ip2long( $network );
+               if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 32 ) {
+                       if ( $bits == 0 ) {
+                               $network = 0;
+                       } else {
+                               $network &= ~( ( 1 << ( 32 - $bits ) ) - 1 );
+                       }
+                       # Convert to unsigned
+                       if ( $network < 0 ) {
+                               $network += pow( 2, 32 );
+                       }
+               } else {
+                       $network = false;
+                       $bits = false;
+               }
+
+               return [ $network, $bits ];
+       }
+
+       /**
+        * Given a string range in a number of formats,
+        * return the start and end of the range in hexadecimal.
+        *
+        * Formats are:
+        *     1.2.3.4/24          CIDR
+        *     1.2.3.4 - 1.2.3.5   Explicit range
+        *     1.2.3.4             Single IP
+        *
+        *     2001:0db8:85a3::7344/96                       CIDR
+        *     2001:0db8:85a3::7344 - 2001:0db8:85a3::7344   Explicit range
+        *     2001:0db8:85a3::7344                          Single IP
+        * @param string $range IP range
+        * @return array(string, string)
+        */
+       public static function parseRange( $range ) {
+               // CIDR notation
+               if ( strpos( $range, '/' ) !== false ) {
+                       if ( self::isIPv6( $range ) ) {
+                               return self::parseRange6( $range );
+                       }
+                       list( $network, $bits ) = self::parseCIDR( $range );
+                       if ( $network === false ) {
+                               $start = $end = false;
+                       } else {
+                               $start = sprintf( '%08X', $network );
+                               $end = sprintf( '%08X', $network + pow( 2, ( 32 - $bits ) ) - 1 );
+                       }
+               // Explicit range
+               } elseif ( strpos( $range, '-' ) !== false ) {
+                       list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
+                       if ( self::isIPv6( $start ) && self::isIPv6( $end ) ) {
+                               return self::parseRange6( $range );
+                       }
+                       if ( self::isIPv4( $start ) && self::isIPv4( $end ) ) {
+                               $start = self::toHex( $start );
+                               $end = self::toHex( $end );
+                               if ( $start > $end ) {
+                                       $start = $end = false;
+                               }
+                       } else {
+                               $start = $end = false;
+                       }
+               } else {
+                       # Single IP
+                       $start = $end = self::toHex( $range );
+               }
+               if ( $start === false || $end === false ) {
+                       return [ false, false ];
+               } else {
+                       return [ $start, $end ];
+               }
+       }
+
+       /**
+        * Convert a network specification in IPv6 CIDR notation to an
+        * integer network and a number of bits
+        *
+        * @param string $range
+        *
+        * @return array(string, int)
+        */
+       private static function parseCIDR6( $range ) {
+               # Explode into <expanded IP,range>
+               $parts = explode( '/', IP::sanitizeIP( $range ), 2 );
+               if ( count( $parts ) != 2 ) {
+                       return [ false, false ];
+               }
+               list( $network, $bits ) = $parts;
+               $network = self::IPv6ToRawHex( $network );
+               if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 128 ) {
+                       if ( $bits == 0 ) {
+                               $network = "0";
+                       } else {
+                               # Native 32 bit functions WONT work here!!!
+                               # Convert to a padded binary number
+                               $network = Wikimedia\base_convert( $network, 16, 2, 128 );
+                               # Truncate the last (128-$bits) bits and replace them with zeros
+                               $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT );
+                               # Convert back to an integer
+                               $network = Wikimedia\base_convert( $network, 2, 10 );
+                       }
+               } else {
+                       $network = false;
+                       $bits = false;
+               }
+
+               return [ $network, (int)$bits ];
+       }
+
+       /**
+        * Given a string range in a number of formats, return the
+        * start and end of the range in hexadecimal. For IPv6.
+        *
+        * Formats are:
+        *     2001:0db8:85a3::7344/96                       CIDR
+        *     2001:0db8:85a3::7344 - 2001:0db8:85a3::7344   Explicit range
+        *     2001:0db8:85a3::7344/96                       Single IP
+        *
+        * @param string $range
+        *
+        * @return array(string, string)
+        */
+       private static function parseRange6( $range ) {
+               # Expand any IPv6 IP
+               $range = IP::sanitizeIP( $range );
+               // CIDR notation...
+               if ( strpos( $range, '/' ) !== false ) {
+                       list( $network, $bits ) = self::parseCIDR6( $range );
+                       if ( $network === false ) {
+                               $start = $end = false;
+                       } else {
+                               $start = Wikimedia\base_convert( $network, 10, 16, 32, false );
+                               # Turn network to binary (again)
+                               $end = Wikimedia\base_convert( $network, 10, 2, 128 );
+                               # Truncate the last (128-$bits) bits and replace them with ones
+                               $end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT );
+                               # Convert to hex
+                               $end = Wikimedia\base_convert( $end, 2, 16, 32, false );
+                               # see toHex() comment
+                               $start = "v6-$start";
+                               $end = "v6-$end";
+                       }
+               // Explicit range notation...
+               } elseif ( strpos( $range, '-' ) !== false ) {
+                       list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
+                       $start = self::toHex( $start );
+                       $end = self::toHex( $end );
+                       if ( $start > $end ) {
+                               $start = $end = false;
+                       }
+               } else {
+                       # Single IP
+                       $start = $end = self::toHex( $range );
+               }
+               if ( $start === false || $end === false ) {
+                       return [ false, false ];
+               } else {
+                       return [ $start, $end ];
+               }
+       }
+
+       /**
+        * Determine if a given IPv4/IPv6 address is in a given CIDR network
+        *
+        * @param string $addr The address to check against the given range.
+        * @param string $range The range to check the given address against.
+        * @return bool Whether or not the given address is in the given range.
+        *
+        * @note This can return unexpected results for invalid arguments!
+        *       Make sure you pass a valid IP address and IP range.
+        */
+       public static function isInRange( $addr, $range ) {
+               $hexIP = self::toHex( $addr );
+               list( $start, $end ) = self::parseRange( $range );
+
+               return ( strcmp( $hexIP, $start ) >= 0 &&
+                       strcmp( $hexIP, $end ) <= 0 );
+       }
+
+       /**
+        * Determines if an IP address is a list of CIDR a.b.c.d/n ranges.
+        *
+        * @since 1.25
+        *
+        * @param string $ip the IP to check
+        * @param array $ranges the IP ranges, each element a range
+        *
+        * @return bool true if the specified adress belongs to the specified range; otherwise, false.
+        */
+       public static function isInRanges( $ip, $ranges ) {
+               foreach ( $ranges as $range ) {
+                       if ( self::isInRange( $ip, $range ) ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Convert some unusual representations of IPv4 addresses to their
+        * canonical dotted quad representation.
+        *
+        * This currently only checks a few IPV4-to-IPv6 related cases.  More
+        * unusual representations may be added later.
+        *
+        * @param string $addr Something that might be an IP address
+        * @return string|null Valid dotted quad IPv4 address or null
+        */
+       public static function canonicalize( $addr ) {
+               // remove zone info (bug 35738)
+               $addr = preg_replace( '/\%.*/', '', $addr );
+
+               if ( self::isValid( $addr ) ) {
+                       return $addr;
+               }
+               // Turn mapped addresses from ::ce:ffff:1.2.3.4 to 1.2.3.4
+               if ( strpos( $addr, ':' ) !== false && strpos( $addr, '.' ) !== false ) {
+                       $addr = substr( $addr, strrpos( $addr, ':' ) + 1 );
+                       if ( self::isIPv4( $addr ) ) {
+                               return $addr;
+                       }
+               }
+               // IPv6 loopback address
+               $m = [];
+               if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) {
+                       return '127.0.0.1';
+               }
+               // IPv4-mapped and IPv4-compatible IPv6 addresses
+               if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) {
+                       return $m[1];
+               }
+               if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD .
+                       ':' . RE_IPV6_WORD . '$/i', $addr, $m )
+               ) {
+                       return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
+               }
+
+               return null; // give up
+       }
+
+       /**
+        * Gets rid of unneeded numbers in quad-dotted/octet IP strings
+        * For example, 127.111.113.151/24 -> 127.111.113.0/24
+        * @param string $range IP address to normalize
+        * @return string
+        */
+       public static function sanitizeRange( $range ) {
+               list( /*...*/, $bits ) = self::parseCIDR( $range );
+               list( $start, /*...*/ ) = self::parseRange( $range );
+               $start = self::formatHex( $start );
+               if ( $bits === false ) {
+                       return $start; // wasn't actually a range
+               }
+
+               return "$start/$bits";
+       }
+
+       /**
+        * Returns the subnet of a given IP
+        *
+        * @param string $ip
+        * @return string|false
+        */
+       public static function getSubnet( $ip ) {
+               $matches = [];
+               $subnet = false;
+               if ( IP::isIPv6( $ip ) ) {
+                       $parts = IP::parseRange( "$ip/64" );
+                       $subnet = $parts[0];
+               } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
+                       // IPv4
+                       $subnet = $matches[1];
+               }
+               return $subnet;
+       }
+}
diff --git a/includes/libs/MWCryptHash.php b/includes/libs/MWCryptHash.php
new file mode 100644 (file)
index 0000000..f9b7172
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+/**
+ * Utility functions for generating hashes
+ *
+ * This is based in part on Drupal code as well as what we used in our own code
+ * prior to introduction of this class, by way of MWCryptRand.
+ *
+ * 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
+ */
+
+class MWCryptHash {
+       /**
+        * The hash algorithm being used
+        */
+       protected static $algo = null;
+
+       /**
+        * The number of bytes outputted by the hash algorithm
+        */
+       protected static $hashLength = [
+               true => null,
+               false => null,
+       ];
+
+       /**
+        * Decide on the best acceptable hash algorithm we have available for hash()
+        * @return string A hash algorithm
+        */
+       public static function hashAlgo() {
+               if ( !is_null( self::$algo ) ) {
+                       return self::$algo;
+               }
+
+               $algos = hash_algos();
+               $preference = [ 'whirlpool', 'sha256', 'sha1', 'md5' ];
+
+               foreach ( $preference as $algorithm ) {
+                       if ( in_array( $algorithm, $algos ) ) {
+                               self::$algo = $algorithm;
+
+                               return self::$algo;
+                       }
+               }
+
+               // We only reach here if no acceptable hash is found in the list, this should
+               // be a technical impossibility since most of php's hash list is fixed and
+               // some of the ones we list are available as their own native functions
+               // But since we already require at least 5.2 and hash() was default in
+               // 5.1.2 we don't bother falling back to methods like sha1 and md5.
+               throw new DomainException( "Could not find an acceptable hashing function in hash_algos()" );
+       }
+
+       /**
+        * Return the byte-length output of the hash algorithm we are
+        * using in self::hash and self::hmac.
+        *
+        * @param bool $raw True to return the length for binary data, false to
+        *   return for hex-encoded
+        * @return int Number of bytes the hash outputs
+        */
+       public static function hashLength( $raw = true ) {
+               $raw = (bool)$raw;
+               if ( is_null( self::$hashLength[$raw] ) ) {
+                       self::$hashLength[$raw] = strlen( self::hash( '', $raw ) );
+               }
+
+               return self::$hashLength[$raw];
+       }
+
+       /**
+        * Generate an acceptably unstable one-way-hash of some text
+        * making use of the best hash algorithm that we have available.
+        *
+        * @param string $data
+        * @param bool $raw True to return binary data, false to return it hex-encoded
+        * @return string A hash of the data
+        */
+       public static function hash( $data, $raw = true ) {
+               return hash( self::hashAlgo(), $data, $raw );
+       }
+
+       /**
+        * Generate an acceptably unstable one-way-hmac of some text
+        * making use of the best hash algorithm that we have available.
+        *
+        * @param string $data
+        * @param string $key
+        * @param bool $raw True to return binary data, false to return it hex-encoded
+        * @return string An hmac hash of the data + key
+        */
+       public static function hmac( $data, $key, $raw = true ) {
+               if ( !is_string( $key ) ) {
+                       // a fatal error in HHVM; an exception will at least give us a stack trace
+                       throw new InvalidArgumentException( 'Invalid key type: ' . gettype( $key ) );
+               }
+               return hash_hmac( self::hashAlgo(), $data, $key, $raw );
+       }
+
+}
index 50e9732..12a5cad 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * APC-backed function memoization
+ * APC-backed and APCu-backed function memoization
  *
  * This class provides memoization for pure functions. A function is pure
  * if its result value depends on nothing other than its input parameters
@@ -8,7 +8,7 @@
  *
  * The first invocation of the memoized callable with a particular set of
  * arguments will be delegated to the underlying callable. Repeat invocations
- * with the same input parameters will be served from APC.
+ * with the same input parameters will be served from APC or APCu.
  *
  * @par Example:
  * @code
@@ -70,7 +70,7 @@ class MemoizedCallable {
        }
 
        /**
-        * Fetch the result of a previous invocation from APC.
+        * Fetch the result of a previous invocation from APC or APCu.
         *
         * @param string $key
         * @param bool &$success
@@ -79,12 +79,14 @@ class MemoizedCallable {
                $success = false;
                if ( function_exists( 'apc_fetch' ) ) {
                        return apc_fetch( $key, $success );
+               } elseif ( function_exists( 'apcu_fetch' ) ) {
+                       return apcu_fetch( $key, $success );
                }
                return false;
        }
 
        /**
-        * Store the result of an invocation in APC.
+        * Store the result of an invocation in APC or APCu.
         *
         * @param string $key
         * @param mixed $result
@@ -92,6 +94,8 @@ class MemoizedCallable {
        protected function storeResult( $key, $result ) {
                if ( function_exists( 'apc_store' ) ) {
                        apc_store( $key, $result, $this->ttl );
+               } elseif ( function_exists( 'apcu_store' ) ) {
+                       apcu_store( $key, $result, $this->ttl );
                }
        }
 
index 320a0b6..a870204 100644 (file)
@@ -184,14 +184,12 @@ class MultiHttpClient {
                unset( $req ); // don't assign over this by accident
 
                $indexes = array_keys( $reqs );
-               if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
-                       if ( isset( $opts['usePipelining'] ) ) {
-                               curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] );
-                       }
-                       if ( isset( $opts['maxConnsPerHost'] ) ) {
-                               // Keep these sockets around as they may be needed later in the request
-                               curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] );
-                       }
+               if ( isset( $opts['usePipelining'] ) ) {
+                       curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] );
+               }
+               if ( isset( $opts['maxConnsPerHost'] ) ) {
+                       // Keep these sockets around as they may be needed later in the request
+                       curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] );
                }
 
                // @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS)
@@ -258,10 +256,8 @@ class MultiHttpClient {
                unset( $req ); // don't assign over this by accident
 
                // Restore the default settings
-               if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
-                       curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining );
-                       curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
-               }
+               curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining );
+               curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
 
                return $reqs;
        }
@@ -292,12 +288,7 @@ class MultiHttpClient {
                curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
 
                $url = $req['url'];
-               // PHP_QUERY_RFC3986 is PHP 5.4+ only
-               $query = str_replace(
-                       [ '+', '%7E' ],
-                       [ '%20', '~' ],
-                       http_build_query( $req['query'], '', '&' )
-               );
+               $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
                if ( $query != '' ) {
                        $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
                }
@@ -351,7 +342,7 @@ class MultiHttpClient {
                                // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
                                // is an array, but not if it's a string. So convert $req['body'] to a string
                                // for safety.
-                               $req['body'] = wfArrayToCgi( $req['body'] );
+                               $req['body'] = http_build_query( $req['body'] );
                        }
                        curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
                } else {
@@ -422,10 +413,8 @@ class MultiHttpClient {
        protected function getCurlMulti() {
                if ( !$this->multiHandle ) {
                        $cmh = curl_multi_init();
-                       if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
-                               curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining );
-                               curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
-                       }
+                       curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining );
+                       curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
                        $this->multiHandle = $cmh;
                }
                return $this->multiHandle;
diff --git a/includes/libs/SamplingStatsdClient.php b/includes/libs/SamplingStatsdClient.php
deleted file mode 100644 (file)
index dd1976c..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-<?php
-/**
- * Copyright 2015
- *
- * 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
- */
-
-use Liuggio\StatsdClient\StatsdClient;
-use Liuggio\StatsdClient\Entity\StatsdData;
-use Liuggio\StatsdClient\Entity\StatsdDataInterface;
-
-/**
- * A statsd client that applies the sampling rate to the data items before sending them.
- *
- * @since 1.26
- */
-class SamplingStatsdClient extends StatsdClient {
-       protected $samplingRates = [];
-
-       /**
-        * Sampling rates as an associative array of patterns and rates.
-        * Patterns are Unix shell patterns (e.g. 'MediaWiki.api.*').
-        * Rates are sampling probabilities (e.g. 0.1 means 1 in 10 events are sampled).
-        * @param array $samplingRates
-        * @since 1.28
-        */
-       public function setSamplingRates( array $samplingRates ) {
-               $this->samplingRates = $samplingRates;
-       }
-
-       /**
-        * Sets sampling rate for all items in $data.
-        * The sample rate specified in a StatsdData entity overrides the sample rate specified here.
-        *
-        * {@inheritDoc}
-        */
-       public function appendSampleRate( $data, $sampleRate = 1 ) {
-               $samplingRates = $this->samplingRates;
-               if ( !$samplingRates && $sampleRate !== 1 ) {
-                       $samplingRates = [ '*' => $sampleRate ];
-               }
-               if ( $samplingRates ) {
-                       array_walk( $data, function( $item ) use ( $samplingRates ) {
-                               /** @var $item StatsdData */
-                               foreach ( $samplingRates as $pattern => $rate ) {
-                                       if ( fnmatch( $pattern, $item->getKey(), FNM_NOESCAPE ) ) {
-                                               $item->setSampleRate( $item->getSampleRate() * $rate );
-                                               break;
-                                       }
-                               }
-                       } );
-               }
-
-               return $data;
-       }
-
-       /*
-        * Send the metrics over UDP
-        * Sample the metrics according to their sample rate and send the remaining ones.
-        *
-        * @param StatsdDataInterface|StatsdDataInterface[] $data message(s) to sent
-        *        strings are not allowed here as sampleData requires a StatsdDataInterface
-        * @param int $sampleRate
-        *
-        * @return integer the data sent in bytes
-        */
-       public function send( $data, $sampleRate = 1 ) {
-               if ( !is_array( $data ) ) {
-                       $data = [ $data ];
-               }
-               if ( !$data ) {
-                       return;
-               }
-               foreach ( $data as $item ) {
-                       if ( !( $item instanceof StatsdDataInterface ) ) {
-                               throw new InvalidArgumentException(
-                                       'SamplingStatsdClient does not accept stringified messages' );
-                       }
-               }
-
-               // add sampling
-               $data = $this->appendSampleRate( $data, $sampleRate );
-               $data = $this->sampleData( $data );
-
-               $data = array_map( 'strval', $data );
-
-               // reduce number of packets
-               if ( $this->getReducePacket() ) {
-                       $data = $this->reduceCount( $data );
-               }
-
-               // failures in any of this should be silently ignored if ..
-               $written = 0;
-               try {
-                       $fp = $this->getSender()->open();
-                       if ( !$fp ) {
-                               return;
-                       }
-                       foreach ( $data as $message ) {
-                               $written += $this->getSender()->write( $fp, $message );
-                       }
-                       $this->getSender()->close( $fp );
-               } catch ( Exception $e ) {
-                       $this->throwException( $e );
-               }
-
-               return $written;
-       }
-
-       /**
-        * Throw away some of the data according to the sample rate.
-        * @param StatsdDataInterface[] $data
-        * @return StatsdDataInterface[]
-        * @throws LogicException
-        */
-       protected function sampleData( $data ) {
-               $newData = [];
-               $mt_rand_max = mt_getrandmax();
-               foreach ( $data as $item ) {
-                       $samplingRate = $item->getSampleRate();
-                       if ( $samplingRate <= 0.0 || $samplingRate > 1.0 ) {
-                               throw new LogicException( 'Sampling rate shall be within ]0, 1]' );
-                       }
-                       if (
-                               $samplingRate === 1 ||
-                               ( mt_rand() / $mt_rand_max <= $samplingRate )
-                       ) {
-                               $newData[] = $item;
-                       }
-               }
-               return $newData;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       protected function throwException( Exception $exception ) {
-               if ( !$this->getFailSilently() ) {
-                       throw $exception;
-               }
-       }
-}
diff --git a/includes/libs/ScopedCallback.php b/includes/libs/ScopedCallback.php
deleted file mode 100644 (file)
index 96075aa..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-/**
- * This file deals with RAII style scoped callbacks.
- *
- * 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
- */
-
-/**
- * Class for asserting that a callback happens when an dummy object leaves scope
- *
- * @since 1.21
- */
-class ScopedCallback {
-       /** @var callable */
-       protected $callback;
-       /** @var array */
-       protected $params;
-
-       /**
-        * @param callable|null $callback
-        * @param array $params Callback arguments (since 1.25)
-        * @throws Exception
-        */
-       public function __construct( $callback, array $params = [] ) {
-               if ( $callback !== null && !is_callable( $callback ) ) {
-                       throw new InvalidArgumentException( "Provided callback is not valid." );
-               }
-               $this->callback = $callback;
-               $this->params = $params;
-       }
-
-       /**
-        * Trigger a scoped callback and destroy it.
-        * This is the same is just setting it to null.
-        *
-        * @param ScopedCallback $sc
-        */
-       public static function consume( ScopedCallback &$sc = null ) {
-               $sc = null;
-       }
-
-       /**
-        * Destroy a scoped callback without triggering it
-        *
-        * @param ScopedCallback $sc
-        */
-       public static function cancel( ScopedCallback &$sc = null ) {
-               if ( $sc ) {
-                       $sc->callback = null;
-               }
-               $sc = null;
-       }
-
-       /**
-        * Trigger the callback when this leaves scope
-        */
-       function __destruct() {
-               if ( $this->callback !== null ) {
-                       call_user_func_array( $this->callback, $this->params );
-               }
-       }
-}
index 1d23f9d..bff9abd 100644 (file)
@@ -58,7 +58,7 @@ class StatusValue {
         * Factory function for fatal errors
         *
         * @param string|MessageSpecifier $message Message key or object
-        * @return StatusValue
+        * @return static
         */
        public static function newFatal( $message /*, parameters...*/ ) {
                $params = func_get_args();
@@ -71,7 +71,7 @@ class StatusValue {
         * Factory function for good results
         *
         * @param mixed $value
-        * @return StatusValue
+        * @return static
         */
        public static function newGood( $value = null ) {
                $result = new static();
@@ -79,6 +79,34 @@ class StatusValue {
                return $result;
        }
 
+       /**
+        * Splits this StatusValue object into two new StatusValue objects, one which contains only
+        * the error messages, and one that contains the warnings, only. The returned array is
+        * defined as:
+        * [
+        *     0 => object(StatusValue) # the StatusValue with error messages, only
+        *         1 => object(StatusValue) # The StatusValue with warning messages, only
+        * ]
+        *
+        * @return array
+        */
+       public function splitByErrorType() {
+               $errorsOnlyStatusValue = clone $this;
+               $warningsOnlyStatusValue = clone $this;
+               $warningsOnlyStatusValue->ok = true;
+
+               $errorsOnlyStatusValue->errors = $warningsOnlyStatusValue->errors = [];
+               foreach ( $this->errors as $item ) {
+                       if ( $item['type'] === 'warning' ) {
+                               $warningsOnlyStatusValue->errors[] = $item;
+                       } else {
+                               $errorsOnlyStatusValue->errors[] = $item;
+                       }
+               };
+
+               return [ $errorsOnlyStatusValue, $warningsOnlyStatusValue ];
+       }
+
        /**
         * Returns whether the operation completed and didn't have any error or
         * warnings
@@ -246,8 +274,8 @@ class StatusValue {
         * Note, due to the lack of tools for comparing IStatusMessage objects, this
         * function will not work when using such an object as the search parameter.
         *
-        * @param IStatusMessage|string $source Message key or object to search for
-        * @param IStatusMessage|string $dest Replacement message key or object
+        * @param MessageSpecifier|string $source Message key or object to search for
+        * @param MessageSpecifier|string $dest Replacement message key or object
         * @return bool Return true if the replacement was done, false otherwise.
         */
        public function replaceMessage( $source, $dest ) {
diff --git a/includes/libs/WaitConditionLoop.php b/includes/libs/WaitConditionLoop.php
deleted file mode 100644 (file)
index 969e86e..0000000
+++ /dev/null
@@ -1,186 +0,0 @@
-<?php
-/**
- * Wait loop that reaches a condition or times out.
- *
- * 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
- */
-
-/**
- * Wait loop that reaches a condition or times out
- * @since 1.28
- */
-class WaitConditionLoop {
-       /** @var callable */
-       private $condition;
-       /** @var callable[] */
-       private $busyCallbacks = [];
-       /** @var float Seconds */
-       private $timeout;
-       /** @var float Seconds */
-       private $lastWaitTime;
-       /** @var integer|null */
-       private $rusageMode;
-
-       const CONDITION_REACHED = 1;
-       const CONDITION_CONTINUE = 0; // evaluates as falsey
-       const CONDITION_FAILED = -1;
-       const CONDITION_TIMED_OUT = -2;
-       const CONDITION_ABORTED = -3;
-
-       /**
-        * @param callable $condition Callback that returns a WaitConditionLoop::CONDITION_ constant
-        * @param float $timeout Timeout in seconds
-        * @param array &$busyCallbacks List of callbacks to do useful work (by reference)
-        */
-       public function __construct( callable $condition, $timeout = 5.0, &$busyCallbacks = [] ) {
-               $this->condition = $condition;
-               $this->timeout = $timeout;
-               $this->busyCallbacks =& $busyCallbacks;
-
-               if ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
-                       $this->rusageMode = 2; // RUSAGE_THREAD
-               } elseif ( function_exists( 'getrusage' ) ) {
-                       $this->rusageMode = 0; // RUSAGE_SELF
-               }
-       }
-
-       /**
-        * Invoke the loop and continue until either:
-        *   - a) The condition callback returns neither CONDITION_CONTINUE nor false
-        *   - b) The timeout is reached
-        * This a condition callback can return true (stop) or false (continue) for convenience.
-        * In such cases, the halting result of "true" will be converted to CONDITION_REACHED.
-        *
-        * If $timeout is 0, then only the condition callback will be called (no busy callbacks),
-        * and this will immediately return CONDITION_FAILED if the condition was not met.
-        *
-        * Exceptions in callbacks will be caught and the callback will be swapped with
-        * one that simply rethrows that exception back to the caller when invoked.
-        *
-        * @return integer WaitConditionLoop::CONDITION_* constant
-        * @throws Exception Any error from the condition callback
-        */
-       public function invoke() {
-               $elapsed = 0.0; // seconds
-               $sleepUs = 0; // microseconds to sleep each time
-               $lastCheck = false;
-               $finalResult = self::CONDITION_TIMED_OUT;
-               do {
-                       $checkStartTime = $this->getWallTime();
-                       // Check if the condition is met yet
-                       $realStart = $this->getWallTime();
-                       $cpuStart = $this->getCpuTime();
-                       $checkResult = call_user_func( $this->condition );
-                       $cpu = $this->getCpuTime() - $cpuStart;
-                       $real = $this->getWallTime() - $realStart;
-                       // Exit if the condition is reached, and error occurs, or this is non-blocking
-                       if ( $this->timeout <= 0 ) {
-                               $finalResult = $checkResult ? self::CONDITION_REACHED : self::CONDITION_FAILED;
-                               break;
-                       } elseif ( (int)$checkResult !== self::CONDITION_CONTINUE ) {
-                               if ( is_int( $checkResult ) ) {
-                                       $finalResult = $checkResult;
-                               } else {
-                                       $finalResult = self::CONDITION_REACHED;
-                               }
-                               break;
-                       } elseif ( $lastCheck ) {
-                               break; // timeout reached
-                       }
-                       // Detect if condition callback seems to block or if justs burns CPU
-                       $conditionUsesInterrupts = ( $real > 0.100 && $cpu <= $real * .03 );
-                       if ( !$this->popAndRunBusyCallback() && !$conditionUsesInterrupts ) {
-                               // 10 queries = 10(10+100)/2 ms = 550ms, 14 queries = 1050ms
-                               $sleepUs = min( $sleepUs + 10 * 1e3, 1e6 ); // stop incrementing at ~1s
-                               $this->usleep( $sleepUs );
-                       }
-                       $checkEndTime = $this->getWallTime();
-                       // The max() protects against the clock getting set back
-                       $elapsed += max( $checkEndTime - $checkStartTime, 0.010 );
-                       // Do not let slow callbacks timeout without checking the condition one more time
-                       $lastCheck = ( $elapsed >= $this->timeout );
-               } while ( true );
-
-               $this->lastWaitTime = $elapsed;
-
-               return $finalResult;
-       }
-
-       /**
-        * @return float Seconds
-        */
-       public function getLastWaitTime() {
-               return $this->lastWaitTime;
-       }
-
-       /**
-        * @param integer $microseconds
-        */
-       protected function usleep( $microseconds ) {
-               usleep( $microseconds );
-       }
-
-       /**
-        * @return float
-        */
-       protected function getWallTime() {
-               return microtime( true );
-       }
-
-       /**
-        * @return float Returns 0.0 if not supported (Windows on PHP < 7)
-        */
-       protected function getCpuTime() {
-               if ( $this->rusageMode === null ) {
-                       return microtime( true ); // assume worst case (all time is CPU)
-               }
-
-               $ru = getrusage( $this->rusageMode );
-               $time = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
-               $time += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
-
-               return $time;
-       }
-
-       /**
-        * Run one of the callbacks that does work ahead of time for another caller
-        *
-        * @return bool Whether a callback was executed
-        */
-       private function popAndRunBusyCallback() {
-               if ( $this->busyCallbacks ) {
-                       reset( $this->busyCallbacks );
-                       $key = key( $this->busyCallbacks );
-                       /** @var callable $workCallback */
-                       $workCallback =& $this->busyCallbacks[$key];
-                       try {
-                               $workCallback();
-                       } catch ( Exception $e ) {
-                               $workCallback = function () use ( $e ) {
-                                       throw $e;
-                               };
-                       }
-                       unset( $this->busyCallbacks[$key] ); // consume
-
-                       return true;
-               }
-
-               return false;
-       }
-}
diff --git a/includes/libs/filebackend/FSFile.php b/includes/libs/filebackend/FSFile.php
new file mode 100644 (file)
index 0000000..dacad1c
--- /dev/null
@@ -0,0 +1,223 @@
+<?php
+/**
+ * Non-directory file on the file system.
+ *
+ * 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
+ */
+
+/**
+ * Class representing a non-directory file on the file system
+ *
+ * @ingroup FileBackend
+ */
+class FSFile {
+       /** @var string Path to file */
+       protected $path;
+
+       /** @var string File SHA-1 in base 36 */
+       protected $sha1Base36;
+
+       /**
+        * Sets up the file object
+        *
+        * @param string $path Path to temporary file on local disk
+        */
+       public function __construct( $path ) {
+               $this->path = $path;
+       }
+
+       /**
+        * Returns the file system path
+        *
+        * @return string
+        */
+       public function getPath() {
+               return $this->path;
+       }
+
+       /**
+        * Checks if the file exists
+        *
+        * @return bool
+        */
+       public function exists() {
+               return is_file( $this->path );
+       }
+
+       /**
+        * Get the file size in bytes
+        *
+        * @return int|bool
+        */
+       public function getSize() {
+               return filesize( $this->path );
+       }
+
+       /**
+        * Get the file's last-modified timestamp
+        *
+        * @return string|bool TS_MW timestamp or false on failure
+        */
+       public function getTimestamp() {
+               MediaWiki\suppressWarnings();
+               $timestamp = filemtime( $this->path );
+               MediaWiki\restoreWarnings();
+               if ( $timestamp !== false ) {
+                       $timestamp = wfTimestamp( TS_MW, $timestamp );
+               }
+
+               return $timestamp;
+       }
+
+       /**
+        * Get an associative array containing information about
+        * a file with the given storage path.
+        *
+        * Resulting array fields include:
+        *   - fileExists
+        *   - size (filesize in bytes)
+        *   - mime (as major/minor)
+        *   - file-mime (as major/minor)
+        *   - sha1 (in base 36)
+        *   - major_mime
+        *   - minor_mime
+        *
+        * @param string|bool $ext The file extension, or true to extract it from the filename.
+        *             Set it to false to ignore the extension. Currently unused.
+        * @return array
+        */
+       public function getProps( $ext = true ) {
+               $info = self::placeholderProps();
+               $info['fileExists'] = $this->exists();
+
+               if ( $info['fileExists'] ) {
+                       $info['size'] = $this->getSize(); // bytes
+                       $info['sha1'] = $this->getSha1Base36();
+
+                       $mime = mime_content_type( $this->path );
+                       # MIME type according to file contents
+                       $info['file-mime'] = ( $mime === false ) ? 'unknown/unknown' : $mime;
+                       # logical MIME type
+                       $info['mime'] = $mime;
+
+                       if ( strpos( $mime, '/' ) !== false ) {
+                               list( $info['major_mime'], $info['minor_mime'] ) = explode( '/', $mime, 2 );
+                       } else {
+                               list( $info['major_mime'], $info['minor_mime'] ) = [ $mime, 'unknown' ];
+                       }
+               }
+
+               return $info;
+       }
+
+       /**
+        * Placeholder file properties to use for files that don't exist
+        *
+        * Resulting array fields include:
+        *   - fileExists
+        *   - size (filesize in bytes)
+        *   - mime (as major/minor)
+        *   - file-mime (as major/minor)
+        *   - sha1 (in base 36)
+        *   - major_mime
+        *   - minor_mime
+        *
+        * @return array
+        */
+       public static function placeholderProps() {
+               $info = [];
+               $info['fileExists'] = false;
+               $info['size'] = 0;
+               $info['file-mime'] = null;
+               $info['major_mime'] = null;
+               $info['minor_mime'] = null;
+               $info['mime'] = null;
+               $info['sha1'] = '';
+
+               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.
+        *
+        * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
+        * fairly neatly.
+        *
+        * @param bool $recache
+        * @return bool|string False on failure
+        */
+       public function getSha1Base36( $recache = false ) {
+               if ( $this->sha1Base36 !== null && !$recache ) {
+                       return $this->sha1Base36;
+               }
+
+               MediaWiki\suppressWarnings();
+               $this->sha1Base36 = sha1_file( $this->path );
+               MediaWiki\restoreWarnings();
+
+               if ( $this->sha1Base36 !== false ) {
+                       $this->sha1Base36 = Wikimedia\base_convert( $this->sha1Base36, 16, 36, 31 );
+               }
+
+               return $this->sha1Base36;
+       }
+
+       /**
+        * Get the final file extension from a file system path
+        *
+        * @param string $path
+        * @return string
+        */
+       public static function extensionFromPath( $path ) {
+               $i = strrpos( $path, '.' );
+
+               return strtolower( $i ? substr( $path, $i + 1 ) : '' );
+       }
+
+       /**
+        * Get an associative array containing information about a file in the local filesystem.
+        *
+        * @param string $path Absolute local filesystem path
+        * @param string|bool $ext The file extension, or true to extract it from the filename.
+        *   Set it to false to ignore the extension.
+        * @return array
+        */
+       public static function getPropsFromPath( $path, $ext = true ) {
+               $fsFile = new self( $path );
+
+               return $fsFile->getProps( $ext );
+       }
+
+       /**
+        * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
+        * encoding, zero padded to 31 digits.
+        *
+        * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
+        * fairly neatly.
+        *
+        * @param string $path
+        * @return bool|string False on failure
+        */
+       public static function getSha1Base36FromPath( $path ) {
+               $fsFile = new self( $path );
+
+               return $fsFile->getSha1Base36();
+       }
+}
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;
+                       }
+               }
+       }
+}
diff --git a/includes/libs/filebackend/FileBackend.php b/includes/libs/filebackend/FileBackend.php
new file mode 100644 (file)
index 0000000..f33f522
--- /dev/null
@@ -0,0 +1,1638 @@
+<?php
+/**
+ * @defgroup FileBackend File backend
+ *
+ * File backend is used to interact with file storage systems,
+ * such as the local file system, NFS, or cloud storage systems.
+ */
+
+/**
+ * Base class for all file 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
+ */
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+
+/**
+ * @brief Base class for all file backend classes (including multi-write backends).
+ *
+ * This class defines the methods as abstract that subclasses must implement.
+ * 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 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 "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 "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
+ * directory files; a regular file cannot have the same name as a directory. Other
+ * backends with virtual directories may not have this limitation. Callers should
+ * store files in such a way that no files and directories are under the same path.
+ *
+ * In general, this class allows for callers to access storage through the same
+ * interface, without regard to the underlying storage system. However, calling code
+ * must follow certain patterns and be aware of certain things to ensure compatibility:
+ *   - a) Always call prepare() on the parent directory before trying to put a file there;
+ *        key/value stores only need the container to exist first, but filesystems need
+ *        all the parent directories to exist first (prepare() is aware of all this)
+ *   - b) Always call clean() on a directory when it might become empty to avoid empty
+ *        directory buildup on filesystems; key/value stores never have empty directories,
+ *        so doing this helps preserve consistency in both cases
+ *   - c) Likewise, do not rely on the existence of empty directories for anything;
+ *        calling directoryExists() on a path that prepare() was previously called on
+ *        will return false for key/value stores if there are no files under that path
+ *   - d) Never alter the resulting FSFile returned from getLocalReference(), as it could
+ *        either be a copy of the source file in /tmp or the original source file itself
+ *   - e) Use a file layout that results in never attempting to store files over directories
+ *        or directories over files; key/value stores allow this but filesystems do not
+ *   - f) Use ASCII file names (e.g. base32, IDs, hashes) to avoid Unicode issues in Windows
+ *   - g) Do not assume that move operations are atomic (difficult with key/value stores)
+ *   - h) Do not assume that file stat or read operations always have immediate consistency;
+ *        various methods have a "latest" flag that should always be used if up-to-date
+ *        information is required (this trades performance for correctness as needed)
+ *   - i) Do not assume that directory listings have immediate consistency
+ *
+ * Methods of subclasses should avoid throwing exceptions at all costs.
+ * As a corollary, external dependencies should be kept to a minimum.
+ *
+ * @ingroup FileBackend
+ * @since 1.19
+ */
+abstract class FileBackend implements LoggerAwareInterface {
+       /** @var string Unique backend name */
+       protected $name;
+
+       /** @var string Unique domain name */
+       protected $domainId;
+
+       /** @var string Read-only explanation message */
+       protected $readOnly;
+
+       /** @var string When to do operations in parallel */
+       protected $parallelize;
+
+       /** @var int How many operations can be done in parallel */
+       protected $concurrency;
+
+       /** @var string Temporary file directory */
+       protected $tmpDirectory;
+
+       /** @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;
+
+       /** Bitfield flags for supported features */
+       const ATTR_HEADERS = 1; // files can be tagged with standard HTTP headers
+       const ATTR_METADATA = 2; // files can be stored with metadata key/values
+       const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
+
+       /**
+        * Create a new backend instance from configuration.
+        * 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.
+        *   - 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.
+        *   - 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.
+        *   - parallelize : When to do file operations in parallel (when possible).
+        *      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.
+        *   - 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->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->domainId ) ) {
+                       throw new InvalidArgumentException(
+                               "Backend domain ID not provided for '{$this->name}'." );
+               }
+               $this->lockManager = isset( $config['lockManager'] )
+                       ? $config['lockManager']
+                       : new NullLockManager( [] );
+               $this->fileJournal = isset( $config['fileJournal'] )
+                       ? $config['fileJournal']
+                       : FileJournal::factory( [ 'class' => 'NullFileJournal' ], $this->name );
+               $this->readOnly = isset( $config['readOnly'] )
+                       ? (string)$config['readOnly']
+                       : '';
+               $this->parallelize = isset( $config['parallelize'] )
+                       ? (string)$config['parallelize']
+                       : 'off';
+               $this->concurrency = isset( $config['concurrency'] )
+                       ? (int)$config['concurrency']
+                       : 50;
+               $this->obResetFunc = isset( $config['obResetFunc'] )
+                       ? $config['obResetFunc']
+                       : [ $this, 'resetOutputBuffer' ];
+               $this->streamMimeFunc = isset( $config['streamMimeFunc'] )
+                       ? $config['streamMimeFunc']
+                       : null;
+               $this->statusWrapper = isset( $config['statusWrapper'] ) ? $config['statusWrapper'] : null;
+
+               $this->profiler = isset( $config['profiler'] ) ? $config['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.
+        * For example, we can have two Swift backends using different proxies.
+        *
+        * @return string
+        */
+       final public function getName() {
+               return $this->name;
+       }
+
+       /**
+        * 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->getDomainId();
+       }
+
+       /**
+        * Check if this backend is read-only
+        *
+        * @return bool
+        */
+       final public function isReadOnly() {
+               return ( $this->readOnly != '' );
+       }
+
+       /**
+        * Get an explanatory message if this backend is read-only
+        *
+        * @return string|bool Returns false if the backend is not read-only
+        */
+       final public function getReadOnlyReason() {
+               return ( $this->readOnly != '' ) ? $this->readOnly : false;
+       }
+
+       /**
+        * Get the a bitfield of extra features supported by the backend medium
+        *
+        * @return int Bitfield of FileBackend::ATTR_* flags
+        * @since 1.23
+        */
+       public function getFeatures() {
+               return self::ATTR_UNICODE_PATHS;
+       }
+
+       /**
+        * Check if the backend medium supports a field of extra features
+        *
+        * @param int $bitfield Bitfield of FileBackend::ATTR_* flags
+        * @return bool
+        * @since 1.23
+        */
+       final public function hasFeatures( $bitfield ) {
+               return ( $this->getFeatures() & $bitfield ) === $bitfield;
+       }
+
+       /**
+        * This is the main entry point into the backend for write operations.
+        * Callers supply an ordered list of operations to perform as a transaction.
+        * Files will be locked, the stat cache cleared, and then the operations attempted.
+        * If any serious errors occur, all attempted operations will be rolled back.
+        *
+        * $ops is an array of arrays. The outer array holds a list of operations.
+        * Each inner array is a set of key value pairs that specify an operation.
+        *
+        * Supported operations and their parameters. The supported actions are:
+        *  - create
+        *  - store
+        *  - copy
+        *  - move
+        *  - delete
+        *  - describe (since 1.21)
+        *  - null
+        *
+        * FSFile/TempFSFile object support was added in 1.27.
+        *
+        * a) Create a new file in storage with the contents of a string
+        * @code
+        *     [
+        *         'op'                  => 'create',
+        *         'dst'                 => <storage path>,
+        *         'content'             => <string of new file contents>,
+        *         'overwrite'           => <boolean>,
+        *         'overwriteSame'       => <boolean>,
+        *         'headers'             => <HTTP header name/value map> # since 1.21
+        *     ]
+        * @endcode
+        *
+        * b) Copy a file system file into storage
+        * @code
+        *     [
+        *         'op'                  => 'store',
+        *         'src'                 => <file system path, FSFile, or TempFSFile>,
+        *         'dst'                 => <storage path>,
+        *         'overwrite'           => <boolean>,
+        *         'overwriteSame'       => <boolean>,
+        *         'headers'             => <HTTP header name/value map> # since 1.21
+        *     ]
+        * @endcode
+        *
+        * c) Copy a file within storage
+        * @code
+        *     [
+        *         'op'                  => 'copy',
+        *         'src'                 => <storage path>,
+        *         'dst'                 => <storage path>,
+        *         'overwrite'           => <boolean>,
+        *         'overwriteSame'       => <boolean>,
+        *         'ignoreMissingSource' => <boolean>, # since 1.21
+        *         'headers'             => <HTTP header name/value map> # since 1.21
+        *     ]
+        * @endcode
+        *
+        * d) Move a file within storage
+        * @code
+        *     [
+        *         'op'                  => 'move',
+        *         'src'                 => <storage path>,
+        *         'dst'                 => <storage path>,
+        *         'overwrite'           => <boolean>,
+        *         'overwriteSame'       => <boolean>,
+        *         'ignoreMissingSource' => <boolean>, # since 1.21
+        *         'headers'             => <HTTP header name/value map> # since 1.21
+        *     ]
+        * @endcode
+        *
+        * e) Delete a file within storage
+        * @code
+        *     [
+        *         'op'                  => 'delete',
+        *         'src'                 => <storage path>,
+        *         'ignoreMissingSource' => <boolean>
+        *     ]
+        * @endcode
+        *
+        * f) Update metadata for a file within storage
+        * @code
+        *     [
+        *         'op'                  => 'describe',
+        *         'src'                 => <storage path>,
+        *         'headers'             => <HTTP header name/value map>
+        *     ]
+        * @endcode
+        *
+        * g) Do nothing (no-op)
+        * @code
+        *     [
+        *         'op'                  => 'null',
+        *     ]
+        * @endcode
+        *
+        * Boolean flags for operations (operation-specific):
+        *   - ignoreMissingSource : The operation will simply succeed and do
+        *                           nothing if the source file does not exist.
+        *   - overwrite           : Any destination file will be overwritten.
+        *   - overwriteSame       : If a file already exists at the destination with the
+        *                           same contents, then do nothing to the destination file
+        *                           instead of giving an error. This does not compare headers.
+        *                           This option is ignored if 'overwrite' is already provided.
+        *   - headers             : If supplied, the result of merging these headers with any
+        *                           existing source file headers (replacing conflicting ones)
+        *                           will be set as the destination file headers. Headers are
+        *                           deleted if their value is set to the empty string. When a
+        *                           file has headers they are included in responses to GET and
+        *                           HEAD requests to the backing store for that file.
+        *                           Header values should be no larger than 255 bytes, except for
+        *                           Content-Disposition. The system might ignore or truncate any
+        *                           headers that are too long to store (exact limits will vary).
+        *                           Backends that don't support metadata ignore this. (since 1.21)
+        *
+        * $opts is an associative of boolean flags, including:
+        *   - force               : Operation precondition errors no longer trigger an abort.
+        *                           Any remaining operations are still attempted. Unexpected
+        *                           failures may still cause remaining operations to be aborted.
+        *   - nonLocking          : No locks are acquired for the operations.
+        *                           This can increase performance for non-critical writes.
+        *                           This has no effect unless the 'force' flag is set.
+        *   - nonJournaled        : Don't log this operation batch in the file journal.
+        *                           This limits the ability of recovery scripts.
+        *   - parallelize         : Try to do operations in parallel when possible.
+        *   - bypassReadOnly      : Allow writes in read-only mode. (since 1.20)
+        *   - preserveCache       : Don't clear the process cache before checking files.
+        *                           This should only be used if all entries in the process
+        *                           cache were added after the files were already locked. (since 1.20)
+        *
+        * @remarks Remarks on locking:
+        * File system paths given to operations should refer to files that are
+        * already locked or otherwise safe from modification from other processes.
+        * Normally these files will be new temp files, which should be adequate.
+        *
+        * @par Return value:
+        *
+        * This returns a Status, which contains all warnings and fatals that occurred
+        * during the operation. The 'failCount', 'successCount', and 'success' members
+        * will reflect each operation attempted.
+        *
+        * The StatusValue will be "OK" unless:
+        *   - a) unexpected operation errors occurred (network partitions, disk full...)
+        *   - b) significant operation errors occurred and 'force' was not set
+        *
+        * @param array $ops List of operations to execute in order
+        * @param array $opts Batch operation options
+        * @return StatusValue
+        */
+       final public function doOperations( array $ops, array $opts = [] ) {
+               if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
+                       return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
+               }
+               if ( !count( $ops ) ) {
+                       return $this->newStatus(); // nothing to do
+               }
+
+               $ops = $this->resolveFSFileObjects( $ops );
+               if ( empty( $opts['force'] ) ) { // sanity
+                       unset( $opts['nonLocking'] );
+               }
+
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
+
+               return $this->doOperationsInternal( $ops, $opts );
+       }
+
+       /**
+        * @see FileBackend::doOperations()
+        * @param array $ops
+        * @param array $opts
+        */
+       abstract protected function doOperationsInternal( array $ops, array $opts );
+
+       /**
+        * Same as doOperations() except it takes a single operation.
+        * If you are doing a batch of operations that should either
+        * all succeed or all fail, then use that function instead.
+        *
+        * @see FileBackend::doOperations()
+        *
+        * @param array $op Operation
+        * @param array $opts Operation options
+        * @return StatusValue
+        */
+       final public function doOperation( array $op, array $opts = [] ) {
+               return $this->doOperations( [ $op ], $opts );
+       }
+
+       /**
+        * Performs a single create operation.
+        * This sets $params['op'] to 'create' and passes it to doOperation().
+        *
+        * @see FileBackend::doOperation()
+        *
+        * @param array $params Operation parameters
+        * @param array $opts Operation options
+        * @return StatusValue
+        */
+       final public function create( array $params, array $opts = [] ) {
+               return $this->doOperation( [ 'op' => 'create' ] + $params, $opts );
+       }
+
+       /**
+        * Performs a single store operation.
+        * This sets $params['op'] to 'store' and passes it to doOperation().
+        *
+        * @see FileBackend::doOperation()
+        *
+        * @param array $params Operation parameters
+        * @param array $opts Operation options
+        * @return StatusValue
+        */
+       final public function store( array $params, array $opts = [] ) {
+               return $this->doOperation( [ 'op' => 'store' ] + $params, $opts );
+       }
+
+       /**
+        * Performs a single copy operation.
+        * This sets $params['op'] to 'copy' and passes it to doOperation().
+        *
+        * @see FileBackend::doOperation()
+        *
+        * @param array $params Operation parameters
+        * @param array $opts Operation options
+        * @return StatusValue
+        */
+       final public function copy( array $params, array $opts = [] ) {
+               return $this->doOperation( [ 'op' => 'copy' ] + $params, $opts );
+       }
+
+       /**
+        * Performs a single move operation.
+        * This sets $params['op'] to 'move' and passes it to doOperation().
+        *
+        * @see FileBackend::doOperation()
+        *
+        * @param array $params Operation parameters
+        * @param array $opts Operation options
+        * @return StatusValue
+        */
+       final public function move( array $params, array $opts = [] ) {
+               return $this->doOperation( [ 'op' => 'move' ] + $params, $opts );
+       }
+
+       /**
+        * Performs a single delete operation.
+        * This sets $params['op'] to 'delete' and passes it to doOperation().
+        *
+        * @see FileBackend::doOperation()
+        *
+        * @param array $params Operation parameters
+        * @param array $opts Operation options
+        * @return StatusValue
+        */
+       final public function delete( array $params, array $opts = [] ) {
+               return $this->doOperation( [ 'op' => 'delete' ] + $params, $opts );
+       }
+
+       /**
+        * Performs a single describe operation.
+        * This sets $params['op'] to 'describe' and passes it to doOperation().
+        *
+        * @see FileBackend::doOperation()
+        *
+        * @param array $params Operation parameters
+        * @param array $opts Operation options
+        * @return StatusValue
+        * @since 1.21
+        */
+       final public function describe( array $params, array $opts = [] ) {
+               return $this->doOperation( [ 'op' => 'describe' ] + $params, $opts );
+       }
+
+       /**
+        * Perform a set of independent file operations on some files.
+        *
+        * This does no locking, nor journaling, and possibly no stat calls.
+        * Any destination files that already exist will be overwritten.
+        * This should *only* be used on non-original files, like cache files.
+        *
+        * Supported operations and their parameters:
+        *  - create
+        *  - store
+        *  - copy
+        *  - move
+        *  - delete
+        *  - describe (since 1.21)
+        *  - null
+        *
+        * FSFile/TempFSFile object support was added in 1.27.
+        *
+        * a) Create a new file in storage with the contents of a string
+        * @code
+        *     [
+        *         'op'                  => 'create',
+        *         'dst'                 => <storage path>,
+        *         'content'             => <string of new file contents>,
+        *         'headers'             => <HTTP header name/value map> # since 1.21
+        *     ]
+        * @endcode
+        *
+        * b) Copy a file system file into storage
+        * @code
+        *     [
+        *         'op'                  => 'store',
+        *         'src'                 => <file system path, FSFile, or TempFSFile>,
+        *         'dst'                 => <storage path>,
+        *         'headers'             => <HTTP header name/value map> # since 1.21
+        *     ]
+        * @endcode
+        *
+        * c) Copy a file within storage
+        * @code
+        *     [
+        *         'op'                  => 'copy',
+        *         'src'                 => <storage path>,
+        *         'dst'                 => <storage path>,
+        *         'ignoreMissingSource' => <boolean>, # since 1.21
+        *         'headers'             => <HTTP header name/value map> # since 1.21
+        *     ]
+        * @endcode
+        *
+        * d) Move a file within storage
+        * @code
+        *     [
+        *         'op'                  => 'move',
+        *         'src'                 => <storage path>,
+        *         'dst'                 => <storage path>,
+        *         'ignoreMissingSource' => <boolean>, # since 1.21
+        *         'headers'             => <HTTP header name/value map> # since 1.21
+        *     ]
+        * @endcode
+        *
+        * e) Delete a file within storage
+        * @code
+        *     [
+        *         'op'                  => 'delete',
+        *         'src'                 => <storage path>,
+        *         'ignoreMissingSource' => <boolean>
+        *     ]
+        * @endcode
+        *
+        * f) Update metadata for a file within storage
+        * @code
+        *     [
+        *         'op'                  => 'describe',
+        *         'src'                 => <storage path>,
+        *         'headers'             => <HTTP header name/value map>
+        *     ]
+        * @endcode
+        *
+        * g) Do nothing (no-op)
+        * @code
+        *     [
+        *         'op'                  => 'null',
+        *     ]
+        * @endcode
+        *
+        * @par Boolean flags for operations (operation-specific):
+        *   - ignoreMissingSource : The operation will simply succeed and do
+        *                           nothing if the source file does not exist.
+        *   - headers             : If supplied with a header name/value map, the backend will
+        *                           reply with these headers when GETs/HEADs of the destination
+        *                           file are made. Header values should be smaller than 256 bytes.
+        *                           Content-Disposition headers can be longer, though the system
+        *                           might ignore or truncate ones that are too long to store.
+        *                           Existing headers will remain, but these will replace any
+        *                           conflicting previous headers, and headers will be removed
+        *                           if they are set to an empty string.
+        *                           Backends that don't support metadata ignore this. (since 1.21)
+        *
+        * $opts is an associative of boolean flags, including:
+        *   - bypassReadOnly      : Allow writes in read-only mode (since 1.20)
+        *
+        * @par Return value:
+        * This returns a Status, which contains all warnings and fatals that occurred
+        * during the operation. The 'failCount', 'successCount', and 'success' members
+        * will reflect each operation attempted for the given files. The StatusValue will be
+        * considered "OK" as long as no fatal errors occurred.
+        *
+        * @param array $ops Set of operations to execute
+        * @param array $opts Batch operation options
+        * @return StatusValue
+        * @since 1.20
+        */
+       final public function doQuickOperations( array $ops, array $opts = [] ) {
+               if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
+                       return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
+               }
+               if ( !count( $ops ) ) {
+                       return $this->newStatus(); // nothing to do
+               }
+
+               $ops = $this->resolveFSFileObjects( $ops );
+               foreach ( $ops as &$op ) {
+                       $op['overwrite'] = true; // avoids RTTs in key/value stores
+               }
+
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
+
+               return $this->doQuickOperationsInternal( $ops );
+       }
+
+       /**
+        * @see FileBackend::doQuickOperations()
+        * @param array $ops
+        * @since 1.20
+        */
+       abstract protected function doQuickOperationsInternal( array $ops );
+
+       /**
+        * Same as doQuickOperations() except it takes a single operation.
+        * If you are doing a batch of operations, then use that function instead.
+        *
+        * @see FileBackend::doQuickOperations()
+        *
+        * @param array $op Operation
+        * @return StatusValue
+        * @since 1.20
+        */
+       final public function doQuickOperation( array $op ) {
+               return $this->doQuickOperations( [ $op ] );
+       }
+
+       /**
+        * Performs a single quick create operation.
+        * This sets $params['op'] to 'create' and passes it to doQuickOperation().
+        *
+        * @see FileBackend::doQuickOperation()
+        *
+        * @param array $params Operation parameters
+        * @return StatusValue
+        * @since 1.20
+        */
+       final public function quickCreate( array $params ) {
+               return $this->doQuickOperation( [ 'op' => 'create' ] + $params );
+       }
+
+       /**
+        * Performs a single quick store operation.
+        * This sets $params['op'] to 'store' and passes it to doQuickOperation().
+        *
+        * @see FileBackend::doQuickOperation()
+        *
+        * @param array $params Operation parameters
+        * @return StatusValue
+        * @since 1.20
+        */
+       final public function quickStore( array $params ) {
+               return $this->doQuickOperation( [ 'op' => 'store' ] + $params );
+       }
+
+       /**
+        * Performs a single quick copy operation.
+        * This sets $params['op'] to 'copy' and passes it to doQuickOperation().
+        *
+        * @see FileBackend::doQuickOperation()
+        *
+        * @param array $params Operation parameters
+        * @return StatusValue
+        * @since 1.20
+        */
+       final public function quickCopy( array $params ) {
+               return $this->doQuickOperation( [ 'op' => 'copy' ] + $params );
+       }
+
+       /**
+        * Performs a single quick move operation.
+        * This sets $params['op'] to 'move' and passes it to doQuickOperation().
+        *
+        * @see FileBackend::doQuickOperation()
+        *
+        * @param array $params Operation parameters
+        * @return StatusValue
+        * @since 1.20
+        */
+       final public function quickMove( array $params ) {
+               return $this->doQuickOperation( [ 'op' => 'move' ] + $params );
+       }
+
+       /**
+        * Performs a single quick delete operation.
+        * This sets $params['op'] to 'delete' and passes it to doQuickOperation().
+        *
+        * @see FileBackend::doQuickOperation()
+        *
+        * @param array $params Operation parameters
+        * @return StatusValue
+        * @since 1.20
+        */
+       final public function quickDelete( array $params ) {
+               return $this->doQuickOperation( [ 'op' => 'delete' ] + $params );
+       }
+
+       /**
+        * Performs a single quick describe operation.
+        * This sets $params['op'] to 'describe' and passes it to doQuickOperation().
+        *
+        * @see FileBackend::doQuickOperation()
+        *
+        * @param array $params Operation parameters
+        * @return StatusValue
+        * @since 1.21
+        */
+       final public function quickDescribe( array $params ) {
+               return $this->doQuickOperation( [ 'op' => 'describe' ] + $params );
+       }
+
+       /**
+        * Concatenate a list of storage files into a single file system file.
+        * The target path should refer to a file that is already locked or
+        * otherwise safe from modification from other processes. Normally,
+        * the file will be a new temp file, which should be adequate.
+        *
+        * @param array $params Operation parameters, include:
+        *   - srcs        : ordered source storage paths (e.g. chunk1, chunk2, ...)
+        *   - dst         : file system path to 0-byte temp file
+        *   - parallelize : try to do operations in parallel when possible
+        * @return StatusValue
+        */
+       abstract public function concatenate( array $params );
+
+       /**
+        * Prepare a storage directory for usage.
+        * This will create any required containers and parent directories.
+        * Backends using key/value stores only need to create the container.
+        *
+        * The 'noAccess' and 'noListing' parameters works the same as in secure(),
+        * except they are only applied *if* the directory/container had to be created.
+        * These flags should always be set for directories that have private files.
+        * However, setting them is not guaranteed to actually do anything.
+        * Additional server configuration may be needed to achieve the desired effect.
+        *
+        * @param array $params Parameters include:
+        *   - dir            : storage directory
+        *   - noAccess       : try to deny file access (since 1.20)
+        *   - noListing      : try to deny file listing (since 1.20)
+        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
+        * @return StatusValue
+        */
+       final public function prepare( array $params ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
+                       return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
+               }
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
+               return $this->doPrepare( $params );
+       }
+
+       /**
+        * @see FileBackend::prepare()
+        * @param array $params
+        */
+       abstract protected function doPrepare( array $params );
+
+       /**
+        * Take measures to block web access to a storage directory and
+        * the container it belongs to. FS backends might add .htaccess
+        * files whereas key/value store backends might revoke container
+        * access to the storage user representing end-users in web requests.
+        *
+        * This is not guaranteed to actually make files or listings publically hidden.
+        * Additional server configuration may be needed to achieve the desired effect.
+        *
+        * @param array $params Parameters include:
+        *   - dir            : storage directory
+        *   - noAccess       : try to deny file access
+        *   - noListing      : try to deny file listing
+        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
+        * @return StatusValue
+        */
+       final public function secure( array $params ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
+                       return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
+               }
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
+               return $this->doSecure( $params );
+       }
+
+       /**
+        * @see FileBackend::secure()
+        * @param array $params
+        */
+       abstract protected function doSecure( array $params );
+
+       /**
+        * Remove measures to block web access to a storage directory and
+        * the container it belongs to. FS backends might remove .htaccess
+        * files whereas key/value store backends might grant container
+        * access to the storage user representing end-users in web requests.
+        * This essentially can undo the result of secure() calls.
+        *
+        * This is not guaranteed to actually make files or listings publically viewable.
+        * Additional server configuration may be needed to achieve the desired effect.
+        *
+        * @param array $params Parameters include:
+        *   - dir            : storage directory
+        *   - access         : try to allow file access
+        *   - listing        : try to allow file listing
+        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
+        * @return StatusValue
+        * @since 1.20
+        */
+       final public function publish( array $params ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
+                       return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
+               }
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
+               return $this->doPublish( $params );
+       }
+
+       /**
+        * @see FileBackend::publish()
+        * @param array $params
+        */
+       abstract protected function doPublish( array $params );
+
+       /**
+        * Delete a storage directory if it is empty.
+        * Backends using key/value stores may do nothing unless the directory
+        * is that of an empty container, in which case it will be deleted.
+        *
+        * @param array $params Parameters include:
+        *   - dir            : storage directory
+        *   - recursive      : recursively delete empty subdirectories first (since 1.20)
+        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
+        * @return StatusValue
+        */
+       final public function clean( array $params ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
+                       return $this->newStatus( 'backend-fail-readonly', $this->name, $this->readOnly );
+               }
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $scope = $this->getScopedPHPBehaviorForOps(); // try to ignore client aborts
+               return $this->doClean( $params );
+       }
+
+       /**
+        * @see FileBackend::clean()
+        * @param array $params
+        */
+       abstract protected function doClean( array $params );
+
+       /**
+        * Enter file operation scope.
+        * This just makes PHP ignore user aborts/disconnects until the return
+        * value leaves scope. This returns null and does nothing in CLI mode.
+        *
+        * @return ScopedCallback|null
+        */
+       final protected function getScopedPHPBehaviorForOps() {
+               if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
+                       $old = ignore_user_abort( true ); // avoid half-finished operations
+                       return new ScopedCallback( function () use ( $old ) {
+                               ignore_user_abort( $old );
+                       } );
+               }
+
+               return null;
+       }
+
+       /**
+        * Check if a file exists at a storage path in the backend.
+        * This returns false if only a directory exists at the path.
+        *
+        * @param array $params Parameters include:
+        *   - src    : source storage path
+        *   - latest : use the latest available data
+        * @return bool|null Returns null on failure
+        */
+       abstract public function fileExists( array $params );
+
+       /**
+        * Get the last-modified timestamp of the file at a storage path.
+        *
+        * @param array $params Parameters include:
+        *   - src    : source storage path
+        *   - latest : use the latest available data
+        * @return string|bool TS_MW timestamp or false on failure
+        */
+       abstract public function getFileTimestamp( array $params );
+
+       /**
+        * Get the contents of a file at a storage path in the backend.
+        * This should be avoided for potentially large files.
+        *
+        * @param array $params Parameters include:
+        *   - src    : source storage path
+        *   - latest : use the latest available data
+        * @return string|bool Returns false on failure
+        */
+       final public function getFileContents( array $params ) {
+               $contents = $this->getFileContentsMulti(
+                       [ 'srcs' => [ $params['src'] ] ] + $params );
+
+               return $contents[$params['src']];
+       }
+
+       /**
+        * Like getFileContents() except it takes an array of storage paths
+        * and returns a map of storage paths to strings (or null on failure).
+        * The map keys (paths) are in the same order as the provided list of paths.
+        *
+        * @see FileBackend::getFileContents()
+        *
+        * @param array $params Parameters include:
+        *   - srcs        : list of source storage paths
+        *   - latest      : use the latest available data
+        *   - parallelize : try to do operations in parallel when possible
+        * @return array Map of (path name => string or false on failure)
+        * @since 1.20
+        */
+       abstract public function getFileContentsMulti( array $params );
+
+       /**
+        * Get metadata about a file at a storage path in the backend.
+        * If the file does not exist, then this returns false.
+        * Otherwise, the result is an associative array that includes:
+        *   - headers  : map of HTTP headers used for GET/HEAD requests (name => value)
+        *   - metadata : map of file metadata (name => value)
+        * Metadata keys and headers names will be returned in all lower-case.
+        * Additional values may be included for internal use only.
+        *
+        * Use FileBackend::hasFeatures() to check how well this is supported.
+        *
+        * @param array $params
+        * $params include:
+        *   - src    : source storage path
+        *   - latest : use the latest available data
+        * @return array|bool Returns false on failure
+        * @since 1.23
+        */
+       abstract public function getFileXAttributes( array $params );
+
+       /**
+        * Get the size (bytes) of a file at a storage path in the backend.
+        *
+        * @param array $params Parameters include:
+        *   - src    : source storage path
+        *   - latest : use the latest available data
+        * @return int|bool Returns false on failure
+        */
+       abstract public function getFileSize( array $params );
+
+       /**
+        * Get quick information about a file at a storage path in the backend.
+        * If the file does not exist, then this returns false.
+        * Otherwise, the result is an associative array that includes:
+        *   - mtime  : the last-modified timestamp (TS_MW)
+        *   - size   : the file size (bytes)
+        * Additional values may be included for internal use only.
+        *
+        * @param array $params Parameters include:
+        *   - src    : source storage path
+        *   - latest : use the latest available data
+        * @return array|bool|null Returns null on failure
+        */
+       abstract public function getFileStat( array $params );
+
+       /**
+        * Get a SHA-1 hash of the file at a storage path in the backend.
+        *
+        * @param array $params Parameters include:
+        *   - src    : source storage path
+        *   - latest : use the latest available data
+        * @return string|bool Hash string or false on failure
+        */
+       abstract public function getFileSha1Base36( array $params );
+
+       /**
+        * Get the properties of the file at a storage path in the backend.
+        * This gives the result of FSFile::getProps() on a local copy of the file.
+        *
+        * @param array $params Parameters include:
+        *   - src    : source storage path
+        *   - latest : use the latest available data
+        * @return array Returns FSFile::placeholderProps() on failure
+        */
+       abstract public function getFileProps( array $params );
+
+       /**
+        * Stream the file at a storage path in the backend.
+        *
+        * If the file does not exists, an HTTP 404 error will be given.
+        * Appropriate HTTP headers (Status, Content-Type, Content-Length)
+        * will be sent if streaming began, while none will be sent otherwise.
+        * Implementations should flush the output buffer before sending data.
+        *
+        * @param array $params Parameters include:
+        *   - src      : source storage path
+        *   - headers  : list of additional HTTP headers to send if the file exists
+        *   - options  : HTTP request header map with lower case keys (since 1.28). Supports:
+        *                range             : format is "bytes=(\d*-\d*)"
+        *                if-modified-since : format is an HTTP date
+        *   - headless : only include the body (and headers from "headers") (since 1.28)
+        *   - latest   : use the latest available data
+        *   - allowOB  : preserve any output buffers (since 1.28)
+        * @return StatusValue
+        */
+       abstract public function streamFile( array $params );
+
+       /**
+        * Returns a file system file, identical to the file at a storage path.
+        * The file returned is either:
+        *   - a) A local copy of the file at a storage path in the backend.
+        *        The temporary copy will have the same extension as the source.
+        *   - b) An original of the file at a storage path in the backend.
+        * Temporary files may be purged when the file object falls out of scope.
+        *
+        * Write operations should *never* be done on this file as some backends
+        * may do internal tracking or may be instances of FileBackendMultiWrite.
+        * In that latter case, there are copies of the file that must stay in sync.
+        * Additionally, further calls to this function may return the same file.
+        *
+        * @param array $params Parameters include:
+        *   - src    : source storage path
+        *   - latest : use the latest available data
+        * @return FSFile|null Returns null on failure
+        */
+       final public function getLocalReference( array $params ) {
+               $fsFiles = $this->getLocalReferenceMulti(
+                       [ 'srcs' => [ $params['src'] ] ] + $params );
+
+               return $fsFiles[$params['src']];
+       }
+
+       /**
+        * Like getLocalReference() except it takes an array of storage paths
+        * and returns a map of storage paths to FSFile objects (or null on failure).
+        * The map keys (paths) are in the same order as the provided list of paths.
+        *
+        * @see FileBackend::getLocalReference()
+        *
+        * @param array $params Parameters include:
+        *   - srcs        : list of source storage paths
+        *   - latest      : use the latest available data
+        *   - parallelize : try to do operations in parallel when possible
+        * @return array Map of (path name => FSFile or null on failure)
+        * @since 1.20
+        */
+       abstract public function getLocalReferenceMulti( array $params );
+
+       /**
+        * Get a local copy on disk of the file at a storage path in the backend.
+        * The temporary copy will have the same file extension as the source.
+        * Temporary files may be purged when the file object falls out of scope.
+        *
+        * @param array $params Parameters include:
+        *   - src    : source storage path
+        *   - latest : use the latest available data
+        * @return TempFSFile|null Returns null on failure
+        */
+       final public function getLocalCopy( array $params ) {
+               $tmpFiles = $this->getLocalCopyMulti(
+                       [ 'srcs' => [ $params['src'] ] ] + $params );
+
+               return $tmpFiles[$params['src']];
+       }
+
+       /**
+        * Like getLocalCopy() except it takes an array of storage paths and
+        * returns a map of storage paths to TempFSFile objects (or null on failure).
+        * The map keys (paths) are in the same order as the provided list of paths.
+        *
+        * @see FileBackend::getLocalCopy()
+        *
+        * @param array $params Parameters include:
+        *   - srcs        : list of source storage paths
+        *   - latest      : use the latest available data
+        *   - parallelize : try to do operations in parallel when possible
+        * @return array Map of (path name => TempFSFile or null on failure)
+        * @since 1.20
+        */
+       abstract public function getLocalCopyMulti( array $params );
+
+       /**
+        * Return an HTTP URL to a given file that requires no authentication to use.
+        * The URL may be pre-authenticated (via some token in the URL) and temporary.
+        * This will return null if the backend cannot make an HTTP URL for the file.
+        *
+        * This is useful for key/value stores when using scripts that seek around
+        * large files and those scripts (and the backend) support HTTP Range headers.
+        * Otherwise, one would need to use getLocalReference(), which involves loading
+        * the entire file on to local disk.
+        *
+        * @param array $params Parameters include:
+        *   - src : source storage path
+        *   - ttl : lifetime (seconds) if pre-authenticated; default is 1 day
+        * @return string|null
+        * @since 1.21
+        */
+       abstract public function getFileHttpUrl( array $params );
+
+       /**
+        * Check if a directory exists at a given storage path.
+        * Backends using key/value stores will check if the path is a
+        * virtual directory, meaning there are files under the given directory.
+        *
+        * Storage backends with eventual consistency might return stale data.
+        *
+        * @param array $params Parameters include:
+        *   - dir : storage directory
+        * @return bool|null Returns null on failure
+        * @since 1.20
+        */
+       abstract public function directoryExists( array $params );
+
+       /**
+        * Get an iterator to list *all* directories under a storage directory.
+        * If the directory is of the form "mwstore://backend/container",
+        * then all directories in the container will be listed.
+        * If the directory is of form "mwstore://backend/container/dir",
+        * then all directories directly under that directory will be listed.
+        * Results will be storage directories relative to the given directory.
+        *
+        * Storage backends with eventual consistency might return stale data.
+        *
+        * Failures during iteration can result in FileBackendError exceptions (since 1.22).
+        *
+        * @param array $params Parameters include:
+        *   - dir     : storage directory
+        *   - topOnly : only return direct child dirs of the directory
+        * @return Traversable|array|null Returns null on failure
+        * @since 1.20
+        */
+       abstract public function getDirectoryList( array $params );
+
+       /**
+        * Same as FileBackend::getDirectoryList() except only lists
+        * directories that are immediately under the given directory.
+        *
+        * Storage backends with eventual consistency might return stale data.
+        *
+        * Failures during iteration can result in FileBackendError exceptions (since 1.22).
+        *
+        * @param array $params Parameters include:
+        *   - dir : storage directory
+        * @return Traversable|array|null Returns null on failure
+        * @since 1.20
+        */
+       final public function getTopDirectoryList( array $params ) {
+               return $this->getDirectoryList( [ 'topOnly' => true ] + $params );
+       }
+
+       /**
+        * Get an iterator to list *all* stored files under a storage directory.
+        * If the directory is of the form "mwstore://backend/container",
+        * then all files in the container will be listed.
+        * If the directory is of form "mwstore://backend/container/dir",
+        * then all files under that directory will be listed.
+        * Results will be storage paths relative to the given directory.
+        *
+        * Storage backends with eventual consistency might return stale data.
+        *
+        * Failures during iteration can result in FileBackendError exceptions (since 1.22).
+        *
+        * @param array $params Parameters include:
+        *   - dir        : storage directory
+        *   - topOnly    : only return direct child files of the directory (since 1.20)
+        *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
+        * @return Traversable|array|null Returns null on failure
+        */
+       abstract public function getFileList( array $params );
+
+       /**
+        * Same as FileBackend::getFileList() except only lists
+        * files that are immediately under the given directory.
+        *
+        * Storage backends with eventual consistency might return stale data.
+        *
+        * Failures during iteration can result in FileBackendError exceptions (since 1.22).
+        *
+        * @param array $params Parameters include:
+        *   - dir        : storage directory
+        *   - adviseStat : set to true if stat requests will be made on the files (since 1.22)
+        * @return Traversable|array|null Returns null on failure
+        * @since 1.20
+        */
+       final public function getTopFileList( array $params ) {
+               return $this->getFileList( [ 'topOnly' => true ] + $params );
+       }
+
+       /**
+        * Preload persistent file stat cache and property cache into in-process cache.
+        * This should be used when stat calls will be made on a known list of a many files.
+        *
+        * @see FileBackend::getFileStat()
+        *
+        * @param array $paths Storage paths
+        */
+       abstract public function preloadCache( array $paths );
+
+       /**
+        * Invalidate any in-process file stat and property cache.
+        * If $paths is given, then only the cache for those files will be cleared.
+        *
+        * @see FileBackend::getFileStat()
+        *
+        * @param array $paths Storage paths (optional)
+        */
+       abstract public function clearCache( array $paths = null );
+
+       /**
+        * Preload file stat information (concurrently if possible) into in-process cache.
+        *
+        * This should be used when stat calls will be made on a known list of a many files.
+        * This does not make use of the persistent file stat cache.
+        *
+        * @see FileBackend::getFileStat()
+        *
+        * @param array $params Parameters include:
+        *   - srcs        : list of source storage paths
+        *   - latest      : use the latest available data
+        * @return bool All requests proceeded without I/O errors (since 1.24)
+        * @since 1.23
+        */
+       abstract public function preloadFileStat( array $params );
+
+       /**
+        * Lock the files at the given storage paths in the backend.
+        * This will either lock all the files or none (on failure).
+        *
+        * Callers should consider using getScopedFileLocks() instead.
+        *
+        * @param array $paths Storage paths
+        * @param int $type LockManager::LOCK_* constant
+        * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
+        * @return StatusValue
+        */
+       final public function lockFiles( array $paths, $type, $timeout = 0 ) {
+               $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
+
+               return $this->wrapStatus( $this->lockManager->lock( $paths, $type, $timeout ) );
+       }
+
+       /**
+        * Unlock the files at the given storage paths in the backend.
+        *
+        * @param array $paths Storage paths
+        * @param int $type LockManager::LOCK_* constant
+        * @return StatusValue
+        */
+       final public function unlockFiles( array $paths, $type ) {
+               $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
+
+               return $this->wrapStatus( $this->lockManager->unlock( $paths, $type ) );
+       }
+
+       /**
+        * Lock the files at the given storage paths in the backend.
+        * This will either lock all the files or none (on failure).
+        * On failure, the StatusValue object will be updated with errors.
+        *
+        * Once the return value goes out scope, the locks will be released and
+        * the StatusValue updated. Unlock fatals will not change the StatusValue "OK" value.
+        *
+        * @see ScopedLock::factory()
+        *
+        * @param array $paths List of storage paths or map of lock types to path lists
+        * @param int|string $type LockManager::LOCK_* constant or "mixed"
+        * @param StatusValue $status StatusValue to update on lock/unlock
+        * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.24)
+        * @return ScopedLock|null Returns null on failure
+        */
+       final public function getScopedFileLocks(
+               array $paths, $type, StatusValue $status, $timeout = 0
+       ) {
+               if ( $type === 'mixed' ) {
+                       foreach ( $paths as &$typePaths ) {
+                               $typePaths = array_map( 'FileBackend::normalizeStoragePath', $typePaths );
+                       }
+               } else {
+                       $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
+               }
+
+               return ScopedLock::factory( $this->lockManager, $paths, $type, $status, $timeout );
+       }
+
+       /**
+        * Get an array of scoped locks needed for a batch of file operations.
+        *
+        * Normally, FileBackend::doOperations() handles locking, unless
+        * the 'nonLocking' param is passed in. This function is useful if you
+        * want the files to be locked for a broader scope than just when the
+        * files are changing. For example, if you need to update DB metadata,
+        * you may want to keep the files locked until finished.
+        *
+        * @see FileBackend::doOperations()
+        *
+        * @param array $ops List of file operations to FileBackend::doOperations()
+        * @param StatusValue $status StatusValue to update on lock/unlock
+        * @return ScopedLock|null
+        * @since 1.20
+        */
+       abstract public function getScopedLocksForOps( array $ops, StatusValue $status );
+
+       /**
+        * Get the root storage path of this backend.
+        * All container paths are "subdirectories" of this path.
+        *
+        * @return string Storage path
+        * @since 1.20
+        */
+       final public function getRootStoragePath() {
+               return "mwstore://{$this->name}";
+       }
+
+       /**
+        * Get the storage path for the given container for this backend
+        *
+        * @param string $container Container name
+        * @return string Storage path
+        * @since 1.21
+        */
+       final public function getContainerStoragePath( $container ) {
+               return $this->getRootStoragePath() . "/{$container}";
+       }
+
+       /**
+        * Get the file journal object for this backend
+        *
+        * @return FileJournal
+        */
+       final public function getJournal() {
+               return $this->fileJournal;
+       }
+
+       /**
+        * Convert FSFile 'src' paths to string paths (with an 'srcRef' field set to the FSFile)
+        *
+        * The 'srcRef' field keeps any TempFSFile objects in scope for the backend to have it
+        * around as long it needs (which may vary greatly depending on configuration)
+        *
+        * @param array $ops File operation batch for FileBaclend::doOperations()
+        * @return array File operation batch
+        */
+       protected function resolveFSFileObjects( array $ops ) {
+               foreach ( $ops as &$op ) {
+                       $src = isset( $op['src'] ) ? $op['src'] : null;
+                       if ( $src instanceof FSFile ) {
+                               $op['srcRef'] = $src;
+                               $op['src'] = $src->getPath();
+                       }
+               }
+               unset( $op );
+
+               return $ops;
+       }
+
+       /**
+        * Check if a given path is a "mwstore://" path.
+        * This does not do any further validation or any existence checks.
+        *
+        * @param string $path
+        * @return bool
+        */
+       final public static function isStoragePath( $path ) {
+               return ( strpos( $path, 'mwstore://' ) === 0 );
+       }
+
+       /**
+        * Split a storage path into a backend name, a container name,
+        * and a relative file path. The relative path may be the empty string.
+        * This does not do any path normalization or traversal checks.
+        *
+        * @param string $storagePath
+        * @return array (backend, container, rel object) or (null, null, null)
+        */
+       final public static function splitStoragePath( $storagePath ) {
+               if ( self::isStoragePath( $storagePath ) ) {
+                       // Remove the "mwstore://" prefix and split the path
+                       $parts = explode( '/', substr( $storagePath, 10 ), 3 );
+                       if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
+                               if ( count( $parts ) == 3 ) {
+                                       return $parts; // e.g. "backend/container/path"
+                               } else {
+                                       return [ $parts[0], $parts[1], '' ]; // e.g. "backend/container"
+                               }
+                       }
+               }
+
+               return [ null, null, null ];
+       }
+
+       /**
+        * Normalize a storage path by cleaning up directory separators.
+        * Returns null if the path is not of the format of a valid storage path.
+        *
+        * @param string $storagePath
+        * @return string|null
+        */
+       final public static function normalizeStoragePath( $storagePath ) {
+               list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
+               if ( $relPath !== null ) { // must be for this backend
+                       $relPath = self::normalizeContainerPath( $relPath );
+                       if ( $relPath !== null ) {
+                               return ( $relPath != '' )
+                                       ? "mwstore://{$backend}/{$container}/{$relPath}"
+                                       : "mwstore://{$backend}/{$container}";
+                       }
+               }
+
+               return null;
+       }
+
+       /**
+        * Get the parent storage directory of a storage path.
+        * This returns a path like "mwstore://backend/container",
+        * "mwstore://backend/container/...", or null if there is no parent.
+        *
+        * @param string $storagePath
+        * @return string|null
+        */
+       final public static function parentStoragePath( $storagePath ) {
+               $storagePath = dirname( $storagePath );
+               list( , , $rel ) = self::splitStoragePath( $storagePath );
+
+               return ( $rel === null ) ? null : $storagePath;
+       }
+
+       /**
+        * Get the final extension from a storage or FS path
+        *
+        * @param string $path
+        * @param string $case One of (rawcase, uppercase, lowercase) (since 1.24)
+        * @return string
+        */
+       final public static function extensionFromPath( $path, $case = 'lowercase' ) {
+               $i = strrpos( $path, '.' );
+               $ext = $i ? substr( $path, $i + 1 ) : '';
+
+               if ( $case === 'lowercase' ) {
+                       $ext = strtolower( $ext );
+               } elseif ( $case === 'uppercase' ) {
+                       $ext = strtoupper( $ext );
+               }
+
+               return $ext;
+       }
+
+       /**
+        * Check if a relative path has no directory traversals
+        *
+        * @param string $path
+        * @return bool
+        * @since 1.20
+        */
+       final public static function isPathTraversalFree( $path ) {
+               return ( self::normalizeContainerPath( $path ) !== null );
+       }
+
+       /**
+        * Build a Content-Disposition header value per RFC 6266.
+        *
+        * @param string $type One of (attachment, inline)
+        * @param string $filename Suggested file name (should not contain slashes)
+        * @throws FileBackendError
+        * @return string
+        * @since 1.20
+        */
+       final public static function makeContentDisposition( $type, $filename = '' ) {
+               $parts = [];
+
+               $type = strtolower( $type );
+               if ( !in_array( $type, [ 'inline', 'attachment' ] ) ) {
+                       throw new InvalidArgumentException( "Invalid Content-Disposition type '$type'." );
+               }
+               $parts[] = $type;
+
+               if ( strlen( $filename ) ) {
+                       $parts[] = "filename*=UTF-8''" . rawurlencode( basename( $filename ) );
+               }
+
+               return implode( ';', $parts );
+       }
+
+       /**
+        * Validate and normalize a relative storage path.
+        * Null is returned if the path involves directory traversal.
+        * Traversal is insecure for FS backends and broken for others.
+        *
+        * This uses the same traversal protection as Title::secureAndSplit().
+        *
+        * @param string $path Storage path relative to a container
+        * @return string|null
+        */
+       final protected static function normalizeContainerPath( $path ) {
+               // Normalize directory separators
+               $path = strtr( $path, '\\', '/' );
+               // Collapse any consecutive directory separators
+               $path = preg_replace( '![/]{2,}!', '/', $path );
+               // Remove any leading directory separator
+               $path = ltrim( $path, '/' );
+               // Use the same traversal protection as Title::secureAndSplit()
+               if ( strpos( $path, '.' ) !== false ) {
+                       if (
+                               $path === '.' ||
+                               $path === '..' ||
+                               strpos( $path, './' ) === 0 ||
+                               strpos( $path, '../' ) === 0 ||
+                               strpos( $path, '/./' ) !== false ||
+                               strpos( $path, '/../' ) !== false
+                       ) {
+                               return null;
+                       }
+               }
+
+               return $path;
+       }
+
+       /**
+        * Yields the result of the status wrapper callback on either:
+        *   - StatusValue::newGood() if this method is called without parameters
+        *   - StatusValue::newFatal() with all parameters to this method if passed in
+        *
+        * @param ... string
+        * @return StatusValue
+        */
+       final protected function newStatus() {
+               $args = func_get_args();
+               if ( count( $args ) ) {
+                       $sv = call_user_func_array( [ 'StatusValue', 'newFatal' ], $args );
+               } else {
+                       $sv = StatusValue::newGood();
+               }
+
+               return $this->wrapStatus( $sv );
+       }
+
+       /**
+        * @param StatusValue $sv
+        * @return StatusValue Modified status or StatusValue subclass
+        */
+       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' ], [ $section ] );
+               }
+
+               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/FileBackendError.php b/includes/libs/filebackend/FileBackendError.php
new file mode 100644 (file)
index 0000000..e233535
--- /dev/null
@@ -0,0 +1,9 @@
+<?php
+/**
+ * File backend exception for checked exceptions (e.g. I/O errors)
+ *
+ * @ingroup FileBackend
+ * @since 1.22
+ */
+class FileBackendError extends Exception {
+}
diff --git a/includes/libs/filebackend/FileBackendMultiWrite.php b/includes/libs/filebackend/FileBackendMultiWrite.php
new file mode 100644 (file)
index 0000000..212e84f
--- /dev/null
@@ -0,0 +1,756 @@
+<?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 ) {
+               $status = $this->newStatus();
+               // 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] );
+
+               $status->merge( $this->backends[$index]->concatenate( $realParams ) );
+
+               return $status;
+       }
+
+       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/TempFSFile.php b/includes/libs/filebackend/TempFSFile.php
new file mode 100644 (file)
index 0000000..fed6812
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+/**
+ * Location holder of files stored temporarily
+ *
+ * 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
+ */
+
+/**
+ * This class is used to hold the location and do limited manipulation
+ * of files stored temporarily (this will be whatever wfTempDir() returns)
+ *
+ * @ingroup FileBackend
+ */
+class TempFSFile extends FSFile {
+       /** @var bool Garbage collect the temp file */
+       protected $canDelete = false;
+
+       /** @var array Map of (path => 1) for paths to delete on shutdown */
+       protected static $pathsCollect = null;
+
+       public function __construct( $path ) {
+               parent::__construct( $path );
+
+               if ( self::$pathsCollect === null ) {
+                       self::$pathsCollect = [];
+                       register_shutdown_function( [ __CLASS__, 'purgeAllOnShutdown' ] );
+               }
+       }
+
+       /**
+        * Make a new temporary file on the file system.
+        * Temporary files may be purged when the file object falls out of scope.
+        *
+        * @param string $prefix
+        * @param string $extension Optional file extension
+        * @param string|null $tmpDirectory Optional parent directory
+        * @return TempFSFile|null
+        */
+       public static function factory( $prefix, $extension = '', $tmpDirectory = null ) {
+               $ext = ( $extension != '' ) ? ".{$extension}" : '';
+
+               $attempts = 5;
+               while ( $attempts-- ) {
+                       $hex = sprintf( '%06x%06x', mt_rand( 0, 0xffffff ), mt_rand( 0, 0xffffff ) );
+                       if ( !is_string( $tmpDirectory ) ) {
+                               $tmpDirectory = self::getUsableTempDirectory();
+                       }
+                       $path = wfTempDir() . '/' . $prefix . $hex . $ext;
+                       MediaWiki\suppressWarnings();
+                       $newFileHandle = fopen( $path, 'x' );
+                       MediaWiki\restoreWarnings();
+                       if ( $newFileHandle ) {
+                               fclose( $newFileHandle );
+                               $tmpFile = new self( $path );
+                               $tmpFile->autocollect();
+                               // Safely instantiated, end loop.
+                               return $tmpFile;
+                       }
+               }
+
+               // Give up
+               return null;
+       }
+
+       /**
+        * @return string Filesystem path to a temporary directory
+        * @throws RuntimeException
+        */
+       public static function getUsableTempDirectory() {
+               $tmpDir = array_map( 'getenv', [ 'TMPDIR', 'TMP', 'TEMP' ] );
+               $tmpDir[] = sys_get_temp_dir();
+               $tmpDir[] = ini_get( 'upload_tmp_dir' );
+               foreach ( $tmpDir as $tmp ) {
+                       if ( $tmp != '' && is_dir( $tmp ) && is_writable( $tmp ) ) {
+                               return $tmp;
+                       }
+               }
+
+               // PHP on Windows will detect C:\Windows\Temp as not writable even though PHP can write to
+               // it so create a directory within that called 'mwtmp' with a suffix of the user running
+               // the current process.
+               // The user is included as if various scripts are run by different users they will likely
+               // not be able to access each others temporary files.
+               if ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' ) {
+                       $tmp = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'mwtmp-' . get_current_user();
+                       if ( !file_exists( $tmp ) ) {
+                               mkdir( $tmp );
+                       }
+                       if ( is_dir( $tmp ) && is_writable( $tmp ) ) {
+                               return $tmp;
+                       }
+               }
+
+               throw new RuntimeException(
+                       'No writable temporary directory could be found. ' .
+                       'Please explicitly specify a writable directory in configuration.' );
+       }
+
+       /**
+        * Purge this file off the file system
+        *
+        * @return bool Success
+        */
+       public function purge() {
+               $this->canDelete = false; // done
+               MediaWiki\suppressWarnings();
+               $ok = unlink( $this->path );
+               MediaWiki\restoreWarnings();
+
+               unset( self::$pathsCollect[$this->path] );
+
+               return $ok;
+       }
+
+       /**
+        * Clean up the temporary file only after an object goes out of scope
+        *
+        * @param object $object
+        * @return TempFSFile This object
+        */
+       public function bind( $object ) {
+               if ( is_object( $object ) ) {
+                       if ( !isset( $object->tempFSFileReferences ) ) {
+                               // Init first since $object might use __get() and return only a copy variable
+                               $object->tempFSFileReferences = [];
+                       }
+                       $object->tempFSFileReferences[] = $this;
+               }
+
+               return $this;
+       }
+
+       /**
+        * Set flag to not clean up after the temporary file
+        *
+        * @return TempFSFile This object
+        */
+       public function preserve() {
+               $this->canDelete = false;
+
+               unset( self::$pathsCollect[$this->path] );
+
+               return $this;
+       }
+
+       /**
+        * Set flag clean up after the temporary file
+        *
+        * @return TempFSFile This object
+        */
+       public function autocollect() {
+               $this->canDelete = true;
+
+               self::$pathsCollect[$this->path] = 1;
+
+               return $this;
+       }
+
+       /**
+        * Try to make sure that all files are purged on error
+        *
+        * This method should only be called internally
+        */
+       public static function purgeAllOnShutdown() {
+               foreach ( self::$pathsCollect as $path ) {
+                       MediaWiki\suppressWarnings();
+                       unlink( $path );
+                       MediaWiki\restoreWarnings();
+               }
+       }
+
+       /**
+        * Cleans up after the temporary file by deleting it
+        */
+       function __destruct() {
+               if ( $this->canDelete ) {
+                       $this->purge();
+               }
+       }
+}
diff --git a/includes/libs/filebackend/filejournal/FileJournal.php b/includes/libs/filebackend/filejournal/FileJournal.php
new file mode 100644 (file)
index 0000000..116c303
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+/**
+ * @defgroup FileJournal File journal
+ * @ingroup FileBackend
+ */
+
+/**
+ * File operation journaling.
+ *
+ * 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 FileJournal
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Class for handling file operation journaling.
+ *
+ * Subclasses should avoid throwing exceptions at all costs.
+ *
+ * @ingroup FileJournal
+ * @since 1.20
+ */
+abstract class FileJournal {
+       /** @var string */
+       protected $backend;
+
+       /** @var int */
+       protected $ttlDays;
+
+       /**
+        * Construct a new instance from configuration.
+        *
+        * @param array $config Includes:
+        *     'ttlDays' : days to keep log entries around (false means "forever")
+        */
+       protected function __construct( array $config ) {
+               $this->ttlDays = isset( $config['ttlDays'] ) ? $config['ttlDays'] : false;
+       }
+
+       /**
+        * Create an appropriate FileJournal object from config
+        *
+        * @param array $config
+        * @param string $backend A registered file backend name
+        * @throws Exception
+        * @return FileJournal
+        */
+       final public static function factory( array $config, $backend ) {
+               $class = $config['class'];
+               $jrn = new $class( $config );
+               if ( !$jrn instanceof self ) {
+                       throw new InvalidArgumentException( "Class given is not an instance of FileJournal." );
+               }
+               $jrn->backend = $backend;
+
+               return $jrn;
+       }
+
+       /**
+        * Get a statistically unique ID string
+        *
+        * @return string <9 char TS_MW timestamp in base 36><22 random base 36 chars>
+        */
+       final public function getTimestampedUUID() {
+               $s = '';
+               for ( $i = 0; $i < 5; $i++ ) {
+                       $s .= mt_rand( 0, 2147483647 );
+               }
+               $s = Wikimedia\base_convert( sha1( $s ), 16, 36, 31 );
+
+               return substr( Wikimedia\base_convert( wfTimestamp( TS_MW ), 10, 36, 9 ) . $s, 0, 31 );
+       }
+
+       /**
+        * Log changes made by a batch file operation.
+        *
+        * @param array $entries List of file operations (each an array of parameters) which contain:
+        *     op      : Basic operation name (create, update, delete)
+        *     path    : The storage path of the file
+        *     newSha1 : The final base 36 SHA-1 of the file
+        *   Note that 'false' should be used as the SHA-1 for non-existing files.
+        * @param string $batchId UUID string that identifies the operation batch
+        * @return StatusValue
+        */
+       final public function logChangeBatch( array $entries, $batchId ) {
+               if ( !count( $entries ) ) {
+                       return StatusValue::newGood();
+               }
+
+               return $this->doLogChangeBatch( $entries, $batchId );
+       }
+
+       /**
+        * @see FileJournal::logChangeBatch()
+        *
+        * @param array $entries List of file operations (each an array of parameters)
+        * @param string $batchId UUID string that identifies the operation batch
+        * @return StatusValue
+        */
+       abstract protected function doLogChangeBatch( array $entries, $batchId );
+
+       /**
+        * Get the position ID of the latest journal entry
+        *
+        * @return int|bool
+        */
+       final public function getCurrentPosition() {
+               return $this->doGetCurrentPosition();
+       }
+
+       /**
+        * @see FileJournal::getCurrentPosition()
+        * @return int|bool
+        */
+       abstract protected function doGetCurrentPosition();
+
+       /**
+        * Get the position ID of the latest journal entry at some point in time
+        *
+        * @param int|string $time Timestamp
+        * @return int|bool
+        */
+       final public function getPositionAtTime( $time ) {
+               return $this->doGetPositionAtTime( $time );
+       }
+
+       /**
+        * @see FileJournal::getPositionAtTime()
+        * @param int|string $time Timestamp
+        * @return int|bool
+        */
+       abstract protected function doGetPositionAtTime( $time );
+
+       /**
+        * Get an array of file change log entries.
+        * A starting change ID and/or limit can be specified.
+        *
+        * @param int $start Starting change ID or null
+        * @param int $limit Maximum number of items to return
+        * @param string &$next Updated to the ID of the next entry.
+        * @return array List of associative arrays, each having:
+        *     id         : unique, monotonic, ID for this change
+        *     batch_uuid : UUID for an operation batch
+        *     backend    : the backend name
+        *     op         : primitive operation (create,update,delete,null)
+        *     path       : affected storage path
+        *     new_sha1   : base 36 sha1 of the new file had the operation succeeded
+        *     timestamp  : TS_MW timestamp of the batch change
+        *   Also, $next is updated to the ID of the next entry.
+        */
+       final public function getChangeEntries( $start = null, $limit = 0, &$next = null ) {
+               $entries = $this->doGetChangeEntries( $start, $limit ? $limit + 1 : 0 );
+               if ( $limit && count( $entries ) > $limit ) {
+                       $last = array_pop( $entries ); // remove the extra entry
+                       $next = $last['id']; // update for next call
+               } else {
+                       $next = null; // end of list
+               }
+
+               return $entries;
+       }
+
+       /**
+        * @see FileJournal::getChangeEntries()
+        * @param int $start
+        * @param int $limit
+        * @return array
+        */
+       abstract protected function doGetChangeEntries( $start, $limit );
+
+       /**
+        * Purge any old log entries
+        *
+        * @return StatusValue
+        */
+       final public function purgeOldLogs() {
+               return $this->doPurgeOldLogs();
+       }
+
+       /**
+        * @see FileJournal::purgeOldLogs()
+        * @return StatusValue
+        */
+       abstract protected function doPurgeOldLogs();
+}
diff --git a/includes/libs/filebackend/filejournal/NullFileJournal.php b/includes/libs/filebackend/filejournal/NullFileJournal.php
new file mode 100644 (file)
index 0000000..8d472ab
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Simple version of FileJournal that does nothing
+ * @since 1.20
+ */
+class NullFileJournal extends FileJournal {
+       /**
+        * @see FileJournal::doLogChangeBatch()
+        * @param array $entries
+        * @param string $batchId
+        * @return StatusValue
+        */
+       protected function doLogChangeBatch( array $entries, $batchId ) {
+               return StatusValue::newGood();
+       }
+
+       /**
+        * @see FileJournal::doGetCurrentPosition()
+        * @return int|bool
+        */
+       protected function doGetCurrentPosition() {
+               return false;
+       }
+
+       /**
+        * @see FileJournal::doGetPositionAtTime()
+        * @param int|string $time Timestamp
+        * @return int|bool
+        */
+       protected function doGetPositionAtTime( $time ) {
+               return false;
+       }
+
+       /**
+        * @see FileJournal::doGetChangeEntries()
+        * @param int $start
+        * @param int $limit
+        * @return array
+        */
+       protected function doGetChangeEntries( $start, $limit ) {
+               return [];
+       }
+
+       /**
+        * @see FileJournal::doPurgeOldLogs()
+        * @return StatusValue
+        */
+       protected function doPurgeOldLogs() {
+               return StatusValue::newGood();
+       }
+}
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..b17b1a0
--- /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; bitfield of IDatabase::DBO_* constants
+        *   - 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 = Database::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();
+               }
+       }
+}
diff --git a/includes/libs/lockmanager/FSLockManager.php b/includes/libs/lockmanager/FSLockManager.php
new file mode 100644 (file)
index 0000000..7f33a0a
--- /dev/null
@@ -0,0 +1,253 @@
+<?php
+/**
+ * Simple version of LockManager based on using FS lock files.
+ *
+ * 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
+ */
+
+/**
+ * Simple version of LockManager based on using FS lock files.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * This should work fine for small sites running off one server.
+ * Do not use this with 'lockDirectory' set to an NFS mount unless the
+ * NFS client is at least version 2.6.12. Otherwise, the BSD flock()
+ * locks will be ignored; see http://nfs.sourceforge.net/#section_d.
+ *
+ * @ingroup LockManager
+ * @since 1.19
+ */
+class FSLockManager extends LockManager {
+       /** @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 string Global dir for all servers */
+       protected $lockDir;
+
+       /** @var array Map of (locked key => lock file handle) */
+       protected $handles = [];
+
+       /** @var bool */
+       protected $isWindows;
+
+       /**
+        * Construct a new instance from configuration.
+        *
+        * @param array $config Includes:
+        *   - lockDirectory : Directory containing the lock files
+        */
+       function __construct( array $config ) {
+               parent::__construct( $config );
+
+               $this->lockDir = $config['lockDirectory'];
+               $this->isWindows = ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' );
+       }
+
+       /**
+        * @see LockManager::doLock()
+        * @param array $paths
+        * @param int $type
+        * @return StatusValue
+        */
+       protected function doLock( array $paths, $type ) {
+               $status = StatusValue::newGood();
+
+               $lockedPaths = []; // files locked in this attempt
+               foreach ( $paths as $path ) {
+                       $status->merge( $this->doSingleLock( $path, $type ) );
+                       if ( $status->isOK() ) {
+                               $lockedPaths[] = $path;
+                       } else {
+                               // Abort and unlock everything
+                               $status->merge( $this->doUnlock( $lockedPaths, $type ) );
+
+                               return $status;
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @see LockManager::doUnlock()
+        * @param array $paths
+        * @param int $type
+        * @return StatusValue
+        */
+       protected function doUnlock( array $paths, $type ) {
+               $status = StatusValue::newGood();
+
+               foreach ( $paths as $path ) {
+                       $status->merge( $this->doSingleUnlock( $path, $type ) );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Lock a single resource key
+        *
+        * @param string $path
+        * @param int $type
+        * @return StatusValue
+        */
+       protected function doSingleLock( $path, $type ) {
+               $status = StatusValue::newGood();
+
+               if ( isset( $this->locksHeld[$path][$type] ) ) {
+                       ++$this->locksHeld[$path][$type];
+               } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
+                       $this->locksHeld[$path][$type] = 1;
+               } else {
+                       if ( isset( $this->handles[$path] ) ) {
+                               $handle = $this->handles[$path];
+                       } else {
+                               MediaWiki\suppressWarnings();
+                               $handle = fopen( $this->getLockPath( $path ), 'a+' );
+                               if ( !$handle ) { // lock dir missing?
+                                       mkdir( $this->lockDir, 0777, true );
+                                       $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
+                               }
+                               MediaWiki\restoreWarnings();
+                       }
+                       if ( $handle ) {
+                               // Either a shared or exclusive lock
+                               $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX;
+                               if ( flock( $handle, $lock | LOCK_NB ) ) {
+                                       // Record this lock as active
+                                       $this->locksHeld[$path][$type] = 1;
+                                       $this->handles[$path] = $handle;
+                               } else {
+                                       fclose( $handle );
+                                       $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                               }
+                       } else {
+                               $status->fatal( 'lockmanager-fail-openlock', $path );
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * Unlock a single resource key
+        *
+        * @param string $path
+        * @param int $type
+        * @return StatusValue
+        */
+       protected function doSingleUnlock( $path, $type ) {
+               $status = StatusValue::newGood();
+
+               if ( !isset( $this->locksHeld[$path] ) ) {
+                       $status->warning( 'lockmanager-notlocked', $path );
+               } elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
+                       $status->warning( 'lockmanager-notlocked', $path );
+               } else {
+                       $handlesToClose = [];
+                       --$this->locksHeld[$path][$type];
+                       if ( $this->locksHeld[$path][$type] <= 0 ) {
+                               unset( $this->locksHeld[$path][$type] );
+                       }
+                       if ( !count( $this->locksHeld[$path] ) ) {
+                               unset( $this->locksHeld[$path] ); // no locks on this path
+                               if ( isset( $this->handles[$path] ) ) {
+                                       $handlesToClose[] = $this->handles[$path];
+                                       unset( $this->handles[$path] );
+                               }
+                       }
+                       // Unlock handles to release locks and delete
+                       // any lock files that end up with no locks on them...
+                       if ( $this->isWindows ) {
+                               // Windows: for any process, including this one,
+                               // calling unlink() on a locked file will fail
+                               $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
+                               $status->merge( $this->pruneKeyLockFiles( $path ) );
+                       } else {
+                               // Unix: unlink() can be used on files currently open by this
+                               // process and we must do so in order to avoid race conditions
+                               $status->merge( $this->pruneKeyLockFiles( $path ) );
+                               $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @param string $path
+        * @param array $handlesToClose
+        * @return StatusValue
+        */
+       private function closeLockHandles( $path, array $handlesToClose ) {
+               $status = StatusValue::newGood();
+               foreach ( $handlesToClose as $handle ) {
+                       if ( !flock( $handle, LOCK_UN ) ) {
+                               $status->fatal( 'lockmanager-fail-releaselock', $path );
+                       }
+                       if ( !fclose( $handle ) ) {
+                               $status->warning( 'lockmanager-fail-closelock', $path );
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * @param string $path
+        * @return StatusValue
+        */
+       private function pruneKeyLockFiles( $path ) {
+               $status = StatusValue::newGood();
+               if ( !isset( $this->locksHeld[$path] ) ) {
+                       # No locks are held for the lock file anymore
+                       if ( !unlink( $this->getLockPath( $path ) ) ) {
+                               $status->warning( 'lockmanager-fail-deletelock', $path );
+                       }
+                       unset( $this->handles[$path] );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Get the path to the lock file for a key
+        * @param string $path
+        * @return string
+        */
+       protected function getLockPath( $path ) {
+               return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock";
+       }
+
+       /**
+        * Make sure remaining locks get cleared for sanity
+        */
+       function __destruct() {
+               while ( count( $this->locksHeld ) ) {
+                       foreach ( $this->locksHeld as $path => $locks ) {
+                               $this->doSingleUnlock( $path, self::LOCK_EX );
+                               $this->doSingleUnlock( $path, self::LOCK_SH );
+                       }
+               }
+       }
+}
diff --git a/includes/libs/lockmanager/LockManager.php b/includes/libs/lockmanager/LockManager.php
new file mode 100644 (file)
index 0000000..bee34dc
--- /dev/null
@@ -0,0 +1,268 @@
+<?php
+/**
+ * @defgroup LockManager Lock management
+ * @ingroup FileBackend
+ */
+use Psr\Log\LoggerInterface;
+use Wikimedia\WaitConditionLoop;
+
+/**
+ * Resource locking handling.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * @brief Class for handling resource locking.
+ *
+ * Locks on resource keys can either be shared or exclusive.
+ *
+ * Implementations must keep track of what is locked by this proccess
+ * in-memory and support nested locking calls (using reference counting).
+ * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op.
+ * Locks should either be non-blocking or have low wait timeouts.
+ *
+ * Subclasses should avoid throwing exceptions at all costs.
+ *
+ * @ingroup LockManager
+ * @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,
+               self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH
+               self::LOCK_EX => self::LOCK_EX
+       ];
+
+       /** @var array Map of (resource path => lock type => count) */
+       protected $locksHeld = [];
+
+       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)
+       const LOCK_EX = 3; // exclusive lock (for writes)
+
+       /** @var int Max expected lock expiry in any context */
+       const MAX_LOCK_TTL = 7200; // 2 hours
+
+       /**
+        * Construct a new instance from configuration
+        *
+        * @param array $config Parameters include:
+        *   - domain  : Domain (usually wiki ID) that all resources are relative to [optional]
+        *   - lockTTL : Age (in seconds) at which resource locks should expire.
+        *               This only applies if locks are not tied to a connection/process.
+        */
+       public function __construct( array $config ) {
+               $this->domain = isset( $config['domain'] ) ? $config['domain'] : 'global';
+               if ( isset( $config['lockTTL'] ) ) {
+                       $this->lockTTL = max( 5, $config['lockTTL'] );
+               } elseif ( PHP_SAPI === 'cli' ) {
+                       $this->lockTTL = 3600;
+               } else {
+                       $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
+                       $this->lockTTL = max( 5 * 60, 2 * (int)$met );
+               }
+
+               // Upper bound on how long to keep lock structures around. This is useful when setting
+               // TTLs, as the "lockTTL" value may vary based on CLI mode and app server group. This is
+               // a "safe" value that can be used to avoid clobbering other locks that use high TTLs.
+               $this->lockTTL = min( $this->lockTTL, self::MAX_LOCK_TTL );
+
+               $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();
+       }
+
+       /**
+        * Lock the resources at the given abstract paths
+        *
+        * @param array $paths List of resource names
+        * @param int $type LockManager::LOCK_* constant
+        * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
+        * @return StatusValue
+        */
+       final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) {
+               return $this->lockByType( [ $type => $paths ], $timeout );
+       }
+
+       /**
+        * Lock the resources at the given abstract paths
+        *
+        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+        * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
+        * @return StatusValue
+        * @since 1.22
+        */
+       final public function lockByType( array $pathsByType, $timeout = 0 ) {
+               $pathsByType = $this->normalizePathsByType( $pathsByType );
+
+               $status = null;
+               $loop = new WaitConditionLoop(
+                       function () use ( &$status, $pathsByType ) {
+                               $status = $this->doLockByType( $pathsByType );
+
+                               return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE;
+                       },
+                       $timeout
+               );
+               $loop->invoke();
+
+               return $status;
+       }
+
+       /**
+        * Unlock the resources at the given abstract paths
+        *
+        * @param array $paths List of paths
+        * @param int $type LockManager::LOCK_* constant
+        * @return StatusValue
+        */
+       final public function unlock( array $paths, $type = self::LOCK_EX ) {
+               return $this->unlockByType( [ $type => $paths ] );
+       }
+
+       /**
+        * Unlock the resources at the given abstract paths
+        *
+        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+        * @return StatusValue
+        * @since 1.22
+        */
+       final public function unlockByType( array $pathsByType ) {
+               $pathsByType = $this->normalizePathsByType( $pathsByType );
+               $status = $this->doUnlockByType( $pathsByType );
+
+               return $status;
+       }
+
+       /**
+        * Get the base 36 SHA-1 of a string, padded to 31 digits.
+        * Before hashing, the path will be prefixed with the domain ID.
+        * This should be used interally for lock key or file names.
+        *
+        * @param string $path
+        * @return string
+        */
+       final protected function sha1Base36Absolute( $path ) {
+               return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 );
+       }
+
+       /**
+        * Get the base 16 SHA-1 of a string, padded to 31 digits.
+        * Before hashing, the path will be prefixed with the domain ID.
+        * This should be used interally for lock key or file names.
+        *
+        * @param string $path
+        * @return string
+        */
+       final protected function sha1Base16Absolute( $path ) {
+               return sha1( "{$this->domain}:{$path}" );
+       }
+
+       /**
+        * Normalize the $paths array by converting LOCK_UW locks into the
+        * appropriate type and removing any duplicated paths for each lock type.
+        *
+        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+        * @return array
+        * @since 1.22
+        */
+       final protected function normalizePathsByType( array $pathsByType ) {
+               $res = [];
+               foreach ( $pathsByType as $type => $paths ) {
+                       $res[$this->lockTypeMap[$type]] = array_unique( $paths );
+               }
+
+               return $res;
+       }
+
+       /**
+        * @see LockManager::lockByType()
+        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+        * @return StatusValue
+        * @since 1.22
+        */
+       protected function doLockByType( array $pathsByType ) {
+               $status = StatusValue::newGood();
+               $lockedByType = []; // map of (type => paths)
+               foreach ( $pathsByType as $type => $paths ) {
+                       $status->merge( $this->doLock( $paths, $type ) );
+                       if ( $status->isOK() ) {
+                               $lockedByType[$type] = $paths;
+                       } else {
+                               // Release the subset of locks that were acquired
+                               foreach ( $lockedByType as $lType => $lPaths ) {
+                                       $status->merge( $this->doUnlock( $lPaths, $lType ) );
+                               }
+                               break;
+                       }
+               }
+
+               return $status;
+       }
+
+       /**
+        * Lock resources with the given keys and lock type
+        *
+        * @param array $paths List of paths
+        * @param int $type LockManager::LOCK_* constant
+        * @return StatusValue
+        */
+       abstract protected function doLock( array $paths, $type );
+
+       /**
+        * @see LockManager::unlockByType()
+        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+        * @return StatusValue
+        * @since 1.22
+        */
+       protected function doUnlockByType( array $pathsByType ) {
+               $status = StatusValue::newGood();
+               foreach ( $pathsByType as $type => $paths ) {
+                       $status->merge( $this->doUnlock( $paths, $type ) );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Unlock resources with the given keys and lock type
+        *
+        * @param array $paths List of paths
+        * @param int $type LockManager::LOCK_* constant
+        * @return StatusValue
+        */
+       abstract protected function doUnlock( array $paths, $type );
+}
diff --git a/includes/libs/lockmanager/MemcLockManager.php b/includes/libs/lockmanager/MemcLockManager.php
new file mode 100644 (file)
index 0000000..aecdf60
--- /dev/null
@@ -0,0 +1,356 @@
+<?php
+/**
+ * Version of LockManager based on using memcached 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
+ */
+use Wikimedia\WaitConditionLoop;
+
+/**
+ * Manage locks using memcached servers.
+ *
+ * Version of LockManager based on using memcached 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 memcached.
+ * A majority of peers must agree for a lock to be acquired.
+ *
+ * @ingroup LockManager
+ * @since 1.20
+ */
+class MemcLockManager 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 MemcachedBagOStuff[] Map of (server name => MemcachedBagOStuff) */
+       protected $cacheServers = [];
+       /** @var HashBagOStuff Server status cache */
+       protected $statusCache;
+
+       /**
+        * 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.
+        *   - memcConfig   : Configuration array for MemcachedBagOStuff::construct() with an
+        *                    additional 'class' parameter specifying which MemcachedBagOStuff
+        *                    subclass to use. The server names will be injected. [optional]
+        * @throws Exception
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+
+               // Sanitize srvsByBucket config to prevent PHP errors
+               $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
+               $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
+
+               $memcConfig = isset( $config['memcConfig'] ) ? $config['memcConfig'] : [];
+               $memcConfig += [ 'class' => 'MemcachedPhpBagOStuff' ]; // default
+
+               $class = $memcConfig['class'];
+               if ( !is_subclass_of( $class, 'MemcachedBagOStuff' ) ) {
+                       throw new InvalidArgumentException( "$class is not of type MemcachedBagOStuff." );
+               }
+
+               foreach ( $config['lockServers'] as $name => $address ) {
+                       $params = [ 'servers' => [ $address ] ] + $memcConfig;
+                       $this->cacheServers[$name] = new $class( $params );
+               }
+
+               $this->statusCache = new HashBagOStuff();
+       }
+
+       protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
+               $status = StatusValue::newGood();
+
+               $memc = $this->getCache( $lockSrv );
+               // List of affected paths
+               $paths = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+               $paths = array_unique( $paths );
+               // List of affected lock record keys
+               $keys = array_map( [ $this, 'recordKeyForPath' ], $paths );
+
+               // Lock all of the active lock record keys...
+               if ( !$this->acquireMutexes( $memc, $keys ) ) {
+                       foreach ( $paths as $path ) {
+                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                       }
+
+                       return $status;
+               }
+
+               // Fetch all the existing lock records...
+               $lockRecords = $memc->getMulti( $keys );
+
+               $now = time();
+               // Check if the requested locks conflict with existing ones...
+               foreach ( $pathsByType as $type => $paths ) {
+                       foreach ( $paths as $path ) {
+                               $locksKey = $this->recordKeyForPath( $path );
+                               $locksHeld = isset( $lockRecords[$locksKey] )
+                                       ? self::sanitizeLockArray( $lockRecords[$locksKey] )
+                                       : self::newLockArray(); // init
+                               foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) {
+                                       if ( $expiry < $now ) { // stale?
+                                               unset( $locksHeld[self::LOCK_EX][$session] );
+                                       } elseif ( $session !== $this->session ) {
+                                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                                       }
+                               }
+                               if ( $type === self::LOCK_EX ) {
+                                       foreach ( $locksHeld[self::LOCK_SH] as $session => $expiry ) {
+                                               if ( $expiry < $now ) { // stale?
+                                                       unset( $locksHeld[self::LOCK_SH][$session] );
+                                               } elseif ( $session !== $this->session ) {
+                                                       $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                                               }
+                                       }
+                               }
+                               if ( $status->isOK() ) {
+                                       // Register the session in the lock record array
+                                       $locksHeld[$type][$this->session] = $now + $this->lockTTL;
+                                       // We will update this record if none of the other locks conflict
+                                       $lockRecords[$locksKey] = $locksHeld;
+                               }
+                       }
+               }
+
+               // If there were no lock conflicts, update all the lock records...
+               if ( $status->isOK() ) {
+                       foreach ( $paths as $path ) {
+                               $locksKey = $this->recordKeyForPath( $path );
+                               $locksHeld = $lockRecords[$locksKey];
+                               $ok = $memc->set( $locksKey, $locksHeld, self::MAX_LOCK_TTL );
+                               if ( !$ok ) {
+                                       $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                               } else {
+                                       $this->logger->debug( __METHOD__ . ": acquired lock on key $locksKey.\n" );
+                               }
+                       }
+               }
+
+               // Unlock all of the active lock record keys...
+               $this->releaseMutexes( $memc, $keys );
+
+               return $status;
+       }
+
+       protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
+               $status = StatusValue::newGood();
+
+               $memc = $this->getCache( $lockSrv );
+               // List of affected paths
+               $paths = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+               $paths = array_unique( $paths );
+               // List of affected lock record keys
+               $keys = array_map( [ $this, 'recordKeyForPath' ], $paths );
+
+               // Lock all of the active lock record keys...
+               if ( !$this->acquireMutexes( $memc, $keys ) ) {
+                       foreach ( $paths as $path ) {
+                               $status->fatal( 'lockmanager-fail-releaselock', $path );
+                       }
+
+                       return $status;
+               }
+
+               // Fetch all the existing lock records...
+               $lockRecords = $memc->getMulti( $keys );
+
+               // Remove the requested locks from all records...
+               foreach ( $pathsByType as $type => $paths ) {
+                       foreach ( $paths as $path ) {
+                               $locksKey = $this->recordKeyForPath( $path ); // lock record
+                               if ( !isset( $lockRecords[$locksKey] ) ) {
+                                       $status->warning( 'lockmanager-fail-releaselock', $path );
+                                       continue; // nothing to do
+                               }
+                               $locksHeld = $this->sanitizeLockArray( $lockRecords[$locksKey] );
+                               if ( isset( $locksHeld[$type][$this->session] ) ) {
+                                       unset( $locksHeld[$type][$this->session] ); // unregister this session
+                                       $lockRecords[$locksKey] = $locksHeld;
+                               } else {
+                                       $status->warning( 'lockmanager-fail-releaselock', $path );
+                               }
+                       }
+               }
+
+               // Persist the new lock record values...
+               foreach ( $paths as $path ) {
+                       $locksKey = $this->recordKeyForPath( $path );
+                       if ( !isset( $lockRecords[$locksKey] ) ) {
+                               continue; // nothing to do
+                       }
+                       $locksHeld = $lockRecords[$locksKey];
+                       if ( $locksHeld === $this->newLockArray() ) {
+                               $ok = $memc->delete( $locksKey );
+                       } else {
+                               $ok = $memc->set( $locksKey, $locksHeld, self::MAX_LOCK_TTL );
+                       }
+                       if ( $ok ) {
+                               $this->logger->debug( __METHOD__ . ": released lock on key $locksKey.\n" );
+                       } else {
+                               $status->fatal( 'lockmanager-fail-releaselock', $path );
+                       }
+               }
+
+               // Unlock all of the active lock record keys...
+               $this->releaseMutexes( $memc, $keys );
+
+               return $status;
+       }
+
+       /**
+        * @see QuorumLockManager::releaseAllLocks()
+        * @return StatusValue
+        */
+       protected function releaseAllLocks() {
+               return StatusValue::newGood(); // not supported
+       }
+
+       /**
+        * @see QuorumLockManager::isServerUp()
+        * @param string $lockSrv
+        * @return bool
+        */
+       protected function isServerUp( $lockSrv ) {
+               return (bool)$this->getCache( $lockSrv );
+       }
+
+       /**
+        * Get the MemcachedBagOStuff object for a $lockSrv
+        *
+        * @param string $lockSrv Server name
+        * @return MemcachedBagOStuff|null
+        */
+       protected function getCache( $lockSrv ) {
+               if ( !isset( $this->cacheServers[$lockSrv] ) ) {
+                       throw new InvalidArgumentException( "Invalid cache server '$lockSrv'." );
+               }
+
+               $online = $this->statusCache->get( "online:$lockSrv" );
+               if ( $online === false ) {
+                       $online = $this->cacheServers[$lockSrv]->set( __CLASS__ . ':ping', 1, 1 );
+                       if ( !$online ) { // server down?
+                               $this->logger->warning( __METHOD__ . ": Could not contact $lockSrv." );
+                       }
+                       $this->statusCache->set( "online:$lockSrv", (int)$online, 30 );
+               }
+
+               return $online ? $this->cacheServers[$lockSrv] : null;
+       }
+
+       /**
+        * @param string $path
+        * @return string
+        */
+       protected function recordKeyForPath( $path ) {
+               return implode( ':', [ __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ] );
+       }
+
+       /**
+        * @return array An empty lock structure for a key
+        */
+       protected function newLockArray() {
+               return [ self::LOCK_SH => [], self::LOCK_EX => [] ];
+       }
+
+       /**
+        * @param array $a
+        * @return array An empty lock structure for a key
+        */
+       protected function sanitizeLockArray( $a ) {
+               if ( is_array( $a ) && isset( $a[self::LOCK_EX] ) && isset( $a[self::LOCK_SH] ) ) {
+                       return $a;
+               }
+
+               $this->logger->error( __METHOD__ . ": reset invalid lock array." );
+
+               return $this->newLockArray();
+       }
+
+       /**
+        * @param MemcachedBagOStuff $memc
+        * @param array $keys List of keys to acquire
+        * @return bool
+        */
+       protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
+               $lockedKeys = [];
+
+               // Acquire the keys in lexicographical order, to avoid deadlock problems.
+               // If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has.
+               sort( $keys );
+
+               // Try to quickly loop to acquire the keys, but back off after a few rounds.
+               // This reduces memcached spam, especially in the rare case where a server acquires
+               // some lock keys and dies without releasing them. Lock keys expire after a few minutes.
+               $loop = new WaitConditionLoop(
+                       function () use ( $memc, $keys, &$lockedKeys ) {
+                               foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
+                                       if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
+                                               $lockedKeys[] = $key;
+                                       }
+                               }
+
+                               return array_diff( $keys, $lockedKeys )
+                                       ? WaitConditionLoop::CONDITION_CONTINUE
+                                       : true;
+                       },
+                       3.0 // timeout
+               );
+               $loop->invoke();
+
+               if ( count( $lockedKeys ) != count( $keys ) ) {
+                       $this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * @param MemcachedBagOStuff $memc
+        * @param array $keys List of acquired keys
+        */
+       protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) {
+               foreach ( $keys as $key ) {
+                       $memc->delete( "$key:mutex" );
+               }
+       }
+
+       /**
+        * Make sure remaining locks get cleared for sanity
+        */
+       function __destruct() {
+               while ( count( $this->locksHeld ) ) {
+                       foreach ( $this->locksHeld as $path => $locks ) {
+                               $this->doUnlock( [ $path ], self::LOCK_EX );
+                               $this->doUnlock( [ $path ], self::LOCK_SH );
+                       }
+               }
+       }
+}
diff --git a/includes/libs/lockmanager/NullLockManager.php b/includes/libs/lockmanager/NullLockManager.php
new file mode 100644 (file)
index 0000000..5ad558f
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Resource locking handling.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Simple version of LockManager that does nothing
+ * @since 1.19
+ */
+class NullLockManager extends LockManager {
+       protected function doLock( array $paths, $type ) {
+               return StatusValue::newGood();
+       }
+
+       protected function doUnlock( array $paths, $type ) {
+               return StatusValue::newGood();
+       }
+}
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/QuorumLockManager.php b/includes/libs/lockmanager/QuorumLockManager.php
new file mode 100644 (file)
index 0000000..a89d864
--- /dev/null
@@ -0,0 +1,248 @@
+<?php
+/**
+ * Version of LockManager that uses a quorum from peer servers for 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 that uses a quorum from peer servers for locks.
+ * The resource space can also be sharded into separate peer groups.
+ *
+ * @ingroup LockManager
+ * @since 1.20
+ */
+abstract class QuorumLockManager extends LockManager {
+       /** @var array Map of bucket indexes to peer server lists */
+       protected $srvsByBucket = []; // (bucket index => (lsrv1, lsrv2, ...))
+
+       /** @var array Map of degraded buckets */
+       protected $degradedBuckets = []; // (bucket index => UNIX timestamp)
+
+       final protected function doLock( array $paths, $type ) {
+               return $this->doLockByType( [ $type => $paths ] );
+       }
+
+       final protected function doUnlock( array $paths, $type ) {
+               return $this->doUnlockByType( [ $type => $paths ] );
+       }
+
+       protected function doLockByType( array $pathsByType ) {
+               $status = StatusValue::newGood();
+
+               $pathsToLock = []; // (bucket => type => paths)
+               // Get locks that need to be acquired (buckets => locks)...
+               foreach ( $pathsByType as $type => $paths ) {
+                       foreach ( $paths as $path ) {
+                               if ( isset( $this->locksHeld[$path][$type] ) ) {
+                                       ++$this->locksHeld[$path][$type];
+                               } else {
+                                       $bucket = $this->getBucketFromPath( $path );
+                                       $pathsToLock[$bucket][$type][] = $path;
+                               }
+                       }
+               }
+
+               $lockedPaths = []; // files locked in this attempt (type => paths)
+               // Attempt to acquire these locks...
+               foreach ( $pathsToLock as $bucket => $pathsToLockByType ) {
+                       // Try to acquire the locks for this bucket
+                       $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) );
+                       if ( !$status->isOK() ) {
+                               $status->merge( $this->doUnlockByType( $lockedPaths ) );
+
+                               return $status;
+                       }
+                       // Record these locks as active
+                       foreach ( $pathsToLockByType as $type => $paths ) {
+                               foreach ( $paths as $path ) {
+                                       $this->locksHeld[$path][$type] = 1; // locked
+                                       // Keep track of what locks were made in this attempt
+                                       $lockedPaths[$type][] = $path;
+                               }
+                       }
+               }
+
+               return $status;
+       }
+
+       protected function doUnlockByType( array $pathsByType ) {
+               $status = StatusValue::newGood();
+
+               $pathsToUnlock = []; // (bucket => type => paths)
+               foreach ( $pathsByType as $type => $paths ) {
+                       foreach ( $paths as $path ) {
+                               if ( !isset( $this->locksHeld[$path][$type] ) ) {
+                                       $status->warning( 'lockmanager-notlocked', $path );
+                               } else {
+                                       --$this->locksHeld[$path][$type];
+                                       // Reference count the locks held and release locks when zero
+                                       if ( $this->locksHeld[$path][$type] <= 0 ) {
+                                               unset( $this->locksHeld[$path][$type] );
+                                               $bucket = $this->getBucketFromPath( $path );
+                                               $pathsToUnlock[$bucket][$type][] = $path;
+                                       }
+                                       if ( !count( $this->locksHeld[$path] ) ) {
+                                               unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
+                                       }
+                               }
+                       }
+               }
+
+               // Remove these specific locks if possible, or at least release
+               // all locks once this process is currently not holding any locks.
+               foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) {
+                       $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) );
+               }
+               if ( !count( $this->locksHeld ) ) {
+                       $status->merge( $this->releaseAllLocks() );
+                       $this->degradedBuckets = []; // safe to retry the normal quorum
+               }
+
+               return $status;
+       }
+
+       /**
+        * Attempt to acquire locks with the peers for a bucket.
+        * This is all or nothing; if any key is locked then this totally fails.
+        *
+        * @param int $bucket
+        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+        * @return StatusValue
+        */
+       final protected function doLockingRequestBucket( $bucket, array $pathsByType ) {
+               $status = StatusValue::newGood();
+
+               $yesVotes = 0; // locks made on trustable servers
+               $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
+               $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
+               // Get votes for each peer, in order, until we have enough...
+               foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
+                       if ( !$this->isServerUp( $lockSrv ) ) {
+                               --$votesLeft;
+                               $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
+                               $this->degradedBuckets[$bucket] = time();
+                               continue; // server down?
+                       }
+                       // Attempt to acquire the lock on this peer
+                       $status->merge( $this->getLocksOnServer( $lockSrv, $pathsByType ) );
+                       if ( !$status->isOK() ) {
+                               return $status; // vetoed; resource locked
+                       }
+                       ++$yesVotes; // success for this peer
+                       if ( $yesVotes >= $quorum ) {
+                               return $status; // lock obtained
+                       }
+                       --$votesLeft;
+                       $votesNeeded = $quorum - $yesVotes;
+                       if ( $votesNeeded > $votesLeft ) {
+                               break; // short-circuit
+                       }
+               }
+               // At this point, we must not have met the quorum
+               $status->setResult( false );
+
+               return $status;
+       }
+
+       /**
+        * Attempt to release locks with the peers for a bucket
+        *
+        * @param int $bucket
+        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+        * @return StatusValue
+        */
+       final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) {
+               $status = StatusValue::newGood();
+
+               $yesVotes = 0; // locks freed on trustable servers
+               $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
+               $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
+               $isDegraded = isset( $this->degradedBuckets[$bucket] ); // not the normal quorum?
+               foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
+                       if ( !$this->isServerUp( $lockSrv ) ) {
+                               $status->warning( 'lockmanager-fail-svr-release', $lockSrv );
+                       } else {
+                               // Attempt to release the lock on this peer
+                               $status->merge( $this->freeLocksOnServer( $lockSrv, $pathsByType ) );
+                               ++$yesVotes; // success for this peer
+                               // Normally the first peers form the quorum, and the others are ignored.
+                               // Ignore them in this case, but not when an alternative quorum was used.
+                               if ( $yesVotes >= $quorum && !$isDegraded ) {
+                                       break; // lock released
+                               }
+                       }
+               }
+               // Set a bad StatusValue if the quorum was not met.
+               // Assumes the same "up" servers as during the acquire step.
+               $status->setResult( $yesVotes >= $quorum );
+
+               return $status;
+       }
+
+       /**
+        * Get the bucket for resource path.
+        * This should avoid throwing any exceptions.
+        *
+        * @param string $path
+        * @return int
+        */
+       protected function getBucketFromPath( $path ) {
+               $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
+               return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket );
+       }
+
+       /**
+        * Check if a lock server is up.
+        * This should process cache results to reduce RTT.
+        *
+        * @param string $lockSrv
+        * @return bool
+        */
+       abstract protected function isServerUp( $lockSrv );
+
+       /**
+        * Get a connection to a lock server and acquire locks
+        *
+        * @param string $lockSrv
+        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+        * @return StatusValue
+        */
+       abstract protected function getLocksOnServer( $lockSrv, array $pathsByType );
+
+       /**
+        * Get a connection to a lock server and release locks on $paths.
+        *
+        * Subclasses must effectively implement this or releaseAllLocks().
+        *
+        * @param string $lockSrv
+        * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
+        * @return StatusValue
+        */
+       abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType );
+
+       /**
+        * Release all locks that this session is holding.
+        *
+        * Subclasses must effectively implement this or freeLocksOnServer().
+        *
+        * @return StatusValue
+        */
+       abstract protected function releaseAllLocks();
+}
diff --git a/includes/libs/lockmanager/RedisLockManager.php b/includes/libs/lockmanager/RedisLockManager.php
new file mode 100644 (file)
index 0000000..ea9dde7
--- /dev/null
@@ -0,0 +1,276 @@
+<?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, rMaxTTL, 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,rMaxTTL)
+                               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]
+                                               self::MAX_LOCK_TTL, // ARGV[3]
+                                               time() // ARGV[4]
+                                       ]
+                               ),
+                               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/lockmanager/ScopedLock.php b/includes/libs/lockmanager/ScopedLock.php
new file mode 100644 (file)
index 0000000..ac8bee8
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+/**
+ * Resource locking handling.
+ *
+ * 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
+ * @author Aaron Schulz
+ */
+
+/**
+ * Self-releasing locks
+ *
+ * LockManager helper class to handle scoped locks, which
+ * release when an object is destroyed or goes out of scope.
+ *
+ * @ingroup LockManager
+ * @since 1.19
+ */
+class ScopedLock {
+       /** @var LockManager */
+       protected $manager;
+
+       /** @var StatusValue */
+       protected $status;
+
+       /** @var array Map of lock types to resource paths */
+       protected $pathsByType;
+
+       /**
+        * @param LockManager $manager
+        * @param array $pathsByType Map of lock types to path lists
+        * @param StatusValue $status
+        */
+       protected function __construct(
+               LockManager $manager, array $pathsByType, StatusValue $status
+       ) {
+               $this->manager = $manager;
+               $this->pathsByType = $pathsByType;
+               $this->status = $status;
+       }
+
+       /**
+        * Get a ScopedLock object representing a lock on resource paths.
+        * Any locks are released once this object goes out of scope.
+        * The StatusValue object is updated with any errors or warnings.
+        *
+        * @param LockManager $manager
+        * @param array $paths List of storage paths or map of lock types to path lists
+        * @param int|string $type LockManager::LOCK_* constant or "mixed" and $paths
+        *   can be a map of types to paths (since 1.22). Otherwise $type should be an
+        *   integer and $paths should be a list of paths.
+        * @param StatusValue $status
+        * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.22)
+        * @return ScopedLock|null Returns null on failure
+        */
+       public static function factory(
+               LockManager $manager, array $paths, $type, StatusValue $status, $timeout = 0
+       ) {
+               $pathsByType = is_integer( $type ) ? [ $type => $paths ] : $paths;
+               $lockStatus = $manager->lockByType( $pathsByType, $timeout );
+               $status->merge( $lockStatus );
+               if ( $lockStatus->isOK() ) {
+                       return new self( $manager, $pathsByType, $status );
+               }
+
+               return null;
+       }
+
+       /**
+        * Release a scoped lock and set any errors in the attatched StatusValue object.
+        * This is useful for early release of locks before function scope is destroyed.
+        * This is the same as setting the lock object to null.
+        *
+        * @param ScopedLock $lock
+        * @since 1.21
+        */
+       public static function release( ScopedLock &$lock = null ) {
+               $lock = null;
+       }
+
+       /**
+        * Release the locks when this goes out of scope
+        */
+       function __destruct() {
+               $wasOk = $this->status->isOK();
+               $this->status->merge( $this->manager->unlockByType( $this->pathsByType ) );
+               if ( $wasOk ) {
+                       // Make sure StatusValue is OK, despite any unlockFiles() fatals
+                       $this->status->setResult( true, $this->status->value );
+               }
+       }
+}
index 8f70fc7..9bfcee7 100644 (file)
@@ -75,25 +75,35 @@ class APCBagOStuff extends BagOStuff {
        }
 
        protected function doGet( $key, $flags = 0 ) {
-               $val = apc_fetch( $key . self::KEY_SUFFIX );
+               return $this->getUnserialize(
+                       apc_fetch( $key . self::KEY_SUFFIX )
+               );
+       }
 
-               if ( is_string( $val ) && !$this->nativeSerialize ) {
-                       $val = $this->isInteger( $val )
-                               ? intval( $val )
-                               : unserialize( $val );
+       protected function getUnserialize( $value ) {
+               if ( is_string( $value ) && !$this->nativeSerialize ) {
+                       $value = $this->isInteger( $value )
+                               ? intval( $value )
+                               : unserialize( $value );
                }
-
-               return $val;
+               return $value;
        }
 
        public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+               apc_store(
+                       $key . self::KEY_SUFFIX,
+                       $this->setSerialize( $value ),
+                       $exptime
+               );
+
+               return true;
+       }
+
+       protected function setSerialize( $value ) {
                if ( !$this->nativeSerialize && !$this->isInteger( $value ) ) {
                        $value = serialize( $value );
                }
-
-               apc_store( $key . self::KEY_SUFFIX, $value, $exptime );
-
-               return true;
+               return $value;
        }
 
        public function delete( $key ) {
diff --git a/includes/libs/objectcache/APCUBagOStuff.php b/includes/libs/objectcache/APCUBagOStuff.php
new file mode 100644 (file)
index 0000000..02b3c92
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Object caching using PHP's APCU accelerator.
+ *
+ * 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 Cache
+ */
+
+/**
+ * This is a wrapper for APCU's shared memory functions
+ *
+ * @ingroup Cache
+ */
+class APCUBagOStuff extends APCBagOStuff {
+       /**
+        * Constructor
+        *
+        * Available parameters are:
+        *   - nativeSerialize:     If true, pass objects to apcu_store(), and trust it
+        *                          to serialize them correctly. If false, serialize
+        *                          all values in PHP.
+        *
+        * @param array $params
+        */
+       public function __construct( array $params = [] ) {
+               parent::__construct( $params );
+       }
+
+       protected function doGet( $key, $flags = 0 ) {
+               return $this->getUnserialize(
+                       apcu_fetch( $key . self::KEY_SUFFIX )
+               );
+       }
+
+       public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+               apcu_store(
+                       $key . self::KEY_SUFFIX,
+                       $this->setSerialize( $value ),
+                       $exptime
+               );
+
+               return true;
+       }
+
+       public function delete( $key ) {
+               apcu_delete( $key . self::KEY_SUFFIX );
+
+               return true;
+       }
+
+       public function incr( $key, $value = 1 ) {
+               /**
+                * @todo When we only support php 7 or higher remove this hack
+                *
+                * https://github.com/krakjoe/apcu/issues/166
+                */
+               if ( apcu_exists( $key . self::KEY_SUFFIX ) ) {
+                       return apcu_inc( $key . self::KEY_SUFFIX, $value );
+               } else {
+                       return apcu_set( $key . self::KEY_SUFFIX, $value );
+               }
+       }
+
+       public function decr( $key, $value = 1 ) {
+               /**
+                * @todo When we only support php 7 or higher remove this hack
+                *
+                * https://github.com/krakjoe/apcu/issues/166
+                */
+               if ( apcu_exists( $key . self::KEY_SUFFIX ) ) {
+                       return apcu_dec( $key . self::KEY_SUFFIX, $value );
+               } else {
+                       return apcu_set( $key . self::KEY_SUFFIX, -$value );
+               }
+       }
+}
index cd79b67..d3deefb 100644 (file)
@@ -29,6 +29,7 @@
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
+use Wikimedia\WaitConditionLoop;
 
 /**
  * interface is intended to be more or less compatible with
index e03cec6..baa3c32 100644 (file)
@@ -20,7 +20,6 @@
  * @file
  * @ingroup Cache
  */
-use Wikimedia\Assert\Assert;
 
 /**
  * Simple store for keeping values in an associative array for the current process.
@@ -46,7 +45,9 @@ class HashBagOStuff extends BagOStuff {
                parent::__construct( $params );
 
                $this->maxCacheKeys = isset( $params['maxKeys'] ) ? $params['maxKeys'] : INF;
-               Assert::parameter( $this->maxCacheKeys > 0, 'maxKeys', 'must be above zero' );
+               if ( $this->maxCacheKeys <= 0 ) {
+                       throw new InvalidArgumentException( '$maxKeys parameter must be above zero' );
+               }
        }
 
        protected function expire( $key ) {
index 6973392..5128d82 100644 (file)
@@ -43,13 +43,11 @@ class MemcachedBagOStuff extends BagOStuff {
         * @return array
         */
        protected function applyDefaultParams( $params ) {
-               if ( !isset( $params['compress_threshold'] ) ) {
-                       $params['compress_threshold'] = 1500;
-               }
-               if ( !isset( $params['connect_timeout'] ) ) {
-                       $params['connect_timeout'] = 0.5;
-               }
-               return $params;
+               return $params + [
+                       'compress_threshold' => 1500,
+                       'connect_timeout' => .5,
+                       'debug' => false
+               ];
        }
 
        protected function doGet( $key, $flags = 0 ) {
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 5f6e324..d7db732 100644 (file)
@@ -88,6 +88,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        /** @var int ERR_* constant for the "last error" registry */
        protected $lastRelayError = self::ERR_NONE;
 
+       /** @var mixed[] Temporary warm-up cache */
+       private $warmupCache = [];
+
        /** Max time expected to pass between delete() and DB commit finishing */
        const MAX_COMMIT_DELAY = 3;
        /** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */
@@ -284,7 +287,14 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                }
 
                // Fetch all of the raw values
-               $wrappedValues = $this->cache->getMulti( array_merge( $valueKeys, $checkKeysFlat ) );
+               $keysGet = array_merge( $valueKeys, $checkKeysFlat );
+               if ( $this->warmupCache ) {
+                       $wrappedValues = array_intersect_key( $this->warmupCache, array_flip( $keysGet ) );
+                       $keysGet = array_diff( $keysGet, array_keys( $wrappedValues ) ); // keys left to fetch
+               } else {
+                       $wrappedValues = [];
+               }
+               $wrappedValues += $this->cache->getMulti( $keysGet );
                // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
                $now = microtime( true );
 
@@ -1016,6 +1026,95 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                return $value;
        }
 
+       /**
+        * Method to fetch/regenerate multiple cache keys at once
+        *
+        * This works the same as getWithSetCallback() except:
+        *   - a) The $keys argument expects the result of WANObjectCache::makeMultiKeys()
+        *   - b) The $callback argument expects a callback taking the following arguments:
+        *         - $id: ID of an entity to query
+        *         - $oldValue : the prior cache value or false if none was present
+        *         - &$ttl : a reference to the new value TTL in seconds
+        *         - &$setOpts : a reference to options for set() which can be altered
+        *         - $oldAsOf : generation UNIX timestamp of $oldValue or null if not present
+        *        Aside from the additional $id argument, the other arguments function the same
+        *        way they do in getWithSetCallback().
+        *   - c) The return value is a map of (cache key => value) in the order of $keyedIds
+        *
+        * @see WANObjectCache::getWithSetCallback()
+        *
+        * Example usage:
+        * @code
+        *     $rows = $cache->getMultiWithSetCallback(
+        *         // Map of cache keys to entitiy IDs
+        *         $cache->makeMultiKeys(
+        *             $this->fileVersionIds(),
+        *             function ( $id, WANObjectCache $cache ) {
+        *                 return $cache->makeKey( 'file-version', $id );
+        *             }
+        *         ),
+        *         // Time-to-live (in seconds)
+        *         $cache::TTL_DAY,
+        *         // Function that derives the new key value
+        *         return function ( $id, $oldValue, &$ttl, array &$setOpts ) {
+        *             $dbr = wfGetDB( DB_REPLICA );
+        *             // Account for any snapshot/replica DB lag
+        *             $setOpts += Database::getCacheSetOptions( $dbr );
+        *
+        *             // Load the row for this file
+        *             $row = $dbr->selectRow( 'file', '*', [ 'id' => $id ], __METHOD__ );
+        *
+        *             return $row ? (array)$row : false;
+        *         },
+        *         [
+        *             // Process cache for 30 seconds
+        *             'pcTTL' => 30,
+        *             // Use a dedicated 500 item cache (initialized on-the-fly)
+        *             'pcGroup' => 'file-versions:500'
+        *         ]
+        *     );
+        *     $files = array_map( [ __CLASS__, 'newFromRow' ], $rows );
+        * @endcode
+        *
+        * @param ArrayIterator $keyedIds Result of WANObjectCache::makeMultiKeys()
+        * @param integer $ttl Seconds to live for key updates
+        * @param callable $callback Callback the yields entity regeneration callbacks
+        * @param array $opts Options map
+        * @return array Map of (cache key => value) in the same order as $keyedIds
+        * @since 1.28
+        */
+       final public function getMultiWithSetCallback(
+               ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
+       ) {
+               $keysWarmUp = iterator_to_array( $keyedIds, true );
+               $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
+               foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
+                       if ( is_int( $i ) ) {
+                               $keysWarmUp[] = $checkKeyOrKeys;
+                       } else {
+                               $keysWarmUp = array_merge( $keysWarmUp, $checkKeyOrKeys );
+                       }
+               }
+
+               $this->warmupCache = $this->cache->getMulti( $keysWarmUp );
+               $this->warmupCache += array_fill_keys( $keysWarmUp, false );
+
+               // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
+               $id = null;
+               $func = function ( $oldValue, &$ttl, array $setOpts, $oldAsOf ) use ( $callback, &$id ) {
+                       return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
+               };
+
+               $values = [];
+               foreach ( $keyedIds as $key => $id ) {
+                       $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
+               }
+
+               $this->warmupCache = [];
+
+               return $values;
+       }
+
        /**
         * @see BagOStuff::makeKey()
         * @param string ... Key component
@@ -1036,6 +1135,21 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
        }
 
+       /**
+        * @param array $entities List of entity IDs
+        * @param callable $keyFunc Callback yielding a key from (entity ID, this WANObjectCache)
+        * @return ArrayIterator Iterator yielding (cache key => entity ID) in $entities order
+        * @since 1.28
+        */
+       public function makeMultiKeys( array $entities, callable $keyFunc ) {
+               $map = [];
+               foreach ( $entities as $entity ) {
+                       $map[$keyFunc( $entity, $this )] = $entity;
+               }
+
+               return new ArrayIterator( $map );
+       }
+
        /**
         * Get the "last error" registered; clearLastError() should be called manually
         * @return int ERR_* class constant for the "last error" registry
diff --git a/includes/libs/rdbms/ChronologyProtector.php b/includes/libs/rdbms/ChronologyProtector.php
new file mode 100644 (file)
index 0000000..88af1db
--- /dev/null
@@ -0,0 +1,326 @@
+<?php
+/**
+ * Generator 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
+ */
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Wikimedia\WaitConditionLoop;
+
+/**
+ * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
+ * Kind of like Hawking's [[Chronology Protection Agency]].
+ */
+class ChronologyProtector implements LoggerAwareInterface {
+       /** @var BagOStuff */
+       protected $store;
+       /** @var LoggerInterface */
+       protected $logger;
+
+       /** @var string Storage key name */
+       protected $key;
+       /** @var string Hash of client parameters */
+       protected $clientId;
+       /** @var float|null Minimum UNIX timestamp of 1+ expected startup positions */
+       protected $waitForPosTime;
+       /** @var int Max seconds to wait on positions to appear */
+       protected $waitForPosTimeout = self::POS_WAIT_TIMEOUT;
+       /** @var bool Whether to no-op all method calls */
+       protected $enabled = true;
+       /** @var bool Whether to check and wait on positions */
+       protected $wait = true;
+
+       /** @var bool Whether the client data was loaded */
+       protected $initialized = false;
+       /** @var DBMasterPos[] Map of (DB master name => position) */
+       protected $startupPositions = [];
+       /** @var DBMasterPos[] Map of (DB master name => position) */
+       protected $shutdownPositions = [];
+       /** @var float[] Map of (DB master name => 1) */
+       protected $shutdownTouchDBs = [];
+
+       /** @var integer Seconds to store positions */
+       const POSITION_TTL = 60;
+       /** @var integer Max time to wait for positions to appear */
+       const POS_WAIT_TIMEOUT = 5;
+
+       /**
+        * @param BagOStuff $store
+        * @param array $client Map of (ip: <IP>, agent: <user-agent>)
+        * @param float $posTime UNIX timestamp
+        * @since 1.27
+        */
+       public function __construct( BagOStuff $store, array $client, $posTime = null ) {
+               $this->store = $store;
+               $this->clientId = md5( $client['ip'] . "\n" . $client['agent'] );
+               $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId );
+               $this->waitForPosTime = $posTime;
+               $this->logger = new \Psr\Log\NullLogger();
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * @param bool $enabled Whether to no-op all method calls
+        * @since 1.27
+        */
+       public function setEnabled( $enabled ) {
+               $this->enabled = $enabled;
+       }
+
+       /**
+        * @param bool $enabled Whether to check and wait on positions
+        * @since 1.27
+        */
+       public function setWaitEnabled( $enabled ) {
+               $this->wait = $enabled;
+       }
+
+       /**
+        * Initialise a ILoadBalancer to give it appropriate chronology protection.
+        *
+        * If the stash has a previous master position recorded, this will try to
+        * make sure that the next query to a replica DB of that master will see changes up
+        * to that position by delaying execution. The delay may timeout and allow stale
+        * data if no non-lagged replica DBs are available.
+        *
+        * @param ILoadBalancer $lb
+        * @return void
+        */
+       public function initLB( ILoadBalancer $lb ) {
+               if ( !$this->enabled || $lb->getServerCount() <= 1 ) {
+                       return; // non-replicated setup or disabled
+               }
+
+               $this->initPositions();
+
+               $masterName = $lb->getServerName( $lb->getWriterIndex() );
+               if ( !empty( $this->startupPositions[$masterName] ) ) {
+                       $pos = $this->startupPositions[$masterName];
+                       $this->logger->info( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
+                       $lb->waitFor( $pos );
+               }
+       }
+
+       /**
+        * Notify the ChronologyProtector that the ILoadBalancer is about to shut
+        * down. Saves replication positions.
+        *
+        * @param ILoadBalancer $lb
+        * @return void
+        */
+       public function shutdownLB( ILoadBalancer $lb ) {
+               if ( !$this->enabled ) {
+                       return; // not enabled
+               } elseif ( !$lb->hasOrMadeRecentMasterChanges( INF ) ) {
+                       // Only save the position if writes have been done on the connection
+                       return;
+               }
+
+               $masterName = $lb->getServerName( $lb->getWriterIndex() );
+               if ( $lb->getServerCount() > 1 ) {
+                       $pos = $lb->getMasterPos();
+                       $this->logger->info( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
+                       $this->shutdownPositions[$masterName] = $pos;
+               } else {
+                       $this->logger->info( __METHOD__ . ": DB '$masterName' touched\n" );
+               }
+               $this->shutdownTouchDBs[$masterName] = 1;
+       }
+
+       /**
+        * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
+        * May commit chronology data to persistent storage.
+        *
+        * @param callable|null $workCallback Work to do instead of waiting on syncing positions
+        * @param string $mode One of (sync, async); whether to wait on remote datacenters
+        * @return DBMasterPos[] Empty on success; returns the (db name => position) map on failure
+        */
+       public function shutdown( callable $workCallback = null, $mode = 'sync' ) {
+               if ( !$this->enabled ) {
+                       return [];
+               }
+
+               $store = $this->store;
+               // Some callers might want to know if a user recently touched a DB.
+               // These writes do not need to block on all datacenters receiving them.
+               foreach ( $this->shutdownTouchDBs as $dbName => $unused ) {
+                       $store->set(
+                               $this->getTouchedKey( $this->store, $dbName ),
+                               microtime( true ),
+                               $store::TTL_DAY
+                       );
+               }
+
+               if ( !count( $this->shutdownPositions ) ) {
+                       return []; // nothing to save
+               }
+
+               $this->logger->info( __METHOD__ . ": saving master pos for " .
+                       implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
+               );
+
+               // CP-protected writes should overwhemingly go to the master datacenter, so get DC-local
+               // lock to merge the values. Use a DC-local get() and a synchronous all-DC set(). This
+               // makes it possible for the BagOStuff class to write in parallel to all DCs with one RTT.
+               if ( $store->lock( $this->key, 3 ) ) {
+                       if ( $workCallback ) {
+                               // Let the store run the work before blocking on a replication sync barrier. By the
+                               // time it's done with the work, the barrier should be fast if replication caught up.
+                               $store->addBusyCallback( $workCallback );
+                       }
+                       $ok = $store->set(
+                               $this->key,
+                               self::mergePositions( $store->get( $this->key ), $this->shutdownPositions ),
+                               self::POSITION_TTL,
+                               ( $mode === 'sync' ) ? $store::WRITE_SYNC : 0
+                       );
+                       $store->unlock( $this->key );
+               } else {
+                       $ok = false;
+               }
+
+               if ( !$ok ) {
+                       $bouncedPositions = $this->shutdownPositions;
+                       // Raced out too many times or stash is down
+                       $this->logger->warning( __METHOD__ . ": failed to save master pos for " .
+                               implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
+                       );
+               } elseif ( $mode === 'sync' &&
+                       $store->getQoS( $store::ATTR_SYNCWRITES ) < $store::QOS_SYNCWRITES_BE
+               ) {
+                       // Positions may not be in all datacenters, force LBFactory to play it safe
+                       $this->logger->info( __METHOD__ . ": store may not support synchronous writes." );
+                       $bouncedPositions = $this->shutdownPositions;
+               } else {
+                       $bouncedPositions = [];
+               }
+
+               return $bouncedPositions;
+       }
+
+       /**
+        * @param string $dbName DB master name (e.g. "db1052")
+        * @return float|bool UNIX timestamp when client last touched the DB; false if not on record
+        * @since 1.28
+        */
+       public function getTouched( $dbName ) {
+               return $this->store->get( $this->getTouchedKey( $this->store, $dbName ) );
+       }
+
+       /**
+        * @param BagOStuff $store
+        * @param string $dbName
+        * @return string
+        */
+       private function getTouchedKey( BagOStuff $store, $dbName ) {
+               return $store->makeGlobalKey( __CLASS__, 'mtime', $this->clientId, $dbName );
+       }
+
+       /**
+        * Load in previous master positions for the client
+        */
+       protected function initPositions() {
+               if ( $this->initialized ) {
+                       return;
+               }
+
+               $this->initialized = true;
+               if ( $this->wait ) {
+                       // If there is an expectation to see master positions with a certain min
+                       // timestamp, then block until they appear, or until a timeout is reached.
+                       if ( $this->waitForPosTime > 0.0 ) {
+                               $data = null;
+                               $loop = new WaitConditionLoop(
+                                       function () use ( &$data ) {
+                                               $data = $this->store->get( $this->key );
+
+                                               return ( self::minPosTime( $data ) >= $this->waitForPosTime )
+                                                       ? WaitConditionLoop::CONDITION_REACHED
+                                                       : WaitConditionLoop::CONDITION_CONTINUE;
+                                       },
+                                       $this->waitForPosTimeout
+                               );
+                               $result = $loop->invoke();
+                               $waitedMs = $loop->getLastWaitTime() * 1e3;
+
+                               if ( $result == $loop::CONDITION_REACHED ) {
+                                       $msg = "expected and found pos time {$this->waitForPosTime} ({$waitedMs}ms)";
+                                       $this->logger->debug( $msg );
+                               } else {
+                                       $msg = "expected but missed pos time {$this->waitForPosTime} ({$waitedMs}ms)";
+                                       $this->logger->info( $msg );
+                               }
+                       } else {
+                               $data = $this->store->get( $this->key );
+                       }
+
+                       $this->startupPositions = $data ? $data['positions'] : [];
+                       $this->logger->info( __METHOD__ . ": key is {$this->key} (read)\n" );
+               } else {
+                       $this->startupPositions = [];
+                       $this->logger->info( __METHOD__ . ": key is {$this->key} (unread)\n" );
+               }
+       }
+
+       /**
+        * @param array|bool $data
+        * @return float|null
+        */
+       private static function minPosTime( $data ) {
+               if ( !isset( $data['positions'] ) ) {
+                       return null;
+               }
+
+               $min = null;
+               foreach ( $data['positions'] as $pos ) {
+                       /** @var DBMasterPos $pos */
+                       $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime();
+               }
+
+               return $min;
+       }
+
+       /**
+        * @param array|bool $curValue
+        * @param DBMasterPos[] $shutdownPositions
+        * @return array
+        */
+       private static function mergePositions( $curValue, array $shutdownPositions ) {
+               /** @var $curPositions DBMasterPos[] */
+               if ( $curValue === false ) {
+                       $curPositions = $shutdownPositions;
+               } else {
+                       $curPositions = $curValue['positions'];
+                       // Use the newest positions for each DB master
+                       foreach ( $shutdownPositions as $db => $pos ) {
+                               if ( !isset( $curPositions[$db] )
+                                       || $pos->asOfTime() > $curPositions[$db]->asOfTime()
+                               ) {
+                                       $curPositions[$db] = $pos;
+                               }
+                       }
+               }
+
+               return [ 'positions' => $curPositions ];
+       }
+}
index 5c9976d..12f6df5 100644 (file)
@@ -29,7 +29,7 @@ use Psr\Log\NullLogger;
 /**
  * Helper class that detects high-contention DB queries via profiling calls
  *
- * This class is meant to work with a DatabaseBase object, which manages queries
+ * This class is meant to work with an IDatabase object, which manages queries
  *
  * @since 1.24
  */
@@ -295,13 +295,15 @@ class TransactionProfiler implements LoggerAwareInterface {
                        }
                }
                if ( $slow ) {
-                       $dbs = implode( ', ', array_keys( $this->dbTrxHoldingLocks[$name]['conns'] ) );
-                       $msg = "Sub-optimal transaction on DB(s) [{$dbs}]:\n";
+                       $trace = '';
                        foreach ( $this->dbTrxMethodTimes[$name] as $i => $info ) {
                                list( $query, $sTime, $end ) = $info;
-                               $msg .= sprintf( "%d\t%.6f\t%s\n", $i, ( $end - $sTime ), $query );
+                               $trace .= sprintf( "%d\t%.6f\t%s\n", $i, ( $end - $sTime ), $query );
                        }
-                       $this->logger->info( $msg );
+                       $this->logger->info( "Sub-optimal transaction on DB(s) [{dbs}]: \n{trace}", [
+                               'dbs' => implode( ', ', array_keys( $this->dbTrxHoldingLocks[$name]['conns'] ) ),
+                               'trace' => $trace
+                       ] );
                }
                unset( $this->dbTrxHoldingLocks[$name] );
                unset( $this->dbTrxMethodTimes[$name] );
diff --git a/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php b/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php
deleted file mode 100644 (file)
index b102f0f..0000000
+++ /dev/null
@@ -1,326 +0,0 @@
-<?php
-/**
- * Generator 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
- */
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-use MediaWiki\Logger\LoggerFactory;
-
-/**
- * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
- * Kind of like Hawking's [[Chronology Protection Agency]].
- */
-class ChronologyProtector implements LoggerAwareInterface{
-       /** @var BagOStuff */
-       protected $store;
-       /** @var LoggerInterface */
-       protected $logger;
-
-       /** @var string Storage key name */
-       protected $key;
-       /** @var string Hash of client parameters */
-       protected $clientId;
-       /** @var float|null Minimum UNIX timestamp of 1+ expected startup positions */
-       protected $waitForPosTime;
-       /** @var int Max seconds to wait on positions to appear */
-       protected $waitForPosTimeout = self::POS_WAIT_TIMEOUT;
-       /** @var bool Whether to no-op all method calls */
-       protected $enabled = true;
-       /** @var bool Whether to check and wait on positions */
-       protected $wait = true;
-
-       /** @var bool Whether the client data was loaded */
-       protected $initialized = false;
-       /** @var DBMasterPos[] Map of (DB master name => position) */
-       protected $startupPositions = [];
-       /** @var DBMasterPos[] Map of (DB master name => position) */
-       protected $shutdownPositions = [];
-       /** @var float[] Map of (DB master name => 1) */
-       protected $shutdownTouchDBs = [];
-
-       /** @var integer Seconds to store positions */
-       const POSITION_TTL = 60;
-       /** @var integer Max time to wait for positions to appear */
-       const POS_WAIT_TIMEOUT = 5;
-
-       /**
-        * @param BagOStuff $store
-        * @param array $client Map of (ip: <IP>, agent: <user-agent>)
-        * @param float $posTime UNIX timestamp
-        * @since 1.27
-        */
-       public function __construct( BagOStuff $store, array $client, $posTime = null ) {
-               $this->store = $store;
-               $this->clientId = md5( $client['ip'] . "\n" . $client['agent'] );
-               $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId );
-               $this->waitForPosTime = $posTime;
-               $this->logger = new \Psr\Log\NullLogger();
-       }
-
-       public function setLogger( LoggerInterface $logger ) {
-               $this->logger = $logger;
-       }
-
-       /**
-        * @param bool $enabled Whether to no-op all method calls
-        * @since 1.27
-        */
-       public function setEnabled( $enabled ) {
-               $this->enabled = $enabled;
-       }
-
-       /**
-        * @param bool $enabled Whether to check and wait on positions
-        * @since 1.27
-        */
-       public function setWaitEnabled( $enabled ) {
-               $this->wait = $enabled;
-       }
-
-       /**
-        * Initialise a ILoadBalancer to give it appropriate chronology protection.
-        *
-        * If the stash has a previous master position recorded, this will try to
-        * make sure that the next query to a replica DB of that master will see changes up
-        * to that position by delaying execution. The delay may timeout and allow stale
-        * data if no non-lagged replica DBs are available.
-        *
-        * @param ILoadBalancer $lb
-        * @return void
-        */
-       public function initLB( ILoadBalancer $lb ) {
-               if ( !$this->enabled || $lb->getServerCount() <= 1 ) {
-                       return; // non-replicated setup or disabled
-               }
-
-               $this->initPositions();
-
-               $masterName = $lb->getServerName( $lb->getWriterIndex() );
-               if ( !empty( $this->startupPositions[$masterName] ) ) {
-                       $pos = $this->startupPositions[$masterName];
-                       $this->logger->info( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
-                       $lb->waitFor( $pos );
-               }
-       }
-
-       /**
-        * Notify the ChronologyProtector that the ILoadBalancer is about to shut
-        * down. Saves replication positions.
-        *
-        * @param ILoadBalancer $lb
-        * @return void
-        */
-       public function shutdownLB( ILoadBalancer $lb ) {
-               if ( !$this->enabled ) {
-                       return; // not enabled
-               } elseif ( !$lb->hasOrMadeRecentMasterChanges( INF ) ) {
-                       // Only save the position if writes have been done on the connection
-                       return;
-               }
-
-               $masterName = $lb->getServerName( $lb->getWriterIndex() );
-               if ( $lb->getServerCount() > 1 ) {
-                       $pos = $lb->getMasterPos();
-                       $this->logger->info( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
-                       $this->shutdownPositions[$masterName] = $pos;
-               } else {
-                       $this->logger->info( __METHOD__ . ": DB '$masterName' touched\n" );
-               }
-               $this->shutdownTouchDBs[$masterName] = 1;
-       }
-
-       /**
-        * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
-        * May commit chronology data to persistent storage.
-        *
-        * @param callable|null $workCallback Work to do instead of waiting on syncing positions
-        * @param string $mode One of (sync, async); whether to wait on remote datacenters
-        * @return DBMasterPos[] Empty on success; returns the (db name => position) map on failure
-        */
-       public function shutdown( callable $workCallback = null, $mode = 'sync' ) {
-               if ( !$this->enabled ) {
-                       return [];
-               }
-
-               $store = $this->store;
-               // Some callers might want to know if a user recently touched a DB.
-               // These writes do not need to block on all datacenters receiving them.
-               foreach ( $this->shutdownTouchDBs as $dbName => $unused ) {
-                       $store->set(
-                               $this->getTouchedKey( $this->store, $dbName ),
-                               microtime( true ),
-                               $store::TTL_DAY
-                       );
-               }
-
-               if ( !count( $this->shutdownPositions ) ) {
-                       return []; // nothing to save
-               }
-
-               $this->logger->info( __METHOD__ . ": saving master pos for " .
-                       implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
-               );
-
-               // CP-protected writes should overwhemingly go to the master datacenter, so get DC-local
-               // lock to merge the values. Use a DC-local get() and a synchronous all-DC set(). This
-               // makes it possible for the BagOStuff class to write in parallel to all DCs with one RTT.
-               if ( $store->lock( $this->key, 3 ) ) {
-                       if ( $workCallback ) {
-                               // Let the store run the work before blocking on a replication sync barrier. By the
-                               // time it's done with the work, the barrier should be fast if replication caught up.
-                               $store->addBusyCallback( $workCallback );
-                       }
-                       $ok = $store->set(
-                               $this->key,
-                               self::mergePositions( $store->get( $this->key ), $this->shutdownPositions ),
-                               self::POSITION_TTL,
-                               ( $mode === 'sync' ) ? $store::WRITE_SYNC : 0
-                       );
-                       $store->unlock( $this->key );
-               } else {
-                       $ok = false;
-               }
-
-               if ( !$ok ) {
-                       $bouncedPositions = $this->shutdownPositions;
-                       // Raced out too many times or stash is down
-                       $this->logger->warning( __METHOD__ . ": failed to save master pos for " .
-                               implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
-                       );
-               } elseif ( $mode === 'sync' &&
-                       $store->getQoS( $store::ATTR_SYNCWRITES ) < $store::QOS_SYNCWRITES_BE
-               ) {
-                       // Positions may not be in all datacenters, force LBFactory to play it safe
-                       $this->logger->info( __METHOD__ . ": store may not support synchronous writes." );
-                       $bouncedPositions = $this->shutdownPositions;
-               } else {
-                       $bouncedPositions = [];
-               }
-
-               return $bouncedPositions;
-       }
-
-       /**
-        * @param string $dbName DB master name (e.g. "db1052")
-        * @return float|bool UNIX timestamp when client last touched the DB; false if not on record
-        * @since 1.28
-        */
-       public function getTouched( $dbName ) {
-               return $this->store->get( $this->getTouchedKey( $this->store, $dbName ) );
-       }
-
-       /**
-        * @param BagOStuff $store
-        * @param string $dbName
-        * @return string
-        */
-       private function getTouchedKey( BagOStuff $store, $dbName ) {
-               return $store->makeGlobalKey( __CLASS__, 'mtime', $this->clientId, $dbName );
-       }
-
-       /**
-        * Load in previous master positions for the client
-        */
-       protected function initPositions() {
-               if ( $this->initialized ) {
-                       return;
-               }
-
-               $this->initialized = true;
-               if ( $this->wait ) {
-                       // If there is an expectation to see master positions with a certain min
-                       // timestamp, then block until they appear, or until a timeout is reached.
-                       if ( $this->waitForPosTime > 0.0 ) {
-                               $data = null;
-                               $loop = new WaitConditionLoop(
-                                       function () use ( &$data ) {
-                                               $data = $this->store->get( $this->key );
-
-                                               return ( self::minPosTime( $data ) >= $this->waitForPosTime )
-                                                       ? WaitConditionLoop::CONDITION_REACHED
-                                                       : WaitConditionLoop::CONDITION_CONTINUE;
-                                       },
-                                       $this->waitForPosTimeout
-                               );
-                               $result = $loop->invoke();
-                               $waitedMs = $loop->getLastWaitTime() * 1e3;
-
-                               if ( $result == $loop::CONDITION_REACHED ) {
-                                       $msg = "expected and found pos time {$this->waitForPosTime} ({$waitedMs}ms)";
-                                       $this->logger->debug( $msg );
-                               } else {
-                                       $msg = "expected but missed pos time {$this->waitForPosTime} ({$waitedMs}ms)";
-                                       $this->logger->info( $msg );
-                               }
-                       } else {
-                               $data = $this->store->get( $this->key );
-                       }
-
-                       $this->startupPositions = $data ? $data['positions'] : [];
-                       $this->logger->info( __METHOD__ . ": key is {$this->key} (read)\n" );
-               } else {
-                       $this->startupPositions = [];
-                       $this->logger->info( __METHOD__ . ": key is {$this->key} (unread)\n" );
-               }
-       }
-
-       /**
-        * @param array|bool $data
-        * @return float|null
-        */
-       private static function minPosTime( $data ) {
-               if ( !isset( $data['positions'] ) ) {
-                       return null;
-               }
-
-               $min = null;
-               foreach ( $data['positions'] as $pos ) {
-                       /** @var DBMasterPos $pos */
-                       $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime();
-               }
-
-               return $min;
-       }
-
-       /**
-        * @param array|bool $curValue
-        * @param DBMasterPos[] $shutdownPositions
-        * @return array
-        */
-       private static function mergePositions( $curValue, array $shutdownPositions ) {
-               /** @var $curPositions DBMasterPos[] */
-               if ( $curValue === false ) {
-                       $curPositions = $shutdownPositions;
-               } else {
-                       $curPositions = $curValue['positions'];
-                       // Use the newest positions for each DB master
-                       foreach ( $shutdownPositions as $db => $pos ) {
-                               if ( !isset( $curPositions[$db] )
-                                       || $pos->asOfTime() > $curPositions[$db]->asOfTime()
-                               ) {
-                                       $curPositions[$db] = $pos;
-                               }
-                       }
-               }
-
-               return [ 'positions' => $curPositions ];
-       }
-}
index f940580..20198bf 100644 (file)
@@ -14,22 +14,22 @@ class DBConnRef implements IDatabase {
        /** @var IDatabase|null Live connection handle */
        private $conn;
 
-       /** @var array|null */
+       /** @var array|null N-tuple of (server index, group, DatabaseDomain|string) */
        private $params;
 
        const FLD_INDEX = 0;
        const FLD_GROUP = 1;
-       const FLD_WIKI = 2;
+       const FLD_DOMAIN = 2;
 
        /**
         * @param ILoadBalancer $lb
-        * @param IDatabase|array $conn Connection or (server index, group, wiki ID)
+        * @param IDatabase|array $conn Connection or (server index, group, DatabaseDomain|string)
         */
        public function __construct( ILoadBalancer $lb, $conn ) {
                $this->lb = $lb;
                if ( $conn instanceof IDatabase ) {
                        $this->conn = $conn; // live handle
-               } elseif ( count( $conn ) >= 3 && $conn[self::FLD_WIKI] !== false ) {
+               } elseif ( count( $conn ) >= 3 && $conn[self::FLD_DOMAIN] !== false ) {
                        $this->params = $conn;
                } else {
                        throw new InvalidArgumentException( "Missing lazy connection arguments." );
@@ -145,15 +145,20 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function getWikiID() {
+       public function getDomainID() {
                if ( $this->conn === null ) {
-                       // Avoid triggering a connection
-                       return $this->params[self::FLD_WIKI];
+                       $domain = $this->params[self::FLD_DOMAIN];
+                       // Avoid triggering a database connection
+                       return $domain instanceof DatabaseDomain ? $domain->getId() : $domain;
                }
 
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
+       public function getWikiID() {
+               return $this->getDomainID();
+       }
+
        public function getType() {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
@@ -303,7 +308,7 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function makeList( $a, $mode = LIST_COMMA ) {
+       public function makeList( $a, $mode = self::LIST_COMMA ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
@@ -311,6 +316,10 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
+       public function aggregateValue( $valuedata, $valuename = 'value' ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
        public function bitNot( $field ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
@@ -333,6 +342,10 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
+       public function buildStringCast( $field ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
        public function selectDB( $db ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
@@ -432,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() );
        }
 
diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php
new file mode 100644 (file)
index 0000000..9f1f228
--- /dev/null
@@ -0,0 +1,3430 @@
+<?php
+/**
+ * @defgroup Database Database
+ *
+ * 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
+ */
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Relational database abstraction object
+ *
+ * @ingroup Database
+ * @since 1.28
+ */
+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 */
+       const DEADLOCK_DELAY_MIN = 500000;
+       /** Maximum time to wait before retry */
+       const DEADLOCK_DELAY_MAX = 1500000;
+
+       /** How long before it is worth doing a dummy query to test the connection */
+       const PING_TTL = 1.0;
+       const PING_QUERY = 'SELECT 1 AS ping';
+
+       const TINY_WRITE_SEC = .010;
+       const SLOW_WRITE_SEC = .500;
+       const SMALL_WRITE_ROWS = 100;
+
+       /** @var string SQL query */
+       protected $mLastQuery = '';
+       /** @var bool */
+       protected $mDoneWrites = false;
+       /** @var string|bool */
+       protected $mPHPError = false;
+       /** @var string */
+       protected $mServer;
+       /** @var string */
+       protected $mUser;
+       /** @var string */
+       protected $mPassword;
+       /** @var string */
+       protected $mDBname;
+       /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
+       protected $tableAliases = [];
+       /** @var bool Whether this PHP instance is for a CLI script */
+       protected $cliMode;
+       /** @var string Agent name for query profiling */
+       protected $agent;
+
+       /** @var BagOStuff APC cache */
+       protected $srvCache;
+       /** @var LoggerInterface */
+       protected $connLogger;
+       /** @var LoggerInterface */
+       protected $queryLogger;
+       /** @var callback Error logging callback */
+       protected $errorLogger;
+
+       /** @var resource Database connection */
+       protected $mConn = null;
+       /** @var bool */
+       protected $mOpened = false;
+
+       /** @var array[] List of (callable, method name) */
+       protected $mTrxIdleCallbacks = [];
+       /** @var array[] List of (callable, method name) */
+       protected $mTrxPreCommitCallbacks = [];
+       /** @var array[] List of (callable, method name) */
+       protected $mTrxEndCallbacks = [];
+       /** @var callable[] Map of (name => callable) */
+       protected $mTrxRecurringCallbacks = [];
+       /** @var bool Whether to suppress triggering of transaction end callbacks */
+       protected $mTrxEndCallbacksSuppressed = false;
+
+       /** @var string */
+       protected $mTablePrefix = '';
+       /** @var string */
+       protected $mSchema = '';
+       /** @var integer */
+       protected $mFlags;
+       /** @var array */
+       protected $mLBInfo = [];
+       /** @var bool|null */
+       protected $mDefaultBigSelects = null;
+       /** @var array|bool */
+       protected $mSchemaVars = false;
+       /** @var array */
+       protected $mSessionVars = [];
+       /** @var array|null */
+       protected $preparedArgs;
+       /** @var string|bool|null Stashed value of html_errors INI setting */
+       protected $htmlErrors;
+       /** @var string */
+       protected $delimiter = ';';
+       /** @var DatabaseDomain */
+       protected $currentDomain;
+
+       /**
+        * Either 1 if a transaction is active or 0 otherwise.
+        * The other Trx fields may not be meaningfull if this is 0.
+        *
+        * @var int
+        */
+       protected $mTrxLevel = 0;
+       /**
+        * Either a short hexidecimal string if a transaction is active or ""
+        *
+        * @var string
+        * @see Database::mTrxLevel
+        */
+       protected $mTrxShortId = '';
+       /**
+        * The UNIX time that the transaction started. Callers can assume that if
+        * snapshot isolation is used, then the data is *at least* up to date to that
+        * point (possibly more up-to-date since the first SELECT defines the snapshot).
+        *
+        * @var float|null
+        * @see Database::mTrxLevel
+        */
+       private $mTrxTimestamp = null;
+       /** @var float Lag estimate at the time of BEGIN */
+       private $mTrxReplicaLag = null;
+       /**
+        * Remembers the function name given for starting the most recent transaction via begin().
+        * Used to provide additional context for error reporting.
+        *
+        * @var string
+        * @see Database::mTrxLevel
+        */
+       private $mTrxFname = null;
+       /**
+        * Record if possible write queries were done in the last transaction started
+        *
+        * @var bool
+        * @see Database::mTrxLevel
+        */
+       private $mTrxDoneWrites = false;
+       /**
+        * Record if the current transaction was started implicitly due to DBO_TRX being set.
+        *
+        * @var bool
+        * @see Database::mTrxLevel
+        */
+       private $mTrxAutomatic = false;
+       /**
+        * Array of levels of atomicity within transactions
+        *
+        * @var array
+        */
+       private $mTrxAtomicLevels = [];
+       /**
+        * Record if the current transaction was started implicitly by Database::startAtomic
+        *
+        * @var bool
+        */
+       private $mTrxAutomaticAtomic = false;
+       /**
+        * Track the write query callers of the current transaction
+        *
+        * @var string[]
+        */
+       private $mTrxWriteCallers = [];
+       /**
+        * @var float Seconds spent in write queries for the current transaction
+        */
+       private $mTrxWriteDuration = 0.0;
+       /**
+        * @var integer Number of write queries for the current transaction
+        */
+       private $mTrxWriteQueryCount = 0;
+       /**
+        * @var float Like mTrxWriteQueryCount but excludes lock-bound, easy to replicate, queries
+        */
+       private $mTrxWriteAdjDuration = 0.0;
+       /**
+        * @var integer Number of write queries counted in mTrxWriteAdjDuration
+        */
+       private $mTrxWriteAdjQueryCount = 0;
+       /**
+        * @var float RTT time estimate
+        */
+       private $mRTTEstimate = 0.0;
+
+       /** @var array Map of (name => 1) for locks obtained via lock() */
+       private $mNamedLocksHeld = [];
+       /** @var array Map of (table name => 1) for TEMPORARY tables */
+       protected $mSessionTempTables = [];
+
+       /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
+       private $lazyMasterHandle;
+
+       /** @var float UNIX timestamp */
+       protected $lastPing = 0.0;
+
+       /** @var int[] Prior mFlags values */
+       private $priorFlags = [];
+
+       /** @var object|string Class name or object With profileIn/profileOut methods */
+       protected $profiler;
+       /** @var TransactionProfiler */
+       protected $trxProfiler;
+
+       /**
+        * Constructor and database handle and attempt to connect to the DB server
+        *
+        * IDatabase classes should not be constructed directly in external
+        * code. Database::factory() should be used instead.
+        *
+        * @param array $params Parameters passed from Database::factory()
+        */
+       function __construct( array $params ) {
+               $server = $params['host'];
+               $user = $params['user'];
+               $password = $params['password'];
+               $dbName = $params['dbname'];
+
+               $this->mSchema = $params['schema'];
+               $this->mTablePrefix = $params['tablePrefix'];
+
+               $this->cliMode = $params['cliMode'];
+               // Agent name is added to SQL queries in a comment, so make sure it can't break out
+               $this->agent = str_replace( '/', '-', $params['agent'] );
+
+               $this->mFlags = $params['flags'];
+               if ( $this->mFlags & self::DBO_DEFAULT ) {
+                       if ( $this->cliMode ) {
+                               $this->mFlags &= ~self::DBO_TRX;
+                       } else {
+                               $this->mFlags |= self::DBO_TRX;
+                       }
+               }
+
+               $this->mSessionVars = $params['variables'];
+
+               $this->srvCache = isset( $params['srvCache'] )
+                       ? $params['srvCache']
+                       : new HashBagOStuff();
+
+               $this->profiler = $params['profiler'];
+               $this->trxProfiler = $params['trxProfiler'];
+               $this->connLogger = $params['connLogger'];
+               $this->queryLogger = $params['queryLogger'];
+               $this->errorLogger = $params['errorLogger'];
+
+               // Set initial dummy domain until open() sets the final DB/prefix
+               $this->currentDomain = DatabaseDomain::newUnspecified();
+
+               if ( $user ) {
+                       $this->open( $server, $user, $password, $dbName );
+               } elseif ( $this->requiresDatabaseUser() ) {
+                       throw new InvalidArgumentException( "No database user provided." );
+               }
+
+               // Set the domain object after open() sets the relevant fields
+               if ( $this->mDBname != '' ) {
+                       // Domains with server scope but a table prefix are not used by IDatabase classes
+                       $this->currentDomain = new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix );
+               }
+       }
+
+       /**
+        * Construct a Database subclass instance given a database type and parameters
+        *
+        * This also connects to the database immediately upon object construction
+        *
+        * @param string $dbType A possible DB type (sqlite, mysql, postgres)
+        * @param array $p Parameter map with keys:
+        *   - host : The hostname of the DB server
+        *   - user : The name of the database user the client operates under
+        *   - password : The password for the database user
+        *   - dbname : The name of the database to use where queries do not specify one.
+        *      The database must exist or an error might be thrown. Setting this to the empty string
+        *      will avoid any such errors and make the handle have no implicit database scope. This is
+        *      useful for queries like SHOW STATUS, CREATE DATABASE, or DROP DATABASE. Note that a
+        *      "database" in Postgres is rougly equivalent to an entire MySQL server. This the domain
+        *      in which user names and such are defined, e.g. users are database-specific in Postgres.
+        *   - schema : The database schema to use (if supported). A "schema" in Postgres is roughly
+        *      equivalent to a "database" in MySQL. Note that MySQL and SQLite do not use schemas.
+        *   - tablePrefix : Optional table prefix that is implicitly added on to all table names
+        *      recognized in queries. This can be used in place of schemas for handle site farms.
+        *   - flags : Optional bitfield of DBO_* constants that define connection, protocol,
+        *      buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT
+        *      flag in place UNLESS this this database simply acts as a key/value store.
+        *   - driver: Optional name of a specific DB client driver. For MySQL, there is the old
+        *      'mysql' driver and the newer 'mysqli' driver.
+        *   - variables: Optional map of session variables to set after connecting. This can be
+        *      used to adjust lock timeouts or encoding modes and the like.
+        *   - connLogger: Optional PSR-3 logger interface instance.
+        *   - queryLogger: Optional PSR-3 logger interface instance.
+        *   - profiler: Optional class name or object with profileIn()/profileOut() methods.
+        *      These will be called in query(), using a simplified version of the SQL that also
+        *      includes the agent as a SQL comment.
+        *   - trxProfiler: Optional TransactionProfiler instance.
+        *   - errorLogger: Optional callback that takes an Exception and logs it.
+        *   - cliMode: Whether to consider the execution context that of a CLI script.
+        *   - agent: Optional name used to identify the end-user in query profiling/logging.
+        *   - srvCache: Optional BagOStuff instance to an APC-style cache.
+        * @return Database|null If the database driver or extension cannot be found
+        * @throws InvalidArgumentException If the database driver or extension cannot be found
+        * @since 1.18
+        */
+       final public static function factory( $dbType, $p = [] ) {
+               static $canonicalDBTypes = [
+                       'mysql' => [ 'mysqli', 'mysql' ],
+                       'postgres' => [],
+                       'sqlite' => [],
+                       'oracle' => [],
+                       'mssql' => [],
+               ];
+
+               $driver = false;
+               $dbType = strtolower( $dbType );
+               if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
+                       $possibleDrivers = $canonicalDBTypes[$dbType];
+                       if ( !empty( $p['driver'] ) ) {
+                               if ( in_array( $p['driver'], $possibleDrivers ) ) {
+                                       $driver = $p['driver'];
+                               } else {
+                                       throw new InvalidArgumentException( __METHOD__ .
+                                               " type '$dbType' does not support driver '{$p['driver']}'" );
+                               }
+                       } else {
+                               foreach ( $possibleDrivers as $posDriver ) {
+                                       if ( extension_loaded( $posDriver ) ) {
+                                               $driver = $posDriver;
+                                               break;
+                                       }
+                               }
+                       }
+               } else {
+                       $driver = $dbType;
+               }
+               if ( $driver === false || $driver === '' ) {
+                       throw new InvalidArgumentException( __METHOD__ .
+                               " no viable database extension found for type '$dbType'" );
+               }
+
+               $class = 'Database' . ucfirst( $driver );
+               if ( class_exists( $class ) && is_subclass_of( $class, 'IDatabase' ) ) {
+                       // Resolve some defaults for b/c
+                       $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
+                       $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
+                       $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
+                       $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
+                       $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
+                       $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
+                       $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
+                       $p['schema'] = isset( $p['schema'] ) ? $p['schema'] : '';
+                       $p['cliMode'] = isset( $p['cliMode'] ) ? $p['cliMode'] : ( PHP_SAPI === 'cli' );
+                       $p['agent'] = isset( $p['agent'] ) ? $p['agent'] : '';
+                       if ( !isset( $p['connLogger'] ) ) {
+                               $p['connLogger'] = new \Psr\Log\NullLogger();
+                       }
+                       if ( !isset( $p['queryLogger'] ) ) {
+                               $p['queryLogger'] = new \Psr\Log\NullLogger();
+                       }
+                       $p['profiler'] = isset( $p['profiler'] ) ? $p['profiler'] : null;
+                       if ( !isset( $p['trxProfiler'] ) ) {
+                               $p['trxProfiler'] = new TransactionProfiler();
+                       }
+                       if ( !isset( $p['errorLogger'] ) ) {
+                               $p['errorLogger'] = function ( Exception $e ) {
+                                       trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
+                               };
+                       }
+
+                       $conn = new $class( $p );
+               } else {
+                       $conn = null;
+               }
+
+               return $conn;
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->queryLogger = $logger;
+       }
+
+       public function getServerInfo() {
+               return $this->getServerVersion();
+       }
+
+       public function bufferResults( $buffer = null ) {
+               $res = !$this->getFlag( self::DBO_NOBUFFER );
+               if ( $buffer !== null ) {
+                       $buffer
+                               ? $this->clearFlag( self::DBO_NOBUFFER )
+                               : $this->setFlag( self::DBO_NOBUFFER );
+               }
+
+               return $res;
+       }
+
+       /**
+        * Turns on (false) or off (true) the automatic generation and sending
+        * of a "we're sorry, but there has been a database error" page on
+        * database errors. Default is on (false). When turned off, the
+        * code should use lastErrno() and lastError() to handle the
+        * situation as appropriate.
+        *
+        * Do not use this function outside of the Database classes.
+        *
+        * @param null|bool $ignoreErrors
+        * @return bool The previous value of the flag.
+        */
+       protected function ignoreErrors( $ignoreErrors = null ) {
+               $res = $this->getFlag( self::DBO_IGNORE );
+               if ( $ignoreErrors !== null ) {
+                       $ignoreErrors
+                               ? $this->setFlag( self::DBO_IGNORE )
+                               : $this->clearFlag( self::DBO_IGNORE );
+               }
+
+               return $res;
+       }
+
+       public function trxLevel() {
+               return $this->mTrxLevel;
+       }
+
+       public function trxTimestamp() {
+               return $this->mTrxLevel ? $this->mTrxTimestamp : null;
+       }
+
+       public function tablePrefix( $prefix = null ) {
+               $old = $this->mTablePrefix;
+               if ( $prefix !== null ) {
+                       $this->mTablePrefix = $prefix;
+                       $this->currentDomain = ( $this->mDBname != '' )
+                               ? new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix )
+                               : DatabaseDomain::newUnspecified();
+               }
+
+               return $old;
+       }
+
+       public function dbSchema( $schema = null ) {
+               $old = $this->mSchema;
+               if ( $schema !== null ) {
+                       $this->mSchema = $schema;
+               }
+
+               return $old;
+       }
+
+       public function getLBInfo( $name = null ) {
+               if ( is_null( $name ) ) {
+                       return $this->mLBInfo;
+               } else {
+                       if ( array_key_exists( $name, $this->mLBInfo ) ) {
+                               return $this->mLBInfo[$name];
+                       } else {
+                               return null;
+                       }
+               }
+       }
+
+       public function setLBInfo( $name, $value = null ) {
+               if ( is_null( $value ) ) {
+                       $this->mLBInfo = $name;
+               } else {
+                       $this->mLBInfo[$name] = $value;
+               }
+       }
+
+       public function setLazyMasterHandle( IDatabase $conn ) {
+               $this->lazyMasterHandle = $conn;
+       }
+
+       /**
+        * @return IDatabase|null
+        * @see setLazyMasterHandle()
+        * @since 1.27
+        */
+       protected function getLazyMasterHandle() {
+               return $this->lazyMasterHandle;
+       }
+
+       public function implicitGroupby() {
+               return true;
+       }
+
+       public function implicitOrderby() {
+               return true;
+       }
+
+       public function lastQuery() {
+               return $this->mLastQuery;
+       }
+
+       public function doneWrites() {
+               return (bool)$this->mDoneWrites;
+       }
+
+       public function lastDoneWrites() {
+               return $this->mDoneWrites ?: false;
+       }
+
+       public function writesPending() {
+               return $this->mTrxLevel && $this->mTrxDoneWrites;
+       }
+
+       public function writesOrCallbacksPending() {
+               return $this->mTrxLevel && (
+                       $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
+               );
+       }
+
+       public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
+               if ( !$this->mTrxLevel ) {
+                       return false;
+               } elseif ( !$this->mTrxDoneWrites ) {
+                       return 0.0;
+               }
+
+               switch ( $type ) {
+                       case self::ESTIMATE_DB_APPLY:
+                               $this->ping( $rtt );
+                               $rttAdjTotal = $this->mTrxWriteAdjQueryCount * $rtt;
+                               $applyTime = max( $this->mTrxWriteAdjDuration - $rttAdjTotal, 0 );
+                               // For omitted queries, make them count as something at least
+                               $omitted = $this->mTrxWriteQueryCount - $this->mTrxWriteAdjQueryCount;
+                               $applyTime += self::TINY_WRITE_SEC * $omitted;
+
+                               return $applyTime;
+                       default: // everything
+                               return $this->mTrxWriteDuration;
+               }
+       }
+
+       public function pendingWriteCallers() {
+               return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
+       }
+
+       protected function pendingWriteAndCallbackCallers() {
+               if ( !$this->mTrxLevel ) {
+                       return [];
+               }
+
+               $fnames = $this->mTrxWriteCallers;
+               foreach ( [
+                       $this->mTrxIdleCallbacks,
+                       $this->mTrxPreCommitCallbacks,
+                       $this->mTrxEndCallbacks
+               ] as $callbacks ) {
+                       foreach ( $callbacks as $callback ) {
+                               $fnames[] = $callback[1];
+                       }
+               }
+
+               return $fnames;
+       }
+
+       public function isOpen() {
+               return $this->mOpened;
+       }
+
+       public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
+               if ( $remember === self::REMEMBER_PRIOR ) {
+                       array_push( $this->priorFlags, $this->mFlags );
+               }
+               $this->mFlags |= $flag;
+       }
+
+       public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
+               if ( $remember === self::REMEMBER_PRIOR ) {
+                       array_push( $this->priorFlags, $this->mFlags );
+               }
+               $this->mFlags &= ~$flag;
+       }
+
+       public function restoreFlags( $state = self::RESTORE_PRIOR ) {
+               if ( !$this->priorFlags ) {
+                       return;
+               }
+
+               if ( $state === self::RESTORE_INITIAL ) {
+                       $this->mFlags = reset( $this->priorFlags );
+                       $this->priorFlags = [];
+               } else {
+                       $this->mFlags = array_pop( $this->priorFlags );
+               }
+       }
+
+       public function getFlag( $flag ) {
+               return !!( $this->mFlags & $flag );
+       }
+
+       public function getProperty( $name ) {
+               return $this->$name;
+       }
+
+       public function getDomainID() {
+               return $this->currentDomain->getId();
+       }
+
+       final public function getWikiID() {
+               return $this->getDomainID();
+       }
+
+       /**
+        * Get information about an index into an object
+        * @param string $table Table name
+        * @param string $index Index name
+        * @param string $fname Calling function name
+        * @return mixed Database-specific index description class or false if the index does not exist
+        */
+       abstract function indexInfo( $table, $index, $fname = __METHOD__ );
+
+       /**
+        * Wrapper for addslashes()
+        *
+        * @param string $s String to be slashed.
+        * @return string Slashed string.
+        */
+       abstract function strencode( $s );
+
+       protected function installErrorHandler() {
+               $this->mPHPError = false;
+               $this->htmlErrors = ini_set( 'html_errors', '0' );
+               set_error_handler( [ $this, 'connectionErrorLogger' ] );
+       }
+
+       /**
+        * @return bool|string
+        */
+       protected function restoreErrorHandler() {
+               restore_error_handler();
+               if ( $this->htmlErrors !== false ) {
+                       ini_set( 'html_errors', $this->htmlErrors );
+               }
+               if ( $this->mPHPError ) {
+                       $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
+                       $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
+
+                       return $error;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * This method should not be used outside of Database classes
+        *
+        * @param int $errno
+        * @param string $errstr
+        */
+       public function connectionErrorLogger( $errno, $errstr ) {
+               $this->mPHPError = $errstr;
+       }
+
+       /**
+        * Create a log context to pass to PSR-3 logger functions.
+        *
+        * @param array $extras Additional data to add to context
+        * @return array
+        */
+       protected function getLogContext( array $extras = [] ) {
+               return array_merge(
+                       [
+                               'db_server' => $this->mServer,
+                               'db_name' => $this->mDBname,
+                               'db_user' => $this->mUser,
+                       ],
+                       $extras
+               );
+       }
+
+       public function close() {
+               if ( $this->mConn ) {
+                       if ( $this->trxLevel() ) {
+                               $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
+                       }
+
+                       $closed = $this->closeConnection();
+                       $this->mConn = false;
+               } elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
+                       throw new RuntimeException( "Transaction callbacks still pending." );
+               } else {
+                       $closed = true;
+               }
+               $this->mOpened = false;
+
+               return $closed;
+       }
+
+       /**
+        * Make sure isOpen() returns true as a sanity check
+        *
+        * @throws DBUnexpectedError
+        */
+       protected function assertOpen() {
+               if ( !$this->isOpen() ) {
+                       throw new DBUnexpectedError( $this, "DB connection was already closed." );
+               }
+       }
+
+       /**
+        * Closes underlying database connection
+        * @since 1.20
+        * @return bool Whether connection was closed successfully
+        */
+       abstract protected function closeConnection();
+
+       public function reportConnectionError( $error = 'Unknown error' ) {
+               $myError = $this->lastError();
+               if ( $myError ) {
+                       $error = $myError;
+               }
+
+               # New method
+               throw new DBConnectionError( $this, $error );
+       }
+
+       /**
+        * The DBMS-dependent part of query()
+        *
+        * @param string $sql SQL query.
+        * @return ResultWrapper|bool Result object to feed to fetchObject,
+        *   fetchRow, ...; or false on failure
+        */
+       abstract protected function doQuery( $sql );
+
+       /**
+        * Determine whether a query writes to the DB.
+        * Should return true if unsure.
+        *
+        * @param string $sql
+        * @return bool
+        */
+       protected function isWriteQuery( $sql ) {
+               return !preg_match(
+                       '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
+       }
+
+       /**
+        * @param $sql
+        * @return string|null
+        */
+       protected function getQueryVerb( $sql ) {
+               return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
+       }
+
+       /**
+        * Determine whether a SQL statement is sensitive to isolation level.
+        * A SQL statement is considered transactable if its result could vary
+        * depending on the transaction isolation level. Operational commands
+        * such as 'SET' and 'SHOW' are not considered to be transactable.
+        *
+        * @param string $sql
+        * @return bool
+        */
+       protected function isTransactableQuery( $sql ) {
+               $verb = $this->getQueryVerb( $sql );
+               return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
+       }
+
+       /**
+        * @param string $sql A SQL query
+        * @return bool Whether $sql is SQL for creating/dropping a new TEMPORARY table
+        */
+       protected function registerTempTableOperation( $sql ) {
+               if ( preg_match(
+                       '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+                       $sql,
+                       $matches
+               ) ) {
+                       $this->mSessionTempTables[$matches[1]] = 1;
+
+                       return true;
+               } elseif ( preg_match(
+                       '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+                       $sql,
+                       $matches
+               ) ) {
+                       unset( $this->mSessionTempTables[$matches[1]] );
+
+                       return true;
+               } elseif ( preg_match(
+                       '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
+                       $sql,
+                       $matches
+               ) ) {
+                       return isset( $this->mSessionTempTables[$matches[1]] );
+               }
+
+               return false;
+       }
+
+       public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
+               $priorWritesPending = $this->writesOrCallbacksPending();
+               $this->mLastQuery = $sql;
+
+               $isWrite = $this->isWriteQuery( $sql ) && !$this->registerTempTableOperation( $sql );
+               if ( $isWrite ) {
+                       $reason = $this->getReadOnlyReason();
+                       if ( $reason !== false ) {
+                               throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
+                       }
+                       # Set a flag indicating that writes have been done
+                       $this->mDoneWrites = microtime( true );
+               }
+
+               // Add trace comment to the begin of the sql string, right after the operator.
+               // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
+               $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( self::DBO_TRX )
+                       && $this->isTransactableQuery( $sql )
+               ) {
+                       $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
+                       $this->mTrxAutomatic = true;
+               }
+
+               # Keep track of whether the transaction has write queries pending
+               if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
+                       $this->mTrxDoneWrites = true;
+                       $this->trxProfiler->transactionWritingIn(
+                               $this->mServer, $this->mDBname, $this->mTrxShortId );
+               }
+
+               if ( $this->getFlag( self::DBO_DEBUG ) ) {
+                       $this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
+               }
+
+               # Avoid fatals if close() was called
+               $this->assertOpen();
+
+               # Send the query to the server
+               $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
+
+               # Try reconnecting if the connection was lost
+               if ( false === $ret && $this->wasErrorReissuable() ) {
+                       $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
+                       # Stash the last error values before anything might clear them
+                       $lastError = $this->lastError();
+                       $lastErrno = $this->lastErrno();
+                       # Update state tracking to reflect transaction loss due to disconnection
+                       $this->handleSessionLoss();
+                       if ( $this->reconnect() ) {
+                               $msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
+                               $this->connLogger->warning( $msg );
+                               $this->queryLogger->warning(
+                                       "$msg:\n" . ( new RuntimeException() )->getTraceAsString() );
+
+                               if ( !$recoverable ) {
+                                       # Callers may catch the exception and continue to use the DB
+                                       $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
+                               } else {
+                                       # Should be safe to silently retry the query
+                                       $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
+                               }
+                       } else {
+                               $msg = __METHOD__ . ": lost connection to {$this->getServer()} permanently";
+                               $this->connLogger->error( $msg );
+                       }
+               }
+
+               if ( false === $ret ) {
+                       # Deadlocks cause the entire transaction to abort, not just the statement.
+                       # http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
+                       # https://www.postgresql.org/docs/9.1/static/explicit-locking.html
+                       if ( $this->wasDeadlock() ) {
+                               if ( $this->explicitTrxActive() || $priorWritesPending ) {
+                                       $tempIgnore = false; // not recoverable
+                               }
+                               # Update state tracking to reflect transaction loss
+                               $this->handleSessionLoss();
+                       }
+
+                       $this->reportQueryError(
+                               $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
+               }
+
+               $res = $this->resultObject( $ret );
+
+               return $res;
+       }
+
+       private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
+               $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+               # generalizeSQL() will probably cut down the query to reasonable
+               # logging size most of the time. The substr is really just a sanity check.
+               if ( $isMaster ) {
+                       $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
+               } else {
+                       $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
+               }
+
+               # Include query transaction state
+               $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
+
+               $startTime = microtime( true );
+               if ( $this->profiler ) {
+                       call_user_func( [ $this->profiler, 'profileIn' ], $queryProf );
+               }
+               $ret = $this->doQuery( $commentedSql );
+               if ( $this->profiler ) {
+                       call_user_func( [ $this->profiler, 'profileOut' ], $queryProf );
+               }
+               $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
+
+               unset( $queryProfSection ); // profile out (if set)
+
+               if ( $ret !== false ) {
+                       $this->lastPing = $startTime;
+                       if ( $isWrite && $this->mTrxLevel ) {
+                               $this->updateTrxWriteQueryTime( $sql, $queryRuntime );
+                               $this->mTrxWriteCallers[] = $fname;
+                       }
+               }
+
+               if ( $sql === self::PING_QUERY ) {
+                       $this->mRTTEstimate = $queryRuntime;
+               }
+
+               $this->trxProfiler->recordQueryCompletion(
+                       $queryProf, $startTime, $isWrite, $this->affectedRows()
+               );
+               $this->queryLogger->debug( $sql, [
+                       'method' => $fname,
+                       'master' => $isMaster,
+                       'runtime' => $queryRuntime,
+               ] );
+
+               return $ret;
+       }
+
+       /**
+        * Update the estimated run-time of a query, not counting large row lock times
+        *
+        * LoadBalancer can be set to rollback transactions that will create huge replication
+        * lag. It bases this estimate off of pendingWriteQueryDuration(). Certain simple
+        * queries, like inserting a row can take a long time due to row locking. This method
+        * uses some simple heuristics to discount those cases.
+        *
+        * @param string $sql A SQL write query
+        * @param float $runtime Total runtime, including RTT
+        */
+       private function updateTrxWriteQueryTime( $sql, $runtime ) {
+               // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
+               $indicativeOfReplicaRuntime = true;
+               if ( $runtime > self::SLOW_WRITE_SEC ) {
+                       $verb = $this->getQueryVerb( $sql );
+                       // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
+                       if ( $verb === 'INSERT' ) {
+                               $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
+                       } elseif ( $verb === 'REPLACE' ) {
+                               $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
+                       }
+               }
+
+               $this->mTrxWriteDuration += $runtime;
+               $this->mTrxWriteQueryCount += 1;
+               if ( $indicativeOfReplicaRuntime ) {
+                       $this->mTrxWriteAdjDuration += $runtime;
+                       $this->mTrxWriteAdjQueryCount += 1;
+               }
+       }
+
+       private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
+               # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
+               # Dropped connections also mean that named locks are automatically released.
+               # Only allow error suppression in autocommit mode or when the lost transaction
+               # didn't matter anyway (aside from DBO_TRX snapshot loss).
+               if ( $this->mNamedLocksHeld ) {
+                       return false; // possible critical section violation
+               } elseif ( $sql === 'COMMIT' ) {
+                       return !$priorWritesPending; // nothing written anyway? (T127428)
+               } elseif ( $sql === 'ROLLBACK' ) {
+                       return true; // transaction lost...which is also what was requested :)
+               } elseif ( $this->explicitTrxActive() ) {
+                       return false; // don't drop atomocity
+               } elseif ( $priorWritesPending ) {
+                       return false; // prior writes lost from implicit transaction
+               }
+
+               return true;
+       }
+
+       private function handleSessionLoss() {
+               $this->mTrxLevel = 0;
+               $this->mTrxIdleCallbacks = []; // bug 65263
+               $this->mTrxPreCommitCallbacks = []; // bug 65263
+               $this->mSessionTempTables = [];
+               $this->mNamedLocksHeld = [];
+               try {
+                       // Handle callbacks in mTrxEndCallbacks
+                       $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+                       $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
+                       return null;
+               } catch ( Exception $e ) {
+                       // Already logged; move on...
+                       return $e;
+               }
+       }
+
+       public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+               if ( $this->ignoreErrors() || $tempIgnore ) {
+                       $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
+               } else {
+                       $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
+                       $this->queryLogger->error(
+                               "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
+                               $this->getLogContext( [
+                                       'method' => __METHOD__,
+                                       'errno' => $errno,
+                                       'error' => $error,
+                                       'sql1line' => $sql1line,
+                                       'fname' => $fname,
+                               ] )
+                       );
+                       $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
+                       throw new DBQueryError( $this, $error, $errno, $sql, $fname );
+               }
+       }
+
+       public function freeResult( $res ) {
+       }
+
+       public function selectField(
+               $table, $var, $cond = '', $fname = __METHOD__, $options = []
+       ) {
+               if ( $var === '*' ) { // sanity
+                       throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
+               }
+
+               if ( !is_array( $options ) ) {
+                       $options = [ $options ];
+               }
+
+               $options['LIMIT'] = 1;
+
+               $res = $this->select( $table, $var, $cond, $fname, $options );
+               if ( $res === false || !$this->numRows( $res ) ) {
+                       return false;
+               }
+
+               $row = $this->fetchRow( $res );
+
+               if ( $row !== false ) {
+                       return reset( $row );
+               } else {
+                       return false;
+               }
+       }
+
+       public function selectFieldValues(
+               $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
+       ) {
+               if ( $var === '*' ) { // sanity
+                       throw new DBUnexpectedError( $this, "Cannot use a * field" );
+               } elseif ( !is_string( $var ) ) { // sanity
+                       throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
+               }
+
+               if ( !is_array( $options ) ) {
+                       $options = [ $options ];
+               }
+
+               $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
+               if ( $res === false ) {
+                       return false;
+               }
+
+               $values = [];
+               foreach ( $res as $row ) {
+                       $values[] = $row->$var;
+               }
+
+               return $values;
+       }
+
+       /**
+        * Returns an optional USE INDEX clause to go after the table, and a
+        * string to go at the end of the query.
+        *
+        * @param array $options Associative array of options to be turned into
+        *   an SQL query, valid keys are listed in the function.
+        * @return array
+        * @see Database::select()
+        */
+       protected function makeSelectOptions( $options ) {
+               $preLimitTail = $postLimitTail = '';
+               $startOpts = '';
+
+               $noKeyOptions = [];
+
+               foreach ( $options as $key => $option ) {
+                       if ( is_numeric( $key ) ) {
+                               $noKeyOptions[$option] = true;
+                       }
+               }
+
+               $preLimitTail .= $this->makeGroupByWithHaving( $options );
+
+               $preLimitTail .= $this->makeOrderBy( $options );
+
+               // if (isset($options['LIMIT'])) {
+               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
+               //              isset($options['OFFSET']) ? $options['OFFSET']
+               //              : false);
+               // }
+
+               if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
+                       $postLimitTail .= ' FOR UPDATE';
+               }
+
+               if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
+                       $postLimitTail .= ' LOCK IN SHARE MODE';
+               }
+
+               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
+                       $startOpts .= 'DISTINCT';
+               }
+
+               # Various MySQL extensions
+               if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
+                       $startOpts .= ' /*! STRAIGHT_JOIN */';
+               }
+
+               if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
+                       $startOpts .= ' HIGH_PRIORITY';
+               }
+
+               if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
+                       $startOpts .= ' SQL_BIG_RESULT';
+               }
+
+               if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
+                       $startOpts .= ' SQL_BUFFER_RESULT';
+               }
+
+               if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
+                       $startOpts .= ' SQL_SMALL_RESULT';
+               }
+
+               if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
+                       $startOpts .= ' SQL_CALC_FOUND_ROWS';
+               }
+
+               if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
+                       $startOpts .= ' SQL_CACHE';
+               }
+
+               if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
+                       $startOpts .= ' SQL_NO_CACHE';
+               }
+
+               if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
+                       $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+               } else {
+                       $useIndex = '';
+               }
+               if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
+                       $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
+               } else {
+                       $ignoreIndex = '';
+               }
+
+               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
+       }
+
+       /**
+        * Returns an optional GROUP BY with an optional HAVING
+        *
+        * @param array $options Associative array of options
+        * @return string
+        * @see Database::select()
+        * @since 1.21
+        */
+       protected function makeGroupByWithHaving( $options ) {
+               $sql = '';
+               if ( isset( $options['GROUP BY'] ) ) {
+                       $gb = is_array( $options['GROUP BY'] )
+                               ? implode( ',', $options['GROUP BY'] )
+                               : $options['GROUP BY'];
+                       $sql .= ' GROUP BY ' . $gb;
+               }
+               if ( isset( $options['HAVING'] ) ) {
+                       $having = is_array( $options['HAVING'] )
+                               ? $this->makeList( $options['HAVING'], self::LIST_AND )
+                               : $options['HAVING'];
+                       $sql .= ' HAVING ' . $having;
+               }
+
+               return $sql;
+       }
+
+       /**
+        * Returns an optional ORDER BY
+        *
+        * @param array $options Associative array of options
+        * @return string
+        * @see Database::select()
+        * @since 1.21
+        */
+       protected function makeOrderBy( $options ) {
+               if ( isset( $options['ORDER BY'] ) ) {
+                       $ob = is_array( $options['ORDER BY'] )
+                               ? implode( ',', $options['ORDER BY'] )
+                               : $options['ORDER BY'];
+
+                       return ' ORDER BY ' . $ob;
+               }
+
+               return '';
+       }
+
+       public function select( $table, $vars, $conds = '', $fname = __METHOD__,
+               $options = [], $join_conds = [] ) {
+               $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
+
+               return $this->query( $sql, $fname );
+       }
+
+       public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               if ( is_array( $vars ) ) {
+                       $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
+               }
+
+               $options = (array)$options;
+               $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
+                       ? $options['USE INDEX']
+                       : [];
+               $ignoreIndexes = (
+                       isset( $options['IGNORE INDEX'] ) &&
+                       is_array( $options['IGNORE INDEX'] )
+               )
+                       ? $options['IGNORE INDEX']
+                       : [];
+
+               if ( is_array( $table ) ) {
+                       $from = ' FROM ' .
+                               $this->tableNamesWithIndexClauseOrJOIN(
+                                       $table, $useIndexes, $ignoreIndexes, $join_conds );
+               } elseif ( $table != '' ) {
+                       if ( $table[0] == ' ' ) {
+                               $from = ' FROM ' . $table;
+                       } else {
+                               $from = ' FROM ' .
+                                       $this->tableNamesWithIndexClauseOrJOIN(
+                                               [ $table ], $useIndexes, $ignoreIndexes, [] );
+                       }
+               } else {
+                       $from = '';
+               }
+
+               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
+                       $this->makeSelectOptions( $options );
+
+               if ( !empty( $conds ) ) {
+                       if ( is_array( $conds ) ) {
+                               $conds = $this->makeList( $conds, self::LIST_AND );
+                       }
+                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
+                               "WHERE $conds $preLimitTail";
+               } else {
+                       $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
+               }
+
+               if ( isset( $options['LIMIT'] ) ) {
+                       $sql = $this->limitResult( $sql, $options['LIMIT'],
+                               isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
+               }
+               $sql = "$sql $postLimitTail";
+
+               if ( isset( $options['EXPLAIN'] ) ) {
+                       $sql = 'EXPLAIN ' . $sql;
+               }
+
+               return $sql;
+       }
+
+       public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               $options = (array)$options;
+               $options['LIMIT'] = 1;
+               $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
+
+               if ( $res === false ) {
+                       return false;
+               }
+
+               if ( !$this->numRows( $res ) ) {
+                       return false;
+               }
+
+               $obj = $this->fetchObject( $res );
+
+               return $obj;
+       }
+
+       public function estimateRowCount(
+               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
+       ) {
+               $rows = 0;
+               $res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
+
+               if ( $res ) {
+                       $row = $this->fetchRow( $res );
+                       $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
+               }
+
+               return $rows;
+       }
+
+       public function selectRowCount(
+               $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
+       ) {
+               $rows = 0;
+               $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
+               $res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
+
+               if ( $res ) {
+                       $row = $this->fetchRow( $res );
+                       $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
+               }
+
+               return $rows;
+       }
+
+       /**
+        * Removes most variables from an SQL query and replaces them with X or N for numbers.
+        * It's only slightly flawed. Don't use for anything important.
+        *
+        * @param string $sql A SQL Query
+        *
+        * @return string
+        */
+       protected static function generalizeSQL( $sql ) {
+               # This does the same as the regexp below would do, but in such a way
+               # as to avoid crashing php on some large strings.
+               # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
+
+               $sql = str_replace( "\\\\", '', $sql );
+               $sql = str_replace( "\\'", '', $sql );
+               $sql = str_replace( "\\\"", '', $sql );
+               $sql = preg_replace( "/'.*'/s", "'X'", $sql );
+               $sql = preg_replace( '/".*"/s', "'X'", $sql );
+
+               # All newlines, tabs, etc replaced by single space
+               $sql = preg_replace( '/\s+/', ' ', $sql );
+
+               # All numbers => N,
+               # except the ones surrounded by characters, e.g. l10n
+               $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
+               $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
+
+               return $sql;
+       }
+
+       public function fieldExists( $table, $field, $fname = __METHOD__ ) {
+               $info = $this->fieldInfo( $table, $field );
+
+               return (bool)$info;
+       }
+
+       public function indexExists( $table, $index, $fname = __METHOD__ ) {
+               if ( !$this->tableExists( $table ) ) {
+                       return null;
+               }
+
+               $info = $this->indexInfo( $table, $index, $fname );
+               if ( is_null( $info ) ) {
+                       return null;
+               } else {
+                       return $info !== false;
+               }
+       }
+
+       public function tableExists( $table, $fname = __METHOD__ ) {
+               $tableRaw = $this->tableName( $table, 'raw' );
+               if ( isset( $this->mSessionTempTables[$tableRaw] ) ) {
+                       return true; // already known to exist
+               }
+
+               $table = $this->tableName( $table );
+               $old = $this->ignoreErrors( true );
+               $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
+               $this->ignoreErrors( $old );
+
+               return (bool)$res;
+       }
+
+       public function indexUnique( $table, $index ) {
+               $indexInfo = $this->indexInfo( $table, $index );
+
+               if ( !$indexInfo ) {
+                       return null;
+               }
+
+               return !$indexInfo[0]->Non_unique;
+       }
+
+       /**
+        * Helper for Database::insert().
+        *
+        * @param array $options
+        * @return string
+        */
+       protected function makeInsertOptions( $options ) {
+               return implode( ' ', $options );
+       }
+
+       public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
+               # No rows to insert, easy just return now
+               if ( !count( $a ) ) {
+                       return true;
+               }
+
+               $table = $this->tableName( $table );
+
+               if ( !is_array( $options ) ) {
+                       $options = [ $options ];
+               }
+
+               $fh = null;
+               if ( isset( $options['fileHandle'] ) ) {
+                       $fh = $options['fileHandle'];
+               }
+               $options = $this->makeInsertOptions( $options );
+
+               if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+                       $multi = true;
+                       $keys = array_keys( $a[0] );
+               } else {
+                       $multi = false;
+                       $keys = array_keys( $a );
+               }
+
+               $sql = 'INSERT ' . $options .
+                       " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+               if ( $multi ) {
+                       $first = true;
+                       foreach ( $a as $row ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $sql .= ',';
+                               }
+                               $sql .= '(' . $this->makeList( $row ) . ')';
+                       }
+               } else {
+                       $sql .= '(' . $this->makeList( $a ) . ')';
+               }
+
+               if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
+                       return false;
+               } elseif ( $fh !== null ) {
+                       return true;
+               }
+
+               return (bool)$this->query( $sql, $fname );
+       }
+
+       /**
+        * Make UPDATE options array for Database::makeUpdateOptions
+        *
+        * @param array $options
+        * @return array
+        */
+       protected function makeUpdateOptionsArray( $options ) {
+               if ( !is_array( $options ) ) {
+                       $options = [ $options ];
+               }
+
+               $opts = [];
+
+               if ( in_array( 'IGNORE', $options ) ) {
+                       $opts[] = 'IGNORE';
+               }
+
+               return $opts;
+       }
+
+       /**
+        * Make UPDATE options for the Database::update function
+        *
+        * @param array $options The options passed to Database::update
+        * @return string
+        */
+       protected function makeUpdateOptions( $options ) {
+               $opts = $this->makeUpdateOptionsArray( $options );
+
+               return implode( ' ', $opts );
+       }
+
+       public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
+               $table = $this->tableName( $table );
+               $opts = $this->makeUpdateOptions( $options );
+               $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
+
+               if ( $conds !== [] && $conds !== '*' ) {
+                       $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
+               }
+
+               return $this->query( $sql, $fname );
+       }
+
+       public function makeList( $a, $mode = self::LIST_COMMA ) {
+               if ( !is_array( $a ) ) {
+                       throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
+               }
+
+               $first = true;
+               $list = '';
+
+               foreach ( $a as $field => $value ) {
+                       if ( !$first ) {
+                               if ( $mode == self::LIST_AND ) {
+                                       $list .= ' AND ';
+                               } elseif ( $mode == self::LIST_OR ) {
+                                       $list .= ' OR ';
+                               } else {
+                                       $list .= ',';
+                               }
+                       } else {
+                               $first = false;
+                       }
+
+                       if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
+                               $list .= "($value)";
+                       } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
+                               $list .= "$value";
+                       } elseif (
+                               ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
+                       ) {
+                               // Remove null from array to be handled separately if found
+                               $includeNull = false;
+                               foreach ( array_keys( $value, null, true ) as $nullKey ) {
+                                       $includeNull = true;
+                                       unset( $value[$nullKey] );
+                               }
+                               if ( count( $value ) == 0 && !$includeNull ) {
+                                       throw new InvalidArgumentException(
+                                               __METHOD__ . ": empty input for field $field" );
+                               } elseif ( count( $value ) == 0 ) {
+                                       // only check if $field is null
+                                       $list .= "$field IS NULL";
+                               } else {
+                                       // IN clause contains at least one valid element
+                                       if ( $includeNull ) {
+                                               // Group subconditions to ensure correct precedence
+                                               $list .= '(';
+                                       }
+                                       if ( count( $value ) == 1 ) {
+                                               // Special-case single values, as IN isn't terribly efficient
+                                               // Don't necessarily assume the single key is 0; we don't
+                                               // enforce linear numeric ordering on other arrays here.
+                                               $value = array_values( $value )[0];
+                                               $list .= $field . " = " . $this->addQuotes( $value );
+                                       } else {
+                                               $list .= $field . " IN (" . $this->makeList( $value ) . ") ";
+                                       }
+                                       // if null present in array, append IS NULL
+                                       if ( $includeNull ) {
+                                               $list .= " OR $field IS NULL)";
+                                       }
+                               }
+                       } elseif ( $value === null ) {
+                               if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
+                                       $list .= "$field IS ";
+                               } elseif ( $mode == self::LIST_SET ) {
+                                       $list .= "$field = ";
+                               }
+                               $list .= 'NULL';
+                       } else {
+                               if (
+                                       $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
+                               ) {
+                                       $list .= "$field = ";
+                               }
+                               $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
+                       }
+               }
+
+               return $list;
+       }
+
+       public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
+               $conds = [];
+
+               foreach ( $data as $base => $sub ) {
+                       if ( count( $sub ) ) {
+                               $conds[] = $this->makeList(
+                                       [ $baseKey => $base, $subKey => array_keys( $sub ) ],
+                                       self::LIST_AND );
+                       }
+               }
+
+               if ( $conds ) {
+                       return $this->makeList( $conds, self::LIST_OR );
+               } else {
+                       // Nothing to search for...
+                       return false;
+               }
+       }
+
+       public function aggregateValue( $valuedata, $valuename = 'value' ) {
+               return $valuename;
+       }
+
+       public function bitNot( $field ) {
+               return "(~$field)";
+       }
+
+       public function bitAnd( $fieldLeft, $fieldRight ) {
+               return "($fieldLeft & $fieldRight)";
+       }
+
+       public function bitOr( $fieldLeft, $fieldRight ) {
+               return "($fieldLeft | $fieldRight)";
+       }
+
+       public function buildConcat( $stringList ) {
+               return 'CONCAT(' . implode( ',', $stringList ) . ')';
+       }
+
+       public function buildGroupConcatField(
+               $delim, $table, $field, $conds = '', $join_conds = []
+       ) {
+               $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
+
+               return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
+       }
+
+       public function buildStringCast( $field ) {
+               return $field;
+       }
+
+       public function selectDB( $db ) {
+               # Stub. Shouldn't cause serious problems if it's not overridden, but
+               # if your database engine supports a concept similar to MySQL's
+               # databases you may as well.
+               $this->mDBname = $db;
+
+               return true;
+       }
+
+       public function getDBname() {
+               return $this->mDBname;
+       }
+
+       public function getServer() {
+               return $this->mServer;
+       }
+
+       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
+               # use of `database`.table. But won't break things if someone wants
+               # to query a database table with a dot in the name.
+               if ( $this->isQuotedIdentifier( $name ) ) {
+                       return $name;
+               }
+
+               # Lets test for any bits of text that should never show up in a table
+               # name. Basically anything like JOIN or ON which are actually part of
+               # SQL queries, but may end up inside of the table value to combine
+               # sql. Such as how the API is doing.
+               # Note that we use a whitespace test rather than a \b test to avoid
+               # any remote case where a word like on may be inside of a table name
+               # surrounded by symbols which may be considered word breaks.
+               if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
+                       return $name;
+               }
+
+               # Split database and table into proper variables.
+               # We reverse the explode so that database.table and table both output
+               # the correct table.
+               $dbDetails = explode( '.', $name, 3 );
+               if ( count( $dbDetails ) == 3 ) {
+                       list( $database, $schema, $table ) = $dbDetails;
+                       # We don't want any prefix added in this case
+                       $prefix = '';
+               } elseif ( count( $dbDetails ) == 2 ) {
+                       list( $database, $table ) = $dbDetails;
+                       # We don't want any prefix added in this case
+                       # In dbs that support it, $database may actually be the schema
+                       # but that doesn't affect any of the functionality here
+                       $prefix = '';
+                       $schema = '';
+               } else {
+                       list( $table ) = $dbDetails;
+                       if ( isset( $this->tableAliases[$table] ) ) {
+                               $database = $this->tableAliases[$table]['dbname'];
+                               $schema = is_string( $this->tableAliases[$table]['schema'] )
+                                       ? $this->tableAliases[$table]['schema']
+                                       : $this->mSchema;
+                               $prefix = is_string( $this->tableAliases[$table]['prefix'] )
+                                       ? $this->tableAliases[$table]['prefix']
+                                       : $this->mTablePrefix;
+                       } else {
+                               $database = '';
+                               $schema = $this->mSchema; # Default schema
+                               $prefix = $this->mTablePrefix; # Default prefix
+                       }
+               }
+
+               # Quote $table and apply the prefix if not quoted.
+               # $tableName might be empty if this is called from Database::replaceVars()
+               $tableName = "{$prefix}{$table}";
+               if ( $format == 'quoted'
+                       && !$this->isQuotedIdentifier( $tableName ) && $tableName !== ''
+               ) {
+                       $tableName = $this->addIdentifierQuotes( $tableName );
+               }
+
+               # Quote $schema and merge it with the table name if needed
+               if ( strlen( $schema ) ) {
+                       if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
+                               $schema = $this->addIdentifierQuotes( $schema );
+                       }
+                       $tableName = $schema . '.' . $tableName;
+               }
+
+               # Quote $database and merge it with the table name if needed
+               if ( $database !== '' ) {
+                       if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
+                               $database = $this->addIdentifierQuotes( $database );
+                       }
+                       $tableName = $database . '.' . $tableName;
+               }
+
+               return $tableName;
+       }
+
+       public function tableNames() {
+               $inArray = func_get_args();
+               $retVal = [];
+
+               foreach ( $inArray as $name ) {
+                       $retVal[$name] = $this->tableName( $name );
+               }
+
+               return $retVal;
+       }
+
+       public function tableNamesN() {
+               $inArray = func_get_args();
+               $retVal = [];
+
+               foreach ( $inArray as $name ) {
+                       $retVal[] = $this->tableName( $name );
+               }
+
+               return $retVal;
+       }
+
+       /**
+        * Get an aliased table name
+        * e.g. tableName AS newTableName
+        *
+        * @param string $name Table name, see tableName()
+        * @param string|bool $alias Alias (optional)
+        * @return string SQL name for aliased table. Will not alias a table to its own name
+        */
+       protected function tableNameWithAlias( $name, $alias = false ) {
+               if ( !$alias || $alias == $name ) {
+                       return $this->tableName( $name );
+               } else {
+                       return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
+               }
+       }
+
+       /**
+        * Gets an array of aliased table names
+        *
+        * @param array $tables [ [alias] => table ]
+        * @return string[] See tableNameWithAlias()
+        */
+       protected function tableNamesWithAlias( $tables ) {
+               $retval = [];
+               foreach ( $tables as $alias => $table ) {
+                       if ( is_numeric( $alias ) ) {
+                               $alias = $table;
+                       }
+                       $retval[] = $this->tableNameWithAlias( $table, $alias );
+               }
+
+               return $retval;
+       }
+
+       /**
+        * Get an aliased field name
+        * e.g. fieldName AS newFieldName
+        *
+        * @param string $name Field name
+        * @param string|bool $alias Alias (optional)
+        * @return string SQL name for aliased field. Will not alias a field to its own name
+        */
+       protected function fieldNameWithAlias( $name, $alias = false ) {
+               if ( !$alias || (string)$alias === (string)$name ) {
+                       return $name;
+               } else {
+                       return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
+               }
+       }
+
+       /**
+        * Gets an array of aliased field names
+        *
+        * @param array $fields [ [alias] => field ]
+        * @return string[] See fieldNameWithAlias()
+        */
+       protected function fieldNamesWithAlias( $fields ) {
+               $retval = [];
+               foreach ( $fields as $alias => $field ) {
+                       if ( is_numeric( $alias ) ) {
+                               $alias = $field;
+                       }
+                       $retval[] = $this->fieldNameWithAlias( $field, $alias );
+               }
+
+               return $retval;
+       }
+
+       /**
+        * Get the aliased table name clause for a FROM clause
+        * which might have a JOIN and/or USE INDEX or IGNORE INDEX clause
+        *
+        * @param array $tables ( [alias] => table )
+        * @param array $use_index Same as for select()
+        * @param array $ignore_index Same as for select()
+        * @param array $join_conds Same as for select()
+        * @return string
+        */
+       protected function tableNamesWithIndexClauseOrJOIN(
+               $tables, $use_index = [], $ignore_index = [], $join_conds = []
+       ) {
+               $ret = [];
+               $retJOIN = [];
+               $use_index = (array)$use_index;
+               $ignore_index = (array)$ignore_index;
+               $join_conds = (array)$join_conds;
+
+               foreach ( $tables as $alias => $table ) {
+                       if ( !is_string( $alias ) ) {
+                               // No alias? Set it equal to the table name
+                               $alias = $table;
+                       }
+                       // Is there a JOIN clause for this table?
+                       if ( isset( $join_conds[$alias] ) ) {
+                               list( $joinType, $conds ) = $join_conds[$alias];
+                               $tableClause = $joinType;
+                               $tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
+                               if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
+                                       $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
+                                       if ( $use != '' ) {
+                                               $tableClause .= ' ' . $use;
+                                       }
+                               }
+                               if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
+                                       $ignore = $this->ignoreIndexClause(
+                                               implode( ',', (array)$ignore_index[$alias] ) );
+                                       if ( $ignore != '' ) {
+                                               $tableClause .= ' ' . $ignore;
+                                       }
+                               }
+                               $on = $this->makeList( (array)$conds, self::LIST_AND );
+                               if ( $on != '' ) {
+                                       $tableClause .= ' ON (' . $on . ')';
+                               }
+
+                               $retJOIN[] = $tableClause;
+                       } elseif ( isset( $use_index[$alias] ) ) {
+                               // Is there an INDEX clause for this table?
+                               $tableClause = $this->tableNameWithAlias( $table, $alias );
+                               $tableClause .= ' ' . $this->useIndexClause(
+                                               implode( ',', (array)$use_index[$alias] )
+                                       );
+
+                               $ret[] = $tableClause;
+                       } elseif ( isset( $ignore_index[$alias] ) ) {
+                               // Is there an INDEX clause for this table?
+                               $tableClause = $this->tableNameWithAlias( $table, $alias );
+                               $tableClause .= ' ' . $this->ignoreIndexClause(
+                                               implode( ',', (array)$ignore_index[$alias] )
+                                       );
+
+                               $ret[] = $tableClause;
+                       } else {
+                               $tableClause = $this->tableNameWithAlias( $table, $alias );
+
+                               $ret[] = $tableClause;
+                       }
+               }
+
+               // We can't separate explicit JOIN clauses with ',', use ' ' for those
+               $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
+               $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
+
+               // Compile our final table clause
+               return implode( ' ', [ $implicitJoins, $explicitJoins ] );
+       }
+
+       /**
+        * Get the name of an index in a given table.
+        *
+        * @param string $index
+        * @return string
+        */
+       protected function indexName( $index ) {
+               return $index;
+       }
+
+       public function addQuotes( $s ) {
+               if ( $s instanceof Blob ) {
+                       $s = $s->fetch();
+               }
+               if ( $s === null ) {
+                       return 'NULL';
+               } elseif ( is_bool( $s ) ) {
+                       return (int)$s;
+               } else {
+                       # This will also quote numeric values. This should be harmless,
+                       # and protects against weird problems that occur when they really
+                       # _are_ strings such as article titles and string->number->string
+                       # conversion is not 1:1.
+                       return "'" . $this->strencode( $s ) . "'";
+               }
+       }
+
+       /**
+        * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
+        * MySQL uses `backticks` while basically everything else uses double quotes.
+        * Since MySQL is the odd one out here the double quotes are our generic
+        * and we implement backticks in DatabaseMysql.
+        *
+        * @param string $s
+        * @return string
+        */
+       public function addIdentifierQuotes( $s ) {
+               return '"' . str_replace( '"', '""', $s ) . '"';
+       }
+
+       /**
+        * Returns if the given identifier looks quoted or not according to
+        * the database convention for quoting identifiers .
+        *
+        * @note Do not use this to determine if untrusted input is safe.
+        *   A malicious user can trick this function.
+        * @param string $name
+        * @return bool
+        */
+       public function isQuotedIdentifier( $name ) {
+               return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
+       }
+
+       /**
+        * @param string $s
+        * @return string
+        */
+       protected function escapeLikeInternal( $s ) {
+               return addcslashes( $s, '\%_' );
+       }
+
+       public function buildLike() {
+               $params = func_get_args();
+
+               if ( count( $params ) > 0 && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+
+               $s = '';
+
+               foreach ( $params as $value ) {
+                       if ( $value instanceof LikeMatch ) {
+                               $s .= $value->toString();
+                       } else {
+                               $s .= $this->escapeLikeInternal( $value );
+                       }
+               }
+
+               return " LIKE {$this->addQuotes( $s )} ";
+       }
+
+       public function anyChar() {
+               return new LikeMatch( '_' );
+       }
+
+       public function anyString() {
+               return new LikeMatch( '%' );
+       }
+
+       public function nextSequenceValue( $seqName ) {
+               return null;
+       }
+
+       /**
+        * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
+        * is only needed because a) MySQL must be as efficient as possible due to
+        * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
+        * which index to pick. Anyway, other databases might have different
+        * indexes on a given table. So don't bother overriding this unless you're
+        * MySQL.
+        * @param string $index
+        * @return string
+        */
+       public function useIndexClause( $index ) {
+               return '';
+       }
+
+       /**
+        * IGNORE INDEX clause. Unlikely to be useful for anything but MySQL. This
+        * is only needed because a) MySQL must be as efficient as possible due to
+        * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
+        * which index to pick. Anyway, other databases might have different
+        * indexes on a given table. So don't bother overriding this unless you're
+        * MySQL.
+        * @param string $index
+        * @return string
+        */
+       public function ignoreIndexClause( $index ) {
+               return '';
+       }
+
+       public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
+               $quotedTable = $this->tableName( $table );
+
+               if ( count( $rows ) == 0 ) {
+                       return;
+               }
+
+               # Single row case
+               if ( !is_array( reset( $rows ) ) ) {
+                       $rows = [ $rows ];
+               }
+
+               // @FXIME: this is not atomic, but a trx would break affectedRows()
+               foreach ( $rows as $row ) {
+                       # Delete rows which collide
+                       if ( $uniqueIndexes ) {
+                               $sql = "DELETE FROM $quotedTable WHERE ";
+                               $first = true;
+                               foreach ( $uniqueIndexes as $index ) {
+                                       if ( $first ) {
+                                               $first = false;
+                                               $sql .= '( ';
+                                       } else {
+                                               $sql .= ' ) OR ( ';
+                                       }
+                                       if ( is_array( $index ) ) {
+                                               $first2 = true;
+                                               foreach ( $index as $col ) {
+                                                       if ( $first2 ) {
+                                                               $first2 = false;
+                                                       } else {
+                                                               $sql .= ' AND ';
+                                                       }
+                                                       $sql .= $col . '=' . $this->addQuotes( $row[$col] );
+                                               }
+                                       } else {
+                                               $sql .= $index . '=' . $this->addQuotes( $row[$index] );
+                                       }
+                               }
+                               $sql .= ' )';
+                               $this->query( $sql, $fname );
+                       }
+
+                       # Now insert the row
+                       $this->insert( $table, $row, $fname );
+               }
+       }
+
+       /**
+        * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
+        * statement.
+        *
+        * @param string $table Table name
+        * @param array|string $rows Row(s) to insert
+        * @param string $fname Caller function name
+        *
+        * @return ResultWrapper
+        */
+       protected function nativeReplace( $table, $rows, $fname ) {
+               $table = $this->tableName( $table );
+
+               # Single row case
+               if ( !is_array( reset( $rows ) ) ) {
+                       $rows = [ $rows ];
+               }
+
+               $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
+               $first = true;
+
+               foreach ( $rows as $row ) {
+                       if ( $first ) {
+                               $first = false;
+                       } else {
+                               $sql .= ',';
+                       }
+
+                       $sql .= '(' . $this->makeList( $row ) . ')';
+               }
+
+               return $this->query( $sql, $fname );
+       }
+
+       public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
+               $fname = __METHOD__
+       ) {
+               if ( !count( $rows ) ) {
+                       return true; // nothing to do
+               }
+
+               if ( !is_array( reset( $rows ) ) ) {
+                       $rows = [ $rows ];
+               }
+
+               if ( count( $uniqueIndexes ) ) {
+                       $clauses = []; // list WHERE clauses that each identify a single row
+                       foreach ( $rows as $row ) {
+                               foreach ( $uniqueIndexes as $index ) {
+                                       $index = is_array( $index ) ? $index : [ $index ]; // columns
+                                       $rowKey = []; // unique key to this row
+                                       foreach ( $index as $column ) {
+                                               $rowKey[$column] = $row[$column];
+                                       }
+                                       $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
+                               }
+                       }
+                       $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
+               } else {
+                       $where = false;
+               }
+
+               $useTrx = !$this->mTrxLevel;
+               if ( $useTrx ) {
+                       $this->begin( $fname, self::TRANSACTION_INTERNAL );
+               }
+               try {
+                       # Update any existing conflicting row(s)
+                       if ( $where !== false ) {
+                               $ok = $this->update( $table, $set, $where, $fname );
+                       } else {
+                               $ok = true;
+                       }
+                       # Now insert any non-conflicting row(s)
+                       $ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
+               } catch ( Exception $e ) {
+                       if ( $useTrx ) {
+                               $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                       }
+                       throw $e;
+               }
+               if ( $useTrx ) {
+                       $this->commit( $fname, self::FLUSHING_INTERNAL );
+               }
+
+               return $ok;
+       }
+
+       public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
+               $fname = __METHOD__
+       ) {
+               if ( !$conds ) {
+                       throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
+               }
+
+               $delTable = $this->tableName( $delTable );
+               $joinTable = $this->tableName( $joinTable );
+               $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
+               if ( $conds != '*' ) {
+                       $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
+               }
+               $sql .= ')';
+
+               $this->query( $sql, $fname );
+       }
+
+       public function textFieldSize( $table, $field ) {
+               $table = $this->tableName( $table );
+               $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
+               $res = $this->query( $sql, __METHOD__ );
+               $row = $this->fetchObject( $res );
+
+               $m = [];
+
+               if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
+                       $size = $m[1];
+               } else {
+                       $size = -1;
+               }
+
+               return $size;
+       }
+
+       public function delete( $table, $conds, $fname = __METHOD__ ) {
+               if ( !$conds ) {
+                       throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
+               }
+
+               $table = $this->tableName( $table );
+               $sql = "DELETE FROM $table";
+
+               if ( $conds != '*' ) {
+                       if ( is_array( $conds ) ) {
+                               $conds = $this->makeList( $conds, self::LIST_AND );
+                       }
+                       $sql .= ' WHERE ' . $conds;
+               }
+
+               return $this->query( $sql, $fname );
+       }
+
+       public function insertSelect(
+               $destTable, $srcTable, $varMap, $conds,
+               $fname = __METHOD__, $insertOptions = [], $selectOptions = []
+       ) {
+               if ( $this->cliMode ) {
+                       // For massive migrations with downtime, we don't want to select everything
+                       // into memory and OOM, so do all this native on the server side if possible.
+                       return $this->nativeInsertSelect(
+                               $destTable,
+                               $srcTable,
+                               $varMap,
+                               $conds,
+                               $fname,
+                               $insertOptions,
+                               $selectOptions
+                       );
+               }
+
+               // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
+               // on only the master (without needing row-based-replication). It also makes it easy to
+               // know how big the INSERT is going to be.
+               $fields = [];
+               foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
+                       $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
+               }
+               $selectOptions[] = 'FOR UPDATE';
+               $res = $this->select( $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions );
+               if ( !$res ) {
+                       return false;
+               }
+
+               $rows = [];
+               foreach ( $res as $row ) {
+                       $rows[] = (array)$row;
+               }
+
+               return $this->insert( $destTable, $rows, $fname, $insertOptions );
+       }
+
+       protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
+               $fname = __METHOD__,
+               $insertOptions = [], $selectOptions = []
+       ) {
+               $destTable = $this->tableName( $destTable );
+
+               if ( !is_array( $insertOptions ) ) {
+                       $insertOptions = [ $insertOptions ];
+               }
+
+               $insertOptions = $this->makeInsertOptions( $insertOptions );
+
+               if ( !is_array( $selectOptions ) ) {
+                       $selectOptions = [ $selectOptions ];
+               }
+
+               list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions(
+                       $selectOptions );
+
+               if ( is_array( $srcTable ) ) {
+                       $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
+               } else {
+                       $srcTable = $this->tableName( $srcTable );
+               }
+
+               $sql = "INSERT $insertOptions" .
+                       " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+                       " SELECT $startOpts " . implode( ',', $varMap ) .
+                       " FROM $srcTable $useIndex $ignoreIndex ";
+
+               if ( $conds != '*' ) {
+                       if ( is_array( $conds ) ) {
+                               $conds = $this->makeList( $conds, self::LIST_AND );
+                       }
+                       $sql .= " WHERE $conds";
+               }
+
+               $sql .= " $tailOpts";
+
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * Construct a LIMIT query with optional offset. This is used for query
+        * pages. The SQL should be adjusted so that only the first $limit rows
+        * are returned. If $offset is provided as well, then the first $offset
+        * rows should be discarded, and the next $limit rows should be returned.
+        * If the result of the query is not ordered, then the rows to be returned
+        * are theoretically arbitrary.
+        *
+        * $sql is expected to be a SELECT, if that makes a difference.
+        *
+        * The version provided by default works in MySQL and SQLite. It will very
+        * likely need to be overridden for most other DBMSes.
+        *
+        * @param string $sql SQL query we will append the limit too
+        * @param int $limit The SQL limit
+        * @param int|bool $offset The SQL offset (default false)
+        * @throws DBUnexpectedError
+        * @return string
+        */
+       public function limitResult( $sql, $limit, $offset = false ) {
+               if ( !is_numeric( $limit ) ) {
+                       throw new DBUnexpectedError( $this,
+                               "Invalid non-numeric limit passed to limitResult()\n" );
+               }
+
+               return "$sql LIMIT "
+               . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
+               . "{$limit} ";
+       }
+
+       public function unionSupportsOrderAndLimit() {
+               return true; // True for almost every DB supported
+       }
+
+       public function unionQueries( $sqls, $all ) {
+               $glue = $all ? ') UNION ALL (' : ') UNION (';
+
+               return '(' . implode( $glue, $sqls ) . ')';
+       }
+
+       public function conditional( $cond, $trueVal, $falseVal ) {
+               if ( is_array( $cond ) ) {
+                       $cond = $this->makeList( $cond, self::LIST_AND );
+               }
+
+               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+       }
+
+       public function strreplace( $orig, $old, $new ) {
+               return "REPLACE({$orig}, {$old}, {$new})";
+       }
+
+       public function getServerUptime() {
+               return 0;
+       }
+
+       public function wasDeadlock() {
+               return false;
+       }
+
+       public function wasLockTimeout() {
+               return false;
+       }
+
+       public function wasErrorReissuable() {
+               return false;
+       }
+
+       public function wasReadOnlyError() {
+               return false;
+       }
+
+       /**
+        * Do not use this method outside of Database/DBError classes
+        *
+        * @param integer|string $errno
+        * @return bool Whether the given query error was a connection drop
+        */
+       public function wasConnectionError( $errno ) {
+               return false;
+       }
+
+       public function deadlockLoop() {
+               $args = func_get_args();
+               $function = array_shift( $args );
+               $tries = self::DEADLOCK_TRIES;
+
+               $this->begin( __METHOD__ );
+
+               $retVal = null;
+               /** @var Exception $e */
+               $e = null;
+               do {
+                       try {
+                               $retVal = call_user_func_array( $function, $args );
+                               break;
+                       } catch ( DBQueryError $e ) {
+                               if ( $this->wasDeadlock() ) {
+                                       // Retry after a randomized delay
+                                       usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
+                               } else {
+                                       // Throw the error back up
+                                       throw $e;
+                               }
+                       }
+               } while ( --$tries > 0 );
+
+               if ( $tries <= 0 ) {
+                       // Too many deadlocks; give up
+                       $this->rollback( __METHOD__ );
+                       throw $e;
+               } else {
+                       $this->commit( __METHOD__ );
+
+                       return $retVal;
+               }
+       }
+
+       public function masterPosWait( DBMasterPos $pos, $timeout ) {
+               # Real waits are implemented in the subclass.
+               return 0;
+       }
+
+       public function getReplicaPos() {
+               # Stub
+               return false;
+       }
+
+       public function getMasterPos() {
+               # Stub
+               return false;
+       }
+
+       public function serverIsReadOnly() {
+               return false;
+       }
+
+       final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
+               if ( !$this->mTrxLevel ) {
+                       throw new DBUnexpectedError( $this, "No transaction is active." );
+               }
+               $this->mTrxEndCallbacks[] = [ $callback, $fname ];
+       }
+
+       final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
+               $this->mTrxIdleCallbacks[] = [ $callback, $fname ];
+               if ( !$this->mTrxLevel ) {
+                       $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
+               }
+       }
+
+       final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
+               if ( $this->mTrxLevel ) {
+                       $this->mTrxPreCommitCallbacks[] = [ $callback, $fname ];
+               } else {
+                       // If no transaction is active, then make one for this callback
+                       $this->startAtomic( __METHOD__ );
+                       try {
+                               call_user_func( $callback );
+                               $this->endAtomic( __METHOD__ );
+                       } catch ( Exception $e ) {
+                               $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
+                               throw $e;
+                       }
+               }
+       }
+
+       final public function setTransactionListener( $name, callable $callback = null ) {
+               if ( $callback ) {
+                       $this->mTrxRecurringCallbacks[$name] = $callback;
+               } else {
+                       unset( $this->mTrxRecurringCallbacks[$name] );
+               }
+       }
+
+       /**
+        * Whether to disable running of post-COMMIT/ROLLBACK callbacks
+        *
+        * This method should not be used outside of Database/LoadBalancer
+        *
+        * @param bool $suppress
+        * @since 1.28
+        */
+       final public function setTrxEndCallbackSuppression( $suppress ) {
+               $this->mTrxEndCallbacksSuppressed = $suppress;
+       }
+
+       /**
+        * Actually run and consume any "on transaction idle/resolution" callbacks.
+        *
+        * This method should not be used outside of Database/LoadBalancer
+        *
+        * @param integer $trigger IDatabase::TRIGGER_* constant
+        * @since 1.20
+        * @throws Exception
+        */
+       public function runOnTransactionIdleCallbacks( $trigger ) {
+               if ( $this->mTrxEndCallbacksSuppressed ) {
+                       return;
+               }
+
+               $autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
+               /** @var Exception $e */
+               $e = null; // first exception
+               do { // callbacks may add callbacks :)
+                       $callbacks = array_merge(
+                               $this->mTrxIdleCallbacks,
+                               $this->mTrxEndCallbacks // include "transaction resolution" callbacks
+                       );
+                       $this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
+                       $this->mTrxEndCallbacks = []; // consumed (recursion guard)
+                       foreach ( $callbacks as $callback ) {
+                               try {
+                                       list( $phpCallback ) = $callback;
+                                       $this->clearFlag( self::DBO_TRX ); // make each query its own transaction
+                                       call_user_func_array( $phpCallback, [ $trigger ] );
+                                       if ( $autoTrx ) {
+                                               $this->setFlag( self::DBO_TRX ); // restore automatic begin()
+                                       } else {
+                                               $this->clearFlag( self::DBO_TRX ); // restore auto-commit
+                                       }
+                               } catch ( Exception $ex ) {
+                                       call_user_func( $this->errorLogger, $ex );
+                                       $e = $e ?: $ex;
+                                       // Some callbacks may use startAtomic/endAtomic, so make sure
+                                       // their transactions are ended so other callbacks don't fail
+                                       if ( $this->trxLevel() ) {
+                                               $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
+                                       }
+                               }
+                       }
+               } while ( count( $this->mTrxIdleCallbacks ) );
+
+               if ( $e instanceof Exception ) {
+                       throw $e; // re-throw any first exception
+               }
+       }
+
+       /**
+        * Actually run and consume any "on transaction pre-commit" callbacks.
+        *
+        * This method should not be used outside of Database/LoadBalancer
+        *
+        * @since 1.22
+        * @throws Exception
+        */
+       public function runOnTransactionPreCommitCallbacks() {
+               $e = null; // first exception
+               do { // callbacks may add callbacks :)
+                       $callbacks = $this->mTrxPreCommitCallbacks;
+                       $this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
+                       foreach ( $callbacks as $callback ) {
+                               try {
+                                       list( $phpCallback ) = $callback;
+                                       call_user_func( $phpCallback );
+                               } catch ( Exception $ex ) {
+                                       call_user_func( $this->errorLogger, $ex );
+                                       $e = $e ?: $ex;
+                               }
+                       }
+               } while ( count( $this->mTrxPreCommitCallbacks ) );
+
+               if ( $e instanceof Exception ) {
+                       throw $e; // re-throw any first exception
+               }
+       }
+
+       /**
+        * Actually run any "transaction listener" callbacks.
+        *
+        * This method should not be used outside of Database/LoadBalancer
+        *
+        * @param integer $trigger IDatabase::TRIGGER_* constant
+        * @throws Exception
+        * @since 1.20
+        */
+       public function runTransactionListenerCallbacks( $trigger ) {
+               if ( $this->mTrxEndCallbacksSuppressed ) {
+                       return;
+               }
+
+               /** @var Exception $e */
+               $e = null; // first exception
+
+               foreach ( $this->mTrxRecurringCallbacks as $phpCallback ) {
+                       try {
+                               $phpCallback( $trigger, $this );
+                       } catch ( Exception $ex ) {
+                               call_user_func( $this->errorLogger, $ex );
+                               $e = $e ?: $ex;
+                       }
+               }
+
+               if ( $e instanceof Exception ) {
+                       throw $e; // re-throw any first exception
+               }
+       }
+
+       final public function startAtomic( $fname = __METHOD__ ) {
+               if ( !$this->mTrxLevel ) {
+                       $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( self::DBO_TRX ) ) {
+                               $this->mTrxAutomaticAtomic = true;
+                       }
+               }
+
+               $this->mTrxAtomicLevels[] = $fname;
+       }
+
+       final public function endAtomic( $fname = __METHOD__ ) {
+               if ( !$this->mTrxLevel ) {
+                       throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
+               }
+               if ( !$this->mTrxAtomicLevels ||
+                       array_pop( $this->mTrxAtomicLevels ) !== $fname
+               ) {
+                       throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
+               }
+
+               if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
+                       $this->commit( $fname, self::FLUSHING_INTERNAL );
+               }
+       }
+
+       final public function doAtomicSection( $fname, callable $callback ) {
+               $this->startAtomic( $fname );
+               try {
+                       $res = call_user_func_array( $callback, [ $this, $fname ] );
+               } catch ( Exception $e ) {
+                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                       throw $e;
+               }
+               $this->endAtomic( $fname );
+
+               return $res;
+       }
+
+       final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
+               // Protect against mismatched atomic section, transaction nesting, and snapshot loss
+               if ( $this->mTrxLevel ) {
+                       if ( $this->mTrxAtomicLevels ) {
+                               $levels = implode( ', ', $this->mTrxAtomicLevels );
+                               $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
+                               throw new DBUnexpectedError( $this, $msg );
+                       } elseif ( !$this->mTrxAutomatic ) {
+                               $msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
+                               throw new DBUnexpectedError( $this, $msg );
+                       } else {
+                               // @TODO: make this an exception at some point
+                               $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
+                               $this->queryLogger->error( $msg );
+                               return; // join the main transaction set
+                       }
+               } 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 );
+                       return; // let any writes be in the main transaction
+               }
+
+               // Avoid fatals if close() was called
+               $this->assertOpen();
+
+               $this->doBegin( $fname );
+               $this->mTrxTimestamp = microtime( true );
+               $this->mTrxFname = $fname;
+               $this->mTrxDoneWrites = false;
+               $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
+               $this->mTrxAutomaticAtomic = false;
+               $this->mTrxAtomicLevels = [];
+               $this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
+               $this->mTrxWriteDuration = 0.0;
+               $this->mTrxWriteQueryCount = 0;
+               $this->mTrxWriteAdjDuration = 0.0;
+               $this->mTrxWriteAdjQueryCount = 0;
+               $this->mTrxWriteCallers = [];
+               // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
+               // Get an estimate of the replica DB lag before then, treating estimate staleness
+               // as lag itself just to be safe
+               $status = $this->getApproximateLagStatus();
+               $this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
+       }
+
+       /**
+        * Issues the BEGIN command to the database server.
+        *
+        * @see Database::begin()
+        * @param string $fname
+        */
+       protected function doBegin( $fname ) {
+               $this->query( 'BEGIN', $fname );
+               $this->mTrxLevel = 1;
+       }
+
+       final public function commit( $fname = __METHOD__, $flush = '' ) {
+               if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
+                       // There are still atomic sections open. This cannot be ignored
+                       $levels = implode( ', ', $this->mTrxAtomicLevels );
+                       throw new DBUnexpectedError(
+                               $this,
+                               "$fname: Got COMMIT while atomic sections $levels are still open."
+                       );
+               }
+
+               if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
+                       if ( !$this->mTrxLevel ) {
+                               return; // nothing to do
+                       } elseif ( !$this->mTrxAutomatic ) {
+                               throw new DBUnexpectedError(
+                                       $this,
+                                       "$fname: Flushing an explicit transaction, getting out of sync."
+                               );
+                       }
+               } else {
+                       if ( !$this->mTrxLevel ) {
+                               $this->queryLogger->error(
+                                       "$fname: No transaction to commit, something got out of sync." );
+                               return; // nothing to do
+                       } elseif ( $this->mTrxAutomatic ) {
+                               // @TODO: make this an exception at some point
+                               $msg = "$fname: Explicit commit of implicit transaction.";
+                               $this->queryLogger->error( $msg );
+                               return; // wait for the main transaction set commit round
+                       }
+               }
+
+               // Avoid fatals if close() was called
+               $this->assertOpen();
+
+               $this->runOnTransactionPreCommitCallbacks();
+               $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
+               $this->doCommit( $fname );
+               if ( $this->mTrxDoneWrites ) {
+                       $this->mDoneWrites = microtime( true );
+                       $this->trxProfiler->transactionWritingOut(
+                               $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
+               }
+
+               $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
+               $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
+       }
+
+       /**
+        * Issues the COMMIT command to the database server.
+        *
+        * @see Database::commit()
+        * @param string $fname
+        */
+       protected function doCommit( $fname ) {
+               if ( $this->mTrxLevel ) {
+                       $this->query( 'COMMIT', $fname );
+                       $this->mTrxLevel = 0;
+               }
+       }
+
+       final public function rollback( $fname = __METHOD__, $flush = '' ) {
+               if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
+                       if ( !$this->mTrxLevel ) {
+                               return; // nothing to do
+                       }
+               } else {
+                       if ( !$this->mTrxLevel ) {
+                               $this->queryLogger->error(
+                                       "$fname: No transaction to rollback, something got out of sync." );
+                               return; // nothing to do
+                       } elseif ( $this->getFlag( self::DBO_TRX ) ) {
+                               throw new DBUnexpectedError(
+                                       $this,
+                                       "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
+                               );
+                       }
+               }
+
+               // Avoid fatals if close() was called
+               $this->assertOpen();
+
+               $this->doRollback( $fname );
+               $this->mTrxAtomicLevels = [];
+               if ( $this->mTrxDoneWrites ) {
+                       $this->trxProfiler->transactionWritingOut(
+                               $this->mServer, $this->mDBname, $this->mTrxShortId );
+               }
+
+               $this->mTrxIdleCallbacks = []; // clear
+               $this->mTrxPreCommitCallbacks = []; // clear
+               $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+               $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
+       }
+
+       /**
+        * Issues the ROLLBACK command to the database server.
+        *
+        * @see Database::rollback()
+        * @param string $fname
+        */
+       protected function doRollback( $fname ) {
+               if ( $this->mTrxLevel ) {
+                       # Disconnects cause rollback anyway, so ignore those errors
+                       $ignoreErrors = true;
+                       $this->query( 'ROLLBACK', $fname, $ignoreErrors );
+                       $this->mTrxLevel = 0;
+               }
+       }
+
+       public function flushSnapshot( $fname = __METHOD__ ) {
+               if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
+                       // This only flushes transactions to clear snapshots, not to write data
+                       $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
+                       throw new DBUnexpectedError(
+                               $this,
+                               "$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
+                       );
+               }
+
+               $this->commit( $fname, self::FLUSHING_INTERNAL );
+       }
+
+       public function explicitTrxActive() {
+               return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
+       }
+
+       /**
+        * Creates a new table with structure copied from existing table
+        * Note that unlike most database abstraction functions, this function does not
+        * automatically append database prefix, because it works at a lower
+        * abstraction level.
+        * The table names passed to this function shall not be quoted (this
+        * function calls addIdentifierQuotes when needed).
+        *
+        * @param string $oldName Name of table whose structure should be copied
+        * @param string $newName Name of table to be created
+        * @param bool $temporary Whether the new table should be temporary
+        * @param string $fname Calling function name
+        * @throws RuntimeException
+        * @return bool True if operation was successful
+        */
+       public function duplicateTableStructure( $oldName, $newName, $temporary = false,
+               $fname = __METHOD__
+       ) {
+               throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
+       }
+
+       public function listTables( $prefix = null, $fname = __METHOD__ ) {
+               throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
+       }
+
+       public function listViews( $prefix = null, $fname = __METHOD__ ) {
+               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
+               return $t->getTimestamp( TS_MW );
+       }
+
+       public function timestampOrNull( $ts = null ) {
+               if ( is_null( $ts ) ) {
+                       return null;
+               } else {
+                       return $this->timestamp( $ts );
+               }
+       }
+
+       /**
+        * Take the result from a query, and wrap it in a ResultWrapper if
+        * necessary. Boolean values are passed through as is, to indicate success
+        * of write queries or failure.
+        *
+        * Once upon a time, Database::query() returned a bare MySQL result
+        * resource, and it was necessary to call this function to convert it to
+        * a wrapper. Nowadays, raw database objects are never exposed to external
+        * callers, so this is unnecessary in external code.
+        *
+        * @param bool|ResultWrapper|resource|object $result
+        * @return bool|ResultWrapper
+        */
+       protected function resultObject( $result ) {
+               if ( !$result ) {
+                       return false;
+               } elseif ( $result instanceof ResultWrapper ) {
+                       return $result;
+               } elseif ( $result === true ) {
+                       // Successful write query
+                       return $result;
+               } else {
+                       return new ResultWrapper( $this, $result );
+               }
+       }
+
+       public function ping( &$rtt = null ) {
+               // Avoid hitting the server if it was hit recently
+               if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
+                       if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
+                               $rtt = $this->mRTTEstimate;
+                               return true; // don't care about $rtt
+                       }
+               }
+
+               // This will reconnect if possible or return false if not
+               $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
+               $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
+               $this->restoreFlags( self::RESTORE_PRIOR );
+
+               if ( $ok ) {
+                       $rtt = $this->mRTTEstimate;
+               }
+
+               return $ok;
+       }
+
+       /**
+        * @return bool
+        */
+       protected function reconnect() {
+               $this->closeConnection();
+               $this->mOpened = false;
+               $this->mConn = false;
+               try {
+                       $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+                       $this->lastPing = microtime( true );
+                       $ok = true;
+               } catch ( DBConnectionError $e ) {
+                       $ok = false;
+               }
+
+               return $ok;
+       }
+
+       public function getSessionLagStatus() {
+               return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
+       }
+
+       /**
+        * Get the replica DB lag when the current transaction started
+        *
+        * This is useful when transactions might use snapshot isolation
+        * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
+        * is this lag plus transaction duration. If they don't, it is still
+        * safe to be pessimistic. This returns null if there is no transaction.
+        *
+        * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
+        * @since 1.27
+        */
+       protected function getTransactionLagStatus() {
+               return $this->mTrxLevel
+                       ? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
+                       : null;
+       }
+
+       /**
+        * Get a replica DB lag estimate for this server
+        *
+        * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
+        * @since 1.27
+        */
+       protected function getApproximateLagStatus() {
+               return [
+                       'lag'   => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
+                       'since' => microtime( true )
+               ];
+       }
+
+       /**
+        * Merge the result of getSessionLagStatus() for several DBs
+        * using the most pessimistic values to estimate the lag of
+        * any data derived from them in combination
+        *
+        * This is information is useful for caching modules
+        *
+        * @see WANObjectCache::set()
+        * @see WANObjectCache::getWithSetCallback()
+        *
+        * @param IDatabase $db1
+        * @param IDatabase ...
+        * @return array Map of values:
+        *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
+        *   - since: oldest UNIX timestamp of any of the DB lag estimates
+        *   - pending: whether any of the DBs have uncommitted changes
+        * @since 1.27
+        */
+       public static function getCacheSetOptions( IDatabase $db1 ) {
+               $res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
+               foreach ( func_get_args() as $db ) {
+                       /** @var IDatabase $db */
+                       $status = $db->getSessionLagStatus();
+                       if ( $status['lag'] === false ) {
+                               $res['lag'] = false;
+                       } elseif ( $res['lag'] !== false ) {
+                               $res['lag'] = max( $res['lag'], $status['lag'] );
+                       }
+                       $res['since'] = min( $res['since'], $status['since'] );
+                       $res['pending'] = $res['pending'] ?: $db->writesPending();
+               }
+
+               return $res;
+       }
+
+       public function getLag() {
+               return 0;
+       }
+
+       public function maxListLen() {
+               return 0;
+       }
+
+       public function encodeBlob( $b ) {
+               return $b;
+       }
+
+       public function decodeBlob( $b ) {
+               if ( $b instanceof Blob ) {
+                       $b = $b->fetch();
+               }
+               return $b;
+       }
+
+       public function setSessionOptions( array $options ) {
+       }
+
+       public function sourceFile(
+               $filename,
+               callable $lineCallback = null,
+               callable $resultCallback = null,
+               $fname = false,
+               callable $inputCallback = null
+       ) {
+               MediaWiki\suppressWarnings();
+               $fp = fopen( $filename, 'r' );
+               MediaWiki\restoreWarnings();
+
+               if ( false === $fp ) {
+                       throw new RuntimeException( "Could not open \"{$filename}\".\n" );
+               }
+
+               if ( !$fname ) {
+                       $fname = __METHOD__ . "( $filename )";
+               }
+
+               try {
+                       $error = $this->sourceStream(
+                               $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
+               } catch ( Exception $e ) {
+                       fclose( $fp );
+                       throw $e;
+               }
+
+               fclose( $fp );
+
+               return $error;
+       }
+
+       public function setSchemaVars( $vars ) {
+               $this->mSchemaVars = $vars;
+       }
+
+       public function sourceStream(
+               $fp,
+               callable $lineCallback = null,
+               callable $resultCallback = null,
+               $fname = __METHOD__,
+               callable $inputCallback = null
+       ) {
+               $cmd = '';
+
+               while ( !feof( $fp ) ) {
+                       if ( $lineCallback ) {
+                               call_user_func( $lineCallback );
+                       }
+
+                       $line = trim( fgets( $fp ) );
+
+                       if ( $line == '' ) {
+                               continue;
+                       }
+
+                       if ( '-' == $line[0] && '-' == $line[1] ) {
+                               continue;
+                       }
+
+                       if ( $cmd != '' ) {
+                               $cmd .= ' ';
+                       }
+
+                       $done = $this->streamStatementEnd( $cmd, $line );
+
+                       $cmd .= "$line\n";
+
+                       if ( $done || feof( $fp ) ) {
+                               $cmd = $this->replaceVars( $cmd );
+
+                               if ( !$inputCallback || call_user_func( $inputCallback, $cmd ) ) {
+                                       $res = $this->query( $cmd, $fname );
+
+                                       if ( $resultCallback ) {
+                                               call_user_func( $resultCallback, $res, $this );
+                                       }
+
+                                       if ( false === $res ) {
+                                               $err = $this->lastError();
+
+                                               return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+                                       }
+                               }
+                               $cmd = '';
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * 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 ) {
+               if ( $this->delimiter ) {
+                       $prev = $newLine;
+                       $newLine = preg_replace(
+                               '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
+                       if ( $newLine != $prev ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Database independent variable replacement. Replaces a set of variables
+        * in an SQL statement with their contents as given by $this->getSchemaVars().
+        *
+        * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
+        *
+        * - '{$var}' should be used for text and is passed through the database's
+        *   addQuotes method.
+        * - `{$var}` should be used for identifiers (e.g. table and database names).
+        *   It is passed through the database's addIdentifierQuotes method which
+        *   can be overridden if the database uses something other than backticks.
+        * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
+        *   database's tableName method.
+        * - / *i* / passes the name that follows through the database's indexName method.
+        * - In all other cases, / *$var* / is left unencoded. Except for table options,
+        *   its use should be avoided. In 1.24 and older, string encoding was applied.
+        *
+        * @param string $ins SQL statement to replace variables in
+        * @return string The new SQL statement with variables replaced
+        */
+       protected function replaceVars( $ins ) {
+               $vars = $this->getSchemaVars();
+               return preg_replace_callback(
+                       '!
+                               /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
+                               \'\{\$ (\w+) }\'                  | # 3. addQuotes
+                               `\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
+                               /\*\$ (\w+) \*/                     # 5. leave unencoded
+                       !x',
+                       function ( $m ) use ( $vars ) {
+                               // Note: Because of <https://bugs.php.net/bug.php?id=51881>,
+                               // check for both nonexistent keys *and* the empty string.
+                               if ( isset( $m[1] ) && $m[1] !== '' ) {
+                                       if ( $m[1] === 'i' ) {
+                                               return $this->indexName( $m[2] );
+                                       } else {
+                                               return $this->tableName( $m[2] );
+                                       }
+                               } elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
+                                       return $this->addQuotes( $vars[$m[3]] );
+                               } elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
+                                       return $this->addIdentifierQuotes( $vars[$m[4]] );
+                               } elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
+                                       return $vars[$m[5]];
+                               } else {
+                                       return $m[0];
+                               }
+                       },
+                       $ins
+               );
+       }
+
+       /**
+        * Get schema variables. If none have been set via setSchemaVars(), then
+        * use some defaults from the current object.
+        *
+        * @return array
+        */
+       protected function getSchemaVars() {
+               if ( $this->mSchemaVars ) {
+                       return $this->mSchemaVars;
+               } else {
+                       return $this->getDefaultSchemaVars();
+               }
+       }
+
+       /**
+        * Get schema variables to use if none have been set via setSchemaVars().
+        *
+        * Override this in derived classes to provide variables for tables.sql
+        * and SQL patch files.
+        *
+        * @return array
+        */
+       protected function getDefaultSchemaVars() {
+               return [];
+       }
+
+       public function lockIsFree( $lockName, $method ) {
+               return true;
+       }
+
+       public function lock( $lockName, $method, $timeout = 5 ) {
+               $this->mNamedLocksHeld[$lockName] = 1;
+
+               return true;
+       }
+
+       public function unlock( $lockName, $method ) {
+               unset( $this->mNamedLocksHeld[$lockName] );
+
+               return true;
+       }
+
+       public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
+               if ( $this->writesOrCallbacksPending() ) {
+                       // This only flushes transactions to clear snapshots, not to write data
+                       $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
+                       throw new DBUnexpectedError(
+                               $this,
+                               "$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
+                       );
+               }
+
+               if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
+                       return null;
+               }
+
+               $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
+                       if ( $this->trxLevel() ) {
+                               // There is a good chance an exception was thrown, causing any early return
+                               // from the caller. Let any error handler get a chance to issue rollback().
+                               // If there isn't one, let the error bubble up and trigger server-side rollback.
+                               $this->onTransactionResolution(
+                                       function () use ( $lockKey, $fname ) {
+                                               $this->unlock( $lockKey, $fname );
+                                       },
+                                       $fname
+                               );
+                       } else {
+                               $this->unlock( $lockKey, $fname );
+                       }
+               } );
+
+               $this->commit( $fname, self::FLUSHING_INTERNAL );
+
+               return $unlocker;
+       }
+
+       public function namedLocksEnqueue() {
+               return false;
+       }
+
+       /**
+        * Lock specific tables
+        *
+        * @param array $read Array of tables to lock for read access
+        * @param array $write Array of tables to lock for write access
+        * @param string $method Name of caller
+        * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
+        * @return bool
+        */
+       public function lockTables( $read, $write, $method, $lowPriority = true ) {
+               return true;
+       }
+
+       /**
+        * Unlock specific tables
+        *
+        * @param string $method The caller
+        * @return bool
+        */
+       public function unlockTables( $method ) {
+               return true;
+       }
+
+       /**
+        * Delete a table
+        * @param string $tableName
+        * @param string $fName
+        * @return bool|ResultWrapper
+        * @since 1.18
+        */
+       public function dropTable( $tableName, $fName = __METHOD__ ) {
+               if ( !$this->tableExists( $tableName, $fName ) ) {
+                       return false;
+               }
+               $sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
+
+               return $this->query( $sql, $fName );
+       }
+
+       public function getInfinity() {
+               return 'infinity';
+       }
+
+       public function encodeExpiry( $expiry ) {
+               return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
+                       ? $this->getInfinity()
+                       : $this->timestamp( $expiry );
+       }
+
+       public function decodeExpiry( $expiry, $format = TS_MW ) {
+               if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
+                       return 'infinity';
+               }
+
+               return ConvertibleTimestamp::convert( $format, $expiry );
+       }
+
+       public function setBigSelects( $value = true ) {
+               // no-op
+       }
+
+       public function isReadOnly() {
+               return ( $this->getReadOnlyReason() !== false );
+       }
+
+       /**
+        * @return string|bool Reason this DB is read-only or false if it is not
+        */
+       protected function getReadOnlyReason() {
+               $reason = $this->getLBInfo( 'readOnlyReason' );
+
+               return is_string( $reason ) ? $reason : false;
+       }
+
+       public function setTableAliases( array $aliases ) {
+               $this->tableAliases = $aliases;
+       }
+
+       /**
+        * @return bool Whether a DB user is required to access the DB
+        * @since 1.28
+        */
+       protected function requiresDatabaseUser() {
+               return true;
+       }
+
+       /**
+        * @since 1.19
+        * @return string
+        */
+       public function __toString() {
+               return (string)$this->mConn;
+       }
+
+       /**
+        * Make sure that copies do not share the same client binding handle
+        * @throws DBConnectionError
+        */
+       public function __clone() {
+               $this->connLogger->warning(
+                       "Cloning " . get_class( $this ) . " is not recomended; forking connection:\n" .
+                       ( new RuntimeException() )->getTraceAsString()
+               );
+
+               if ( $this->isOpen() ) {
+                       // Open a new connection resource without messing with the old one
+                       $this->mOpened = false;
+                       $this->mConn = false;
+                       $this->mTrxEndCallbacks = []; // don't copy
+                       $this->handleSessionLoss(); // no trx or locks anymore
+                       $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+                       $this->lastPing = microtime( true );
+               }
+       }
+
+       /**
+        * Called by serialize. Throw an exception when DB connection is serialized.
+        * This causes problems on some database engines because the connection is
+        * not restored on unserialize.
+        */
+       public function __sleep() {
+               throw new RuntimeException( 'Database serialization may cause problems, since ' .
+                       'the connection is not restored on wakeup.' );
+       }
+
+       /**
+        * Run a few simple sanity checks and close dangling connections
+        */
+       public function __destruct() {
+               if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
+                       trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
+               }
+
+               $danglingWriters = $this->pendingWriteAndCallbackCallers();
+               if ( $danglingWriters ) {
+                       $fnames = implode( ', ', $danglingWriters );
+                       trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
+               }
+
+               if ( $this->mConn ) {
+                       // Avoid connection leaks for sanity
+                       $this->closeConnection();
+                       $this->mConn = false;
+                       $this->mOpened = false;
+               }
+       }
+}
+
+class_alias( 'Database', 'DatabaseBase' );
diff --git a/includes/libs/rdbms/database/DatabaseDomain.php b/includes/libs/rdbms/database/DatabaseDomain.php
new file mode 100644 (file)
index 0000000..a3ae6f1
--- /dev/null
@@ -0,0 +1,206 @@
+<?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 Database
+ */
+
+/**
+ * Class to handle database/prefix specification for IDatabase domains
+ */
+class DatabaseDomain {
+       /** @var string|null */
+       private $database;
+       /** @var string|null */
+       private $schema;
+       /** @var string */
+       private $prefix;
+
+       /** @var string Cache of convertToString() */
+       private $equivalentString;
+
+       /**
+        * @param string|null $database Database name
+        * @param string|null $schema Schema name
+        * @param string $prefix Table prefix
+        */
+       public function __construct( $database, $schema, $prefix ) {
+               if ( $database !== null && ( !is_string( $database ) || !strlen( $database ) ) ) {
+                       throw new InvalidArgumentException( "Database must be null or a non-empty string." );
+               }
+               $this->database = $database;
+               if ( $schema !== null && ( !is_string( $schema ) || !strlen( $schema ) ) ) {
+                       throw new InvalidArgumentException( "Schema must be null or a non-empty string." );
+               }
+               $this->schema = $schema;
+               if ( !is_string( $prefix ) ) {
+                       throw new InvalidArgumentException( "Prefix must be a string." );
+               }
+               $this->prefix = $prefix;
+       }
+
+       /**
+        * @param DatabaseDomain|string $domain Result of DatabaseDomain::toString()
+        * @return DatabaseDomain
+        */
+       public static function newFromId( $domain ) {
+               if ( $domain instanceof self ) {
+                       return $domain;
+               }
+
+               $parts = array_map( [ __CLASS__, 'decode' ], explode( '-', $domain ) );
+
+               $schema = null;
+               $prefix = '';
+
+               if ( count( $parts ) == 1 ) {
+                       $database = $parts[0];
+               } elseif ( count( $parts ) == 2 ) {
+                       list( $database, $prefix ) = $parts;
+               } elseif ( count( $parts ) == 3 ) {
+                       list( $database, $schema, $prefix ) = $parts;
+               } else {
+                       throw new InvalidArgumentException( "Domain has too few or too many parts." );
+               }
+
+               if ( $database === '' ) {
+                       $database = null;
+               }
+
+               return new self( $database, $schema, $prefix );
+       }
+
+       /**
+        * @return DatabaseDomain
+        */
+       public static function newUnspecified() {
+               return new self( null, null, '' );
+       }
+
+       /**
+        * @param DatabaseDomain|string $other
+        * @return bool
+        */
+       public function equals( $other ) {
+               if ( $other instanceof DatabaseDomain ) {
+                       return (
+                               $this->database === $other->database &&
+                               $this->schema === $other->schema &&
+                               $this->prefix === $other->prefix
+                       );
+               }
+
+               return ( $this->getId() === $other );
+       }
+
+       /**
+        * @return string|null Database name
+        */
+       public function getDatabase() {
+               return $this->database;
+       }
+
+       /**
+        * @return string|null Database schema
+        */
+       public function getSchema() {
+               return $this->schema;
+       }
+
+       /**
+        * @return string Table prefix
+        */
+       public function getTablePrefix() {
+               return $this->prefix;
+       }
+
+       /**
+        * @return string
+        */
+       public function getId() {
+               if ( $this->equivalentString === null ) {
+                       $this->equivalentString = $this->convertToString();
+               }
+
+               return $this->equivalentString;
+       }
+
+       /**
+        * @return string
+        */
+       private function convertToString() {
+               $parts = [ $this->database ];
+               if ( $this->schema !== null ) {
+                       $parts[] = $this->schema;
+               }
+               if ( $this->prefix != '' ) {
+                       $parts[] = $this->prefix;
+               }
+
+               return implode( '-', array_map( [ __CLASS__, 'encode' ], $parts ) );
+       }
+
+       private static function encode( $decoded ) {
+               $encoded = '';
+
+               $length = strlen( $decoded );
+               for ( $i = 0; $i < $length; ++$i ) {
+                       $char = $decoded[$i];
+                       if ( $char === '-' ) {
+                               $encoded .= '?h';
+                       } elseif ( $char === '?' ) {
+                               $encoded .= '??';
+                       } else {
+                               $encoded .= $char;
+                       }
+               }
+
+               return $encoded;
+       }
+
+       private static function decode( $encoded ) {
+               $decoded = '';
+
+               $length = strlen( $encoded );
+               for ( $i = 0; $i < $length; ++$i ) {
+                       $char = $encoded[$i];
+                       if ( $char === '?' ) {
+                               $nextChar = isset( $encoded[$i + 1] ) ? $encoded[$i + 1] : null;
+                               if ( $nextChar === 'h' ) {
+                                       $decoded .= '-';
+                                       ++$i;
+                               } elseif ( $nextChar === '?' ) {
+                                       $decoded .= '?';
+                                       ++$i;
+                               } else {
+                                       $decoded .= $char;
+                               }
+                       } else {
+                               $decoded .= $char;
+                       }
+               }
+
+               return $decoded;
+       }
+
+       /**
+        * @return string
+        */
+       function __toString() {
+               return $this->getId();
+       }
+}
diff --git a/includes/libs/rdbms/database/DatabaseMysql.php b/includes/libs/rdbms/database/DatabaseMysql.php
new file mode 100644 (file)
index 0000000..9ab7c64
--- /dev/null
@@ -0,0 +1,204 @@
+<?php
+/**
+ * This is the MySQL database abstraction layer.
+ *
+ * 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
+ */
+
+/**
+ * Database abstraction object for PHP extension mysql.
+ *
+ * @ingroup Database
+ * @see Database
+ */
+class DatabaseMysql extends DatabaseMysqlBase {
+       /**
+        * @param string $sql
+        * @return resource False on error
+        */
+       protected function doQuery( $sql ) {
+               $conn = $this->getBindingHandle();
+
+               if ( $this->bufferResults() ) {
+                       $ret = mysql_query( $sql, $conn );
+               } else {
+                       $ret = mysql_unbuffered_query( $sql, $conn );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * @param string $realServer
+        * @return bool|resource MySQL Database connection or false on failure to connect
+        * @throws DBConnectionError
+        */
+       protected function mysqlConnect( $realServer ) {
+               # Avoid a suppressed fatal error, which is very hard to track down
+               if ( !extension_loaded( 'mysql' ) ) {
+                       throw new DBConnectionError(
+                               $this,
+                               "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n"
+                       );
+               }
+
+               $connFlags = 0;
+               if ( $this->mFlags & self::DBO_SSL ) {
+                       $connFlags |= MYSQL_CLIENT_SSL;
+               }
+               if ( $this->mFlags & self::DBO_COMPRESS ) {
+                       $connFlags |= MYSQL_CLIENT_COMPRESS;
+               }
+
+               if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
+                       $numAttempts = 2;
+               } else {
+                       $numAttempts = 1;
+               }
+
+               $conn = false;
+
+               # The kernel's default SYN retransmission period is far too slow for us,
+               # so we use a short timeout plus a manual retry. Retrying means that a small
+               # but finite rate of SYN packet loss won't cause user-visible errors.
+               for ( $i = 0; $i < $numAttempts && !$conn; $i++ ) {
+                       if ( $i > 1 ) {
+                               usleep( 1000 );
+                       }
+                       if ( $this->mFlags & self::DBO_PERSISTENT ) {
+                               $conn = mysql_pconnect( $realServer, $this->mUser, $this->mPassword, $connFlags );
+                       } else {
+                               # Create a new connection...
+                               $conn = mysql_connect( $realServer, $this->mUser, $this->mPassword, true, $connFlags );
+                       }
+               }
+
+               return $conn;
+       }
+
+       /**
+        * @param string $charset
+        * @return bool
+        */
+       protected function mysqlSetCharset( $charset ) {
+               $conn = $this->getBindingHandle();
+
+               if ( function_exists( 'mysql_set_charset' ) ) {
+                       return mysql_set_charset( $charset, $conn );
+               } else {
+                       return $this->query( 'SET NAMES ' . $charset, __METHOD__ );
+               }
+       }
+
+       /**
+        * @return bool
+        */
+       protected function closeConnection() {
+               $conn = $this->getBindingHandle();
+
+               return mysql_close( $conn );
+       }
+
+       /**
+        * @return int
+        */
+       function insertId() {
+               $conn = $this->getBindingHandle();
+
+               return mysql_insert_id( $conn );
+       }
+
+       /**
+        * @return int
+        */
+       function lastErrno() {
+               if ( $this->mConn ) {
+                       return mysql_errno( $this->mConn );
+               } else {
+                       return mysql_errno();
+               }
+       }
+
+       /**
+        * @return int
+        */
+       function affectedRows() {
+               $conn = $this->getBindingHandle();
+
+               return mysql_affected_rows( $conn );
+       }
+
+       /**
+        * @param string $db
+        * @return bool
+        */
+       function selectDB( $db ) {
+               $conn = $this->getBindingHandle();
+
+               $this->mDBname = $db;
+
+               return mysql_select_db( $db, $conn );
+       }
+
+       protected function mysqlFreeResult( $res ) {
+               return mysql_free_result( $res );
+       }
+
+       protected function mysqlFetchObject( $res ) {
+               return mysql_fetch_object( $res );
+       }
+
+       protected function mysqlFetchArray( $res ) {
+               return mysql_fetch_array( $res );
+       }
+
+       protected function mysqlNumRows( $res ) {
+               return mysql_num_rows( $res );
+       }
+
+       protected function mysqlNumFields( $res ) {
+               return mysql_num_fields( $res );
+       }
+
+       protected function mysqlFetchField( $res, $n ) {
+               return mysql_fetch_field( $res, $n );
+       }
+
+       protected function mysqlFieldName( $res, $n ) {
+               return mysql_field_name( $res, $n );
+       }
+
+       protected function mysqlFieldType( $res, $n ) {
+               return mysql_field_type( $res, $n );
+       }
+
+       protected function mysqlDataSeek( $res, $row ) {
+               return mysql_data_seek( $res, $row );
+       }
+
+       protected function mysqlError( $conn = null ) {
+               return ( $conn !== null ) ? mysql_error( $conn ) : mysql_error(); // avoid warning
+       }
+
+       protected function mysqlRealEscapeString( $s ) {
+               $conn = $this->getBindingHandle();
+
+               return mysql_real_escape_string( $s, $conn );
+       }
+}
diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php
new file mode 100644 (file)
index 0000000..f504ec4
--- /dev/null
@@ -0,0 +1,1336 @@
+<?php
+/**
+ * This is the MySQL database abstraction layer.
+ *
+ * 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
+ */
+
+/**
+ * Database abstraction object for MySQL.
+ * Defines methods independent on used MySQL extension.
+ *
+ * @ingroup Database
+ * @since 1.22
+ * @see Database
+ */
+abstract class DatabaseMysqlBase extends Database {
+       /** @var MysqlMasterPos */
+       protected $lastKnownReplicaPos;
+       /** @var string Method to detect replica DB lag */
+       protected $lagDetectionMethod;
+       /** @var array Method to detect replica DB lag */
+       protected $lagDetectionOptions = [];
+       /** @var bool bool Whether to use GTID methods */
+       protected $useGTIDs = false;
+       /** @var string|null */
+       protected $sslKeyPath;
+       /** @var string|null */
+       protected $sslCertPath;
+       /** @var string|null */
+       protected $sslCAPath;
+       /** @var string[]|null */
+       protected $sslCiphers;
+       /** @var string sql_mode value to send on connection */
+       protected $sqlMode;
+       /** @var bool Use experimental UTF-8 transmission encoding */
+       protected $utf8Mode;
+
+       /** @var string|null */
+       private $serverVersion = null;
+
+       /**
+        * Additional $params include:
+        *   - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
+        *       pt-heartbeat assumes the table is at heartbeat.heartbeat
+        *       and uses UTC timestamps in the heartbeat.ts column.
+        *       (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html)
+        *   - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map to change
+        *       the default behavior. Normally, the heartbeat row with the server
+        *       ID of this server's master will be used. Set the "conds" field to
+        *       override the query conditions, e.g. ['shard' => 's1'].
+        *   - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible.
+        *   - sslKeyPath : path to key file [default: null]
+        *   - sslCertPath : path to certificate file [default: null]
+        *   - sslCAPath : parth to certificate authority PEM files [default: null]
+        *   - sslCiphers : array list of allowable ciphers [default: null]
+        * @param array $params
+        */
+       function __construct( array $params ) {
+               parent::__construct( $params );
+
+               $this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
+                       ? $params['lagDetectionMethod']
+                       : 'Seconds_Behind_Master';
+               $this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
+                       ? $params['lagDetectionOptions']
+                       : [];
+               $this->useGTIDs = !empty( $params['useGTIDs' ] );
+               foreach ( [ 'KeyPath', 'CertPath', 'CAPath', 'Ciphers' ] as $name ) {
+                       $var = "ssl{$name}";
+                       if ( isset( $params[$var] ) ) {
+                               $this->$var = $params[$var];
+                       }
+               }
+               $this->sqlMode = isset( $params['sqlMode'] ) ? $params['sqlMode'] : '';
+               $this->utf8Mode = !empty( $params['utf8Mode'] );
+       }
+
+       /**
+        * @return string
+        */
+       function getType() {
+               return 'mysql';
+       }
+
+       /**
+        * @param string $server
+        * @param string $user
+        * @param string $password
+        * @param string $dbName
+        * @throws Exception|DBConnectionError
+        * @return bool
+        */
+       function open( $server, $user, $password, $dbName ) {
+               # Close/unset connection handle
+               $this->close();
+
+               $this->mServer = $server;
+               $this->mUser = $user;
+               $this->mPassword = $password;
+               $this->mDBname = $dbName;
+
+               $this->installErrorHandler();
+               try {
+                       $this->mConn = $this->mysqlConnect( $this->mServer );
+               } catch ( Exception $ex ) {
+                       $this->restoreErrorHandler();
+                       throw $ex;
+               }
+               $error = $this->restoreErrorHandler();
+
+               # Always log connection errors
+               if ( !$this->mConn ) {
+                       if ( !$error ) {
+                               $error = $this->lastError();
+                       }
+                       $this->connLogger->error(
+                               "Error connecting to {db_server}: {error}",
+                               $this->getLogContext( [
+                                       'method' => __METHOD__,
+                                       'error' => $error,
+                               ] )
+                       );
+                       $this->connLogger->debug( "DB connection error\n" .
+                               "Server: $server, User: $user, Password: " .
+                               substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
+
+                       $this->reportConnectionError( $error );
+               }
+
+               if ( $dbName != '' ) {
+                       MediaWiki\suppressWarnings();
+                       $success = $this->selectDB( $dbName );
+                       MediaWiki\restoreWarnings();
+                       if ( !$success ) {
+                               $this->queryLogger->error(
+                                       "Error selecting database {db_name} on server {db_server}",
+                                       $this->getLogContext( [
+                                               'method' => __METHOD__,
+                                       ] )
+                               );
+                               $this->queryLogger->debug(
+                                       "Error selecting database $dbName on server {$this->mServer}" );
+
+                               $this->reportConnectionError( "Error selecting database $dbName" );
+                       }
+               }
+
+               // Tell the server what we're communicating with
+               if ( !$this->connectInitCharset() ) {
+                       $this->reportConnectionError( "Error setting character set" );
+               }
+
+               // Abstract over any insane MySQL defaults
+               $set = [ 'group_concat_max_len = 262144' ];
+               // Set SQL mode, default is turning them all off, can be overridden or skipped with null
+               if ( is_string( $this->sqlMode ) ) {
+                       $set[] = 'sql_mode = ' . $this->addQuotes( $this->sqlMode );
+               }
+               // Set any custom settings defined by site config
+               // (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html)
+               foreach ( $this->mSessionVars as $var => $val ) {
+                       // Escape strings but not numbers to avoid MySQL complaining
+                       if ( !is_int( $val ) && !is_float( $val ) ) {
+                               $val = $this->addQuotes( $val );
+                       }
+                       $set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val;
+               }
+
+               if ( $set ) {
+                       // Use doQuery() to avoid opening implicit transactions (DBO_TRX)
+                       $success = $this->doQuery( 'SET ' . implode( ', ', $set ) );
+                       if ( !$success ) {
+                               $this->queryLogger->error(
+                                       'Error setting MySQL variables on server {db_server} (check $wgSQLMode)',
+                                       $this->getLogContext( [
+                                               'method' => __METHOD__,
+                                       ] )
+                               );
+                               $this->reportConnectionError(
+                                       'Error setting MySQL variables on server {db_server} (check $wgSQLMode)' );
+                       }
+               }
+
+               $this->mOpened = true;
+
+               return true;
+       }
+
+       /**
+        * Set the character set information right after connection
+        * @return bool
+        */
+       protected function connectInitCharset() {
+               if ( $this->utf8Mode ) {
+                       // Tell the server we're communicating with it in UTF-8.
+                       // This may engage various charset conversions.
+                       return $this->mysqlSetCharset( 'utf8' );
+               } else {
+                       return $this->mysqlSetCharset( 'binary' );
+               }
+       }
+
+       /**
+        * Open a connection to a MySQL server
+        *
+        * @param string $realServer
+        * @return mixed Raw connection
+        * @throws DBConnectionError
+        */
+       abstract protected function mysqlConnect( $realServer );
+
+       /**
+        * Set the character set of the MySQL link
+        *
+        * @param string $charset
+        * @return bool
+        */
+       abstract protected function mysqlSetCharset( $charset );
+
+       /**
+        * @param ResultWrapper|resource $res
+        * @throws DBUnexpectedError
+        */
+       function freeResult( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               MediaWiki\suppressWarnings();
+               $ok = $this->mysqlFreeResult( $res );
+               MediaWiki\restoreWarnings();
+               if ( !$ok ) {
+                       throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
+               }
+       }
+
+       /**
+        * Free result memory
+        *
+        * @param resource $res Raw result
+        * @return bool
+        */
+       abstract protected function mysqlFreeResult( $res );
+
+       /**
+        * @param ResultWrapper|resource $res
+        * @return stdClass|bool
+        * @throws DBUnexpectedError
+        */
+       function fetchObject( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               MediaWiki\suppressWarnings();
+               $row = $this->mysqlFetchObject( $res );
+               MediaWiki\restoreWarnings();
+
+               $errno = $this->lastErrno();
+               // Unfortunately, mysql_fetch_object does not reset the last errno.
+               // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
+               // these are the only errors mysql_fetch_object can cause.
+               // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+               if ( $errno == 2000 || $errno == 2013 ) {
+                       throw new DBUnexpectedError(
+                               $this,
+                               'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() )
+                       );
+               }
+
+               return $row;
+       }
+
+       /**
+        * Fetch a result row as an object
+        *
+        * @param resource $res Raw result
+        * @return stdClass
+        */
+       abstract protected function mysqlFetchObject( $res );
+
+       /**
+        * @param ResultWrapper|resource $res
+        * @return array|bool
+        * @throws DBUnexpectedError
+        */
+       function fetchRow( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               MediaWiki\suppressWarnings();
+               $row = $this->mysqlFetchArray( $res );
+               MediaWiki\restoreWarnings();
+
+               $errno = $this->lastErrno();
+               // Unfortunately, mysql_fetch_array does not reset the last errno.
+               // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
+               // these are the only errors mysql_fetch_array can cause.
+               // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+               if ( $errno == 2000 || $errno == 2013 ) {
+                       throw new DBUnexpectedError(
+                               $this,
+                               'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() )
+                       );
+               }
+
+               return $row;
+       }
+
+       /**
+        * Fetch a result row as an associative and numeric array
+        *
+        * @param resource $res Raw result
+        * @return array
+        */
+       abstract protected function mysqlFetchArray( $res );
+
+       /**
+        * @throws DBUnexpectedError
+        * @param ResultWrapper|resource $res
+        * @return int
+        */
+       function numRows( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               MediaWiki\suppressWarnings();
+               $n = $this->mysqlNumRows( $res );
+               MediaWiki\restoreWarnings();
+
+               // Unfortunately, mysql_num_rows does not reset the last errno.
+               // We are not checking for any errors here, since
+               // these are no errors mysql_num_rows can cause.
+               // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+               // See https://phabricator.wikimedia.org/T44430
+               return $n;
+       }
+
+       /**
+        * Get number of rows in result
+        *
+        * @param resource $res Raw result
+        * @return int
+        */
+       abstract protected function mysqlNumRows( $res );
+
+       /**
+        * @param ResultWrapper|resource $res
+        * @return int
+        */
+       function numFields( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+
+               return $this->mysqlNumFields( $res );
+       }
+
+       /**
+        * Get number of fields in result
+        *
+        * @param resource $res Raw result
+        * @return int
+        */
+       abstract protected function mysqlNumFields( $res );
+
+       /**
+        * @param ResultWrapper|resource $res
+        * @param int $n
+        * @return string
+        */
+       function fieldName( $res, $n ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+
+               return $this->mysqlFieldName( $res, $n );
+       }
+
+       /**
+        * Get the name of the specified field in a result
+        *
+        * @param ResultWrapper|resource $res
+        * @param int $n
+        * @return string
+        */
+       abstract protected function mysqlFieldName( $res, $n );
+
+       /**
+        * mysql_field_type() wrapper
+        * @param ResultWrapper|resource $res
+        * @param int $n
+        * @return string
+        */
+       public function fieldType( $res, $n ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+
+               return $this->mysqlFieldType( $res, $n );
+       }
+
+       /**
+        * Get the type of the specified field in a result
+        *
+        * @param ResultWrapper|resource $res
+        * @param int $n
+        * @return string
+        */
+       abstract protected function mysqlFieldType( $res, $n );
+
+       /**
+        * @param ResultWrapper|resource $res
+        * @param int $row
+        * @return bool
+        */
+       function dataSeek( $res, $row ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+
+               return $this->mysqlDataSeek( $res, $row );
+       }
+
+       /**
+        * Move internal result pointer
+        *
+        * @param ResultWrapper|resource $res
+        * @param int $row
+        * @return bool
+        */
+       abstract protected function mysqlDataSeek( $res, $row );
+
+       /**
+        * @return string
+        */
+       function lastError() {
+               if ( $this->mConn ) {
+                       # Even if it's non-zero, it can still be invalid
+                       MediaWiki\suppressWarnings();
+                       $error = $this->mysqlError( $this->mConn );
+                       if ( !$error ) {
+                               $error = $this->mysqlError();
+                       }
+                       MediaWiki\restoreWarnings();
+               } else {
+                       $error = $this->mysqlError();
+               }
+               if ( $error ) {
+                       $error .= ' (' . $this->mServer . ')';
+               }
+
+               return $error;
+       }
+
+       /**
+        * Returns the text of the error message from previous MySQL operation
+        *
+        * @param resource $conn Raw connection
+        * @return string
+        */
+       abstract protected function mysqlError( $conn = null );
+
+       /**
+        * @param string $table
+        * @param array $uniqueIndexes
+        * @param array $rows
+        * @param string $fname
+        * @return ResultWrapper
+        */
+       function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
+               return $this->nativeReplace( $table, $rows, $fname );
+       }
+
+       /**
+        * Estimate rows in dataset
+        * Returns estimated count, based on EXPLAIN output
+        * Takes same arguments as Database::select()
+        *
+        * @param string|array $table
+        * @param string|array $vars
+        * @param string|array $conds
+        * @param string $fname
+        * @param string|array $options
+        * @return bool|int
+        */
+       public function estimateRowCount( $table, $vars = '*', $conds = '',
+               $fname = __METHOD__, $options = []
+       ) {
+               $options['EXPLAIN'] = true;
+               $res = $this->select( $table, $vars, $conds, $fname, $options );
+               if ( $res === false ) {
+                       return false;
+               }
+               if ( !$this->numRows( $res ) ) {
+                       return 0;
+               }
+
+               $rows = 1;
+               foreach ( $res as $plan ) {
+                       $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
+               }
+
+               return (int)$rows;
+       }
+
+       function tableExists( $table, $fname = __METHOD__ ) {
+               $table = $this->tableName( $table, 'raw' );
+               if ( isset( $this->mSessionTempTables[$table] ) ) {
+                       return true; // already known to exist and won't show in SHOW TABLES anyway
+               }
+
+               $encLike = $this->buildLike( $table );
+
+               return $this->query( "SHOW TABLES $encLike", $fname )->numRows() > 0;
+       }
+
+       /**
+        * @param string $table
+        * @param string $field
+        * @return bool|MySQLField
+        */
+       function fieldInfo( $table, $field ) {
+               $table = $this->tableName( $table );
+               $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
+               if ( !$res ) {
+                       return false;
+               }
+               $n = $this->mysqlNumFields( $res->result );
+               for ( $i = 0; $i < $n; $i++ ) {
+                       $meta = $this->mysqlFetchField( $res->result, $i );
+                       if ( $field == $meta->name ) {
+                               return new MySQLField( $meta );
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Get column information from a result
+        *
+        * @param resource $res Raw result
+        * @param int $n
+        * @return stdClass
+        */
+       abstract protected function mysqlFetchField( $res, $n );
+
+       /**
+        * Get information about an index into an object
+        * Returns false if the index does not exist
+        *
+        * @param string $table
+        * @param string $index
+        * @param string $fname
+        * @return bool|array|null False or null on failure
+        */
+       function indexInfo( $table, $index, $fname = __METHOD__ ) {
+               # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
+               # SHOW INDEX should work for 3.x and up:
+               # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
+               $table = $this->tableName( $table );
+               $index = $this->indexName( $index );
+
+               $sql = 'SHOW INDEX FROM ' . $table;
+               $res = $this->query( $sql, $fname );
+
+               if ( !$res ) {
+                       return null;
+               }
+
+               $result = [];
+
+               foreach ( $res as $row ) {
+                       if ( $row->Key_name == $index ) {
+                               $result[] = $row;
+                       }
+               }
+
+               return empty( $result ) ? false : $result;
+       }
+
+       /**
+        * @param string $s
+        * @return string
+        */
+       function strencode( $s ) {
+               return $this->mysqlRealEscapeString( $s );
+       }
+
+       /**
+        * @param string $s
+        * @return mixed
+        */
+       abstract protected function mysqlRealEscapeString( $s );
+
+       /**
+        * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
+        *
+        * @param string $s
+        * @return string
+        */
+       public function addIdentifierQuotes( $s ) {
+               // Characters in the range \u0001-\uFFFF are valid in a quoted identifier
+               // Remove NUL bytes and escape backticks by doubling
+               return '`' . str_replace( [ "\0", '`' ], [ '', '``' ], $s ) . '`';
+       }
+
+       /**
+        * @param string $name
+        * @return bool
+        */
+       public function isQuotedIdentifier( $name ) {
+               return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
+       }
+
+       function getLag() {
+               if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
+                       return $this->getLagFromPtHeartbeat();
+               } else {
+                       return $this->getLagFromSlaveStatus();
+               }
+       }
+
+       /**
+        * @return string
+        */
+       protected function getLagDetectionMethod() {
+               return $this->lagDetectionMethod;
+       }
+
+       /**
+        * @return bool|int
+        */
+       protected function getLagFromSlaveStatus() {
+               $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
+               $row = $res ? $res->fetchObject() : false;
+               if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
+                       return intval( $row->Seconds_Behind_Master );
+               }
+
+               return false;
+       }
+
+       /**
+        * @return bool|float
+        */
+       protected function getLagFromPtHeartbeat() {
+               $options = $this->lagDetectionOptions;
+
+               if ( isset( $options['conds'] ) ) {
+                       // Best method for multi-DC setups: use logical channel names
+                       $data = $this->getHeartbeatData( $options['conds'] );
+               } else {
+                       // Standard method: use master server ID (works with stock pt-heartbeat)
+                       $masterInfo = $this->getMasterServerInfo();
+                       if ( !$masterInfo ) {
+                               $this->queryLogger->error(
+                                       "Unable to query master of {db_server} for server ID",
+                                       $this->getLogContext( [
+                                               'method' => __METHOD__
+                                       ] )
+                               );
+
+                               return false; // could not get master server ID
+                       }
+
+                       $conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
+                       $data = $this->getHeartbeatData( $conds );
+               }
+
+               list( $time, $nowUnix ) = $data;
+               if ( $time !== null ) {
+                       // @time is in ISO format like "2015-09-25T16:48:10.000510"
+                       $dateTime = new DateTime( $time, new DateTimeZone( 'UTC' ) );
+                       $timeUnix = (int)$dateTime->format( 'U' ) + $dateTime->format( 'u' ) / 1e6;
+
+                       return max( $nowUnix - $timeUnix, 0.0 );
+               }
+
+               $this->queryLogger->error(
+                       "Unable to find pt-heartbeat row for {db_server}",
+                       $this->getLogContext( [
+                               'method' => __METHOD__
+                       ] )
+               );
+
+               return false;
+       }
+
+       protected function getMasterServerInfo() {
+               $cache = $this->srvCache;
+               $key = $cache->makeGlobalKey(
+                       'mysql',
+                       'master-info',
+                       // Using one key for all cluster replica DBs is preferable
+                       $this->getLBInfo( 'clusterMasterHost' ) ?: $this->getServer()
+               );
+
+               return $cache->getWithSetCallback(
+                       $key,
+                       $cache::TTL_INDEFINITE,
+                       function () use ( $cache, $key ) {
+                               // Get and leave a lock key in place for a short period
+                               if ( !$cache->lock( $key, 0, 10 ) ) {
+                                       return false; // avoid master connection spike slams
+                               }
+
+                               $conn = $this->getLazyMasterHandle();
+                               if ( !$conn ) {
+                                       return false; // something is misconfigured
+                               }
+
+                               // Connect to and query the master; catch errors to avoid outages
+                               try {
+                                       $res = $conn->query( 'SELECT @@server_id AS id', __METHOD__ );
+                                       $row = $res ? $res->fetchObject() : false;
+                                       $id = $row ? (int)$row->id : 0;
+                               } catch ( DBError $e ) {
+                                       $id = 0;
+                               }
+
+                               // Cache the ID if it was retrieved
+                               return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false;
+                       }
+               );
+       }
+
+       /**
+        * @param array $conds WHERE clause conditions to find a row
+        * @return array (heartbeat `ts` column value or null, UNIX timestamp) for the newest beat
+        * @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html
+        */
+       protected function getHeartbeatData( array $conds ) {
+               $whereSQL = $this->makeList( $conds, self::LIST_AND );
+               // Use ORDER BY for channel based queries since that field might not be UNIQUE.
+               // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
+               // percision field is not supported in MySQL <= 5.5.
+               $res = $this->query(
+                       "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1"
+               );
+               $row = $res ? $res->fetchObject() : false;
+
+               return [ $row ? $row->ts : null, microtime( true ) ];
+       }
+
+       public function getApproximateLagStatus() {
+               if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
+                       // Disable caching since this is fast enough and we don't wan't
+                       // to be *too* pessimistic by having both the cache TTL and the
+                       // pt-heartbeat interval count as lag in getSessionLagStatus()
+                       return parent::getApproximateLagStatus();
+               }
+
+               $key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServer() );
+               $approxLag = $this->srvCache->get( $key );
+               if ( !$approxLag ) {
+                       $approxLag = parent::getApproximateLagStatus();
+                       $this->srvCache->set( $key, $approxLag, 1 );
+               }
+
+               return $approxLag;
+       }
+
+       function masterPosWait( DBMasterPos $pos, $timeout ) {
+               if ( !( $pos instanceof MySQLMasterPos ) ) {
+                       throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" );
+               }
+
+               if ( $this->getLBInfo( 'is static' ) === true ) {
+                       return 0; // this is a copy of a read-only dataset with no master DB
+               } elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) {
+                       return 0; // already reached this point for sure
+               }
+
+               // Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
+               if ( $this->useGTIDs && $pos->gtids ) {
+                       // Wait on the GTID set (MariaDB only)
+                       $gtidArg = $this->addQuotes( implode( ',', $pos->gtids ) );
+                       $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+               } else {
+                       // Wait on the binlog coordinates
+                       $encFile = $this->addQuotes( $pos->file );
+                       $encPos = intval( $pos->pos );
+                       $res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
+               }
+
+               $row = $res ? $this->fetchRow( $res ) : false;
+               if ( !$row ) {
+                       throw new DBExpectedError( $this, "Failed to query MASTER_POS_WAIT()" );
+               }
+
+               // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
+               $status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
+               if ( $status === null ) {
+                       // T126436: jobs programmed to wait on master positions might be referencing binlogs
+                       // 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->getReplicaPos();
+                       if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) {
+                               $this->lastKnownReplicaPos = $replicationPos;
+                               $status = 0;
+                       }
+               } elseif ( $status >= 0 ) {
+                       // Remember that this position was reached to save queries next time
+                       $this->lastKnownReplicaPos = $pos;
+               }
+
+               return $status;
+       }
+
+       /**
+        * Get the position of the master from SHOW SLAVE STATUS
+        *
+        * @return MySQLMasterPos|bool
+        */
+       function getReplicaPos() {
+               $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
+               $row = $this->fetchObject( $res );
+
+               if ( $row ) {
+                       $pos = isset( $row->Exec_master_log_pos )
+                               ? $row->Exec_master_log_pos
+                               : $row->Exec_Master_Log_Pos;
+                       // Also fetch the last-applied GTID set (MariaDB)
+                       if ( $this->useGTIDs ) {
+                               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_slave_pos'", __METHOD__ );
+                               $gtidRow = $this->fetchObject( $res );
+                               $gtidSet = $gtidRow ? $gtidRow->Value : '';
+                       } else {
+                               $gtidSet = '';
+                       }
+
+                       return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos, $gtidSet );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Get the position of the master from SHOW MASTER STATUS
+        *
+        * @return MySQLMasterPos|bool
+        */
+       function getMasterPos() {
+               $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
+               $row = $this->fetchObject( $res );
+
+               if ( $row ) {
+                       // Also fetch the last-written GTID set (MariaDB)
+                       if ( $this->useGTIDs ) {
+                               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_binlog_pos'", __METHOD__ );
+                               $gtidRow = $this->fetchObject( $res );
+                               $gtidSet = $gtidRow ? $gtidRow->Value : '';
+                       } else {
+                               $gtidSet = '';
+                       }
+
+                       return new MySQLMasterPos( $row->File, $row->Position, $gtidSet );
+               } else {
+                       return false;
+               }
+       }
+
+       public function serverIsReadOnly() {
+               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__ );
+               $row = $this->fetchObject( $res );
+
+               return $row ? ( strtolower( $row->Value ) === 'on' ) : false;
+       }
+
+       /**
+        * @param string $index
+        * @return string
+        */
+       function useIndexClause( $index ) {
+               return "FORCE INDEX (" . $this->indexName( $index ) . ")";
+       }
+
+       /**
+        * @param string $index
+        * @return string
+        */
+       function ignoreIndexClause( $index ) {
+               return "IGNORE INDEX (" . $this->indexName( $index ) . ")";
+       }
+
+       /**
+        * @return string
+        */
+       function lowPriorityOption() {
+               return 'LOW_PRIORITY';
+       }
+
+       /**
+        * @return string
+        */
+       public function getSoftwareLink() {
+               // MariaDB includes its name in its version string; this is how MariaDB's version of
+               // the mysql command-line client identifies MariaDB servers (see mariadb_connection()
+               // in libmysql/libmysql.c).
+               $version = $this->getServerVersion();
+               if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) {
+                       return '[{{int:version-db-mariadb-url}} MariaDB]';
+               }
+
+               // Percona Server's version suffix is not very distinctive, and @@version_comment
+               // doesn't give the necessary info for source builds, so assume the server is MySQL.
+               // (Even Percona's version of mysql doesn't try to make the distinction.)
+               return '[{{int:version-db-mysql-url}} MySQL]';
+       }
+
+       /**
+        * @return string
+        */
+       public function getServerVersion() {
+               // Not using mysql_get_server_info() or similar for consistency: in the handshake,
+               // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
+               // it off (see RPL_VERSION_HACK in include/mysql_com.h).
+               if ( $this->serverVersion === null ) {
+                       $this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ );
+               }
+               return $this->serverVersion;
+       }
+
+       /**
+        * @param array $options
+        */
+       public function setSessionOptions( array $options ) {
+               if ( isset( $options['connTimeout'] ) ) {
+                       $timeout = (int)$options['connTimeout'];
+                       $this->query( "SET net_read_timeout=$timeout" );
+                       $this->query( "SET net_write_timeout=$timeout" );
+               }
+       }
+
+       /**
+        * @param string $sql
+        * @param string $newLine
+        * @return bool
+        */
+       public function streamStatementEnd( &$sql, &$newLine ) {
+               if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
+                       preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
+                       $this->delimiter = $m[1];
+                       $newLine = '';
+               }
+
+               return parent::streamStatementEnd( $sql, $newLine );
+       }
+
+       /**
+        * Check to see if a named lock is available. This is non-blocking.
+        *
+        * @param string $lockName Name of lock to poll
+        * @param string $method Name of method calling us
+        * @return bool
+        * @since 1.20
+        */
+       public function lockIsFree( $lockName, $method ) {
+               $encName = $this->addQuotes( $this->makeLockName( $lockName ) );
+               $result = $this->query( "SELECT IS_FREE_LOCK($encName) AS lockstatus", $method );
+               $row = $this->fetchObject( $result );
+
+               return ( $row->lockstatus == 1 );
+       }
+
+       /**
+        * @param string $lockName
+        * @param string $method
+        * @param int $timeout
+        * @return bool
+        */
+       public function lock( $lockName, $method, $timeout = 5 ) {
+               $encName = $this->addQuotes( $this->makeLockName( $lockName ) );
+               $result = $this->query( "SELECT GET_LOCK($encName, $timeout) AS lockstatus", $method );
+               $row = $this->fetchObject( $result );
+
+               if ( $row->lockstatus == 1 ) {
+                       parent::lock( $lockName, $method, $timeout ); // record
+                       return true;
+               }
+
+               $this->queryLogger->warning( __METHOD__ . " failed to acquire lock '$lockName'\n" );
+
+               return false;
+       }
+
+       /**
+        * FROM MYSQL DOCS:
+        * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
+        * @param string $lockName
+        * @param string $method
+        * @return bool
+        */
+       public function unlock( $lockName, $method ) {
+               $encName = $this->addQuotes( $this->makeLockName( $lockName ) );
+               $result = $this->query( "SELECT RELEASE_LOCK($encName) as lockstatus", $method );
+               $row = $this->fetchObject( $result );
+
+               if ( $row->lockstatus == 1 ) {
+                       parent::unlock( $lockName, $method ); // record
+                       return true;
+               }
+
+               $this->queryLogger->warning( __METHOD__ . " failed to release lock '$lockName'\n" );
+
+               return false;
+       }
+
+       private function makeLockName( $lockName ) {
+               // http://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
+               // Newer version enforce a 64 char length limit.
+               return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
+       }
+
+       public function namedLocksEnqueue() {
+               return true;
+       }
+
+       /**
+        * @param array $read
+        * @param array $write
+        * @param string $method
+        * @param bool $lowPriority
+        * @return bool
+        */
+       public function lockTables( $read, $write, $method, $lowPriority = true ) {
+               $items = [];
+
+               foreach ( $write as $table ) {
+                       $tbl = $this->tableName( $table ) .
+                               ( $lowPriority ? ' LOW_PRIORITY' : '' ) .
+                               ' WRITE';
+                       $items[] = $tbl;
+               }
+               foreach ( $read as $table ) {
+                       $items[] = $this->tableName( $table ) . ' READ';
+               }
+               $sql = "LOCK TABLES " . implode( ',', $items );
+               $this->query( $sql, $method );
+
+               return true;
+       }
+
+       /**
+        * @param string $method
+        * @return bool
+        */
+       public function unlockTables( $method ) {
+               $this->query( "UNLOCK TABLES", $method );
+
+               return true;
+       }
+
+       /**
+        * @param bool $value
+        */
+       public function setBigSelects( $value = true ) {
+               if ( $value === 'default' ) {
+                       if ( $this->mDefaultBigSelects === null ) {
+                               # Function hasn't been called before so it must already be set to the default
+                               return;
+                       } else {
+                               $value = $this->mDefaultBigSelects;
+                       }
+               } elseif ( $this->mDefaultBigSelects === null ) {
+                       $this->mDefaultBigSelects =
+                               (bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
+               }
+               $encValue = $value ? '1' : '0';
+               $this->query( "SET sql_big_selects=$encValue", __METHOD__ );
+       }
+
+       /**
+        * DELETE where the condition is a join. MySql uses multi-table deletes.
+        * @param string $delTable
+        * @param string $joinTable
+        * @param string $delVar
+        * @param string $joinVar
+        * @param array|string $conds
+        * @param bool|string $fname
+        * @throws DBUnexpectedError
+        * @return bool|ResultWrapper
+        */
+       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ ) {
+               if ( !$conds ) {
+                       throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
+               }
+
+               $delTable = $this->tableName( $delTable );
+               $joinTable = $this->tableName( $joinTable );
+               $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
+
+               if ( $conds != '*' ) {
+                       $sql .= ' AND ' . $this->makeList( $conds, self::LIST_AND );
+               }
+
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * @param string $table
+        * @param array $rows
+        * @param array $uniqueIndexes
+        * @param array $set
+        * @param string $fname
+        * @return bool
+        */
+       public function upsert( $table, array $rows, array $uniqueIndexes,
+               array $set, $fname = __METHOD__
+       ) {
+               if ( !count( $rows ) ) {
+                       return true; // nothing to do
+               }
+
+               if ( !is_array( reset( $rows ) ) ) {
+                       $rows = [ $rows ];
+               }
+
+               $table = $this->tableName( $table );
+               $columns = array_keys( $rows[0] );
+
+               $sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES ';
+               $rowTuples = [];
+               foreach ( $rows as $row ) {
+                       $rowTuples[] = '(' . $this->makeList( $row ) . ')';
+               }
+               $sql .= implode( ',', $rowTuples );
+               $sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, self::LIST_SET );
+
+               return (bool)$this->query( $sql, $fname );
+       }
+
+       /**
+        * Determines how long the server has been up
+        *
+        * @return int
+        */
+       function getServerUptime() {
+               $vars = $this->getMysqlStatus( 'Uptime' );
+
+               return (int)$vars['Uptime'];
+       }
+
+       /**
+        * Determines if the last failure was due to a deadlock
+        *
+        * @return bool
+        */
+       function wasDeadlock() {
+               return $this->lastErrno() == 1213;
+       }
+
+       /**
+        * Determines if the last failure was due to a lock timeout
+        *
+        * @return bool
+        */
+       function wasLockTimeout() {
+               return $this->lastErrno() == 1205;
+       }
+
+       function wasErrorReissuable() {
+               return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
+       }
+
+       /**
+        * Determines if the last failure was due to the database being read-only.
+        *
+        * @return bool
+        */
+       function wasReadOnlyError() {
+               return $this->lastErrno() == 1223 ||
+                       ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
+       }
+
+       function wasConnectionError( $errno ) {
+               return $errno == 2013 || $errno == 2006;
+       }
+
+       /**
+        * Get the underlying binding handle, mConn
+        *
+        * Makes sure that mConn is set (disconnects and ping() failure can unset it).
+        * This catches broken callers than catch and ignore disconnection exceptions.
+        * Unlike checking isOpen(), this is safe to call inside of open().
+        *
+        * @return resource|object
+        * @throws DBUnexpectedError
+        * @since 1.26
+        */
+       protected function getBindingHandle() {
+               if ( !$this->mConn ) {
+                       throw new DBUnexpectedError(
+                               $this,
+                               'DB connection was already closed or the connection dropped.'
+                       );
+               }
+
+               return $this->mConn;
+       }
+
+       /**
+        * @param string $oldName
+        * @param string $newName
+        * @param bool $temporary
+        * @param string $fname
+        * @return bool
+        */
+       function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
+               $tmp = $temporary ? 'TEMPORARY ' : '';
+               $newName = $this->addIdentifierQuotes( $newName );
+               $oldName = $this->addIdentifierQuotes( $oldName );
+               $query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
+
+               return $this->query( $query, $fname );
+       }
+
+       /**
+        * List all tables on the database
+        *
+        * @param string $prefix Only show tables with this prefix, e.g. mw_
+        * @param string $fname Calling function name
+        * @return array
+        */
+       function listTables( $prefix = null, $fname = __METHOD__ ) {
+               $result = $this->query( "SHOW TABLES", $fname );
+
+               $endArray = [];
+
+               foreach ( $result as $table ) {
+                       $vars = get_object_vars( $table );
+                       $table = array_pop( $vars );
+
+                       if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
+                               $endArray[] = $table;
+                       }
+               }
+
+               return $endArray;
+       }
+
+       /**
+        * @param string $tableName
+        * @param string $fName
+        * @return bool|ResultWrapper
+        */
+       public function dropTable( $tableName, $fName = __METHOD__ ) {
+               if ( !$this->tableExists( $tableName, $fName ) ) {
+                       return false;
+               }
+
+               return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
+       }
+
+       /**
+        * Get status information from SHOW STATUS in an associative array
+        *
+        * @param string $which
+        * @return array
+        */
+       function getMysqlStatus( $which = "%" ) {
+               $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
+               $status = [];
+
+               foreach ( $res as $row ) {
+                       $status[$row->Variable_name] = $row->Value;
+               }
+
+               return $status;
+       }
+
+       /**
+        * Lists VIEWs in the database
+        *
+        * @param string $prefix Only show VIEWs with this prefix, eg.
+        * unit_test_, or $wgDBprefix. Default: null, would return all views.
+        * @param string $fname Name of calling function
+        * @return array
+        * @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;
+
+               // 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 $allViews;
+               }
+
+               $filteredViews = [];
+               foreach ( $allViews as $viewName ) {
+                       // Does the name of this VIEW start with the table-prefix?
+                       if ( strpos( $viewName, $prefix ) === 0 ) {
+                               array_push( $filteredViews, $viewName );
+                       }
+               }
+
+               return $filteredViews;
+       }
+
+       /**
+        * Differentiates between a TABLE and a VIEW.
+        *
+        * @param string $name Name of the TABLE/VIEW to test
+        * @param string $prefix
+        * @return bool
+        * @since 1.22
+        */
+       public function isView( $name, $prefix = null ) {
+               return in_array( $name, $this->listViews( $prefix ) );
+       }
+}
+
diff --git a/includes/libs/rdbms/database/DatabaseMysqli.php b/includes/libs/rdbms/database/DatabaseMysqli.php
new file mode 100644 (file)
index 0000000..c34f901
--- /dev/null
@@ -0,0 +1,332 @@
+<?php
+/**
+ * This is the MySQLi database abstraction layer.
+ *
+ * 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
+ */
+
+/**
+ * Database abstraction object for PHP extension mysqli.
+ *
+ * @ingroup Database
+ * @since 1.22
+ * @see Database
+ */
+class DatabaseMysqli extends DatabaseMysqlBase {
+       /** @var mysqli */
+       protected $mConn;
+
+       /**
+        * @param string $sql
+        * @return resource
+        */
+       protected function doQuery( $sql ) {
+               $conn = $this->getBindingHandle();
+
+               if ( $this->bufferResults() ) {
+                       $ret = $conn->query( $sql );
+               } else {
+                       $ret = $conn->query( $sql, MYSQLI_USE_RESULT );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * @param string $realServer
+        * @return bool|mysqli
+        * @throws DBConnectionError
+        */
+       protected function mysqlConnect( $realServer ) {
+               # Avoid suppressed fatal error, which is very hard to track down
+               if ( !function_exists( 'mysqli_init' ) ) {
+                       throw new DBConnectionError( $this, "MySQLi functions missing,"
+                               . " have you compiled PHP with the --with-mysqli option?\n" );
+               }
+
+               // Other than mysql_connect, mysqli_real_connect expects an explicit port
+               // and socket parameters. So we need to parse the port and socket out of
+               // $realServer
+               $port = null;
+               $socket = null;
+               $hostAndPort = IP::splitHostAndPort( $realServer );
+               if ( $hostAndPort ) {
+                       $realServer = $hostAndPort[0];
+                       if ( $hostAndPort[1] ) {
+                               $port = $hostAndPort[1];
+                       }
+               } elseif ( substr_count( $realServer, ':' ) == 1 ) {
+                       // If we have a colon and something that's not a port number
+                       // inside the hostname, assume it's the socket location
+                       $hostAndSocket = explode( ':', $realServer );
+                       $realServer = $hostAndSocket[0];
+                       $socket = $hostAndSocket[1];
+               }
+
+               $mysqli = mysqli_init();
+
+               $connFlags = 0;
+               if ( $this->mFlags & self::DBO_SSL ) {
+                       $connFlags |= MYSQLI_CLIENT_SSL;
+                       $mysqli->ssl_set(
+                               $this->sslKeyPath,
+                               $this->sslCertPath,
+                               null,
+                               $this->sslCAPath,
+                               $this->sslCiphers
+                       );
+               }
+               if ( $this->mFlags & self::DBO_COMPRESS ) {
+                       $connFlags |= MYSQLI_CLIENT_COMPRESS;
+               }
+               if ( $this->mFlags & self::DBO_PERSISTENT ) {
+                       $realServer = 'p:' . $realServer;
+               }
+
+               if ( $this->utf8Mode ) {
+                       // Tell the server we're communicating with it in UTF-8.
+                       // This may engage various charset conversions.
+                       $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'utf8' );
+               } else {
+                       $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'binary' );
+               }
+               $mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, 3 );
+
+               if ( $mysqli->real_connect( $realServer, $this->mUser,
+                       $this->mPassword, $this->mDBname, $port, $socket, $connFlags )
+               ) {
+                       return $mysqli;
+               }
+
+               return false;
+       }
+
+       protected function connectInitCharset() {
+               // already done in mysqlConnect()
+               return true;
+       }
+
+       /**
+        * @param string $charset
+        * @return bool
+        */
+       protected function mysqlSetCharset( $charset ) {
+               $conn = $this->getBindingHandle();
+
+               if ( method_exists( $conn, 'set_charset' ) ) {
+                       return $conn->set_charset( $charset );
+               } else {
+                       return $this->query( 'SET NAMES ' . $charset, __METHOD__ );
+               }
+       }
+
+       /**
+        * @return bool
+        */
+       protected function closeConnection() {
+               $conn = $this->getBindingHandle();
+
+               return $conn->close();
+       }
+
+       /**
+        * @return int
+        */
+       function insertId() {
+               $conn = $this->getBindingHandle();
+
+               return (int)$conn->insert_id;
+       }
+
+       /**
+        * @return int
+        */
+       function lastErrno() {
+               if ( $this->mConn ) {
+                       return $this->mConn->errno;
+               } else {
+                       return mysqli_connect_errno();
+               }
+       }
+
+       /**
+        * @return int
+        */
+       function affectedRows() {
+               $conn = $this->getBindingHandle();
+
+               return $conn->affected_rows;
+       }
+
+       /**
+        * @param string $db
+        * @return bool
+        */
+       function selectDB( $db ) {
+               $conn = $this->getBindingHandle();
+
+               $this->mDBname = $db;
+
+               return $conn->select_db( $db );
+       }
+
+       /**
+        * @param mysqli $res
+        * @return bool
+        */
+       protected function mysqlFreeResult( $res ) {
+               $res->free_result();
+
+               return true;
+       }
+
+       /**
+        * @param mysqli $res
+        * @return bool
+        */
+       protected function mysqlFetchObject( $res ) {
+               $object = $res->fetch_object();
+               if ( $object === null ) {
+                       return false;
+               }
+
+               return $object;
+       }
+
+       /**
+        * @param mysqli $res
+        * @return bool
+        */
+       protected function mysqlFetchArray( $res ) {
+               $array = $res->fetch_array();
+               if ( $array === null ) {
+                       return false;
+               }
+
+               return $array;
+       }
+
+       /**
+        * @param mysqli $res
+        * @return mixed
+        */
+       protected function mysqlNumRows( $res ) {
+               return $res->num_rows;
+       }
+
+       /**
+        * @param mysqli $res
+        * @return mixed
+        */
+       protected function mysqlNumFields( $res ) {
+               return $res->field_count;
+       }
+
+       /**
+        * @param mysqli $res
+        * @param int $n
+        * @return mixed
+        */
+       protected function mysqlFetchField( $res, $n ) {
+               $field = $res->fetch_field_direct( $n );
+
+               // Add missing properties to result (using flags property)
+               // which will be part of function mysql-fetch-field for backward compatibility
+               $field->not_null = $field->flags & MYSQLI_NOT_NULL_FLAG;
+               $field->primary_key = $field->flags & MYSQLI_PRI_KEY_FLAG;
+               $field->unique_key = $field->flags & MYSQLI_UNIQUE_KEY_FLAG;
+               $field->multiple_key = $field->flags & MYSQLI_MULTIPLE_KEY_FLAG;
+               $field->binary = $field->flags & MYSQLI_BINARY_FLAG;
+               $field->numeric = $field->flags & MYSQLI_NUM_FLAG;
+               $field->blob = $field->flags & MYSQLI_BLOB_FLAG;
+               $field->unsigned = $field->flags & MYSQLI_UNSIGNED_FLAG;
+               $field->zerofill = $field->flags & MYSQLI_ZEROFILL_FLAG;
+
+               return $field;
+       }
+
+       /**
+        * @param resource|ResultWrapper $res
+        * @param int $n
+        * @return mixed
+        */
+       protected function mysqlFieldName( $res, $n ) {
+               $field = $res->fetch_field_direct( $n );
+
+               return $field->name;
+       }
+
+       /**
+        * @param resource|ResultWrapper $res
+        * @param int $n
+        * @return mixed
+        */
+       protected function mysqlFieldType( $res, $n ) {
+               $field = $res->fetch_field_direct( $n );
+
+               return $field->type;
+       }
+
+       /**
+        * @param resource|ResultWrapper $res
+        * @param int $row
+        * @return mixed
+        */
+       protected function mysqlDataSeek( $res, $row ) {
+               return $res->data_seek( $row );
+       }
+
+       /**
+        * @param mysqli $conn Optional connection object
+        * @return string
+        */
+       protected function mysqlError( $conn = null ) {
+               if ( $conn === null ) {
+                       return mysqli_connect_error();
+               } else {
+                       return $conn->error;
+               }
+       }
+
+       /**
+        * Escapes special characters in a string for use in an SQL statement
+        * @param string $s
+        * @return string
+        */
+       protected function mysqlRealEscapeString( $s ) {
+               $conn = $this->getBindingHandle();
+
+               return $conn->real_escape_string( $s );
+       }
+
+       /**
+        * Give an id for the connection
+        *
+        * mysql driver used resource id, but mysqli objects cannot be cast to string.
+        * @return string
+        */
+       public function __toString() {
+               if ( $this->mConn instanceof mysqli ) {
+                       return (string)$this->mConn->thread_id;
+               } else {
+                       // mConn might be false or something.
+                       return (string)$this->mConn;
+               }
+       }
+}
diff --git a/includes/libs/rdbms/database/DatabasePostgres.php b/includes/libs/rdbms/database/DatabasePostgres.php
new file mode 100644 (file)
index 0000000..f82d76d
--- /dev/null
@@ -0,0 +1,1435 @@
+<?php
+/**
+ * This is the Postgres database abstraction layer.
+ *
+ * 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
+ */
+use Wikimedia\WaitConditionLoop;
+
+/**
+ * @ingroup Database
+ */
+class DatabasePostgres extends Database {
+       /** @var int|bool */
+       protected $port;
+
+       /** @var resource */
+       protected $mLastResult = null;
+       /** @var int The number of rows affected as an integer */
+       protected $mAffectedRows = null;
+
+       /** @var int */
+       private $mInsertId = null;
+       /** @var float|string */
+       private $numericVersion = null;
+       /** @var string Connect string to open a PostgreSQL connection */
+       private $connectString;
+       /** @var string */
+       private $mCoreSchema;
+
+       public function __construct( array $params ) {
+               $this->port = isset( $params['port'] ) ? $params['port'] : false;
+               parent::__construct( $params );
+       }
+
+       function getType() {
+               return 'postgres';
+       }
+
+       function implicitGroupby() {
+               return false;
+       }
+
+       function implicitOrderby() {
+               return false;
+       }
+
+       function hasConstraint( $name ) {
+               $sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " .
+                       "WHERE c.connamespace = n.oid AND conname = '" .
+                       pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" .
+                       pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'";
+               $res = $this->doQuery( $sql );
+
+               return $this->numRows( $res );
+       }
+
+       /**
+        * Usually aborts on failure
+        * @param string $server
+        * @param string $user
+        * @param string $password
+        * @param string $dbName
+        * @throws DBConnectionError|Exception
+        * @return resource|bool|null
+        */
+       function open( $server, $user, $password, $dbName ) {
+               # Test for Postgres support, to avoid suppressed fatal error
+               if ( !function_exists( 'pg_connect' ) ) {
+                       throw new DBConnectionError(
+                               $this,
+                               "Postgres functions missing, have you compiled PHP with the --with-pgsql\n" .
+                               "option? (Note: if you recently installed PHP, you may need to restart your\n" .
+                               "webserver and database)\n"
+                       );
+               }
+
+               if ( !strlen( $user ) ) { # e.g. the class is being loaded
+                       return null;
+               }
+
+               $this->mServer = $server;
+               $this->mUser = $user;
+               $this->mPassword = $password;
+               $this->mDBname = $dbName;
+
+               $connectVars = [
+                       'dbname' => $dbName,
+                       'user' => $user,
+                       'password' => $password
+               ];
+               if ( $server != false && $server != '' ) {
+                       $connectVars['host'] = $server;
+               }
+               if ( (int)$this->port > 0 ) {
+                       $connectVars['port'] = (int)$this->port;
+               }
+               if ( $this->mFlags & self::DBO_SSL ) {
+                       $connectVars['sslmode'] = 1;
+               }
+
+               $this->connectString = $this->makeConnectionString( $connectVars );
+               $this->close();
+               $this->installErrorHandler();
+
+               try {
+                       $this->mConn = pg_connect( $this->connectString );
+               } catch ( Exception $ex ) {
+                       $this->restoreErrorHandler();
+                       throw $ex;
+               }
+
+               $phpError = $this->restoreErrorHandler();
+
+               if ( !$this->mConn ) {
+                       $this->queryLogger->debug( "DB connection error\n" );
+                       $this->queryLogger->debug(
+                               "Server: $server, Database: $dbName, User: $user, Password: " .
+                               substr( $password, 0, 3 ) . "...\n" );
+                       $this->queryLogger->debug( $this->lastError() . "\n" );
+                       throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) );
+               }
+
+               $this->mOpened = true;
+
+               # If called from the command-line (e.g. importDump), only show errors
+               if ( $this->cliMode ) {
+                       $this->doQuery( "SET client_min_messages = 'ERROR'" );
+               }
+
+               $this->query( "SET client_encoding='UTF8'", __METHOD__ );
+               $this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ );
+               $this->query( "SET timezone = 'GMT'", __METHOD__ );
+               $this->query( "SET standard_conforming_strings = on", __METHOD__ );
+               if ( $this->getServerVersion() >= 9.0 ) {
+                       $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
+               }
+
+               $this->determineCoreSchema( $this->mSchema );
+
+               return $this->mConn;
+       }
+
+       /**
+        * Postgres doesn't support selectDB in the same way MySQL does. So if the
+        * DB name doesn't match the open connection, open a new one
+        * @param string $db
+        * @return bool
+        */
+       function selectDB( $db ) {
+               if ( $this->mDBname !== $db ) {
+                       return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db );
+               } else {
+                       return true;
+               }
+       }
+
+       function makeConnectionString( $vars ) {
+               $s = '';
+               foreach ( $vars as $name => $value ) {
+                       $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
+               }
+
+               return $s;
+       }
+
+       /**
+        * Closes a database connection, if it is open
+        * Returns success, true if already closed
+        * @return bool
+        */
+       protected function closeConnection() {
+               return pg_close( $this->mConn );
+       }
+
+       public function doQuery( $sql ) {
+               $sql = mb_convert_encoding( $sql, 'UTF-8' );
+               // Clear previously left over PQresult
+               while ( $res = pg_get_result( $this->mConn ) ) {
+                       pg_free_result( $res );
+               }
+               if ( pg_send_query( $this->mConn, $sql ) === false ) {
+                       throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
+               }
+               $this->mLastResult = pg_get_result( $this->mConn );
+               $this->mAffectedRows = null;
+               if ( pg_result_error( $this->mLastResult ) ) {
+                       return false;
+               }
+
+               return $this->mLastResult;
+       }
+
+       protected function dumpError() {
+               $diags = [
+                       PGSQL_DIAG_SEVERITY,
+                       PGSQL_DIAG_SQLSTATE,
+                       PGSQL_DIAG_MESSAGE_PRIMARY,
+                       PGSQL_DIAG_MESSAGE_DETAIL,
+                       PGSQL_DIAG_MESSAGE_HINT,
+                       PGSQL_DIAG_STATEMENT_POSITION,
+                       PGSQL_DIAG_INTERNAL_POSITION,
+                       PGSQL_DIAG_INTERNAL_QUERY,
+                       PGSQL_DIAG_CONTEXT,
+                       PGSQL_DIAG_SOURCE_FILE,
+                       PGSQL_DIAG_SOURCE_LINE,
+                       PGSQL_DIAG_SOURCE_FUNCTION
+               ];
+               foreach ( $diags as $d ) {
+                       $this->queryLogger->debug( sprintf( "PgSQL ERROR(%d): %s\n",
+                               $d, pg_result_error_field( $this->mLastResult, $d ) ) );
+               }
+       }
+
+       function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+               if ( $tempIgnore ) {
+                       /* Check for constraint violation */
+                       if ( $errno === '23505' ) {
+                               parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore );
+
+                               return;
+                       }
+               }
+               /* Transaction stays in the ERROR state until rolled back */
+               if ( $this->mTrxLevel ) {
+                       $ignore = $this->ignoreErrors( true );
+                       $this->rollback( __METHOD__ );
+                       $this->ignoreErrors( $ignore );
+               }
+               parent::reportQueryError( $error, $errno, $sql, $fname, false );
+       }
+
+       function queryIgnore( $sql, $fname = __METHOD__ ) {
+               return $this->query( $sql, $fname, true );
+       }
+
+       /**
+        * @param stdClass|ResultWrapper $res
+        * @throws DBUnexpectedError
+        */
+       function freeResult( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               MediaWiki\suppressWarnings();
+               $ok = pg_free_result( $res );
+               MediaWiki\restoreWarnings();
+               if ( !$ok ) {
+                       throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" );
+               }
+       }
+
+       /**
+        * @param ResultWrapper|stdClass $res
+        * @return stdClass
+        * @throws DBUnexpectedError
+        */
+       function fetchObject( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               MediaWiki\suppressWarnings();
+               $row = pg_fetch_object( $res );
+               MediaWiki\restoreWarnings();
+               # @todo FIXME: HACK HACK HACK HACK debug
+
+               # @todo hashar: not sure if the following test really trigger if the object
+               #          fetching failed.
+               if ( pg_last_error( $this->mConn ) ) {
+                       throw new DBUnexpectedError(
+                               $this,
+                               'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
+                       );
+               }
+
+               return $row;
+       }
+
+       function fetchRow( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               MediaWiki\suppressWarnings();
+               $row = pg_fetch_array( $res );
+               MediaWiki\restoreWarnings();
+               if ( pg_last_error( $this->mConn ) ) {
+                       throw new DBUnexpectedError(
+                               $this,
+                               'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
+                       );
+               }
+
+               return $row;
+       }
+
+       function numRows( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               MediaWiki\suppressWarnings();
+               $n = pg_num_rows( $res );
+               MediaWiki\restoreWarnings();
+               if ( pg_last_error( $this->mConn ) ) {
+                       throw new DBUnexpectedError(
+                               $this,
+                               'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) )
+                       );
+               }
+
+               return $n;
+       }
+
+       function numFields( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+
+               return pg_num_fields( $res );
+       }
+
+       function fieldName( $res, $n ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+
+               return pg_field_name( $res, $n );
+       }
+
+       /**
+        * Return the result of the last call to nextSequenceValue();
+        * This must be called after nextSequenceValue().
+        *
+        * @return int|null
+        */
+       function insertId() {
+               return $this->mInsertId;
+       }
+
+       /**
+        * @param mixed $res
+        * @param int $row
+        * @return bool
+        */
+       function dataSeek( $res, $row ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+
+               return pg_result_seek( $res, $row );
+       }
+
+       function lastError() {
+               if ( $this->mConn ) {
+                       if ( $this->mLastResult ) {
+                               return pg_result_error( $this->mLastResult );
+                       } else {
+                               return pg_last_error();
+                       }
+               } else {
+                       return 'No database connection';
+               }
+       }
+
+       function lastErrno() {
+               if ( $this->mLastResult ) {
+                       return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE );
+               } else {
+                       return false;
+               }
+       }
+
+       function affectedRows() {
+               if ( !is_null( $this->mAffectedRows ) ) {
+                       // Forced result for simulated queries
+                       return $this->mAffectedRows;
+               }
+               if ( empty( $this->mLastResult ) ) {
+                       return 0;
+               }
+
+               return pg_affected_rows( $this->mLastResult );
+       }
+
+       /**
+        * Estimate rows in dataset
+        * Returns estimated count, based on EXPLAIN output
+        * This is not necessarily an accurate estimate, so use sparingly
+        * Returns -1 if count cannot be found
+        * Takes same arguments as Database::select()
+        *
+        * @param string $table
+        * @param string $vars
+        * @param string $conds
+        * @param string $fname
+        * @param array $options
+        * @return int
+        */
+       function estimateRowCount( $table, $vars = '*', $conds = '',
+               $fname = __METHOD__, $options = []
+       ) {
+               $options['EXPLAIN'] = true;
+               $res = $this->select( $table, $vars, $conds, $fname, $options );
+               $rows = -1;
+               if ( $res ) {
+                       $row = $this->fetchRow( $res );
+                       $count = [];
+                       if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
+                               $rows = (int)$count[1];
+                       }
+               }
+
+               return $rows;
+       }
+
+       /**
+        * Returns information about an index
+        * If errors are explicitly ignored, returns NULL on failure
+        *
+        * @param string $table
+        * @param string $index
+        * @param string $fname
+        * @return bool|null
+        */
+       function indexInfo( $table, $index, $fname = __METHOD__ ) {
+               $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
+               $res = $this->query( $sql, $fname );
+               if ( !$res ) {
+                       return null;
+               }
+               foreach ( $res as $row ) {
+                       if ( $row->indexname == $this->indexName( $index ) ) {
+                               return $row;
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * Returns is of attributes used in index
+        *
+        * @since 1.19
+        * @param string $index
+        * @param bool|string $schema
+        * @return array
+        */
+       function indexAttributes( $index, $schema = false ) {
+               if ( $schema === false ) {
+                       $schema = $this->getCoreSchema();
+               }
+               /*
+                * A subquery would be not needed if we didn't care about the order
+                * of attributes, but we do
+                */
+               $sql = <<<__INDEXATTR__
+
+                       SELECT opcname,
+                               attname,
+                               i.indoption[s.g] as option,
+                               pg_am.amname
+                       FROM
+                               (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g
+                                       FROM
+                                               pg_index isub
+                                       JOIN pg_class cis
+                                               ON cis.oid=isub.indexrelid
+                                       JOIN pg_namespace ns
+                                               ON cis.relnamespace = ns.oid
+                                       WHERE cis.relname='$index' AND ns.nspname='$schema') AS s,
+                               pg_attribute,
+                               pg_opclass opcls,
+                               pg_am,
+                               pg_class ci
+                               JOIN pg_index i
+                                       ON ci.oid=i.indexrelid
+                               JOIN pg_class ct
+                                       ON ct.oid = i.indrelid
+                               JOIN pg_namespace n
+                                       ON ci.relnamespace = n.oid
+                               WHERE
+                                       ci.relname='$index' AND n.nspname='$schema'
+                                       AND     attrelid = ct.oid
+                                       AND     i.indkey[s.g] = attnum
+                                       AND     i.indclass[s.g] = opcls.oid
+                                       AND     pg_am.oid = opcls.opcmethod
+__INDEXATTR__;
+               $res = $this->query( $sql, __METHOD__ );
+               $a = [];
+               if ( $res ) {
+                       foreach ( $res as $row ) {
+                               $a[] = [
+                                       $row->attname,
+                                       $row->opcname,
+                                       $row->amname,
+                                       $row->option ];
+                       }
+               } else {
+                       return null;
+               }
+
+               return $a;
+       }
+
+       function indexUnique( $table, $index, $fname = __METHOD__ ) {
+               $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" .
+                       " AND indexdef LIKE 'CREATE UNIQUE%(" .
+                       $this->strencode( $this->indexName( $index ) ) .
+                       ")'";
+               $res = $this->query( $sql, $fname );
+               if ( !$res ) {
+                       return null;
+               }
+
+               return $res->numRows() > 0;
+       }
+
+       function selectSQLText(
+               $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
+       ) {
+               // Change the FOR UPDATE option as necessary based on the join conditions. Then pass
+               // to the parent function to get the actual SQL text.
+               // In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
+               // can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to
+               // do so causes a DB error. This wrapper checks which tables can be locked and adjusts it
+               // accordingly.
+               // MySQL uses "ORDER BY NULL" as an optimization hint, but that is illegal in PostgreSQL.
+               if ( is_array( $options ) ) {
+                       $forUpdateKey = array_search( 'FOR UPDATE', $options, true );
+                       if ( $forUpdateKey !== false && $join_conds ) {
+                               unset( $options[$forUpdateKey] );
+
+                               foreach ( $join_conds as $table_cond => $join_cond ) {
+                                       if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
+                                               $options['FOR UPDATE'][] = $table_cond;
+                                       }
+                               }
+                       }
+
+                       if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
+                               unset( $options['ORDER BY'] );
+                       }
+               }
+
+               return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
+       }
+
+       /**
+        * INSERT wrapper, inserts an array into a table
+        *
+        * $args may be a single associative array, or an array of these with numeric keys,
+        * for multi-row insert (Postgres version 8.2 and above only).
+        *
+        * @param string $table Name of the table to insert to.
+        * @param array $args Items to insert into the table.
+        * @param string $fname Name of the function, for profiling
+        * @param array|string $options String or array. Valid options: IGNORE
+        * @return bool Success of insert operation. IGNORE always returns true.
+        */
+       function insert( $table, $args, $fname = __METHOD__, $options = [] ) {
+               if ( !count( $args ) ) {
+                       return true;
+               }
+
+               $table = $this->tableName( $table );
+               if ( !isset( $this->numericVersion ) ) {
+                       $this->getServerVersion();
+               }
+
+               if ( !is_array( $options ) ) {
+                       $options = [ $options ];
+               }
+
+               if ( isset( $args[0] ) && is_array( $args[0] ) ) {
+                       $multi = true;
+                       $keys = array_keys( $args[0] );
+               } else {
+                       $multi = false;
+                       $keys = array_keys( $args );
+               }
+
+               // If IGNORE is set, we use savepoints to emulate mysql's behavior
+               $savepoint = $olde = null;
+               $numrowsinserted = 0;
+               if ( in_array( 'IGNORE', $options ) ) {
+                       $savepoint = new SavepointPostgres( $this, 'mw', $this->queryLogger );
+                       $olde = error_reporting( 0 );
+                       // For future use, we may want to track the number of actual inserts
+                       // Right now, insert (all writes) simply return true/false
+               }
+
+               $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+               if ( $multi ) {
+                       if ( $this->numericVersion >= 8.2 && !$savepoint ) {
+                               $first = true;
+                               foreach ( $args as $row ) {
+                                       if ( $first ) {
+                                               $first = false;
+                                       } else {
+                                               $sql .= ',';
+                                       }
+                                       $sql .= '(' . $this->makeList( $row ) . ')';
+                               }
+                               $res = (bool)$this->query( $sql, $fname, $savepoint );
+                       } else {
+                               $res = true;
+                               $origsql = $sql;
+                               foreach ( $args as $row ) {
+                                       $tempsql = $origsql;
+                                       $tempsql .= '(' . $this->makeList( $row ) . ')';
+
+                                       if ( $savepoint ) {
+                                               $savepoint->savepoint();
+                                       }
+
+                                       $tempres = (bool)$this->query( $tempsql, $fname, $savepoint );
+
+                                       if ( $savepoint ) {
+                                               $bar = pg_result_error( $this->mLastResult );
+                                               if ( $bar != false ) {
+                                                       $savepoint->rollback();
+                                               } else {
+                                                       $savepoint->release();
+                                                       $numrowsinserted++;
+                                               }
+                                       }
+
+                                       // If any of them fail, we fail overall for this function call
+                                       // Note that this will be ignored if IGNORE is set
+                                       if ( !$tempres ) {
+                                               $res = false;
+                                       }
+                               }
+                       }
+               } else {
+                       // Not multi, just a lone insert
+                       if ( $savepoint ) {
+                               $savepoint->savepoint();
+                       }
+
+                       $sql .= '(' . $this->makeList( $args ) . ')';
+                       $res = (bool)$this->query( $sql, $fname, $savepoint );
+                       if ( $savepoint ) {
+                               $bar = pg_result_error( $this->mLastResult );
+                               if ( $bar != false ) {
+                                       $savepoint->rollback();
+                               } else {
+                                       $savepoint->release();
+                                       $numrowsinserted++;
+                               }
+                       }
+               }
+               if ( $savepoint ) {
+                       error_reporting( $olde );
+                       $savepoint->commit();
+
+                       // Set the affected row count for the whole operation
+                       $this->mAffectedRows = $numrowsinserted;
+
+                       // IGNORE always returns true
+                       return true;
+               }
+
+               return $res;
+       }
+
+       /**
+        * INSERT SELECT wrapper
+        * $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ]
+        * Source items may be literals rather then field names, but strings should
+        * be quoted with Database::addQuotes()
+        * $conds may be "*" to copy the whole table
+        * srcTable may be an array of tables.
+        * @todo FIXME: Implement this a little better (seperate select/insert)?
+        *
+        * @param string $destTable
+        * @param array|string $srcTable
+        * @param array $varMap
+        * @param array $conds
+        * @param string $fname
+        * @param array $insertOptions
+        * @param array $selectOptions
+        * @return bool
+        */
+       function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
+               $insertOptions = [], $selectOptions = [] ) {
+               $destTable = $this->tableName( $destTable );
+
+               if ( !is_array( $insertOptions ) ) {
+                       $insertOptions = [ $insertOptions ];
+               }
+
+               /*
+                * If IGNORE is set, we use savepoints to emulate mysql's behavior
+                * Ignore LOW PRIORITY option, since it is MySQL-specific
+                */
+               $savepoint = $olde = null;
+               $numrowsinserted = 0;
+               if ( in_array( 'IGNORE', $insertOptions ) ) {
+                       $savepoint = new SavepointPostgres( $this, 'mw', $this->queryLogger );
+                       $olde = error_reporting( 0 );
+                       $savepoint->savepoint();
+               }
+
+               if ( !is_array( $selectOptions ) ) {
+                       $selectOptions = [ $selectOptions ];
+               }
+               list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) =
+                       $this->makeSelectOptions( $selectOptions );
+               if ( is_array( $srcTable ) ) {
+                       $srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
+               } else {
+                       $srcTable = $this->tableName( $srcTable );
+               }
+
+               $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+                       " SELECT $startOpts " . implode( ',', $varMap ) .
+                       " FROM $srcTable $useIndex $ignoreIndex ";
+
+               if ( $conds != '*' ) {
+                       $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+               }
+
+               $sql .= " $tailOpts";
+
+               $res = (bool)$this->query( $sql, $fname, $savepoint );
+               if ( $savepoint ) {
+                       $bar = pg_result_error( $this->mLastResult );
+                       if ( $bar != false ) {
+                               $savepoint->rollback();
+                       } else {
+                               $savepoint->release();
+                               $numrowsinserted++;
+                       }
+                       error_reporting( $olde );
+                       $savepoint->commit();
+
+                       // Set the affected row count for the whole operation
+                       $this->mAffectedRows = $numrowsinserted;
+
+                       // IGNORE always returns true
+                       return true;
+               }
+
+               return $res;
+       }
+
+       function tableName( $name, $format = 'quoted' ) {
+               # Replace reserved words with better ones
+               switch ( $name ) {
+                       case 'user':
+                               return $this->realTableName( 'mwuser', $format );
+                       case 'text':
+                               return $this->realTableName( 'pagecontent', $format );
+                       default:
+                               return $this->realTableName( $name, $format );
+               }
+       }
+
+       /* Don't cheat on installer */
+       function realTableName( $name, $format = 'quoted' ) {
+               return parent::tableName( $name, $format );
+       }
+
+       /**
+        * Return the next in a sequence, save the value for retrieval via insertId()
+        *
+        * @param string $seqName
+        * @return int|null
+        */
+       function nextSequenceValue( $seqName ) {
+               $safeseq = str_replace( "'", "''", $seqName );
+               $res = $this->query( "SELECT nextval('$safeseq')" );
+               $row = $this->fetchRow( $res );
+               $this->mInsertId = $row[0];
+
+               return $this->mInsertId;
+       }
+
+       /**
+        * Return the current value of a sequence. Assumes it has been nextval'ed in this session.
+        *
+        * @param string $seqName
+        * @return int
+        */
+       function currentSequenceValue( $seqName ) {
+               $safeseq = str_replace( "'", "''", $seqName );
+               $res = $this->query( "SELECT currval('$safeseq')" );
+               $row = $this->fetchRow( $res );
+               $currval = $row[0];
+
+               return $currval;
+       }
+
+       # Returns the size of a text field, or -1 for "unlimited"
+       function textFieldSize( $table, $field ) {
+               $table = $this->tableName( $table );
+               $sql = "SELECT t.typname as ftype,a.atttypmod as size
+                       FROM pg_class c, pg_attribute a, pg_type t
+                       WHERE relname='$table' AND a.attrelid=c.oid AND
+                               a.atttypid=t.oid and a.attname='$field'";
+               $res = $this->query( $sql );
+               $row = $this->fetchObject( $res );
+               if ( $row->ftype == 'varchar' ) {
+                       $size = $row->size - 4;
+               } else {
+                       $size = $row->size;
+               }
+
+               return $size;
+       }
+
+       function limitResult( $sql, $limit, $offset = false ) {
+               return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
+       }
+
+       function wasDeadlock() {
+               return $this->lastErrno() == '40P01';
+       }
+
+       function duplicateTableStructure(
+               $oldName, $newName, $temporary = false, $fname = __METHOD__
+       ) {
+               $newName = $this->addIdentifierQuotes( $newName );
+               $oldName = $this->addIdentifierQuotes( $oldName );
+
+               return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " .
+                       "(LIKE $oldName INCLUDING DEFAULTS)", $fname );
+       }
+
+       function listTables( $prefix = null, $fname = __METHOD__ ) {
+               $eschema = $this->addQuotes( $this->getCoreSchema() );
+               $result = $this->query(
+                       "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
+               $endArray = [];
+
+               foreach ( $result as $table ) {
+                       $vars = get_object_vars( $table );
+                       $table = array_pop( $vars );
+                       if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
+                               $endArray[] = $table;
+                       }
+               }
+
+               return $endArray;
+       }
+
+       function timestamp( $ts = 0 ) {
+               $ct = new ConvertibleTimestamp( $ts );
+
+               return $ct->getTimestamp( TS_POSTGRES );
+       }
+
+       /**
+        * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12
+        * to http://www.php.net/manual/en/ref.pgsql.php
+        *
+        * Parsing a postgres array can be a tricky problem, he's my
+        * take on this, it handles multi-dimensional arrays plus
+        * escaping using a nasty regexp to determine the limits of each
+        * data-item.
+        *
+        * This should really be handled by PHP PostgreSQL module
+        *
+        * @since 1.19
+        * @param string $text Postgreql array returned in a text form like {a,b}
+        * @param string $output
+        * @param int|bool $limit
+        * @param int $offset
+        * @return string
+        */
+       function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
+               if ( false === $limit ) {
+                       $limit = strlen( $text ) - 1;
+                       $output = [];
+               }
+               if ( '{}' == $text ) {
+                       return $output;
+               }
+               do {
+                       if ( '{' != $text[$offset] ) {
+                               preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
+                                       $text, $match, 0, $offset );
+                               $offset += strlen( $match[0] );
+                               $output[] = ( '"' != $match[1][0]
+                                       ? $match[1]
+                                       : stripcslashes( substr( $match[1], 1, -1 ) ) );
+                               if ( '},' == $match[3] ) {
+                                       return $output;
+                               }
+                       } else {
+                               $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 );
+                       }
+               } while ( $limit > $offset );
+
+               return $output;
+       }
+
+       /**
+        * Return aggregated value function call
+        * @param array $valuedata
+        * @param string $valuename
+        * @return array
+        */
+       public function aggregateValue( $valuedata, $valuename = 'value' ) {
+               return $valuedata;
+       }
+
+       /**
+        * @return string Wikitext of a link to the server software's web site
+        */
+       public function getSoftwareLink() {
+               return '[{{int:version-db-postgres-url}} PostgreSQL]';
+       }
+
+       /**
+        * Return current schema (executes SELECT current_schema())
+        * Needs transaction
+        *
+        * @since 1.19
+        * @return string Default schema for the current session
+        */
+       function getCurrentSchema() {
+               $res = $this->query( "SELECT current_schema()", __METHOD__ );
+               $row = $this->fetchRow( $res );
+
+               return $row[0];
+       }
+
+       /**
+        * Return list of schemas which are accessible without schema name
+        * This is list does not contain magic keywords like "$user"
+        * Needs transaction
+        *
+        * @see getSearchPath()
+        * @see setSearchPath()
+        * @since 1.19
+        * @return array List of actual schemas for the current sesson
+        */
+       function getSchemas() {
+               $res = $this->query( "SELECT current_schemas(false)", __METHOD__ );
+               $row = $this->fetchRow( $res );
+               $schemas = [];
+
+               /* PHP pgsql support does not support array type, "{a,b}" string is returned */
+
+               return $this->pg_array_parse( $row[0], $schemas );
+       }
+
+       /**
+        * Return search patch for schemas
+        * This is different from getSchemas() since it contain magic keywords
+        * (like "$user").
+        * Needs transaction
+        *
+        * @since 1.19
+        * @return array How to search for table names schemas for the current user
+        */
+       function getSearchPath() {
+               $res = $this->query( "SHOW search_path", __METHOD__ );
+               $row = $this->fetchRow( $res );
+
+               /* PostgreSQL returns SHOW values as strings */
+
+               return explode( ",", $row[0] );
+       }
+
+       /**
+        * Update search_path, values should already be sanitized
+        * Values may contain magic keywords like "$user"
+        * @since 1.19
+        *
+        * @param array $search_path List of schemas to be searched by default
+        */
+       function setSearchPath( $search_path ) {
+               $this->query( "SET search_path = " . implode( ", ", $search_path ) );
+       }
+
+       /**
+        * Determine default schema for the current application
+        * Adjust this session schema search path if desired schema exists
+        * and is not alread there.
+        *
+        * We need to have name of the core schema stored to be able
+        * to query database metadata.
+        *
+        * This will be also called by the installer after the schema is created
+        *
+        * @since 1.19
+        *
+        * @param string $desiredSchema
+        */
+       function determineCoreSchema( $desiredSchema ) {
+               $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
+               if ( $this->schemaExists( $desiredSchema ) ) {
+                       if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
+                               $this->mCoreSchema = $desiredSchema;
+                               $this->queryLogger->debug(
+                                       "Schema \"" . $desiredSchema . "\" already in the search path\n" );
+                       } else {
+                               /**
+                                * Prepend our schema (e.g. 'mediawiki') in front
+                                * of the search path
+                                * Fixes bug 15816
+                                */
+                               $search_path = $this->getSearchPath();
+                               array_unshift( $search_path,
+                                       $this->addIdentifierQuotes( $desiredSchema ) );
+                               $this->setSearchPath( $search_path );
+                               $this->mCoreSchema = $desiredSchema;
+                               $this->queryLogger->debug(
+                                       "Schema \"" . $desiredSchema . "\" added to the search path\n" );
+                       }
+               } else {
+                       $this->mCoreSchema = $this->getCurrentSchema();
+                       $this->queryLogger->debug(
+                               "Schema \"" . $desiredSchema . "\" not found, using current \"" .
+                               $this->mCoreSchema . "\"\n" );
+               }
+               /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */
+               $this->commit( __METHOD__ );
+       }
+
+       /**
+        * Return schema name for core application tables
+        *
+        * @since 1.19
+        * @return string Core schema name
+        */
+       function getCoreSchema() {
+               return $this->mCoreSchema;
+       }
+
+       /**
+        * @return string Version information from the database
+        */
+       function getServerVersion() {
+               if ( !isset( $this->numericVersion ) ) {
+                       $versionInfo = pg_version( $this->mConn );
+                       if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
+                               // Old client, abort install
+                               $this->numericVersion = '7.3 or earlier';
+                       } elseif ( isset( $versionInfo['server'] ) ) {
+                               // Normal client
+                               $this->numericVersion = $versionInfo['server'];
+                       } else {
+                               // Bug 16937: broken pgsql extension from PHP<5.3
+                               $this->numericVersion = pg_parameter_status( $this->mConn, 'server_version' );
+                       }
+               }
+
+               return $this->numericVersion;
+       }
+
+       /**
+        * Query whether a given relation exists (in the given schema, or the
+        * default mw one if not given)
+        * @param string $table
+        * @param array|string $types
+        * @param bool|string $schema
+        * @return bool
+        */
+       function relationExists( $table, $types, $schema = false ) {
+               if ( !is_array( $types ) ) {
+                       $types = [ $types ];
+               }
+               if ( !$schema ) {
+                       $schema = $this->getCoreSchema();
+               }
+               $table = $this->realTableName( $table, 'raw' );
+               $etable = $this->addQuotes( $table );
+               $eschema = $this->addQuotes( $schema );
+               $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
+                       . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
+                       . "AND c.relkind IN ('" . implode( "','", $types ) . "')";
+               $res = $this->query( $sql );
+               $count = $res ? $res->numRows() : 0;
+
+               return (bool)$count;
+       }
+
+       /**
+        * For backward compatibility, this function checks both tables and
+        * views.
+        * @param string $table
+        * @param string $fname
+        * @param bool|string $schema
+        * @return bool
+        */
+       function tableExists( $table, $fname = __METHOD__, $schema = false ) {
+               return $this->relationExists( $table, [ 'r', 'v' ], $schema );
+       }
+
+       function sequenceExists( $sequence, $schema = false ) {
+               return $this->relationExists( $sequence, 'S', $schema );
+       }
+
+       function triggerExists( $table, $trigger ) {
+               $q = <<<SQL
+       SELECT 1 FROM pg_class, pg_namespace, pg_trigger
+               WHERE relnamespace=pg_namespace.oid AND relkind='r'
+                         AND tgrelid=pg_class.oid
+                         AND nspname=%s AND relname=%s AND tgname=%s
+SQL;
+               $res = $this->query(
+                       sprintf(
+                               $q,
+                               $this->addQuotes( $this->getCoreSchema() ),
+                               $this->addQuotes( $table ),
+                               $this->addQuotes( $trigger )
+                       )
+               );
+               if ( !$res ) {
+                       return null;
+               }
+               $rows = $res->numRows();
+
+               return $rows;
+       }
+
+       function ruleExists( $table, $rule ) {
+               $exists = $this->selectField( 'pg_rules', 'rulename',
+                       [
+                               'rulename' => $rule,
+                               'tablename' => $table,
+                               'schemaname' => $this->getCoreSchema()
+                       ]
+               );
+
+               return $exists === $rule;
+       }
+
+       function constraintExists( $table, $constraint ) {
+               $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " .
+                       "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
+                       $this->addQuotes( $this->getCoreSchema() ),
+                       $this->addQuotes( $table ),
+                       $this->addQuotes( $constraint )
+               );
+               $res = $this->query( $sql );
+               if ( !$res ) {
+                       return null;
+               }
+               $rows = $res->numRows();
+
+               return $rows;
+       }
+
+       /**
+        * Query whether a given schema exists. Returns true if it does, false if it doesn't.
+        * @param string $schema
+        * @return bool
+        */
+       function schemaExists( $schema ) {
+               $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1,
+                       [ 'nspname' => $schema ], __METHOD__ );
+
+               return (bool)$exists;
+       }
+
+       /**
+        * Returns true if a given role (i.e. user) exists, false otherwise.
+        * @param string $roleName
+        * @return bool
+        */
+       function roleExists( $roleName ) {
+               $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1,
+                       [ 'rolname' => $roleName ], __METHOD__ );
+
+               return (bool)$exists;
+       }
+
+       /**
+        * @var string $table
+        * @var string $field
+        * @return PostgresField|null
+        */
+       function fieldInfo( $table, $field ) {
+               return PostgresField::fromText( $this, $table, $field );
+       }
+
+       /**
+        * pg_field_type() wrapper
+        * @param ResultWrapper|resource $res ResultWrapper or PostgreSQL query result resource
+        * @param int $index Field number, starting from 0
+        * @return string
+        */
+       function fieldType( $res, $index ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+
+               return pg_field_type( $res, $index );
+       }
+
+       /**
+        * @param string $b
+        * @return Blob
+        */
+       function encodeBlob( $b ) {
+               return new PostgresBlob( pg_escape_bytea( $b ) );
+       }
+
+       function decodeBlob( $b ) {
+               if ( $b instanceof PostgresBlob ) {
+                       $b = $b->fetch();
+               } elseif ( $b instanceof Blob ) {
+                       return $b->fetch();
+               }
+
+               return pg_unescape_bytea( $b );
+       }
+
+       function strencode( $s ) {
+               // Should not be called by us
+
+               return pg_escape_string( $this->mConn, $s );
+       }
+
+       /**
+        * @param string|int|null|bool|Blob $s
+        * @return string|int
+        */
+       function addQuotes( $s ) {
+               if ( is_null( $s ) ) {
+                       return 'NULL';
+               } elseif ( is_bool( $s ) ) {
+                       return intval( $s );
+               } elseif ( $s instanceof Blob ) {
+                       if ( $s instanceof PostgresBlob ) {
+                               $s = $s->fetch();
+                       } else {
+                               $s = pg_escape_bytea( $this->mConn, $s->fetch() );
+                       }
+                       return "'$s'";
+               }
+
+               return "'" . pg_escape_string( $this->mConn, $s ) . "'";
+       }
+
+       /**
+        * Postgres specific version of replaceVars.
+        * Calls the parent version in Database.php
+        *
+        * @param string $ins SQL string, read from a stream (usually tables.sql)
+        * @return string SQL string
+        */
+       protected function replaceVars( $ins ) {
+               $ins = parent::replaceVars( $ins );
+
+               if ( $this->numericVersion >= 8.3 ) {
+                       // Thanks for not providing backwards-compatibility, 8.3
+                       $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
+               }
+
+               if ( $this->numericVersion <= 8.1 ) { // Our minimum version
+                       $ins = str_replace( 'USING gin', 'USING gist', $ins );
+               }
+
+               return $ins;
+       }
+
+       /**
+        * Various select options
+        *
+        * @param array $options An associative array of options to be turned into
+        *   an SQL query, valid keys are listed in the function.
+        * @return array
+        */
+       function makeSelectOptions( $options ) {
+               $preLimitTail = $postLimitTail = '';
+               $startOpts = $useIndex = $ignoreIndex = '';
+
+               $noKeyOptions = [];
+               foreach ( $options as $key => $option ) {
+                       if ( is_numeric( $key ) ) {
+                               $noKeyOptions[$option] = true;
+                       }
+               }
+
+               $preLimitTail .= $this->makeGroupByWithHaving( $options );
+
+               $preLimitTail .= $this->makeOrderBy( $options );
+
+               // if ( isset( $options['LIMIT'] ) ) {
+               //      $tailOpts .= $this->limitResult( '', $options['LIMIT'],
+               //              isset( $options['OFFSET'] ) ? $options['OFFSET']
+               //              : false );
+               // }
+
+               if ( isset( $options['FOR UPDATE'] ) ) {
+                       $postLimitTail .= ' FOR UPDATE OF ' .
+                               implode( ', ', array_map( [ &$this, 'tableName' ], $options['FOR UPDATE'] ) );
+               } elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
+                       $postLimitTail .= ' FOR UPDATE';
+               }
+
+               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
+                       $startOpts .= 'DISTINCT';
+               }
+
+               return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
+       }
+
+       function getDBname() {
+               return $this->mDBname;
+       }
+
+       function getServer() {
+               return $this->mServer;
+       }
+
+       function buildConcat( $stringList ) {
+               return implode( ' || ', $stringList );
+       }
+
+       public function buildGroupConcatField(
+               $delimiter, $table, $field, $conds = '', $options = [], $join_conds = []
+       ) {
+               $fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delimiter ) . ')';
+
+               return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
+       }
+
+       /**
+        * @param string $field Field or column to cast
+        * @return string
+        * @since 1.28
+        */
+       public function buildStringCast( $field ) {
+               return $field . '::text';
+       }
+
+       public function streamStatementEnd( &$sql, &$newLine ) {
+               # Allow dollar quoting for function declarations
+               if ( substr( $newLine, 0, 4 ) == '$mw$' ) {
+                       if ( $this->delimiter ) {
+                               $this->delimiter = false;
+                       } else {
+                               $this->delimiter = ';';
+                       }
+               }
+
+               return parent::streamStatementEnd( $sql, $newLine );
+       }
+
+       /**
+        * Check to see if a named lock is available. This is non-blocking.
+        * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+        *
+        * @param string $lockName Name of lock to poll
+        * @param string $method Name of method calling us
+        * @return bool
+        * @since 1.20
+        */
+       public function lockIsFree( $lockName, $method ) {
+               $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
+               $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key))
+                       WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method );
+               $row = $this->fetchObject( $result );
+
+               return ( $row->lockstatus === 't' );
+       }
+
+       /**
+        * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+        * @param string $lockName
+        * @param string $method
+        * @param int $timeout
+        * @return bool
+        */
+       public function lock( $lockName, $method, $timeout = 5 ) {
+               $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
+               $loop = new WaitConditionLoop(
+                       function () use ( $lockName, $key, $timeout, $method ) {
+                               $res = $this->query( "SELECT pg_try_advisory_lock($key) AS lockstatus", $method );
+                               $row = $this->fetchObject( $res );
+                               if ( $row->lockstatus === 't' ) {
+                                       parent::lock( $lockName, $method, $timeout ); // record
+                                       return true;
+                               }
+
+                               return WaitConditionLoop::CONDITION_CONTINUE;
+                       },
+                       $timeout
+               );
+
+               return ( $loop->invoke() === $loop::CONDITION_REACHED );
+       }
+
+       /**
+        * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM
+        * PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+        * @param string $lockName
+        * @param string $method
+        * @return bool
+        */
+       public function unlock( $lockName, $method ) {
+               $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) );
+               $result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method );
+               $row = $this->fetchObject( $result );
+
+               if ( $row->lockstatus === 't' ) {
+                       parent::unlock( $lockName, $method ); // record
+                       return true;
+               }
+
+               $this->queryLogger->debug( __METHOD__ . " failed to release lock\n" );
+
+               return false;
+       }
+
+       /**
+        * @param string $lockName
+        * @return string Integer
+        */
+       private function bigintFromLockName( $lockName ) {
+               return Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 );
+       }
+}
diff --git a/includes/libs/rdbms/database/DatabaseSqlite.php b/includes/libs/rdbms/database/DatabaseSqlite.php
new file mode 100644 (file)
index 0000000..3ccf3f0
--- /dev/null
@@ -0,0 +1,1049 @@
+<?php
+/**
+ * This is the SQLite database abstraction layer.
+ * See maintenance/sqlite/README for development notes and other specific information
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
+/**
+ * @ingroup Database
+ */
+class DatabaseSqlite extends Database {
+       /** @var bool Whether full text is enabled */
+       private static $fulltextEnabled = null;
+
+       /** @var string Directory */
+       protected $dbDir;
+
+       /** @var string File name for SQLite database file */
+       protected $dbPath;
+
+       /** @var string Transaction mode */
+       protected $trxMode;
+
+       /** @var int The number of rows affected as an integer */
+       protected $mAffectedRows;
+
+       /** @var resource */
+       protected $mLastResult;
+
+       /** @var PDO */
+       protected $mConn;
+
+       /** @var FSLockManager (hopefully on the same server as the DB) */
+       protected $lockMgr;
+
+       /**
+        * Additional params include:
+        *   - dbDirectory : directory containing the DB and the lock file directory
+        *                   [defaults to $wgSQLiteDataDir]
+        *   - dbFilePath  : use this to force the path of the DB file
+        *   - trxMode     : one of (deferred, immediate, exclusive)
+        * @param array $p
+        */
+       function __construct( array $p ) {
+               if ( isset( $p['dbFilePath'] ) ) {
+                       parent::__construct( $p );
+                       // Standalone .sqlite file mode.
+                       // Super doesn't open when $user is false, but we can work with $dbName,
+                       // which is derived from the file path in this case.
+                       $this->openFile( $p['dbFilePath'] );
+                       $lockDomain = md5( $p['dbFilePath'] );
+               } elseif ( !isset( $p['dbDirectory'] ) ) {
+                       throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
+               } else {
+                       $this->dbDir = $p['dbDirectory'];
+                       $this->mDBname = $p['dbname'];
+                       $lockDomain = $this->mDBname;
+                       // Stock wiki mode using standard file names per DB.
+                       parent::__construct( $p );
+                       // Super doesn't open when $user is false, but we can work with $dbName
+                       if ( $p['dbname'] && !$this->isOpen() ) {
+                               if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) {
+                                       $done = [];
+                                       foreach ( $this->tableAliases as $params ) {
+                                               if ( isset( $done[$params['dbname']] ) ) {
+                                                       continue;
+                                               }
+                                               $this->attachDatabase( $params['dbname'] );
+                                               $done[$params['dbname']] = 1;
+                                       }
+                               }
+                       }
+               }
+
+               $this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
+               if ( $this->trxMode &&
+                       !in_array( $this->trxMode, [ 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ] )
+               ) {
+                       $this->trxMode = null;
+                       $this->queryLogger->warning( "Invalid SQLite transaction mode provided." );
+               }
+
+               $this->lockMgr = new FSLockManager( [
+                       'domain' => $lockDomain,
+                       'lockDirectory' => "{$this->dbDir}/locks"
+               ] );
+       }
+
+       /**
+        * @param string $filename
+        * @param array $p Options map; supports:
+        *   - flags       : (same as __construct counterpart)
+        *   - trxMode     : (same as __construct counterpart)
+        *   - dbDirectory : (same as __construct counterpart)
+        * @return DatabaseSqlite
+        * @since 1.25
+        */
+       public static function newStandaloneInstance( $filename, array $p = [] ) {
+               $p['dbFilePath'] = $filename;
+               $p['schema'] = false;
+               $p['tablePrefix'] = '';
+
+               return Database::factory( 'sqlite', $p );
+       }
+
+       /**
+        * @return string
+        */
+       function getType() {
+               return 'sqlite';
+       }
+
+       /**
+        * @todo Check if it should be true like parent class
+        *
+        * @return bool
+        */
+       function implicitGroupby() {
+               return false;
+       }
+
+       /** Open an SQLite database and return a resource handle to it
+        *  NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
+        *
+        * @param string $server
+        * @param string $user
+        * @param string $pass
+        * @param string $dbName
+        *
+        * @throws DBConnectionError
+        * @return PDO
+        */
+       function open( $server, $user, $pass, $dbName ) {
+               $this->close();
+               $fileName = self::generateFileName( $this->dbDir, $dbName );
+               if ( !is_readable( $fileName ) ) {
+                       $this->mConn = false;
+                       throw new DBConnectionError( $this, "SQLite database not accessible" );
+               }
+               $this->openFile( $fileName );
+
+               return $this->mConn;
+       }
+
+       /**
+        * Opens a database file
+        *
+        * @param string $fileName
+        * @throws DBConnectionError
+        * @return PDO|bool SQL connection or false if failed
+        */
+       protected function openFile( $fileName ) {
+               $err = false;
+
+               $this->dbPath = $fileName;
+               try {
+                       if ( $this->mFlags & self::DBO_PERSISTENT ) {
+                               $this->mConn = new PDO( "sqlite:$fileName", '', '',
+                                       [ PDO::ATTR_PERSISTENT => true ] );
+                       } else {
+                               $this->mConn = new PDO( "sqlite:$fileName", '', '' );
+                       }
+               } catch ( PDOException $e ) {
+                       $err = $e->getMessage();
+               }
+
+               if ( !$this->mConn ) {
+                       $this->queryLogger->debug( "DB connection error: $err\n" );
+                       throw new DBConnectionError( $this, $err );
+               }
+
+               $this->mOpened = !!$this->mConn;
+               if ( $this->mOpened ) {
+                       # Set error codes only, don't raise exceptions
+                       $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
+                       # Enforce LIKE to be case sensitive, just like MySQL
+                       $this->query( 'PRAGMA case_sensitive_like = 1' );
+
+                       return $this->mConn;
+               }
+
+               return false;
+       }
+
+       /**
+        * @return string SQLite DB file path
+        * @since 1.25
+        */
+       public function getDbFilePath() {
+               return $this->dbPath;
+       }
+
+       /**
+        * Does not actually close the connection, just destroys the reference for GC to do its work
+        * @return bool
+        */
+       protected function closeConnection() {
+               $this->mConn = null;
+
+               return true;
+       }
+
+       /**
+        * Generates a database file name. Explicitly public for installer.
+        * @param string $dir Directory where database resides
+        * @param string $dbName Database name
+        * @return string
+        */
+       public static function generateFileName( $dir, $dbName ) {
+               return "$dir/$dbName.sqlite";
+       }
+
+       /**
+        * Check if the searchindext table is FTS enabled.
+        * @return bool False if not enabled.
+        */
+       function checkForEnabledSearch() {
+               if ( self::$fulltextEnabled === null ) {
+                       self::$fulltextEnabled = false;
+                       $table = $this->tableName( 'searchindex' );
+                       $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
+                       if ( $res ) {
+                               $row = $res->fetchRow();
+                               self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
+                       }
+               }
+
+               return self::$fulltextEnabled;
+       }
+
+       /**
+        * Returns version of currently supported SQLite fulltext search module or false if none present.
+        * @return string
+        */
+       static function getFulltextSearchModule() {
+               static $cachedResult = null;
+               if ( $cachedResult !== null ) {
+                       return $cachedResult;
+               }
+               $cachedResult = false;
+               $table = 'dummy_search_test';
+
+               $db = self::newStandaloneInstance( ':memory:' );
+               if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
+                       $cachedResult = 'FTS3';
+               }
+               $db->close();
+
+               return $cachedResult;
+       }
+
+       /**
+        * Attaches external database to our connection, see http://sqlite.org/lang_attach.html
+        * for details.
+        *
+        * @param string $name Database name to be used in queries like
+        *   SELECT foo FROM dbname.table
+        * @param bool|string $file Database file name. If omitted, will be generated
+        *   using $name and configured data directory
+        * @param string $fname Calling function name
+        * @return ResultWrapper
+        */
+       function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
+               if ( !$file ) {
+                       $file = self::generateFileName( $this->dbDir, $name );
+               }
+               $file = $this->addQuotes( $file );
+
+               return $this->query( "ATTACH DATABASE $file AS $name", $fname );
+       }
+
+       function isWriteQuery( $sql ) {
+               return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
+       }
+
+       /**
+        * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
+        *
+        * @param string $sql
+        * @return bool|ResultWrapper
+        */
+       protected function doQuery( $sql ) {
+               $res = $this->mConn->query( $sql );
+               if ( $res === false ) {
+                       return false;
+               }
+
+               $r = $res instanceof ResultWrapper ? $res->result : $res;
+               $this->mAffectedRows = $r->rowCount();
+               $res = new ResultWrapper( $this, $r->fetchAll() );
+
+               return $res;
+       }
+
+       /**
+        * @param ResultWrapper|mixed $res
+        */
+       function freeResult( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res->result = null;
+               } else {
+                       $res = null;
+               }
+       }
+
+       /**
+        * @param ResultWrapper|array $res
+        * @return stdClass|bool
+        */
+       function fetchObject( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $r =& $res->result;
+               } else {
+                       $r =& $res;
+               }
+
+               $cur = current( $r );
+               if ( is_array( $cur ) ) {
+                       next( $r );
+                       $obj = new stdClass;
+                       foreach ( $cur as $k => $v ) {
+                               if ( !is_numeric( $k ) ) {
+                                       $obj->$k = $v;
+                               }
+                       }
+
+                       return $obj;
+               }
+
+               return false;
+       }
+
+       /**
+        * @param ResultWrapper|mixed $res
+        * @return array|bool
+        */
+       function fetchRow( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $r =& $res->result;
+               } else {
+                       $r =& $res;
+               }
+               $cur = current( $r );
+               if ( is_array( $cur ) ) {
+                       next( $r );
+
+                       return $cur;
+               }
+
+               return false;
+       }
+
+       /**
+        * The PDO::Statement class implements the array interface so count() will work
+        *
+        * @param ResultWrapper|array $res
+        * @return int
+        */
+       function numRows( $res ) {
+               $r = $res instanceof ResultWrapper ? $res->result : $res;
+
+               return count( $r );
+       }
+
+       /**
+        * @param ResultWrapper $res
+        * @return int
+        */
+       function numFields( $res ) {
+               $r = $res instanceof ResultWrapper ? $res->result : $res;
+               if ( is_array( $r ) && count( $r ) > 0 ) {
+                       // The size of the result array is twice the number of fields. (Bug: 65578)
+                       return count( $r[0] ) / 2;
+               } else {
+                       // If the result is empty return 0
+                       return 0;
+               }
+       }
+
+       /**
+        * @param ResultWrapper $res
+        * @param int $n
+        * @return bool
+        */
+       function fieldName( $res, $n ) {
+               $r = $res instanceof ResultWrapper ? $res->result : $res;
+               if ( is_array( $r ) ) {
+                       $keys = array_keys( $r[0] );
+
+                       return $keys[$n];
+               }
+
+               return false;
+       }
+
+       /**
+        * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
+        *
+        * @param string $name
+        * @param string $format
+        * @return string
+        */
+       function tableName( $name, $format = 'quoted' ) {
+               // table names starting with sqlite_ are reserved
+               if ( strpos( $name, 'sqlite_' ) === 0 ) {
+                       return $name;
+               }
+
+               return str_replace( '"', '', parent::tableName( $name, $format ) );
+       }
+
+       /**
+        * This must be called after nextSequenceVal
+        *
+        * @return int
+        */
+       function insertId() {
+               // PDO::lastInsertId yields a string :(
+               return intval( $this->mConn->lastInsertId() );
+       }
+
+       /**
+        * @param ResultWrapper|array $res
+        * @param int $row
+        */
+       function dataSeek( $res, $row ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $r =& $res->result;
+               } else {
+                       $r =& $res;
+               }
+               reset( $r );
+               if ( $row > 0 ) {
+                       for ( $i = 0; $i < $row; $i++ ) {
+                               next( $r );
+                       }
+               }
+       }
+
+       /**
+        * @return string
+        */
+       function lastError() {
+               if ( !is_object( $this->mConn ) ) {
+                       return "Cannot return last error, no db connection";
+               }
+               $e = $this->mConn->errorInfo();
+
+               return isset( $e[2] ) ? $e[2] : '';
+       }
+
+       /**
+        * @return string
+        */
+       function lastErrno() {
+               if ( !is_object( $this->mConn ) ) {
+                       return "Cannot return last error, no db connection";
+               } else {
+                       $info = $this->mConn->errorInfo();
+
+                       return $info[1];
+               }
+       }
+
+       /**
+        * @return int
+        */
+       function affectedRows() {
+               return $this->mAffectedRows;
+       }
+
+       /**
+        * Returns information about an index
+        * Returns false if the index does not exist
+        * - if errors are explicitly ignored, returns NULL on failure
+        *
+        * @param string $table
+        * @param string $index
+        * @param string $fname
+        * @return array
+        */
+       function indexInfo( $table, $index, $fname = __METHOD__ ) {
+               $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
+               $res = $this->query( $sql, $fname );
+               if ( !$res ) {
+                       return null;
+               }
+               if ( $res->numRows() == 0 ) {
+                       return false;
+               }
+               $info = [];
+               foreach ( $res as $row ) {
+                       $info[] = $row->name;
+               }
+
+               return $info;
+       }
+
+       /**
+        * @param string $table
+        * @param string $index
+        * @param string $fname
+        * @return bool|null
+        */
+       function indexUnique( $table, $index, $fname = __METHOD__ ) {
+               $row = $this->selectRow( 'sqlite_master', '*',
+                       [
+                               'type' => 'index',
+                               'name' => $this->indexName( $index ),
+                       ], $fname );
+               if ( !$row || !isset( $row->sql ) ) {
+                       return null;
+               }
+
+               // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
+               $indexPos = strpos( $row->sql, 'INDEX' );
+               if ( $indexPos === false ) {
+                       return null;
+               }
+               $firstPart = substr( $row->sql, 0, $indexPos );
+               $options = explode( ' ', $firstPart );
+
+               return in_array( 'UNIQUE', $options );
+       }
+
+       /**
+        * Filter the options used in SELECT statements
+        *
+        * @param array $options
+        * @return array
+        */
+       function makeSelectOptions( $options ) {
+               foreach ( $options as $k => $v ) {
+                       if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
+                               $options[$k] = '';
+                       }
+               }
+
+               return parent::makeSelectOptions( $options );
+       }
+
+       /**
+        * @param array $options
+        * @return string
+        */
+       protected function makeUpdateOptionsArray( $options ) {
+               $options = parent::makeUpdateOptionsArray( $options );
+               $options = self::fixIgnore( $options );
+
+               return $options;
+       }
+
+       /**
+        * @param array $options
+        * @return array
+        */
+       static function fixIgnore( $options ) {
+               # SQLite uses OR IGNORE not just IGNORE
+               foreach ( $options as $k => $v ) {
+                       if ( $v == 'IGNORE' ) {
+                               $options[$k] = 'OR IGNORE';
+                       }
+               }
+
+               return $options;
+       }
+
+       /**
+        * @param array $options
+        * @return string
+        */
+       function makeInsertOptions( $options ) {
+               $options = self::fixIgnore( $options );
+
+               return parent::makeInsertOptions( $options );
+       }
+
+       /**
+        * Based on generic method (parent) with some prior SQLite-sepcific adjustments
+        * @param string $table
+        * @param array $a
+        * @param string $fname
+        * @param array $options
+        * @return bool
+        */
+       function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
+               if ( !count( $a ) ) {
+                       return true;
+               }
+
+               # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
+               if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+                       $ret = true;
+                       foreach ( $a as $v ) {
+                               if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) {
+                                       $ret = false;
+                               }
+                       }
+               } else {
+                       $ret = parent::insert( $table, $a, "$fname/single-row", $options );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * @param string $table
+        * @param array $uniqueIndexes Unused
+        * @param string|array $rows
+        * @param string $fname
+        * @return bool|ResultWrapper
+        */
+       function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
+               if ( !count( $rows ) ) {
+                       return true;
+               }
+
+               # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
+               if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
+                       $ret = true;
+                       foreach ( $rows as $v ) {
+                               if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) {
+                                       $ret = false;
+                               }
+                       }
+               } else {
+                       $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Returns the size of a text field, or -1 for "unlimited"
+        * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though.
+        *
+        * @param string $table
+        * @param string $field
+        * @return int
+        */
+       function textFieldSize( $table, $field ) {
+               return -1;
+       }
+
+       /**
+        * @return bool
+        */
+       function unionSupportsOrderAndLimit() {
+               return false;
+       }
+
+       /**
+        * @param string $sqls
+        * @param bool $all Whether to "UNION ALL" or not
+        * @return string
+        */
+       function unionQueries( $sqls, $all ) {
+               $glue = $all ? ' UNION ALL ' : ' UNION ';
+
+               return implode( $glue, $sqls );
+       }
+
+       /**
+        * @return bool
+        */
+       function wasDeadlock() {
+               return $this->lastErrno() == 5; // SQLITE_BUSY
+       }
+
+       /**
+        * @return bool
+        */
+       function wasErrorReissuable() {
+               return $this->lastErrno() == 17; // SQLITE_SCHEMA;
+       }
+
+       /**
+        * @return bool
+        */
+       function wasReadOnlyError() {
+               return $this->lastErrno() == 8; // SQLITE_READONLY;
+       }
+
+       /**
+        * @return string Wikitext of a link to the server software's web site
+        */
+       public function getSoftwareLink() {
+               return "[{{int:version-db-sqlite-url}} SQLite]";
+       }
+
+       /**
+        * @return string Version information from the database
+        */
+       function getServerVersion() {
+               $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
+
+               return $ver;
+       }
+
+       /**
+        * Get information about a given field
+        * Returns false if the field does not exist.
+        *
+        * @param string $table
+        * @param string $field
+        * @return SQLiteField|bool False on failure
+        */
+       function fieldInfo( $table, $field ) {
+               $tableName = $this->tableName( $table );
+               $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
+               $res = $this->query( $sql, __METHOD__ );
+               foreach ( $res as $row ) {
+                       if ( $row->name == $field ) {
+                               return new SQLiteField( $row, $tableName );
+                       }
+               }
+
+               return false;
+       }
+
+       protected function doBegin( $fname = '' ) {
+               if ( $this->trxMode ) {
+                       $this->query( "BEGIN {$this->trxMode}", $fname );
+               } else {
+                       $this->query( 'BEGIN', $fname );
+               }
+               $this->mTrxLevel = 1;
+       }
+
+       /**
+        * @param string $s
+        * @return string
+        */
+       function strencode( $s ) {
+               return substr( $this->addQuotes( $s ), 1, -1 );
+       }
+
+       /**
+        * @param string $b
+        * @return Blob
+        */
+       function encodeBlob( $b ) {
+               return new Blob( $b );
+       }
+
+       /**
+        * @param Blob|string $b
+        * @return string
+        */
+       function decodeBlob( $b ) {
+               if ( $b instanceof Blob ) {
+                       $b = $b->fetch();
+               }
+
+               return $b;
+       }
+
+       /**
+        * @param string|int|null|bool|Blob $s
+        * @return string|int
+        */
+       function addQuotes( $s ) {
+               if ( $s instanceof Blob ) {
+                       return "x'" . bin2hex( $s->fetch() ) . "'";
+               } elseif ( is_bool( $s ) ) {
+                       return (int)$s;
+               } elseif ( strpos( $s, "\0" ) !== false ) {
+                       // SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
+                       // This is a known limitation of SQLite's mprintf function which PDO
+                       // should work around, but doesn't. I have reported this to php.net as bug #63419:
+                       // https://bugs.php.net/bug.php?id=63419
+                       // There was already a similar report for SQLite3::escapeString, bug #62361:
+                       // https://bugs.php.net/bug.php?id=62361
+                       // There is an additional bug regarding sorting this data after insert
+                       // on older versions of sqlite shipped with ubuntu 12.04
+                       // https://phabricator.wikimedia.org/T74367
+                       $this->queryLogger->debug(
+                               __FUNCTION__ .
+                               ': Quoting value containing null byte. ' .
+                               'For consistency all binary data should have been ' .
+                               'first processed with self::encodeBlob()'
+                       );
+                       return "x'" . bin2hex( $s ) . "'";
+               } else {
+                       return $this->mConn->quote( $s );
+               }
+       }
+
+       /**
+        * @return string
+        */
+       function buildLike() {
+               $params = func_get_args();
+               if ( count( $params ) > 0 && is_array( $params[0] ) ) {
+                       $params = $params[0];
+               }
+
+               return parent::buildLike( $params ) . "ESCAPE '\' ";
+       }
+
+       /**
+        * @param string $field Field or column to cast
+        * @return string
+        * @since 1.28
+        */
+       public function buildStringCast( $field ) {
+               return 'CAST ( ' . $field . ' AS TEXT )';
+       }
+
+       /**
+        * No-op version of deadlockLoop
+        *
+        * @return mixed
+        */
+       public function deadlockLoop( /*...*/ ) {
+               $args = func_get_args();
+               $function = array_shift( $args );
+
+               return call_user_func_array( $function, $args );
+       }
+
+       /**
+        * @param string $s
+        * @return string
+        */
+       protected function replaceVars( $s ) {
+               $s = parent::replaceVars( $s );
+               if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
+                       // CREATE TABLE hacks to allow schema file sharing with MySQL
+
+                       // binary/varbinary column type -> blob
+                       $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
+                       // no such thing as unsigned
+                       $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
+                       // INT -> INTEGER
+                       $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
+                       // floating point types -> REAL
+                       $s = preg_replace(
+                               '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i',
+                               'REAL',
+                               $s
+                       );
+                       // varchar -> TEXT
+                       $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
+                       // TEXT normalization
+                       $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
+                       // BLOB normalization
+                       $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
+                       // BOOL -> INTEGER
+                       $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
+                       // DATETIME -> TEXT
+                       $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
+                       // No ENUM type
+                       $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
+                       // binary collation type -> nothing
+                       $s = preg_replace( '/\bbinary\b/i', '', $s );
+                       // auto_increment -> autoincrement
+                       $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
+                       // No explicit options
+                       $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
+                       // AUTOINCREMENT should immedidately follow PRIMARY KEY
+                       $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
+               } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
+                       // No truncated indexes
+                       $s = preg_replace( '/\(\d+\)/', '', $s );
+                       // No FULLTEXT
+                       $s = preg_replace( '/\bfulltext\b/i', '', $s );
+               } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
+                       // DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
+                       $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
+               } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) {
+                       // INSERT IGNORE --> INSERT OR IGNORE
+                       $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s );
+               }
+
+               return $s;
+       }
+
+       public function lock( $lockName, $method, $timeout = 5 ) {
+               if ( !is_dir( "{$this->dbDir}/locks" ) ) { // create dir as needed
+                       if ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) ) {
+                               throw new DBError( $this, "Cannot create directory \"{$this->dbDir}/locks\"." );
+                       }
+               }
+
+               return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK();
+       }
+
+       public function unlock( $lockName, $method ) {
+               return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isOK();
+       }
+
+       /**
+        * Build a concatenation list to feed into a SQL query
+        *
+        * @param string[] $stringList
+        * @return string
+        */
+       function buildConcat( $stringList ) {
+               return '(' . implode( ') || (', $stringList ) . ')';
+       }
+
+       public function buildGroupConcatField(
+               $delim, $table, $field, $conds = '', $join_conds = []
+       ) {
+               $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
+
+               return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
+       }
+
+       /**
+        * @param string $oldName
+        * @param string $newName
+        * @param bool $temporary
+        * @param string $fname
+        * @return bool|ResultWrapper
+        * @throws RuntimeException
+        */
+       function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
+               $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" .
+                       $this->addQuotes( $oldName ) . " AND type='table'", $fname );
+               $obj = $this->fetchObject( $res );
+               if ( !$obj ) {
+                       throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
+               }
+               $sql = $obj->sql;
+               $sql = preg_replace(
+                       '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/',
+                       $this->addIdentifierQuotes( $newName ),
+                       $sql,
+                       1
+               );
+               if ( $temporary ) {
+                       if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
+                               $this->queryLogger->debug(
+                                       "Table $oldName is virtual, can't create a temporary duplicate.\n" );
+                       } else {
+                               $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
+                       }
+               }
+
+               $res = $this->query( $sql, $fname );
+
+               // Take over indexes
+               $indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );
+               foreach ( $indexList as $index ) {
+                       if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
+                               continue;
+                       }
+
+                       if ( $index->unique ) {
+                               $sql = 'CREATE UNIQUE INDEX';
+                       } else {
+                               $sql = 'CREATE INDEX';
+                       }
+                       // Try to come up with a new index name, given indexes have database scope in SQLite
+                       $indexName = $newName . '_' . $index->name;
+                       $sql .= ' ' . $indexName . ' ON ' . $newName;
+
+                       $indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' );
+                       $fields = [];
+                       foreach ( $indexInfo as $indexInfoRow ) {
+                               $fields[$indexInfoRow->seqno] = $indexInfoRow->name;
+                       }
+
+                       $sql .= '(' . implode( ',', $fields ) . ')';
+
+                       $this->query( $sql );
+               }
+
+               return $res;
+       }
+
+       /**
+        * List all tables on the database
+        *
+        * @param string $prefix Only show tables with this prefix, e.g. mw_
+        * @param string $fname Calling function name
+        *
+        * @return array
+        */
+       function listTables( $prefix = null, $fname = __METHOD__ ) {
+               $result = $this->select(
+                       'sqlite_master',
+                       'name',
+                       "type='table'"
+               );
+
+               $endArray = [];
+
+               foreach ( $result as $table ) {
+                       $vars = get_object_vars( $table );
+                       $table = array_pop( $vars );
+
+                       if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
+                               if ( strpos( $table, 'sqlite_' ) !== 0 ) {
+                                       $endArray[] = $table;
+                               }
+                       }
+               }
+
+               return $endArray;
+       }
+
+       /**
+        * Override due to no CASCADE support
+        *
+        * @param string $tableName
+        * @param string $fName
+        * @return bool|ResultWrapper
+        * @throws DBReadOnlyError
+        */
+       public function dropTable( $tableName, $fName = __METHOD__ ) {
+               if ( !$this->tableExists( $tableName, $fName ) ) {
+                       return false;
+               }
+               $sql = "DROP TABLE " . $this->tableName( $tableName );
+
+               return $this->query( $sql, $fName );
+       }
+
+       protected function requiresDatabaseUser() {
+               return false; // just a file
+       }
+
+       /**
+        * @return string
+        */
+       public function __toString() {
+               return 'SQLite ' . (string)$this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
+       }
+}
index 9a0ffd5..c32a7ff 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
@@ -63,6 +62,38 @@ interface IDatabase {
        /** @var string Estimate time to apply (scanning, applying) */
        const ESTIMATE_DB_APPLY = 'apply';
 
+       /** @var int Combine list with comma delimeters */
+       const LIST_COMMA = 0;
+       /** @var int Combine list with AND clauses */
+       const LIST_AND = 1;
+       /** @var int Convert map into a SET clause */
+       const LIST_SET = 2;
+       /** @var int Treat as field name and do not apply value escaping */
+       const LIST_NAMES = 3;
+       /** @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.
@@ -73,15 +104,14 @@ interface IDatabase {
        public function getServerInfo();
 
        /**
-        * Turns buffering of SQL result sets on (true) or off (false). Default is
-        * "on".
+        * Turns buffering of SQL result sets on (true) or off (false). Default is "on".
         *
         * Unbuffered queries are very troublesome in MySQL:
         *
         *   - If another query is executed while the first query is being read
         *     out, the first query is killed. This means you can't call normal
-        *     MediaWiki functions while you are reading an unbuffered query result
-        *     from a normal wfGetDB() connection.
+        *     Database functions while you are reading an unbuffered query result
+        *     from a normal Database connection.
         *
         *   - Unbuffered queries cause the MySQL server to use large amounts of
         *     memory and to hold broad locks which block other queries.
@@ -302,6 +332,13 @@ interface IDatabase {
        /**
         * @return string
         */
+       public function getDomainID();
+
+       /**
+        * Alias for getDomainID()
+        *
+        * @return string
+        */
        public function getWikiID();
 
        /**
@@ -576,7 +613,7 @@ interface IDatabase {
         * for use in field names (e.g. a.user_name).
         *
         * All of the table names given here are automatically run through
-        * DatabaseBase::tableName(), which causes the table prefix (if any) to be
+        * Database::tableName(), which causes the table prefix (if any) to be
         * added, and various other table name mappings to be performed.
         *
         * Do not use untrusted user input as a table name. Alias names should
@@ -639,7 +676,7 @@ interface IDatabase {
         *
         *   - OFFSET: Skip this many rows at the start of the result set. OFFSET
         *     with LIMIT can theoretically be used for paging through a result set,
-        *     but this is discouraged in MediaWiki for performance reasons.
+        *     but this is discouraged for performance reasons.
         *
         *   - LIMIT: Integer: return at most this many rows. The rows are sorted
         *     and then the first rows are taken until the limit is reached. LIMIT
@@ -858,7 +895,7 @@ interface IDatabase {
         *     IDatabase::affectedRows().
         *
         * @param string $table Table name. This will be passed through
-        *   DatabaseBase::tableName().
+        *   Database::tableName().
         * @param array $a Array of rows to insert
         * @param string $fname Calling function name (use __METHOD__) for logs/profiling
         * @param array $options Array of options
@@ -871,7 +908,7 @@ interface IDatabase {
         * UPDATE wrapper. Takes a condition array and a SET array.
         *
         * @param string $table Name of the table to UPDATE. This will be passed through
-        *   DatabaseBase::tableName().
+        *   Database::tableName().
         * @param array $values An array of values to SET. For each array element,
         *   the key gives the field name, and the value gives the data to set
         *   that field to. The data will be quoted by IDatabase::addQuotes().
@@ -890,18 +927,29 @@ interface IDatabase {
        /**
         * Makes an encoded list of strings from an array
         *
+        * These can be used to make conjunctions or disjunctions on SQL condition strings
+        * derived from an array (see IDatabase::select() $conds documentation).
+        *
+        * Example usage:
+        * @code
+        *     $sql = $db->makeList( [
+        *         'rev_user' => $id,
+        *         $db->makeList( [ 'rev_minor' => 1, 'rev_len' < 500 ], $db::LIST_OR ] )
+        *     ], $db::LIST_AND );
+        * @endcode
+        * This would set $sql to "rev_user = '$id' AND (rev_minor = '1' OR rev_len < '500')"
+        *
         * @param array $a Containing the data
-        * @param int $mode Constant
-        *    - LIST_COMMA: Comma separated, no field names
-        *    - LIST_AND:   ANDed WHERE clause (without the WHERE). See the
-        *      documentation for $conds in IDatabase::select().
-        *    - LIST_OR:    ORed WHERE clause (without the WHERE)
-        *    - LIST_SET:   Comma separated with field names, like a SET clause
-        *    - LIST_NAMES: Comma separated field names
+        * @param int $mode IDatabase class constant:
+        *    - IDatabase::LIST_COMMA: Comma separated, no field names
+        *    - IDatabase::LIST_AND:   ANDed WHERE clause (without the WHERE).
+        *    - IDatabase::LIST_OR:    ORed WHERE clause (without the WHERE)
+        *    - IDatabase::LIST_SET:   Comma separated with field names, like a SET clause
+        *    - IDatabase::LIST_NAMES: Comma separated field names
         * @throws DBError
         * @return string
         */
-       public function makeList( $a, $mode = LIST_COMMA );
+       public function makeList( $a, $mode = self::LIST_COMMA );
 
        /**
         * Build a partial where clause from a 2-d array such as used for LinkBatch.
@@ -915,6 +963,16 @@ interface IDatabase {
         */
        public function makeWhereFrom2d( $data, $baseKey, $subKey );
 
+       /**
+        * Return aggregated value alias
+        *
+        * @param array $valuedata
+        * @param string $valuename
+        *
+        * @return string
+        */
+       public function aggregateValue( $valuedata, $valuename = 'value' );
+
        /**
         * @param string $field
         * @return string
@@ -963,6 +1021,13 @@ interface IDatabase {
                $delim, $table, $field, $conds = '', $join_conds = []
        );
 
+       /**
+        * @param string $field Field or column to cast
+        * @return string
+        * @since 1.28
+        */
+       public function buildStringCast( $field );
+
        /**
         * Change the current database
         *
@@ -986,8 +1051,8 @@ interface IDatabase {
        /**
         * Adds quotes and backslashes.
         *
-        * @param string|Blob $s
-        * @return string
+        * @param string|int|null|bool|Blob $s
+        * @return string|int
         */
        public function addQuotes( $s );
 
@@ -1084,7 +1149,7 @@ interface IDatabase {
         *
         * @since 1.22
         *
-        * @param string $table Table name. This will be passed through DatabaseBase::tableName().
+        * @param string $table Table name. This will be passed through Database::tableName().
         * @param array $rows A single row or list of rows to insert
         * @param array $uniqueIndexes List of single field names or field name tuples
         * @param array $set An array of values to SET. For each array element, the
@@ -1257,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
@@ -1358,10 +1423,7 @@ interface IDatabase {
         * The goal of this function is to create an atomic section of SQL queries
         * without having to start a new transaction if it already exists.
         *
-        * Atomic sections are more strict than transactions. With transactions,
-        * attempting to begin a new transaction when one is already running results
-        * in MediaWiki issuing a brief warning and doing an implicit commit. All
-        * atomic levels *must* be explicitly closed using IDatabase::endAtomic(),
+        * All atomic levels *must* be explicitly closed using IDatabase::endAtomic(),
         * and any database transactions cannot be began or committed until all atomic
         * levels are closed. There is no such thing as implicitly opening or closing
         * an atomic section.
@@ -1401,8 +1463,8 @@ interface IDatabase {
         *
         * This can be an alternative to explicit startAtomic()/endAtomic() calls.
         *
-        * @see DatabaseBase::startAtomic
-        * @see DatabaseBase::endAtomic
+        * @see Database::startAtomic
+        * @see Database::endAtomic
         *
         * @param string $fname Caller name (usually __METHOD__)
         * @param callable $callback Callback that issues DB updates
diff --git a/includes/libs/rdbms/database/IMaintainableDatabase.php b/includes/libs/rdbms/database/IMaintainableDatabase.php
new file mode 100644 (file)
index 0000000..8395359
--- /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 callable|null $lineCallback Optional function called before reading each line
+        * @param callable|null $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 callable|null $inputCallback Optional function called for each
+        *   complete line sent
+        * @return bool|string
+        * @throws Exception
+        */
+       public function sourceFile(
+               $filename,
+               callable $lineCallback = null,
+               callable $resultCallback = null,
+               $fname = false,
+               callable $inputCallback = null
+       );
+
+       /**
+        * 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 callable|null $lineCallback Optional function called before reading each query
+        * @param callable|null $resultCallback Optional function called for each MySQL result
+        * @param string $fname Calling function name
+        * @param callable|null $inputCallback Optional function called for each complete query sent
+        * @return bool|string
+        */
+       public function sourceStream(
+               $fp,
+               callable $lineCallback = null,
+               callable $resultCallback = null,
+               $fname = __METHOD__,
+               callable $inputCallback = null
+       );
+
+       /**
+        * 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 774def8..1a046cf 100644 (file)
@@ -4,35 +4,19 @@
  * doesn't go anywhere near an actual database.
  */
 class FakeResultWrapper extends ResultWrapper {
-       /** @var array */
-       public $result = [];
-
-       /** @var null And it's going to stay that way :D */
-       protected $db = null;
-
-       /** @var int */
-       protected $pos = 0;
-
-       /** @var array|stdClass|bool */
-       protected $currentRow = null;
+       /** @var $result stdClass[] */
 
        /**
-        * @param array $array
+        * @param stdClass[] $rows
         */
-       function __construct( $array ) {
-               $this->result = $array;
+       function __construct( array $rows ) {
+               parent::__construct( null, $rows );
        }
 
-       /**
-        * @return int
-        */
        function numRows() {
                return count( $this->result );
        }
 
-       /**
-        * @return array|bool
-        */
        function fetchRow() {
                if ( $this->pos < count( $this->result ) ) {
                        $this->currentRow = $this->result[$this->pos];
@@ -54,10 +38,6 @@ class FakeResultWrapper extends ResultWrapper {
        function free() {
        }
 
-       /**
-        * Callers want to be able to access fields with $this->fieldName
-        * @return bool|stdClass
-        */
        function fetchObject() {
                $this->fetchRow();
                if ( $this->currentRow ) {
@@ -72,9 +52,6 @@ class FakeResultWrapper extends ResultWrapper {
                $this->currentRow = null;
        }
 
-       /**
-        * @return bool|stdClass
-        */
        function next() {
                return $this->fetchObject();
        }
index cccb8f1..b591f4f 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 class MssqlResultWrapper extends ResultWrapper {
+       /** @var integer|null */
        private $mSeekTo = null;
 
        /**
@@ -16,7 +17,7 @@ class MssqlResultWrapper extends ResultWrapper {
                        $result = sqlsrv_fetch_object( $res );
                }
 
-               // MediaWiki expects us to return boolean false when there are no more rows instead of null
+               // Return boolean false when there are no more rows instead of null
                if ( $result === null ) {
                        return false;
                }
@@ -38,7 +39,7 @@ class MssqlResultWrapper extends ResultWrapper {
                        $result = sqlsrv_fetch_array( $res );
                }
 
-               // MediaWiki expects us to return boolean false when there are no more rows instead of null
+               // Return boolean false when there are no more rows instead of null
                if ( $result === null ) {
                        return false;
                }
index 252f4f7..53109c8 100644 (file)
@@ -1,30 +1,43 @@
 <?php
 /**
- * Result wrapper for grabbing data queried by someone else
+ * Result wrapper for grabbing data queried from an IDatabase object
+ *
+ * Note that using the Iterator methods in combination with the non-Iterator
+ * DB result iteration functions may cause rows to be skipped or repeated.
+ *
+ * By default, this will use the iteration methods of the IDatabase handle if provided.
+ * Subclasses can override methods to make it solely work on the result resource instead.
+ * If no database is provided, and the subclass does not override the DB iteration methods,
+ * then a RuntimeException will be thrown when iteration is attempted.
+ *
+ * The result resource field should not be accessed from non-Database related classes.
+ * It is database class specific and is stored here to associate iterators with queries.
+ *
  * @ingroup Database
  */
 class ResultWrapper implements Iterator {
-       /** @var resource */
+       /** @var resource|array|null Optional underlying result handle for subclass usage */
        public $result;
 
-       /** @var DatabaseBase */
+       /** @var IDatabase|null */
        protected $db;
 
        /** @var int */
        protected $pos = 0;
-
-       /** @var object|null */
+       /** @var stdClass|null */
        protected $currentRow = null;
 
        /**
-        * Create a new result object from a result resource and a Database object
+        * Create a row iterator from a result resource and an optional Database object
+        *
+        * Only Database-related classes should construct ResultWrapper. Other code may
+        * use the FakeResultWrapper subclass for convenience or compatibility shims, however.
         *
-        * @param DatabaseBase $database
-        * @param resource|ResultWrapper $result
+        * @param IDatabase|null $db Optional database handle
+        * @param ResultWrapper|array|resource $result Optional underlying result handle
         */
-       function __construct( $database, $result ) {
-               $this->db = $database;
-
+       public function __construct( IDatabase $db = null, $result ) {
+               $this->db = $db;
                if ( $result instanceof ResultWrapper ) {
                        $this->result = $result->result;
                } else {
@@ -37,8 +50,8 @@ class ResultWrapper implements Iterator {
         *
         * @return int
         */
-       function numRows() {
-               return $this->db->numRows( $this );
+       public function numRows() {
+               return $this->getDB()->numRows( $this );
        }
 
        /**
@@ -49,8 +62,8 @@ class ResultWrapper implements Iterator {
         * @return stdClass|bool
         * @throws DBUnexpectedError Thrown if the database returns an error
         */
-       function fetchObject() {
-               return $this->db->fetchObject( $this );
+       public function fetchObject() {
+               return $this->getDB()->fetchObject( $this );
        }
 
        /**
@@ -60,38 +73,49 @@ class ResultWrapper implements Iterator {
         * @return array|bool
         * @throws DBUnexpectedError Thrown if the database returns an error
         */
-       function fetchRow() {
-               return $this->db->fetchRow( $this );
+       public function fetchRow() {
+               return $this->getDB()->fetchRow( $this );
        }
 
        /**
-        * Free a result object
+        * Change the position of the cursor in a result object.
+        * See mysql_data_seek()
+        *
+        * @param int $row
         */
-       function free() {
-               $this->db->freeResult( $this );
-               unset( $this->result );
-               unset( $this->db );
+       public function seek( $row ) {
+               $this->getDB()->dataSeek( $this, $row );
        }
 
        /**
-        * Change the position of the cursor in a result object.
-        * See mysql_data_seek()
+        * Free a result object
         *
-        * @param int $row
+        * This either saves memory in PHP (buffered queries) or on the server (unbuffered queries).
+        * In general, queries are not large enough in result sets for this to be worth calling.
         */
-       function seek( $row ) {
-               $this->db->dataSeek( $this, $row );
+       public function free() {
+               if ( $this->db ) {
+                       $this->db->freeResult( $this );
+                       $this->db = null;
+               }
+               $this->result = null;
        }
 
-       /*
-        * ======= Iterator functions =======
-        * Note that using these in combination with the non-iterator functions
-        * above may cause rows to be skipped or repeated.
+       /**
+        * @return IDatabase
+        * @throws RuntimeException
         */
+       private function getDB() {
+               if ( !$this->db ) {
+                       throw new RuntimeException( get_class( $this ) . ' needs a DB handle for iteration.' );
+               }
+
+               return $this->db;
+       }
 
        function rewind() {
                if ( $this->numRows() ) {
-                       $this->db->dataSeek( $this, 0 );
+                       $this->getDB()->dataSeek( $this, 0 );
                }
                $this->pos = 0;
                $this->currentRow = null;
@@ -125,9 +149,6 @@ class ResultWrapper implements Iterator {
                return $this->currentRow;
        }
 
-       /**
-        * @return bool
-        */
        function valid() {
                return $this->current() !== false;
        }
diff --git a/includes/libs/rdbms/database/utils/SavepointPostgres.php b/includes/libs/rdbms/database/utils/SavepointPostgres.php
new file mode 100644 (file)
index 0000000..ec4d09f
--- /dev/null
@@ -0,0 +1,101 @@
+<?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 Database
+ */
+use Psr\Log\LoggerInterface;
+
+/**
+ * Manage savepoints within a transaction
+ * @ingroup Database
+ * @since 1.19
+ */
+class SavepointPostgres {
+       /** @var DatabasePostgres Establish a savepoint within a transaction */
+       protected $dbw;
+       /** @var LoggerInterface */
+       protected $logger;
+       /** @var int */
+       protected $id;
+       /** @var bool */
+       protected $didbegin;
+
+       /**
+        * @param DatabasePostgres $dbw
+        * @param int $id
+        * @param LoggerInterface $logger
+        */
+       public function __construct( DatabasePostgres $dbw, $id, LoggerInterface $logger ) {
+               $this->dbw = $dbw;
+               $this->logger = $logger;
+               $this->id = $id;
+               $this->didbegin = false;
+               /* If we are not in a transaction, we need to be for savepoint trickery */
+               if ( !$dbw->trxLevel() ) {
+                       $dbw->begin( "FOR SAVEPOINT", DatabasePostgres::TRANSACTION_INTERNAL );
+                       $this->didbegin = true;
+               }
+       }
+
+       public function __destruct() {
+               if ( $this->didbegin ) {
+                       $this->dbw->rollback();
+                       $this->didbegin = false;
+               }
+       }
+
+       public function commit() {
+               if ( $this->didbegin ) {
+                       $this->dbw->commit();
+                       $this->didbegin = false;
+               }
+       }
+
+       protected function query( $keyword, $msg_ok, $msg_failed ) {
+               if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) {
+                       $this->logger->debug( sprintf( $msg_ok, $this->id ) );
+               } else {
+                       $this->logger->debug( sprintf( $msg_failed, $this->id ) );
+               }
+       }
+
+       public function savepoint() {
+               $this->query( "SAVEPOINT",
+                       "Transaction state: savepoint \"%s\" established.\n",
+                       "Transaction state: establishment of savepoint \"%s\" FAILED.\n"
+               );
+       }
+
+       public function release() {
+               $this->query( "RELEASE",
+                       "Transaction state: savepoint \"%s\" released.\n",
+                       "Transaction state: release of savepoint \"%s\" FAILED.\n"
+               );
+       }
+
+       public function rollback() {
+               $this->query( "ROLLBACK TO",
+                       "Transaction state: savepoint \"%s\" rolled back.\n",
+                       "Transaction state: rollback of savepoint \"%s\" FAILED.\n"
+               );
+       }
+
+       public function __toString() {
+               return (string)$this->id;
+       }
+}
index 48baa3c..692a704 100644 (file)
@@ -3,33 +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)
-/**@}*/
-
-/**@{
- * Flags for IDatabase::makeList()
- * These are also available as Database class constants
- */
-define( 'LIST_COMMA', 0 );
-define( 'LIST_AND', 1 );
-define( 'LIST_SET', 2 );
-define( 'LIST_NAMES', 3 );
-define( 'LIST_OR', 4 );
+define( 'DB_REPLICA', ILoadBalancer::DB_REPLICA );
+define( 'DB_MASTER', ILoadBalancer::DB_MASTER );
 /**@}*/
index 5dee884..b0b3c87 100644 (file)
@@ -1,8 +1,8 @@
 <?php
 /**
- * Used by DatabaseBase::buildLike() to represent characters that have special
+ * Used by Database::buildLike() to represent characters that have special
  * meaning in SQL LIKE clauses and thus need no escaping. Don't instantiate it
- * manually, use DatabaseBase::anyChar() and anyString() instead.
+ * manually, use Database::anyChar() and anyString() instead.
  */
 class LikeMatch {
        /** @var string */
diff --git a/includes/libs/rdbms/exception/DBAccessError.php b/includes/libs/rdbms/exception/DBAccessError.php
new file mode 100644 (file)
index 0000000..c00082c
--- /dev/null
@@ -0,0 +1,30 @@
+<?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 Database
+ */
+
+/**
+ * Exception class for attempted DB access
+ * @ingroup Database
+ */
+class DBAccessError extends DBUnexpectedError {
+       public function __construct() {
+               parent::__construct( "Database access has been disabled." );
+       }
+}
diff --git a/includes/libs/rdbms/exception/DBConnectionError.php b/includes/libs/rdbms/exception/DBConnectionError.php
new file mode 100644 (file)
index 0000000..47f8c96
--- /dev/null
@@ -0,0 +1,38 @@
+<?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 Database
+ */
+
+/**
+ * @ingroup Database
+ */
+class DBConnectionError extends DBExpectedError {
+       /**
+        * @param IDatabase $db Object throwing the error
+        * @param string $error Error text
+        */
+       function __construct( IDatabase $db = null, $error = 'unknown error' ) {
+               $msg = 'Cannot access the database';
+               if ( trim( $error ) != '' ) {
+                       $msg .= ": $error";
+               }
+
+               parent::__construct( $db, $msg );
+       }
+}
index 38887cf..526596d 100644 (file)
@@ -1,7 +1,5 @@
 <?php
 /**
- * This file contains database error classes.
- *
  * 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
@@ -39,135 +37,3 @@ class DBError extends Exception {
                parent::__construct( $error );
        }
 }
-
-/**
- * Base class for the more common types of database errors. These are known to occur
- * frequently, so we try to give friendly error messages for them.
- *
- * @ingroup Database
- * @since 1.23
- */
-class DBExpectedError extends DBError implements MessageSpecifier {
-       /** @var string[] Message parameters */
-       protected $params;
-
-       function __construct( IDatabase $db = null, $error, array $params = [] ) {
-               parent::__construct( $db, $error );
-               $this->params = $params;
-       }
-
-       public function getKey() {
-               return 'databaseerror-text';
-       }
-
-       public function getParams() {
-               return $this->params;
-       }
-}
-
-/**
- * @ingroup Database
- */
-class DBConnectionError extends DBExpectedError {
-       /**
-        * @param IDatabase $db Object throwing the error
-        * @param string $error Error text
-        */
-       function __construct( IDatabase $db = null, $error = 'unknown error' ) {
-               $msg = 'Cannot access the database';
-               if ( trim( $error ) != '' ) {
-                       $msg .= ": $error";
-               }
-
-               parent::__construct( $db, $msg );
-       }
-}
-
-/**
- * @ingroup Database
- */
-class DBQueryError extends DBExpectedError {
-       /** @var string */
-       public $error;
-       /** @var integer */
-       public $errno;
-       /** @var string */
-       public $sql;
-       /** @var string */
-       public $fname;
-
-       /**
-        * @param IDatabase $db
-        * @param string $error
-        * @param int|string $errno
-        * @param string $sql
-        * @param string $fname
-        */
-       function __construct( IDatabase $db, $error, $errno, $sql, $fname ) {
-               if ( $db instanceof DatabaseBase && $db->wasConnectionError( $errno ) ) {
-                       $message = "A connection error occured. \n" .
-                               "Query: $sql\n" .
-                               "Function: $fname\n" .
-                               "Error: $errno $error\n";
-               } else {
-                       $message = "A database error has occurred. Did you forget to run " .
-                               "maintenance/update.php after upgrading?  See: " .
-                               "https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
-                               "Query: $sql\n" .
-                               "Function: $fname\n" .
-                               "Error: $errno $error\n";
-               }
-               parent::__construct( $db, $message );
-
-               $this->error = $error;
-               $this->errno = $errno;
-               $this->sql = $sql;
-               $this->fname = $fname;
-       }
-}
-
-/**
- * @ingroup Database
- */
-class DBReadOnlyError extends DBExpectedError {
-}
-
-/**
- * @ingroup Database
- */
-class DBTransactionError extends DBExpectedError {
-}
-
-/**
- * @ingroup Database
- */
-class DBTransactionSizeError extends DBTransactionError {
-       function getKey() {
-               return 'transaction-duration-limit-exceeded';
-       }
-}
-
-/**
- * Exception class for replica DB wait timeouts
- * @ingroup Database
- */
-class DBReplicationWaitError extends DBExpectedError {
-}
-
-/**
- * @ingroup Database
- */
-class DBUnexpectedError extends DBError {
-}
-
-/**
- * Exception class for attempted DB access
- * @ingroup Database
- */
-class DBAccessError extends DBUnexpectedError {
-       public function __construct() {
-               parent::__construct( "Mediawiki tried to access the database via wfGetDB(). " .
-                       "This is not allowed, because database access has been disabled." );
-       }
-}
-
diff --git a/includes/libs/rdbms/exception/DBExpectedError.php b/includes/libs/rdbms/exception/DBExpectedError.php
new file mode 100644 (file)
index 0000000..9e10884
--- /dev/null
@@ -0,0 +1,45 @@
+<?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 Database
+ */
+
+/**
+ * Base class for the more common types of database errors. These are known to occur
+ * frequently, so we try to give friendly error messages for them.
+ *
+ * @ingroup Database
+ * @since 1.23
+ */
+class DBExpectedError extends DBError implements MessageSpecifier {
+       /** @var string[] Message parameters */
+       protected $params;
+
+       function __construct( IDatabase $db = null, $error, array $params = [] ) {
+               parent::__construct( $db, $error );
+               $this->params = $params;
+       }
+
+       public function getKey() {
+               return 'databaseerror-text';
+       }
+
+       public function getParams() {
+               return $this->params;
+       }
+}
diff --git a/includes/libs/rdbms/exception/DBQueryError.php b/includes/libs/rdbms/exception/DBQueryError.php
new file mode 100644 (file)
index 0000000..002d253
--- /dev/null
@@ -0,0 +1,63 @@
+<?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 Database
+ */
+
+/**
+ * @ingroup Database
+ */
+class DBQueryError extends DBExpectedError {
+       /** @var string */
+       public $error;
+       /** @var integer */
+       public $errno;
+       /** @var string */
+       public $sql;
+       /** @var string */
+       public $fname;
+
+       /**
+        * @param IDatabase $db
+        * @param string $error
+        * @param int|string $errno
+        * @param string $sql
+        * @param string $fname
+        */
+       function __construct( IDatabase $db, $error, $errno, $sql, $fname ) {
+               if ( $db instanceof Database && $db->wasConnectionError( $errno ) ) {
+                       $message = "A connection error occured. \n" .
+                               "Query: $sql\n" .
+                               "Function: $fname\n" .
+                               "Error: $errno $error\n";
+               } else {
+                       $message = "A database query error has occurred. Did you forget to run " .
+                               "your application's database schema updater after upgrading? \n" .
+                               "Query: $sql\n" .
+                               "Function: $fname\n" .
+                               "Error: $errno $error\n";
+               }
+
+               parent::__construct( $db, $message );
+
+               $this->error = $error;
+               $this->errno = $errno;
+               $this->sql = $sql;
+               $this->fname = $fname;
+       }
+}
diff --git a/includes/libs/rdbms/exception/DBReadOnlyError.php b/includes/libs/rdbms/exception/DBReadOnlyError.php
new file mode 100644 (file)
index 0000000..d4dce1e
--- /dev/null
@@ -0,0 +1,26 @@
+<?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 Database
+ */
+
+/**
+ * @ingroup Database
+ */
+class DBReadOnlyError extends DBExpectedError {
+}
diff --git a/includes/libs/rdbms/exception/DBReplicationWaitError.php b/includes/libs/rdbms/exception/DBReplicationWaitError.php
new file mode 100644 (file)
index 0000000..f1dabd5
--- /dev/null
@@ -0,0 +1,28 @@
+<?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 Database
+ */
+
+/**
+ * Exception class for replica DB wait timeouts
+ * @ingroup Database
+ */
+class DBReplicationWaitError extends DBExpectedError {
+}
+
diff --git a/includes/libs/rdbms/exception/DBTransactionError.php b/includes/libs/rdbms/exception/DBTransactionError.php
new file mode 100644 (file)
index 0000000..a488667
--- /dev/null
@@ -0,0 +1,26 @@
+<?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 Database
+ */
+
+/**
+ * @ingroup Database
+ */
+class DBTransactionError extends DBExpectedError {
+}
diff --git a/includes/libs/rdbms/exception/DBTransactionSizeError.php b/includes/libs/rdbms/exception/DBTransactionSizeError.php
new file mode 100644 (file)
index 0000000..4e467b2
--- /dev/null
@@ -0,0 +1,29 @@
+<?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 Database
+ */
+
+/**
+ * @ingroup Database
+ */
+class DBTransactionSizeError extends DBTransactionError {
+       function getKey() {
+               return 'transaction-duration-limit-exceeded';
+       }
+}
diff --git a/includes/libs/rdbms/exception/DBUnexpectedError.php b/includes/libs/rdbms/exception/DBUnexpectedError.php
new file mode 100644 (file)
index 0000000..5a12671
--- /dev/null
@@ -0,0 +1,26 @@
+<?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 Database
+ */
+
+/**
+ * @ingroup Database
+ */
+class DBUnexpectedError extends DBError {
+}
diff --git a/includes/libs/rdbms/field/PostgresField.php b/includes/libs/rdbms/field/PostgresField.php
new file mode 100644 (file)
index 0000000..36337e2
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+class PostgresField implements Field {
+       private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname,
+               $has_default, $default;
+
+       /**
+        * @param DatabasePostgres $db
+        * @param string $table
+        * @param string $field
+        * @return null|PostgresField
+        */
+       static function fromText( $db, $table, $field ) {
+               $q = <<<SQL
+SELECT
+ attnotnull, attlen, conname AS conname,
+ atthasdef,
+ adsrc,
+ COALESCE(condeferred, 'f') AS deferred,
+ COALESCE(condeferrable, 'f') AS deferrable,
+ CASE WHEN typname = 'int2' THEN 'smallint'
+  WHEN typname = 'int4' THEN 'integer'
+  WHEN typname = 'int8' THEN 'bigint'
+  WHEN typname = 'bpchar' THEN 'char'
+ ELSE typname END AS typname
+FROM pg_class c
+JOIN pg_namespace n ON (n.oid = c.relnamespace)
+JOIN pg_attribute a ON (a.attrelid = c.oid)
+JOIN pg_type t ON (t.oid = a.atttypid)
+LEFT JOIN pg_constraint o ON (o.conrelid = c.oid AND a.attnum = ANY(o.conkey) AND o.contype = 'f')
+LEFT JOIN pg_attrdef d on c.oid=d.adrelid and a.attnum=d.adnum
+WHERE relkind = 'r'
+AND nspname=%s
+AND relname=%s
+AND attname=%s;
+SQL;
+
+               $table = $db->tableName( $table, 'raw' );
+               $res = $db->query(
+                       sprintf( $q,
+                               $db->addQuotes( $db->getCoreSchema() ),
+                               $db->addQuotes( $table ),
+                               $db->addQuotes( $field )
+                       )
+               );
+               $row = $db->fetchObject( $res );
+               if ( !$row ) {
+                       return null;
+               }
+               $n = new PostgresField;
+               $n->type = $row->typname;
+               $n->nullable = ( $row->attnotnull == 'f' );
+               $n->name = $field;
+               $n->tablename = $table;
+               $n->max_length = $row->attlen;
+               $n->deferrable = ( $row->deferrable == 't' );
+               $n->deferred = ( $row->deferred == 't' );
+               $n->conname = $row->conname;
+               $n->has_default = ( $row->atthasdef === 't' );
+               $n->default = $row->adsrc;
+
+               return $n;
+       }
+
+       function name() {
+               return $this->name;
+       }
+
+       function tableName() {
+               return $this->tablename;
+       }
+
+       function type() {
+               return $this->type;
+       }
+
+       function isNullable() {
+               return $this->nullable;
+       }
+
+       function maxLength() {
+               return $this->max_length;
+       }
+
+       function is_deferrable() {
+               return $this->deferrable;
+       }
+
+       function is_deferred() {
+               return $this->deferred;
+       }
+
+       function conname() {
+               return $this->conname;
+       }
+
+       /**
+        * @since 1.19
+        * @return bool|mixed
+        */
+       function defaultValue() {
+               if ( $this->has_default ) {
+                       return $this->default;
+               } else {
+                       return false;
+               }
+       }
+}
diff --git a/includes/libs/rdbms/lbfactory/ILBFactory.php b/includes/libs/rdbms/lbfactory/ILBFactory.php
new file mode 100644 (file)
index 0000000..9c9f18d
--- /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 open transactions on all connections. This is useful for two main cases:
+        *   - a) To commit changes to the masters.
+        *   - b) To release the snapshot on all connections, master and replica DBs.
+        * @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 107a7e2..929bd4d 100644 (file)
@@ -27,9 +27,11 @@ 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 */
+       protected $profiler;
        /** @var TransactionProfiler */
        protected $trxProfiler;
        /** @var LoggerInterface */
@@ -49,10 +51,13 @@ abstract class LBFactory {
        /** @var WANObjectCache */
        protected $wanCache;
 
-       /** @var string Local domain */
-       protected $domain;
+       /** @var DatabaseDomain Local domain */
+       protected $localDomain;
        /** @var string Local hostname of the app server */
        protected $hostname;
+       /** @var array Web request information about the client */
+       protected $requestInfo;
+
        /** @var mixed */
        protected $ticket;
        /** @var string|bool String if a requested DBO_TRX transaction round is active */
@@ -67,19 +72,13 @@ 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' ];
 
-       /**
-        * @TODO: document base params here
-        * @param array $conf
-        */
        public function __construct( array $conf ) {
-               $this->domain = isset( $conf['domain'] ) ? $conf['domain'] : '';
+               $this->localDomain = isset( $conf['localDomain'] )
+                       ? DatabaseDomain::newFromId( $conf['localDomain'] )
+                       : DatabaseDomain::newUnspecified();
 
                if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
                        $this->readOnlyReason = $conf['readOnlyReason'];
@@ -99,96 +98,73 @@ abstract class LBFactory {
                        : function ( Exception $e ) {
                                trigger_error( E_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
                        };
-               $this->hostname = isset( $conf['hostname'] )
-                       ? $conf['hostname']
-                       : gethostname();
 
-               $this->chronProt = isset( $conf['chronProt'] )
-                       ? $conf['chronProt']
-                       : $this->newChronologyProtector();
+               $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
                $this->trxProfiler = isset( $conf['trxProfiler'] )
                        ? $conf['trxProfiler']
                        : new TransactionProfiler();
 
-               $this->ticket = mt_rand();
+               $this->requestInfo = [
+                       'IPAddress' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
+                       'UserAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
+                       'ChronologyProtection' => 'true'
+               ];
+
                $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
+               $this->hostname = isset( $conf['hostname'] ) ? $conf['hostname'] : gethostname();
                $this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
+
+               $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 LoadBalancer::disable()
-        */
        public function destroy() {
                $this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
                $this->forEachLBCallMethod( 'disable' );
        }
 
+       public function shutdown(
+               $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
+       ) {
+               $chronProt = $this->getChronologyProtector();
+               if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
+                       $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
+               } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
+                       $this->shutdownChronologyProtector( $chronProt, null, 'async' );
+               }
+
+               $this->commitMasterChanges( __METHOD__ ); // sanity
+       }
+
        /**
-        * 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 Wiki ID, or false for the current wiki
-        * @return ILoadBalancer
+        * @see ILBFactory::newMainLB()
+        * @param bool $domain
+        * @return LoadBalancer
         */
        abstract public function newMainLB( $domain = false );
 
        /**
-        * Get a cached (tracked) load balancer object.
-        *
-        * @param bool|string $domain Wiki ID, or false for the current wiki
-        * @return ILoadBalancer
+        * @see ILBFactory::getMainLB()
+        * @param bool $domain
+        * @return LoadBalancer
         */
        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 Wiki ID, or false for the current wiki
-        * @return ILoadBalancer
+        * @see ILBFactory::newExternalLB()
+        * @param string $cluster
+        * @param bool $domain
+        * @return LoadBalancer
         */
-       abstract protected function newExternalLB( $cluster, $domain = false );
+       abstract 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 Wiki ID, or false for the current wiki
-        * @return ILoadBalancer
+        * @see ILBFactory::getExternalLB()
+        * @param string $cluster
+        * @param bool $domain
+        * @return LoadBalancer
         */
        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
-       ) {
-               if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
-                       $this->shutdownChronologyProtector( $this->chronProt, $workCallback, 'sync' );
-               } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
-                       $this->shutdownChronologyProtector( $this->chronProt, null, 'async' );
-               }
-
-               $this->commitMasterChanges( __METHOD__ ); // sanity
-       }
-
        /**
         * Call a method of each tracked load balancer
         *
@@ -204,43 +180,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(
@@ -253,13 +201,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(
@@ -267,6 +208,8 @@ abstract class LBFactory {
                                "$fname: transaction round '{$this->trxRoundId}' still running."
                        );
                }
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $scope = $this->getScopedPHPBehaviorForCommit(); // try to ignore client aborts
                // Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure
                $this->forEachLBCallMethod( 'finalizeMasterChanges' );
                $this->trxRoundId = false;
@@ -291,11 +234,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' );
@@ -329,11 +267,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 ) {
@@ -343,11 +276,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 ) {
@@ -357,12 +285,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 ) {
@@ -371,45 +293,25 @@ 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 wiki, use the 'wiki' 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:
-        *   - wiki : wait on the load balancer DBs that handles the given wiki
-        *   - 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 += [
-                       'wiki' => false,
+                       'domain' => false,
                        'cluster' => false,
                        'timeout' => 60,
                        'ifWritesSince' => null
                ];
 
+               if ( $opts['domain'] === false && isset( $opts['wiki'] ) ) {
+                       $opts['domain'] = $opts['wiki']; // b/c
+               }
+
                // Figure out which clusters need to be checked
                /** @var ILoadBalancer[] $lbs */
                $lbs = [];
                if ( $opts['cluster'] !== false ) {
                        $lbs[] = $this->getExternalLB( $opts['cluster'] );
-               } elseif ( $opts['wiki'] !== false ) {
-                       $lbs[] = $this->getMainLB( $opts['wiki'] );
+               } elseif ( $opts['domain'] !== false ) {
+                       $lbs[] = $this->getMainLB( $opts['domain'] );
                } else {
                        $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$lbs ) {
                                $lbs[] = $lb;
@@ -445,8 +347,7 @@ abstract class LBFactory {
                $failed = [];
                foreach ( $lbs as $i => $lb ) {
                        if ( $masterPositions[$i] ) {
-                               // The DBMS may not support getMasterPos() or the whole
-                               // load balancer might be fake (e.g. $wgAllDBsAreLocalhost).
+                               // The DBMS may not support getMasterPos()
                                if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) {
                                        $failed[] = $lb->getServerName( $lb->getWriterIndex() );
                                }
@@ -461,15 +362,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;
@@ -478,36 +370,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;
                }
 
@@ -529,44 +407,44 @@ 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->chronProt->getTouched( $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->chronProt->setEnabled( false );
+               $this->getChronologyProtector()->setEnabled( false );
        }
 
        /**
         * @return ChronologyProtector
         */
-       protected function newChronologyProtector() {
-               $chronProt = new ChronologyProtector(
+       protected function getChronologyProtector() {
+               if ( $this->chronProt ) {
+                       return $this->chronProt;
+               }
+
+               $this->chronProt = new ChronologyProtector(
                        $this->memCache,
                        [
-                               'ip' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
-                               'agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : ''
+                               'ip' => $this->requestInfo['IPAddress'],
+                               'agent' => $this->requestInfo['UserAgent'],
                        ],
                        isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
                );
-               $chronProt->setLogger( $this->replLogger );
+               $this->chronProt->setLogger( $this->replLogger );
+
                if ( $this->cliMode ) {
-                       $chronProt->setEnabled( false );
+                       $this->chronProt->setEnabled( false );
+               } elseif ( $this->requestInfo['ChronologyProtection'] === 'false' ) {
+                       // Request opted out of using position wait logic. This is useful for requests
+                       // done by the job queue or background ETL that do not have a meaningful session.
+                       $this->chronProt->setWaitEnabled( false );
                }
 
-               return $chronProt;
+               $this->replLogger->debug( __METHOD__ . ': using request info ' .
+                       json_encode( $this->requestInfo, JSON_PRETTY_PRINT ) );
+
+               return $this->chronProt;
        }
 
        /**
@@ -608,10 +486,11 @@ abstract class LBFactory {
         */
        final protected function baseLoadBalancerParams() {
                return [
-                       'localDomain' => $this->domain,
+                       'localDomain' => $this->localDomain,
                        'readOnlyReason' => $this->readOnlyReason,
                        'srvCache' => $this->srvCache,
                        'wanCache' => $this->wanCache,
+                       'profiler' => $this->profiler,
                        'trxProfiler' => $this->trxProfiler,
                        'queryLogger' => $this->queryLogger,
                        'connLogger' => $this->connLogger,
@@ -632,31 +511,61 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * Define a new local domain (for testing)
-        *
-        * Caller should make sure no local connection are open to the old local domain
-        *
-        * @param string $domain
-        * @since 1.28
-        */
-       public function setDomainPrefix( $domain ) {
-               $this->domain = $domain;
+       public function setDomainPrefix( $prefix ) {
+               $this->localDomain = new DatabaseDomain(
+                       $this->localDomain->getDatabase(),
+                       null,
+                       $prefix
+               );
+
+               $this->forEachLB( function( ILoadBalancer $lb ) use ( $prefix ) {
+                       $lb->setDomainPrefix( $prefix );
+               } );
        }
 
-       /**
-        * 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;
        }
+
+       public function appendPreShutdownTimeAsQuery( $url, $time ) {
+               $usedCluster = 0;
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
+                       $usedCluster |= ( $lb->getServerCount() > 1 );
+               } );
+
+               if ( !$usedCluster ) {
+                       return $url; // no master/replica clusters touched
+               }
+
+               return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
+       }
+
+       public function setRequestInfo( array $info ) {
+               $this->requestInfo = $info + $this->requestInfo;
+       }
+
+       /**
+        * Make PHP ignore user aborts/disconnects until the returned
+        * value leaves scope. This returns null and does nothing in CLI mode.
+        *
+        * @return ScopedCallback|null
+        */
+       final protected function getScopedPHPBehaviorForCommit() {
+               if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
+                       $old = ignore_user_abort( true ); // avoid half-finished operations
+                       return new ScopedCallback( function () use ( $old ) {
+                               ignore_user_abort( $old );
+                       } );
+               }
+
+               return null;
+       }
+
+       function __destruct() {
+               $this->destroy();
+       }
 }
diff --git a/includes/libs/rdbms/lbfactory/LBFactoryMulti.php b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php
new file mode 100644 (file)
index 0000000..83ca650
--- /dev/null
@@ -0,0 +1,413 @@
+<?php
+/**
+ * Advanced generator of database load balancing objects for database farms.
+ *
+ * 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
+ */
+
+/**
+ * A multi-database, multi-master factory for Wikimedia and similar installations.
+ * Ignores the old configuration globals.
+ *
+ * @ingroup Database
+ */
+class LBFactoryMulti extends LBFactory {
+       /** @var array A map of database names to section names */
+       private $sectionsByDB;
+
+       /**
+        * @var array A 2-d map. For each section, gives a map of server names to
+        * load ratios
+        */
+       private $sectionLoads;
+
+       /**
+        * @var array[] Server info associative array
+        * @note The host, hostName and load entries will be overridden
+        */
+       private $serverTemplate;
+
+       // Optional settings
+
+       /** @var array A 3-d map giving server load ratios for each section and group */
+       private $groupLoadsBySection = [];
+
+       /** @var array A 3-d map giving server load ratios by DB name */
+       private $groupLoadsByDB = [];
+
+       /** @var array A map of hostname to IP address */
+       private $hostsByName = [];
+
+       /** @var array A map of external storage cluster name to server load map */
+       private $externalLoads = [];
+
+       /**
+        * @var array A set of server info keys overriding serverTemplate for
+        * external storage
+        */
+       private $externalTemplateOverrides;
+
+       /**
+        * @var array A 2-d map overriding serverTemplate and
+        * externalTemplateOverrides on a server-by-server basis. Applies to both
+        * core and external storage
+        */
+       private $templateOverridesByServer;
+
+       /** @var array A 2-d map overriding the server info by section */
+       private $templateOverridesBySection;
+
+       /** @var array A 2-d map overriding the server info by external storage cluster */
+       private $templateOverridesByCluster;
+
+       /** @var array An override array for all master servers */
+       private $masterTemplateOverrides;
+
+       /**
+        * @var array|bool A map of section name to read-only message. Missing or
+        * false for read/write
+        */
+       private $readOnlyBySection = [];
+
+       /** @var array Load balancer factory configuration */
+       private $conf;
+
+       /** @var LoadBalancer[] */
+       private $mainLBs = [];
+
+       /** @var LoadBalancer[] */
+       private $extLBs = [];
+
+       /** @var string */
+       private $loadMonitorClass = 'LoadMonitor';
+
+       /** @var string */
+       private $lastDomain;
+
+       /** @var string */
+       private $lastSection;
+
+       /**
+        * @see LBFactory::__construct()
+        *
+        * Template override precedence (highest => lowest):
+        *   - templateOverridesByServer
+        *   - masterTemplateOverrides
+        *   - templateOverridesBySection/templateOverridesByCluster
+        *   - externalTemplateOverrides
+        *   - serverTemplate
+        * Overrides only work on top level keys (so nested values will not be merged).
+        *
+        * Server configuration maps should be of the format Database::factory() requires.
+        * Additionally, a 'max lag' key should also be set on server maps, indicating how stale the
+        * data can be before the load balancer tries to avoid using it. The map can have 'is static'
+        * set to disable blocking  replication sync checks (intended for archive servers with
+        * unchanging data).
+        *
+        * @param array $conf Parameters of LBFactory::__construct() as well as:
+        *   - sectionsByDB                Map of database names to section names.
+        *   - sectionLoads                2-d map. For each section, gives a map of server names to
+        *                                 load ratios. For example:
+        *                                 [
+        *                                     'section1' => [
+        *                                         'db1' => 100,
+        *                                         'db2' => 100
+        *                                     ]
+        *                                 ]
+        *   - serverTemplate              Server configuration map intended for Database::factory().
+        *                                 Note that "host", "hostName" and "load" entries will be
+        *                                 overridden by "sectionLoads" and "hostsByName".
+        *   - groupLoadsBySection         3-d map giving server load ratios for each section/group.
+        *                                 For example:
+        *                                 [
+        *                                     'section1' => [
+        *                                         'group1' => [
+        *                                             'db1' => 100,
+        *                                             'db2' => 100
+        *                                         ]
+        *                                     ]
+        *                                 ]
+        *   - groupLoadsByDB              3-d map giving server load ratios by DB name.
+        *   - hostsByName                 Map of hostname to IP address.
+        *   - externalLoads               Map of external storage cluster name to server load map.
+        *   - externalTemplateOverrides   Set of server configuration maps overriding
+        *                                 "serverTemplate" for external storage.
+        *   - templateOverridesByServer   2-d map overriding "serverTemplate" and
+        *                                 "externalTemplateOverrides" on a server-by-server basis.
+        *                                 Applies to both core and external storage.
+        *   - templateOverridesBySection  2-d map overriding the server configuration maps by section.
+        *   - templateOverridesByCluster  2-d map overriding the server configuration maps by external
+        *                                 storage cluster.
+        *   - masterTemplateOverrides     Server configuration map overrides for all master servers.
+        *   - loadMonitorClass            Name of the LoadMonitor class to always use.
+        *   - readOnlyBySection           A map of section name to read-only message.
+        *                                 Missing or false for read/write.
+        */
+       public function __construct( array $conf ) {
+               parent::__construct( $conf );
+
+               $this->conf = $conf;
+               $required = [ 'sectionsByDB', 'sectionLoads', 'serverTemplate' ];
+               $optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
+                       'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
+                       'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides',
+                       'readOnlyBySection', 'loadMonitorClass' ];
+
+               foreach ( $required as $key ) {
+                       if ( !isset( $conf[$key] ) ) {
+                               throw new InvalidArgumentException( __CLASS__ . ": $key is required." );
+                       }
+                       $this->$key = $conf[$key];
+               }
+
+               foreach ( $optional as $key ) {
+                       if ( isset( $conf[$key] ) ) {
+                               $this->$key = $conf[$key];
+                       }
+               }
+       }
+
+       /**
+        * @param bool|string $domain
+        * @return string
+        */
+       private function getSectionForDomain( $domain = false ) {
+               if ( $this->lastDomain === $domain ) {
+                       return $this->lastSection;
+               }
+               list( $dbName, ) = $this->getDBNameAndPrefix( $domain );
+               if ( isset( $this->sectionsByDB[$dbName] ) ) {
+                       $section = $this->sectionsByDB[$dbName];
+               } else {
+                       $section = 'DEFAULT';
+               }
+               $this->lastSection = $section;
+               $this->lastDomain = $domain;
+
+               return $section;
+       }
+
+       /**
+        * @param bool|string $domain
+        * @return LoadBalancer
+        */
+       public function newMainLB( $domain = false ) {
+               list( $dbName, ) = $this->getDBNameAndPrefix( $domain );
+               $section = $this->getSectionForDomain( $domain );
+               if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
+                       $groupLoads = $this->groupLoadsByDB[$dbName];
+               } else {
+                       $groupLoads = [];
+               }
+
+               if ( isset( $this->groupLoadsBySection[$section] ) ) {
+                       $groupLoads = array_merge_recursive(
+                               $groupLoads, $this->groupLoadsBySection[$section] );
+               }
+
+               $readOnlyReason = $this->readOnlyReason;
+               // Use the LB-specific read-only reason if everything isn't already read-only
+               if ( $readOnlyReason === false && isset( $this->readOnlyBySection[$section] ) ) {
+                       $readOnlyReason = $this->readOnlyBySection[$section];
+               }
+
+               $template = $this->serverTemplate;
+               if ( isset( $this->templateOverridesBySection[$section] ) ) {
+                       $template = $this->templateOverridesBySection[$section] + $template;
+               }
+
+               return $this->newLoadBalancer(
+                       $template,
+                       $this->sectionLoads[$section],
+                       $groupLoads,
+                       $readOnlyReason
+               );
+       }
+
+       /**
+        * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
+        * @return LoadBalancer
+        */
+       public function getMainLB( $domain = false ) {
+               $section = $this->getSectionForDomain( $domain );
+               if ( !isset( $this->mainLBs[$section] ) ) {
+                       $lb = $this->newMainLB( $domain );
+                       $this->getChronologyProtector()->initLB( $lb );
+                       $this->mainLBs[$section] = $lb;
+               }
+
+               return $this->mainLBs[$section];
+       }
+
+       /**
+        * @param string $cluster
+        * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
+        * @throws InvalidArgumentException
+        * @return LoadBalancer
+        */
+       public function newExternalLB( $cluster, $domain = false ) {
+               if ( !isset( $this->externalLoads[$cluster] ) ) {
+                       throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
+               }
+               $template = $this->serverTemplate;
+               if ( isset( $this->externalTemplateOverrides ) ) {
+                       $template = $this->externalTemplateOverrides + $template;
+               }
+               if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
+                       $template = $this->templateOverridesByCluster[$cluster] + $template;
+               }
+
+               return $this->newLoadBalancer(
+                       $template,
+                       $this->externalLoads[$cluster],
+                       [],
+                       $this->readOnlyReason
+               );
+       }
+
+       /**
+        * @param string $cluster External storage cluster, or false for core
+        * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
+        * @return LoadBalancer
+        */
+       public function getExternalLB( $cluster, $domain = false ) {
+               if ( !isset( $this->extLBs[$cluster] ) ) {
+                       $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $domain );
+                       $this->getChronologyProtector()->initLB( $this->extLBs[$cluster] );
+               }
+
+               return $this->extLBs[$cluster];
+       }
+
+       /**
+        * Make a new load balancer object based on template and load array
+        *
+        * @param array $template
+        * @param array $loads
+        * @param array $groupLoads
+        * @param string|bool $readOnlyReason
+        * @return LoadBalancer
+        */
+       private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) {
+               $lb = new LoadBalancer( array_merge(
+                       $this->baseLoadBalancerParams(),
+                       [
+                               'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
+                               'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
+                               'readOnlyReason' => $readOnlyReason
+                       ]
+               ) );
+               $this->initLoadBalancer( $lb );
+
+               return $lb;
+       }
+
+       /**
+        * Make a server array as expected by LoadBalancer::__construct, using a template and load array
+        *
+        * @param array $template
+        * @param array $loads
+        * @param array $groupLoads
+        * @return array
+        */
+       private function makeServerArray( $template, $loads, $groupLoads ) {
+               $servers = [];
+               $master = true;
+               $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
+               foreach ( $groupLoadsByServer as $server => $stuff ) {
+                       if ( !isset( $loads[$server] ) ) {
+                               $loads[$server] = 0;
+                       }
+               }
+               foreach ( $loads as $serverName => $load ) {
+                       $serverInfo = $template;
+                       if ( $master ) {
+                               $serverInfo['master'] = true;
+                               if ( isset( $this->masterTemplateOverrides ) ) {
+                                       $serverInfo = $this->masterTemplateOverrides + $serverInfo;
+                               }
+                               $master = false;
+                       } else {
+                               $serverInfo['replica'] = true;
+                       }
+                       if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
+                               $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
+                       }
+                       if ( isset( $groupLoadsByServer[$serverName] ) ) {
+                               $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
+                       }
+                       if ( isset( $this->hostsByName[$serverName] ) ) {
+                               $serverInfo['host'] = $this->hostsByName[$serverName];
+                       } else {
+                               $serverInfo['host'] = $serverName;
+                       }
+                       $serverInfo['hostName'] = $serverName;
+                       $serverInfo['load'] = $load;
+                       $serverInfo += [ 'flags' => IDatabase::DBO_DEFAULT ];
+
+                       $servers[] = $serverInfo;
+               }
+
+               return $servers;
+       }
+
+       /**
+        * Take a group load array indexed by group then server, and reindex it by server then group
+        * @param array $groupLoads
+        * @return array
+        */
+       private function reindexGroupLoads( $groupLoads ) {
+               $reindexed = [];
+               foreach ( $groupLoads as $group => $loads ) {
+                       foreach ( $loads as $server => $load ) {
+                               $reindexed[$server][$group] = $load;
+                       }
+               }
+
+               return $reindexed;
+       }
+
+       /**
+        * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
+        * @return array [database name, table prefix]
+        */
+       private function getDBNameAndPrefix( $domain = false ) {
+               $domain = ( $domain === false )
+                       ? $this->localDomain
+                       : DatabaseDomain::newFromId( $domain );
+
+               return [ $domain->getDatabase(), $domain->getTablePrefix() ];
+       }
+
+       /**
+        * 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 = [] ) {
+               foreach ( $this->mainLBs as $lb ) {
+                       call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
+               }
+               foreach ( $this->extLBs as $lb ) {
+                       call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
+               }
+       }
+}
diff --git a/includes/libs/rdbms/lbfactory/LBFactorySimple.php b/includes/libs/rdbms/lbfactory/LBFactorySimple.php
new file mode 100644 (file)
index 0000000..674bafd
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+/**
+ * Generator 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
+ */
+
+/**
+ * A simple single-master LBFactory that gets its configuration from the b/c globals
+ */
+class LBFactorySimple extends LBFactory {
+       /** @var LoadBalancer */
+       private $mainLB;
+       /** @var LoadBalancer[] */
+       private $extLBs = [];
+
+       /** @var array[] Map of (server index => server config) */
+       private $servers = [];
+       /** @var array[] Map of (cluster => (server index => server config)) */
+       private $externalClusters = [];
+
+       /** @var string */
+       private $loadMonitorClass;
+
+       /**
+        * @see LBFactory::__construct()
+        * @param array $conf Parameters of LBFactory::__construct() as well as:
+        *   - servers : list of server configuration maps to Database::factory().
+        *      Additionally, the server maps should have a 'load' key, which is used to decide
+        *      how often clients connect to one server verses the others. A 'max lag' key should
+        *      also be set on server maps, indicating how stale the data can be before the load
+        *      balancer tries to avoid using it. The map can have 'is static' set to disable blocking
+        *      replication sync checks (intended for archive servers with unchanging data).
+        *   - externalClusters : map of cluster names to server arrays. The servers arrays have the
+        *      same format as "servers" above.
+        */
+       public function __construct( array $conf ) {
+               parent::__construct( $conf );
+
+               $this->servers = isset( $conf['servers'] ) ? $conf['servers'] : [];
+               foreach ( $this->servers as $i => $server ) {
+                       if ( $i == 0 ) {
+                               $this->servers[$i]['master'] = true;
+                       } else {
+                               $this->servers[$i]['replica'] = true;
+                       }
+               }
+
+               $this->externalClusters = isset( $conf['externalClusters'] )
+                       ? $conf['externalClusters']
+                       : [];
+               $this->loadMonitorClass = isset( $conf['loadMonitorClass'] )
+                       ? $conf['loadMonitorClass']
+                       : 'LoadMonitor';
+       }
+
+       /**
+        * @param bool|string $domain
+        * @return LoadBalancer
+        */
+       public function newMainLB( $domain = false ) {
+               return $this->newLoadBalancer( $this->servers );
+       }
+
+       /**
+        * @param bool|string $domain
+        * @return LoadBalancer
+        */
+       public function getMainLB( $domain = false ) {
+               if ( !isset( $this->mainLB ) ) {
+                       $this->mainLB = $this->newMainLB( $domain );
+                       $this->getChronologyProtector()->initLB( $this->mainLB );
+               }
+
+               return $this->mainLB;
+       }
+
+       /**
+        * @param string $cluster
+        * @param bool|string $domain
+        * @return LoadBalancer
+        * @throws InvalidArgumentException
+        */
+       public function newExternalLB( $cluster, $domain = false ) {
+               if ( !isset( $this->externalClusters[$cluster] ) ) {
+                       throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"." );
+               }
+
+               return $this->newLoadBalancer( $this->externalClusters[$cluster] );
+       }
+
+       /**
+        * @param string $cluster
+        * @param bool|string $domain
+        * @return LoadBalancer
+        */
+       public function getExternalLB( $cluster, $domain = false ) {
+               if ( !isset( $this->extLBs[$cluster] ) ) {
+                       $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $domain );
+                       $this->getChronologyProtector()->initLB( $this->extLBs[$cluster] );
+               }
+
+               return $this->extLBs[$cluster];
+       }
+
+       private function newLoadBalancer( array $servers ) {
+               $lb = new LoadBalancer( array_merge(
+                       $this->baseLoadBalancerParams(),
+                       [
+                               'servers' => $servers,
+                               'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
+                       ]
+               ) );
+               $this->initLoadBalancer( $lb );
+
+               return $lb;
+       }
+
+       /**
+        * 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 = [] ) {
+               if ( isset( $this->mainLB ) ) {
+                       call_user_func_array( $callback, array_merge( [ $this->mainLB ], $params ) );
+               }
+               foreach ( $this->extLBs as $lb ) {
+                       call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
+               }
+       }
+}
diff --git a/includes/libs/rdbms/lbfactory/LBFactorySingle.php b/includes/libs/rdbms/lbfactory/LBFactorySingle.php
new file mode 100644 (file)
index 0000000..af4a350
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+/**
+ * Simple generator of database connections that always returns the same object.
+ *
+ * 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 LBFactory class that always returns a single database object.
+ */
+class LBFactorySingle extends LBFactory {
+       /** @var LoadBalancerSingle */
+       private $lb;
+
+       /**
+        * @param array $conf An associative array with one member:
+        *  - connection: The IDatabase connection object
+        */
+       public function __construct( array $conf ) {
+               parent::__construct( $conf );
+
+               if ( !isset( $conf['connection'] ) ) {
+                       throw new InvalidArgumentException( "Missing 'connection' argument." );
+               }
+
+               $this->lb = new LoadBalancerSingle( array_merge( $this->baseLoadBalancerParams(), $conf ) );
+       }
+
+       /**
+        * @param IDatabase $db Live connection handle
+        * @param array $params Parameter map to LBFactorySingle::__constructs()
+        * @return LBFactorySingle
+        * @since 1.28
+        */
+       public static function newFromConnection( IDatabase $db, array $params = [] ) {
+               return new static( [ 'connection' => $db ] + $params );
+       }
+
+       /**
+        * @param bool|string $wiki
+        * @return LoadBalancerSingle
+        */
+       public function newMainLB( $wiki = false ) {
+               return $this->lb;
+       }
+
+       /**
+        * @param bool|string $wiki
+        * @return LoadBalancerSingle
+        */
+       public function getMainLB( $wiki = false ) {
+               return $this->lb;
+       }
+
+       /**
+        * @param string $cluster External storage cluster, or false for core
+        * @param bool|string $wiki Wiki ID, or false for the current wiki
+        * @return LoadBalancerSingle
+        */
+       public function newExternalLB( $cluster, $wiki = false ) {
+               return $this->lb;
+       }
+
+       /**
+        * @param string $cluster External storage cluster, or false for core
+        * @param bool|string $wiki Wiki ID, or false for the current wiki
+        * @return LoadBalancerSingle
+        */
+       public function getExternalLB( $cluster, $wiki = false ) {
+               return $this->lb;
+       }
+
+       /**
+        * @param string|callable $callback
+        * @param array $params
+        */
+       public function forEachLB( $callback, array $params = [] ) {
+               call_user_func_array( $callback, array_merge( [ $this->lb ], $params ) );
+       }
+}
index 0f6bea3..8854479 100644 (file)
  */
 
 /**
- * Interface for database load balancing object that manages IDatabase handles
+ * Database cluster connection, tracking, load balancing, and transaction manager interface
+ *
+ * A "cluster" is considered to be one master database and zero or more replica databases.
+ * Typically, the replica DBs replicate from the master asynchronously. The first node in the
+ * "servers" configuration array is always considered the "master". However, this class can still
+ * be used when all or some of the "replica" DBs are multi-master peers of the master or even
+ * when all the DBs are non-replicating clones of each other holding read-only data. Thus, the
+ * role of "master" is in some cases merely nominal.
+ *
+ * By default, each DB server uses DBO_DEFAULT for its 'flags' setting, unless explicitly set
+ * otherwise in configuration. DBO_DEFAULT behavior depends on whether 'cliMode' is set:
+ *   - In CLI mode, the flag has no effect with regards to LoadBalancer.
+ *   - In non-CLI mode, the flag causes implicit transactions to be used; the first query on
+ *     a database starts a transaction on that database. The transactions are meant to remain
+ *     pending until either commitMasterChanges() or rollbackMasterChanges() is called. The
+ *     application must have some point where it calls commitMasterChanges() near the end of
+ *     the PHP request.
+ * Every iteration of beginMasterChanges()/commitMasterChanges() is called a "transaction round".
+ * Rounds are useful on the master DB connections because they make single-DB (and by and large
+ * multi-DB) updates in web requests all-or-nothing. Also, transactions on replica DBs are useful
+ * when REPEATABLE-READ or SERIALIZABLE isolation is used because all foriegn keys and constraints
+ * hold across separate queries in the DB transaction since the data appears within a consistent
+ * point-in-time snapshot.
+ *
+ * The typical caller will use LoadBalancer::getConnection( DB_* ) to yield a live database
+ * connection handle. The choice of which DB server to use is based on pre-defined loads for
+ * weighted random selection, adjustments thereof by LoadMonitor, and the amount of replication
+ * lag on each DB server. Lag checks might cause problems in certain setups, so they should be
+ * tuned in the server configuration maps as follows:
+ *   - Master + N Replica(s): set 'max lag' to an appropriate threshold for avoiding any database
+ *      lagged by this much or more. If all DBs are this lagged, then the load balancer considers
+ *      the cluster to be read-only.
+ *   - Galera Cluster: Seconds_Behind_Master will be 0, so there probably is nothing to tune.
+ *      Note that lag is still possible depending on how wsrep-sync-wait is set server-side.
+ *   - 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).
+ *
+ * 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
+ * the (logical) replica server entry. The proxy could map the load balancer's "replica" DB to
+ * any number of physical replica DBs.
  *
  * @since 1.28
  * @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;
+
+       /** @var string Domain specifier when no specific database needs to be selected */
+       const DOMAIN_ANY = '';
+
        /**
-        * @param array $params Array with keys:
+        * Construct a manager of IDatabase connection objects
+        *
+        * @param array $params Parameter map with keys:
         *  - servers : Required. Array of server info structures.
+        *  - localDomain: A DatabaseDomain or domain ID string.
         *  - loadMonitor : Name of a class used to fetch server lag and load.
         *  - readOnlyReason : Reason the master DB is read-only if so [optional]
         *  - waitTimeout : Maximum time to wait for replicas for consistency [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]
+        *  - 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 $params );
@@ -50,11 +113,11 @@ interface ILoadBalancer {
         *
         * Side effect: opens connections to databases
         * @param string|bool $group Query group, or false for the generic reader
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @param string|bool $domain Domain ID, or false for the current domain
         * @throws DBError
         * @return bool|int|string
         */
-       public function getReaderIndex( $group = false, $wiki = false );
+       public function getReaderIndex( $group = false, $domain = false );
 
        /**
         * Set the master wait position
@@ -98,12 +161,12 @@ interface ILoadBalancer {
         *
         * @param int $i Server index
         * @param array|string|bool $groups Query group(s), or false for the generic reader
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @param string|bool $domain Domain ID, or false for the current domain
         *
         * @throws DBError
         * @return IDatabase
         */
-       public function getConnection( $i, $groups = [], $wiki = false );
+       public function getConnection( $i, $groups = [], $domain = false );
 
        /**
         * Mark a foreign connection as being available for reuse under a different
@@ -124,10 +187,10 @@ interface ILoadBalancer {
         *
         * @param int $db
         * @param array|string|bool $groups Query group(s), or false for the generic reader
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @param string|bool $domain Domain ID, or false for the current domain
         * @return DBConnRef
         */
-       public function getConnectionRef( $db, $groups = [], $wiki = false );
+       public function getConnectionRef( $db, $groups = [], $domain = false );
 
        /**
         * Get a database connection handle reference without connecting yet
@@ -138,10 +201,10 @@ interface ILoadBalancer {
         *
         * @param int $db
         * @param array|string|bool $groups Query group(s), or false for the generic reader
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @param string|bool $domain Domain ID, or false for the current domain
         * @return DBConnRef
         */
-       public function getLazyConnectionRef( $db, $groups = [], $wiki = false );
+       public function getLazyConnectionRef( $db, $groups = [], $domain = false );
 
        /**
         * Open a connection to the server given by the specified index
@@ -154,10 +217,11 @@ interface ILoadBalancer {
         * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
         *
         * @param int $i Server index
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @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, $wiki = false );
+       public function openConnection( $i, $domain = false );
 
        /**
         * @return int
@@ -352,10 +416,10 @@ interface ILoadBalancer {
 
        /**
         * @note This method will trigger a DB connection if not yet done
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @param string|bool $domain Domain ID, or false for the current domain
         * @return bool Whether the generic connection for reads is highly "lagged"
         */
-       public function getLaggedReplicaMode( $wiki = false );
+       public function getLaggedReplicaMode( $domain = false );
 
        /**
         * @note This method will never cause a new DB connection
@@ -365,11 +429,11 @@ interface ILoadBalancer {
 
        /**
         * @note This method may trigger a DB connection if not yet done
-        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @param string|bool $domain Domain ID, or false for the current domain
         * @param IDatabase|null DB master connection; used to avoid loops [optional]
         * @return string|bool Reason the master is read-only or false if it is not
         */
-       public function getReadOnlyReason( $wiki = false, IDatabase $conn = null );
+       public function getReadOnlyReason( $domain = false, IDatabase $conn = null );
 
        /**
         * Disables/enables lag checks
@@ -411,10 +475,10 @@ interface ILoadBalancer {
         * May attempt to open connections to replica DBs on the default DB. If there is
         * no lag, the maximum lag will be reported as -1.
         *
-        * @param bool|string $wiki Wiki ID, or false for the default database
+        * @param bool|string $domain Domain ID, or false for the default database
         * @return array ( host, max lag, index of max lagged host )
         */
-       public function getMaxLag( $wiki = false );
+       public function getMaxLag( $domain = false );
 
        /**
         * Get an estimate of replication lag (in seconds) for each server
@@ -423,10 +487,10 @@ interface ILoadBalancer {
         *
         * Values may be "false" if replication is too broken to estimate
         *
-        * @param string|bool $wiki
+        * @param string|bool $domain
         * @return int[] Map of (server index => float|int|bool)
         */
-       public function getLagTimes( $wiki = false );
+       public function getLagTimes( $domain = false );
 
        /**
         * Get the lag in seconds for a given connection, or zero if this load
@@ -456,13 +520,6 @@ interface ILoadBalancer {
         */
        public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null );
 
-       /**
-        * Clear the cache for slag lag delay times
-        *
-        * This is only used for testing
-        */
-       public function clearLagTimeCache();
-
        /**
         * Set a callback via IDatabase::setTransactionListener() on
         * all current and future master connections of this load balancer
@@ -471,4 +528,25 @@ interface ILoadBalancer {
         * @param callable|null $callback
         */
        public function setTransactionListener( $name, callable $callback = null );
+
+       /**
+        * Set a new table prefix for the existing local domain ID for testing
+        *
+        * @param string $prefix
+        */
+       public function setDomainPrefix( $prefix );
+
+       /**
+        * Make certain table names use their own database, schema, and table prefix
+        * when passed into SQL queries pre-escaped and without a qualified database name
+        *
+        * For example, "user" can be converted to "myschema.mydbname.user" for convenience.
+        * Appearances like `user`, somedb.user, somedb.someschema.user will used literally.
+        *
+        * Calling this twice will completely clear any old table aliases. Also, note that
+        * callers are responsible for making sure the schemas and databases actually exist.
+        *
+        * @param array[] $aliases Map of (table => (dbname, schema, prefix) map)
+        */
+       public function setTableAliases( array $aliases );
 }
index db69de1..33f3561 100644 (file)
@@ -23,7 +23,7 @@
 use Psr\Log\LoggerInterface;
 
 /**
- * Database load balancing, tracking, and transaction management object
+ * Database connection, tracking, load balancing, and transaction manager for a cluster
  *
  * @ingroup Database
  */
@@ -32,7 +32,7 @@ class LoadBalancer implements ILoadBalancer {
        private $mServers;
        /** @var array[] Map of (local/foreignUsed/foreignFree => server index => IDatabase array) */
        private $mConns;
-       /** @var array Map of (server index => weight) */
+       /** @var float[] Map of (server index => weight) */
        private $mLoads;
        /** @var array[] Map of (group => server index => weight) */
        private $mGroupLoads;
@@ -40,19 +40,21 @@ class LoadBalancer implements ILoadBalancer {
        private $mAllowLagged;
        /** @var integer Seconds to spend waiting on replica DB lag to resolve */
        private $mWaitTimeout;
-       /** @var string The LoadMonitor subclass name */
-       private $mLoadMonitorClass;
+       /** @var array The LoadMonitor configuration */
+       private $loadMonitorConfig;
        /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
        private $tableAliases = [];
 
        /** @var ILoadMonitor */
-       private $mLoadMonitor;
+       private $loadMonitor;
        /** @var BagOStuff */
        private $srvCache;
        /** @var BagOStuff */
        private $memCache;
        /** @var WANObjectCache */
        private $wanCache;
+       /** @var object|string Class name or object With profileIn/profileOut methods */
+       protected $profiler;
        /** @var TransactionProfiler */
        protected $trxProfiler;
        /** @var LoggerInterface */
@@ -84,8 +86,10 @@ class LoadBalancer implements ILoadBalancer {
        private $trxRoundId = false;
        /** @var array[] Map of (name => callable) */
        private $trxRecurringCallbacks = [];
-       /** @var string Local Domain ID and default for selectDB() calls */
+       /** @var DatabaseDomain Local Domain ID and default for selectDB() calls */
        private $localDomain;
+       /** @var string Alternate ID string for the domain instead of DatabaseDomain::getId() */
+       private $localDomainIdAlias;
        /** @var string Current server name */
        private $host;
        /** @var bool Whether this PHP instance is for a CLI script */
@@ -101,10 +105,9 @@ class LoadBalancer implements ILoadBalancer {
 
        /** @var integer Warn when this many connection are held */
        const CONN_HELD_WARN_THRESHOLD = 10;
+
        /** @var integer Default 'max lag' when unspecified */
        const MAX_LAG_DEFAULT = 10;
-       /** @var integer Max time to wait for a replica DB to catch up (e.g. ChronologyProtector) */
-       const POS_WAIT_TIMEOUT = 10;
        /** @var integer Seconds to cache master server read-only status */
        const TTL_CACHE_READONLY = 5;
 
@@ -113,16 +116,27 @@ class LoadBalancer implements ILoadBalancer {
                        throw new InvalidArgumentException( __CLASS__ . ': missing servers parameter' );
                }
                $this->mServers = $params['servers'];
-               $this->mWaitTimeout = isset( $params['waitTimeout'] )
-                       ? $params['waitTimeout']
-                       : self::POS_WAIT_TIMEOUT;
-               $this->localDomain = isset( $params['localDomain'] ) ? $params['localDomain'] : '';
+
+               $this->localDomain = isset( $params['localDomain'] )
+                       ? DatabaseDomain::newFromId( $params['localDomain'] )
+                       : DatabaseDomain::newUnspecified();
+               // In case a caller assumes that the domain ID is simply <db>-<prefix>, which is almost
+               // always true, gracefully handle the case when they fail to account for escaping.
+               if ( $this->localDomain->getTablePrefix() != '' ) {
+                       $this->localDomainIdAlias =
+                               $this->localDomain->getDatabase() . '-' . $this->localDomain->getTablePrefix();
+               } else {
+                       $this->localDomainIdAlias = $this->localDomain->getDatabase();
+               }
+
+               $this->mWaitTimeout = isset( $params['waitTimeout'] ) ? $params['waitTimeout'] : 10;
 
                $this->mReadIndex = -1;
                $this->mConns = [
-                       'local' => [],
+                       'local'       => [],
                        'foreignUsed' => [],
-                       'foreignFree' => [] ];
+                       'foreignFree' => []
+               ];
                $this->mLoads = [];
                $this->mWaitForPos = false;
                $this->mErrorConnection = false;
@@ -133,14 +147,9 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                if ( isset( $params['loadMonitor'] ) ) {
-                       $this->mLoadMonitorClass = $params['loadMonitor'];
+                       $this->loadMonitorConfig = $params['loadMonitor'];
                } else {
-                       $master = reset( $params['servers'] );
-                       if ( isset( $master['type'] ) && $master['type'] === 'mysql' ) {
-                               $this->mLoadMonitorClass = 'LoadMonitorMySQL';
-                       } else {
-                               $this->mLoadMonitorClass = 'LoadMonitorNull';
-                       }
+                       $this->loadMonitorConfig = [ 'class' => 'LoadMonitorNull' ];
                }
 
                foreach ( $params['servers'] as $i => $server ) {
@@ -170,6 +179,7 @@ class LoadBalancer implements ILoadBalancer {
                } else {
                        $this->wanCache = WANObjectCache::newEmpty();
                }
+               $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
                if ( isset( $params['trxProfiler'] ) ) {
                        $this->trxProfiler = $params['trxProfiler'];
                } else {
@@ -199,13 +209,14 @@ class LoadBalancer implements ILoadBalancer {
         * @return ILoadMonitor
         */
        private function getLoadMonitor() {
-               if ( !isset( $this->mLoadMonitor ) ) {
-                       $class = $this->mLoadMonitorClass;
-                       $this->mLoadMonitor = new $class( $this, $this->srvCache, $this->memCache );
-                       $this->mLoadMonitor->setLogger( $this->replLogger );
+               if ( !isset( $this->loadMonitor ) ) {
+                       $class = $this->loadMonitorConfig['class'];
+                       $this->loadMonitor = new $class(
+                               $this, $this->srvCache, $this->memCache, $this->loadMonitorConfig );
+                       $this->loadMonitor->setLogger( $this->replLogger );
                }
 
-               return $this->mLoadMonitor;
+               return $this->loadMonitor;
        }
 
        /**
@@ -287,7 +298,7 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                # Scale the configured load ratios according to the dynamic load if supported
-               $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $domain );
+               $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $domain );
 
                $laggedReplicaMode = false;
 
@@ -389,16 +400,6 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-       /**
-        * Set the master wait position and wait for a "generic" replica DB to catch up to it
-        *
-        * This can be used a faster proxy for waitForAll()
-        *
-        * @param DBMasterPos $pos
-        * @param int $timeout Max seconds to wait; default is mWaitTimeout
-        * @return bool Success (able to connect and no timeouts reached)
-        * @since 1.26
-        */
        public function waitForOne( $pos, $timeout = null ) {
                $this->mWaitForPos = $pos;
 
@@ -473,7 +474,7 @@ class LoadBalancer implements ILoadBalancer {
 
                                return false;
                        } else {
-                               $conn = $this->openConnection( $index, '' );
+                               $conn = $this->openConnection( $index, self::DOMAIN_ANY );
                                if ( !$conn ) {
                                        $this->replLogger->warning( __METHOD__ . ": failed to connect to $server" );
 
@@ -508,24 +509,33 @@ 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__ .
                                ' with invalid server index' );
                }
 
-               if ( $domain === $this->localDomain ) {
-                       $domain = false;
+               if ( $this->localDomain->equals( $domain ) || $domain === $this->localDomainIdAlias ) {
+                       $domain = false; // local connection requested
                }
 
                $groups = ( $groups === false || $groups === [] )
                        ? [ 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)
@@ -539,7 +549,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 )
@@ -548,15 +558,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
@@ -581,7 +594,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 );
                         *   }
@@ -589,23 +602,34 @@ class LoadBalancer implements ILoadBalancer {
                         * should be ignored
                         */
                        return;
+               } elseif ( $conn instanceof DBConnRef ) {
+                       // DBConnRef already handles calling reuseConnection() and only passes the live
+                       // Database instance to this method. Any caller passing in a DBConnRef is broken.
+                       $this->connLogger->error( __METHOD__ . ": got DBConnRef instance.\n" .
+                               ( new RuntimeException() )->getTraceAsString() );
+
+                       return;
                }
 
-               $dbName = $conn->getDBname();
-               $prefix = $conn->tablePrefix();
-               if ( strval( $prefix ) !== '' ) {
-                       $domain = "$dbName-$prefix";
-               } else {
-                       $domain = $dbName;
+               if ( $this->disabled ) {
+                       return; // DBConnRef handle probably survived longer than the LoadBalancer
                }
-               if ( $this->mConns['foreignUsed'][$serverIndex][$domain] !== $conn ) {
-                       throw new InvalidArgumentException( __METHOD__ . ": connection not found, has " .
-                               "the connection been freed already?" );
+
+               $domain = $conn->getDomainID();
+               if ( !isset( $this->mConns['foreignUsed'][$serverIndex][$domain] ) ) {
+                       throw new InvalidArgumentException( __METHOD__ .
+                               ": connection $serverIndex/$domain not found; it may have already been freed." );
+               } elseif ( $this->mConns['foreignUsed'][$serverIndex][$domain] !== $conn ) {
+                       throw new InvalidArgumentException( __METHOD__ .
+                               ": connection $serverIndex/$domain mismatched; it may have already been freed." );
                }
                $conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
                if ( $refCount <= 0 ) {
                        $this->mConns['foreignFree'][$serverIndex][$domain] = $conn;
                        unset( $this->mConns['foreignUsed'][$serverIndex][$domain] );
+                       if ( !$this->mConns['foreignUsed'][$serverIndex] ) {
+                               unset( $this->mConns[ 'foreignUsed' ][$serverIndex] ); // clean up
+                       }
                        $this->connLogger->debug( __METHOD__ . ": freed connection $serverIndex/$domain" );
                } else {
                        $this->connLogger->debug( __METHOD__ .
@@ -613,43 +637,31 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-       /**
-        * Get a database connection handle reference
-        *
-        * The handle's methods wrap simply wrap those of a IDatabase handle
-        *
-        * @see LoadBalancer::getConnection() for parameter information
-        *
-        * @param int $db
-        * @param array|string|bool $groups Query group(s), or false for the generic reader
-        * @param string|bool $domain Domain ID, or false for the current domain
-        * @return DBConnRef
-        * @since 1.22
-        */
        public function getConnectionRef( $db, $groups = [], $domain = false ) {
+               $domain = ( $domain !== false ) ? $domain : $this->localDomain;
+
                return new DBConnRef( $this, $this->getConnection( $db, $groups, $domain ) );
        }
 
-       /**
-        * Get a database connection handle reference without connecting yet
-        *
-        * The handle's methods wrap simply wrap those of a IDatabase handle
-        *
-        * @see LoadBalancer::getConnection() for parameter information
-        *
-        * @param int $db
-        * @param array|string|bool $groups Query group(s), or false for the generic reader
-        * @param string|bool $domain Domain ID, or false for the current domain
-        * @return DBConnRef
-        * @since 1.22
-        */
        public function getLazyConnectionRef( $db, $groups = [], $domain = false ) {
                $domain = ( $domain !== false ) ? $domain : $this->localDomain;
 
                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
+               }
+
                if ( $domain !== false ) {
                        $conn = $this->openForeignConnection( $i, $domain );
                } elseif ( isset( $this->mConns['local'][$i][0] ) ) {
@@ -699,10 +711,12 @@ 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 ) {
-               list( $dbName, $prefix ) = explode( '-', $domain, 2 ) + [ '', '' ];
+               $domainInstance = DatabaseDomain::newFromId( $domain );
+               $dbName = $domainInstance->getDatabase();
+               $prefix = $domainInstance->getTablePrefix();
 
                if ( isset( $this->mConns['foreignUsed'][$i][$domain] ) ) {
                        // Reuse an already-used connection
@@ -718,11 +732,10 @@ class LoadBalancer implements ILoadBalancer {
                        // Reuse a connection from another domain
                        $conn = reset( $this->mConns['foreignFree'][$i] );
                        $oldDomain = key( $this->mConns['foreignFree'][$i] );
-
                        // The empty string as a DB name means "don't care".
                        // DatabaseMysqlBase::open() already handle this on connection.
-                       if ( $dbName !== '' && !$conn->selectDB( $dbName ) ) {
-                               $this->mLastError = "Error selecting database $dbName on server " .
+                       if ( strlen( $dbName ) && !$conn->selectDB( $dbName ) ) {
+                               $this->mLastError = "Error selecting database '$dbName' on server " .
                                        $conn->getServer() . " from client host {$this->host}";
                                $this->mErrorConnection = $conn;
                                $conn = false;
@@ -781,8 +794,8 @@ class LoadBalancer implements ILoadBalancer {
         * @access private
         *
         * @param array $server
-        * @param bool $dbNameOverride
-        * @return IDatabase
+        * @param string|bool $dbNameOverride Use "" to not select any database
+        * @return Database
         * @throws DBAccessError
         * @throws InvalidArgumentException
         */
@@ -812,17 +825,22 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                $server['srvCache'] = $this->srvCache;
-               // Set loggers
+               // Set loggers and profilers
                $server['connLogger'] = $this->connLogger;
                $server['queryLogger'] = $this->queryLogger;
+               $server['errorLogger'] = $this->errorLogger;
+               $server['profiler'] = $this->profiler;
                $server['trxProfiler'] = $this->trxProfiler;
+               // Use the same agent and PHP mode for all DB handles
                $server['cliMode'] = $this->cliMode;
-               $server['errorLogger'] = $this->errorLogger;
                $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'] : IDatabase::DBO_DEFAULT;
 
                // Create a live connection object
                try {
-                       $db = DatabaseBase::factory( $server['type'], $server );
+                       $db = Database::factory( $server['type'], $server );
                } catch ( DBConnectionError $e ) {
                        // FIXME: This is probably the ugliest thing I have ever done to
                        // PHP. I'm half-expecting it to segfault, just out of disgust. -- TS
@@ -831,7 +849,7 @@ class LoadBalancer implements ILoadBalancer {
 
                $db->setLBInfo( $server );
                $db->setLazyMasterHandle(
-                       $this->getLazyConnectionRef( DB_MASTER, [], $db->getWikiID() )
+                       $this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() )
                );
                $db->setTableAliases( $this->tableAliases );
 
@@ -849,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,
@@ -877,8 +894,6 @@ class LoadBalancer implements ILoadBalancer {
                        // throws DBConnectionError
                        $conn->reportConnectionError( "{$this->mLastError} ({$context['db_server']})" );
                }
-
-               return false; /* not reached */
        }
 
        public function getWriterIndex() {
@@ -930,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 {
@@ -940,12 +955,6 @@ class LoadBalancer implements ILoadBalancer {
                return false;
        }
 
-       /**
-        * Disable this load balancer. All connections are closed, and any attempt to
-        * open a new connection will result in a DBAccessError.
-        *
-        * @since 1.27
-        */
        public function disable() {
                $this->closeAll();
                $this->disabled = true;
@@ -953,6 +962,8 @@ class LoadBalancer implements ILoadBalancer {
 
        public function closeAll() {
                $this->forEachOpenConnection( function ( IDatabase $conn ) {
+                       $host = $conn->getServer();
+                       $this->connLogger->debug( "Closing connection to database '$host'." );
                        $conn->close();
                } );
 
@@ -973,6 +984,8 @@ class LoadBalancer implements ILoadBalancer {
 
                        foreach ( $connsByServer[$serverIndex] as $i => $trackedConn ) {
                                if ( $conn === $trackedConn ) {
+                                       $host = $this->getServerName( $i );
+                                       $this->connLogger->debug( "Closing connection to database $i at '$host'." );
                                        unset( $this->mConns[$type][$serverIndex][$i] );
                                        --$this->connsOpened;
                                        break 2;
@@ -1010,13 +1023,8 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-       /**
-        * Perform all pre-commit callbacks that remain part of the atomic transactions
-        * and disable any post-commit callbacks until runMasterPostTrxCallbacks()
-        * @since 1.28
-        */
        public function finalizeMasterChanges() {
-               $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
+               $this->forEachOpenMasterConnection( function ( Database $conn ) {
                        // Any error should cause all DB transactions to be rolled back together
                        $conn->setTrxEndCallbackSuppression( false );
                        $conn->runOnTransactionPreCommitCallbacks();
@@ -1025,13 +1033,6 @@ class LoadBalancer implements ILoadBalancer {
                } );
        }
 
-       /**
-        * Perform all pre-commit checks for things like replication safety
-        * @param array $options Includes:
-        *   - maxWriteDuration : max write query duration time in seconds
-        * @throws DBTransactionError
-        * @since 1.28
-        */
        public function approveMasterChanges( array $options ) {
                $limit = isset( $options['maxWriteDuration'] ) ? $options['maxWriteDuration'] : 0;
                $this->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( $limit ) {
@@ -1065,19 +1066,6 @@ class LoadBalancer implements ILoadBalancer {
                } );
        }
 
-       /**
-        * 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 DBExpectedError
-        * @since 1.28
-        */
        public function beginMasterChanges( $fname = __METHOD__ ) {
                if ( $this->trxRoundId !== false ) {
                        throw new DBTransactionError(
@@ -1089,7 +1077,7 @@ class LoadBalancer implements ILoadBalancer {
 
                $failures = [];
                $this->forEachOpenMasterConnection(
-                       function ( DatabaseBase $conn ) use ( $fname, &$failures ) {
+                       function ( Database $conn ) use ( $fname, &$failures ) {
                                $conn->setTrxEndCallbackSuppression( true );
                                try {
                                        $conn->flushSnapshot( $fname );
@@ -1113,6 +1101,9 @@ class LoadBalancer implements ILoadBalancer {
        public function commitMasterChanges( $fname = __METHOD__ ) {
                $failures = [];
 
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $scope = $this->getScopedPHPBehaviorForCommit(); // try to ignore client aborts
+
                $restore = ( $this->trxRoundId !== false );
                $this->trxRoundId = false;
                $this->forEachOpenMasterConnection(
@@ -1141,15 +1132,9 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-       /**
-        * Issue all pending post-COMMIT/ROLLBACK callbacks
-        * @param integer $type IDatabase::TRIGGER_* constant
-        * @return Exception|null The first exception or null if there were none
-        * @since 1.28
-        */
        public function runMasterPostTrxCallbacks( $type ) {
                $e = null; // first exception
-               $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $type, &$e ) {
+               $this->forEachOpenMasterConnection( function ( Database $conn ) use ( $type, &$e ) {
                        $conn->setTrxEndCallbackSuppression( false );
                        if ( $conn->writesOrCallbacksPending() ) {
                                // This happens if onTransactionIdle() callbacks leave callbacks on *another* DB
@@ -1180,12 +1165,6 @@ class LoadBalancer implements ILoadBalancer {
                return $e;
        }
 
-       /**
-        * Issue ROLLBACK only on master, only if queries were done on connection
-        * @param string $fname Caller name
-        * @throws DBExpectedError
-        * @since 1.23
-        */
        public function rollbackMasterChanges( $fname = __METHOD__ ) {
                $restore = ( $this->trxRoundId !== false );
                $this->trxRoundId = false;
@@ -1201,13 +1180,8 @@ class LoadBalancer implements ILoadBalancer {
                );
        }
 
-       /**
-        * Suppress all pending post-COMMIT/ROLLBACK callbacks
-        * @return Exception|null The first exception or null if there were none
-        * @since 1.28
-        */
        public function suppressTransactionEndCallbacks() {
-               $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
+               $this->forEachOpenMasterConnection( function ( Database $conn ) {
                        $conn->setTrxEndCallbackSuppression( true );
                } );
        }
@@ -1216,10 +1190,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.
@@ -1230,36 +1204,21 @@ 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 );
                }
        }
 
-       /**
-        * 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->forEachOpenReplicaConnection( function ( IDatabase $conn ) {
                        $conn->flushSnapshot( __METHOD__ );
                } );
        }
 
-       /**
-        * @return bool Whether a master connection is already open
-        * @since 1.24
-        */
        public function hasMasterConnection() {
                return $this->isOpen( $this->getWriterIndex() );
        }
 
-       /**
-        * Determine if there are pending changes in a transaction by this thread
-        * @since 1.23
-        * @return bool
-        */
        public function hasMasterChanges() {
                $pending = 0;
                $this->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( &$pending ) {
@@ -1269,11 +1228,6 @@ class LoadBalancer implements ILoadBalancer {
                return (bool)$pending;
        }
 
-       /**
-        * Get the timestamp of the latest write query done by this thread
-        * @since 1.25
-        * @return float|bool UNIX timestamp or false
-        */
        public function lastMasterChangeTimestamp() {
                $lastTime = false;
                $this->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( &$lastTime ) {
@@ -1283,14 +1237,6 @@ class LoadBalancer implements ILoadBalancer {
                return $lastTime;
        }
 
-       /**
-        * Check if this load balancer object had any recent or still
-        * pending writes issued against it by this PHP thread
-        *
-        * @param float $age How many seconds ago is "recent" [defaults to mWaitTimeout]
-        * @return bool
-        * @since 1.25
-        */
        public function hasOrMadeRecentMasterChanges( $age = null ) {
                $age = ( $age === null ) ? $this->mWaitTimeout : $age;
 
@@ -1298,12 +1244,6 @@ class LoadBalancer implements ILoadBalancer {
                        || $this->lastMasterChangeTimestamp() > microtime( true ) - $age );
        }
 
-       /**
-        * Get the list of callers that have pending master changes
-        *
-        * @return string[] List of method names
-        * @since 1.27
-        */
        public function pendingMasterChangeCallers() {
                $fnames = [];
                $this->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( &$fnames ) {
@@ -1318,7 +1258,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
@@ -1339,11 +1279,6 @@ class LoadBalancer implements ILoadBalancer {
                return $this->getLaggedReplicaMode( $domain );
        }
 
-       /**
-        * @note This method will never cause a new DB connection
-        * @return bool Whether any generic connection used for reads was highly "lagged"
-        * @since 1.28
-        */
        public function laggedReplicaUsed() {
                return $this->laggedReplicaMode;
        }
@@ -1357,13 +1292,6 @@ class LoadBalancer implements ILoadBalancer {
                return $this->laggedReplicaUsed();
        }
 
-       /**
-        * @note This method may trigger a DB connection if not yet done
-        * @param string|bool $domain Domain ID, or false for the current domain
-        * @param IDatabase|null DB master connection; used to avoid loops [optional]
-        * @return string|bool Reason the master is read-only or false if it is not
-        * @since 1.27
-        */
        public function getReadOnlyReason( $domain = false, IDatabase $conn = null ) {
                if ( $this->readOnlyReason !== false ) {
                        return $this->readOnlyReason;
@@ -1397,8 +1325,11 @@ 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 );
+                                       }
                                } catch ( DBError $e ) {
                                        $readOnly = 0;
                                }
@@ -1440,12 +1371,6 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-       /**
-        * Call a function with each open connection object to a master
-        * @param callable $callback
-        * @param array $params
-        * @since 1.28
-        */
        public function forEachOpenMasterConnection( $callback, array $params = [] ) {
                $masterIndex = $this->getWriterIndex();
                foreach ( $this->mConns as $connsByServer ) {
@@ -1459,12 +1384,6 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-       /**
-        * Call a function with each open replica DB connection object
-        * @param callable $callback
-        * @param array $params
-        * @since 1.28
-        */
        public function forEachOpenReplicaConnection( $callback, array $params = [] ) {
                foreach ( $this->mConns as $connsByServer ) {
                        foreach ( $connsByServer as $i => $serverConns ) {
@@ -1502,73 +1421,65 @@ class LoadBalancer implements ILoadBalancer {
 
        public function getLagTimes( $domain = false ) {
                if ( $this->getServerCount() <= 1 ) {
-                       return [ 0 => 0 ]; // no replication = no lag
+                       return [ $this->getWriterIndex() => 0 ]; // no replication = no lag
+               }
+
+               $knownLagTimes = []; // map of (server index => 0 seconds)
+               $indexesWithLag = [];
+               foreach ( $this->mServers as $i => $server ) {
+                       if ( empty( $server['is static'] ) ) {
+                               $indexesWithLag[] = $i; // DB server might have replication lag
+                       } else {
+                               $knownLagTimes[$i] = 0; // DB server is a non-replicating and read-only archive
+                       }
                }
 
-               # Send the request to the load monitor
-               return $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $domain );
+               return $this->getLoadMonitor()->getLagTimes( $indexesWithLag, $domain ) + $knownLagTimes;
        }
 
        public function safeGetLag( IDatabase $conn ) {
-               if ( $this->getServerCount() == 1 ) {
+               if ( $this->getServerCount() <= 1 ) {
                        return 0;
                } else {
                        return $conn->getLag();
                }
        }
 
-       /**
-        * Wait for a replica DB to reach a specified master position
-        *
-        * This will connect to the master to get an accurate position if $pos is not given
-        *
-        * @param IDatabase $conn Replica DB
-        * @param DBMasterPos|bool $pos Master position; default: current position
-        * @param integer $timeout Timeout in seconds
-        * @return bool Success
-        * @since 1.27
-        */
        public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) {
-               if ( $this->getServerCount() == 1 || !$conn->getLBInfo( 'replica' ) ) {
+               if ( $this->getServerCount() <= 1 || !$conn->getLBInfo( 'replica' ) ) {
                        return true; // server is not a replica DB
                }
 
-               $pos = $pos ?: $this->getConnection( DB_MASTER )->getMasterPos();
-               if ( !( $pos instanceof DBMasterPos ) ) {
-                       return false; // something is misconfigured
+               if ( !$pos ) {
+                       // Get the current master position, opening a connection if needed
+                       $masterConn = $this->getAnyOpenConnection( $this->getWriterIndex() );
+                       if ( $masterConn ) {
+                               $pos = $masterConn->getMasterPos();
+                       } else {
+                               $masterConn = $this->openConnection( $this->getWriterIndex(), self::DOMAIN_ANY );
+                               $pos = $masterConn->getMasterPos();
+                               $this->closeConnection( $masterConn );
+                       }
                }
 
-               $result = $conn->masterPosWait( $pos, $timeout );
-               if ( $result == -1 || is_null( $result ) ) {
-                       $msg = __METHOD__ . ": Timed out waiting on {$conn->getServer()} pos {$pos}";
-                       $this->replLogger->warning( "$msg" );
-                       $ok = false;
+               if ( $pos instanceof DBMasterPos ) {
+                       $result = $conn->masterPosWait( $pos, $timeout );
+                       if ( $result == -1 || is_null( $result ) ) {
+                               $msg = __METHOD__ . ": Timed out waiting on {$conn->getServer()} pos {$pos}";
+                               $this->replLogger->warning( "$msg" );
+                               $ok = false;
+                       } else {
+                               $this->replLogger->info( __METHOD__ . ": Done" );
+                               $ok = true;
+                       }
                } else {
-                       $this->replLogger->info( __METHOD__ . ": Done" );
-                       $ok = true;
+                       $ok = false; // something is misconfigured
+                       $this->replLogger->error( "Could not get master pos for {$conn->getServer()}." );
                }
 
                return $ok;
        }
 
-       /**
-        * Clear the cache for slag lag delay times
-        *
-        * This is only used for testing
-        * @since 1.26
-        */
-       public function clearLagTimeCache() {
-               $this->getLoadMonitor()->clearCaches();
-       }
-
-       /**
-        * Set a callback via IDatabase::setTransactionListener() on
-        * all current and future master connections of this load balancer
-        *
-        * @param string $name Callback name
-        * @param callable|null $callback
-        * @since 1.28
-        */
        public function setTransactionListener( $name, callable $callback = null ) {
                if ( $callback ) {
                        $this->trxRecurringCallbacks[$name] = $callback;
@@ -1582,32 +1493,52 @@ class LoadBalancer implements ILoadBalancer {
                );
        }
 
-       /**
-        * Make certain table names use their own database, schema, and table prefix
-        * when passed into SQL queries pre-escaped and without a qualified database name
-        *
-        * For example, "user" can be converted to "myschema.mydbname.user" for convenience.
-        * Appearances like `user`, somedb.user, somedb.someschema.user will used literally.
-        *
-        * Calling this twice will completely clear any old table aliases. Also, note that
-        * callers are responsible for making sure the schemas and databases actually exist.
-        *
-        * @param array[] $aliases Map of (table => (dbname, schema, prefix) map)
-        * @since 1.28
-        */
        public function setTableAliases( array $aliases ) {
                $this->tableAliases = $aliases;
        }
 
+       public function setDomainPrefix( $prefix ) {
+               if ( $this->mConns['foreignUsed'] ) {
+                       // Do not switch connections to explicit foreign domains unless marked as free
+                       $domains = [];
+                       foreach ( $this->mConns['foreignUsed'] as $i => $connsByDomain ) {
+                               $domains = array_merge( $domains, array_keys( $connsByDomain ) );
+                       }
+                       $domains = implode( ', ', $domains );
+                       throw new DBUnexpectedError( null,
+                               "Foreign domain connections are still in use ($domains)." );
+               }
+
+               $this->localDomain = new DatabaseDomain(
+                       $this->localDomain->getDatabase(),
+                       null,
+                       $prefix
+               );
+
+               $this->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix ) {
+                       $db->tablePrefix( $prefix );
+               } );
+       }
+
        /**
-        * Set a new table prefix for the existing local domain ID for testing
+        * Make PHP ignore user aborts/disconnects until the returned
+        * value leaves scope. This returns null and does nothing in CLI mode.
         *
-        * @param string $prefix
-        * @since 1.28
+        * @return ScopedCallback|null
         */
-       public function setDomainPrefix( $prefix ) {
-               list( $dbName, ) = explode( '-', $this->localDomain, 2 );
+       final protected function getScopedPHPBehaviorForCommit() {
+               if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
+                       $old = ignore_user_abort( true ); // avoid half-finished operations
+                       return new ScopedCallback( function () use ( $old ) {
+                               ignore_user_abort( $old );
+                       } );
+               }
+
+               return null;
+       }
 
-               $this->localDomain = "{$dbName}-{$prefix}";
+       function __destruct() {
+               // Avoid connection leaks for sanity
+               $this->disable();
        }
 }
diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php b/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php
new file mode 100644 (file)
index 0000000..9de4850
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Simple generator of database connections that always returns the same object.
+ *
+ * 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
+ */
+
+/**
+ * Trivial LoadBalancer that always returns an injected connection handle
+ */
+class LoadBalancerSingle extends LoadBalancer {
+       /** @var IDatabase */
+       private $db;
+
+       /**
+        * @param array $params An associative array with one member:
+        *   - connection: An IDatabase connection object
+        */
+       public function __construct( array $params ) {
+               if ( !isset( $params['connection'] ) ) {
+                       throw new InvalidArgumentException( "Missing 'connection' argument." );
+               }
+
+               $this->db = $params['connection'];
+
+               parent::__construct( [
+                       'servers' => [
+                               [
+                                       'type' => $this->db->getType(),
+                                       'host' => $this->db->getServer(),
+                                       'dbname' => $this->db->getDBname(),
+                                       'load' => 1,
+                               ]
+                       ],
+                       'trxProfiler' => isset( $params['trxProfiler'] ) ? $params['trxProfiler'] : null,
+                       'srvCache' => isset( $params['srvCache'] ) ? $params['srvCache'] : null,
+                       'wanCache' => isset( $params['wanCache'] ) ? $params['wanCache'] : null
+               ] );
+
+               if ( isset( $params['readOnlyReason'] ) ) {
+                       $this->db->setLBInfo( 'readOnlyReason', $params['readOnlyReason'] );
+               }
+       }
+
+       /**
+        * @param IDatabase $db Live connection handle
+        * @param array $params Parameter map to LoadBalancerSingle::__constructs()
+        * @return LoadBalancerSingle
+        * @since 1.28
+        */
+       public static function newFromConnection( IDatabase $db, array $params = [] ) {
+               return new static( [ 'connection' => $db ] + $params );
+       }
+
+       /**
+        *
+        * @param string $server
+        * @param bool $dbNameOverride
+        *
+        * @return IDatabase
+        */
+       protected function reallyOpenConnection( $server, $dbNameOverride = false ) {
+               return $this->db;
+       }
+}
index e355c03..14a52c5 100644 (file)
@@ -34,16 +34,19 @@ interface ILoadMonitor extends LoggerAwareInterface {
         * @param ILoadBalancer $lb LoadBalancer this instance serves
         * @param BagOStuff $sCache Local server memory cache
         * @param BagOStuff $cCache Local cluster memory cache
+        * @param array $options Options map
         */
-       public function __construct( ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache );
+       public function __construct(
+               ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache, array $options = []
+       );
 
        /**
-        * Perform pre-connection load ratio adjustment.
-        * @param int[] &$loads
-        * @param string|bool $group The selected query group. Default: false
-        * @param string|bool $domain Default: false
+        * Perform load ratio adjustment before deciding which server to use
+        *
+        * @param int[] &$weightByServer Map of (server index => float weight)
+        * @param string|bool $domain
         */
-       public function scaleLoads( &$loads, $group = false, $domain = false );
+       public function scaleLoads( array &$weightByServer, $domain );
 
        /**
         * Get an estimate of replication lag (in seconds) for each server
@@ -52,14 +55,7 @@ interface ILoadMonitor extends LoggerAwareInterface {
         *
         * @param integer[] $serverIndexes
         * @param string $domain
-        *
         * @return array Map of (server index => float|int|bool)
         */
-       public function getLagTimes( $serverIndexes, $domain );
-
-       /**
-        * Clear any process and persistent cache of lag times
-        * @since 1.27
-        */
-       public function clearCaches();
+       public function getLagTimes( array $serverIndexes, $domain );
 }
index 1da8f4e..499542d 100644 (file)
@@ -37,27 +37,59 @@ class LoadMonitor implements ILoadMonitor {
        /** @var LoggerInterface */
        protected $replLogger;
 
-       public function __construct( ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache ) {
+       /** @var float Moving average ratio (e.g. 0.1 for 10% weight to new weight) */
+       private $movingAveRatio;
+
+       const VERSION = 1; // cache key version
+
+       public function __construct(
+               ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache, array $options = []
+       ) {
                $this->parent = $lb;
                $this->srvCache = $srvCache;
                $this->mainCache = $cache;
                $this->replLogger = new \Psr\Log\NullLogger();
+
+               $this->movingAveRatio = isset( $options['movingAveRatio'] )
+                       ? $options['movingAveRatio']
+                       : 0.1;
        }
 
        public function setLogger( LoggerInterface $logger ) {
                $this->replLogger = $logger;
        }
 
-       public function scaleLoads( &$loads, $group = false, $domain = false ) {
+       public function scaleLoads( array &$weightByServer, $domain ) {
+               $serverIndexes = array_keys( $weightByServer );
+               $states = $this->getServerStates( $serverIndexes, $domain );
+               $coefficientsByServer = $states['weightScales'];
+               foreach ( $weightByServer as $i => $weight ) {
+                       if ( isset( $coefficientsByServer[$i] ) ) {
+                               $weightByServer[$i] = $weight * $coefficientsByServer[$i];
+                       } else { // server recently added to config?
+                               $host = $this->parent->getServerName( $i );
+                               $this->replLogger->error( __METHOD__ . ": host $host not in cache" );
+                       }
+               }
        }
 
-       public function getLagTimes( $serverIndexes, $domain ) {
-               if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
+       public function getLagTimes( array $serverIndexes, $domain ) {
+               $states = $this->getServerStates( $serverIndexes, $domain );
+
+               return $states['lagTimes'];
+       }
+
+       protected function getServerStates( array $serverIndexes, $domain ) {
+               $writerIndex = $this->parent->getWriterIndex();
+               if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == $writerIndex ) {
                        # Single server only, just return zero without caching
-                       return [ 0 => 0 ];
+                       return [
+                               'lagTimes' => [ $writerIndex => 0 ],
+                               'weightScales' => [ $writerIndex => 1.0 ]
+                       ];
                }
 
-               $key = $this->getLagTimeCacheKey();
+               $key = $this->getCacheKey( $serverIndexes );
                # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
                $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
                # Keep keys around longer as fallbacks
@@ -67,7 +99,7 @@ class LoadMonitor implements ILoadMonitor {
                $value = $this->srvCache->get( $key );
                if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
                        $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from local cache" );
-                       return $value['lagTimes']; // cache hit
+                       return $value; // cache hit
                }
                $staleValue = $value ?: false;
 
@@ -77,7 +109,7 @@ class LoadMonitor implements ILoadMonitor {
                        $this->srvCache->set( $key, $value, $staleTTL );
                        $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from main cache" );
 
-                       return $value['lagTimes']; // cache hit
+                       return $value; // cache hit
                }
                $staleValue = $value ?: $staleValue;
 
@@ -91,13 +123,16 @@ class LoadMonitor implements ILoadMonitor {
                        } );
                } elseif ( $staleValue ) {
                        # Could not acquire lock but an old cache exists, so use it
-                       return $staleValue['lagTimes'];
+                       return $staleValue;
                }
 
                $lagTimes = [];
+               $weightScales = [];
+               $movAveRatio = $this->movingAveRatio;
                foreach ( $serverIndexes as $i ) {
                        if ( $i == $this->parent->getWriterIndex() ) {
                                $lagTimes[$i] = 0; // master always has no lag
+                               $weightScales[$i] = 1.0; // nominal weight
                                continue;
                        }
 
@@ -109,17 +144,26 @@ class LoadMonitor implements ILoadMonitor {
                                $close = true; // new connection
                        }
 
+                       $lastWeight = isset( $staleValue['weightScales'][$i] )
+                               ? $staleValue['weightScales'][$i]
+                               : 1.0;
+                       $coefficient = $this->getWeightScale( $i, $conn ?: null );
+                       $newWeight = $movAveRatio * $coefficient + ( 1 - $movAveRatio ) * $lastWeight;
+
+                       // Scale from 10% to 100% of nominal weight
+                       $weightScales[$i] = max( $newWeight, .10 );
+
                        if ( !$conn ) {
                                $lagTimes[$i] = false;
                                $host = $this->parent->getServerName( $i );
-                               $this->replLogger->error( __METHOD__ . ": host $host (#$i) is unreachable" );
+                               $this->replLogger->error( __METHOD__ . ": host $host is unreachable" );
                                continue;
                        }
 
                        $lagTimes[$i] = $conn->getLag();
                        if ( $lagTimes[$i] === false ) {
                                $host = $this->parent->getServerName( $i );
-                               $this->replLogger->error( __METHOD__ . ": host $host (#$i) is not replicating?" );
+                               $this->replLogger->error( __METHOD__ . ": host $host is not replicating?" );
                        }
 
                        if ( $close ) {
@@ -132,26 +176,35 @@ class LoadMonitor implements ILoadMonitor {
                }
 
                # Add a timestamp key so we know when it was cached
-               $value = [ 'lagTimes' => $lagTimes, 'timestamp' => microtime( true ) ];
+               $value = [
+                       'lagTimes' => $lagTimes,
+                       'weightScales' => $weightScales,
+                       'timestamp' => microtime( true )
+               ];
                $this->mainCache->set( $key, $value, $staleTTL );
                $this->srvCache->set( $key, $value, $staleTTL );
                $this->replLogger->info( __METHOD__ . ": re-calculated lag times ($key)" );
 
-               return $value['lagTimes'];
+               return $value;
        }
 
-       public function clearCaches() {
-               $key = $this->getLagTimeCacheKey();
-               $this->srvCache->delete( $key );
-               $this->mainCache->delete( $key );
+       /**
+        * @param integer $index Server index
+        * @param IDatabase|null $conn Connection handle or null on connection failure
+        * @return float
+        */
+       protected function getWeightScale( $index, IDatabase $conn = null ) {
+               return $conn ? 1.0 : 0.0;
        }
 
-       private function getLagTimeCacheKey() {
-               $writerIndex = $this->parent->getWriterIndex();
+       private function getCacheKey( array $serverIndexes ) {
+               sort( $serverIndexes );
                // Lag is per-server, not per-DB, so key on the master DB name
                return $this->srvCache->makeGlobalKey(
                        'lag-times',
-                       $this->parent->getServerName( $writerIndex )
+                       self::VERSION,
+                       $this->parent->getServerName( $this->parent->getWriterIndex() ),
+                       implode( '-', $serverIndexes )
                );
        }
 }
index 7286417..babd609 100644 (file)
  * @ingroup Database
  */
 class LoadMonitorMySQL extends LoadMonitor {
-       public function scaleLoads( &$loads, $group = false, $domain = false ) {
-               // @TODO: maybe use Threads_running/Threads_created ratio to guess load
-               // and Queries/Uptime to guess if a server is warming up the buffer pool
+       /** @var float What buffer pool use ratio counts as "warm" (e.g. 0.5 for 50% usage) */
+       private $warmCacheRatio;
+
+       public function __construct(
+               ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache, array $options = []
+       ) {
+               parent::__construct( $lb, $srvCache, $cache, $options );
+
+               $this->warmCacheRatio = isset( $options['warmCacheRatio'] )
+                       ? $options['warmCacheRatio']
+                       : 0.0;
+       }
+
+       protected function getWeightScale( $index, IDatabase $conn = null ) {
+               if ( !$conn ) {
+                       return 0.0;
+               }
+
+               $weight = 1.0;
+               if ( $this->warmCacheRatio > 0 ) {
+                       $res = $conn->query( 'SHOW STATUS', false );
+                       $s = $res ? $conn->fetchObject( $res ) : false;
+                       if ( $s === false ) {
+                               $host = $this->parent->getServerName( $index );
+                               $this->replLogger->error( __METHOD__ . ": could not get status for $host" );
+                       } else {
+                               // http://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html
+                               if ( $s->Innodb_buffer_pool_pages_total > 0 ) {
+                                       $ratio = $s->Innodb_buffer_pool_pages_data / $s->Innodb_buffer_pool_pages_total;
+                               } elseif ( $s->Qcache_total_blocks > 0 ) {
+                                       $ratio = 1.0 - $s->Qcache_free_blocks / $s->Qcache_total_blocks;
+                               } else {
+                                       $ratio = 1.0;
+                               }
+                               // Stop caring once $ratio >= $this->warmCacheRatio
+                               $weight *= min( $ratio / $this->warmCacheRatio, 1.0 );
+                       }
+               }
+
+               return $weight;
        }
 }
index 8062001..c4e25dc 100644 (file)
 use Psr\Log\LoggerInterface;
 
 class LoadMonitorNull implements ILoadMonitor {
-       public function __construct( ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache ) {
+       public function __construct(
+               ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache, array $options = []
+       ) {
 
        }
 
        public function setLogger( LoggerInterface $logger ) {
        }
 
-       public function scaleLoads( &$loads, $group = false, $domain = false ) {
+       public function scaleLoads( array &$loads, $domain ) {
 
        }
 
-       public function getLagTimes( $serverIndexes, $domain ) {
+       public function getLagTimes( array $serverIndexes, $domain ) {
                return array_fill_keys( $serverIndexes, 0 );
        }
 
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();
+                       }
+               }
+       }
+}
diff --git a/includes/libs/stats/SamplingStatsdClient.php b/includes/libs/stats/SamplingStatsdClient.php
new file mode 100644 (file)
index 0000000..dd1976c
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Copyright 2015
+ *
+ * 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
+ */
+
+use Liuggio\StatsdClient\StatsdClient;
+use Liuggio\StatsdClient\Entity\StatsdData;
+use Liuggio\StatsdClient\Entity\StatsdDataInterface;
+
+/**
+ * A statsd client that applies the sampling rate to the data items before sending them.
+ *
+ * @since 1.26
+ */
+class SamplingStatsdClient extends StatsdClient {
+       protected $samplingRates = [];
+
+       /**
+        * Sampling rates as an associative array of patterns and rates.
+        * Patterns are Unix shell patterns (e.g. 'MediaWiki.api.*').
+        * Rates are sampling probabilities (e.g. 0.1 means 1 in 10 events are sampled).
+        * @param array $samplingRates
+        * @since 1.28
+        */
+       public function setSamplingRates( array $samplingRates ) {
+               $this->samplingRates = $samplingRates;
+       }
+
+       /**
+        * Sets sampling rate for all items in $data.
+        * The sample rate specified in a StatsdData entity overrides the sample rate specified here.
+        *
+        * {@inheritDoc}
+        */
+       public function appendSampleRate( $data, $sampleRate = 1 ) {
+               $samplingRates = $this->samplingRates;
+               if ( !$samplingRates && $sampleRate !== 1 ) {
+                       $samplingRates = [ '*' => $sampleRate ];
+               }
+               if ( $samplingRates ) {
+                       array_walk( $data, function( $item ) use ( $samplingRates ) {
+                               /** @var $item StatsdData */
+                               foreach ( $samplingRates as $pattern => $rate ) {
+                                       if ( fnmatch( $pattern, $item->getKey(), FNM_NOESCAPE ) ) {
+                                               $item->setSampleRate( $item->getSampleRate() * $rate );
+                                               break;
+                                       }
+                               }
+                       } );
+               }
+
+               return $data;
+       }
+
+       /*
+        * Send the metrics over UDP
+        * Sample the metrics according to their sample rate and send the remaining ones.
+        *
+        * @param StatsdDataInterface|StatsdDataInterface[] $data message(s) to sent
+        *        strings are not allowed here as sampleData requires a StatsdDataInterface
+        * @param int $sampleRate
+        *
+        * @return integer the data sent in bytes
+        */
+       public function send( $data, $sampleRate = 1 ) {
+               if ( !is_array( $data ) ) {
+                       $data = [ $data ];
+               }
+               if ( !$data ) {
+                       return;
+               }
+               foreach ( $data as $item ) {
+                       if ( !( $item instanceof StatsdDataInterface ) ) {
+                               throw new InvalidArgumentException(
+                                       'SamplingStatsdClient does not accept stringified messages' );
+                       }
+               }
+
+               // add sampling
+               $data = $this->appendSampleRate( $data, $sampleRate );
+               $data = $this->sampleData( $data );
+
+               $data = array_map( 'strval', $data );
+
+               // reduce number of packets
+               if ( $this->getReducePacket() ) {
+                       $data = $this->reduceCount( $data );
+               }
+
+               // failures in any of this should be silently ignored if ..
+               $written = 0;
+               try {
+                       $fp = $this->getSender()->open();
+                       if ( !$fp ) {
+                               return;
+                       }
+                       foreach ( $data as $message ) {
+                               $written += $this->getSender()->write( $fp, $message );
+                       }
+                       $this->getSender()->close( $fp );
+               } catch ( Exception $e ) {
+                       $this->throwException( $e );
+               }
+
+               return $written;
+       }
+
+       /**
+        * Throw away some of the data according to the sample rate.
+        * @param StatsdDataInterface[] $data
+        * @return StatsdDataInterface[]
+        * @throws LogicException
+        */
+       protected function sampleData( $data ) {
+               $newData = [];
+               $mt_rand_max = mt_getrandmax();
+               foreach ( $data as $item ) {
+                       $samplingRate = $item->getSampleRate();
+                       if ( $samplingRate <= 0.0 || $samplingRate > 1.0 ) {
+                               throw new LogicException( 'Sampling rate shall be within ]0, 1]' );
+                       }
+                       if (
+                               $samplingRate === 1 ||
+                               ( mt_rand() / $mt_rand_max <= $samplingRate )
+                       ) {
+                               $newData[] = $item;
+                       }
+               }
+               return $newData;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       protected function throwException( Exception $exception ) {
+               if ( !$this->getFailSilently() ) {
+                       throw $exception;
+               }
+       }
+}
diff --git a/includes/libs/time/ConvertableTimestamp.php b/includes/libs/time/ConvertableTimestamp.php
deleted file mode 100644 (file)
index af7eca6..0000000
+++ /dev/null
@@ -1,243 +0,0 @@
-<?php
-/**
- * Creation, parsing, and conversion of timestamps.
- *
- * 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
- * @since 1.20
- * @author Tyler Romeo, 2012
- */
-
-/**
- * Library for creating, parsing, and converting timestamps. Based on the JS
- * library that does the same thing.
- *
- * @since 1.28
- */
-class ConvertableTimestamp {
-       /**
-        * Standard gmdate() formats for the different timestamp types.
-        */
-       private static $formats = [
-               TS_UNIX => 'U',
-               TS_MW => 'YmdHis',
-               TS_DB => 'Y-m-d H:i:s',
-               TS_ISO_8601 => 'Y-m-d\TH:i:s\Z',
-               TS_ISO_8601_BASIC => 'Ymd\THis\Z',
-               TS_EXIF => 'Y:m:d H:i:s', // This shouldn't ever be used, but is included for completeness
-               TS_RFC2822 => 'D, d M Y H:i:s',
-               TS_ORACLE => 'd-m-Y H:i:s.000000', // Was 'd-M-y h.i.s A' . ' +00:00' before r51500
-               TS_POSTGRES => 'Y-m-d H:i:s',
-       ];
-
-       /**
-        * The actual timestamp being wrapped (DateTime object).
-        * @var DateTime
-        */
-       public $timestamp;
-
-       /**
-        * Make a new timestamp and set it to the specified time,
-        * or the current time if unspecified.
-        *
-        * @param bool|string|int|float|DateTime $timestamp Timestamp to set, or false for current time
-        */
-       public function __construct( $timestamp = false ) {
-               if ( $timestamp instanceof DateTime ) {
-                       $this->timestamp = $timestamp;
-               } else {
-                       $this->setTimestamp( $timestamp );
-               }
-       }
-
-       /**
-        * Set the timestamp to the specified time, or the current time if unspecified.
-        *
-        * Parse the given timestamp into either a DateTime object or a Unix timestamp,
-        * and then store it.
-        *
-        * @param string|bool $ts Timestamp to store, or false for now
-        * @throws TimestampException
-        */
-       public function setTimestamp( $ts = false ) {
-               $m = [];
-               $da = [];
-               $strtime = '';
-
-               // We want to catch 0, '', null... but not date strings starting with a letter.
-               if ( !$ts || $ts === "\0\0\0\0\0\0\0\0\0\0\0\0\0\0" ) {
-                       $uts = time();
-                       $strtime = "@$uts";
-               } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) {
-                       # TS_DB
-               } elseif ( preg_match( '/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) {
-                       # TS_EXIF
-               } elseif ( preg_match( '/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D', $ts, $da ) ) {
-                       # TS_MW
-               } elseif ( preg_match( '/^(-?\d{1,13})(\.\d+)?$/D', $ts, $m ) ) {
-                       # TS_UNIX
-                       $strtime = "@{$m[1]}"; // http://php.net/manual/en/datetime.formats.compound.php
-               } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) {
-                       # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6
-                       $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3",
-                               str_replace( '+00:00', 'UTC', $ts ) );
-               } elseif ( preg_match(
-                       '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.*\d*)?Z?$/',
-                       $ts,
-                       $da
-               ) ) {
-                       # TS_ISO_8601
-               } elseif ( preg_match(
-                       '/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(?:\.*\d*)?Z?$/',
-                       $ts,
-                       $da
-               ) ) {
-                       # TS_ISO_8601_BASIC
-               } elseif ( preg_match(
-                       '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d*[\+\- ](\d\d)$/',
-                       $ts,
-                       $da
-               ) ) {
-                       # TS_POSTGRES
-               } elseif ( preg_match(
-                       '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d* GMT$/',
-                       $ts,
-                       $da
-               ) ) {
-                       # TS_POSTGRES
-               } elseif ( preg_match(
-               # Day of week
-                       '/^[ \t\r\n]*([A-Z][a-z]{2},[ \t\r\n]*)?' .
-                       # dd Mon yyyy
-                       '\d\d?[ \t\r\n]*[A-Z][a-z]{2}[ \t\r\n]*\d{2}(?:\d{2})?' .
-                       # hh:mm:ss
-                       '[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d/S',
-                       $ts
-               ) ) {
-                       # TS_RFC2822, accepting a trailing comment.
-                       # See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html / r77171
-                       # The regex is a superset of rfc2822 for readability
-                       $strtime = strtok( $ts, ';' );
-               } elseif ( preg_match( '/^[A-Z][a-z]{5,8}, \d\d-[A-Z][a-z]{2}-\d{2} \d\d:\d\d:\d\d/', $ts ) ) {
-                       # TS_RFC850
-                       $strtime = $ts;
-               } elseif ( preg_match( '/^[A-Z][a-z]{2} [A-Z][a-z]{2} +\d{1,2} \d\d:\d\d:\d\d \d{4}/', $ts ) ) {
-                       # asctime
-                       $strtime = $ts;
-               } else {
-                       throw new TimestampException( __METHOD__ . ": Invalid timestamp - $ts" );
-               }
-
-               if ( !$strtime ) {
-                       $da = array_map( 'intval', $da );
-                       $da[0] = "%04d-%02d-%02dT%02d:%02d:%02d.00+00:00";
-                       $strtime = call_user_func_array( "sprintf", $da );
-               }
-
-               try {
-                       $final = new DateTime( $strtime, new DateTimeZone( 'GMT' ) );
-               } catch ( Exception $e ) {
-                       throw new TimestampException( __METHOD__ . ': Invalid timestamp format.', $e->getCode(), $e );
-               }
-
-               if ( $final === false ) {
-                       throw new TimestampException( __METHOD__ . ': Invalid timestamp format.' );
-               }
-
-               $this->timestamp = $final;
-       }
-
-       /**
-        * Get the timestamp represented by this object in a certain form.
-        *
-        * Convert the internal timestamp to the specified format and then
-        * return it.
-        *
-        * @param int $style Constant Output format for timestamp
-        * @throws TimestampException
-        * @return string The formatted timestamp
-        */
-       public function getTimestamp( $style = TS_UNIX ) {
-               if ( !isset( self::$formats[$style] ) ) {
-                       throw new TimestampException( __METHOD__ . ': Illegal timestamp output type.' );
-               }
-
-               $output = $this->timestamp->format( self::$formats[$style] );
-
-               if ( ( $style == TS_RFC2822 ) || ( $style == TS_POSTGRES ) ) {
-                       $output .= ' GMT';
-               }
-
-               if ( $style == TS_MW && strlen( $output ) !== 14 ) {
-                       throw new TimestampException( __METHOD__ . ': The timestamp cannot be represented in ' .
-                               'the specified format' );
-               }
-
-               return $output;
-       }
-
-       /**
-        * @return string
-        */
-       public function __toString() {
-               return $this->getTimestamp();
-       }
-
-       /**
-        * Calculate the difference between two ConvertableTimestamp objects.
-        *
-        * @param ConvertableTimestamp $relativeTo Base time to calculate difference from
-        * @return DateInterval|bool The DateInterval object representing the
-        *   difference between the two dates or false on failure
-        */
-       public function diff( ConvertableTimestamp $relativeTo ) {
-               return $this->timestamp->diff( $relativeTo->timestamp );
-       }
-
-       /**
-        * Set the timezone of this timestamp to the specified timezone.
-        *
-        * @param string $timezone Timezone to set
-        * @throws TimestampException
-        */
-       public function setTimezone( $timezone ) {
-               try {
-                       $this->timestamp->setTimezone( new DateTimeZone( $timezone ) );
-               } catch ( Exception $e ) {
-                       throw new TimestampException( __METHOD__ . ': Invalid timezone.', $e->getCode(), $e );
-               }
-       }
-
-       /**
-        * Get the timezone of this timestamp.
-        *
-        * @return DateTimeZone The timezone
-        */
-       public function getTimezone() {
-               return $this->timestamp->getTimezone();
-       }
-
-       /**
-        * Format the timestamp in a given format.
-        *
-        * @param string $format Pattern to format in
-        * @return string The formatted timestamp
-        */
-       public function format( $format ) {
-               return $this->timestamp->format( $format );
-       }
-}
diff --git a/includes/libs/time/ConvertibleTimestamp.php b/includes/libs/time/ConvertibleTimestamp.php
new file mode 100644 (file)
index 0000000..b02985a
--- /dev/null
@@ -0,0 +1,269 @@
+<?php
+/**
+ * Creation, parsing, and conversion of timestamps.
+ *
+ * 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
+ * @since 1.20
+ * @author Tyler Romeo, 2012
+ */
+
+/**
+ * Library for creating, parsing, and converting timestamps. Based on the JS
+ * library that does the same thing.
+ *
+ * @since 1.28
+ */
+class ConvertibleTimestamp {
+       /**
+        * Standard gmdate() formats for the different timestamp types.
+        */
+       private static $formats = [
+               TS_UNIX => 'U',
+               TS_MW => 'YmdHis',
+               TS_DB => 'Y-m-d H:i:s',
+               TS_ISO_8601 => 'Y-m-d\TH:i:s\Z',
+               TS_ISO_8601_BASIC => 'Ymd\THis\Z',
+               TS_EXIF => 'Y:m:d H:i:s', // This shouldn't ever be used, but is included for completeness
+               TS_RFC2822 => 'D, d M Y H:i:s',
+               TS_ORACLE => 'd-m-Y H:i:s.000000', // Was 'd-M-y h.i.s A' . ' +00:00' before r51500
+               TS_POSTGRES => 'Y-m-d H:i:s',
+       ];
+
+       /**
+        * The actual timestamp being wrapped (DateTime object).
+        * @var DateTime
+        */
+       public $timestamp;
+
+       /**
+        * Make a new timestamp and set it to the specified time,
+        * or the current time if unspecified.
+        *
+        * @param bool|string|int|float|DateTime $timestamp Timestamp to set, or false for current time
+        */
+       public function __construct( $timestamp = false ) {
+               if ( $timestamp instanceof DateTime ) {
+                       $this->timestamp = $timestamp;
+               } else {
+                       $this->setTimestamp( $timestamp );
+               }
+       }
+
+       /**
+        * Set the timestamp to the specified time, or the current time if unspecified.
+        *
+        * Parse the given timestamp into either a DateTime object or a Unix timestamp,
+        * and then store it.
+        *
+        * @param string|bool $ts Timestamp to store, or false for now
+        * @throws TimestampException
+        */
+       public function setTimestamp( $ts = false ) {
+               $m = [];
+               $da = [];
+               $strtime = '';
+
+               // We want to catch 0, '', null... but not date strings starting with a letter.
+               if ( !$ts || $ts === "\0\0\0\0\0\0\0\0\0\0\0\0\0\0" ) {
+                       $uts = time();
+                       $strtime = "@$uts";
+               } elseif ( preg_match( '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) {
+                       # TS_DB
+               } elseif ( preg_match( '/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D', $ts, $da ) ) {
+                       # TS_EXIF
+               } elseif ( preg_match( '/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D', $ts, $da ) ) {
+                       # TS_MW
+               } elseif ( preg_match( '/^(-?\d{1,13})(\.\d+)?$/D', $ts, $m ) ) {
+                       # TS_UNIX
+                       $strtime = "@{$m[1]}"; // http://php.net/manual/en/datetime.formats.compound.php
+               } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) {
+                       # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6
+                       $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3",
+                               str_replace( '+00:00', 'UTC', $ts ) );
+               } elseif ( preg_match(
+                       '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.*\d*)?Z?$/',
+                       $ts,
+                       $da
+               ) ) {
+                       # TS_ISO_8601
+               } elseif ( preg_match(
+                       '/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(?:\.*\d*)?Z?$/',
+                       $ts,
+                       $da
+               ) ) {
+                       # TS_ISO_8601_BASIC
+               } elseif ( preg_match(
+                       '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d*[\+\- ](\d\d)$/',
+                       $ts,
+                       $da
+               ) ) {
+                       # TS_POSTGRES
+               } elseif ( preg_match(
+                       '/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)\.*\d* GMT$/',
+                       $ts,
+                       $da
+               ) ) {
+                       # TS_POSTGRES
+               } elseif ( preg_match(
+               # Day of week
+                       '/^[ \t\r\n]*([A-Z][a-z]{2},[ \t\r\n]*)?' .
+                       # dd Mon yyyy
+                       '\d\d?[ \t\r\n]*[A-Z][a-z]{2}[ \t\r\n]*\d{2}(?:\d{2})?' .
+                       # hh:mm:ss
+                       '[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d/S',
+                       $ts
+               ) ) {
+                       # TS_RFC2822, accepting a trailing comment.
+                       # See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html / r77171
+                       # The regex is a superset of rfc2822 for readability
+                       $strtime = strtok( $ts, ';' );
+               } elseif ( preg_match( '/^[A-Z][a-z]{5,8}, \d\d-[A-Z][a-z]{2}-\d{2} \d\d:\d\d:\d\d/', $ts ) ) {
+                       # TS_RFC850
+                       $strtime = $ts;
+               } elseif ( preg_match( '/^[A-Z][a-z]{2} [A-Z][a-z]{2} +\d{1,2} \d\d:\d\d:\d\d \d{4}/', $ts ) ) {
+                       # asctime
+                       $strtime = $ts;
+               } else {
+                       throw new TimestampException( __METHOD__ . ": Invalid timestamp - $ts" );
+               }
+
+               if ( !$strtime ) {
+                       $da = array_map( 'intval', $da );
+                       $da[0] = "%04d-%02d-%02dT%02d:%02d:%02d.00+00:00";
+                       $strtime = call_user_func_array( "sprintf", $da );
+               }
+
+               try {
+                       $final = new DateTime( $strtime, new DateTimeZone( 'GMT' ) );
+               } catch ( Exception $e ) {
+                       throw new TimestampException( __METHOD__ . ': Invalid timestamp format.', $e->getCode(), $e );
+               }
+
+               if ( $final === false ) {
+                       throw new TimestampException( __METHOD__ . ': Invalid timestamp format.' );
+               }
+
+               $this->timestamp = $final;
+       }
+
+       /**
+        * Convert a timestamp string to a given format.
+        *
+        * @param int $style Constant Output format for timestamp
+        * @param string $ts Timestamp
+        * @return string|bool Formatted timestamp or false on failure
+        */
+       public static function convert( $style = TS_UNIX, $ts ) {
+               try {
+                       $ct = new static( $ts );
+                       return $ct->getTimestamp( $style );
+               } catch ( TimestampException $e ) {
+                       return false;
+               }
+       }
+
+       /**
+        * Get the current time in the given format
+        *
+        * @param int $style Constant Output format for timestamp
+        * @return string
+        */
+       public static function now( $style = TS_MW ) {
+               return static::convert( $style, time() );
+       }
+
+       /**
+        * Get the timestamp represented by this object in a certain form.
+        *
+        * Convert the internal timestamp to the specified format and then
+        * return it.
+        *
+        * @param int $style Constant Output format for timestamp
+        * @throws TimestampException
+        * @return string The formatted timestamp
+        */
+       public function getTimestamp( $style = TS_UNIX ) {
+               if ( !isset( self::$formats[$style] ) ) {
+                       throw new TimestampException( __METHOD__ . ': Illegal timestamp output type.' );
+               }
+
+               $output = $this->timestamp->format( self::$formats[$style] );
+
+               if ( ( $style == TS_RFC2822 ) || ( $style == TS_POSTGRES ) ) {
+                       $output .= ' GMT';
+               }
+
+               if ( $style == TS_MW && strlen( $output ) !== 14 ) {
+                       throw new TimestampException( __METHOD__ . ': The timestamp cannot be represented in ' .
+                               'the specified format' );
+               }
+
+               return $output;
+       }
+
+       /**
+        * @return string
+        */
+       public function __toString() {
+               return $this->getTimestamp();
+       }
+
+       /**
+        * Calculate the difference between two ConvertibleTimestamp objects.
+        *
+        * @param ConvertibleTimestamp $relativeTo Base time to calculate difference from
+        * @return DateInterval|bool The DateInterval object representing the
+        *   difference between the two dates or false on failure
+        */
+       public function diff( ConvertibleTimestamp $relativeTo ) {
+               return $this->timestamp->diff( $relativeTo->timestamp );
+       }
+
+       /**
+        * Set the timezone of this timestamp to the specified timezone.
+        *
+        * @param string $timezone Timezone to set
+        * @throws TimestampException
+        */
+       public function setTimezone( $timezone ) {
+               try {
+                       $this->timestamp->setTimezone( new DateTimeZone( $timezone ) );
+               } catch ( Exception $e ) {
+                       throw new TimestampException( __METHOD__ . ': Invalid timezone.', $e->getCode(), $e );
+               }
+       }
+
+       /**
+        * Get the timezone of this timestamp.
+        *
+        * @return DateTimeZone The timezone
+        */
+       public function getTimezone() {
+               return $this->timestamp->getTimezone();
+       }
+
+       /**
+        * Format the timestamp in a given format.
+        *
+        * @param string $format Pattern to format in
+        * @return string The formatted timestamp
+        */
+       public function format( $format ) {
+               return $this->timestamp->format( $format );
+       }
+}
index 0864e5c..1b7545a 100644 (file)
@@ -304,7 +304,7 @@ class VirtualRESTServiceClient {
         */
        private function getInstance( $prefix ) {
                if ( !isset( $this->instances[$prefix] ) ) {
-                       throw new RunTimeException( "No service registered at prefix '{$prefix}'." );
+                       throw new RuntimeException( "No service registered at prefix '{$prefix}'." );
                }
 
                if ( !( $this->instances[$prefix] instanceof VirtualRESTService ) ) {
diff --git a/includes/libs/xmp/XMP.php b/includes/libs/xmp/XMP.php
new file mode 100644 (file)
index 0000000..70f67b7
--- /dev/null
@@ -0,0 +1,1383 @@
+<?php
+/**
+ * Reader for XMP data containing properties relevant to images.
+ *
+ * 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 Media
+ */
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * Class for reading xmp data containing properties relevant to
+ * images, and spitting out an array that FormatMetadata accepts.
+ *
+ * Note, this is not meant to recognize every possible thing you can
+ * encode in XMP. It should recognize all the properties we want.
+ * For example it doesn't have support for structures with multiple
+ * nesting levels, as none of the properties we're supporting use that
+ * feature. If it comes across properties it doesn't recognize, it should
+ * ignore them.
+ *
+ * The public methods one would call in this class are
+ * - parse( $content )
+ *    Reads in xmp content.
+ *    Can potentially be called multiple times with partial data each time.
+ * - parseExtended( $content )
+ *    Reads XMPExtended blocks (jpeg files only).
+ * - getResults
+ *    Outputs a results array.
+ *
+ * Note XMP kind of looks like rdf. They are not the same thing - XMP is
+ * encoded as a specific subset of rdf. This class can read XMP. It cannot
+ * read rdf.
+ *
+ */
+class XMPReader implements LoggerAwareInterface {
+       /** @var array XMP item configuration array */
+       protected $items;
+
+       /** @var array Array to hold the current element (and previous element, and so on) */
+       private $curItem = [];
+
+       /** @var bool|string The structure name when processing nested structures. */
+       private $ancestorStruct = false;
+
+       /** @var bool|string Temporary holder for character data that appears in xmp doc. */
+       private $charContent = false;
+
+       /** @var array Stores the state the xmpreader is in (see MODE_FOO constants) */
+       private $mode = [];
+
+       /** @var array Array to hold results */
+       private $results = [];
+
+       /** @var bool If we're doing a seq or bag. */
+       private $processingArray = false;
+
+       /** @var bool|string Used for lang alts only */
+       private $itemLang = false;
+
+       /** @var resource A resource handle for the XML parser */
+       private $xmlParser;
+
+       /** @var bool|string Character set like 'UTF-8' */
+       private $charset = false;
+
+       /** @var int */
+       private $extendedXMPOffset = 0;
+
+       /** @var int Flag determining if the XMP is safe to parse **/
+       private $parsable = 0;
+
+       /** @var string Buffer of XML to parse **/
+       private $xmlParsableBuffer = '';
+
+       /**
+        * These are various mode constants.
+        * they are used to figure out what to do
+        * with an element when its encountered.
+        *
+        * For example, MODE_IGNORE is used when processing
+        * a property we're not interested in. So if a new
+        * element pops up when we're in that mode, we ignore it.
+        */
+       const MODE_INITIAL = 0;
+       const MODE_IGNORE = 1;
+       const MODE_LI = 2;
+       const MODE_LI_LANG = 3;
+       const MODE_QDESC = 4;
+
+       // The following MODE constants are also used in the
+       // $items array to denote what type of property the item is.
+       const MODE_SIMPLE = 10;
+       const MODE_STRUCT = 11; // structure (associative array)
+       const MODE_SEQ = 12; // ordered list
+       const MODE_BAG = 13; // unordered list
+       const MODE_LANG = 14;
+       const MODE_ALT = 15; // non-language alt. Currently not implemented, and not needed atm.
+       const MODE_BAGSTRUCT = 16; // A BAG of Structs.
+
+       const NS_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
+       const NS_XML = 'http://www.w3.org/XML/1998/namespace';
+
+       // States used while determining if XML is safe to parse
+       const PARSABLE_UNKNOWN = 0;
+       const PARSABLE_OK = 1;
+       const PARSABLE_BUFFERING = 2;
+       const PARSABLE_NO = 3;
+
+       /**
+        * @var LoggerInterface
+        */
+       private $logger;
+
+       /**
+        * Constructor.
+        *
+        * Primary job is to initialize the XMLParser
+        */
+       function __construct( LoggerInterface $logger = null ) {
+
+               if ( !function_exists( 'xml_parser_create_ns' ) ) {
+                       // this should already be checked by this point
+                       throw new RuntimeException( 'XMP support requires XML Parser' );
+               }
+               if ( $logger ) {
+                       $this->setLogger( $logger );
+               } else {
+                       $this->setLogger( new NullLogger() );
+               }
+
+               $this->items = XMPInfo::getItems();
+
+               $this->resetXMLParser();
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * free the XML parser.
+        *
+        * @note It is unclear to me if we really need to do this ourselves
+        *  or if php garbage collection will automatically free the xmlParser
+        *  when it is no longer needed.
+        */
+       private function destroyXMLParser() {
+               if ( $this->xmlParser ) {
+                       xml_parser_free( $this->xmlParser );
+                       $this->xmlParser = null;
+               }
+       }
+
+       /**
+        * Main use is if a single item has multiple xmp documents describing it.
+        * For example in jpeg's with extendedXMP
+        */
+       private function resetXMLParser() {
+
+               $this->destroyXMLParser();
+
+               $this->xmlParser = xml_parser_create_ns( 'UTF-8', ' ' );
+               xml_parser_set_option( $this->xmlParser, XML_OPTION_CASE_FOLDING, 0 );
+               xml_parser_set_option( $this->xmlParser, XML_OPTION_SKIP_WHITE, 1 );
+
+               xml_set_element_handler( $this->xmlParser,
+                       [ $this, 'startElement' ],
+                       [ $this, 'endElement' ] );
+
+               xml_set_character_data_handler( $this->xmlParser, [ $this, 'char' ] );
+
+               $this->parsable = self::PARSABLE_UNKNOWN;
+               $this->xmlParsableBuffer = '';
+       }
+
+       /**
+        * Check if this instance supports using this class
+        */
+       public static function isSupported() {
+               return function_exists( 'xml_parser_create_ns' ) && class_exists( 'XMLReader' );
+       }
+
+       /** Get the result array. Do some post-processing before returning
+        * the array, and transform any metadata that is special-cased.
+        *
+        * @return array Array of results as an array of arrays suitable for
+        *    FormatMetadata::getFormattedData().
+        */
+       public function getResults() {
+               // xmp-special is for metadata that affects how stuff
+               // is extracted. For example xmpNote:HasExtendedXMP.
+
+               // It is also used to handle photoshop:AuthorsPosition
+               // which is weird and really part of another property,
+               // see 2:85 in IPTC. See also pg 21 of IPTC4XMP standard.
+               // The location fields also use it.
+
+               $data = $this->results;
+
+               if ( isset( $data['xmp-special']['AuthorsPosition'] )
+                       && is_string( $data['xmp-special']['AuthorsPosition'] )
+                       && isset( $data['xmp-general']['Artist'][0] )
+               ) {
+                       // Note, if there is more than one creator,
+                       // this only applies to first. This also will
+                       // only apply to the dc:Creator prop, not the
+                       // exif:Artist prop.
+
+                       $data['xmp-general']['Artist'][0] =
+                               $data['xmp-special']['AuthorsPosition'] . ', '
+                               . $data['xmp-general']['Artist'][0];
+               }
+
+               // Go through the LocationShown and LocationCreated
+               // changing it to the non-hierarchal form used by
+               // the other location fields.
+
+               if ( isset( $data['xmp-special']['LocationShown'][0] )
+                       && is_array( $data['xmp-special']['LocationShown'][0] )
+               ) {
+                       // the is_array is just paranoia. It should always
+                       // be an array.
+                       foreach ( $data['xmp-special']['LocationShown'] as $loc ) {
+                               if ( !is_array( $loc ) ) {
+                                       // To avoid copying over the _type meta-fields.
+                                       continue;
+                               }
+                               foreach ( $loc as $field => $val ) {
+                                       $data['xmp-general'][$field . 'Dest'][] = $val;
+                               }
+                       }
+               }
+               if ( isset( $data['xmp-special']['LocationCreated'][0] )
+                       && is_array( $data['xmp-special']['LocationCreated'][0] )
+               ) {
+                       // the is_array is just paranoia. It should always
+                       // be an array.
+                       foreach ( $data['xmp-special']['LocationCreated'] as $loc ) {
+                               if ( !is_array( $loc ) ) {
+                                       // To avoid copying over the _type meta-fields.
+                                       continue;
+                               }
+                               foreach ( $loc as $field => $val ) {
+                                       $data['xmp-general'][$field . 'Created'][] = $val;
+                               }
+                       }
+               }
+
+               // We don't want to return the special values, since they're
+               // special and not info to be stored about the file.
+               unset( $data['xmp-special'] );
+
+               // Convert GPSAltitude to negative if below sea level.
+               if ( isset( $data['xmp-exif']['GPSAltitudeRef'] )
+                       && isset( $data['xmp-exif']['GPSAltitude'] )
+               ) {
+
+                       // Must convert to a real before multiplying by -1
+                       // XMPValidate guarantees there will always be a '/' in this value.
+                       list( $nom, $denom ) = explode( '/', $data['xmp-exif']['GPSAltitude'] );
+                       $data['xmp-exif']['GPSAltitude'] = $nom / $denom;
+
+                       if ( $data['xmp-exif']['GPSAltitudeRef'] == '1' ) {
+                               $data['xmp-exif']['GPSAltitude'] *= -1;
+                       }
+                       unset( $data['xmp-exif']['GPSAltitudeRef'] );
+               }
+
+               return $data;
+       }
+
+       /**
+        * Main function to call to parse XMP. Use getResults to
+        * get results.
+        *
+        * Also catches any errors during processing, writes them to
+        * debug log, blanks result array and returns false.
+        *
+        * @param string $content XMP data
+        * @param bool $allOfIt If this is all the data (true) or if its split up (false). Default true
+        * @throws RuntimeException
+        * @return bool Success.
+        */
+       public function parse( $content, $allOfIt = true ) {
+               if ( !$this->xmlParser ) {
+                       $this->resetXMLParser();
+               }
+               try {
+
+                       // detect encoding by looking for BOM which is supposed to be in processing instruction.
+                       // see page 12 of http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart3.pdf
+                       if ( !$this->charset ) {
+                               $bom = [];
+                               if ( preg_match( '/\xEF\xBB\xBF|\xFE\xFF|\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\xFF\xFE/',
+                                       $content, $bom )
+                               ) {
+                                       switch ( $bom[0] ) {
+                                               case "\xFE\xFF":
+                                                       $this->charset = 'UTF-16BE';
+                                                       break;
+                                               case "\xFF\xFE":
+                                                       $this->charset = 'UTF-16LE';
+                                                       break;
+                                               case "\x00\x00\xFE\xFF":
+                                                       $this->charset = 'UTF-32BE';
+                                                       break;
+                                               case "\xFF\xFE\x00\x00":
+                                                       $this->charset = 'UTF-32LE';
+                                                       break;
+                                               case "\xEF\xBB\xBF":
+                                                       $this->charset = 'UTF-8';
+                                                       break;
+                                               default:
+                                                       // this should be impossible to get to
+                                                       throw new RuntimeException( "Invalid BOM" );
+                                       }
+                               } else {
+                                       // standard specifically says, if no bom assume utf-8
+                                       $this->charset = 'UTF-8';
+                               }
+                       }
+                       if ( $this->charset !== 'UTF-8' ) {
+                               // don't convert if already utf-8
+                               MediaWiki\suppressWarnings();
+                               $content = iconv( $this->charset, 'UTF-8//IGNORE', $content );
+                               MediaWiki\restoreWarnings();
+                       }
+
+                       // Ensure the XMP block does not have an xml doctype declaration, which
+                       // could declare entities unsafe to parse with xml_parse (T85848/T71210).
+                       if ( $this->parsable !== self::PARSABLE_OK ) {
+                               if ( $this->parsable === self::PARSABLE_NO ) {
+                                       throw new RuntimeException( 'Unsafe doctype declaration in XML.' );
+                               }
+
+                               $content = $this->xmlParsableBuffer . $content;
+                               if ( !$this->checkParseSafety( $content ) ) {
+                                       if ( !$allOfIt && $this->parsable !== self::PARSABLE_NO ) {
+                                               // parse wasn't Unsuccessful yet, so return true
+                                               // in this case.
+                                               return true;
+                                       }
+                                       $msg = ( $this->parsable === self::PARSABLE_NO ) ?
+                                               'Unsafe doctype declaration in XML.' :
+                                               'No root element found in XML.';
+                                       throw new RuntimeException( $msg );
+                               }
+                       }
+
+                       $ok = xml_parse( $this->xmlParser, $content, $allOfIt );
+                       if ( !$ok ) {
+                               $code = xml_get_error_code( $this->xmlParser );
+                               $error = xml_error_string( $code );
+                               $line = xml_get_current_line_number( $this->xmlParser );
+                               $col = xml_get_current_column_number( $this->xmlParser );
+                               $offset = xml_get_current_byte_index( $this->xmlParser );
+
+                               $this->logger->warning(
+                                       '{method} : Error reading XMP content: {error} ' .
+                                       '(line: {line} column: {column} byte offset: {offset})',
+                                       [
+                                               'method' => __METHOD__,
+                                               'error_code' => $code,
+                                               'error' => $error,
+                                               'line' => $line,
+                                               'column' => $col,
+                                               'offset' => $offset,
+                                               'content' => $content,
+                               ] );
+                               $this->results = []; // blank if error.
+                               $this->destroyXMLParser();
+                               return false;
+                       }
+               } catch ( Exception $e ) {
+                       $this->logger->warning(
+                               '{method} Exception caught while parsing: ' . $e->getMessage(),
+                               [
+                                       'method' => __METHOD__,
+                                       'exception' => $e,
+                                       'content' => $content,
+                               ]
+                       );
+                       $this->results = [];
+                       return false;
+               }
+               if ( $allOfIt ) {
+                       $this->destroyXMLParser();
+               }
+
+               return true;
+       }
+
+       /** Entry point for XMPExtended blocks in jpeg files
+        *
+        * @todo In serious need of testing
+        * @see http://www.adobe.ge/devnet/xmp/pdfs/XMPSpecificationPart3.pdf XMP spec part 3 page 20
+        * @param string $content XMPExtended block minus the namespace signature
+        * @return bool If it succeeded.
+        */
+       public function parseExtended( $content ) {
+               // @todo FIXME: This is untested. Hard to find example files
+               // or programs that make such files..
+               $guid = substr( $content, 0, 32 );
+               if ( !isset( $this->results['xmp-special']['HasExtendedXMP'] )
+                       || $this->results['xmp-special']['HasExtendedXMP'] !== $guid
+               ) {
+                       $this->logger->info( __METHOD__ .
+                               " Ignoring XMPExtended block due to wrong guid (guid= '$guid')" );
+
+                       return false;
+               }
+               $len = unpack( 'Nlength/Noffset', substr( $content, 32, 8 ) );
+
+               if ( !$len ||
+                       $len['length'] < 4 ||
+                       $len['offset'] < 0 ||
+                       $len['offset'] > $len['length']
+               ) {
+                       $this->logger->info(
+                               __METHOD__ . 'Error reading extended XMP block, invalid length or offset.'
+                       );
+
+                       return false;
+               }
+
+               // we're not very robust here. we should accept it in the wrong order.
+               // To quote the XMP standard:
+               // "A JPEG writer should write the ExtendedXMP marker segments in order,
+               // immediately following the StandardXMP. However, the JPEG standard
+               // does not require preservation of marker segment order. A robust JPEG
+               // reader should tolerate the marker segments in any order."
+               // On the other hand, the probability that an image will have more than
+               // 128k of metadata is rather low... so the probability that it will have
+               // > 128k, and be in the wrong order is very low...
+
+               if ( $len['offset'] !== $this->extendedXMPOffset ) {
+                       $this->logger->info( __METHOD__ . 'Ignoring XMPExtended block due to wrong order. (Offset was '
+                               . $len['offset'] . ' but expected ' . $this->extendedXMPOffset . ')' );
+
+                       return false;
+               }
+
+               if ( $len['offset'] === 0 ) {
+                       // if we're starting the extended block, we've probably already
+                       // done the XMPStandard block, so reset.
+                       $this->resetXMLParser();
+               }
+
+               $this->extendedXMPOffset += $len['length'];
+
+               $actualContent = substr( $content, 40 );
+
+               if ( $this->extendedXMPOffset === strlen( $actualContent ) ) {
+                       $atEnd = true;
+               } else {
+                       $atEnd = false;
+               }
+
+               $this->logger->debug( __METHOD__ . 'Parsing a XMPExtended block' );
+
+               return $this->parse( $actualContent, $atEnd );
+       }
+
+       /**
+        * Character data handler
+        * Called whenever character data is found in the xmp document.
+        *
+        * does nothing if we're in MODE_IGNORE or if the data is whitespace
+        * throws an error if we're not in MODE_SIMPLE (as we're not allowed to have character
+        * data in the other modes).
+        *
+        * As an example, this happens when we encounter XMP like:
+        * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
+        * and are processing the 0/10 bit.
+        *
+        * @param XMLParser $parser XMLParser reference to the xml parser
+        * @param string $data Character data
+        * @throws RuntimeException On invalid data
+        */
+       function char( $parser, $data ) {
+
+               $data = trim( $data );
+               if ( trim( $data ) === "" ) {
+                       return;
+               }
+
+               if ( !isset( $this->mode[0] ) ) {
+                       throw new RuntimeException( 'Unexpected character data before first rdf:Description element' );
+               }
+
+               if ( $this->mode[0] === self::MODE_IGNORE ) {
+                       return;
+               }
+
+               if ( $this->mode[0] !== self::MODE_SIMPLE
+                       && $this->mode[0] !== self::MODE_QDESC
+               ) {
+                       throw new RuntimeException( 'character data where not expected. (mode ' . $this->mode[0] . ')' );
+               }
+
+               // to check, how does this handle w.s.
+               if ( $this->charContent === false ) {
+                       $this->charContent = $data;
+               } else {
+                       $this->charContent .= $data;
+               }
+       }
+
+       /**
+        * Check if a block of XML is safe to pass to xml_parse, i.e. doesn't
+        * contain a doctype declaration which could contain a dos attack if we
+        * parse it and expand internal entities (T85848).
+        *
+        * @param string $content xml string to check for parse safety
+        * @return bool true if the xml is safe to parse, false otherwise
+        */
+       private function checkParseSafety( $content ) {
+               $reader = new XMLReader();
+               $result = null;
+
+               // For XMLReader to parse incomplete/invalid XML, it has to be open()'ed
+               // instead of using XML().
+               $reader->open(
+                       'data://text/plain,' . urlencode( $content ),
+                       null,
+                       LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET
+               );
+
+               $oldDisable = libxml_disable_entity_loader( true );
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $reset = new ScopedCallback(
+                       'libxml_disable_entity_loader',
+                       [ $oldDisable ]
+               );
+               $reader->setParserProperty( XMLReader::SUBST_ENTITIES, false );
+
+               // Even with LIBXML_NOWARNING set, XMLReader::read gives a warning
+               // when parsing truncated XML, which causes unit tests to fail.
+               MediaWiki\suppressWarnings();
+               while ( $reader->read() ) {
+                       if ( $reader->nodeType === XMLReader::ELEMENT ) {
+                               // Reached the first element without hitting a doctype declaration
+                               $this->parsable = self::PARSABLE_OK;
+                               $result = true;
+                               break;
+                       }
+                       if ( $reader->nodeType === XMLReader::DOC_TYPE ) {
+                               $this->parsable = self::PARSABLE_NO;
+                               $result = false;
+                               break;
+                       }
+               }
+               MediaWiki\restoreWarnings();
+
+               if ( !is_null( $result ) ) {
+                       return $result;
+               }
+
+               // Reached the end of the parsable xml without finding an element
+               // or doctype. Buffer and try again.
+               $this->parsable = self::PARSABLE_BUFFERING;
+               $this->xmlParsableBuffer = $content;
+               return false;
+       }
+
+       /** When we hit a closing element in MODE_IGNORE
+        * Check to see if this is the element we started to ignore,
+        * in which case we get out of MODE_IGNORE
+        *
+        * @param string $elm Namespace of element followed by a space and then tag name of element.
+        */
+       private function endElementModeIgnore( $elm ) {
+               if ( $this->curItem[0] === $elm ) {
+                       array_shift( $this->curItem );
+                       array_shift( $this->mode );
+               }
+       }
+
+       /**
+        * Hit a closing element when in MODE_SIMPLE.
+        * This generally means that we finished processing a
+        * property value, and now have to save the result to the
+        * results array
+        *
+        * For example, when processing:
+        * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
+        * this deals with when we hit </exif:DigitalZoomRatio>.
+        *
+        * Or it could be if we hit the end element of a property
+        * of a compound data structure (like a member of an array).
+        *
+        * @param string $elm Namespace, space, and tag name.
+        */
+       private function endElementModeSimple( $elm ) {
+               if ( $this->charContent !== false ) {
+                       if ( $this->processingArray ) {
+                               // if we're processing an array, use the original element
+                               // name instead of rdf:li.
+                               list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
+                       } else {
+                               list( $ns, $tag ) = explode( ' ', $elm, 2 );
+                       }
+                       $this->saveValue( $ns, $tag, $this->charContent );
+
+                       $this->charContent = false; // reset
+               }
+               array_shift( $this->curItem );
+               array_shift( $this->mode );
+       }
+
+       /**
+        * Hit a closing element in MODE_STRUCT, MODE_SEQ, MODE_BAG
+        * generally means we've finished processing a nested structure.
+        * resets some internal variables to indicate that.
+        *
+        * Note this means we hit the closing element not the "</rdf:Seq>".
+        *
+        * @par For example, when processing:
+        * @code{,xml}
+        * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
+        *   </rdf:Seq> </exif:ISOSpeedRatings>
+        * @endcode
+        *
+        * This method is called when we hit the "</exif:ISOSpeedRatings>" tag.
+        *
+        * @param string $elm Namespace . space . tag name.
+        * @throws RuntimeException
+        */
+       private function endElementNested( $elm ) {
+
+               /* cur item must be the same as $elm, unless if in MODE_STRUCT
+                  in which case it could also be rdf:Description */
+               if ( $this->curItem[0] !== $elm
+                       && !( $elm === self::NS_RDF . ' Description'
+                               && $this->mode[0] === self::MODE_STRUCT )
+               ) {
+                       throw new RuntimeException( "nesting mismatch. got a </$elm> but expected a </" .
+                               $this->curItem[0] . '>' );
+               }
+
+               // Validate structures.
+               list( $ns, $tag ) = explode( ' ', $elm, 2 );
+               if ( isset( $this->items[$ns][$tag]['validate'] ) ) {
+                       $info =& $this->items[$ns][$tag];
+                       $finalName = isset( $info['map_name'] )
+                               ? $info['map_name'] : $tag;
+
+                       if ( is_array( $info['validate'] ) ) {
+                               $validate = $info['validate'];
+                       } else {
+                               $validator = new XMPValidate( $this->logger );
+                               $validate = [ $validator, $info['validate'] ];
+                       }
+
+                       if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) {
+                               // This can happen if all the members of the struct failed validation.
+                               $this->logger->debug( __METHOD__ . " <$ns:$tag> has no valid members." );
+                       } elseif ( is_callable( $validate ) ) {
+                               $val =& $this->results['xmp-' . $info['map_group']][$finalName];
+                               call_user_func_array( $validate, [ $info, &$val, false ] );
+                               if ( is_null( $val ) ) {
+                                       // the idea being the validation function will unset the variable if
+                                       // its invalid.
+                                       $this->logger->info( __METHOD__ . " <$ns:$tag> failed validation." );
+                                       unset( $this->results['xmp-' . $info['map_group']][$finalName] );
+                               }
+                       } else {
+                               $this->logger->warning( __METHOD__ . " Validation function for $finalName ("
+                                       . $validate[0] . '::' . $validate[1] . '()) is not callable.' );
+                       }
+               }
+
+               array_shift( $this->curItem );
+               array_shift( $this->mode );
+               $this->ancestorStruct = false;
+               $this->processingArray = false;
+               $this->itemLang = false;
+       }
+
+       /**
+        * Hit a closing element in MODE_LI (either rdf:Seq, or rdf:Bag )
+        * Add information about what type of element this is.
+        *
+        * Note we still have to hit the outer "</property>"
+        *
+        * @par For example, when processing:
+        * @code{,xml}
+        * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
+        *   </rdf:Seq> </exif:ISOSpeedRatings>
+        * @endcode
+        *
+        * This method is called when we hit the "</rdf:Seq>".
+        * (For comparison, we call endElementModeSimple when we
+        * hit the "</rdf:li>")
+        *
+        * @param string $elm Namespace . ' ' . element name
+        * @throws RuntimeException
+        */
+       private function endElementModeLi( $elm ) {
+               list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
+               $info = $this->items[$ns][$tag];
+               $finalName = isset( $info['map_name'] )
+                       ? $info['map_name'] : $tag;
+
+               array_shift( $this->mode );
+
+               if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) {
+                       $this->logger->debug( __METHOD__ . " Empty compund element $finalName." );
+
+                       return;
+               }
+
+               if ( $elm === self::NS_RDF . ' Seq' ) {
+                       $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'ol';
+               } elseif ( $elm === self::NS_RDF . ' Bag' ) {
+                       $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'ul';
+               } elseif ( $elm === self::NS_RDF . ' Alt' ) {
+                       // extra if needed as you could theoretically have a non-language alt.
+                       if ( $info['mode'] === self::MODE_LANG ) {
+                               $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'lang';
+                       }
+               } else {
+                       throw new RuntimeException(
+                               __METHOD__ . " expected </rdf:seq> or </rdf:bag> but instead got $elm."
+                       );
+               }
+       }
+
+       /**
+        * End element while in MODE_QDESC
+        * mostly when ending an element when we have a simple value
+        * that has qualifiers.
+        *
+        * Qualifiers aren't all that common, and we don't do anything
+        * with them.
+        *
+        * @param string $elm Namespace and element
+        */
+       private function endElementModeQDesc( $elm ) {
+
+               if ( $elm === self::NS_RDF . ' value' ) {
+                       list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
+                       $this->saveValue( $ns, $tag, $this->charContent );
+
+                       return;
+               } else {
+                       array_shift( $this->mode );
+                       array_shift( $this->curItem );
+               }
+       }
+
+       /**
+        * Handler for hitting a closing element.
+        *
+        * generally just calls a helper function depending on what
+        * mode we're in.
+        *
+        * Ignores the outer wrapping elements that are optional in
+        * xmp and have no meaning.
+        *
+        * @param XMLParser $parser
+        * @param string $elm Namespace . ' ' . element name
+        * @throws RuntimeException
+        */
+       function endElement( $parser, $elm ) {
+               if ( $elm === ( self::NS_RDF . ' RDF' )
+                       || $elm === 'adobe:ns:meta/ xmpmeta'
+                       || $elm === 'adobe:ns:meta/ xapmeta'
+               ) {
+                       // ignore these.
+                       return;
+               }
+
+               if ( $elm === self::NS_RDF . ' type' ) {
+                       // these aren't really supported properly yet.
+                       // However, it appears they almost never used.
+                       $this->logger->info( __METHOD__ . ' encountered <rdf:type>' );
+               }
+
+               if ( strpos( $elm, ' ' ) === false ) {
+                       // This probably shouldn't happen.
+                       // However, there is a bug in an adobe product
+                       // that forgets the namespace on some things.
+                       // (Luckily they are unimportant things).
+                       $this->logger->info( __METHOD__ . " Encountered </$elm> which has no namespace. Skipping." );
+
+                       return;
+               }
+
+               if ( count( $this->mode[0] ) === 0 ) {
+                       // This should never ever happen and means
+                       // there is a pretty major bug in this class.
+                       throw new RuntimeException( 'Encountered end element with no mode' );
+               }
+
+               if ( count( $this->curItem ) == 0 && $this->mode[0] !== self::MODE_INITIAL ) {
+                       // just to be paranoid. Should always have a curItem, except for initially
+                       // (aka during MODE_INITAL).
+                       throw new RuntimeException( "Hit end element </$elm> but no curItem" );
+               }
+
+               switch ( $this->mode[0] ) {
+                       case self::MODE_IGNORE:
+                               $this->endElementModeIgnore( $elm );
+                               break;
+                       case self::MODE_SIMPLE:
+                               $this->endElementModeSimple( $elm );
+                               break;
+                       case self::MODE_STRUCT:
+                       case self::MODE_SEQ:
+                       case self::MODE_BAG:
+                       case self::MODE_LANG:
+                       case self::MODE_BAGSTRUCT:
+                               $this->endElementNested( $elm );
+                               break;
+                       case self::MODE_INITIAL:
+                               if ( $elm === self::NS_RDF . ' Description' ) {
+                                       array_shift( $this->mode );
+                               } else {
+                                       throw new RuntimeException( 'Element ended unexpectedly while in MODE_INITIAL' );
+                               }
+                               break;
+                       case self::MODE_LI:
+                       case self::MODE_LI_LANG:
+                               $this->endElementModeLi( $elm );
+                               break;
+                       case self::MODE_QDESC:
+                               $this->endElementModeQDesc( $elm );
+                               break;
+                       default:
+                               $this->logger->warning( __METHOD__ . " no mode (elm = $elm)" );
+                               break;
+               }
+       }
+
+       /**
+        * Hit an opening element while in MODE_IGNORE
+        *
+        * XMP is extensible, so ignore any tag we don't understand.
+        *
+        * Mostly ignores, unless we encounter the element that we are ignoring.
+        * in which case we add it to the item stack, so we can ignore things
+        * that are nested, correctly.
+        *
+        * @param string $elm Namespace . ' ' . tag name
+        */
+       private function startElementModeIgnore( $elm ) {
+               if ( $elm === $this->curItem[0] ) {
+                       array_unshift( $this->curItem, $elm );
+                       array_unshift( $this->mode, self::MODE_IGNORE );
+               }
+       }
+
+       /**
+        *  Start element in MODE_BAG (unordered array)
+        * this should always be <rdf:Bag>
+        *
+        * @param string $elm Namespace . ' ' . tag
+        * @throws RuntimeException If we have an element that's not <rdf:Bag>
+        */
+       private function startElementModeBag( $elm ) {
+               if ( $elm === self::NS_RDF . ' Bag' ) {
+                       array_unshift( $this->mode, self::MODE_LI );
+               } else {
+                       throw new RuntimeException( "Expected <rdf:Bag> but got $elm." );
+               }
+       }
+
+       /**
+        * Start element in MODE_SEQ (ordered array)
+        * this should always be <rdf:Seq>
+        *
+        * @param string $elm Namespace . ' ' . tag
+        * @throws RuntimeException If we have an element that's not <rdf:Seq>
+        */
+       private function startElementModeSeq( $elm ) {
+               if ( $elm === self::NS_RDF . ' Seq' ) {
+                       array_unshift( $this->mode, self::MODE_LI );
+               } elseif ( $elm === self::NS_RDF . ' Bag' ) {
+                       # bug 27105
+                       $this->logger->info( __METHOD__ . ' Expected an rdf:Seq, but got an rdf:Bag. Pretending'
+                               . ' it is a Seq, since some buggy software is known to screw this up.' );
+                       array_unshift( $this->mode, self::MODE_LI );
+               } else {
+                       throw new RuntimeException( "Expected <rdf:Seq> but got $elm." );
+               }
+       }
+
+       /**
+        * Start element in MODE_LANG (language alternative)
+        * this should always be <rdf:Alt>
+        *
+        * This tag tends to be used for metadata like describe this
+        * picture, which can be translated into multiple languages.
+        *
+        * XMP supports non-linguistic alternative selections,
+        * which are really only used for thumbnails, which
+        * we don't care about.
+        *
+        * @param string $elm Namespace . ' ' . tag
+        * @throws RuntimeException If we have an element that's not <rdf:Alt>
+        */
+       private function startElementModeLang( $elm ) {
+               if ( $elm === self::NS_RDF . ' Alt' ) {
+                       array_unshift( $this->mode, self::MODE_LI_LANG );
+               } else {
+                       throw new RuntimeException( "Expected <rdf:Seq> but got $elm." );
+               }
+       }
+
+       /**
+        * Handle an opening element when in MODE_SIMPLE
+        *
+        * This should not happen often. This is for if a simple element
+        * already opened has a child element. Could happen for a
+        * qualified element.
+        *
+        * For example:
+        * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
+        *   <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
+        *   </exif:DigitalZoomRatio>
+        *
+        * This method is called when processing the <rdf:Description> element
+        *
+        * @param string $elm Namespace and tag names separated by space.
+        * @param array $attribs Attributes of the element.
+        * @throws RuntimeException
+        */
+       private function startElementModeSimple( $elm, $attribs ) {
+               if ( $elm === self::NS_RDF . ' Description' ) {
+                       // If this value has qualifiers
+                       array_unshift( $this->mode, self::MODE_QDESC );
+                       array_unshift( $this->curItem, $this->curItem[0] );
+
+                       if ( isset( $attribs[self::NS_RDF . ' value'] ) ) {
+                               list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
+                               $this->saveValue( $ns, $tag, $attribs[self::NS_RDF . ' value'] );
+                       }
+               } elseif ( $elm === self::NS_RDF . ' value' ) {
+                       // This should not be here.
+                       throw new RuntimeException( __METHOD__ . ' Encountered <rdf:value> where it was unexpected.' );
+               } else {
+                       // something else we don't recognize, like a qualifier maybe.
+                       $this->logger->info( __METHOD__ .
+                               " Encountered element <$elm> where only expecting character data as value of " .
+                               $this->curItem[0] );
+                       array_unshift( $this->mode, self::MODE_IGNORE );
+                       array_unshift( $this->curItem, $elm );
+               }
+       }
+
+       /**
+        * Start an element when in MODE_QDESC.
+        * This generally happens when a simple element has an inner
+        * rdf:Description to hold qualifier elements.
+        *
+        * For example in:
+        * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
+        *   <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
+        *   </exif:DigitalZoomRatio>
+        * Called when processing the <rdf:value> or <foo:someQualifier>.
+        *
+        * @param string $elm Namespace and tag name separated by a space.
+        *
+        */
+       private function startElementModeQDesc( $elm ) {
+               if ( $elm === self::NS_RDF . ' value' ) {
+                       return; // do nothing
+               } else {
+                       // otherwise its a qualifier, which we ignore
+                       array_unshift( $this->mode, self::MODE_IGNORE );
+                       array_unshift( $this->curItem, $elm );
+               }
+       }
+
+       /**
+        * Starting an element when in MODE_INITIAL
+        * This usually happens when we hit an element inside
+        * the outer rdf:Description
+        *
+        * This is generally where most properties start.
+        *
+        * @param string $ns Namespace
+        * @param string $tag Tag name (without namespace prefix)
+        * @param array $attribs Array of attributes
+        * @throws RuntimeException
+        */
+       private function startElementModeInitial( $ns, $tag, $attribs ) {
+               if ( $ns !== self::NS_RDF ) {
+
+                       if ( isset( $this->items[$ns][$tag] ) ) {
+                               if ( isset( $this->items[$ns][$tag]['structPart'] ) ) {
+                                       // If this element is supposed to appear only as
+                                       // a child of a structure, but appears here (not as
+                                       // a child of a struct), then something weird is
+                                       // happening, so ignore this element and its children.
+
+                                       $this->logger->warning( "Encountered <$ns:$tag> outside"
+                                               . " of its expected parent. Ignoring." );
+
+                                       array_unshift( $this->mode, self::MODE_IGNORE );
+                                       array_unshift( $this->curItem, $ns . ' ' . $tag );
+
+                                       return;
+                               }
+                               $mode = $this->items[$ns][$tag]['mode'];
+                               array_unshift( $this->mode, $mode );
+                               array_unshift( $this->curItem, $ns . ' ' . $tag );
+                               if ( $mode === self::MODE_STRUCT ) {
+                                       $this->ancestorStruct = isset( $this->items[$ns][$tag]['map_name'] )
+                                               ? $this->items[$ns][$tag]['map_name'] : $tag;
+                               }
+                               if ( $this->charContent !== false ) {
+                                       // Something weird.
+                                       // Should not happen in valid XMP.
+                                       throw new RuntimeException( 'tag nested in non-whitespace characters.' );
+                               }
+                       } else {
+                               // This element is not on our list of allowed elements so ignore.
+                               $this->logger->debug( __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." );
+                               array_unshift( $this->mode, self::MODE_IGNORE );
+                               array_unshift( $this->curItem, $ns . ' ' . $tag );
+
+                               return;
+                       }
+               }
+               // process attributes
+               $this->doAttribs( $attribs );
+       }
+
+       /**
+        * Hit an opening element when in a Struct (MODE_STRUCT)
+        * This is generally for fields of a compound property.
+        *
+        * Example of a struct (abbreviated; flash has more properties):
+        *
+        * <exif:Flash> <rdf:Description> <exif:Fired>True</exif:Fired>
+        *  <exif:Mode>1</exif:Mode></rdf:Description></exif:Flash>
+        *
+        * or:
+        *
+        * <exif:Flash rdf:parseType='Resource'> <exif:Fired>True</exif:Fired>
+        *  <exif:Mode>1</exif:Mode></exif:Flash>
+        *
+        * @param string $ns Namespace
+        * @param string $tag Tag name (no ns)
+        * @param array $attribs Array of attribs w/ values.
+        * @throws RuntimeException
+        */
+       private function startElementModeStruct( $ns, $tag, $attribs ) {
+               if ( $ns !== self::NS_RDF ) {
+
+                       if ( isset( $this->items[$ns][$tag] ) ) {
+                               if ( isset( $this->items[$ns][$this->ancestorStruct]['children'] )
+                                       && !isset( $this->items[$ns][$this->ancestorStruct]['children'][$tag] )
+                               ) {
+                                       // This assumes that we don't have inter-namespace nesting
+                                       // which we don't in all the properties we're interested in.
+                                       throw new RuntimeException( " <$tag> appeared nested in <" . $this->ancestorStruct
+                                               . "> where it is not allowed." );
+                               }
+                               array_unshift( $this->mode, $this->items[$ns][$tag]['mode'] );
+                               array_unshift( $this->curItem, $ns . ' ' . $tag );
+                               if ( $this->charContent !== false ) {
+                                       // Something weird.
+                                       // Should not happen in valid XMP.
+                                       throw new RuntimeException( "tag <$tag> nested in non-whitespace characters (" .
+                                               $this->charContent . ")." );
+                               }
+                       } else {
+                               array_unshift( $this->mode, self::MODE_IGNORE );
+                               array_unshift( $this->curItem, $elm );
+
+                               return;
+                       }
+               }
+
+               if ( $ns === self::NS_RDF && $tag === 'Description' ) {
+                       $this->doAttribs( $attribs );
+                       array_unshift( $this->mode, self::MODE_STRUCT );
+                       array_unshift( $this->curItem, $this->curItem[0] );
+               }
+       }
+
+       /**
+        * opening element in MODE_LI
+        * process elements of arrays.
+        *
+        * Example:
+        * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
+        *   </rdf:Seq> </exif:ISOSpeedRatings>
+        * This method is called when we hit the <rdf:li> element.
+        *
+        * @param string $elm Namespace . ' ' . tagname
+        * @param array $attribs Attributes. (needed for BAGSTRUCTS)
+        * @throws RuntimeException If gets a tag other than <rdf:li>
+        */
+       private function startElementModeLi( $elm, $attribs ) {
+               if ( ( $elm ) !== self::NS_RDF . ' li' ) {
+                       throw new RuntimeException( "<rdf:li> expected but got $elm." );
+               }
+
+               if ( !isset( $this->mode[1] ) ) {
+                       // This should never ever ever happen. Checking for it
+                       // to be paranoid.
+                       throw new RuntimeException( 'In mode Li, but no 2xPrevious mode!' );
+               }
+
+               if ( $this->mode[1] === self::MODE_BAGSTRUCT ) {
+                       // This list item contains a compound (STRUCT) value.
+                       array_unshift( $this->mode, self::MODE_STRUCT );
+                       array_unshift( $this->curItem, $elm );
+                       $this->processingArray = true;
+
+                       if ( !isset( $this->curItem[1] ) ) {
+                               // be paranoid.
+                               throw new RuntimeException( 'Can not find parent of BAGSTRUCT.' );
+                       }
+                       list( $curNS, $curTag ) = explode( ' ', $this->curItem[1] );
+                       $this->ancestorStruct = isset( $this->items[$curNS][$curTag]['map_name'] )
+                               ? $this->items[$curNS][$curTag]['map_name'] : $curTag;
+
+                       $this->doAttribs( $attribs );
+               } else {
+                       // Normal BAG or SEQ containing simple values.
+                       array_unshift( $this->mode, self::MODE_SIMPLE );
+                       // need to add curItem[0] on again since one is for the specific item
+                       // and one is for the entire group.
+                       array_unshift( $this->curItem, $this->curItem[0] );
+                       $this->processingArray = true;
+               }
+       }
+
+       /**
+        * Opening element in MODE_LI_LANG.
+        * process elements of language alternatives
+        *
+        * Example:
+        * <dc:title> <rdf:Alt> <rdf:li xml:lang="x-default">My house
+        *  </rdf:li> </rdf:Alt> </dc:title>
+        *
+        * This method is called when we hit the <rdf:li> element.
+        *
+        * @param string $elm Namespace . ' ' . tag
+        * @param array $attribs Array of elements (most importantly xml:lang)
+        * @throws RuntimeException If gets a tag other than <rdf:li> or if no xml:lang
+        */
+       private function startElementModeLiLang( $elm, $attribs ) {
+               if ( $elm !== self::NS_RDF . ' li' ) {
+                       throw new RuntimeException( __METHOD__ . " <rdf:li> expected but got $elm." );
+               }
+               if ( !isset( $attribs[self::NS_XML . ' lang'] )
+                       || !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $attribs[self::NS_XML . ' lang'] )
+               ) {
+                       throw new RuntimeException( __METHOD__
+                               . " <rdf:li> did not contain, or has invalid xml:lang attribute in lang alternative" );
+               }
+
+               // Lang is case-insensitive.
+               $this->itemLang = strtolower( $attribs[self::NS_XML . ' lang'] );
+
+               // need to add curItem[0] on again since one is for the specific item
+               // and one is for the entire group.
+               array_unshift( $this->curItem, $this->curItem[0] );
+               array_unshift( $this->mode, self::MODE_SIMPLE );
+               $this->processingArray = true;
+       }
+
+       /**
+        * Hits an opening element.
+        * Generally just calls a helper based on what MODE we're in.
+        * Also does some initial set up for the wrapper element
+        *
+        * @param XMLParser $parser
+        * @param string $elm Namespace "<space>" element
+        * @param array $attribs Attribute name => value
+        * @throws RuntimeException
+        */
+       function startElement( $parser, $elm, $attribs ) {
+
+               if ( $elm === self::NS_RDF . ' RDF'
+                       || $elm === 'adobe:ns:meta/ xmpmeta'
+                       || $elm === 'adobe:ns:meta/ xapmeta'
+               ) {
+                       /* ignore. */
+                       return;
+               } elseif ( $elm === self::NS_RDF . ' Description' ) {
+                       if ( count( $this->mode ) === 0 ) {
+                               // outer rdf:desc
+                               array_unshift( $this->mode, self::MODE_INITIAL );
+                       }
+               } elseif ( $elm === self::NS_RDF . ' type' ) {
+                       // This doesn't support rdf:type properly.
+                       // In practise I have yet to see a file that
+                       // uses this element, however it is mentioned
+                       // on page 25 of part 1 of the xmp standard.
+                       // Also it seems as if exiv2 and exiftool do not support
+                       // this either (That or I misunderstand the standard)
+                       $this->logger->info( __METHOD__ . ' Encountered <rdf:type> which isn\'t currently supported' );
+               }
+
+               if ( strpos( $elm, ' ' ) === false ) {
+                       // This probably shouldn't happen.
+                       $this->logger->info( __METHOD__ . " Encountered <$elm> which has no namespace. Skipping." );
+
+                       return;
+               }
+
+               list( $ns, $tag ) = explode( ' ', $elm, 2 );
+
+               if ( count( $this->mode ) === 0 ) {
+                       // This should not happen.
+                       throw new RuntimeException( 'Error extracting XMP, '
+                               . "encountered <$elm> with no mode" );
+               }
+
+               switch ( $this->mode[0] ) {
+                       case self::MODE_IGNORE:
+                               $this->startElementModeIgnore( $elm );
+                               break;
+                       case self::MODE_SIMPLE:
+                               $this->startElementModeSimple( $elm, $attribs );
+                               break;
+                       case self::MODE_INITIAL:
+                               $this->startElementModeInitial( $ns, $tag, $attribs );
+                               break;
+                       case self::MODE_STRUCT:
+                               $this->startElementModeStruct( $ns, $tag, $attribs );
+                               break;
+                       case self::MODE_BAG:
+                       case self::MODE_BAGSTRUCT:
+                               $this->startElementModeBag( $elm );
+                               break;
+                       case self::MODE_SEQ:
+                               $this->startElementModeSeq( $elm );
+                               break;
+                       case self::MODE_LANG:
+                               $this->startElementModeLang( $elm );
+                               break;
+                       case self::MODE_LI_LANG:
+                               $this->startElementModeLiLang( $elm, $attribs );
+                               break;
+                       case self::MODE_LI:
+                               $this->startElementModeLi( $elm, $attribs );
+                               break;
+                       case self::MODE_QDESC:
+                               $this->startElementModeQDesc( $elm );
+                               break;
+                       default:
+                               throw new RuntimeException( 'StartElement in unknown mode: ' . $this->mode[0] );
+               }
+       }
+
+       // @codingStandardsIgnoreStart Generic.Files.LineLength
+       /**
+        * Process attributes.
+        * Simple values can be stored as either a tag or attribute
+        *
+        * Often the initial "<rdf:Description>" tag just has all the simple
+        * properties as attributes.
+        *
+        * @par Example:
+        * @code
+        * <rdf:Description rdf:about="" xmlns:exif="http://ns.adobe.com/exif/1.0/" exif:DigitalZoomRatio="0/10">
+        * @endcode
+        *
+        * @param array $attribs Array attribute=>value
+        * @throws RuntimeException
+        */
+       // @codingStandardsIgnoreEnd
+       private function doAttribs( $attribs ) {
+               // first check for rdf:parseType attribute, as that can change
+               // how the attributes are interperted.
+
+               if ( isset( $attribs[self::NS_RDF . ' parseType'] )
+                       && $attribs[self::NS_RDF . ' parseType'] === 'Resource'
+                       && $this->mode[0] === self::MODE_SIMPLE
+               ) {
+                       // this is equivalent to having an inner rdf:Description
+                       $this->mode[0] = self::MODE_QDESC;
+               }
+               foreach ( $attribs as $name => $val ) {
+                       if ( strpos( $name, ' ' ) === false ) {
+                               // This shouldn't happen, but so far some old software forgets namespace
+                               // on rdf:about.
+                               $this->logger->info( __METHOD__ . ' Encountered non-namespaced attribute: '
+                                       . " $name=\"$val\". Skipping. " );
+                               continue;
+                       }
+                       list( $ns, $tag ) = explode( ' ', $name, 2 );
+                       if ( $ns === self::NS_RDF ) {
+                               if ( $tag === 'value' || $tag === 'resource' ) {
+                                       // resource is for url.
+                                       // value attribute is a weird way of just putting the contents.
+                                       $this->char( $this->xmlParser, $val );
+                               }
+                       } elseif ( isset( $this->items[$ns][$tag] ) ) {
+                               if ( $this->mode[0] === self::MODE_SIMPLE ) {
+                                       throw new RuntimeException( __METHOD__
+                                               . " $ns:$tag found as attribute where not allowed" );
+                               }
+                               $this->saveValue( $ns, $tag, $val );
+                       } else {
+                               $this->logger->debug( __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." );
+                       }
+               }
+       }
+
+       /**
+        * Given an extracted value, save it to results array
+        *
+        * note also uses $this->ancestorStruct and
+        * $this->processingArray to determine what name to
+        * save the value under. (in addition to $tag).
+        *
+        * @param string $ns Namespace of tag this is for
+        * @param string $tag Tag name
+        * @param string $val Value to save
+        */
+       private function saveValue( $ns, $tag, $val ) {
+
+               $info =& $this->items[$ns][$tag];
+               $finalName = isset( $info['map_name'] )
+                       ? $info['map_name'] : $tag;
+               if ( isset( $info['validate'] ) ) {
+                       if ( is_array( $info['validate'] ) ) {
+                               $validate = $info['validate'];
+                       } else {
+                               $validator = new XMPValidate( $this->logger );
+                               $validate = [ $validator, $info['validate'] ];
+                       }
+
+                       if ( is_callable( $validate ) ) {
+                               call_user_func_array( $validate, [ $info, &$val, true ] );
+                               // the reasoning behind using &$val instead of using the return value
+                               // is to be consistent between here and validating structures.
+                               if ( is_null( $val ) ) {
+                                       $this->logger->info( __METHOD__ . " <$ns:$tag> failed validation." );
+
+                                       return;
+                               }
+                       } else {
+                               $this->logger->warning( __METHOD__ . " Validation function for $finalName ("
+                                       . $validate[0] . '::' . $validate[1] . '()) is not callable.' );
+                       }
+               }
+
+               if ( $this->ancestorStruct && $this->processingArray ) {
+                       // Aka both an array and a struct. ( self::MODE_BAGSTRUCT )
+                       $this->results['xmp-' . $info['map_group']][$this->ancestorStruct][][$finalName] = $val;
+               } elseif ( $this->ancestorStruct ) {
+                       $this->results['xmp-' . $info['map_group']][$this->ancestorStruct][$finalName] = $val;
+               } elseif ( $this->processingArray ) {
+                       if ( $this->itemLang === false ) {
+                               // normal array
+                               $this->results['xmp-' . $info['map_group']][$finalName][] = $val;
+                       } else {
+                               // lang array.
+                               $this->results['xmp-' . $info['map_group']][$finalName][$this->itemLang] = $val;
+                       }
+               } else {
+                       $this->results['xmp-' . $info['map_group']][$finalName] = $val;
+               }
+       }
+}
diff --git a/includes/libs/xmp/XMPInfo.php b/includes/libs/xmp/XMPInfo.php
new file mode 100644 (file)
index 0000000..052be33
--- /dev/null
@@ -0,0 +1,1168 @@
+<?php
+/**
+ * Definitions for XMPReader class.
+ *
+ * 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 Media
+ */
+
+/**
+ * This class is just a container for a big array
+ * used by XMPReader to determine which XMP items to
+ * extract.
+ */
+class XMPInfo {
+       /** Get the items array
+        * @return array XMP item configuration array.
+        */
+       public static function getItems() {
+               return self::$items;
+       }
+
+       /**
+        * XMPInfo::$items keeps a list of all the items
+        * we are interested to extract, as well as
+        * information about the item like what type
+        * it is.
+        *
+        * Format is an array of namespaces,
+        * each containing an array of tags
+        * each tag is an array of information about the
+        * tag, including:
+        *   * map_group - What group (used for precedence during conflicts).
+        *   * mode - What type of item (self::MODE_SIMPLE usually, see above for
+        *     all values).
+        *   * validate - Method to validate input. Could also post-process the
+        *     input. A string value is assumed to be a method of
+        *     XMPValidate. Can also take a array( 'className', 'methodName' ).
+        *   * choices - Array of potential values (format of 'value' => true ).
+        *     Only used with validateClosed.
+        *   * rangeLow and rangeHigh - Alternative to choices for numeric ranges.
+        *     Again for validateClosed only.
+        *   * children - For MODE_STRUCT items, allowed children.
+        *   * structPart - Indicates that this element can only appear as a member
+        *     of a structure.
+        *
+        * Currently this just has a bunch of EXIF values as this class is only half-done.
+        */
+       static private $items = [
+               'http://ns.adobe.com/exif/1.0/' => [
+                       'ApertureValue' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'BrightnessValue' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'CompressedBitsPerPixel' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'DigitalZoomRatio' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'ExposureBiasValue' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'ExposureIndex' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'ExposureTime' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'FlashEnergy' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational',
+                       ],
+                       'FNumber' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'FocalLength' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'FocalPlaneXResolution' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'FocalPlaneYResolution' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'GPSAltitude' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational',
+                       ],
+                       'GPSDestBearing' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'GPSDestDistance' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'GPSDOP' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'GPSImgDirection' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'GPSSpeed' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'GPSTrack' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'MaxApertureValue' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'ShutterSpeedValue' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       'SubjectDistance' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational'
+                       ],
+                       /* Flash */
+                       'Flash' => [
+                               'mode' => XMPReader::MODE_STRUCT,
+                               'children' => [
+                                       'Fired' => true,
+                                       'Function' => true,
+                                       'Mode' => true,
+                                       'RedEyeMode' => true,
+                                       'Return' => true,
+                               ],
+                               'validate' => 'validateFlash',
+                               'map_group' => 'exif',
+                       ],
+                       'Fired' => [
+                               'map_group' => 'exif',
+                               'validate' => 'validateBoolean',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'Function' => [
+                               'map_group' => 'exif',
+                               'validate' => 'validateBoolean',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'Mode' => [
+                               'map_group' => 'exif',
+                               'validate' => 'validateClosed',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'choices' => [ '0' => true, '1' => true,
+                                       '2' => true, '3' => true ],
+                               'structPart' => true,
+                       ],
+                       'Return' => [
+                               'map_group' => 'exif',
+                               'validate' => 'validateClosed',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'choices' => [ '0' => true,
+                                       '2' => true, '3' => true ],
+                               'structPart' => true,
+                       ],
+                       'RedEyeMode' => [
+                               'map_group' => 'exif',
+                               'validate' => 'validateBoolean',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       /* End Flash */
+                       'ISOSpeedRatings' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SEQ,
+                               'validate' => 'validateInteger'
+                       ],
+                       /* end rational things */
+                       'ColorSpace' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '1' => true, '65535' => true ],
+                       ],
+                       'ComponentsConfiguration' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SEQ,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '1' => true, '2' => true, '3' => true, '4' => true,
+                                       '5' => true, '6' => true ]
+                       ],
+                       'Contrast' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '0' => true, '1' => true, '2' => true ]
+                       ],
+                       'CustomRendered' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '0' => true, '1' => true ]
+                       ],
+                       'DateTimeOriginal' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateDate',
+                       ],
+                       'DateTimeDigitized' => [ /* xmp:CreateDate */
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateDate',
+                       ],
+                       /* todo: there might be interesting information in
+                        * exif:DeviceSettingDescription, but need to find an
+                        * example
+                        */
+                       'ExifVersion' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'ExposureMode' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'rangeLow' => 0,
+                               'rangeHigh' => 2,
+                       ],
+                       'ExposureProgram' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'rangeLow' => 0,
+                               'rangeHigh' => 8,
+                       ],
+                       'FileSource' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '3' => true ]
+                       ],
+                       'FlashpixVersion' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'FocalLengthIn35mmFilm' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateInteger',
+                       ],
+                       'FocalPlaneResolutionUnit' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '2' => true, '3' => true ],
+                       ],
+                       'GainControl' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'rangeLow' => 0,
+                               'rangeHigh' => 4,
+                       ],
+                       /* this value is post-processed out later */
+                       'GPSAltitudeRef' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '0' => true, '1' => true ],
+                       ],
+                       'GPSAreaInformation' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'GPSDestBearingRef' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ 'T' => true, 'M' => true ],
+                       ],
+                       'GPSDestDistanceRef' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ 'K' => true, 'M' => true,
+                                       'N' => true ],
+                       ],
+                       'GPSDestLatitude' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateGPS',
+                       ],
+                       'GPSDestLongitude' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateGPS',
+                       ],
+                       'GPSDifferential' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '0' => true, '1' => true ],
+                       ],
+                       'GPSImgDirectionRef' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ 'T' => true, 'M' => true ],
+                       ],
+                       'GPSLatitude' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateGPS',
+                       ],
+                       'GPSLongitude' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateGPS',
+                       ],
+                       'GPSMapDatum' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'GPSMeasureMode' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '2' => true, '3' => true ]
+                       ],
+                       'GPSProcessingMethod' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'GPSSatellites' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'GPSSpeedRef' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ 'K' => true, 'M' => true,
+                                       'N' => true ],
+                       ],
+                       'GPSStatus' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ 'A' => true, 'V' => true ]
+                       ],
+                       'GPSTimeStamp' => [
+                               'map_group' => 'exif',
+                               // Note: in exif, GPSDateStamp does not include
+                               // the time, where here it does.
+                               'map_name' => 'GPSDateStamp',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateDate',
+                       ],
+                       'GPSTrackRef' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ 'T' => true, 'M' => true ]
+                       ],
+                       'GPSVersionID' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'ImageUniqueID' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'LightSource' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               /* can't use a range, as it skips... */
+                               'choices' => [ '0' => true, '1' => true,
+                                       '2' => true, '3' => true, '4' => true,
+                                       '9' => true, '10' => true, '11' => true,
+                                       '12' => true, '13' => true,
+                                       '14' => true, '15' => true,
+                                       '17' => true, '18' => true,
+                                       '19' => true, '20' => true,
+                                       '21' => true, '22' => true,
+                                       '23' => true, '24' => true,
+                                       '255' => true,
+                               ],
+                       ],
+                       'MeteringMode' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'rangeLow' => 0,
+                               'rangeHigh' => 6,
+                               'choices' => [ '255' => true ],
+                       ],
+                       /* Pixel(X|Y)Dimension are rather useless, but for
+                        * completeness since we do it with exif.
+                        */
+                       'PixelXDimension' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateInteger',
+                       ],
+                       'PixelYDimension' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateInteger',
+                       ],
+                       'Saturation' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'rangeLow' => 0,
+                               'rangeHigh' => 2,
+                       ],
+                       'SceneCaptureType' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'rangeLow' => 0,
+                               'rangeHigh' => 3,
+                       ],
+                       'SceneType' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '1' => true ],
+                       ],
+                       // Note, 6 is not valid SensingMethod.
+                       'SensingMethod' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'rangeLow' => 1,
+                               'rangeHigh' => 5,
+                               'choices' => [ '7' => true, 8 => true ],
+                       ],
+                       'Sharpness' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'rangeLow' => 0,
+                               'rangeHigh' => 2,
+                       ],
+                       'SpectralSensitivity' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       // This tag should perhaps be displayed to user better.
+                       'SubjectArea' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SEQ,
+                               'validate' => 'validateInteger',
+                       ],
+                       'SubjectDistanceRange' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'rangeLow' => 0,
+                               'rangeHigh' => 3,
+                       ],
+                       'SubjectLocation' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SEQ,
+                               'validate' => 'validateInteger',
+                       ],
+                       'UserComment' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_LANG,
+                       ],
+                       'WhiteBalance' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '0' => true, '1' => true ]
+                       ],
+               ],
+               'http://ns.adobe.com/tiff/1.0/' => [
+                       'Artist' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'BitsPerSample' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SEQ,
+                               'validate' => 'validateInteger',
+                       ],
+                       'Compression' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '1' => true, '6' => true ],
+                       ],
+                       /* this prop should not be used in XMP. dc:rights is the correct prop */
+                       'Copyright' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_LANG,
+                       ],
+                       'DateTime' => [ /* proper prop is xmp:ModifyDate */
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateDate',
+                       ],
+                       'ImageDescription' => [ /* proper one is dc:description */
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_LANG,
+                       ],
+                       'ImageLength' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateInteger',
+                       ],
+                       'ImageWidth' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateInteger',
+                       ],
+                       'Make' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'Model' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       /**** Do not extract this property
+                        * It interferes with auto exif rotation.
+                        * 'Orientation'       => array(
+                        *    'map_group' => 'exif',
+                        *    'mode'      => XMPReader::MODE_SIMPLE,
+                        *    'validate'  => 'validateClosed',
+                        *    'choices'   => array( '1' => true, '2' => true, '3' => true, '4' => true, 5 => true,
+                        *            '6' => true, '7' => true, '8' => true ),
+                        *),
+                        ******/
+                       'PhotometricInterpretation' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '2' => true, '6' => true ],
+                       ],
+                       'PlanerConfiguration' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '1' => true, '2' => true ],
+                       ],
+                       'PrimaryChromaticities' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SEQ,
+                               'validate' => 'validateRational',
+                       ],
+                       'ReferenceBlackWhite' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SEQ,
+                               'validate' => 'validateRational',
+                       ],
+                       'ResolutionUnit' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '2' => true, '3' => true ],
+                       ],
+                       'SamplesPerPixel' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateInteger',
+                       ],
+                       'Software' => [ /* see xmp:CreatorTool */
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       /* ignore TransferFunction */
+                       'WhitePoint' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SEQ,
+                               'validate' => 'validateRational',
+                       ],
+                       'XResolution' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational',
+                       ],
+                       'YResolution' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRational',
+                       ],
+                       'YCbCrCoefficients' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SEQ,
+                               'validate' => 'validateRational',
+                       ],
+                       'YCbCrPositioning' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateClosed',
+                               'choices' => [ '1' => true, '2' => true ],
+                       ],
+                       /********
+                        * Disable extracting this property (bug 31944)
+                        * Several files have a string instead of a Seq
+                        * for this property. XMPReader doesn't handle
+                        * mismatched types very gracefully (it marks
+                        * the entire file as invalid, instead of just
+                        * the relavent prop). Since this prop
+                        * doesn't communicate all that useful information
+                        * just disable this prop for now, until such
+                        * XMPReader is more graceful (bug 32172)
+                        * 'YCbCrSubSampling'  => array(
+                        *    'map_group' => 'exif',
+                        *    'mode'      => XMPReader::MODE_SEQ,
+                        *    'validate'  => 'validateClosed',
+                        *    'choices'   => array( '1' => true, '2' => true ),
+                        * ),
+                        */
+               ],
+               'http://ns.adobe.com/exif/1.0/aux/' => [
+                       'Lens' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'SerialNumber' => [
+                               'map_group' => 'exif',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'OwnerName' => [
+                               'map_group' => 'exif',
+                               'map_name' => 'CameraOwnerName',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+               ],
+               'http://purl.org/dc/elements/1.1/' => [
+                       'title' => [
+                               'map_group' => 'general',
+                               'map_name' => 'ObjectName',
+                               'mode' => XMPReader::MODE_LANG
+                       ],
+                       'description' => [
+                               'map_group' => 'general',
+                               'map_name' => 'ImageDescription',
+                               'mode' => XMPReader::MODE_LANG
+                       ],
+                       'contributor' => [
+                               'map_group' => 'general',
+                               'map_name' => 'dc-contributor',
+                               'mode' => XMPReader::MODE_BAG
+                       ],
+                       'coverage' => [
+                               'map_group' => 'general',
+                               'map_name' => 'dc-coverage',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'creator' => [
+                               'map_group' => 'general',
+                               'map_name' => 'Artist', // map with exif Artist, iptc byline (2:80)
+                               'mode' => XMPReader::MODE_SEQ,
+                       ],
+                       'date' => [
+                               'map_group' => 'general',
+                               // Note, not mapped with other date properties, as this type of date is
+                               // non-specific: "A point or period of time associated with an event in
+                               //  the lifecycle of the resource"
+                               'map_name' => 'dc-date',
+                               'mode' => XMPReader::MODE_SEQ,
+                               'validate' => 'validateDate',
+                       ],
+                       /* Do not extract dc:format, as we've got better ways to determine MIME type */
+                       'identifier' => [
+                               'map_group' => 'deprecated',
+                               'map_name' => 'Identifier',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'language' => [
+                               'map_group' => 'general',
+                               'map_name' => 'LanguageCode', /* mapped with iptc 2:135 */
+                               'mode' => XMPReader::MODE_BAG,
+                               'validate' => 'validateLangCode',
+                       ],
+                       'publisher' => [
+                               'map_group' => 'general',
+                               'map_name' => 'dc-publisher',
+                               'mode' => XMPReader::MODE_BAG,
+                       ],
+                       // for related images/resources
+                       'relation' => [
+                               'map_group' => 'general',
+                               'map_name' => 'dc-relation',
+                               'mode' => XMPReader::MODE_BAG,
+                       ],
+                       'rights' => [
+                               'map_group' => 'general',
+                               'map_name' => 'Copyright',
+                               'mode' => XMPReader::MODE_LANG,
+                       ],
+                       // Note: source is not mapped with iptc source, since iptc
+                       // source describes the source of the image in terms of a person
+                       // who provided the image, where this is to describe an image that the
+                       // current one is based on.
+                       'source' => [
+                               'map_group' => 'general',
+                               'map_name' => 'dc-source',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'subject' => [
+                               'map_group' => 'general',
+                               'map_name' => 'Keywords', /* maps to iptc 2:25 */
+                               'mode' => XMPReader::MODE_BAG,
+                       ],
+                       'type' => [
+                               'map_group' => 'general',
+                               'map_name' => 'dc-type',
+                               'mode' => XMPReader::MODE_BAG,
+                       ],
+               ],
+               'http://ns.adobe.com/xap/1.0/' => [
+                       'CreateDate' => [
+                               'map_group' => 'general',
+                               'map_name' => 'DateTimeDigitized',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateDate',
+                       ],
+                       'CreatorTool' => [
+                               'map_group' => 'general',
+                               'map_name' => 'Software',
+                               'mode' => XMPReader::MODE_SIMPLE
+                       ],
+                       'Identifier' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_BAG,
+                       ],
+                       'Label' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'ModifyDate' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'DateTime',
+                               'validate' => 'validateDate',
+                       ],
+                       'MetadataDate' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               // map_name to be consistent with other date names.
+                               'map_name' => 'DateTimeMetadata',
+                               'validate' => 'validateDate',
+                       ],
+                       'Nickname' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'Rating' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateRating',
+                       ],
+               ],
+               'http://ns.adobe.com/xap/1.0/rights/' => [
+                       'Certificate' => [
+                               'map_group' => 'general',
+                               'map_name' => 'RightsCertificate',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'Marked' => [
+                               'map_group' => 'general',
+                               'map_name' => 'Copyrighted',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateBoolean',
+                       ],
+                       'Owner' => [
+                               'map_group' => 'general',
+                               'map_name' => 'CopyrightOwner',
+                               'mode' => XMPReader::MODE_BAG,
+                       ],
+                       // this seems similar to dc:rights.
+                       'UsageTerms' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_LANG,
+                       ],
+                       'WebStatement' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+               ],
+               // XMP media management.
+               'http://ns.adobe.com/xap/1.0/mm/' => [
+                       // if we extract the exif UniqueImageID, might
+                       // as well do this too.
+                       'OriginalDocumentID' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       // It might also be useful to do xmpMM:LastURL
+                       // and xmpMM:DerivedFrom as you can potentially,
+                       // get the url of this document/source for this
+                       // document. However whats more likely is you'd
+                       // get a file:// url for the path of the doc,
+                       // which is somewhat of a privacy issue.
+               ],
+               'http://creativecommons.org/ns#' => [
+                       'license' => [
+                               'map_name' => 'LicenseUrl',
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'morePermissions' => [
+                               'map_name' => 'MorePermissionsUrl',
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'attributionURL' => [
+                               'map_group' => 'general',
+                               'map_name' => 'AttributionUrl',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'attributionName' => [
+                               'map_group' => 'general',
+                               'map_name' => 'PreferredAttributionName',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+               ],
+               // Note, this property affects how jpeg metadata is extracted.
+               'http://ns.adobe.com/xmp/note/' => [
+                       'HasExtendedXMP' => [
+                               'map_group' => 'special',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+               ],
+               /* Note, in iptc schemas, the legacy properties are denoted
+                * as deprecated, since other properties should used instead,
+                * and properties marked as deprecated in the standard are
+                * are marked as general here as they don't have replacements
+                */
+               'http://ns.adobe.com/photoshop/1.0/' => [
+                       'City' => [
+                               'map_group' => 'deprecated',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'CityDest',
+                       ],
+                       'Country' => [
+                               'map_group' => 'deprecated',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'CountryDest',
+                       ],
+                       'State' => [
+                               'map_group' => 'deprecated',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'ProvinceOrStateDest',
+                       ],
+                       'DateCreated' => [
+                               'map_group' => 'deprecated',
+                               // marking as deprecated as the xmp prop preferred
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'DateTimeOriginal',
+                               'validate' => 'validateDate',
+                               // note this prop is an XMP, not IPTC date
+                       ],
+                       'CaptionWriter' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'Writer',
+                       ],
+                       'Instructions' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'SpecialInstructions',
+                       ],
+                       'TransmissionReference' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'OriginalTransmissionRef',
+                       ],
+                       'AuthorsPosition' => [
+                               /* This corresponds with 2:85
+                                * By-line Title, which needs to be
+                                * handled weirdly to correspond
+                                * with iptc/exif. */
+                               'map_group' => 'special',
+                               'mode' => XMPReader::MODE_SIMPLE
+                       ],
+                       'Credit' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'Source' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'Urgency' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'Category' => [
+                               // Note, this prop is deprecated, but in general
+                               // group since it doesn't have a replacement.
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'iimCategory',
+                       ],
+                       'SupplementalCategories' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_BAG,
+                               'map_name' => 'iimSupplementalCategory',
+                       ],
+                       'Headline' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE
+                       ],
+               ],
+               'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/' => [
+                       'CountryCode' => [
+                               'map_group' => 'deprecated',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'CountryCodeDest',
+                       ],
+                       'IntellectualGenre' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       // Note, this is a six digit code.
+                       // See: http://cv.iptc.org/newscodes/scene/
+                       // Since these aren't really all that common,
+                       // we just show the number.
+                       'Scene' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_BAG,
+                               'validate' => 'validateInteger',
+                               'map_name' => 'SceneCode',
+                       ],
+                       /* Note: SubjectCode should be an 8 ascii digits.
+                        * it is not really an integer (has leading 0's,
+                        * cannot have a +/- sign), but validateInteger
+                        * will let it through.
+                        */
+                       'SubjectCode' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_BAG,
+                               'map_name' => 'SubjectNewsCode',
+                               'validate' => 'validateInteger'
+                       ],
+                       'Location' => [
+                               'map_group' => 'deprecated',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'map_name' => 'SublocationDest',
+                       ],
+                       'CreatorContactInfo' => [
+                               /* Note this maps to 2:118 in iim
+                                * (Contact) field. However those field
+                                * types are slightly different - 2:118
+                                * is free form text field, where this
+                                * is more structured.
+                                */
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_STRUCT,
+                               'map_name' => 'Contact',
+                               'children' => [
+                                       'CiAdrExtadr' => true,
+                                       'CiAdrCity' => true,
+                                       'CiAdrCtry' => true,
+                                       'CiEmailWork' => true,
+                                       'CiTelWork' => true,
+                                       'CiAdrPcode' => true,
+                                       'CiAdrRegion' => true,
+                                       'CiUrlWork' => true,
+                               ],
+                       ],
+                       'CiAdrExtadr' => [ /* address */
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'CiAdrCity' => [ /* city */
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'CiAdrCtry' => [ /* country */
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'CiEmailWork' => [ /* email (possibly separated by ',') */
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'CiTelWork' => [ /* telephone */
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'CiAdrPcode' => [ /* postal code */
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'CiAdrRegion' => [ /* province/state */
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'CiUrlWork' => [ /* url. Multiple may be separated by comma. */
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       /* End contact info struct properties */
+               ],
+               'http://iptc.org/std/Iptc4xmpExt/2008-02-29/' => [
+                       'Event' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ],
+                       'OrganisationInImageName' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_BAG,
+                               'map_name' => 'OrganisationInImage'
+                       ],
+                       'PersonInImage' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_BAG,
+                       ],
+                       'MaxAvailHeight' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateInteger',
+                               'map_name' => 'OriginalImageHeight',
+                       ],
+                       'MaxAvailWidth' => [
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateInteger',
+                               'map_name' => 'OriginalImageWidth',
+                       ],
+                       // LocationShown and LocationCreated are handled
+                       // specially because they are hierarchical, but we
+                       // also want to merge with the old non-hierarchical.
+                       'LocationShown' => [
+                               'map_group' => 'special',
+                               'mode' => XMPReader::MODE_BAGSTRUCT,
+                               'children' => [
+                                       'WorldRegion' => true,
+                                       'CountryCode' => true, /* iso code */
+                                       'CountryName' => true,
+                                       'ProvinceState' => true,
+                                       'City' => true,
+                                       'Sublocation' => true,
+                               ],
+                       ],
+                       'LocationCreated' => [
+                               'map_group' => 'special',
+                               'mode' => XMPReader::MODE_BAGSTRUCT,
+                               'children' => [
+                                       'WorldRegion' => true,
+                                       'CountryCode' => true, /* iso code */
+                                       'CountryName' => true,
+                                       'ProvinceState' => true,
+                                       'City' => true,
+                                       'Sublocation' => true,
+                               ],
+                       ],
+                       'WorldRegion' => [
+                               'map_group' => 'special',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'CountryCode' => [
+                               'map_group' => 'special',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'CountryName' => [
+                               'map_group' => 'special',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                               'map_name' => 'Country',
+                       ],
+                       'ProvinceState' => [
+                               'map_group' => 'special',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                               'map_name' => 'ProvinceOrState',
+                       ],
+                       'City' => [
+                               'map_group' => 'special',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+                       'Sublocation' => [
+                               'map_group' => 'special',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                               'structPart' => true,
+                       ],
+
+                       /* Other props that might be interesting but
+                        * Not currently extracted:
+                        * ArtworkOrObject, (info about objects in picture)
+                        * DigitalSourceType
+                        * RegistryId
+                        */
+               ],
+
+               /* Plus props we might want to consider:
+                * (Note: some of these have unclear/incomplete definitions
+                * from the iptc4xmp standard).
+                * ImageSupplier (kind of like iptc source field)
+                * ImageSupplierId (id code for image from supplier)
+                * CopyrightOwner
+                * ImageCreator
+                * Licensor
+                * Various model release fields
+                * Property release fields.
+                */
+       ];
+}
diff --git a/includes/libs/xmp/XMPValidate.php b/includes/libs/xmp/XMPValidate.php
new file mode 100644 (file)
index 0000000..31eaa3b
--- /dev/null
@@ -0,0 +1,400 @@
+<?php
+/**
+ * Methods for validating XMP properties.
+ *
+ * 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 Media
+ */
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\LoggerAwareInterface;
+
+/**
+ * This contains some static methods for
+ * validating XMP properties. See XMPInfo and XMPReader classes.
+ *
+ * Each of these functions take the same parameters
+ * * an info array which is a subset of the XMPInfo::items array
+ * * A value (passed as reference) to validate. This can be either a
+ *    simple value or an array
+ * * A boolean to determine if this is validating a simple or complex values
+ *
+ * It should be noted that when an array is being validated, typically the validation
+ * function is called once for each value, and then once at the end for the entire array.
+ *
+ * These validation functions can also be used to modify the data. See the gps and flash one's
+ * for example.
+ *
+ * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart1.pdf starting at pg 28
+ * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf starting at pg 11
+ */
+class XMPValidate implements LoggerAwareInterface {
+
+       /**
+        * @var LoggerInterface
+        */
+       private $logger;
+
+       public function __construct( LoggerInterface $logger ) {
+               $this->setLogger( $logger );
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+       /**
+        * Function to validate boolean properties ( True or False )
+        *
+        * @param array $info Information about current property
+        * @param mixed &$val Current value to validate
+        * @param bool $standalone If this is a simple property or array
+        */
+       public function validateBoolean( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               if ( $val !== 'True' && $val !== 'False' ) {
+                       $this->logger->info( __METHOD__ . " Expected True or False but got $val" );
+                       $val = null;
+               }
+       }
+
+       /**
+        * function to validate rational properties ( 12/10 )
+        *
+        * @param array $info Information about current property
+        * @param mixed &$val Current value to validate
+        * @param bool $standalone If this is a simple property or array
+        */
+       public function validateRational( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               if ( !preg_match( '/^(?:-?\d+)\/(?:\d+[1-9]|[1-9]\d*)$/D', $val ) ) {
+                       $this->logger->info( __METHOD__ . " Expected rational but got $val" );
+                       $val = null;
+               }
+       }
+
+       /**
+        * function to validate rating properties -1, 0-5
+        *
+        * if its outside of range put it into range.
+        *
+        * @see MWG spec
+        * @param array $info Information about current property
+        * @param mixed &$val Current value to validate
+        * @param bool $standalone If this is a simple property or array
+        */
+       public function validateRating( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               if ( !preg_match( '/^[-+]?\d*(?:\.?\d*)$/D', $val )
+                       || !is_numeric( $val )
+               ) {
+                       $this->logger->info( __METHOD__ . " Expected rating but got $val" );
+                       $val = null;
+
+                       return;
+               } else {
+                       $nVal = (float)$val;
+                       if ( $nVal < 0 ) {
+                               // We do < 0 here instead of < -1 here, since
+                               // the values between 0 and -1 are also illegal
+                               // as -1 is meant as a special reject rating.
+                               $this->logger->info( __METHOD__ . " Rating too low, setting to -1 (Rejected)" );
+                               $val = '-1';
+
+                               return;
+                       }
+                       if ( $nVal > 5 ) {
+                               $this->logger->info( __METHOD__ . " Rating too high, setting to 5" );
+                               $val = '5';
+
+                               return;
+                       }
+               }
+       }
+
+       /**
+        * function to validate integers
+        *
+        * @param array $info Information about current property
+        * @param mixed &$val Current value to validate
+        * @param bool $standalone If this is a simple property or array
+        */
+       public function validateInteger( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               if ( !preg_match( '/^[-+]?\d+$/D', $val ) ) {
+                       $this->logger->info( __METHOD__ . " Expected integer but got $val" );
+                       $val = null;
+               }
+       }
+
+       /**
+        * function to validate properties with a fixed number of allowed
+        * choices. (closed choice)
+        *
+        * @param array $info Information about current property
+        * @param mixed &$val Current value to validate
+        * @param bool $standalone If this is a simple property or array
+        */
+       public function validateClosed( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+
+               // check if its in a numeric range
+               $inRange = false;
+               if ( isset( $info['rangeLow'] )
+                       && isset( $info['rangeHigh'] )
+                       && is_numeric( $val )
+                       && ( intval( $val ) <= $info['rangeHigh'] )
+                       && ( intval( $val ) >= $info['rangeLow'] )
+               ) {
+                       $inRange = true;
+               }
+
+               if ( !isset( $info['choices'][$val] ) && !$inRange ) {
+                       $this->logger->info( __METHOD__ . " Expected closed choice, but got $val" );
+                       $val = null;
+               }
+       }
+
+       /**
+        * function to validate and modify flash structure
+        *
+        * @param array $info Information about current property
+        * @param mixed &$val Current value to validate
+        * @param bool $standalone If this is a simple property or array
+        */
+       public function validateFlash( $info, &$val, $standalone ) {
+               if ( $standalone ) {
+                       // this only validates flash structs, not individual properties
+                       return;
+               }
+               if ( !( isset( $val['Fired'] )
+                       && isset( $val['Function'] )
+                       && isset( $val['Mode'] )
+                       && isset( $val['RedEyeMode'] )
+                       && isset( $val['Return'] )
+               ) ) {
+                       $this->logger->info( __METHOD__ . " Flash structure did not have all the required components" );
+                       $val = null;
+               } else {
+                       $val = ( "\0" | ( $val['Fired'] === 'True' )
+                               | ( intval( $val['Return'] ) << 1 )
+                               | ( intval( $val['Mode'] ) << 3 )
+                               | ( ( $val['Function'] === 'True' ) << 5 )
+                               | ( ( $val['RedEyeMode'] === 'True' ) << 6 ) );
+               }
+       }
+
+       /**
+        * function to validate LangCode properties ( en-GB, etc )
+        *
+        * This is just a naive check to make sure it somewhat looks like a lang code.
+        *
+        * @see BCP 47
+        * @see https://wwwimages2.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/
+        *      XMP%20SDK%20Release%20cc-2014-12/XMPSpecificationPart1.pdf page 22 (section 8.2.2.4)
+        *
+        * @param array $info Information about current property
+        * @param mixed &$val Current value to validate
+        * @param bool $standalone If this is a simple property or array
+        */
+       public function validateLangCode( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               if ( !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $val ) ) {
+                       // this is a rather naive check.
+                       $this->logger->info( __METHOD__ . " Expected Lang code but got $val" );
+                       $val = null;
+               }
+       }
+
+       /**
+        * function to validate date properties, and convert to (partial) Exif format.
+        *
+        * Dates can be one of the following formats:
+        * YYYY
+        * YYYY-MM
+        * YYYY-MM-DD
+        * YYYY-MM-DDThh:mmTZD
+        * YYYY-MM-DDThh:mm:ssTZD
+        * YYYY-MM-DDThh:mm:ss.sTZD
+        *
+        * @param array $info Information about current property
+        * @param mixed &$val Current value to validate. Converts to TS_EXIF as a side-effect.
+        *    in cases where there's only a partial date, it will give things like
+        *    2011:04.
+        * @param bool $standalone If this is a simple property or array
+        */
+       public function validateDate( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               $res = [];
+               // @codingStandardsIgnoreStart Long line that cannot be broken
+               if ( !preg_match(
+                       /* ahh! scary regex... */
+                       '/^([0-3]\d{3})(?:-([01]\d)(?:-([0-3]\d)(?:T([0-2]\d):([0-6]\d)(?::([0-6]\d)(?:\.\d+)?)?([-+]\d{2}:\d{2}|Z)?)?)?)?$/D',
+                       $val, $res )
+               ) {
+                       // @codingStandardsIgnoreEnd
+
+                       $this->logger->info( __METHOD__ . " Expected date but got $val" );
+                       $val = null;
+               } else {
+                       /*
+                        * $res is formatted as follows:
+                        * 0 -> full date.
+                        * 1 -> year, 2-> month, 3-> day, 4-> hour, 5-> minute, 6->second
+                        * 7-> Timezone specifier (Z or something like +12:30 )
+                        * many parts are optional, some aren't. For example if you specify
+                        * minute, you must specify hour, day, month, and year but not second or TZ.
+                        */
+
+                       /*
+                        * First of all, if year = 0000, Something is wrongish,
+                        * so don't extract. This seems to happen when
+                        * some programs convert between metadata formats.
+                        */
+                       if ( $res[1] === '0000' ) {
+                               $this->logger->info( __METHOD__ . " Invalid date (year 0): $val" );
+                               $val = null;
+
+                               return;
+                       }
+
+                       if ( !isset( $res[4] ) ) { // hour
+                               // just have the year month day (if that)
+                               $val = $res[1];
+                               if ( isset( $res[2] ) ) {
+                                       $val .= ':' . $res[2];
+                               }
+                               if ( isset( $res[3] ) ) {
+                                       $val .= ':' . $res[3];
+                               }
+
+                               return;
+                       }
+
+                       if ( !isset( $res[7] ) || $res[7] === 'Z' ) {
+                               // if hour is set, then minute must also be or regex above will fail.
+                               $val = $res[1] . ':' . $res[2] . ':' . $res[3]
+                                       . ' ' . $res[4] . ':' . $res[5];
+                               if ( isset( $res[6] ) && $res[6] !== '' ) {
+                                       $val .= ':' . $res[6];
+                               }
+
+                               return;
+                       }
+
+                       // Extra check for empty string necessary due to TZ but no second case.
+                       $stripSeconds = false;
+                       if ( !isset( $res[6] ) || $res[6] === '' ) {
+                               $res[6] = '00';
+                               $stripSeconds = true;
+                       }
+
+                       // Do timezone processing. We've already done the case that tz = Z.
+
+                       // We know that if we got to this step, year, month day hour and min must be set
+                       // by virtue of regex not failing.
+
+                       $unix = ConvertibleTimestamp::convert( TS_UNIX,
+                               $res[1] . $res[2] . $res[3] . $res[4] . $res[5] . $res[6]
+                       );
+                       $offset = intval( substr( $res[7], 1, 2 ) ) * 60 * 60;
+                       $offset += intval( substr( $res[7], 4, 2 ) ) * 60;
+                       if ( substr( $res[7], 0, 1 ) === '-' ) {
+                               $offset = -$offset;
+                       }
+                       $val = ConvertibleTimestamp::convert( TS_EXIF, $unix + $offset );
+
+                       if ( $stripSeconds ) {
+                               // If seconds weren't specified, remove the trailing ':00'.
+                               $val = substr( $val, 0, -3 );
+                       }
+               }
+       }
+
+       /** function to validate, and more importantly
+        * translate the XMP DMS form of gps coords to
+        * the decimal form we use.
+        *
+        * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf
+        *        section 1.2.7.4 on page 23
+        *
+        * @param array $info Unused (info about prop)
+        * @param string &$val GPS string in either DDD,MM,SSk or
+        *   or DDD,MM.mmk form
+        * @param bool $standalone If its a simple prop (should always be true)
+        */
+       public function validateGPS( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       return;
+               }
+
+               $m = [];
+               if ( preg_match(
+                       '/(\d{1,3}),(\d{1,2}),(\d{1,2})([NWSE])/D',
+                       $val, $m )
+               ) {
+                       $coord = intval( $m[1] );
+                       $coord += intval( $m[2] ) * ( 1 / 60 );
+                       $coord += intval( $m[3] ) * ( 1 / 3600 );
+                       if ( $m[4] === 'S' || $m[4] === 'W' ) {
+                               $coord = -$coord;
+                       }
+                       $val = $coord;
+
+                       return;
+               } elseif ( preg_match(
+                       '/(\d{1,3}),(\d{1,2}(?:.\d*)?)([NWSE])/D',
+                       $val, $m )
+               ) {
+                       $coord = intval( $m[1] );
+                       $coord += floatval( $m[2] ) * ( 1 / 60 );
+                       if ( $m[3] === 'S' || $m[3] === 'W' ) {
+                               $coord = -$coord;
+                       }
+                       $val = $coord;
+
+                       return;
+               } else {
+                       $this->logger->info( __METHOD__
+                               . " Expected GPSCoordinate, but got $val." );
+                       $val = null;
+
+                       return;
+               }
+       }
+}
index 7746d99..21864ee 100644 (file)
@@ -714,6 +714,12 @@ class ManualLogEntry extends LogEntryBase {
                                        $rc = $this->getRecentChange( $newId );
 
                                        if ( $to === 'rc' || $to === 'rcandudp' ) {
+                                               // save RC, passing tags so they are applied there
+                                               $tags = $this->getTags();
+                                               if ( is_null( $tags ) ) {
+                                                       $tags = [];
+                                               }
+                                               $rc->addTags( $tags );
                                                $rc->save( 'pleasedontudp' );
                                        }
 
@@ -727,14 +733,6 @@ class ManualLogEntry extends LogEntryBase {
                                        ) {
                                                PatrolLog::record( $rc, true, $this->getPerformer() );
                                        }
-
-                                       // Add change tags to the log entry and (if applicable) the associated revision
-                                       $tags = $this->getTags();
-                                       if ( !is_null( $tags ) ) {
-                                               $rcId = $rc->getAttribute( 'rc_id' );
-                                               $revId = $this->getAssociatedRevId(); // Use null if $revId is 0
-                                               ChangeTags::addTags( $tags, $rcId, $revId > 0 ? $revId : null, $newId );
-                                       }
                                }
                        },
                        DeferredUpdates::POSTSEND,
index f29c9e4..0cf584b 100644 (file)
@@ -310,7 +310,7 @@ class LogEventsList extends ContextSource {
                        return '';
                }
                $html = '';
-               $html .= xml::label( wfMessage( 'log-action-filter-' . $types[0] )->text(),
+               $html .= Xml::label( wfMessage( 'log-action-filter-' . $types[0] )->text(),
                        'action-filter-' .$types[0] ) . "\n";
                $select = new XmlSelect( 'subtype' );
                $select->addOption( wfMessage( 'log-action-filter-all' )->text(), '' );
@@ -319,7 +319,7 @@ class LogEventsList extends ContextSource {
                        $select->addOption( wfMessage( $msgKey )->text(), $value );
                }
                $select->setDefault( $action );
-               $html .= $select->getHtml();
+               $html .= $select->getHTML();
                return $html;
        }
 
index 4b9b268..0229ac1 100644 (file)
@@ -51,7 +51,7 @@ class BmpHandler extends BitmapHandler {
        /**
         * Get width and height from the bmp header.
         *
-        * @param File $image
+        * @param File|FSFile $image
         * @param string $filename
         * @return array
         */
index ccd345c..c86eabd 100644 (file)
@@ -276,7 +276,7 @@ class BitmapHandler extends TransformationalImageHandler {
         */
        protected function transformImageMagickExt( $image, $params ) {
                global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
-                       $wgMaxInterlacingAreas, $wgJpegPixelFormat;
+                       $wgJpegPixelFormat;
 
                try {
                        $im = new Imagick();
index 9add138..18f75ec 100644 (file)
@@ -235,7 +235,7 @@ class DjVuHandler extends ImageHandler {
        /**
         * Cache an instance of DjVuImage in an Image object, return that instance
         *
-        * @param File $image
+        * @param File|FSFile $image
         * @param string $path
         * @return DjVuImage
         */
@@ -335,11 +335,6 @@ class DjVuHandler extends ImageHandler {
                }
        }
 
-       /**
-        * @param File $image
-        * @param string $path
-        * @return bool|array False on failure
-        */
        function getImageSize( $image, $path ) {
                return $this->getDjVuImage( $image, $path )->getImageSize();
        }
index 732be3d..7aeefa0 100644 (file)
@@ -165,7 +165,7 @@ class ExifBitmapHandler extends BitmapHandler {
         * Wrapper for base classes ImageHandler::getImageSize() that checks for
         * rotation reported from metadata and swaps the sizes to match.
         *
-        * @param File $image
+        * @param File|FSFile $image
         * @param string $path
         * @return array
         */
index 81722c6..9ad4097 100644 (file)
@@ -155,7 +155,7 @@ class JpegMetadataExtractor {
                        } else {
                                // segment we don't care about, so skip
                                $size = wfUnpack( "nint", fread( $fh, 2 ), 2 );
-                               if ( $size['int'] <= 2 ) {
+                               if ( $size['int'] < 2 ) {
                                        throw new MWException( "invalid marker size in jpeg" );
                                }
                                fseek( $fh, $size['int'] - 2, SEEK_CUR );
@@ -173,9 +173,13 @@ class JpegMetadataExtractor {
         */
        private static function jpegExtractMarker( &$fh ) {
                $size = wfUnpack( "nint", fread( $fh, 2 ), 2 );
-               if ( $size['int'] <= 2 ) {
+               if ( $size['int'] < 2 ) {
                        throw new MWException( "invalid marker size in jpeg" );
                }
+               if ( $size['int'] === 2 ) {
+                       // fread( ..., 0 ) generates a warning
+                       return '';
+               }
                $segment = fread( $fh, $size['int'] - 2 );
                if ( strlen( $segment ) !== $size['int'] - 2 ) {
                        throw new MWException( "Segment shorter than expected" );
index 70a43f2..4bc36ba 100644 (file)
@@ -100,21 +100,22 @@ abstract class MediaHandler {
         * @note If this is a multipage file, return the width and height of the
         *  first page.
         *
-        * @param File $image The image object, or false if there isn't one
+        * @param File|FSFile $image The image object, or false if there isn't one.
+        *   Warning, FSFile::getPropsFromPath might pass an FSFile instead of File (!)
         * @param string $path The filename
-        * @return array Follow the format of PHP getimagesize() internal function.
+        * @return array|bool Follow the format of PHP getimagesize() internal function.
         *   See http://www.php.net/getimagesize. MediaWiki will only ever use the
         *   first two array keys (the width and height), and the 'bits' associative
         *   key. All other array keys are ignored. Returning a 'bits' key is optional
-        *   as not all formats have a notion of "bitdepth".
+        *   as not all formats have a notion of "bitdepth". Returns false on failure.
         */
        abstract function getImageSize( $image, $path );
 
        /**
         * Get handler-specific metadata which will be saved in the img_metadata field.
         *
-        * @param File $image The image object, or false if there isn't one.
-        *   Warning, FSFile::getPropsFromPath might pass an (object)array() instead (!)
+        * @param File|FSFile $image The image object, or false if there isn't one.
+        *   Warning, FSFile::getPropsFromPath might pass an FSFile instead of File (!)
         * @param string $path The filename
         * @return string A string of metadata in php serialized form (Run through serialize())
         */
@@ -462,16 +463,16 @@ abstract class MediaHandler {
        /**
         * Get an array structure that looks like this:
         *
-        * array(
-        *    'visible' => array(
+        * [
+        *    'visible' => [
         *       'Human-readable name' => 'Human readable value',
         *       ...
-        *    ),
-        *    'collapsed' => array(
+        *    ],
+        *    'collapsed' => [
         *       'Human-readable name' => 'Human readable value',
         *       ...
-        *    )
-        * )
+        *    ]
+        * ]
         * The UI will format this into a table where the visible fields are always
         * visible, and the collapsed fields are optionally visible.
         *
@@ -842,11 +843,11 @@ abstract class MediaHandler {
        /**
         * Gets configuration for the file warning message. Return value of
         * the following structure:
-        *   array(
+        *   [
         *     // Required, module with messages loaded for the client
         *     'module' => 'example.filewarning.messages',
         *     // Required, array of names of messages
-        *     'messages' => array(
+        *     'messages' => [
         *       // Required, main warning message
         *       'main' => 'example-filewarning-main',
         *       // Optional, header for warning dialog
@@ -855,10 +856,10 @@ abstract class MediaHandler {
         *       'footer' => 'example-filewarning-footer',
         *       // Optional, text for more-information link (see below)
         *       'info' => 'example-filewarning-info',
-        *     ),
+        *     ],
         *     // Optional, link for more information
         *     'link' => 'http://example.com',
-        *   )
+        *   ]
         *
         * Returns null if no warning is necessary.
         * @param File $file
index 8a3e001..294abb3 100644 (file)
@@ -30,7 +30,7 @@ class PNGHandler extends BitmapHandler {
        const BROKEN_FILE = '0';
 
        /**
-        * @param File $image
+        * @param File|FSFile $image
         * @param string $filename
         * @return string
         */
index 2bb6d13..8360920 100644 (file)
@@ -301,13 +301,13 @@ class SvgHandler extends ImageHandler {
        }
 
        /**
-        * @param File $file
+        * @param File|FSFile $file
         * @param string $path Unused
         * @param bool|array $metadata
         * @return array
         */
        function getImageSize( $file, $path, $metadata = false ) {
-               if ( $metadata === false ) {
+               if ( $metadata === false && $file instanceof File ) {
                        $metadata = $file->getMetadata();
                }
                $metadata = $this->unpackMetadata( $metadata );
@@ -355,7 +355,7 @@ class SvgHandler extends ImageHandler {
        }
 
        /**
-        * @param File $file
+        * @param File|FSFile $file
         * @param string $filename
         * @return string Serialised metadata
         */
index 2e73249..f0f4cda 100644 (file)
@@ -71,13 +71,14 @@ class TiffHandler extends ExifBitmapHandler {
        }
 
        /**
-        * @param File $image
+        * @param File|FSFile $image
         * @param string $filename
         * @throws MWException
         * @return string
         */
        function getMetadata( $image, $filename ) {
                global $wgShowEXIF;
+
                if ( $wgShowEXIF ) {
                        try {
                                $meta = BitmapMetadataHandler::Tiff( $filename );
index 3287fac..11c4d42 100644 (file)
@@ -25,6 +25,7 @@
  * @file
  * @ingroup Media
  */
+use MediaWiki\MediaWikiServices;
 
 /**
  * Handler for images that need to be transformed
@@ -302,7 +303,7 @@ abstract class TransformationalImageHandler extends ImageHandler {
         * Values can be one of client, im, custom, gd, imext, or an array
         * of object, method-name to call that specific method.
         *
-        * If specifying a custom scaler command with array( Obj, method ),
+        * If specifying a custom scaler command with [ Obj, method ],
         * the method in question should take 2 parameters, a File object,
         * and a $scalerParams array with various options (See doTransform
         * for what is in $scalerParams). On error it should return a
@@ -509,7 +510,7 @@ abstract class TransformationalImageHandler extends ImageHandler {
         * @return string|bool Representing the IM version; false on error
         */
        protected function getMagickVersion() {
-               $cache = ObjectCache::getLocalServerInstance( CACHE_NONE );
+               $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
                return $cache->getWithSetCallback(
                        'imagemagick-version',
                        $cache::TTL_HOUR,
index 35e885f..e2c2d2d 100644 (file)
@@ -230,7 +230,7 @@ class WebPHandler extends BitmapHandler {
                if ( $file === null ) {
                        $metadata = self::getMetadata( $file, $path );
                }
-               if ( $metadata === false ) {
+               if ( $metadata === false && $file instanceof File ) {
                        $metadata = $file->getMetadata();
                }
 
index 6ac675e..108d6fb 100644 (file)
@@ -56,7 +56,7 @@ class XCFHandler extends BitmapHandler {
        /**
         * Get width and height from the XCF header.
         *
-        * @param File $image
+        * @param File|FSFile $image
         * @param string $filename
         * @return array
         */
@@ -149,7 +149,7 @@ class XCFHandler extends BitmapHandler {
         *
         * Greyscale files need different command line options.
         *
-        * @param File $file The image object, or false if there isn't one.
+        * @param File|FSFile $file The image object, or false if there isn't one.
         *   Warning, FSFile::getPropsFromPath might pass an (object)array() instead (!)
         * @param string $filename The filename
         * @return string
diff --git a/includes/media/XMP.php b/includes/media/XMP.php
deleted file mode 100644 (file)
index 70f67b7..0000000
+++ /dev/null
@@ -1,1383 +0,0 @@
-<?php
-/**
- * Reader for XMP data containing properties relevant to images.
- *
- * 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 Media
- */
-
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-
-/**
- * Class for reading xmp data containing properties relevant to
- * images, and spitting out an array that FormatMetadata accepts.
- *
- * Note, this is not meant to recognize every possible thing you can
- * encode in XMP. It should recognize all the properties we want.
- * For example it doesn't have support for structures with multiple
- * nesting levels, as none of the properties we're supporting use that
- * feature. If it comes across properties it doesn't recognize, it should
- * ignore them.
- *
- * The public methods one would call in this class are
- * - parse( $content )
- *    Reads in xmp content.
- *    Can potentially be called multiple times with partial data each time.
- * - parseExtended( $content )
- *    Reads XMPExtended blocks (jpeg files only).
- * - getResults
- *    Outputs a results array.
- *
- * Note XMP kind of looks like rdf. They are not the same thing - XMP is
- * encoded as a specific subset of rdf. This class can read XMP. It cannot
- * read rdf.
- *
- */
-class XMPReader implements LoggerAwareInterface {
-       /** @var array XMP item configuration array */
-       protected $items;
-
-       /** @var array Array to hold the current element (and previous element, and so on) */
-       private $curItem = [];
-
-       /** @var bool|string The structure name when processing nested structures. */
-       private $ancestorStruct = false;
-
-       /** @var bool|string Temporary holder for character data that appears in xmp doc. */
-       private $charContent = false;
-
-       /** @var array Stores the state the xmpreader is in (see MODE_FOO constants) */
-       private $mode = [];
-
-       /** @var array Array to hold results */
-       private $results = [];
-
-       /** @var bool If we're doing a seq or bag. */
-       private $processingArray = false;
-
-       /** @var bool|string Used for lang alts only */
-       private $itemLang = false;
-
-       /** @var resource A resource handle for the XML parser */
-       private $xmlParser;
-
-       /** @var bool|string Character set like 'UTF-8' */
-       private $charset = false;
-
-       /** @var int */
-       private $extendedXMPOffset = 0;
-
-       /** @var int Flag determining if the XMP is safe to parse **/
-       private $parsable = 0;
-
-       /** @var string Buffer of XML to parse **/
-       private $xmlParsableBuffer = '';
-
-       /**
-        * These are various mode constants.
-        * they are used to figure out what to do
-        * with an element when its encountered.
-        *
-        * For example, MODE_IGNORE is used when processing
-        * a property we're not interested in. So if a new
-        * element pops up when we're in that mode, we ignore it.
-        */
-       const MODE_INITIAL = 0;
-       const MODE_IGNORE = 1;
-       const MODE_LI = 2;
-       const MODE_LI_LANG = 3;
-       const MODE_QDESC = 4;
-
-       // The following MODE constants are also used in the
-       // $items array to denote what type of property the item is.
-       const MODE_SIMPLE = 10;
-       const MODE_STRUCT = 11; // structure (associative array)
-       const MODE_SEQ = 12; // ordered list
-       const MODE_BAG = 13; // unordered list
-       const MODE_LANG = 14;
-       const MODE_ALT = 15; // non-language alt. Currently not implemented, and not needed atm.
-       const MODE_BAGSTRUCT = 16; // A BAG of Structs.
-
-       const NS_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
-       const NS_XML = 'http://www.w3.org/XML/1998/namespace';
-
-       // States used while determining if XML is safe to parse
-       const PARSABLE_UNKNOWN = 0;
-       const PARSABLE_OK = 1;
-       const PARSABLE_BUFFERING = 2;
-       const PARSABLE_NO = 3;
-
-       /**
-        * @var LoggerInterface
-        */
-       private $logger;
-
-       /**
-        * Constructor.
-        *
-        * Primary job is to initialize the XMLParser
-        */
-       function __construct( LoggerInterface $logger = null ) {
-
-               if ( !function_exists( 'xml_parser_create_ns' ) ) {
-                       // this should already be checked by this point
-                       throw new RuntimeException( 'XMP support requires XML Parser' );
-               }
-               if ( $logger ) {
-                       $this->setLogger( $logger );
-               } else {
-                       $this->setLogger( new NullLogger() );
-               }
-
-               $this->items = XMPInfo::getItems();
-
-               $this->resetXMLParser();
-       }
-
-       public function setLogger( LoggerInterface $logger ) {
-               $this->logger = $logger;
-       }
-
-       /**
-        * free the XML parser.
-        *
-        * @note It is unclear to me if we really need to do this ourselves
-        *  or if php garbage collection will automatically free the xmlParser
-        *  when it is no longer needed.
-        */
-       private function destroyXMLParser() {
-               if ( $this->xmlParser ) {
-                       xml_parser_free( $this->xmlParser );
-                       $this->xmlParser = null;
-               }
-       }
-
-       /**
-        * Main use is if a single item has multiple xmp documents describing it.
-        * For example in jpeg's with extendedXMP
-        */
-       private function resetXMLParser() {
-
-               $this->destroyXMLParser();
-
-               $this->xmlParser = xml_parser_create_ns( 'UTF-8', ' ' );
-               xml_parser_set_option( $this->xmlParser, XML_OPTION_CASE_FOLDING, 0 );
-               xml_parser_set_option( $this->xmlParser, XML_OPTION_SKIP_WHITE, 1 );
-
-               xml_set_element_handler( $this->xmlParser,
-                       [ $this, 'startElement' ],
-                       [ $this, 'endElement' ] );
-
-               xml_set_character_data_handler( $this->xmlParser, [ $this, 'char' ] );
-
-               $this->parsable = self::PARSABLE_UNKNOWN;
-               $this->xmlParsableBuffer = '';
-       }
-
-       /**
-        * Check if this instance supports using this class
-        */
-       public static function isSupported() {
-               return function_exists( 'xml_parser_create_ns' ) && class_exists( 'XMLReader' );
-       }
-
-       /** Get the result array. Do some post-processing before returning
-        * the array, and transform any metadata that is special-cased.
-        *
-        * @return array Array of results as an array of arrays suitable for
-        *    FormatMetadata::getFormattedData().
-        */
-       public function getResults() {
-               // xmp-special is for metadata that affects how stuff
-               // is extracted. For example xmpNote:HasExtendedXMP.
-
-               // It is also used to handle photoshop:AuthorsPosition
-               // which is weird and really part of another property,
-               // see 2:85 in IPTC. See also pg 21 of IPTC4XMP standard.
-               // The location fields also use it.
-
-               $data = $this->results;
-
-               if ( isset( $data['xmp-special']['AuthorsPosition'] )
-                       && is_string( $data['xmp-special']['AuthorsPosition'] )
-                       && isset( $data['xmp-general']['Artist'][0] )
-               ) {
-                       // Note, if there is more than one creator,
-                       // this only applies to first. This also will
-                       // only apply to the dc:Creator prop, not the
-                       // exif:Artist prop.
-
-                       $data['xmp-general']['Artist'][0] =
-                               $data['xmp-special']['AuthorsPosition'] . ', '
-                               . $data['xmp-general']['Artist'][0];
-               }
-
-               // Go through the LocationShown and LocationCreated
-               // changing it to the non-hierarchal form used by
-               // the other location fields.
-
-               if ( isset( $data['xmp-special']['LocationShown'][0] )
-                       && is_array( $data['xmp-special']['LocationShown'][0] )
-               ) {
-                       // the is_array is just paranoia. It should always
-                       // be an array.
-                       foreach ( $data['xmp-special']['LocationShown'] as $loc ) {
-                               if ( !is_array( $loc ) ) {
-                                       // To avoid copying over the _type meta-fields.
-                                       continue;
-                               }
-                               foreach ( $loc as $field => $val ) {
-                                       $data['xmp-general'][$field . 'Dest'][] = $val;
-                               }
-                       }
-               }
-               if ( isset( $data['xmp-special']['LocationCreated'][0] )
-                       && is_array( $data['xmp-special']['LocationCreated'][0] )
-               ) {
-                       // the is_array is just paranoia. It should always
-                       // be an array.
-                       foreach ( $data['xmp-special']['LocationCreated'] as $loc ) {
-                               if ( !is_array( $loc ) ) {
-                                       // To avoid copying over the _type meta-fields.
-                                       continue;
-                               }
-                               foreach ( $loc as $field => $val ) {
-                                       $data['xmp-general'][$field . 'Created'][] = $val;
-                               }
-                       }
-               }
-
-               // We don't want to return the special values, since they're
-               // special and not info to be stored about the file.
-               unset( $data['xmp-special'] );
-
-               // Convert GPSAltitude to negative if below sea level.
-               if ( isset( $data['xmp-exif']['GPSAltitudeRef'] )
-                       && isset( $data['xmp-exif']['GPSAltitude'] )
-               ) {
-
-                       // Must convert to a real before multiplying by -1
-                       // XMPValidate guarantees there will always be a '/' in this value.
-                       list( $nom, $denom ) = explode( '/', $data['xmp-exif']['GPSAltitude'] );
-                       $data['xmp-exif']['GPSAltitude'] = $nom / $denom;
-
-                       if ( $data['xmp-exif']['GPSAltitudeRef'] == '1' ) {
-                               $data['xmp-exif']['GPSAltitude'] *= -1;
-                       }
-                       unset( $data['xmp-exif']['GPSAltitudeRef'] );
-               }
-
-               return $data;
-       }
-
-       /**
-        * Main function to call to parse XMP. Use getResults to
-        * get results.
-        *
-        * Also catches any errors during processing, writes them to
-        * debug log, blanks result array and returns false.
-        *
-        * @param string $content XMP data
-        * @param bool $allOfIt If this is all the data (true) or if its split up (false). Default true
-        * @throws RuntimeException
-        * @return bool Success.
-        */
-       public function parse( $content, $allOfIt = true ) {
-               if ( !$this->xmlParser ) {
-                       $this->resetXMLParser();
-               }
-               try {
-
-                       // detect encoding by looking for BOM which is supposed to be in processing instruction.
-                       // see page 12 of http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart3.pdf
-                       if ( !$this->charset ) {
-                               $bom = [];
-                               if ( preg_match( '/\xEF\xBB\xBF|\xFE\xFF|\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\xFF\xFE/',
-                                       $content, $bom )
-                               ) {
-                                       switch ( $bom[0] ) {
-                                               case "\xFE\xFF":
-                                                       $this->charset = 'UTF-16BE';
-                                                       break;
-                                               case "\xFF\xFE":
-                                                       $this->charset = 'UTF-16LE';
-                                                       break;
-                                               case "\x00\x00\xFE\xFF":
-                                                       $this->charset = 'UTF-32BE';
-                                                       break;
-                                               case "\xFF\xFE\x00\x00":
-                                                       $this->charset = 'UTF-32LE';
-                                                       break;
-                                               case "\xEF\xBB\xBF":
-                                                       $this->charset = 'UTF-8';
-                                                       break;
-                                               default:
-                                                       // this should be impossible to get to
-                                                       throw new RuntimeException( "Invalid BOM" );
-                                       }
-                               } else {
-                                       // standard specifically says, if no bom assume utf-8
-                                       $this->charset = 'UTF-8';
-                               }
-                       }
-                       if ( $this->charset !== 'UTF-8' ) {
-                               // don't convert if already utf-8
-                               MediaWiki\suppressWarnings();
-                               $content = iconv( $this->charset, 'UTF-8//IGNORE', $content );
-                               MediaWiki\restoreWarnings();
-                       }
-
-                       // Ensure the XMP block does not have an xml doctype declaration, which
-                       // could declare entities unsafe to parse with xml_parse (T85848/T71210).
-                       if ( $this->parsable !== self::PARSABLE_OK ) {
-                               if ( $this->parsable === self::PARSABLE_NO ) {
-                                       throw new RuntimeException( 'Unsafe doctype declaration in XML.' );
-                               }
-
-                               $content = $this->xmlParsableBuffer . $content;
-                               if ( !$this->checkParseSafety( $content ) ) {
-                                       if ( !$allOfIt && $this->parsable !== self::PARSABLE_NO ) {
-                                               // parse wasn't Unsuccessful yet, so return true
-                                               // in this case.
-                                               return true;
-                                       }
-                                       $msg = ( $this->parsable === self::PARSABLE_NO ) ?
-                                               'Unsafe doctype declaration in XML.' :
-                                               'No root element found in XML.';
-                                       throw new RuntimeException( $msg );
-                               }
-                       }
-
-                       $ok = xml_parse( $this->xmlParser, $content, $allOfIt );
-                       if ( !$ok ) {
-                               $code = xml_get_error_code( $this->xmlParser );
-                               $error = xml_error_string( $code );
-                               $line = xml_get_current_line_number( $this->xmlParser );
-                               $col = xml_get_current_column_number( $this->xmlParser );
-                               $offset = xml_get_current_byte_index( $this->xmlParser );
-
-                               $this->logger->warning(
-                                       '{method} : Error reading XMP content: {error} ' .
-                                       '(line: {line} column: {column} byte offset: {offset})',
-                                       [
-                                               'method' => __METHOD__,
-                                               'error_code' => $code,
-                                               'error' => $error,
-                                               'line' => $line,
-                                               'column' => $col,
-                                               'offset' => $offset,
-                                               'content' => $content,
-                               ] );
-                               $this->results = []; // blank if error.
-                               $this->destroyXMLParser();
-                               return false;
-                       }
-               } catch ( Exception $e ) {
-                       $this->logger->warning(
-                               '{method} Exception caught while parsing: ' . $e->getMessage(),
-                               [
-                                       'method' => __METHOD__,
-                                       'exception' => $e,
-                                       'content' => $content,
-                               ]
-                       );
-                       $this->results = [];
-                       return false;
-               }
-               if ( $allOfIt ) {
-                       $this->destroyXMLParser();
-               }
-
-               return true;
-       }
-
-       /** Entry point for XMPExtended blocks in jpeg files
-        *
-        * @todo In serious need of testing
-        * @see http://www.adobe.ge/devnet/xmp/pdfs/XMPSpecificationPart3.pdf XMP spec part 3 page 20
-        * @param string $content XMPExtended block minus the namespace signature
-        * @return bool If it succeeded.
-        */
-       public function parseExtended( $content ) {
-               // @todo FIXME: This is untested. Hard to find example files
-               // or programs that make such files..
-               $guid = substr( $content, 0, 32 );
-               if ( !isset( $this->results['xmp-special']['HasExtendedXMP'] )
-                       || $this->results['xmp-special']['HasExtendedXMP'] !== $guid
-               ) {
-                       $this->logger->info( __METHOD__ .
-                               " Ignoring XMPExtended block due to wrong guid (guid= '$guid')" );
-
-                       return false;
-               }
-               $len = unpack( 'Nlength/Noffset', substr( $content, 32, 8 ) );
-
-               if ( !$len ||
-                       $len['length'] < 4 ||
-                       $len['offset'] < 0 ||
-                       $len['offset'] > $len['length']
-               ) {
-                       $this->logger->info(
-                               __METHOD__ . 'Error reading extended XMP block, invalid length or offset.'
-                       );
-
-                       return false;
-               }
-
-               // we're not very robust here. we should accept it in the wrong order.
-               // To quote the XMP standard:
-               // "A JPEG writer should write the ExtendedXMP marker segments in order,
-               // immediately following the StandardXMP. However, the JPEG standard
-               // does not require preservation of marker segment order. A robust JPEG
-               // reader should tolerate the marker segments in any order."
-               // On the other hand, the probability that an image will have more than
-               // 128k of metadata is rather low... so the probability that it will have
-               // > 128k, and be in the wrong order is very low...
-
-               if ( $len['offset'] !== $this->extendedXMPOffset ) {
-                       $this->logger->info( __METHOD__ . 'Ignoring XMPExtended block due to wrong order. (Offset was '
-                               . $len['offset'] . ' but expected ' . $this->extendedXMPOffset . ')' );
-
-                       return false;
-               }
-
-               if ( $len['offset'] === 0 ) {
-                       // if we're starting the extended block, we've probably already
-                       // done the XMPStandard block, so reset.
-                       $this->resetXMLParser();
-               }
-
-               $this->extendedXMPOffset += $len['length'];
-
-               $actualContent = substr( $content, 40 );
-
-               if ( $this->extendedXMPOffset === strlen( $actualContent ) ) {
-                       $atEnd = true;
-               } else {
-                       $atEnd = false;
-               }
-
-               $this->logger->debug( __METHOD__ . 'Parsing a XMPExtended block' );
-
-               return $this->parse( $actualContent, $atEnd );
-       }
-
-       /**
-        * Character data handler
-        * Called whenever character data is found in the xmp document.
-        *
-        * does nothing if we're in MODE_IGNORE or if the data is whitespace
-        * throws an error if we're not in MODE_SIMPLE (as we're not allowed to have character
-        * data in the other modes).
-        *
-        * As an example, this happens when we encounter XMP like:
-        * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
-        * and are processing the 0/10 bit.
-        *
-        * @param XMLParser $parser XMLParser reference to the xml parser
-        * @param string $data Character data
-        * @throws RuntimeException On invalid data
-        */
-       function char( $parser, $data ) {
-
-               $data = trim( $data );
-               if ( trim( $data ) === "" ) {
-                       return;
-               }
-
-               if ( !isset( $this->mode[0] ) ) {
-                       throw new RuntimeException( 'Unexpected character data before first rdf:Description element' );
-               }
-
-               if ( $this->mode[0] === self::MODE_IGNORE ) {
-                       return;
-               }
-
-               if ( $this->mode[0] !== self::MODE_SIMPLE
-                       && $this->mode[0] !== self::MODE_QDESC
-               ) {
-                       throw new RuntimeException( 'character data where not expected. (mode ' . $this->mode[0] . ')' );
-               }
-
-               // to check, how does this handle w.s.
-               if ( $this->charContent === false ) {
-                       $this->charContent = $data;
-               } else {
-                       $this->charContent .= $data;
-               }
-       }
-
-       /**
-        * Check if a block of XML is safe to pass to xml_parse, i.e. doesn't
-        * contain a doctype declaration which could contain a dos attack if we
-        * parse it and expand internal entities (T85848).
-        *
-        * @param string $content xml string to check for parse safety
-        * @return bool true if the xml is safe to parse, false otherwise
-        */
-       private function checkParseSafety( $content ) {
-               $reader = new XMLReader();
-               $result = null;
-
-               // For XMLReader to parse incomplete/invalid XML, it has to be open()'ed
-               // instead of using XML().
-               $reader->open(
-                       'data://text/plain,' . urlencode( $content ),
-                       null,
-                       LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET
-               );
-
-               $oldDisable = libxml_disable_entity_loader( true );
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $reset = new ScopedCallback(
-                       'libxml_disable_entity_loader',
-                       [ $oldDisable ]
-               );
-               $reader->setParserProperty( XMLReader::SUBST_ENTITIES, false );
-
-               // Even with LIBXML_NOWARNING set, XMLReader::read gives a warning
-               // when parsing truncated XML, which causes unit tests to fail.
-               MediaWiki\suppressWarnings();
-               while ( $reader->read() ) {
-                       if ( $reader->nodeType === XMLReader::ELEMENT ) {
-                               // Reached the first element without hitting a doctype declaration
-                               $this->parsable = self::PARSABLE_OK;
-                               $result = true;
-                               break;
-                       }
-                       if ( $reader->nodeType === XMLReader::DOC_TYPE ) {
-                               $this->parsable = self::PARSABLE_NO;
-                               $result = false;
-                               break;
-                       }
-               }
-               MediaWiki\restoreWarnings();
-
-               if ( !is_null( $result ) ) {
-                       return $result;
-               }
-
-               // Reached the end of the parsable xml without finding an element
-               // or doctype. Buffer and try again.
-               $this->parsable = self::PARSABLE_BUFFERING;
-               $this->xmlParsableBuffer = $content;
-               return false;
-       }
-
-       /** When we hit a closing element in MODE_IGNORE
-        * Check to see if this is the element we started to ignore,
-        * in which case we get out of MODE_IGNORE
-        *
-        * @param string $elm Namespace of element followed by a space and then tag name of element.
-        */
-       private function endElementModeIgnore( $elm ) {
-               if ( $this->curItem[0] === $elm ) {
-                       array_shift( $this->curItem );
-                       array_shift( $this->mode );
-               }
-       }
-
-       /**
-        * Hit a closing element when in MODE_SIMPLE.
-        * This generally means that we finished processing a
-        * property value, and now have to save the result to the
-        * results array
-        *
-        * For example, when processing:
-        * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
-        * this deals with when we hit </exif:DigitalZoomRatio>.
-        *
-        * Or it could be if we hit the end element of a property
-        * of a compound data structure (like a member of an array).
-        *
-        * @param string $elm Namespace, space, and tag name.
-        */
-       private function endElementModeSimple( $elm ) {
-               if ( $this->charContent !== false ) {
-                       if ( $this->processingArray ) {
-                               // if we're processing an array, use the original element
-                               // name instead of rdf:li.
-                               list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
-                       } else {
-                               list( $ns, $tag ) = explode( ' ', $elm, 2 );
-                       }
-                       $this->saveValue( $ns, $tag, $this->charContent );
-
-                       $this->charContent = false; // reset
-               }
-               array_shift( $this->curItem );
-               array_shift( $this->mode );
-       }
-
-       /**
-        * Hit a closing element in MODE_STRUCT, MODE_SEQ, MODE_BAG
-        * generally means we've finished processing a nested structure.
-        * resets some internal variables to indicate that.
-        *
-        * Note this means we hit the closing element not the "</rdf:Seq>".
-        *
-        * @par For example, when processing:
-        * @code{,xml}
-        * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
-        *   </rdf:Seq> </exif:ISOSpeedRatings>
-        * @endcode
-        *
-        * This method is called when we hit the "</exif:ISOSpeedRatings>" tag.
-        *
-        * @param string $elm Namespace . space . tag name.
-        * @throws RuntimeException
-        */
-       private function endElementNested( $elm ) {
-
-               /* cur item must be the same as $elm, unless if in MODE_STRUCT
-                  in which case it could also be rdf:Description */
-               if ( $this->curItem[0] !== $elm
-                       && !( $elm === self::NS_RDF . ' Description'
-                               && $this->mode[0] === self::MODE_STRUCT )
-               ) {
-                       throw new RuntimeException( "nesting mismatch. got a </$elm> but expected a </" .
-                               $this->curItem[0] . '>' );
-               }
-
-               // Validate structures.
-               list( $ns, $tag ) = explode( ' ', $elm, 2 );
-               if ( isset( $this->items[$ns][$tag]['validate'] ) ) {
-                       $info =& $this->items[$ns][$tag];
-                       $finalName = isset( $info['map_name'] )
-                               ? $info['map_name'] : $tag;
-
-                       if ( is_array( $info['validate'] ) ) {
-                               $validate = $info['validate'];
-                       } else {
-                               $validator = new XMPValidate( $this->logger );
-                               $validate = [ $validator, $info['validate'] ];
-                       }
-
-                       if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) {
-                               // This can happen if all the members of the struct failed validation.
-                               $this->logger->debug( __METHOD__ . " <$ns:$tag> has no valid members." );
-                       } elseif ( is_callable( $validate ) ) {
-                               $val =& $this->results['xmp-' . $info['map_group']][$finalName];
-                               call_user_func_array( $validate, [ $info, &$val, false ] );
-                               if ( is_null( $val ) ) {
-                                       // the idea being the validation function will unset the variable if
-                                       // its invalid.
-                                       $this->logger->info( __METHOD__ . " <$ns:$tag> failed validation." );
-                                       unset( $this->results['xmp-' . $info['map_group']][$finalName] );
-                               }
-                       } else {
-                               $this->logger->warning( __METHOD__ . " Validation function for $finalName ("
-                                       . $validate[0] . '::' . $validate[1] . '()) is not callable.' );
-                       }
-               }
-
-               array_shift( $this->curItem );
-               array_shift( $this->mode );
-               $this->ancestorStruct = false;
-               $this->processingArray = false;
-               $this->itemLang = false;
-       }
-
-       /**
-        * Hit a closing element in MODE_LI (either rdf:Seq, or rdf:Bag )
-        * Add information about what type of element this is.
-        *
-        * Note we still have to hit the outer "</property>"
-        *
-        * @par For example, when processing:
-        * @code{,xml}
-        * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
-        *   </rdf:Seq> </exif:ISOSpeedRatings>
-        * @endcode
-        *
-        * This method is called when we hit the "</rdf:Seq>".
-        * (For comparison, we call endElementModeSimple when we
-        * hit the "</rdf:li>")
-        *
-        * @param string $elm Namespace . ' ' . element name
-        * @throws RuntimeException
-        */
-       private function endElementModeLi( $elm ) {
-               list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
-               $info = $this->items[$ns][$tag];
-               $finalName = isset( $info['map_name'] )
-                       ? $info['map_name'] : $tag;
-
-               array_shift( $this->mode );
-
-               if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) {
-                       $this->logger->debug( __METHOD__ . " Empty compund element $finalName." );
-
-                       return;
-               }
-
-               if ( $elm === self::NS_RDF . ' Seq' ) {
-                       $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'ol';
-               } elseif ( $elm === self::NS_RDF . ' Bag' ) {
-                       $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'ul';
-               } elseif ( $elm === self::NS_RDF . ' Alt' ) {
-                       // extra if needed as you could theoretically have a non-language alt.
-                       if ( $info['mode'] === self::MODE_LANG ) {
-                               $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'lang';
-                       }
-               } else {
-                       throw new RuntimeException(
-                               __METHOD__ . " expected </rdf:seq> or </rdf:bag> but instead got $elm."
-                       );
-               }
-       }
-
-       /**
-        * End element while in MODE_QDESC
-        * mostly when ending an element when we have a simple value
-        * that has qualifiers.
-        *
-        * Qualifiers aren't all that common, and we don't do anything
-        * with them.
-        *
-        * @param string $elm Namespace and element
-        */
-       private function endElementModeQDesc( $elm ) {
-
-               if ( $elm === self::NS_RDF . ' value' ) {
-                       list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
-                       $this->saveValue( $ns, $tag, $this->charContent );
-
-                       return;
-               } else {
-                       array_shift( $this->mode );
-                       array_shift( $this->curItem );
-               }
-       }
-
-       /**
-        * Handler for hitting a closing element.
-        *
-        * generally just calls a helper function depending on what
-        * mode we're in.
-        *
-        * Ignores the outer wrapping elements that are optional in
-        * xmp and have no meaning.
-        *
-        * @param XMLParser $parser
-        * @param string $elm Namespace . ' ' . element name
-        * @throws RuntimeException
-        */
-       function endElement( $parser, $elm ) {
-               if ( $elm === ( self::NS_RDF . ' RDF' )
-                       || $elm === 'adobe:ns:meta/ xmpmeta'
-                       || $elm === 'adobe:ns:meta/ xapmeta'
-               ) {
-                       // ignore these.
-                       return;
-               }
-
-               if ( $elm === self::NS_RDF . ' type' ) {
-                       // these aren't really supported properly yet.
-                       // However, it appears they almost never used.
-                       $this->logger->info( __METHOD__ . ' encountered <rdf:type>' );
-               }
-
-               if ( strpos( $elm, ' ' ) === false ) {
-                       // This probably shouldn't happen.
-                       // However, there is a bug in an adobe product
-                       // that forgets the namespace on some things.
-                       // (Luckily they are unimportant things).
-                       $this->logger->info( __METHOD__ . " Encountered </$elm> which has no namespace. Skipping." );
-
-                       return;
-               }
-
-               if ( count( $this->mode[0] ) === 0 ) {
-                       // This should never ever happen and means
-                       // there is a pretty major bug in this class.
-                       throw new RuntimeException( 'Encountered end element with no mode' );
-               }
-
-               if ( count( $this->curItem ) == 0 && $this->mode[0] !== self::MODE_INITIAL ) {
-                       // just to be paranoid. Should always have a curItem, except for initially
-                       // (aka during MODE_INITAL).
-                       throw new RuntimeException( "Hit end element </$elm> but no curItem" );
-               }
-
-               switch ( $this->mode[0] ) {
-                       case self::MODE_IGNORE:
-                               $this->endElementModeIgnore( $elm );
-                               break;
-                       case self::MODE_SIMPLE:
-                               $this->endElementModeSimple( $elm );
-                               break;
-                       case self::MODE_STRUCT:
-                       case self::MODE_SEQ:
-                       case self::MODE_BAG:
-                       case self::MODE_LANG:
-                       case self::MODE_BAGSTRUCT:
-                               $this->endElementNested( $elm );
-                               break;
-                       case self::MODE_INITIAL:
-                               if ( $elm === self::NS_RDF . ' Description' ) {
-                                       array_shift( $this->mode );
-                               } else {
-                                       throw new RuntimeException( 'Element ended unexpectedly while in MODE_INITIAL' );
-                               }
-                               break;
-                       case self::MODE_LI:
-                       case self::MODE_LI_LANG:
-                               $this->endElementModeLi( $elm );
-                               break;
-                       case self::MODE_QDESC:
-                               $this->endElementModeQDesc( $elm );
-                               break;
-                       default:
-                               $this->logger->warning( __METHOD__ . " no mode (elm = $elm)" );
-                               break;
-               }
-       }
-
-       /**
-        * Hit an opening element while in MODE_IGNORE
-        *
-        * XMP is extensible, so ignore any tag we don't understand.
-        *
-        * Mostly ignores, unless we encounter the element that we are ignoring.
-        * in which case we add it to the item stack, so we can ignore things
-        * that are nested, correctly.
-        *
-        * @param string $elm Namespace . ' ' . tag name
-        */
-       private function startElementModeIgnore( $elm ) {
-               if ( $elm === $this->curItem[0] ) {
-                       array_unshift( $this->curItem, $elm );
-                       array_unshift( $this->mode, self::MODE_IGNORE );
-               }
-       }
-
-       /**
-        *  Start element in MODE_BAG (unordered array)
-        * this should always be <rdf:Bag>
-        *
-        * @param string $elm Namespace . ' ' . tag
-        * @throws RuntimeException If we have an element that's not <rdf:Bag>
-        */
-       private function startElementModeBag( $elm ) {
-               if ( $elm === self::NS_RDF . ' Bag' ) {
-                       array_unshift( $this->mode, self::MODE_LI );
-               } else {
-                       throw new RuntimeException( "Expected <rdf:Bag> but got $elm." );
-               }
-       }
-
-       /**
-        * Start element in MODE_SEQ (ordered array)
-        * this should always be <rdf:Seq>
-        *
-        * @param string $elm Namespace . ' ' . tag
-        * @throws RuntimeException If we have an element that's not <rdf:Seq>
-        */
-       private function startElementModeSeq( $elm ) {
-               if ( $elm === self::NS_RDF . ' Seq' ) {
-                       array_unshift( $this->mode, self::MODE_LI );
-               } elseif ( $elm === self::NS_RDF . ' Bag' ) {
-                       # bug 27105
-                       $this->logger->info( __METHOD__ . ' Expected an rdf:Seq, but got an rdf:Bag. Pretending'
-                               . ' it is a Seq, since some buggy software is known to screw this up.' );
-                       array_unshift( $this->mode, self::MODE_LI );
-               } else {
-                       throw new RuntimeException( "Expected <rdf:Seq> but got $elm." );
-               }
-       }
-
-       /**
-        * Start element in MODE_LANG (language alternative)
-        * this should always be <rdf:Alt>
-        *
-        * This tag tends to be used for metadata like describe this
-        * picture, which can be translated into multiple languages.
-        *
-        * XMP supports non-linguistic alternative selections,
-        * which are really only used for thumbnails, which
-        * we don't care about.
-        *
-        * @param string $elm Namespace . ' ' . tag
-        * @throws RuntimeException If we have an element that's not <rdf:Alt>
-        */
-       private function startElementModeLang( $elm ) {
-               if ( $elm === self::NS_RDF . ' Alt' ) {
-                       array_unshift( $this->mode, self::MODE_LI_LANG );
-               } else {
-                       throw new RuntimeException( "Expected <rdf:Seq> but got $elm." );
-               }
-       }
-
-       /**
-        * Handle an opening element when in MODE_SIMPLE
-        *
-        * This should not happen often. This is for if a simple element
-        * already opened has a child element. Could happen for a
-        * qualified element.
-        *
-        * For example:
-        * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
-        *   <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
-        *   </exif:DigitalZoomRatio>
-        *
-        * This method is called when processing the <rdf:Description> element
-        *
-        * @param string $elm Namespace and tag names separated by space.
-        * @param array $attribs Attributes of the element.
-        * @throws RuntimeException
-        */
-       private function startElementModeSimple( $elm, $attribs ) {
-               if ( $elm === self::NS_RDF . ' Description' ) {
-                       // If this value has qualifiers
-                       array_unshift( $this->mode, self::MODE_QDESC );
-                       array_unshift( $this->curItem, $this->curItem[0] );
-
-                       if ( isset( $attribs[self::NS_RDF . ' value'] ) ) {
-                               list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
-                               $this->saveValue( $ns, $tag, $attribs[self::NS_RDF . ' value'] );
-                       }
-               } elseif ( $elm === self::NS_RDF . ' value' ) {
-                       // This should not be here.
-                       throw new RuntimeException( __METHOD__ . ' Encountered <rdf:value> where it was unexpected.' );
-               } else {
-                       // something else we don't recognize, like a qualifier maybe.
-                       $this->logger->info( __METHOD__ .
-                               " Encountered element <$elm> where only expecting character data as value of " .
-                               $this->curItem[0] );
-                       array_unshift( $this->mode, self::MODE_IGNORE );
-                       array_unshift( $this->curItem, $elm );
-               }
-       }
-
-       /**
-        * Start an element when in MODE_QDESC.
-        * This generally happens when a simple element has an inner
-        * rdf:Description to hold qualifier elements.
-        *
-        * For example in:
-        * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
-        *   <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
-        *   </exif:DigitalZoomRatio>
-        * Called when processing the <rdf:value> or <foo:someQualifier>.
-        *
-        * @param string $elm Namespace and tag name separated by a space.
-        *
-        */
-       private function startElementModeQDesc( $elm ) {
-               if ( $elm === self::NS_RDF . ' value' ) {
-                       return; // do nothing
-               } else {
-                       // otherwise its a qualifier, which we ignore
-                       array_unshift( $this->mode, self::MODE_IGNORE );
-                       array_unshift( $this->curItem, $elm );
-               }
-       }
-
-       /**
-        * Starting an element when in MODE_INITIAL
-        * This usually happens when we hit an element inside
-        * the outer rdf:Description
-        *
-        * This is generally where most properties start.
-        *
-        * @param string $ns Namespace
-        * @param string $tag Tag name (without namespace prefix)
-        * @param array $attribs Array of attributes
-        * @throws RuntimeException
-        */
-       private function startElementModeInitial( $ns, $tag, $attribs ) {
-               if ( $ns !== self::NS_RDF ) {
-
-                       if ( isset( $this->items[$ns][$tag] ) ) {
-                               if ( isset( $this->items[$ns][$tag]['structPart'] ) ) {
-                                       // If this element is supposed to appear only as
-                                       // a child of a structure, but appears here (not as
-                                       // a child of a struct), then something weird is
-                                       // happening, so ignore this element and its children.
-
-                                       $this->logger->warning( "Encountered <$ns:$tag> outside"
-                                               . " of its expected parent. Ignoring." );
-
-                                       array_unshift( $this->mode, self::MODE_IGNORE );
-                                       array_unshift( $this->curItem, $ns . ' ' . $tag );
-
-                                       return;
-                               }
-                               $mode = $this->items[$ns][$tag]['mode'];
-                               array_unshift( $this->mode, $mode );
-                               array_unshift( $this->curItem, $ns . ' ' . $tag );
-                               if ( $mode === self::MODE_STRUCT ) {
-                                       $this->ancestorStruct = isset( $this->items[$ns][$tag]['map_name'] )
-                                               ? $this->items[$ns][$tag]['map_name'] : $tag;
-                               }
-                               if ( $this->charContent !== false ) {
-                                       // Something weird.
-                                       // Should not happen in valid XMP.
-                                       throw new RuntimeException( 'tag nested in non-whitespace characters.' );
-                               }
-                       } else {
-                               // This element is not on our list of allowed elements so ignore.
-                               $this->logger->debug( __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." );
-                               array_unshift( $this->mode, self::MODE_IGNORE );
-                               array_unshift( $this->curItem, $ns . ' ' . $tag );
-
-                               return;
-                       }
-               }
-               // process attributes
-               $this->doAttribs( $attribs );
-       }
-
-       /**
-        * Hit an opening element when in a Struct (MODE_STRUCT)
-        * This is generally for fields of a compound property.
-        *
-        * Example of a struct (abbreviated; flash has more properties):
-        *
-        * <exif:Flash> <rdf:Description> <exif:Fired>True</exif:Fired>
-        *  <exif:Mode>1</exif:Mode></rdf:Description></exif:Flash>
-        *
-        * or:
-        *
-        * <exif:Flash rdf:parseType='Resource'> <exif:Fired>True</exif:Fired>
-        *  <exif:Mode>1</exif:Mode></exif:Flash>
-        *
-        * @param string $ns Namespace
-        * @param string $tag Tag name (no ns)
-        * @param array $attribs Array of attribs w/ values.
-        * @throws RuntimeException
-        */
-       private function startElementModeStruct( $ns, $tag, $attribs ) {
-               if ( $ns !== self::NS_RDF ) {
-
-                       if ( isset( $this->items[$ns][$tag] ) ) {
-                               if ( isset( $this->items[$ns][$this->ancestorStruct]['children'] )
-                                       && !isset( $this->items[$ns][$this->ancestorStruct]['children'][$tag] )
-                               ) {
-                                       // This assumes that we don't have inter-namespace nesting
-                                       // which we don't in all the properties we're interested in.
-                                       throw new RuntimeException( " <$tag> appeared nested in <" . $this->ancestorStruct
-                                               . "> where it is not allowed." );
-                               }
-                               array_unshift( $this->mode, $this->items[$ns][$tag]['mode'] );
-                               array_unshift( $this->curItem, $ns . ' ' . $tag );
-                               if ( $this->charContent !== false ) {
-                                       // Something weird.
-                                       // Should not happen in valid XMP.
-                                       throw new RuntimeException( "tag <$tag> nested in non-whitespace characters (" .
-                                               $this->charContent . ")." );
-                               }
-                       } else {
-                               array_unshift( $this->mode, self::MODE_IGNORE );
-                               array_unshift( $this->curItem, $elm );
-
-                               return;
-                       }
-               }
-
-               if ( $ns === self::NS_RDF && $tag === 'Description' ) {
-                       $this->doAttribs( $attribs );
-                       array_unshift( $this->mode, self::MODE_STRUCT );
-                       array_unshift( $this->curItem, $this->curItem[0] );
-               }
-       }
-
-       /**
-        * opening element in MODE_LI
-        * process elements of arrays.
-        *
-        * Example:
-        * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
-        *   </rdf:Seq> </exif:ISOSpeedRatings>
-        * This method is called when we hit the <rdf:li> element.
-        *
-        * @param string $elm Namespace . ' ' . tagname
-        * @param array $attribs Attributes. (needed for BAGSTRUCTS)
-        * @throws RuntimeException If gets a tag other than <rdf:li>
-        */
-       private function startElementModeLi( $elm, $attribs ) {
-               if ( ( $elm ) !== self::NS_RDF . ' li' ) {
-                       throw new RuntimeException( "<rdf:li> expected but got $elm." );
-               }
-
-               if ( !isset( $this->mode[1] ) ) {
-                       // This should never ever ever happen. Checking for it
-                       // to be paranoid.
-                       throw new RuntimeException( 'In mode Li, but no 2xPrevious mode!' );
-               }
-
-               if ( $this->mode[1] === self::MODE_BAGSTRUCT ) {
-                       // This list item contains a compound (STRUCT) value.
-                       array_unshift( $this->mode, self::MODE_STRUCT );
-                       array_unshift( $this->curItem, $elm );
-                       $this->processingArray = true;
-
-                       if ( !isset( $this->curItem[1] ) ) {
-                               // be paranoid.
-                               throw new RuntimeException( 'Can not find parent of BAGSTRUCT.' );
-                       }
-                       list( $curNS, $curTag ) = explode( ' ', $this->curItem[1] );
-                       $this->ancestorStruct = isset( $this->items[$curNS][$curTag]['map_name'] )
-                               ? $this->items[$curNS][$curTag]['map_name'] : $curTag;
-
-                       $this->doAttribs( $attribs );
-               } else {
-                       // Normal BAG or SEQ containing simple values.
-                       array_unshift( $this->mode, self::MODE_SIMPLE );
-                       // need to add curItem[0] on again since one is for the specific item
-                       // and one is for the entire group.
-                       array_unshift( $this->curItem, $this->curItem[0] );
-                       $this->processingArray = true;
-               }
-       }
-
-       /**
-        * Opening element in MODE_LI_LANG.
-        * process elements of language alternatives
-        *
-        * Example:
-        * <dc:title> <rdf:Alt> <rdf:li xml:lang="x-default">My house
-        *  </rdf:li> </rdf:Alt> </dc:title>
-        *
-        * This method is called when we hit the <rdf:li> element.
-        *
-        * @param string $elm Namespace . ' ' . tag
-        * @param array $attribs Array of elements (most importantly xml:lang)
-        * @throws RuntimeException If gets a tag other than <rdf:li> or if no xml:lang
-        */
-       private function startElementModeLiLang( $elm, $attribs ) {
-               if ( $elm !== self::NS_RDF . ' li' ) {
-                       throw new RuntimeException( __METHOD__ . " <rdf:li> expected but got $elm." );
-               }
-               if ( !isset( $attribs[self::NS_XML . ' lang'] )
-                       || !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $attribs[self::NS_XML . ' lang'] )
-               ) {
-                       throw new RuntimeException( __METHOD__
-                               . " <rdf:li> did not contain, or has invalid xml:lang attribute in lang alternative" );
-               }
-
-               // Lang is case-insensitive.
-               $this->itemLang = strtolower( $attribs[self::NS_XML . ' lang'] );
-
-               // need to add curItem[0] on again since one is for the specific item
-               // and one is for the entire group.
-               array_unshift( $this->curItem, $this->curItem[0] );
-               array_unshift( $this->mode, self::MODE_SIMPLE );
-               $this->processingArray = true;
-       }
-
-       /**
-        * Hits an opening element.
-        * Generally just calls a helper based on what MODE we're in.
-        * Also does some initial set up for the wrapper element
-        *
-        * @param XMLParser $parser
-        * @param string $elm Namespace "<space>" element
-        * @param array $attribs Attribute name => value
-        * @throws RuntimeException
-        */
-       function startElement( $parser, $elm, $attribs ) {
-
-               if ( $elm === self::NS_RDF . ' RDF'
-                       || $elm === 'adobe:ns:meta/ xmpmeta'
-                       || $elm === 'adobe:ns:meta/ xapmeta'
-               ) {
-                       /* ignore. */
-                       return;
-               } elseif ( $elm === self::NS_RDF . ' Description' ) {
-                       if ( count( $this->mode ) === 0 ) {
-                               // outer rdf:desc
-                               array_unshift( $this->mode, self::MODE_INITIAL );
-                       }
-               } elseif ( $elm === self::NS_RDF . ' type' ) {
-                       // This doesn't support rdf:type properly.
-                       // In practise I have yet to see a file that
-                       // uses this element, however it is mentioned
-                       // on page 25 of part 1 of the xmp standard.
-                       // Also it seems as if exiv2 and exiftool do not support
-                       // this either (That or I misunderstand the standard)
-                       $this->logger->info( __METHOD__ . ' Encountered <rdf:type> which isn\'t currently supported' );
-               }
-
-               if ( strpos( $elm, ' ' ) === false ) {
-                       // This probably shouldn't happen.
-                       $this->logger->info( __METHOD__ . " Encountered <$elm> which has no namespace. Skipping." );
-
-                       return;
-               }
-
-               list( $ns, $tag ) = explode( ' ', $elm, 2 );
-
-               if ( count( $this->mode ) === 0 ) {
-                       // This should not happen.
-                       throw new RuntimeException( 'Error extracting XMP, '
-                               . "encountered <$elm> with no mode" );
-               }
-
-               switch ( $this->mode[0] ) {
-                       case self::MODE_IGNORE:
-                               $this->startElementModeIgnore( $elm );
-                               break;
-                       case self::MODE_SIMPLE:
-                               $this->startElementModeSimple( $elm, $attribs );
-                               break;
-                       case self::MODE_INITIAL:
-                               $this->startElementModeInitial( $ns, $tag, $attribs );
-                               break;
-                       case self::MODE_STRUCT:
-                               $this->startElementModeStruct( $ns, $tag, $attribs );
-                               break;
-                       case self::MODE_BAG:
-                       case self::MODE_BAGSTRUCT:
-                               $this->startElementModeBag( $elm );
-                               break;
-                       case self::MODE_SEQ:
-                               $this->startElementModeSeq( $elm );
-                               break;
-                       case self::MODE_LANG:
-                               $this->startElementModeLang( $elm );
-                               break;
-                       case self::MODE_LI_LANG:
-                               $this->startElementModeLiLang( $elm, $attribs );
-                               break;
-                       case self::MODE_LI:
-                               $this->startElementModeLi( $elm, $attribs );
-                               break;
-                       case self::MODE_QDESC:
-                               $this->startElementModeQDesc( $elm );
-                               break;
-                       default:
-                               throw new RuntimeException( 'StartElement in unknown mode: ' . $this->mode[0] );
-               }
-       }
-
-       // @codingStandardsIgnoreStart Generic.Files.LineLength
-       /**
-        * Process attributes.
-        * Simple values can be stored as either a tag or attribute
-        *
-        * Often the initial "<rdf:Description>" tag just has all the simple
-        * properties as attributes.
-        *
-        * @par Example:
-        * @code
-        * <rdf:Description rdf:about="" xmlns:exif="http://ns.adobe.com/exif/1.0/" exif:DigitalZoomRatio="0/10">
-        * @endcode
-        *
-        * @param array $attribs Array attribute=>value
-        * @throws RuntimeException
-        */
-       // @codingStandardsIgnoreEnd
-       private function doAttribs( $attribs ) {
-               // first check for rdf:parseType attribute, as that can change
-               // how the attributes are interperted.
-
-               if ( isset( $attribs[self::NS_RDF . ' parseType'] )
-                       && $attribs[self::NS_RDF . ' parseType'] === 'Resource'
-                       && $this->mode[0] === self::MODE_SIMPLE
-               ) {
-                       // this is equivalent to having an inner rdf:Description
-                       $this->mode[0] = self::MODE_QDESC;
-               }
-               foreach ( $attribs as $name => $val ) {
-                       if ( strpos( $name, ' ' ) === false ) {
-                               // This shouldn't happen, but so far some old software forgets namespace
-                               // on rdf:about.
-                               $this->logger->info( __METHOD__ . ' Encountered non-namespaced attribute: '
-                                       . " $name=\"$val\". Skipping. " );
-                               continue;
-                       }
-                       list( $ns, $tag ) = explode( ' ', $name, 2 );
-                       if ( $ns === self::NS_RDF ) {
-                               if ( $tag === 'value' || $tag === 'resource' ) {
-                                       // resource is for url.
-                                       // value attribute is a weird way of just putting the contents.
-                                       $this->char( $this->xmlParser, $val );
-                               }
-                       } elseif ( isset( $this->items[$ns][$tag] ) ) {
-                               if ( $this->mode[0] === self::MODE_SIMPLE ) {
-                                       throw new RuntimeException( __METHOD__
-                                               . " $ns:$tag found as attribute where not allowed" );
-                               }
-                               $this->saveValue( $ns, $tag, $val );
-                       } else {
-                               $this->logger->debug( __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." );
-                       }
-               }
-       }
-
-       /**
-        * Given an extracted value, save it to results array
-        *
-        * note also uses $this->ancestorStruct and
-        * $this->processingArray to determine what name to
-        * save the value under. (in addition to $tag).
-        *
-        * @param string $ns Namespace of tag this is for
-        * @param string $tag Tag name
-        * @param string $val Value to save
-        */
-       private function saveValue( $ns, $tag, $val ) {
-
-               $info =& $this->items[$ns][$tag];
-               $finalName = isset( $info['map_name'] )
-                       ? $info['map_name'] : $tag;
-               if ( isset( $info['validate'] ) ) {
-                       if ( is_array( $info['validate'] ) ) {
-                               $validate = $info['validate'];
-                       } else {
-                               $validator = new XMPValidate( $this->logger );
-                               $validate = [ $validator, $info['validate'] ];
-                       }
-
-                       if ( is_callable( $validate ) ) {
-                               call_user_func_array( $validate, [ $info, &$val, true ] );
-                               // the reasoning behind using &$val instead of using the return value
-                               // is to be consistent between here and validating structures.
-                               if ( is_null( $val ) ) {
-                                       $this->logger->info( __METHOD__ . " <$ns:$tag> failed validation." );
-
-                                       return;
-                               }
-                       } else {
-                               $this->logger->warning( __METHOD__ . " Validation function for $finalName ("
-                                       . $validate[0] . '::' . $validate[1] . '()) is not callable.' );
-                       }
-               }
-
-               if ( $this->ancestorStruct && $this->processingArray ) {
-                       // Aka both an array and a struct. ( self::MODE_BAGSTRUCT )
-                       $this->results['xmp-' . $info['map_group']][$this->ancestorStruct][][$finalName] = $val;
-               } elseif ( $this->ancestorStruct ) {
-                       $this->results['xmp-' . $info['map_group']][$this->ancestorStruct][$finalName] = $val;
-               } elseif ( $this->processingArray ) {
-                       if ( $this->itemLang === false ) {
-                               // normal array
-                               $this->results['xmp-' . $info['map_group']][$finalName][] = $val;
-                       } else {
-                               // lang array.
-                               $this->results['xmp-' . $info['map_group']][$finalName][$this->itemLang] = $val;
-                       }
-               } else {
-                       $this->results['xmp-' . $info['map_group']][$finalName] = $val;
-               }
-       }
-}
diff --git a/includes/media/XMPInfo.php b/includes/media/XMPInfo.php
deleted file mode 100644 (file)
index 052be33..0000000
+++ /dev/null
@@ -1,1168 +0,0 @@
-<?php
-/**
- * Definitions for XMPReader class.
- *
- * 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 Media
- */
-
-/**
- * This class is just a container for a big array
- * used by XMPReader to determine which XMP items to
- * extract.
- */
-class XMPInfo {
-       /** Get the items array
-        * @return array XMP item configuration array.
-        */
-       public static function getItems() {
-               return self::$items;
-       }
-
-       /**
-        * XMPInfo::$items keeps a list of all the items
-        * we are interested to extract, as well as
-        * information about the item like what type
-        * it is.
-        *
-        * Format is an array of namespaces,
-        * each containing an array of tags
-        * each tag is an array of information about the
-        * tag, including:
-        *   * map_group - What group (used for precedence during conflicts).
-        *   * mode - What type of item (self::MODE_SIMPLE usually, see above for
-        *     all values).
-        *   * validate - Method to validate input. Could also post-process the
-        *     input. A string value is assumed to be a method of
-        *     XMPValidate. Can also take a array( 'className', 'methodName' ).
-        *   * choices - Array of potential values (format of 'value' => true ).
-        *     Only used with validateClosed.
-        *   * rangeLow and rangeHigh - Alternative to choices for numeric ranges.
-        *     Again for validateClosed only.
-        *   * children - For MODE_STRUCT items, allowed children.
-        *   * structPart - Indicates that this element can only appear as a member
-        *     of a structure.
-        *
-        * Currently this just has a bunch of EXIF values as this class is only half-done.
-        */
-       static private $items = [
-               'http://ns.adobe.com/exif/1.0/' => [
-                       'ApertureValue' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'BrightnessValue' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'CompressedBitsPerPixel' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'DigitalZoomRatio' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'ExposureBiasValue' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'ExposureIndex' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'ExposureTime' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'FlashEnergy' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational',
-                       ],
-                       'FNumber' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'FocalLength' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'FocalPlaneXResolution' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'FocalPlaneYResolution' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'GPSAltitude' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational',
-                       ],
-                       'GPSDestBearing' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'GPSDestDistance' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'GPSDOP' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'GPSImgDirection' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'GPSSpeed' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'GPSTrack' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'MaxApertureValue' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'ShutterSpeedValue' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       'SubjectDistance' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational'
-                       ],
-                       /* Flash */
-                       'Flash' => [
-                               'mode' => XMPReader::MODE_STRUCT,
-                               'children' => [
-                                       'Fired' => true,
-                                       'Function' => true,
-                                       'Mode' => true,
-                                       'RedEyeMode' => true,
-                                       'Return' => true,
-                               ],
-                               'validate' => 'validateFlash',
-                               'map_group' => 'exif',
-                       ],
-                       'Fired' => [
-                               'map_group' => 'exif',
-                               'validate' => 'validateBoolean',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'Function' => [
-                               'map_group' => 'exif',
-                               'validate' => 'validateBoolean',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'Mode' => [
-                               'map_group' => 'exif',
-                               'validate' => 'validateClosed',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'choices' => [ '0' => true, '1' => true,
-                                       '2' => true, '3' => true ],
-                               'structPart' => true,
-                       ],
-                       'Return' => [
-                               'map_group' => 'exif',
-                               'validate' => 'validateClosed',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'choices' => [ '0' => true,
-                                       '2' => true, '3' => true ],
-                               'structPart' => true,
-                       ],
-                       'RedEyeMode' => [
-                               'map_group' => 'exif',
-                               'validate' => 'validateBoolean',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       /* End Flash */
-                       'ISOSpeedRatings' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SEQ,
-                               'validate' => 'validateInteger'
-                       ],
-                       /* end rational things */
-                       'ColorSpace' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '1' => true, '65535' => true ],
-                       ],
-                       'ComponentsConfiguration' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SEQ,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '1' => true, '2' => true, '3' => true, '4' => true,
-                                       '5' => true, '6' => true ]
-                       ],
-                       'Contrast' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '0' => true, '1' => true, '2' => true ]
-                       ],
-                       'CustomRendered' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '0' => true, '1' => true ]
-                       ],
-                       'DateTimeOriginal' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateDate',
-                       ],
-                       'DateTimeDigitized' => [ /* xmp:CreateDate */
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateDate',
-                       ],
-                       /* todo: there might be interesting information in
-                        * exif:DeviceSettingDescription, but need to find an
-                        * example
-                        */
-                       'ExifVersion' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'ExposureMode' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'rangeLow' => 0,
-                               'rangeHigh' => 2,
-                       ],
-                       'ExposureProgram' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'rangeLow' => 0,
-                               'rangeHigh' => 8,
-                       ],
-                       'FileSource' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '3' => true ]
-                       ],
-                       'FlashpixVersion' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'FocalLengthIn35mmFilm' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateInteger',
-                       ],
-                       'FocalPlaneResolutionUnit' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '2' => true, '3' => true ],
-                       ],
-                       'GainControl' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'rangeLow' => 0,
-                               'rangeHigh' => 4,
-                       ],
-                       /* this value is post-processed out later */
-                       'GPSAltitudeRef' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '0' => true, '1' => true ],
-                       ],
-                       'GPSAreaInformation' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'GPSDestBearingRef' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ 'T' => true, 'M' => true ],
-                       ],
-                       'GPSDestDistanceRef' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ 'K' => true, 'M' => true,
-                                       'N' => true ],
-                       ],
-                       'GPSDestLatitude' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateGPS',
-                       ],
-                       'GPSDestLongitude' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateGPS',
-                       ],
-                       'GPSDifferential' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '0' => true, '1' => true ],
-                       ],
-                       'GPSImgDirectionRef' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ 'T' => true, 'M' => true ],
-                       ],
-                       'GPSLatitude' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateGPS',
-                       ],
-                       'GPSLongitude' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateGPS',
-                       ],
-                       'GPSMapDatum' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'GPSMeasureMode' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '2' => true, '3' => true ]
-                       ],
-                       'GPSProcessingMethod' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'GPSSatellites' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'GPSSpeedRef' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ 'K' => true, 'M' => true,
-                                       'N' => true ],
-                       ],
-                       'GPSStatus' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ 'A' => true, 'V' => true ]
-                       ],
-                       'GPSTimeStamp' => [
-                               'map_group' => 'exif',
-                               // Note: in exif, GPSDateStamp does not include
-                               // the time, where here it does.
-                               'map_name' => 'GPSDateStamp',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateDate',
-                       ],
-                       'GPSTrackRef' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ 'T' => true, 'M' => true ]
-                       ],
-                       'GPSVersionID' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'ImageUniqueID' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'LightSource' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               /* can't use a range, as it skips... */
-                               'choices' => [ '0' => true, '1' => true,
-                                       '2' => true, '3' => true, '4' => true,
-                                       '9' => true, '10' => true, '11' => true,
-                                       '12' => true, '13' => true,
-                                       '14' => true, '15' => true,
-                                       '17' => true, '18' => true,
-                                       '19' => true, '20' => true,
-                                       '21' => true, '22' => true,
-                                       '23' => true, '24' => true,
-                                       '255' => true,
-                               ],
-                       ],
-                       'MeteringMode' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'rangeLow' => 0,
-                               'rangeHigh' => 6,
-                               'choices' => [ '255' => true ],
-                       ],
-                       /* Pixel(X|Y)Dimension are rather useless, but for
-                        * completeness since we do it with exif.
-                        */
-                       'PixelXDimension' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateInteger',
-                       ],
-                       'PixelYDimension' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateInteger',
-                       ],
-                       'Saturation' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'rangeLow' => 0,
-                               'rangeHigh' => 2,
-                       ],
-                       'SceneCaptureType' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'rangeLow' => 0,
-                               'rangeHigh' => 3,
-                       ],
-                       'SceneType' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '1' => true ],
-                       ],
-                       // Note, 6 is not valid SensingMethod.
-                       'SensingMethod' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'rangeLow' => 1,
-                               'rangeHigh' => 5,
-                               'choices' => [ '7' => true, 8 => true ],
-                       ],
-                       'Sharpness' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'rangeLow' => 0,
-                               'rangeHigh' => 2,
-                       ],
-                       'SpectralSensitivity' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       // This tag should perhaps be displayed to user better.
-                       'SubjectArea' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SEQ,
-                               'validate' => 'validateInteger',
-                       ],
-                       'SubjectDistanceRange' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'rangeLow' => 0,
-                               'rangeHigh' => 3,
-                       ],
-                       'SubjectLocation' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SEQ,
-                               'validate' => 'validateInteger',
-                       ],
-                       'UserComment' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_LANG,
-                       ],
-                       'WhiteBalance' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '0' => true, '1' => true ]
-                       ],
-               ],
-               'http://ns.adobe.com/tiff/1.0/' => [
-                       'Artist' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'BitsPerSample' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SEQ,
-                               'validate' => 'validateInteger',
-                       ],
-                       'Compression' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '1' => true, '6' => true ],
-                       ],
-                       /* this prop should not be used in XMP. dc:rights is the correct prop */
-                       'Copyright' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_LANG,
-                       ],
-                       'DateTime' => [ /* proper prop is xmp:ModifyDate */
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateDate',
-                       ],
-                       'ImageDescription' => [ /* proper one is dc:description */
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_LANG,
-                       ],
-                       'ImageLength' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateInteger',
-                       ],
-                       'ImageWidth' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateInteger',
-                       ],
-                       'Make' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'Model' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       /**** Do not extract this property
-                        * It interferes with auto exif rotation.
-                        * 'Orientation'       => array(
-                        *    'map_group' => 'exif',
-                        *    'mode'      => XMPReader::MODE_SIMPLE,
-                        *    'validate'  => 'validateClosed',
-                        *    'choices'   => array( '1' => true, '2' => true, '3' => true, '4' => true, 5 => true,
-                        *            '6' => true, '7' => true, '8' => true ),
-                        *),
-                        ******/
-                       'PhotometricInterpretation' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '2' => true, '6' => true ],
-                       ],
-                       'PlanerConfiguration' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '1' => true, '2' => true ],
-                       ],
-                       'PrimaryChromaticities' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SEQ,
-                               'validate' => 'validateRational',
-                       ],
-                       'ReferenceBlackWhite' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SEQ,
-                               'validate' => 'validateRational',
-                       ],
-                       'ResolutionUnit' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '2' => true, '3' => true ],
-                       ],
-                       'SamplesPerPixel' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateInteger',
-                       ],
-                       'Software' => [ /* see xmp:CreatorTool */
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       /* ignore TransferFunction */
-                       'WhitePoint' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SEQ,
-                               'validate' => 'validateRational',
-                       ],
-                       'XResolution' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational',
-                       ],
-                       'YResolution' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRational',
-                       ],
-                       'YCbCrCoefficients' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SEQ,
-                               'validate' => 'validateRational',
-                       ],
-                       'YCbCrPositioning' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateClosed',
-                               'choices' => [ '1' => true, '2' => true ],
-                       ],
-                       /********
-                        * Disable extracting this property (bug 31944)
-                        * Several files have a string instead of a Seq
-                        * for this property. XMPReader doesn't handle
-                        * mismatched types very gracefully (it marks
-                        * the entire file as invalid, instead of just
-                        * the relavent prop). Since this prop
-                        * doesn't communicate all that useful information
-                        * just disable this prop for now, until such
-                        * XMPReader is more graceful (bug 32172)
-                        * 'YCbCrSubSampling'  => array(
-                        *    'map_group' => 'exif',
-                        *    'mode'      => XMPReader::MODE_SEQ,
-                        *    'validate'  => 'validateClosed',
-                        *    'choices'   => array( '1' => true, '2' => true ),
-                        * ),
-                        */
-               ],
-               'http://ns.adobe.com/exif/1.0/aux/' => [
-                       'Lens' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'SerialNumber' => [
-                               'map_group' => 'exif',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'OwnerName' => [
-                               'map_group' => 'exif',
-                               'map_name' => 'CameraOwnerName',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-               ],
-               'http://purl.org/dc/elements/1.1/' => [
-                       'title' => [
-                               'map_group' => 'general',
-                               'map_name' => 'ObjectName',
-                               'mode' => XMPReader::MODE_LANG
-                       ],
-                       'description' => [
-                               'map_group' => 'general',
-                               'map_name' => 'ImageDescription',
-                               'mode' => XMPReader::MODE_LANG
-                       ],
-                       'contributor' => [
-                               'map_group' => 'general',
-                               'map_name' => 'dc-contributor',
-                               'mode' => XMPReader::MODE_BAG
-                       ],
-                       'coverage' => [
-                               'map_group' => 'general',
-                               'map_name' => 'dc-coverage',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'creator' => [
-                               'map_group' => 'general',
-                               'map_name' => 'Artist', // map with exif Artist, iptc byline (2:80)
-                               'mode' => XMPReader::MODE_SEQ,
-                       ],
-                       'date' => [
-                               'map_group' => 'general',
-                               // Note, not mapped with other date properties, as this type of date is
-                               // non-specific: "A point or period of time associated with an event in
-                               //  the lifecycle of the resource"
-                               'map_name' => 'dc-date',
-                               'mode' => XMPReader::MODE_SEQ,
-                               'validate' => 'validateDate',
-                       ],
-                       /* Do not extract dc:format, as we've got better ways to determine MIME type */
-                       'identifier' => [
-                               'map_group' => 'deprecated',
-                               'map_name' => 'Identifier',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'language' => [
-                               'map_group' => 'general',
-                               'map_name' => 'LanguageCode', /* mapped with iptc 2:135 */
-                               'mode' => XMPReader::MODE_BAG,
-                               'validate' => 'validateLangCode',
-                       ],
-                       'publisher' => [
-                               'map_group' => 'general',
-                               'map_name' => 'dc-publisher',
-                               'mode' => XMPReader::MODE_BAG,
-                       ],
-                       // for related images/resources
-                       'relation' => [
-                               'map_group' => 'general',
-                               'map_name' => 'dc-relation',
-                               'mode' => XMPReader::MODE_BAG,
-                       ],
-                       'rights' => [
-                               'map_group' => 'general',
-                               'map_name' => 'Copyright',
-                               'mode' => XMPReader::MODE_LANG,
-                       ],
-                       // Note: source is not mapped with iptc source, since iptc
-                       // source describes the source of the image in terms of a person
-                       // who provided the image, where this is to describe an image that the
-                       // current one is based on.
-                       'source' => [
-                               'map_group' => 'general',
-                               'map_name' => 'dc-source',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'subject' => [
-                               'map_group' => 'general',
-                               'map_name' => 'Keywords', /* maps to iptc 2:25 */
-                               'mode' => XMPReader::MODE_BAG,
-                       ],
-                       'type' => [
-                               'map_group' => 'general',
-                               'map_name' => 'dc-type',
-                               'mode' => XMPReader::MODE_BAG,
-                       ],
-               ],
-               'http://ns.adobe.com/xap/1.0/' => [
-                       'CreateDate' => [
-                               'map_group' => 'general',
-                               'map_name' => 'DateTimeDigitized',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateDate',
-                       ],
-                       'CreatorTool' => [
-                               'map_group' => 'general',
-                               'map_name' => 'Software',
-                               'mode' => XMPReader::MODE_SIMPLE
-                       ],
-                       'Identifier' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_BAG,
-                       ],
-                       'Label' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'ModifyDate' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'DateTime',
-                               'validate' => 'validateDate',
-                       ],
-                       'MetadataDate' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               // map_name to be consistent with other date names.
-                               'map_name' => 'DateTimeMetadata',
-                               'validate' => 'validateDate',
-                       ],
-                       'Nickname' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'Rating' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateRating',
-                       ],
-               ],
-               'http://ns.adobe.com/xap/1.0/rights/' => [
-                       'Certificate' => [
-                               'map_group' => 'general',
-                               'map_name' => 'RightsCertificate',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'Marked' => [
-                               'map_group' => 'general',
-                               'map_name' => 'Copyrighted',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateBoolean',
-                       ],
-                       'Owner' => [
-                               'map_group' => 'general',
-                               'map_name' => 'CopyrightOwner',
-                               'mode' => XMPReader::MODE_BAG,
-                       ],
-                       // this seems similar to dc:rights.
-                       'UsageTerms' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_LANG,
-                       ],
-                       'WebStatement' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-               ],
-               // XMP media management.
-               'http://ns.adobe.com/xap/1.0/mm/' => [
-                       // if we extract the exif UniqueImageID, might
-                       // as well do this too.
-                       'OriginalDocumentID' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       // It might also be useful to do xmpMM:LastURL
-                       // and xmpMM:DerivedFrom as you can potentially,
-                       // get the url of this document/source for this
-                       // document. However whats more likely is you'd
-                       // get a file:// url for the path of the doc,
-                       // which is somewhat of a privacy issue.
-               ],
-               'http://creativecommons.org/ns#' => [
-                       'license' => [
-                               'map_name' => 'LicenseUrl',
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'morePermissions' => [
-                               'map_name' => 'MorePermissionsUrl',
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'attributionURL' => [
-                               'map_group' => 'general',
-                               'map_name' => 'AttributionUrl',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'attributionName' => [
-                               'map_group' => 'general',
-                               'map_name' => 'PreferredAttributionName',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-               ],
-               // Note, this property affects how jpeg metadata is extracted.
-               'http://ns.adobe.com/xmp/note/' => [
-                       'HasExtendedXMP' => [
-                               'map_group' => 'special',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-               ],
-               /* Note, in iptc schemas, the legacy properties are denoted
-                * as deprecated, since other properties should used instead,
-                * and properties marked as deprecated in the standard are
-                * are marked as general here as they don't have replacements
-                */
-               'http://ns.adobe.com/photoshop/1.0/' => [
-                       'City' => [
-                               'map_group' => 'deprecated',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'CityDest',
-                       ],
-                       'Country' => [
-                               'map_group' => 'deprecated',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'CountryDest',
-                       ],
-                       'State' => [
-                               'map_group' => 'deprecated',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'ProvinceOrStateDest',
-                       ],
-                       'DateCreated' => [
-                               'map_group' => 'deprecated',
-                               // marking as deprecated as the xmp prop preferred
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'DateTimeOriginal',
-                               'validate' => 'validateDate',
-                               // note this prop is an XMP, not IPTC date
-                       ],
-                       'CaptionWriter' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'Writer',
-                       ],
-                       'Instructions' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'SpecialInstructions',
-                       ],
-                       'TransmissionReference' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'OriginalTransmissionRef',
-                       ],
-                       'AuthorsPosition' => [
-                               /* This corresponds with 2:85
-                                * By-line Title, which needs to be
-                                * handled weirdly to correspond
-                                * with iptc/exif. */
-                               'map_group' => 'special',
-                               'mode' => XMPReader::MODE_SIMPLE
-                       ],
-                       'Credit' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'Source' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'Urgency' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'Category' => [
-                               // Note, this prop is deprecated, but in general
-                               // group since it doesn't have a replacement.
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'iimCategory',
-                       ],
-                       'SupplementalCategories' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_BAG,
-                               'map_name' => 'iimSupplementalCategory',
-                       ],
-                       'Headline' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE
-                       ],
-               ],
-               'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/' => [
-                       'CountryCode' => [
-                               'map_group' => 'deprecated',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'CountryCodeDest',
-                       ],
-                       'IntellectualGenre' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       // Note, this is a six digit code.
-                       // See: http://cv.iptc.org/newscodes/scene/
-                       // Since these aren't really all that common,
-                       // we just show the number.
-                       'Scene' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_BAG,
-                               'validate' => 'validateInteger',
-                               'map_name' => 'SceneCode',
-                       ],
-                       /* Note: SubjectCode should be an 8 ascii digits.
-                        * it is not really an integer (has leading 0's,
-                        * cannot have a +/- sign), but validateInteger
-                        * will let it through.
-                        */
-                       'SubjectCode' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_BAG,
-                               'map_name' => 'SubjectNewsCode',
-                               'validate' => 'validateInteger'
-                       ],
-                       'Location' => [
-                               'map_group' => 'deprecated',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'map_name' => 'SublocationDest',
-                       ],
-                       'CreatorContactInfo' => [
-                               /* Note this maps to 2:118 in iim
-                                * (Contact) field. However those field
-                                * types are slightly different - 2:118
-                                * is free form text field, where this
-                                * is more structured.
-                                */
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_STRUCT,
-                               'map_name' => 'Contact',
-                               'children' => [
-                                       'CiAdrExtadr' => true,
-                                       'CiAdrCity' => true,
-                                       'CiAdrCtry' => true,
-                                       'CiEmailWork' => true,
-                                       'CiTelWork' => true,
-                                       'CiAdrPcode' => true,
-                                       'CiAdrRegion' => true,
-                                       'CiUrlWork' => true,
-                               ],
-                       ],
-                       'CiAdrExtadr' => [ /* address */
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'CiAdrCity' => [ /* city */
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'CiAdrCtry' => [ /* country */
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'CiEmailWork' => [ /* email (possibly separated by ',') */
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'CiTelWork' => [ /* telephone */
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'CiAdrPcode' => [ /* postal code */
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'CiAdrRegion' => [ /* province/state */
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'CiUrlWork' => [ /* url. Multiple may be separated by comma. */
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       /* End contact info struct properties */
-               ],
-               'http://iptc.org/std/Iptc4xmpExt/2008-02-29/' => [
-                       'Event' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                       ],
-                       'OrganisationInImageName' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_BAG,
-                               'map_name' => 'OrganisationInImage'
-                       ],
-                       'PersonInImage' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_BAG,
-                       ],
-                       'MaxAvailHeight' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateInteger',
-                               'map_name' => 'OriginalImageHeight',
-                       ],
-                       'MaxAvailWidth' => [
-                               'map_group' => 'general',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'validate' => 'validateInteger',
-                               'map_name' => 'OriginalImageWidth',
-                       ],
-                       // LocationShown and LocationCreated are handled
-                       // specially because they are hierarchical, but we
-                       // also want to merge with the old non-hierarchical.
-                       'LocationShown' => [
-                               'map_group' => 'special',
-                               'mode' => XMPReader::MODE_BAGSTRUCT,
-                               'children' => [
-                                       'WorldRegion' => true,
-                                       'CountryCode' => true, /* iso code */
-                                       'CountryName' => true,
-                                       'ProvinceState' => true,
-                                       'City' => true,
-                                       'Sublocation' => true,
-                               ],
-                       ],
-                       'LocationCreated' => [
-                               'map_group' => 'special',
-                               'mode' => XMPReader::MODE_BAGSTRUCT,
-                               'children' => [
-                                       'WorldRegion' => true,
-                                       'CountryCode' => true, /* iso code */
-                                       'CountryName' => true,
-                                       'ProvinceState' => true,
-                                       'City' => true,
-                                       'Sublocation' => true,
-                               ],
-                       ],
-                       'WorldRegion' => [
-                               'map_group' => 'special',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'CountryCode' => [
-                               'map_group' => 'special',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'CountryName' => [
-                               'map_group' => 'special',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                               'map_name' => 'Country',
-                       ],
-                       'ProvinceState' => [
-                               'map_group' => 'special',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                               'map_name' => 'ProvinceOrState',
-                       ],
-                       'City' => [
-                               'map_group' => 'special',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-                       'Sublocation' => [
-                               'map_group' => 'special',
-                               'mode' => XMPReader::MODE_SIMPLE,
-                               'structPart' => true,
-                       ],
-
-                       /* Other props that might be interesting but
-                        * Not currently extracted:
-                        * ArtworkOrObject, (info about objects in picture)
-                        * DigitalSourceType
-                        * RegistryId
-                        */
-               ],
-
-               /* Plus props we might want to consider:
-                * (Note: some of these have unclear/incomplete definitions
-                * from the iptc4xmp standard).
-                * ImageSupplier (kind of like iptc source field)
-                * ImageSupplierId (id code for image from supplier)
-                * CopyrightOwner
-                * ImageCreator
-                * Licensor
-                * Various model release fields
-                * Property release fields.
-                */
-       ];
-}
diff --git a/includes/media/XMPValidate.php b/includes/media/XMPValidate.php
deleted file mode 100644 (file)
index fe47f47..0000000
+++ /dev/null
@@ -1,398 +0,0 @@
-<?php
-/**
- * Methods for validating XMP properties.
- *
- * 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 Media
- */
-
-use Psr\Log\LoggerInterface;
-use Psr\Log\LoggerAwareInterface;
-
-/**
- * This contains some static methods for
- * validating XMP properties. See XMPInfo and XMPReader classes.
- *
- * Each of these functions take the same parameters
- * * an info array which is a subset of the XMPInfo::items array
- * * A value (passed as reference) to validate. This can be either a
- *    simple value or an array
- * * A boolean to determine if this is validating a simple or complex values
- *
- * It should be noted that when an array is being validated, typically the validation
- * function is called once for each value, and then once at the end for the entire array.
- *
- * These validation functions can also be used to modify the data. See the gps and flash one's
- * for example.
- *
- * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart1.pdf starting at pg 28
- * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf starting at pg 11
- */
-class XMPValidate implements LoggerAwareInterface {
-
-       /**
-        * @var LoggerInterface
-        */
-       private $logger;
-
-       public function __construct( LoggerInterface $logger ) {
-               $this->setLogger( $logger );
-       }
-
-       public function setLogger( LoggerInterface $logger ) {
-               $this->logger = $logger;
-       }
-       /**
-        * Function to validate boolean properties ( True or False )
-        *
-        * @param array $info Information about current property
-        * @param mixed &$val Current value to validate
-        * @param bool $standalone If this is a simple property or array
-        */
-       public function validateBoolean( $info, &$val, $standalone ) {
-               if ( !$standalone ) {
-                       // this only validates standalone properties, not arrays, etc
-                       return;
-               }
-               if ( $val !== 'True' && $val !== 'False' ) {
-                       $this->logger->info( __METHOD__ . " Expected True or False but got $val" );
-                       $val = null;
-               }
-       }
-
-       /**
-        * function to validate rational properties ( 12/10 )
-        *
-        * @param array $info Information about current property
-        * @param mixed &$val Current value to validate
-        * @param bool $standalone If this is a simple property or array
-        */
-       public function validateRational( $info, &$val, $standalone ) {
-               if ( !$standalone ) {
-                       // this only validates standalone properties, not arrays, etc
-                       return;
-               }
-               if ( !preg_match( '/^(?:-?\d+)\/(?:\d+[1-9]|[1-9]\d*)$/D', $val ) ) {
-                       $this->logger->info( __METHOD__ . " Expected rational but got $val" );
-                       $val = null;
-               }
-       }
-
-       /**
-        * function to validate rating properties -1, 0-5
-        *
-        * if its outside of range put it into range.
-        *
-        * @see MWG spec
-        * @param array $info Information about current property
-        * @param mixed &$val Current value to validate
-        * @param bool $standalone If this is a simple property or array
-        */
-       public function validateRating( $info, &$val, $standalone ) {
-               if ( !$standalone ) {
-                       // this only validates standalone properties, not arrays, etc
-                       return;
-               }
-               if ( !preg_match( '/^[-+]?\d*(?:\.?\d*)$/D', $val )
-                       || !is_numeric( $val )
-               ) {
-                       $this->logger->info( __METHOD__ . " Expected rating but got $val" );
-                       $val = null;
-
-                       return;
-               } else {
-                       $nVal = (float)$val;
-                       if ( $nVal < 0 ) {
-                               // We do < 0 here instead of < -1 here, since
-                               // the values between 0 and -1 are also illegal
-                               // as -1 is meant as a special reject rating.
-                               $this->logger->info( __METHOD__ . " Rating too low, setting to -1 (Rejected)" );
-                               $val = '-1';
-
-                               return;
-                       }
-                       if ( $nVal > 5 ) {
-                               $this->logger->info( __METHOD__ . " Rating too high, setting to 5" );
-                               $val = '5';
-
-                               return;
-                       }
-               }
-       }
-
-       /**
-        * function to validate integers
-        *
-        * @param array $info Information about current property
-        * @param mixed &$val Current value to validate
-        * @param bool $standalone If this is a simple property or array
-        */
-       public function validateInteger( $info, &$val, $standalone ) {
-               if ( !$standalone ) {
-                       // this only validates standalone properties, not arrays, etc
-                       return;
-               }
-               if ( !preg_match( '/^[-+]?\d+$/D', $val ) ) {
-                       $this->logger->info( __METHOD__ . " Expected integer but got $val" );
-                       $val = null;
-               }
-       }
-
-       /**
-        * function to validate properties with a fixed number of allowed
-        * choices. (closed choice)
-        *
-        * @param array $info Information about current property
-        * @param mixed &$val Current value to validate
-        * @param bool $standalone If this is a simple property or array
-        */
-       public function validateClosed( $info, &$val, $standalone ) {
-               if ( !$standalone ) {
-                       // this only validates standalone properties, not arrays, etc
-                       return;
-               }
-
-               // check if its in a numeric range
-               $inRange = false;
-               if ( isset( $info['rangeLow'] )
-                       && isset( $info['rangeHigh'] )
-                       && is_numeric( $val )
-                       && ( intval( $val ) <= $info['rangeHigh'] )
-                       && ( intval( $val ) >= $info['rangeLow'] )
-               ) {
-                       $inRange = true;
-               }
-
-               if ( !isset( $info['choices'][$val] ) && !$inRange ) {
-                       $this->logger->info( __METHOD__ . " Expected closed choice, but got $val" );
-                       $val = null;
-               }
-       }
-
-       /**
-        * function to validate and modify flash structure
-        *
-        * @param array $info Information about current property
-        * @param mixed &$val Current value to validate
-        * @param bool $standalone If this is a simple property or array
-        */
-       public function validateFlash( $info, &$val, $standalone ) {
-               if ( $standalone ) {
-                       // this only validates flash structs, not individual properties
-                       return;
-               }
-               if ( !( isset( $val['Fired'] )
-                       && isset( $val['Function'] )
-                       && isset( $val['Mode'] )
-                       && isset( $val['RedEyeMode'] )
-                       && isset( $val['Return'] )
-               ) ) {
-                       $this->logger->info( __METHOD__ . " Flash structure did not have all the required components" );
-                       $val = null;
-               } else {
-                       $val = ( "\0" | ( $val['Fired'] === 'True' )
-                               | ( intval( $val['Return'] ) << 1 )
-                               | ( intval( $val['Mode'] ) << 3 )
-                               | ( ( $val['Function'] === 'True' ) << 5 )
-                               | ( ( $val['RedEyeMode'] === 'True' ) << 6 ) );
-               }
-       }
-
-       /**
-        * function to validate LangCode properties ( en-GB, etc )
-        *
-        * This is just a naive check to make sure it somewhat looks like a lang code.
-        *
-        * @see BCP 47
-        * @see https://wwwimages2.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/
-        *      XMP%20SDK%20Release%20cc-2014-12/XMPSpecificationPart1.pdf page 22 (section 8.2.2.4)
-        *
-        * @param array $info Information about current property
-        * @param mixed &$val Current value to validate
-        * @param bool $standalone If this is a simple property or array
-        */
-       public function validateLangCode( $info, &$val, $standalone ) {
-               if ( !$standalone ) {
-                       // this only validates standalone properties, not arrays, etc
-                       return;
-               }
-               if ( !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $val ) ) {
-                       // this is a rather naive check.
-                       $this->logger->info( __METHOD__ . " Expected Lang code but got $val" );
-                       $val = null;
-               }
-       }
-
-       /**
-        * function to validate date properties, and convert to (partial) Exif format.
-        *
-        * Dates can be one of the following formats:
-        * YYYY
-        * YYYY-MM
-        * YYYY-MM-DD
-        * YYYY-MM-DDThh:mmTZD
-        * YYYY-MM-DDThh:mm:ssTZD
-        * YYYY-MM-DDThh:mm:ss.sTZD
-        *
-        * @param array $info Information about current property
-        * @param mixed &$val Current value to validate. Converts to TS_EXIF as a side-effect.
-        *    in cases where there's only a partial date, it will give things like
-        *    2011:04.
-        * @param bool $standalone If this is a simple property or array
-        */
-       public function validateDate( $info, &$val, $standalone ) {
-               if ( !$standalone ) {
-                       // this only validates standalone properties, not arrays, etc
-                       return;
-               }
-               $res = [];
-               // @codingStandardsIgnoreStart Long line that cannot be broken
-               if ( !preg_match(
-                       /* ahh! scary regex... */
-                       '/^([0-3]\d{3})(?:-([01]\d)(?:-([0-3]\d)(?:T([0-2]\d):([0-6]\d)(?::([0-6]\d)(?:\.\d+)?)?([-+]\d{2}:\d{2}|Z)?)?)?)?$/D',
-                       $val, $res )
-               ) {
-                       // @codingStandardsIgnoreEnd
-
-                       $this->logger->info( __METHOD__ . " Expected date but got $val" );
-                       $val = null;
-               } else {
-                       /*
-                        * $res is formatted as follows:
-                        * 0 -> full date.
-                        * 1 -> year, 2-> month, 3-> day, 4-> hour, 5-> minute, 6->second
-                        * 7-> Timezone specifier (Z or something like +12:30 )
-                        * many parts are optional, some aren't. For example if you specify
-                        * minute, you must specify hour, day, month, and year but not second or TZ.
-                        */
-
-                       /*
-                        * First of all, if year = 0000, Something is wrongish,
-                        * so don't extract. This seems to happen when
-                        * some programs convert between metadata formats.
-                        */
-                       if ( $res[1] === '0000' ) {
-                               $this->logger->info( __METHOD__ . " Invalid date (year 0): $val" );
-                               $val = null;
-
-                               return;
-                       }
-
-                       if ( !isset( $res[4] ) ) { // hour
-                               // just have the year month day (if that)
-                               $val = $res[1];
-                               if ( isset( $res[2] ) ) {
-                                       $val .= ':' . $res[2];
-                               }
-                               if ( isset( $res[3] ) ) {
-                                       $val .= ':' . $res[3];
-                               }
-
-                               return;
-                       }
-
-                       if ( !isset( $res[7] ) || $res[7] === 'Z' ) {
-                               // if hour is set, then minute must also be or regex above will fail.
-                               $val = $res[1] . ':' . $res[2] . ':' . $res[3]
-                                       . ' ' . $res[4] . ':' . $res[5];
-                               if ( isset( $res[6] ) && $res[6] !== '' ) {
-                                       $val .= ':' . $res[6];
-                               }
-
-                               return;
-                       }
-
-                       // Extra check for empty string necessary due to TZ but no second case.
-                       $stripSeconds = false;
-                       if ( !isset( $res[6] ) || $res[6] === '' ) {
-                               $res[6] = '00';
-                               $stripSeconds = true;
-                       }
-
-                       // Do timezone processing. We've already done the case that tz = Z.
-
-                       // We know that if we got to this step, year, month day hour and min must be set
-                       // by virtue of regex not failing.
-
-                       $unix = wfTimestamp( TS_UNIX, $res[1] . $res[2] . $res[3] . $res[4] . $res[5] . $res[6] );
-                       $offset = intval( substr( $res[7], 1, 2 ) ) * 60 * 60;
-                       $offset += intval( substr( $res[7], 4, 2 ) ) * 60;
-                       if ( substr( $res[7], 0, 1 ) === '-' ) {
-                               $offset = -$offset;
-                       }
-                       $val = wfTimestamp( TS_EXIF, $unix + $offset );
-
-                       if ( $stripSeconds ) {
-                               // If seconds weren't specified, remove the trailing ':00'.
-                               $val = substr( $val, 0, -3 );
-                       }
-               }
-       }
-
-       /** function to validate, and more importantly
-        * translate the XMP DMS form of gps coords to
-        * the decimal form we use.
-        *
-        * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf
-        *        section 1.2.7.4 on page 23
-        *
-        * @param array $info Unused (info about prop)
-        * @param string &$val GPS string in either DDD,MM,SSk or
-        *   or DDD,MM.mmk form
-        * @param bool $standalone If its a simple prop (should always be true)
-        */
-       public function validateGPS( $info, &$val, $standalone ) {
-               if ( !$standalone ) {
-                       return;
-               }
-
-               $m = [];
-               if ( preg_match(
-                       '/(\d{1,3}),(\d{1,2}),(\d{1,2})([NWSE])/D',
-                       $val, $m )
-               ) {
-                       $coord = intval( $m[1] );
-                       $coord += intval( $m[2] ) * ( 1 / 60 );
-                       $coord += intval( $m[3] ) * ( 1 / 3600 );
-                       if ( $m[4] === 'S' || $m[4] === 'W' ) {
-                               $coord = -$coord;
-                       }
-                       $val = $coord;
-
-                       return;
-               } elseif ( preg_match(
-                       '/(\d{1,3}),(\d{1,2}(?:.\d*)?)([NWSE])/D',
-                       $val, $m )
-               ) {
-                       $coord = intval( $m[1] );
-                       $coord += floatval( $m[2] ) * ( 1 / 60 );
-                       if ( $m[3] === 'S' || $m[3] === 'W' ) {
-                               $coord = -$coord;
-                       }
-                       $val = $coord;
-
-                       return;
-               } else {
-                       $this->logger->info( __METHOD__
-                               . " Expected GPSCoordinate, but got $val." );
-                       $val = null;
-
-                       return;
-               }
-       }
-}
index 1e0013f..8160a75 100644 (file)
@@ -23,7 +23,6 @@
 
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Services\ServiceDisabledException;
 
 /**
  * Functions to get cache objects
@@ -51,7 +50,7 @@ use MediaWiki\Services\ServiceDisabledException;
  *
  * - ObjectCache::getLocalServerInstance( $fallbackType )
  *   Purpose: Memory cache for very hot keys.
- *   Stored only on the individual web server (typically APC for web requests,
+ *   Stored only on the individual web server (typically APC or APCu for web requests,
  *   and EmptyBagOStuff in CLI mode).
  *   Not replicated to the other servers.
  *
@@ -118,13 +117,20 @@ class ObjectCache {
         *
         * @param string $id A key in $wgObjectCaches.
         * @return BagOStuff
-        * @throws MWException
+        * @throws InvalidArgumentException
         */
        public static function newFromId( $id ) {
                global $wgObjectCaches;
 
                if ( !isset( $wgObjectCaches[$id] ) ) {
-                       throw new MWException( "Invalid object cache type \"$id\" requested. " .
+                       // Always recognize these ones
+                       if ( $id === CACHE_NONE ) {
+                               return new EmptyBagOStuff();
+                       } elseif ( $id === 'hash' ) {
+                               return new HashBagOStuff();
+                       }
+
+                       throw new InvalidArgumentException( "Invalid object cache type \"$id\" requested. " .
                                "It is not present in \$wgObjectCaches." );
                }
 
@@ -160,7 +166,7 @@ class ObjectCache {
         *  - loggroup: Alias to set 'logger' key with LoggerFactory group.
         *  - .. Other parameters passed to factory or class.
         * @return BagOStuff
-        * @throws MWException
+        * @throws InvalidArgumentException
         */
        public static function newFromParams( $params ) {
                if ( isset( $params['loggroup'] ) ) {
@@ -183,8 +189,25 @@ class ObjectCache {
                        $params['reportDupes'] = isset( $params['reportDupes'] )
                                ? $params['reportDupes']
                                : true;
+                       // Do b/c logic for SqlBagOStuff
+                       if ( is_a( $class, SqlBagOStuff::class, true ) ) {
+                               if ( isset( $params['server'] ) && !isset( $params['servers'] ) ) {
+                                       $params['servers'] = [ $params['server'] ];
+                                       unset( $params['server'] );
+                               }
+                               // In the past it was not required to set 'dbDirectory' in $wgObjectCaches
+                               if ( isset( $params['servers'] ) ) {
+                                       foreach ( $params['servers'] as &$server ) {
+                                               if ( $server['type'] === 'sqlite' && !isset( $server['dbDirectory'] ) ) {
+                                                       $server['dbDirectory'] = MediaWikiServices::getInstance()
+                                                               ->getMainConfig()->get( 'SQLiteDataDir' );
+                                               }
+                                       }
+                               }
+                       }
+
                        // Do b/c logic for MemcachedBagOStuff
-                       if ( is_subclass_of( $class, 'MemcachedBagOStuff' ) ) {
+                       if ( is_subclass_of( $class, MemcachedBagOStuff::class ) ) {
                                if ( !isset( $params['servers'] ) ) {
                                        $params['servers'] = $GLOBALS['wgMemCachedServers'];
                                }
@@ -200,7 +223,7 @@ class ObjectCache {
                        }
                        return new $class( $params );
                } else {
-                       throw new MWException( "The definition of cache type \""
+                       throw new InvalidArgumentException( "The definition of cache type \""
                                . print_r( $params, true ) . "\" lacks both "
                                . "factory and class parameters." );
                }
@@ -242,7 +265,7 @@ class ObjectCache {
        /**
         * Factory function for CACHE_ACCEL (referenced from DefaultSettings.php)
         *
-        * This will look for any APC style server-local cache.
+        * This will look for any APC or APCu style server-local cache.
         * A fallback cache can be specified if none is found.
         *
         *     // Direct calls
@@ -253,25 +276,19 @@ class ObjectCache {
         *
         * @param int|string|array $fallback Fallback cache or parameter map with 'fallback'
         * @return BagOStuff
-        * @throws MWException
+        * @throws InvalidArgumentException
         * @since 1.27
         */
        public static function getLocalServerInstance( $fallback = CACHE_NONE ) {
-               if ( function_exists( 'apc_fetch' ) ) {
-                       $id = 'apc';
-               } elseif ( function_exists( 'xcache_get' ) && wfIniGetBool( 'xcache.var_size' ) ) {
-                       $id = 'xcache';
-               } elseif ( function_exists( 'wincache_ucache_get' ) ) {
-                       $id = 'wincache';
-               } else {
+               $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
+               if ( $cache instanceof EmptyBagOStuff ) {
                        if ( is_array( $fallback ) ) {
-                               $id = isset( $fallback['fallback'] ) ? $fallback['fallback'] : CACHE_NONE;
-                       } else {
-                               $id = $fallback;
+                               $fallback = isset( $fallback['fallback'] ) ? $fallback['fallback'] : CACHE_NONE;
                        }
+                       $cache = self::getInstance( $fallback );
                }
 
-               return self::getInstance( $id );
+               return $cache;
        }
 
        /**
@@ -298,23 +315,41 @@ class ObjectCache {
         * @since 1.26
         * @param string $id A key in $wgWANObjectCaches.
         * @return WANObjectCache
-        * @throws MWException
+        * @throws UnexpectedValueException
         */
        public static function newWANCacheFromId( $id ) {
-               global $wgWANObjectCaches;
+               global $wgWANObjectCaches, $wgObjectCaches;
 
                if ( !isset( $wgWANObjectCaches[$id] ) ) {
-                       throw new MWException( "Invalid object cache type \"$id\" requested. " .
-                               "It is not present in \$wgWANObjectCaches." );
+                       throw new UnexpectedValueException(
+                               "Cache type \"$id\" requested is not present in \$wgWANObjectCaches." );
                }
 
                $params = $wgWANObjectCaches[$id];
+               if ( !isset( $wgObjectCaches[$params['cacheId']] ) ) {
+                       throw new UnexpectedValueException(
+                               "Cache type \"{$params['cacheId']}\" is not present in \$wgObjectCaches." );
+               }
+               $params['store'] = $wgObjectCaches[$params['cacheId']];
+
+               return self::newWANCacheFromParams( $params );
+       }
+
+       /**
+        * Create a new cache object of the specified type.
+        *
+        * @since 1.28
+        * @param array $params
+        * @return WANObjectCache
+        * @throws UnexpectedValueException
+        */
+       public static function newWANCacheFromParams( array $params ) {
                foreach ( $params['channels'] as $action => $channel ) {
                        $params['relayers'][$action] = MediaWikiServices::getInstance()->getEventRelayerGroup()
                                ->getRelayer( $channel );
                        $params['channels'][$action] = $channel;
                }
-               $params['cache'] = self::newFromId( $params['cacheId'] );
+               $params['cache'] = self::newFromParams( $params['store'] );
                if ( isset( $params['loggroup'] ) ) {
                        $params['logger'] = LoggerFactory::getInstance( $params['loggroup'] );
                } else {
@@ -342,11 +377,10 @@ class ObjectCache {
         *
         * @since 1.26
         * @return WANObjectCache
+        * @deprecated Since 1.28 Use MediaWikiServices::getMainWANCache()
         */
        public static function getMainWANInstance() {
-               global $wgMainWANCache;
-
-               return self::getWANInstance( $wgMainWANCache );
+               return MediaWikiServices::getInstance()->getMainWANObjectCache();
        }
 
        /**
@@ -366,11 +400,10 @@ class ObjectCache {
         *
         * @return BagOStuff
         * @since 1.26
+        * @deprecated Since 1.28 Use MediaWikiServices::getMainObjectStash
         */
        public static function getMainStashInstance() {
-               global $wgMainStash;
-
-               return self::getInstance( $wgMainStash );
+               return MediaWikiServices::getInstance()->getMainObjectStash();
        }
 
        /**
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 d06213f..47dae78 100644 (file)
@@ -182,7 +182,7 @@ class SqlBagOStuff extends BagOStuff {
                                $this->logger->debug( __CLASS__ . ": connecting to $host" );
                                // Use a blank trx profiler to ignore expections as this is a cache
                                $info['trxProfiler'] = new TransactionProfiler();
-                               $db = DatabaseBase::factory( $type, $info );
+                               $db = Database::factory( $type, $info );
                                $db->clearFlag( DBO_TRX );
                        } else {
                                $index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
index db6df86..9428609 100644 (file)
@@ -333,7 +333,7 @@ class Article implements Page {
        function fetchContent() {
                // BC cruft!
 
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
 
                if ( $this->mContentLoaded && $this->mContent ) {
                        return $this->mContent;
@@ -347,7 +347,11 @@ class Article implements Page {
 
                // @todo Get rid of mContent everywhere!
                $this->mContent = ContentHandler::getContentText( $content );
-               ContentHandler::runLegacyHooks( 'ArticleAfterFetchContent', [ &$this, &$this->mContent ] );
+               ContentHandler::runLegacyHooks(
+                       'ArticleAfterFetchContent',
+                       [ &$this, &$this->mContent ],
+                       '1.21'
+               );
 
                return $this->mContent;
        }
@@ -424,7 +428,11 @@ class Article implements Page {
                $this->mContentObject = $content;
                $this->mRevIdFetched = $this->mRevision->getId();
 
-               Hooks::run( 'ArticleAfterFetchContentObject', [ &$this, &$this->mContentObject ] );
+               ContentHandler::runLegacyHooks(
+                       'ArticleAfterFetchContentObject',
+                       [ &$this, &$this->mContentObject ],
+                       '1.21'
+               );
 
                return $this->mContentObject;
        }
@@ -623,9 +631,11 @@ class Article implements Page {
 
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
-                                       } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom',
-                                                       [ $this->fetchContentObject(), $this->getTitle(), $outputPage ] ) ) {
-
+                                       } elseif ( !ContentHandler::runLegacyHooks(
+                                               'ArticleViewCustom',
+                                               [ $this->fetchContentObject(), $this->getTitle(), $outputPage ],
+                                               '1.21'
+                                       ) ) {
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        }
@@ -798,8 +808,9 @@ class Article implements Page {
                        // Give hooks a chance to customise the output
                        if ( ContentHandler::runLegacyHooks(
                                'ShowRawCssJs',
-                               [ $this->mContentObject, $this->getTitle(), $outputPage ] )
-                       ) {
+                               [ $this->mContentObject, $this->getTitle(), $outputPage ],
+                               '1.24'
+                       ) ) {
                                // If no legacy hooks ran, display the content of the parser output, including RL modules,
                                // but excluding metadata like categories and language links
                                $po = $this->mContentObject->getParserOutput( $this->getTitle() );
@@ -1932,12 +1943,13 @@ class Article implements Page {
 
        /**
         * Check if the page can be cached
+        * @param integer $mode One of the HTMLFileCache::MODE_* constants (since 1.28)
         * @return bool
         */
-       public function isFileCacheable() {
+       public function isFileCacheable( $mode = HTMLFileCache::MODE_NORMAL ) {
                $cacheable = false;
 
-               if ( HTMLFileCache::useFileCache( $this->getContext() ) ) {
+               if ( HTMLFileCache::useFileCache( $this->getContext(), $mode ) ) {
                        $cacheable = $this->mPage->getId()
                                && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect();
                        // Extension may have reason to disable file caching on some pages.
@@ -2090,10 +2102,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
                );
        }
 
@@ -2108,9 +2121,11 @@ class Article implements Page {
        /**
         * Call to WikiPage function for backwards compatibility.
         * @see WikiPage::doEdit
+        *
+        * @deprecated since 1.21: use doEditContent() instead.
         */
        public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
                return $this->mPage->doEdit( $text, $summary, $flags, $baseRevId, $user );
        }
 
@@ -2150,18 +2165,6 @@ class Article implements Page {
                return $this->mPage->getLastPurgeTimestamp();
        }
 
-       /**
-        * Call to WikiPage function for backwards compatibility.
-        * @see WikiPage::doQuickEditContent
-        */
-       public function doQuickEditContent(
-               Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
-       ) {
-               return $this->mPage->doQuickEditContent(
-                       $content, $user, $comment, $minor, $serialFormat
-               );
-       }
-
        /**
         * Call to WikiPage function for backwards compatibility.
         * @see WikiPage::doViewUpdates
@@ -2504,6 +2507,7 @@ class Article implements Page {
 
        /**
         * Call to WikiPage function for backwards compatibility.
+        * @deprecated since 1.21, use prepareContentForEdit
         * @see WikiPage::prepareTextForEdit
         */
        public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
index faac26d..2833965 100644 (file)
@@ -693,7 +693,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @deprecated since 1.21, getContent() should be used instead.
         */
        public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
 
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
@@ -1577,7 +1577,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @deprecated since 1.21: use doEditContent() instead.
         */
        public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
 
                $content = ContentHandler::makeContent( $text, $this->getTitle() );
 
@@ -1678,7 +1678,7 @@ class WikiPage implements Page, IDBAccessObject {
                                                        $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
                // Check if the hook rejected the attempted save
                if ( !Hooks::run( 'PageContentSave', $hook_args )
-                       || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args )
+                       || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args, '1.21' )
                ) {
                        if ( $hookStatus->isOK() ) {
                                // Hook returned false but didn't call fatal(); use generic message
@@ -2023,11 +2023,11 @@ class WikiPage implements Page, IDBAccessObject {
                                        // Trigger post-create hook
                                        $params = [ &$this, &$user, $content, $summary,
                                                $flags & EDIT_MINOR, null, null, &$flags, $revision ];
-                                       ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params );
+                                       ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params, '1.21' );
                                        Hooks::run( 'PageContentInsertComplete', $params );
                                        // Trigger post-save hook
                                        $params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
-                                       ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
+                                       ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params, '1.21' );
                                        Hooks::run( 'PageContentSaveComplete', $params );
 
                                }
@@ -2396,41 +2396,6 @@ class WikiPage implements Page, IDBAccessObject {
                }
        }
 
-       /**
-        * Edit an article without doing all that other stuff
-        * The article must already exist; link tables etc
-        * are not updated, caches are not flushed.
-        *
-        * @param Content $content Content submitted
-        * @param User $user The relevant user
-        * @param string $comment Comment submitted
-        * @param bool $minor Whereas it's a minor modification
-        * @param string $serialFormat Format for storing the content in the database
-        */
-       public function doQuickEditContent(
-               Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
-       ) {
-
-               $serialized = $content->serialize( $serialFormat );
-
-               $dbw = wfGetDB( DB_MASTER );
-               $revision = new Revision( [
-                       'title'      => $this->getTitle(), // for determining the default content model
-                       'page'       => $this->getId(),
-                       'user_text'  => $user->getName(),
-                       'user'       => $user->getId(),
-                       'text'       => $serialized,
-                       'length'     => $content->getSize(),
-                       'comment'    => $comment,
-                       'minor_edit' => $minor ? 1 : 0,
-               ] ); // XXX: set the content object?
-               $revision->insertOn( $dbw );
-               $this->updateRevisionOn( $dbw, $revision );
-
-               Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
-
-       }
-
        /**
         * Update the article's restriction field, and leave a log entry.
         * This works for protection both existing and non-existing pages.
@@ -2882,12 +2847,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;
 
@@ -3017,10 +2984,7 @@ class WikiPage implements Page, IDBAccessObject {
 
                // Now that it's safely backed up, delete it
                $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
-
-               if ( !$dbw->cascadingDeletes() ) {
-                       $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
-               }
+               $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
 
                // Log the deletion, if the page was suppressed, put it in the suppression log instead
                $logtype = $suppress ? 'suppress' : 'delete';
@@ -3029,6 +2993,7 @@ class WikiPage implements Page, IDBAccessObject {
                $logEntry->setPerformer( $user );
                $logEntry->setTarget( $logTitle );
                $logEntry->setComment( $reason );
+               $logEntry->setTags( $tags );
                $logid = $logEntry->insert();
 
                $dbw->onTransactionPreCommitOrIdle(
@@ -3502,7 +3467,7 @@ class WikiPage implements Page, IDBAccessObject {
                $dbr = wfGetDB( DB_REPLICA );
                $res = $dbr->select( 'categorylinks',
                        [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
-                       // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
+                       // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
                        // as not being aliases, and NS_CATEGORY is numeric
                        [ 'cl_from' => $id ],
                        __METHOD__ );
@@ -3553,7 +3518,7 @@ class WikiPage implements Page, IDBAccessObject {
                // NOTE: stub for backwards-compatibility. assumes the given text is
                // wikitext. will break horribly if it isn't.
 
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
 
                $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
                $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
@@ -3738,7 +3703,7 @@ class WikiPage implements Page, IDBAccessObject {
         *
         * @param Content|null $content Optional Content object for determining the
         *   necessary updates.
-        * @return DataUpdate[]
+        * @return DeferrableUpdate[]
         */
        public function getDeletionUpdates( Content $content = null ) {
                if ( !$content ) {
index 7c18798..a32acc2 100644 (file)
@@ -251,7 +251,7 @@ class Parser {
        protected $mProfiler;
 
        /**
-        * @var \MediaWiki\Linker\LinkRenderer
+        * @var LinkRenderer
         */
        protected $mLinkRenderer;
 
@@ -887,10 +887,10 @@ class Parser {
        }
 
        /**
-        * Get a \MediaWiki\Linker\LinkRenderer instance to make links with
+        * Get a LinkRenderer instance to make links with
         *
         * @since 1.28
-        * @return \MediaWiki\Linker\LinkRenderer
+        * @return LinkRenderer
         */
        public function getLinkRenderer() {
                if ( !$this->mLinkRenderer ) {
@@ -4138,12 +4138,13 @@ class Parser {
                        # * <b> (r105284)
                        # * <bdi> (bug 72884)
                        # * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
+                       # * <s> and <strike> (T35715)
                        # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
                        # to allow setting directionality in toc items.
                        $tocline = preg_replace(
                                [
-                                       '#<(?!/?(span|sup|sub|bdi|i|b)(?: [^>]*)?>).*?>#',
-                                       '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b))(?: .*?)?>#'
+                                       '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
+                                       '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
                                ],
                                [ '', '<$1>' ],
                                $safeHeadline
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 297dcb2..db9c95b 100644 (file)
@@ -162,9 +162,9 @@ abstract class Profiler {
        abstract public function scopedProfileIn( $section );
 
        /**
-        * @param ScopedCallback $section
+        * @param SectionProfileCallback $section
         */
-       public function scopedProfileOut( ScopedCallback &$section = null ) {
+       public function scopedProfileOut( SectionProfileCallback &$section = null ) {
                $section = null;
        }
 
index 65ac6e6..7e3c398 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup Profiler
  * @author Aaron Schulz
  */
+use Wikimedia\ScopedCallback;
 
 /**
  * Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle
@@ -57,7 +58,7 @@ class SectionProfiler {
 
        /**
         * @param string $section
-        * @return ScopedCallback
+        * @return SectionProfileCallback
         */
        public function scopedProfileIn( $section ) {
                $this->profileInInternal( $section );
index 5555e8b..745c233 100644 (file)
@@ -110,6 +110,7 @@ class ExtensionProcessor implements Processor {
                'type',
                'config',
                'config_prefix',
+               'ServiceWiringFiles',
                'ParserTestFiles',
                'AutoloadClasses',
                'manifest_version',
@@ -174,6 +175,7 @@ class ExtensionProcessor implements Processor {
                $this->extractMessagesDirs( $dir, $info );
                $this->extractNamespaces( $info );
                $this->extractResourceLoaderModules( $dir, $info );
+               $this->extractServiceWiringFiles( $dir, $info );
                $this->extractParserTestFiles( $dir, $info );
                if ( isset( $info['callback'] ) ) {
                        $this->callbacks[] = $info['callback'];
@@ -406,6 +408,14 @@ class ExtensionProcessor implements Processor {
                }
        }
 
+       protected function extractServiceWiringFiles( $dir, array $info ) {
+               if ( isset( $info['ServiceWiringFiles'] ) ) {
+                       foreach ( $info['ServiceWiringFiles'] as $path ) {
+                               $this->globals['wgServiceWiringFiles'][] = "$dir/$path";
+                       }
+               }
+       }
+
        protected function extractParserTestFiles( $dir, array $info ) {
                if ( isset( $info['ParserTestFiles'] ) ) {
                        foreach ( $info['ParserTestFiles'] as $path ) {
index 3bec457..35044e1 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * ExtensionRegistry class
  *
@@ -86,7 +88,7 @@ class ExtensionRegistry {
                // we don't want to fail here if $wgObjectCaches is not configured
                // properly for APC setup
                try {
-                       $this->cache = ObjectCache::getLocalServerInstance();
+                       $this->cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
                } catch ( MWException $e ) {
                        $this->cache = new EmptyBagOStuff();
                }
index 4a2f759..b17200f 100644 (file)
@@ -26,7 +26,7 @@ use MediaWiki\Logger\LoggerFactory;
 
 /**
  * Object passed around to modules which contains information about the state
- * of a specific loader request
+ * of a specific loader request.
  */
 class ResourceLoaderContext {
        protected $resourceLoader;
@@ -62,26 +62,33 @@ class ResourceLoaderContext {
                $this->request = $request;
                $this->logger = $resourceLoader->getLogger();
 
+               // Future developers: Avoid use of getVal() in this class, which performs
+               // expensive UTF normalisation by default. Use getRawVal() instead.
+               // Values here are either one of a finite number of internal IDs,
+               // or previously-stored user input (e.g. titles, user names) that were passed
+               // to this endpoint by ResourceLoader itself from the canonical value.
+               // Values do not come directly from user input and need not match.
+
                // List of modules
-               $modules = $request->getVal( 'modules' );
+               $modules = $request->getRawVal( 'modules' );
                $this->modules = $modules ? self::expandModuleNames( $modules ) : [];
 
                // Various parameters
-               $this->user = $request->getVal( 'user' );
+               $this->user = $request->getRawVal( 'user' );
                $this->debug = $request->getFuzzyBool(
                        'debug',
                        $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' )
                );
-               $this->only = $request->getVal( 'only', null );
-               $this->version = $request->getVal( 'version', null );
+               $this->only = $request->getRawVal( 'only', null );
+               $this->version = $request->getRawVal( 'version', null );
                $this->raw = $request->getFuzzyBool( 'raw' );
 
                // Image requests
-               $this->image = $request->getVal( 'image' );
-               $this->variant = $request->getVal( 'variant' );
-               $this->format = $request->getVal( 'format' );
+               $this->image = $request->getRawVal( 'image' );
+               $this->variant = $request->getRawVal( 'variant' );
+               $this->format = $request->getRawVal( 'format' );
 
-               $this->skin = $request->getVal( 'skin' );
+               $this->skin = $request->getRawVal( 'skin' );
                $skinnames = Skin::getSkinNames();
                // If no skin is specified, or we don't recognize the skin, use the default skin
                if ( !$this->skin || !isset( $skinnames[$this->skin] ) ) {
@@ -171,7 +178,7 @@ class ResourceLoaderContext {
                if ( $this->language === null ) {
                        // Must be a valid language code after this point (T64849)
                        // Only support uselang values that follow built-in conventions (T102058)
-                       $lang = $this->getRequest()->getVal( 'lang', '' );
+                       $lang = $this->getRequest()->getRawVal( 'lang', '' );
                        // Stricter version of RequestContext::sanitizeLangCode()
                        if ( !Language::isValidBuiltInCode( $lang ) ) {
                                wfDebug( "Invalid user language code\n" );
@@ -187,7 +194,7 @@ class ResourceLoaderContext {
         */
        public function getDirection() {
                if ( $this->direction === null ) {
-                       $this->direction = $this->getRequest()->getVal( 'dir' );
+                       $this->direction = $this->getRequest()->getRawVal( 'dir' );
                        if ( !$this->direction ) {
                                // Determine directionality based on user language (bug 6100)
                                $this->direction = Language::factory( $this->getLanguage() )->getDir();
index 4d0bff7..aef1c74 100644 (file)
@@ -55,12 +55,6 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
                        $rules[] = "a { text-decoration: " .
                                ( $options['underline'] ? 'underline' : 'none' ) . "; }";
                }
-               if ( $options['editfont'] !== 'default' ) {
-                       // Double-check that $options['editfont'] consists of safe characters only
-                       if ( preg_match( '/^[a-zA-Z0-9_, -]+$/', $options['editfont'] ) ) {
-                               $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
-                       }
-               }
                $style = implode( "\n", $rules );
                if ( $this->getFlip( $context ) ) {
                        $style = CSSJanus::transform( $style, true, false );
index 4fdd86e..7cbec70 100644 (file)
@@ -305,7 +305,11 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
                $titleInfo = [];
                $batch = new LinkBatch;
                foreach ( $pages as $titleText ) {
-                       $batch->addObj( Title::newFromText( $titleText ) );
+                       $title = Title::newFromText( $titleText );
+                       if ( $title ) {
+                               // Page name may be invalid if user-provided (e.g. gadgets)
+                               $batch->addObj( $title );
+                       }
                }
                if ( !$batch->isEmpty() ) {
                        $res = $db->select( 'page',
@@ -359,8 +363,16 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
                        // Before we intersect, map the names to canonical form (T145673).
                        $intersect = [];
                        foreach ( $pages as $page => $unused ) {
-                               $title = Title::newFromText( $page )->getPrefixedText();
-                               $intersect[$title] = 1;
+                               $title = Title::newFromText( $page );
+                               if ( $title ) {
+                                       $intersect[ $title->getPrefixedText() ] = 1;
+                               } else {
+                                       // Page name may be invalid if user-provided (e.g. gadgets)
+                                       $rl->getLogger()->info(
+                                               'Invalid wiki page title "{title}" in ' . __METHOD__,
+                                               [ 'title' => $page ]
+                                       );
+                               }
                        }
                        $info = array_intersect_key( $allInfo, $intersect );
 
index 1eba141..90bfebd 100644 (file)
@@ -267,36 +267,60 @@ abstract class SearchEngine {
 
        /**
         * Parse some common prefixes: all (search everything)
-        * or namespace names
+        * or namespace names and set the list of namespaces
+        * of this class accordingly.
         *
         * @param string $query
         * @return string
         */
        function replacePrefixes( $query ) {
+               $queryAndNs = self::parseNamespacePrefixes( $query );
+               if ( $queryAndNs === false ) {
+                       return $query;
+               }
+               $this->namespaces = $queryAndNs[1];
+               return $queryAndNs[0];
+       }
+
+       /**
+        * Parse some common prefixes: all (search everything)
+        * or namespace names
+        *
+        * @param string $query
+        * @return false|array false if no namespace was extracted, an array
+        * with the parsed query at index 0 and an array of namespaces at index
+        * 1 (or null for all namespaces).
+        */
+       public static function parseNamespacePrefixes( $query ) {
                global $wgContLang;
 
                $parsed = $query;
                if ( strpos( $query, ':' ) === false ) { // nothing to do
-                       return $parsed;
+                       return false;
                }
+               $extractedNamespace = null;
 
                $allkeyword = wfMessage( 'searchall' )->inContentLanguage()->text() . ":";
                if ( strncmp( $query, $allkeyword, strlen( $allkeyword ) ) == 0 ) {
-                       $this->namespaces = null;
+                       $extractedNamespace = null;
                        $parsed = substr( $query, strlen( $allkeyword ) );
                } elseif ( strpos( $query, ':' ) !== false ) {
+                       // TODO: should we unify with PrefixSearch::extractNamespace ?
                        $prefix = str_replace( ' ', '_', substr( $query, 0, strpos( $query, ':' ) ) );
                        $index = $wgContLang->getNsIndex( $prefix );
                        if ( $index !== false ) {
-                               $this->namespaces = [ $index ];
+                               $extractedNamespace = [ $index ];
                                $parsed = substr( $query, strlen( $prefix ) + 1 );
+                       } else {
+                               return false;
                        }
                }
+
                if ( trim( $parsed ) == '' ) {
                        $parsed = $query; // prefix was the whole query
                }
 
-               return $parsed;
+               return [ $parsed, $extractedNamespace ];
        }
 
        /**
@@ -523,13 +547,20 @@ abstract class SearchEngine {
                        return $sugg->getSuggestedTitle()->getPrefixedText();
                } );
 
-               // Rescore results with an exact title match
-               // NOTE: in some cases like cross-namespace redirects
-               // (frequently used as shortcuts e.g. WP:WP on huwiki) some
-               // backends like Cirrus will return no results. We should still
-               // try an exact title match to workaround this limitation
-               $rescorer = new SearchExactMatchRescorer();
-               $rescoredResults = $rescorer->rescore( $search, $this->namespaces, $results, $this->limit );
+               if ( $this->offset === 0 ) {
+                       // Rescore results with an exact title match
+                       // NOTE: in some cases like cross-namespace redirects
+                       // (frequently used as shortcuts e.g. WP:WP on huwiki) some
+                       // backends like Cirrus will return no results. We should still
+                       // try an exact title match to workaround this limitation
+                       $rescorer = new SearchExactMatchRescorer();
+                       $rescoredResults = $rescorer->rescore( $search, $this->namespaces, $results, $this->limit );
+               } else {
+                       // No need to rescore if offset is not 0
+                       // The exact match must have been returned at position 0
+                       // if it existed.
+                       $rescoredResults = $results;
+               }
 
                if ( count( $rescoredResults ) > 0 ) {
                        $found = array_search( $rescoredResults[0], $results );
@@ -648,10 +679,11 @@ abstract class SearchEngine {
         * - default: set to true if this profile is the default
         *
         * @since 1.28
-        * @param $profileType the type of profiles
+        * @param string $profileType the type of profiles
+        * @param User|null $user the user requesting the list of profiles
         * @return array|null the list of profiles or null if none available
         */
-       public function getProfiles( $profileType ) {
+       public function getProfiles( $profileType, User $user = null ) {
                return null;
        }
 
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.
         */
diff --git a/includes/services/CannotReplaceActiveServiceException.php b/includes/services/CannotReplaceActiveServiceException.php
new file mode 100644 (file)
index 0000000..4993073
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to replace an already active service.
+ *
+ * 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
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to replace an already active service.
+ */
+class CannotReplaceActiveServiceException extends RuntimeException {
+
+       /**
+        * @param string $serviceName
+        * @param Exception|null $previous
+        */
+       public function __construct( $serviceName, Exception $previous = null ) {
+               parent::__construct( "Cannot replace an active service: $serviceName", 0, $previous );
+       }
+
+}
diff --git a/includes/services/ContainerDisabledException.php b/includes/services/ContainerDisabledException.php
new file mode 100644 (file)
index 0000000..ede076d
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to access a service on a disabled container or factory.
+ *
+ * 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
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to access a service on a disabled container or factory.
+ */
+class ContainerDisabledException extends RuntimeException {
+
+       /**
+        * @param Exception|null $previous
+        */
+       public function __construct( Exception $previous = null ) {
+               parent::__construct( 'Container disabled!', 0, $previous );
+       }
+
+}
diff --git a/includes/services/DestructibleService.php b/includes/services/DestructibleService.php
new file mode 100644 (file)
index 0000000..6ce9af2
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+namespace MediaWiki\Services;
+
+/**
+ * Interface for destructible services.
+ *
+ * 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
+ *
+ * @since 1.27
+ */
+
+/**
+ * DestructibleService defines a standard interface for shutting down a service instance.
+ * The intended use is for a service container to be able to shut down services that should
+ * no longer be used, and allow such services to release any system resources.
+ *
+ * @note There is no expectation that services will be destroyed when the process (or web request)
+ * terminates.
+ */
+interface DestructibleService {
+
+       /**
+        * Notifies the service object that it should expect to no longer be used, and should release
+        * any system resources it may own. The behavior of all service methods becomes undefined after
+        * destroy() has been called. It is recommended that implementing classes should throw an
+        * exception when service methods are accessed after destroy() has been called.
+        */
+       public function destroy();
+
+}
diff --git a/includes/services/NoSuchServiceException.php b/includes/services/NoSuchServiceException.php
new file mode 100644 (file)
index 0000000..36e50d2
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when the requested service is not known.
+ *
+ * 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
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when the requested service is not known.
+ */
+class NoSuchServiceException extends RuntimeException {
+
+       /**
+        * @param string $serviceName
+        * @param Exception|null $previous
+        */
+       public function __construct( $serviceName, Exception $previous = null ) {
+               parent::__construct( "No such service: $serviceName", 0, $previous );
+       }
+
+}
diff --git a/includes/services/SalvageableService.php b/includes/services/SalvageableService.php
new file mode 100644 (file)
index 0000000..a613050
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+namespace MediaWiki\Services;
+
+/**
+ * Interface for salvageable services.
+ *
+ * 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
+ *
+ * @since 1.28
+ */
+
+/**
+ * SalvageableService defines an interface for services that are able to salvage state from a
+ * previous instance of the same class. The intent is to allow new service instances to re-use
+ * resources that would be expensive to re-create, such as cached data or network connections.
+ *
+ * @note There is no expectation that services will be destroyed when the process (or web request)
+ * terminates.
+ */
+interface SalvageableService {
+
+       /**
+        * Re-uses state from $other. $other must not be used after being passed to salvage(),
+        * and should be considered to be destroyed.
+        *
+        * @note Implementations are responsible for determining what parts of $other can be re-used
+        * safely. In particular, implementations should check that the relevant configuration of
+        * $other is the same as in $this before re-using resources from $other.
+        *
+        * @note Implementations must take care to detach any re-used resources from the original
+        * service instance. If $other is destroyed later, resources that are now used by the
+        * new service instance must not be affected.
+        *
+        * @note If $other is a DestructibleService, implementations should make sure that $other
+        * is in destroyed state after salvage finished. This may be done by calling $other->destroy()
+        * after carefully detaching all relevant resources.
+        *
+        * @param SalvageableService $other The object to salvage state from. $other must have the
+        * exact same type as $this.
+        */
+       public function salvage( SalvageableService $other );
+
+}
diff --git a/includes/services/ServiceAlreadyDefinedException.php b/includes/services/ServiceAlreadyDefinedException.php
new file mode 100644 (file)
index 0000000..c6344d3
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when a service was already defined, but the
+ * caller expected it to not exist.
+ *
+ * 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
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when a service was already defined, but the
+ * caller expected it to not exist.
+ */
+class ServiceAlreadyDefinedException extends RuntimeException {
+
+       /**
+        * @param string $serviceName
+        * @param Exception|null $previous
+        */
+       public function __construct( $serviceName, Exception $previous = null ) {
+               parent::__construct( "Service already defined: $serviceName", 0, $previous );
+       }
+
+}
diff --git a/includes/services/ServiceContainer.php b/includes/services/ServiceContainer.php
new file mode 100644 (file)
index 0000000..bad0ef9
--- /dev/null
@@ -0,0 +1,378 @@
+<?php
+namespace MediaWiki\Services;
+
+use InvalidArgumentException;
+use RuntimeException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Generic service container.
+ *
+ * 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
+ *
+ * @since 1.27
+ */
+
+/**
+ * ServiceContainer provides a generic service to manage named services using
+ * lazy instantiation based on instantiator callback functions.
+ *
+ * Services managed by an instance of ServiceContainer may or may not implement
+ * a common interface.
+ *
+ * @note When using ServiceContainer to manage a set of services, consider
+ * creating a wrapper or a subclass that provides access to the services via
+ * getter methods with more meaningful names and more specific return type
+ * declarations.
+ *
+ * @see docs/injection.txt for an overview of using dependency injection in the
+ *      MediaWiki code base.
+ */
+class ServiceContainer implements DestructibleService {
+
+       /**
+        * @var object[]
+        */
+       private $services = [];
+
+       /**
+        * @var callable[]
+        */
+       private $serviceInstantiators = [];
+
+       /**
+        * @var boolean[] disabled status, per service name
+        */
+       private $disabled = [];
+
+       /**
+        * @var array
+        */
+       private $extraInstantiationParams;
+
+       /**
+        * @var boolean
+        */
+       private $destroyed = false;
+
+       /**
+        * @param array $extraInstantiationParams Any additional parameters to be passed to the
+        * instantiator function when creating a service. This is typically used to provide
+        * access to additional ServiceContainers or Config objects.
+        */
+       public function __construct( array $extraInstantiationParams = [] ) {
+               $this->extraInstantiationParams = $extraInstantiationParams;
+       }
+
+       /**
+        * Destroys all contained service instances that implement the DestructibleService
+        * interface. This will render all services obtained from this MediaWikiServices
+        * instance unusable. In particular, this will disable access to the storage backend
+        * via any of these services. Any future call to getService() will throw an exception.
+        *
+        * @see resetGlobalInstance()
+        */
+       public function destroy() {
+               foreach ( $this->getServiceNames() as $name ) {
+                       $service = $this->peekService( $name );
+                       if ( $service !== null && $service instanceof DestructibleService ) {
+                               $service->destroy();
+                       }
+               }
+
+               $this->destroyed = true;
+       }
+
+       /**
+        * @param array $wiringFiles A list of PHP files to load wiring information from.
+        * Each file is loaded using PHP's include mechanism. Each file is expected to
+        * return an associative array that maps service names to instantiator functions.
+        */
+       public function loadWiringFiles( array $wiringFiles ) {
+               foreach ( $wiringFiles as $file ) {
+                       // the wiring file is required to return an array of instantiators.
+                       $wiring = require $file;
+
+                       Assert::postcondition(
+                               is_array( $wiring ),
+                               "Wiring file $file is expected to return an array!"
+                       );
+
+                       $this->applyWiring( $wiring );
+               }
+       }
+
+       /**
+        * Registers multiple services (aka a "wiring").
+        *
+        * @param array $serviceInstantiators An associative array mapping service names to
+        *        instantiator functions.
+        */
+       public function applyWiring( array $serviceInstantiators ) {
+               Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' );
+
+               foreach ( $serviceInstantiators as $name => $instantiator ) {
+                       $this->defineService( $name, $instantiator );
+               }
+       }
+
+       /**
+        * Imports all wiring defined in $container. Wiring defined in $container
+        * will override any wiring already defined locally. However, already
+        * existing service instances will be preserved.
+        *
+        * @since 1.28
+        *
+        * @param ServiceContainer $container
+        * @param string[] $skip A list of service names to skip during import
+        */
+       public function importWiring( ServiceContainer $container, $skip = [] ) {
+               $newInstantiators = array_diff_key(
+                       $container->serviceInstantiators,
+                       array_flip( $skip )
+               );
+
+               $this->serviceInstantiators = array_merge(
+                       $this->serviceInstantiators,
+                       $newInstantiators
+               );
+       }
+
+       /**
+        * Returns true if a service is defined for $name, that is, if a call to getService( $name )
+        * would return a service instance.
+        *
+        * @param string $name
+        *
+        * @return bool
+        */
+       public function hasService( $name ) {
+               return isset( $this->serviceInstantiators[$name] );
+       }
+
+       /**
+        * Returns the service instance for $name only if that service has already been instantiated.
+        * This is intended for situations where services get destroyed/cleaned up, so we can
+        * avoid creating a service just to destroy it again.
+        *
+        * @note This is intended for internal use and for test fixtures.
+        * Application logic should use getService() instead.
+        *
+        * @see getService().
+        *
+        * @param string $name
+        *
+        * @return object|null The service instance, or null if the service has not yet been instantiated.
+        * @throws RuntimeException if $name does not refer to a known service.
+        */
+       public function peekService( $name ) {
+               if ( !$this->hasService( $name ) ) {
+                       throw new NoSuchServiceException( $name );
+               }
+
+               return isset( $this->services[$name] ) ? $this->services[$name] : null;
+       }
+
+       /**
+        * @return string[]
+        */
+       public function getServiceNames() {
+               return array_keys( $this->serviceInstantiators );
+       }
+
+       /**
+        * Define a new service. The service must not be known already.
+        *
+        * @see getService().
+        * @see replaceService().
+        *
+        * @param string $name The name of the service to register, for use with getService().
+        * @param callable $instantiator Callback that returns a service instance.
+        *        Will be called with this MediaWikiServices instance as the only parameter.
+        *        Any extra instantiation parameters provided to the constructor will be
+        *        passed as subsequent parameters when invoking the instantiator.
+        *
+        * @throws RuntimeException if there is already a service registered as $name.
+        */
+       public function defineService( $name, callable $instantiator ) {
+               Assert::parameterType( 'string', $name, '$name' );
+
+               if ( $this->hasService( $name ) ) {
+                       throw new ServiceAlreadyDefinedException( $name );
+               }
+
+               $this->serviceInstantiators[$name] = $instantiator;
+       }
+
+       /**
+        * Replace an already defined service.
+        *
+        * @see defineService().
+        *
+        * @note This causes any previously instantiated instance of the service to be discarded.
+        *
+        * @param string $name The name of the service to register.
+        * @param callable $instantiator Callback function that returns a service instance.
+        *        Will be called with this MediaWikiServices instance as the only parameter.
+        *        The instantiator must return a service compatible with the originally defined service.
+        *        Any extra instantiation parameters provided to the constructor will be
+        *        passed as subsequent parameters when invoking the instantiator.
+        *
+        * @throws RuntimeException if $name is not a known service.
+        */
+       public function redefineService( $name, callable $instantiator ) {
+               Assert::parameterType( 'string', $name, '$name' );
+
+               if ( !$this->hasService( $name ) ) {
+                       throw new NoSuchServiceException( $name );
+               }
+
+               if ( isset( $this->services[$name] ) ) {
+                       throw new CannotReplaceActiveServiceException( $name );
+               }
+
+               $this->serviceInstantiators[$name] = $instantiator;
+               unset( $this->disabled[$name] );
+       }
+
+       /**
+        * Disables a service.
+        *
+        * @note Attempts to call getService() for a disabled service will result
+        * in a DisabledServiceException. Calling peekService for a disabled service will
+        * return null. Disabled services are listed by getServiceNames(). A disabled service
+        * can be enabled again using redefineService().
+        *
+        * @note If the service was already active (that is, instantiated) when getting disabled,
+        * and the service instance implements DestructibleService, destroy() is called on the
+        * service instance.
+        *
+        * @see redefineService()
+        * @see resetService()
+        *
+        * @param string $name The name of the service to disable.
+        *
+        * @throws RuntimeException if $name is not a known service.
+        */
+       public function disableService( $name ) {
+               $this->resetService( $name );
+
+               $this->disabled[$name] = true;
+       }
+
+       /**
+        * Resets a service by dropping the service instance.
+        * If the service instances implements DestructibleService, destroy()
+        * is called on the service instance.
+        *
+        * @warning This is generally unsafe! Other services may still retain references
+        * to the stale service instance, leading to failures and inconsistencies. Subclasses
+        * may use this method to reset specific services under specific instances, but
+        * it should not be exposed to application logic.
+        *
+        * @note This is declared final so subclasses can not interfere with the expectations
+        * disableService() has when calling resetService().
+        *
+        * @see redefineService()
+        * @see disableService().
+        *
+        * @param string $name The name of the service to reset.
+        * @param bool $destroy Whether the service instance should be destroyed if it exists.
+        *        When set to false, any existing service instance will effectively be detached
+        *        from the container.
+        *
+        * @throws RuntimeException if $name is not a known service.
+        */
+       final protected function resetService( $name, $destroy = true ) {
+               Assert::parameterType( 'string', $name, '$name' );
+
+               $instance = $this->peekService( $name );
+
+               if ( $destroy && $instance instanceof DestructibleService )  {
+                       $instance->destroy();
+               }
+
+               unset( $this->services[$name] );
+               unset( $this->disabled[$name] );
+       }
+
+       /**
+        * Returns a service object of the kind associated with $name.
+        * Services instances are instantiated lazily, on demand.
+        * This method may or may not return the same service instance
+        * when called multiple times with the same $name.
+        *
+        * @note Rather than calling this method directly, it is recommended to provide
+        * getters with more meaningful names and more specific return types, using
+        * a subclass or wrapper.
+        *
+        * @see redefineService().
+        *
+        * @param string $name The service name
+        *
+        * @throws NoSuchServiceException if $name is not a known service.
+        * @throws ContainerDisabledException if this container has already been destroyed.
+        * @throws ServiceDisabledException if the requested service has been disabled.
+        *
+        * @return object The service instance
+        */
+       public function getService( $name ) {
+               if ( $this->destroyed ) {
+                       throw new ContainerDisabledException();
+               }
+
+               if ( isset( $this->disabled[$name] ) ) {
+                       throw new ServiceDisabledException( $name );
+               }
+
+               if ( !isset( $this->services[$name] ) ) {
+                       $this->services[$name] = $this->createService( $name );
+               }
+
+               return $this->services[$name];
+       }
+
+       /**
+        * @param string $name
+        *
+        * @throws InvalidArgumentException if $name is not a known service.
+        * @return object
+        */
+       private function createService( $name ) {
+               if ( isset( $this->serviceInstantiators[$name] ) ) {
+                       $service = call_user_func_array(
+                               $this->serviceInstantiators[$name],
+                               array_merge( [ $this ], $this->extraInstantiationParams )
+                       );
+                       // NOTE: when adding more wiring logic here, make sure copyWiring() is kept in sync!
+               } else {
+                       throw new NoSuchServiceException( $name );
+               }
+
+               return $service;
+       }
+
+       /**
+        * @param string $name
+        * @return bool Whether the service is disabled
+        * @since 1.28
+        */
+       public function isServiceDisabled( $name ) {
+               return isset( $this->disabled[$name] );
+       }
+}
diff --git a/includes/services/ServiceDisabledException.php b/includes/services/ServiceDisabledException.php
new file mode 100644 (file)
index 0000000..ae15b7c
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to access a disabled service.
+ *
+ * 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
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to access a disabled service.
+ */
+class ServiceDisabledException extends RuntimeException {
+
+       /**
+        * @param string $serviceName
+        * @param Exception|null $previous
+        */
+       public function __construct( $serviceName, Exception $previous = null ) {
+               parent::__construct( "Service disabled: $serviceName", 0, $previous );
+       }
+
+}
index 432d5ce..e5247f2 100644 (file)
@@ -130,8 +130,6 @@ class DBSiteStore implements SiteStore {
                                $this->sites->setSite( $site );
                        }
                }
-
-               $this->dbLoadBalancer->reuseConnection( $dbr );
        }
 
        /**
@@ -249,8 +247,6 @@ class DBSiteStore implements SiteStore {
 
                $dbw->endAtomic( __METHOD__ );
 
-               $this->dbLoadBalancer->reuseConnection( $dbw );
-
                $this->reset();
 
                return $success;
@@ -280,8 +276,6 @@ class DBSiteStore implements SiteStore {
                $ok = $dbw->delete( 'site_identifiers', '*', __METHOD__ ) && $ok;
                $dbw->endAtomic( __METHOD__ );
 
-               $this->dbLoadBalancer->reuseConnection( $dbw );
-
                $this->reset();
 
                return $ok;
index 2d37a0f..87865df 100644 (file)
@@ -55,7 +55,6 @@ abstract class BaseTemplate extends QuickTemplate {
         * @return array
         */
        function getToolbox() {
-
                $toolbox = [];
                if ( isset( $this->data['nav_urls']['whatlinkshere'] )
                        && $this->data['nav_urls']['whatlinkshere']
@@ -69,6 +68,7 @@ abstract class BaseTemplate extends QuickTemplate {
                        $toolbox['recentchangeslinked'] = $this->data['nav_urls']['recentchangeslinked'];
                        $toolbox['recentchangeslinked']['msg'] = 'recentchangeslinked-toolbox';
                        $toolbox['recentchangeslinked']['id'] = 't-recentchangeslinked';
+                       $toolbox['recentchangeslinked']['rel'] = 'nofollow';
                }
                if ( isset( $this->data['feeds'] ) && $this->data['feeds'] ) {
                        $toolbox['feeds']['id'] = 'feedlinks';
index 2351ab8..3efbd3b 100644 (file)
@@ -180,6 +180,7 @@ class SkinTemplate extends Skin {
                                        'text' => $ilLangName,
                                        'title' => $ilTitle,
                                        'class' => $class,
+                                       'link-class' => 'interlanguage-link-target',
                                        'lang' => $ilInterwikiCodeBCP47,
                                        'hreflang' => $ilInterwikiCodeBCP47,
                                ];
@@ -1302,6 +1303,7 @@ class SkinTemplate extends Skin {
 
                        if ( $this->showEmailUser( $user ) ) {
                                $nav_urls['emailuser'] = [
+                                       'text' => $this->msg( 'tool-link-emailuser', $rootUser )->text(),
                                        'href' => self::makeSpecialUrlSubpage( 'Emailuser', $rootUser ),
                                        'tooltip-params' => [ $rootUser ],
                                ];
@@ -1312,6 +1314,7 @@ class SkinTemplate extends Skin {
                                $sur->setContext( $this->getContext() );
                                if ( $sur->userCanExecute( $this->getUser() ) ) {
                                        $nav_urls['userrights'] = [
+                                               'text' => $this->msg( 'tool-link-userrights', $this->getUser()->getName() )->text(),
                                                'href' => self::makeSpecialUrlSubpage( 'Userrights', $rootUser )
                                        ];
                                }
index 3adf5a6..bdf7638 100644 (file)
@@ -43,7 +43,7 @@ abstract class AuthManagerSpecialPage extends SpecialPage {
         * Change the form descriptor that determines how a field will look in the authentication form.
         * Called from fieldInfoToFormDescriptor().
         * @param AuthenticationRequest[] $requests
-        * @param string $fieldInfo Field information array (union of all
+        * @param array $fieldInfo Field information array (union of all
         *    AuthenticationRequest::getFieldInfo() responses).
         * @param array $formDescriptor HTMLForm descriptor. The special key 'weight' can be set to
         *    change the order of the fields.
@@ -205,6 +205,7 @@ abstract class AuthManagerSpecialPage extends SpecialPage {
        /**
         * Return custom message key.
         * Allows subclasses to customize messages.
+        * @param string $defaultKey
         * @return string
         */
        protected function messageKey( $defaultKey ) {
@@ -668,6 +669,7 @@ abstract class AuthManagerSpecialPage extends SpecialPage {
         * Maps an authentication field configuration for a single field (as returned by
         * AuthenticationRequest::getFieldInfo()) to a HTMLForm field descriptor.
         * @param array $singleFieldInfo
+        * @param string $fieldName
         * @return array
         */
        protected static function mapSingleFieldInfo( $singleFieldInfo, $fieldName ) {
index 9d17e7d..bf83e7b 100644 (file)
@@ -359,7 +359,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                                $this->authAction = $this->isSignup() ? AuthManager::ACTION_CREATE_CONTINUE
                                        : AuthManager::ACTION_LOGIN_CONTINUE;
                                $this->authRequests = $response->neededRequests;
-                               $this->mainLoginForm( $response->neededRequests, $response->message, 'warning' );
+                               $this->mainLoginForm( $response->neededRequests, $response->message, $response->messageType );
                                break;
                        default:
                                throw new LogicException( 'invalid AuthenticationResponse' );
@@ -499,7 +499,21 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
 
                $form = $this->getAuthForm( $requests, $this->authAction, $msg, $msgtype );
                $form->prepareForm();
-               $formHtml = $form->getHTML( $msg ? Status::newFatal( $msg ) : false );
+
+               $submitStatus = Status::newGood();
+               if ( $msg && $msgtype === 'warning' ) {
+                       $submitStatus->warning( $msg );
+               } elseif ( $msg && $msgtype === 'error' ) {
+                       $submitStatus->fatal( $msg );
+               }
+
+               // warning header for non-standard workflows (e.g. security reauthentication)
+               if ( !$this->isSignup() && $this->getUser()->isLoggedIn() ) {
+                       $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
+                       $submitStatus->warning( $reauthMessage, $this->getUser()->getName() );
+               }
+
+               $formHtml = $form->getHTML( $submitStatus );
 
                $out->addHTML( $this->getPageHtml( $formHtml ) );
        }
@@ -621,13 +635,6 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                        $form->setId( 'userlogin2' );
                }
 
-               // warning header for non-standard workflows (e.g. security reauthentication)
-               if ( !$this->isSignup() && $this->getUser()->isLoggedIn() ) {
-                       $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
-                       $form->addHeaderText( Html::rawElement( 'div', [ 'class' => 'warningbox' ],
-                               $this->msg( $reauthMessage )->params( $this->getUser()->getName() )->parse() ) );
-               }
-
                $form->suppressDefaultSubmit();
 
                $this->authForm = $form;
index 9975e41..1dd78d7 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 {
@@ -234,7 +224,7 @@ class SpecialBotPasswords extends FormSpecialPage {
                                        'name' => 'op',
                                        'value' => 'create',
                                        'label-message' => 'botpasswords-label-create',
-                                       'flags' => [ 'primary', 'constructive' ],
+                                       'flags' => [ 'primary', 'progressive' ],
                                ] );
                        }
 
@@ -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 68289a7..0858b18 100644 (file)
@@ -496,12 +496,12 @@ class SpecialContributions extends IncludableSpecialPage {
 
                if ( $tagFilter ) {
                        $filterSelection = Html::rawElement(
-                               'td',
+                               'div',
                                [],
                                implode( '&#160;', $tagFilter )
                        );
                } else {
-                       $filterSelection = Html::rawElement( 'td', [ 'colspan' => 2 ], '' );
+                       $filterSelection = Html::rawElement( 'div', [], '' );
                }
 
                $this->getOutput()->addModules( 'mediawiki.userSuggest' );
@@ -542,13 +542,13 @@ class SpecialContributions extends IncludableSpecialPage {
                );
 
                $targetSelection = Html::rawElement(
-                       'td',
-                       [ 'colspan' => 2 ],
-                       $labelNewbies . '<br />' . $labelUsername . ' ' . $input . ' '
+                       'div',
+                       [],
+                       $labelNewbies . '<br>' . $labelUsername . ' ' . $input . ' '
                );
 
                $namespaceSelection = Xml::tags(
-                       'td',
+                       'div',
                        [],
                        Xml::label(
                                $this->msg( 'namespace' )->text(),
@@ -647,12 +647,12 @@ class SpecialContributions extends IncludableSpecialPage {
                );
 
                $extraOptions = Html::rawElement(
-                       'td',
-                       [ 'colspan' => 2 ],
+                       'div',
+                       [],
                        implode( '', $filters )
                );
 
-               $dateSelectionAndSubmit = Xml::tags( 'td', [ 'colspan' => 2 ],
+               $dateSelectionAndSubmit = Xml::tags( 'div', [],
                        Xml::dateMenu(
                                $this->opts['year'] === '' ? MWTimestamp::getInstance()->format( 'Y' ) : $this->opts['year'],
                                $this->opts['month']
@@ -663,13 +663,14 @@ class SpecialContributions extends IncludableSpecialPage {
                                )
                );
 
-               $form .= Xml::fieldset( $this->msg( 'sp-contributions-search' )->text() );
-               $form .= Html::rawElement( 'table', [ 'class' => 'mw-contributions-table' ], "\n" .
-                       Html::rawElement( 'tr', [], $targetSelection ) . "\n" .
-                       Html::rawElement( 'tr', [], $namespaceSelection ) . "\n" .
-                       Html::rawElement( 'tr', [], $filterSelection ) . "\n" .
-                       Html::rawElement( 'tr', [], $extraOptions ) . "\n" .
-                       Html::rawElement( 'tr', [], $dateSelectionAndSubmit ) . "\n"
+               $form .= Xml::fieldset(
+                       $this->msg( 'sp-contributions-search' )->text(),
+                       $targetSelection .
+                       $namespaceSelection .
+                       $filterSelection .
+                       $extraOptions .
+                       $dateSelectionAndSubmit,
+                       [ 'class' => 'mw-contributions-table' ]
                );
 
                $explain = $this->msg( 'sp-contributions-explain' );
@@ -677,7 +678,7 @@ class SpecialContributions extends IncludableSpecialPage {
                        $form .= "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>";
                }
 
-               $form .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' );
+               $form .= Xml::closeElement( 'form' );
 
                return $form;
        }
index a5a45d5..0defcd1 100644 (file)
@@ -68,8 +68,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
         */
        private function initServices() {
                if ( !$this->titleParser ) {
-                       $lang = $this->getContext()->getLanguage();
-                       $this->titleParser = new MediaWikiTitleCodec( $lang, GenderCache::singleton() );
+                       $this->titleParser = MediaWikiServices::getInstance()->getTitleParser();
                }
        }
 
@@ -205,7 +204,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
                        }
                }
 
-               GenderCache::singleton()->doTitlesArray( $titles );
+               MediaWikiServices::getInstance()->getGenderCache()->doTitlesArray( $titles );
 
                $list = [];
                /** @var Title $title */
@@ -355,7 +354,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
                                }
                        }
 
-                       GenderCache::singleton()->doTitlesArray( $titles );
+                       MediaWikiServices::getInstance()->getGenderCache()->doTitlesArray( $titles );
 
                        foreach ( $titles as $title ) {
                                $list[] = $title->getPrefixedText();
@@ -431,20 +430,22 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
                }
 
                $user = $this->getUser();
-               $store = MediaWikiServices::getInstance()->getWatchedItemStore();
-
-               foreach ( $this->badItems as $row ) {
-                       list( $title, $namespace, $dbKey ) = $row;
-                       $action = $title ? 'cleaning up' : 'deleting';
-                       wfDebug( "User {$user->getName()} has broken watchlist item ns($namespace):$dbKey, $action.\n" );
-
-                       $store->removeWatch( $user, new TitleValue( (int)$namespace, $dbKey ) );
-
-                       // Can't just do an UPDATE instead of DELETE/INSERT due to unique index
-                       if ( $title ) {
-                               $user->addWatch( $title );
+               $badItems = $this->badItems;
+               DeferredUpdates::addCallableUpdate( function () use ( $user, $badItems ) {
+                       $store = MediaWikiServices::getInstance()->getWatchedItemStore();
+                       foreach ( $badItems as $row ) {
+                               list( $title, $namespace, $dbKey ) = $row;
+                               $action = $title ? 'cleaning up' : 'deleting';
+                               wfDebug( "User {$user->getName()} has broken watchlist item " .
+                                       "ns($namespace):$dbKey, $action.\n" );
+
+                               $store->removeWatch( $user, new TitleValue( (int)$namespace, $dbKey ) );
+                               // Can't just do an UPDATE instead of DELETE/INSERT due to unique index
+                               if ( $title ) {
+                                       $user->addWatch( $title );
+                               }
                        }
-               }
+               } );
        }
 
        /**
index fe1dd98..c58af60 100644 (file)
@@ -594,9 +594,13 @@ class ImportReporter extends ContextSource {
                $this->mPageCount++;
 
                if ( $successCount > 0 ) {
+                       // <bdi> prevents jumbling of the versions count
+                       // in RTL wikis in case the page title is LTR
                        $this->getOutput()->addHTML(
                                "<li>" . Linker::linkKnown( $title ) . " " .
+                                       "<bdi>" .
                                        $this->msg( 'import-revision-count' )->numParams( $successCount )->escaped() .
+                                       "</bdi>" .
                                        "</li>\n"
                        );
 
index e3be225..6093f83 100644 (file)
@@ -35,7 +35,7 @@ class MIMEsearchPage extends QueryPage {
        }
 
        public function isExpensive() {
-               return true;
+               return false;
        }
 
        function isSyndicated() {
index 3a12cf3..7b7661d 100644 (file)
@@ -462,7 +462,7 @@ class MovePageForm extends UnlistedSpecialPage {
                                'name' => 'wpMove',
                                'value' => $this->msg( 'movepagebtn' )->text(),
                                'label' => $this->msg( 'movepagebtn' )->text(),
-                               'flags' => [ 'constructive', 'primary' ],
+                               'flags' => [ 'primary', 'progressive' ],
                                'type' => 'submit',
                        ] ),
                        [
old mode 100644 (file)
new mode 100755 (executable)
index 718a6dc..d719e53
@@ -376,7 +376,11 @@ class SpecialNewpages extends IncludableSpecialPage {
 
                if ( !$title->equals( $oldTitle ) ) {
                        $oldTitleText = $oldTitle->getPrefixedText();
-                       $oldTitleText = $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped();
+                       $oldTitleText = Html::rawElement(
+                               'span',
+                               [ 'class' => 'mw-newpages-oldtitle' ],
+                               $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped()
+                       );
                }
 
                return "<li{$css}>{$time} {$dm}{$plink} {$hist} {$dm}{$length} "
index 3697e5d..eee5b64 100644 (file)
@@ -59,7 +59,7 @@ class SpecialPreferences extends SpecialPage {
                        $session->remove( 'specialPreferencesSaveSuccess' );
                        $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' );
 
-                       $out->addHtml(
+                       $out->addHTML(
                                Html::rawElement(
                                        'div',
                                        [
index fc924a4..adf12d4 100644 (file)
@@ -272,7 +272,7 @@ class SpecialRandomInCategory extends FormSpecialPage {
                                'high' => 'MAX( cl_timestamp )'
                        ],
                        [
-                               'cl_to' => $this->category->getDBKey(),
+                               'cl_to' => $this->category->getDBkey(),
                        ],
                        __METHOD__,
                        [
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 8a06abf..433dcab 100644 (file)
@@ -156,7 +156,7 @@ class UserrightsPage extends SpecialPage {
                if ( $request->getCheck( 'success' ) && $this->mFetchedUser !== null ) {
                        $out->addModules( [ 'mediawiki.special.userrights' ] );
                        $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' );
-                       $out->addHtml(
+                       $out->addHTML(
                                Html::rawElement(
                                        'div',
                                        [
index c8b85ae..2cd492e 100644 (file)
@@ -633,7 +633,7 @@ class SpecialVersion extends SpecialPage {
                        usort( $wgExtensionCredits[$type], [ $this, 'compare' ] );
 
                        foreach ( $wgExtensionCredits[$type] as $extension ) {
-                               $out .= $this->getCreditsForExtension( $extension );
+                               $out .= $this->getCreditsForExtension( $type, $extension );
                        }
                }
 
@@ -669,11 +669,12 @@ class SpecialVersion extends SpecialPage {
         *  - Description of extension (descriptionmsg or description)
         *  - List of authors (author) and link to a ((AUTHORS)|(CREDITS))(\.txt)? file if it exists
         *
+        * @param string $type Category name of the extension
         * @param array $extension
         *
         * @return string Raw HTML
         */
-       public function getCreditsForExtension( array $extension ) {
+       public function getCreditsForExtension( $type, array $extension ) {
                $out = $this->getOutput();
 
                // We must obtain the information for all the bits and pieces!
@@ -830,7 +831,7 @@ class SpecialVersion extends SpecialPage {
                // Finally! Create the table
                $html = Html::openElement( 'tr', [
                                'class' => 'mw-version-ext',
-                               'id' => Sanitizer::escapeId( 'mw-version-ext-' . $extension['name'] )
+                               'id' => Sanitizer::escapeId( 'mw-version-ext-' . $type . '-' . $extension['name'] )
                        ]
                );
 
index 666f3f9..069b460 100644 (file)
@@ -729,7 +729,7 @@ class BalanceStack implements IteratorAggregate {
                        $this->config['tidyCompat'] && !$isComment &&
                        $this->currentNode->isA( BalanceSets::$tidyPWrapSet )
                ) {
-                       $this->insertHTMLELement( 'mw:p-wrap', [] );
+                       $this->insertHTMLElement( 'mw:p-wrap', [] );
                        return $this->insertText( $value );
                } else {
                        $this->currentNode->appendChild( $value );
index 1be5c24..91b4133 100644 (file)
@@ -446,7 +446,8 @@ abstract class UploadBase {
                        return $status;
                }
 
-               $this->mFileProps = FSFile::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
+               $mwProps = new MWFileProps( MimeMagic::singleton() );
+               $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
                $mime = $this->mFileProps['mime'];
 
                if ( $wgVerifyMimeType ) {
@@ -504,7 +505,8 @@ abstract class UploadBase {
                # getTitle() sets some internal parameters like $this->mFinalExtension
                $this->getTitle();
 
-               $this->mFileProps = FSFile::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
+               $mwProps = new MWFileProps( MimeMagic::singleton() );
+               $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
 
                # check MIME type, if desired
                $mime = $this->mFileProps['file-mime'];
index 9145a85..08cf434 100644 (file)
@@ -130,7 +130,7 @@ class UploadFromChunks extends UploadFromFile {
                // Get the file extension from the last chunk
                $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath );
                // Get a 0-byte temp file to perform the concatenation at
-               $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext );
+               $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext, wfTempDir() );
                $tmpPath = false; // fail in concatenate()
                if ( $tmpFile ) {
                        // keep alive with $this
index 6639c34..865f630 100644 (file)
@@ -202,7 +202,7 @@ class UploadFromUrl extends UploadBase {
         * @return string Path to the file
         */
        protected function makeTemporaryFile() {
-               $tmpFile = TempFSFile::factory( 'URL' );
+               $tmpFile = TempFSFile::factory( 'URL', 'urlupload_', wfTempDir() );
                $tmpFile->bind( $this );
 
                return $tmpFile->getPath();
index 000c6a4..b7160b3 100644 (file)
@@ -207,7 +207,9 @@ class UploadStash {
                        wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist\n" );
                        throw new UploadStashBadPathException( "path doesn't exist" );
                }
-               $fileProps = FSFile::getPropsFromPath( $path );
+
+               $mwProps = new MWFileProps( MimeMagic::singleton() );
+               $fileProps = $mwProps->getPropsFromPath( $path, true );
                wfDebug( __METHOD__ . " stashing file at '$path'\n" );
 
                // we will be initializing from some tmpnam files that don't have extensions.
index eae57f4..57610fc 100644 (file)
@@ -68,7 +68,7 @@ class BotPassword implements IDBAccessObject {
        /**
         * Get a database connection for the bot passwords database
         * @param int $db Index of the connection to get, e.g. DB_MASTER or DB_REPLICA.
-        * @return DatabaseBase
+        * @return Database
         */
        public static function getDB( $db ) {
                global $wgBotPasswordsCluster, $wgBotPasswordsDatabase;
index 0d06c7b..00fc9be 100644 (file)
@@ -1434,11 +1434,11 @@ class User implements IDBAccessObject {
         * protected against race conditions using a compare-and-set (CAS) mechanism
         * based on comparing $this->mTouched with the user_touched field.
         *
-        * @param DatabaseBase $db
-        * @param array $conditions WHERE conditions for use with DatabaseBase::update
-        * @return array WHERE conditions for use with DatabaseBase::update
+        * @param Database $db
+        * @param array $conditions WHERE conditions for use with Database::update
+        * @return array WHERE conditions for use with Database::update
         */
-       protected function makeUpdateConditions( DatabaseBase $db, array $conditions ) {
+       protected function makeUpdateConditions( Database $db, array $conditions ) {
                if ( $this->mTouched ) {
                        // CAS check: only update if the row wasn't changed sicne it was loaded.
                        $conditions['user_touched'] = $db->timestamp( $this->mTouched );
@@ -1802,12 +1802,16 @@ class User implements IDBAccessObject {
                        return false;
                }
 
+               $limits = array_merge(
+                       [ '&can-bypass' => true ],
+                       $wgRateLimits[$action]
+               );
+
                // Some groups shouldn't trigger the ping limiter, ever
-               if ( !$this->isPingLimitable() ) {
+               if ( $limits['&can-bypass'] && !$this->isPingLimitable() ) {
                        return false;
                }
 
-               $limits = $wgRateLimits[$action];
                $keys = [];
                $id = $this->getId();
                $userLimit = false;
index 395ce37..319b5d4 100644 (file)
@@ -160,6 +160,7 @@ class AutoloadGenerator {
         *
         * @param {string} $commandName Command name to include in comment
         * @param {string} $filename of PHP file to put autoload information in.
+        * @return string
         */
        protected function generatePHPAutoload( $commandName, $filename ) {
                // No existing JSON file found; update/generate PHP file
@@ -290,6 +291,10 @@ EOD;
                foreach ( glob( $this->basepath . '/*.php' ) as $file ) {
                        $this->readFile( $file );
                }
+
+               // Legacy aliases
+               $this->forceClassPath( 'DatabaseBase',
+                       $this->basepath . '/includes/libs/rdbms/database/Database.php' );
        }
 }
 
diff --git a/includes/utils/IP.php b/includes/utils/IP.php
deleted file mode 100644 (file)
index 8676baf..0000000
+++ /dev/null
@@ -1,791 +0,0 @@
-<?php
-/**
- * Functions and constants to play with IP addresses and ranges
- *
- * 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 Antoine Musso "<hashar at free dot fr>", Aaron Schulz
- */
-
-use IPSet\IPSet;
-
-// Some regex definition to "play" with IP address and IP address blocks
-
-// An IPv4 address is made of 4 bytes from x00 to xFF which is d0 to d255
-define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])' );
-define( 'RE_IP_ADD', RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE );
-// An IPv4 block is an IP address and a prefix (d1 to d32)
-define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)' );
-define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX );
-
-// An IPv6 address is made up of 8 words (each x0000 to xFFFF).
-// However, the "::" abbreviation can be used on consecutive x0000 words.
-define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' );
-define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)' );
-define( 'RE_IPV6_ADD',
-       '(?:' . // starts with "::" (including "::")
-               ':(?::|(?::' . RE_IPV6_WORD . '){1,7})' .
-       '|' . // ends with "::" (except "::")
-               RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,6}::' .
-       '|' . // contains one "::" in the middle (the ^ makes the test fail if none found)
-               RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)' .
-       '|' . // contains no "::"
-               RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' .
-       ')'
-);
-// An IPv6 block is an IP address and a prefix (d1 to d128)
-define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX );
-// For IPv6 canonicalization (NOT for strict validation; these are quite lax!)
-define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' );
-define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' );
-
-// This might be useful for regexps used elsewhere, matches any IPv4 or IPv6 address or network
-define( 'IP_ADDRESS_STRING',
-       '(?:' .
-               RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?' . // IPv4
-       '|' .
-               RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?' . // IPv6
-       ')'
-);
-
-/**
- * A collection of public static functions to play with IP address
- * and IP blocks.
- */
-class IP {
-       /** @var IPSet */
-       private static $proxyIpSet = null;
-
-       /**
-        * Determine if a string is as valid IP address or network (CIDR prefix).
-        * SIIT IPv4-translated addresses are rejected.
-        * @note canonicalize() tries to convert translated addresses to IPv4.
-        *
-        * @param string $ip Possible IP address
-        * @return bool
-        */
-       public static function isIPAddress( $ip ) {
-               return (bool)preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip );
-       }
-
-       /**
-        * Given a string, determine if it as valid IP in IPv6 only.
-        * @note Unlike isValid(), this looks for networks too.
-        *
-        * @param string $ip Possible IP address
-        * @return bool
-        */
-       public static function isIPv6( $ip ) {
-               return (bool)preg_match( '/^' . RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?$/', $ip );
-       }
-
-       /**
-        * Given a string, determine if it as valid IP in IPv4 only.
-        * @note Unlike isValid(), this looks for networks too.
-        *
-        * @param string $ip Possible IP address
-        * @return bool
-        */
-       public static function isIPv4( $ip ) {
-               return (bool)preg_match( '/^' . RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?$/', $ip );
-       }
-
-       /**
-        * Validate an IP address. Ranges are NOT considered valid.
-        * SIIT IPv4-translated addresses are rejected.
-        * @note canonicalize() tries to convert translated addresses to IPv4.
-        *
-        * @param string $ip
-        * @return bool True if it is valid
-        */
-       public static function isValid( $ip ) {
-               return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip )
-                       || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip ) );
-       }
-
-       /**
-        * Validate an IP Block (valid address WITH a valid prefix).
-        * SIIT IPv4-translated addresses are rejected.
-        * @note canonicalize() tries to convert translated addresses to IPv4.
-        *
-        * @param string $ipblock
-        * @return bool True if it is valid
-        */
-       public static function isValidBlock( $ipblock ) {
-               return ( preg_match( '/^' . RE_IPV6_BLOCK . '$/', $ipblock )
-                       || preg_match( '/^' . RE_IP_BLOCK . '$/', $ipblock ) );
-       }
-
-       /**
-        * Convert an IP into a verbose, uppercase, normalized form.
-        * Both IPv4 and IPv6 addresses are trimmed. Additionally,
-        * IPv6 addresses in octet notation are expanded to 8 words;
-        * IPv4 addresses have leading zeros, in each octet, removed.
-        *
-        * @param string $ip IP address in quad or octet form (CIDR or not).
-        * @return string
-        */
-       public static function sanitizeIP( $ip ) {
-               $ip = trim( $ip );
-               if ( $ip === '' ) {
-                       return null;
-               }
-               /* If not an IP, just return trimmed value, since sanitizeIP() is called
-                * in a number of contexts where usernames are supplied as input.
-                */
-               if ( !self::isIPAddress( $ip ) ) {
-                       return $ip;
-               }
-               if ( self::isIPv4( $ip ) ) {
-                       // Remove leading 0's from octet representation of IPv4 address
-                       $ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip );
-                       return $ip;
-               }
-               // Remove any whitespaces, convert to upper case
-               $ip = strtoupper( $ip );
-               // Expand zero abbreviations
-               $abbrevPos = strpos( $ip, '::' );
-               if ( $abbrevPos !== false ) {
-                       // We know this is valid IPv6. Find the last index of the
-                       // address before any CIDR number (e.g. "a:b:c::/24").
-                       $CIDRStart = strpos( $ip, "/" );
-                       $addressEnd = ( $CIDRStart !== false )
-                               ? $CIDRStart - 1
-                               : strlen( $ip ) - 1;
-                       // If the '::' is at the beginning...
-                       if ( $abbrevPos == 0 ) {
-                               $repeat = '0:';
-                               $extra = ( $ip == '::' ) ? '0' : ''; // for the address '::'
-                               $pad = 9; // 7+2 (due to '::')
-                       // If the '::' is at the end...
-                       } elseif ( $abbrevPos == ( $addressEnd - 1 ) ) {
-                               $repeat = ':0';
-                               $extra = '';
-                               $pad = 9; // 7+2 (due to '::')
-                       // If the '::' is in the middle...
-                       } else {
-                               $repeat = ':0';
-                               $extra = ':';
-                               $pad = 8; // 6+2 (due to '::')
-                       }
-                       $ip = str_replace( '::',
-                               str_repeat( $repeat, $pad - substr_count( $ip, ':' ) ) . $extra,
-                               $ip
-                       );
-               }
-               // Remove leading zeros from each bloc as needed
-               $ip = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip );
-
-               return $ip;
-       }
-
-       /**
-        * Prettify an IP for display to end users.
-        * This will make it more compact and lower-case.
-        *
-        * @param string $ip
-        * @return string
-        */
-       public static function prettifyIP( $ip ) {
-               $ip = self::sanitizeIP( $ip ); // normalize (removes '::')
-               if ( self::isIPv6( $ip ) ) {
-                       // Split IP into an address and a CIDR
-                       if ( strpos( $ip, '/' ) !== false ) {
-                               list( $ip, $cidr ) = explode( '/', $ip, 2 );
-                       } else {
-                               list( $ip, $cidr ) = [ $ip, '' ];
-                       }
-                       // Get the largest slice of words with multiple zeros
-                       $offset = 0;
-                       $longest = $longestPos = false;
-                       while ( preg_match(
-                               '!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset
-                       ) ) {
-                               list( $match, $pos ) = $m[0]; // full match
-                               if ( strlen( $match ) > strlen( $longest ) ) {
-                                       $longest = $match;
-                                       $longestPos = $pos;
-                               }
-                               $offset = ( $pos + strlen( $match ) ); // advance
-                       }
-                       if ( $longest !== false ) {
-                               // Replace this portion of the string with the '::' abbreviation
-                               $ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) );
-                       }
-                       // Add any CIDR back on
-                       if ( $cidr !== '' ) {
-                               $ip = "{$ip}/{$cidr}";
-                       }
-                       // Convert to lower case to make it more readable
-                       $ip = strtolower( $ip );
-               }
-
-               return $ip;
-       }
-
-       /**
-        * Given a host/port string, like one might find in the host part of a URL
-        * per RFC 2732, split the hostname part and the port part and return an
-        * array with an element for each. If there is no port part, the array will
-        * have false in place of the port. If the string was invalid in some way,
-        * false is returned.
-        *
-        * This was easy with IPv4 and was generally done in an ad-hoc way, but
-        * with IPv6 it's somewhat more complicated due to the need to parse the
-        * square brackets and colons.
-        *
-        * A bare IPv6 address is accepted despite the lack of square brackets.
-        *
-        * @param string $both The string with the host and port
-        * @return array|false Array normally, false on certain failures
-        */
-       public static function splitHostAndPort( $both ) {
-               if ( substr( $both, 0, 1 ) === '[' ) {
-                       if ( preg_match( '/^\[(' . RE_IPV6_ADD . ')\](?::(?P<port>\d+))?$/', $both, $m ) ) {
-                               if ( isset( $m['port'] ) ) {
-                                       return [ $m[1], intval( $m['port'] ) ];
-                               } else {
-                                       return [ $m[1], false ];
-                               }
-                       } else {
-                               // Square bracket found but no IPv6
-                               return false;
-                       }
-               }
-               $numColons = substr_count( $both, ':' );
-               if ( $numColons >= 2 ) {
-                       // Is it a bare IPv6 address?
-                       if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) {
-                               return [ $both, false ];
-                       } else {
-                               // Not valid IPv6, but too many colons for anything else
-                               return false;
-                       }
-               }
-               if ( $numColons >= 1 ) {
-                       // Host:port?
-                       $bits = explode( ':', $both );
-                       if ( preg_match( '/^\d+/', $bits[1] ) ) {
-                               return [ $bits[0], intval( $bits[1] ) ];
-                       } else {
-                               // Not a valid port
-                               return false;
-                       }
-               }
-
-               // Plain hostname
-               return [ $both, false ];
-       }
-
-       /**
-        * Given a host name and a port, combine them into host/port string like
-        * you might find in a URL. If the host contains a colon, wrap it in square
-        * brackets like in RFC 2732. If the port matches the default port, omit
-        * the port specification
-        *
-        * @param string $host
-        * @param int $port
-        * @param bool|int $defaultPort
-        * @return string
-        */
-       public static function combineHostAndPort( $host, $port, $defaultPort = false ) {
-               if ( strpos( $host, ':' ) !== false ) {
-                       $host = "[$host]";
-               }
-               if ( $defaultPort !== false && $port == $defaultPort ) {
-                       return $host;
-               } else {
-                       return "$host:$port";
-               }
-       }
-
-       /**
-        * Convert an IPv4 or IPv6 hexadecimal representation back to readable format
-        *
-        * @param string $hex Number, with "v6-" prefix if it is IPv6
-        * @return string Quad-dotted (IPv4) or octet notation (IPv6)
-        */
-       public static function formatHex( $hex ) {
-               if ( substr( $hex, 0, 3 ) == 'v6-' ) { // IPv6
-                       return self::hexToOctet( substr( $hex, 3 ) );
-               } else { // IPv4
-                       return self::hexToQuad( $hex );
-               }
-       }
-
-       /**
-        * Converts a hexadecimal number to an IPv6 address in octet notation
-        *
-        * @param string $ip_hex Pure hex (no v6- prefix)
-        * @return string (of format a:b:c:d:e:f:g:h)
-        */
-       public static function hexToOctet( $ip_hex ) {
-               // Pad hex to 32 chars (128 bits)
-               $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT );
-               // Separate into 8 words
-               $ip_oct = substr( $ip_hex, 0, 4 );
-               for ( $n = 1; $n < 8; $n++ ) {
-                       $ip_oct .= ':' . substr( $ip_hex, 4 * $n, 4 );
-               }
-               // NO leading zeroes
-               $ip_oct = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip_oct );
-
-               return $ip_oct;
-       }
-
-       /**
-        * Converts a hexadecimal number to an IPv4 address in quad-dotted notation
-        *
-        * @param string $ip_hex Pure hex
-        * @return string (of format a.b.c.d)
-        */
-       public static function hexToQuad( $ip_hex ) {
-               // Pad hex to 8 chars (32 bits)
-               $ip_hex = str_pad( strtoupper( $ip_hex ), 8, '0', STR_PAD_LEFT );
-               // Separate into four quads
-               $s = '';
-               for ( $i = 0; $i < 4; $i++ ) {
-                       if ( $s !== '' ) {
-                               $s .= '.';
-                       }
-                       $s .= base_convert( substr( $ip_hex, $i * 2, 2 ), 16, 10 );
-               }
-
-               return $s;
-       }
-
-       /**
-        * Determine if an IP address really is an IP address, and if it is public,
-        * i.e. not RFC 1918 or similar
-        *
-        * @param string $ip
-        * @return bool
-        */
-       public static function isPublic( $ip ) {
-               static $privateSet = null;
-               if ( !$privateSet ) {
-                       $privateSet = new IPSet( [
-                               '10.0.0.0/8', # RFC 1918 (private)
-                               '172.16.0.0/12', # RFC 1918 (private)
-                               '192.168.0.0/16', # RFC 1918 (private)
-                               '0.0.0.0/8', # this network
-                               '127.0.0.0/8', # loopback
-                               'fc00::/7', # RFC 4193 (local)
-                               '0:0:0:0:0:0:0:1', # loopback
-                               '169.254.0.0/16', # link-local
-                               'fe80::/10', # link-local
-                       ] );
-               }
-               return !$privateSet->match( $ip );
-       }
-
-       /**
-        * Return a zero-padded upper case hexadecimal representation of an IP address.
-        *
-        * Hexadecimal addresses are used because they can easily be extended to
-        * IPv6 support. To separate the ranges, the return value from this
-        * function for an IPv6 address will be prefixed with "v6-", a non-
-        * hexadecimal string which sorts after the IPv4 addresses.
-        *
-        * @param string $ip Quad dotted/octet IP address.
-        * @return string|bool False on failure
-        */
-       public static function toHex( $ip ) {
-               if ( self::isIPv6( $ip ) ) {
-                       $n = 'v6-' . self::IPv6ToRawHex( $ip );
-               } elseif ( self::isIPv4( $ip ) ) {
-                       // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08),
-                       // also double/triple 0 needs to be changed to just a single 0 for ip2long.
-                       $ip = self::sanitizeIP( $ip );
-                       $n = ip2long( $ip );
-                       if ( $n < 0 ) {
-                               $n += pow( 2, 32 );
-                               # On 32-bit platforms (and on Windows), 2^32 does not fit into an int,
-                               # so $n becomes a float. We convert it to string instead.
-                               if ( is_float( $n ) ) {
-                                       $n = (string)$n;
-                               }
-                       }
-                       if ( $n !== false ) {
-                               # Floating points can handle the conversion; faster than Wikimedia\base_convert()
-                               $n = strtoupper( str_pad( base_convert( $n, 10, 16 ), 8, '0', STR_PAD_LEFT ) );
-                       }
-               } else {
-                       $n = false;
-               }
-
-               return $n;
-       }
-
-       /**
-        * Given an IPv6 address in octet notation, returns a pure hex string.
-        *
-        * @param string $ip Octet ipv6 IP address.
-        * @return string|bool Pure hex (uppercase); false on failure
-        */
-       private static function IPv6ToRawHex( $ip ) {
-               $ip = self::sanitizeIP( $ip );
-               if ( !$ip ) {
-                       return false;
-               }
-               $r_ip = '';
-               foreach ( explode( ':', $ip ) as $v ) {
-                       $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT );
-               }
-
-               return $r_ip;
-       }
-
-       /**
-        * Convert a network specification in CIDR notation
-        * to an integer network and a number of bits
-        *
-        * @param string $range IP with CIDR prefix
-        * @return array(int or string, int)
-        */
-       public static function parseCIDR( $range ) {
-               if ( self::isIPv6( $range ) ) {
-                       return self::parseCIDR6( $range );
-               }
-               $parts = explode( '/', $range, 2 );
-               if ( count( $parts ) != 2 ) {
-                       return [ false, false ];
-               }
-               list( $network, $bits ) = $parts;
-               $network = ip2long( $network );
-               if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 32 ) {
-                       if ( $bits == 0 ) {
-                               $network = 0;
-                       } else {
-                               $network &= ~( ( 1 << ( 32 - $bits ) ) - 1 );
-                       }
-                       # Convert to unsigned
-                       if ( $network < 0 ) {
-                               $network += pow( 2, 32 );
-                       }
-               } else {
-                       $network = false;
-                       $bits = false;
-               }
-
-               return [ $network, $bits ];
-       }
-
-       /**
-        * Given a string range in a number of formats,
-        * return the start and end of the range in hexadecimal.
-        *
-        * Formats are:
-        *     1.2.3.4/24          CIDR
-        *     1.2.3.4 - 1.2.3.5   Explicit range
-        *     1.2.3.4             Single IP
-        *
-        *     2001:0db8:85a3::7344/96                       CIDR
-        *     2001:0db8:85a3::7344 - 2001:0db8:85a3::7344   Explicit range
-        *     2001:0db8:85a3::7344                          Single IP
-        * @param string $range IP range
-        * @return array(string, string)
-        */
-       public static function parseRange( $range ) {
-               // CIDR notation
-               if ( strpos( $range, '/' ) !== false ) {
-                       if ( self::isIPv6( $range ) ) {
-                               return self::parseRange6( $range );
-                       }
-                       list( $network, $bits ) = self::parseCIDR( $range );
-                       if ( $network === false ) {
-                               $start = $end = false;
-                       } else {
-                               $start = sprintf( '%08X', $network );
-                               $end = sprintf( '%08X', $network + pow( 2, ( 32 - $bits ) ) - 1 );
-                       }
-               // Explicit range
-               } elseif ( strpos( $range, '-' ) !== false ) {
-                       list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
-                       if ( self::isIPv6( $start ) && self::isIPv6( $end ) ) {
-                               return self::parseRange6( $range );
-                       }
-                       if ( self::isIPv4( $start ) && self::isIPv4( $end ) ) {
-                               $start = self::toHex( $start );
-                               $end = self::toHex( $end );
-                               if ( $start > $end ) {
-                                       $start = $end = false;
-                               }
-                       } else {
-                               $start = $end = false;
-                       }
-               } else {
-                       # Single IP
-                       $start = $end = self::toHex( $range );
-               }
-               if ( $start === false || $end === false ) {
-                       return [ false, false ];
-               } else {
-                       return [ $start, $end ];
-               }
-       }
-
-       /**
-        * Convert a network specification in IPv6 CIDR notation to an
-        * integer network and a number of bits
-        *
-        * @param string $range
-        *
-        * @return array(string, int)
-        */
-       private static function parseCIDR6( $range ) {
-               # Explode into <expanded IP,range>
-               $parts = explode( '/', IP::sanitizeIP( $range ), 2 );
-               if ( count( $parts ) != 2 ) {
-                       return [ false, false ];
-               }
-               list( $network, $bits ) = $parts;
-               $network = self::IPv6ToRawHex( $network );
-               if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 128 ) {
-                       if ( $bits == 0 ) {
-                               $network = "0";
-                       } else {
-                               # Native 32 bit functions WONT work here!!!
-                               # Convert to a padded binary number
-                               $network = Wikimedia\base_convert( $network, 16, 2, 128 );
-                               # Truncate the last (128-$bits) bits and replace them with zeros
-                               $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT );
-                               # Convert back to an integer
-                               $network = Wikimedia\base_convert( $network, 2, 10 );
-                       }
-               } else {
-                       $network = false;
-                       $bits = false;
-               }
-
-               return [ $network, (int)$bits ];
-       }
-
-       /**
-        * Given a string range in a number of formats, return the
-        * start and end of the range in hexadecimal. For IPv6.
-        *
-        * Formats are:
-        *     2001:0db8:85a3::7344/96                       CIDR
-        *     2001:0db8:85a3::7344 - 2001:0db8:85a3::7344   Explicit range
-        *     2001:0db8:85a3::7344/96                       Single IP
-        *
-        * @param string $range
-        *
-        * @return array(string, string)
-        */
-       private static function parseRange6( $range ) {
-               # Expand any IPv6 IP
-               $range = IP::sanitizeIP( $range );
-               // CIDR notation...
-               if ( strpos( $range, '/' ) !== false ) {
-                       list( $network, $bits ) = self::parseCIDR6( $range );
-                       if ( $network === false ) {
-                               $start = $end = false;
-                       } else {
-                               $start = Wikimedia\base_convert( $network, 10, 16, 32, false );
-                               # Turn network to binary (again)
-                               $end = Wikimedia\base_convert( $network, 10, 2, 128 );
-                               # Truncate the last (128-$bits) bits and replace them with ones
-                               $end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT );
-                               # Convert to hex
-                               $end = Wikimedia\base_convert( $end, 2, 16, 32, false );
-                               # see toHex() comment
-                               $start = "v6-$start";
-                               $end = "v6-$end";
-                       }
-               // Explicit range notation...
-               } elseif ( strpos( $range, '-' ) !== false ) {
-                       list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
-                       $start = self::toHex( $start );
-                       $end = self::toHex( $end );
-                       if ( $start > $end ) {
-                               $start = $end = false;
-                       }
-               } else {
-                       # Single IP
-                       $start = $end = self::toHex( $range );
-               }
-               if ( $start === false || $end === false ) {
-                       return [ false, false ];
-               } else {
-                       return [ $start, $end ];
-               }
-       }
-
-       /**
-        * Determine if a given IPv4/IPv6 address is in a given CIDR network
-        *
-        * @param string $addr The address to check against the given range.
-        * @param string $range The range to check the given address against.
-        * @return bool Whether or not the given address is in the given range.
-        *
-        * @note This can return unexpected results for invalid arguments!
-        *       Make sure you pass a valid IP address and IP range.
-        */
-       public static function isInRange( $addr, $range ) {
-               $hexIP = self::toHex( $addr );
-               list( $start, $end ) = self::parseRange( $range );
-
-               return ( strcmp( $hexIP, $start ) >= 0 &&
-                       strcmp( $hexIP, $end ) <= 0 );
-       }
-
-       /**
-        * Determines if an IP address is a list of CIDR a.b.c.d/n ranges.
-        *
-        * @since 1.25
-        *
-        * @param string $ip the IP to check
-        * @param array $ranges the IP ranges, each element a range
-        *
-        * @return bool true if the specified adress belongs to the specified range; otherwise, false.
-        */
-       public static function isInRanges( $ip, $ranges ) {
-               foreach ( $ranges as $range ) {
-                       if ( self::isInRange( $ip, $range ) ) {
-                               return true;
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Convert some unusual representations of IPv4 addresses to their
-        * canonical dotted quad representation.
-        *
-        * This currently only checks a few IPV4-to-IPv6 related cases.  More
-        * unusual representations may be added later.
-        *
-        * @param string $addr Something that might be an IP address
-        * @return string|null Valid dotted quad IPv4 address or null
-        */
-       public static function canonicalize( $addr ) {
-               // remove zone info (bug 35738)
-               $addr = preg_replace( '/\%.*/', '', $addr );
-
-               if ( self::isValid( $addr ) ) {
-                       return $addr;
-               }
-               // Turn mapped addresses from ::ce:ffff:1.2.3.4 to 1.2.3.4
-               if ( strpos( $addr, ':' ) !== false && strpos( $addr, '.' ) !== false ) {
-                       $addr = substr( $addr, strrpos( $addr, ':' ) + 1 );
-                       if ( self::isIPv4( $addr ) ) {
-                               return $addr;
-                       }
-               }
-               // IPv6 loopback address
-               $m = [];
-               if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) {
-                       return '127.0.0.1';
-               }
-               // IPv4-mapped and IPv4-compatible IPv6 addresses
-               if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) {
-                       return $m[1];
-               }
-               if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD .
-                       ':' . RE_IPV6_WORD . '$/i', $addr, $m )
-               ) {
-                       return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
-               }
-
-               return null; // give up
-       }
-
-       /**
-        * Gets rid of unneeded numbers in quad-dotted/octet IP strings
-        * For example, 127.111.113.151/24 -> 127.111.113.0/24
-        * @param string $range IP address to normalize
-        * @return string
-        */
-       public static function sanitizeRange( $range ) {
-               list( /*...*/, $bits ) = self::parseCIDR( $range );
-               list( $start, /*...*/ ) = self::parseRange( $range );
-               $start = self::formatHex( $start );
-               if ( $bits === false ) {
-                       return $start; // wasn't actually a range
-               }
-
-               return "$start/$bits";
-       }
-
-       /**
-        * Checks if an IP is a trusted proxy provider.
-        * Useful to tell if X-Forwarded-For data is possibly bogus.
-        * CDN cache servers for the site are whitelisted.
-        * @since 1.24
-        *
-        * @param string $ip
-        * @return bool
-        */
-       public static function isTrustedProxy( $ip ) {
-               $trusted = self::isConfiguredProxy( $ip );
-               Hooks::run( 'IsTrustedProxy', [ &$ip, &$trusted ] );
-               return $trusted;
-       }
-
-       /**
-        * Checks if an IP matches a proxy we've configured
-        * @since 1.24
-        *
-        * @param string $ip
-        * @return bool
-        */
-       public static function isConfiguredProxy( $ip ) {
-               global $wgSquidServers, $wgSquidServersNoPurge;
-
-               // Quick check of known singular proxy servers
-               $trusted = in_array( $ip, $wgSquidServers );
-
-               // Check against addresses and CIDR nets in the NoPurge list
-               if ( !$trusted ) {
-                       if ( !self::$proxyIpSet ) {
-                               self::$proxyIpSet = new IPSet( $wgSquidServersNoPurge );
-                       }
-                       $trusted = self::$proxyIpSet->match( $ip );
-               }
-
-               return $trusted;
-       }
-
-       /**
-        * Clears precomputed data used for proxy support.
-        * Use this only for unit tests.
-        */
-       public static function clearCaches() {
-               self::$proxyIpSet = null;
-       }
-
-       /**
-        * Returns the subnet of a given IP
-        *
-        * @param string $ip
-        * @return string|false
-        */
-       public static function getSubnet( $ip ) {
-               $matches = [];
-               $subnet = false;
-               if ( IP::isIPv6( $ip ) ) {
-                       $parts = IP::parseRange( "$ip/64" );
-                       $subnet = $parts[0];
-               } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
-                       // IPv4
-                       $subnet = $matches[1];
-               }
-               return $subnet;
-       }
-}
index 1376fa7..2756861 100644 (file)
@@ -29,6 +29,7 @@
  * @author Chris Steipp
  * @file
  */
+use MediaWiki\MediaWikiServices;
 
 class MWCryptHKDF {
 
@@ -160,7 +161,7 @@ class MWCryptHKDF {
         * @throws MWException
         */
        protected static function singleton() {
-               global $wgHKDFAlgorithm, $wgHKDFSecret, $wgSecretKey, $wgMainCacheType;
+               global $wgHKDFAlgorithm, $wgHKDFSecret, $wgSecretKey;
 
                $secret = $wgHKDFSecret ?: $wgSecretKey;
                if ( !$secret ) {
@@ -174,8 +175,12 @@ class MWCryptHKDF {
                $context[] = getmypid();
                $context[] = gethostname();
 
-               // Setup salt cache. Use APC, or fallback to the main cache if it isn't setup
-               $cache = ObjectCache::getLocalServerInstance( $wgMainCacheType );
+               // Setup salt cache
+               $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
+               if ( $cache instanceof EmptyBagOStuff ) {
+                       // Use APC, or fallback to the main cache if it isn't setup
+                       $cache = ObjectCache::getLocalClusterInstance();
+               }
 
                if ( is_null( self::$singleton ) ) {
                        self::$singleton = new self( $secret, $wgHKDFAlgorithm, $cache, $context );
diff --git a/includes/utils/MWCryptHash.php b/includes/utils/MWCryptHash.php
deleted file mode 100644 (file)
index 1117357..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-<?php
-/**
- * Utility functions for generating hashes
- *
- * This is based in part on Drupal code as well as what we used in our own code
- * prior to introduction of this class, by way of MWCryptRand.
- *
- * 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
- */
-
-class MWCryptHash {
-       /**
-        * The hash algorithm being used
-        */
-       protected static $algo = null;
-
-       /**
-        * The number of bytes outputted by the hash algorithm
-        */
-       protected static $hashLength = [
-               true => null,
-               false => null,
-       ];
-
-       /**
-        * Decide on the best acceptable hash algorithm we have available for hash()
-        * @return string A hash algorithm
-        */
-       public static function hashAlgo() {
-               if ( !is_null( self::$algo ) ) {
-                       return self::$algo;
-               }
-
-               $algos = hash_algos();
-               $preference = [ 'whirlpool', 'sha256', 'sha1', 'md5' ];
-
-               foreach ( $preference as $algorithm ) {
-                       if ( in_array( $algorithm, $algos ) ) {
-                               self::$algo = $algorithm;
-                               wfDebug( __METHOD__ . ': Using the ' . self::$algo . " hash algorithm.\n" );
-
-                               return self::$algo;
-                       }
-               }
-
-               // We only reach here if no acceptable hash is found in the list, this should
-               // be a technical impossibility since most of php's hash list is fixed and
-               // some of the ones we list are available as their own native functions
-               // But since we already require at least 5.2 and hash() was default in
-               // 5.1.2 we don't bother falling back to methods like sha1 and md5.
-               throw new DomainException( "Could not find an acceptable hashing function in hash_algos()" );
-       }
-
-       /**
-        * Return the byte-length output of the hash algorithm we are
-        * using in self::hash and self::hmac.
-        *
-        * @param bool $raw True to return the length for binary data, false to
-        *   return for hex-encoded
-        * @return int Number of bytes the hash outputs
-        */
-       public static function hashLength( $raw = true ) {
-               $raw = (bool)$raw;
-               if ( is_null( self::$hashLength[$raw] ) ) {
-                       self::$hashLength[$raw] = strlen( self::hash( '', $raw ) );
-               }
-
-               return self::$hashLength[$raw];
-       }
-
-       /**
-        * Generate an acceptably unstable one-way-hash of some text
-        * making use of the best hash algorithm that we have available.
-        *
-        * @param string $data
-        * @param bool $raw True to return binary data, false to return it hex-encoded
-        * @return string A hash of the data
-        */
-       public static function hash( $data, $raw = true ) {
-               return hash( self::hashAlgo(), $data, $raw );
-       }
-
-       /**
-        * Generate an acceptably unstable one-way-hmac of some text
-        * making use of the best hash algorithm that we have available.
-        *
-        * @param string $data
-        * @param string $key
-        * @param bool $raw True to return binary data, false to return it hex-encoded
-        * @return string An hmac hash of the data + key
-        */
-       public static function hmac( $data, $key, $raw = true ) {
-               if ( !is_string( $key ) ) {
-                       // a fatal error in HHVM; an exception will at least give us a stack trace
-                       throw new InvalidArgumentException( 'Invalid key type: ' . gettype( $key ) );
-               }
-               return hash_hmac( self::hashAlgo(), $data, $key, $raw );
-       }
-
-}
index dd3ea1b..5818958 100644 (file)
  * @file
  */
 
-class MWCryptRand {
-       /**
-        * Minimum number of iterations we want to make in our drift calculations.
-        */
-       const MIN_ITERATIONS = 1000;
-
-       /**
-        * Number of milliseconds we want to spend generating each separate byte
-        * of the final generated bytes.
-        * This is used in combination with the hash length to determine the duration
-        * we should spend doing drift calculations.
-        */
-       const MSEC_PER_BYTE = 0.5;
-
-       /**
-        * Singleton instance for public use
-        */
-       protected static $singleton = null;
-
-       /**
-        * A boolean indicating whether the previous random generation was done using
-        * cryptographically strong random number generator or not.
-        */
-       protected $strong = null;
-
-       /**
-        * Initialize an initial random state based off of whatever we can find
-        * @return string
-        */
-       protected function initialRandomState() {
-               // $_SERVER contains a variety of unstable user and system specific information
-               // It'll vary a little with each page, and vary even more with separate users
-               // It'll also vary slightly across different machines
-               $state = serialize( $_SERVER );
-
-               // To try vary the system information of the state a bit more
-               // by including the system's hostname into the state
-               $state .= wfHostname();
-
-               // Try to gather a little entropy from the different php rand sources
-               $state .= rand() . uniqid( mt_rand(), true );
-
-               // Include some information about the filesystem's current state in the random state
-               $files = [];
-
-               // We know this file is here so grab some info about ourselves
-               $files[] = __FILE__;
-
-               // We must also have a parent folder, and with the usual file structure, a grandparent
-               $files[] = __DIR__;
-               $files[] = dirname( __DIR__ );
-
-               // The config file is likely the most often edited file we know should
-               // be around so include its stat info into the state.
-               // The constant with its location will almost always be defined, as
-               // WebStart.php defines MW_CONFIG_FILE to $IP/LocalSettings.php unless
-               // being configured with MW_CONFIG_CALLBACK (e.g. the installer).
-               if ( defined( 'MW_CONFIG_FILE' ) ) {
-                       $files[] = MW_CONFIG_FILE;
-               }
-
-               foreach ( $files as $file ) {
-                       MediaWiki\suppressWarnings();
-                       $stat = stat( $file );
-                       MediaWiki\restoreWarnings();
-                       if ( $stat ) {
-                               // stat() duplicates data into numeric and string keys so kill off all the numeric ones
-                               foreach ( $stat as $k => $v ) {
-                                       if ( is_numeric( $k ) ) {
-                                               unset( $k );
-                                       }
-                               }
-                               // The absolute filename itself will differ from install to install so don't leave it out
-                               $path = realpath( $file );
-                               if ( $path !== false ) {
-                                       $state .= $path;
-                               } else {
-                                       $state .= $file;
-                               }
-                               $state .= implode( '', $stat );
-                       } else {
-                               // The fact that the file isn't there is worth at least a
-                               // minuscule amount of entropy.
-                               $state .= '0';
-                       }
-               }
-
-               // Try and make this a little more unstable by including the varying process
-               // id of the php process we are running inside of if we are able to access it
-               if ( function_exists( 'getmypid' ) ) {
-                       $state .= getmypid();
-               }
-
-               // If available try to increase the instability of the data by throwing in
-               // the precise amount of memory that we happen to be using at the moment.
-               if ( function_exists( 'memory_get_usage' ) ) {
-                       $state .= memory_get_usage( true );
-               }
-
-               // It's mostly worthless but throw the wiki's id into the data for a little more variance
-               $state .= wfWikiID();
-
-               // If we have a secret key set then throw it into the state as well
-               global $wgSecretKey;
-               if ( $wgSecretKey ) {
-                       $state .= $wgSecretKey;
-               }
-
-               return $state;
-       }
-
-       /**
-        * Randomly hash data while mixing in clock drift data for randomness
-        *
-        * @param string $data The data to randomly hash.
-        * @return string The hashed bytes
-        * @author Tim Starling
-        */
-       protected function driftHash( $data ) {
-               // Minimum number of iterations (to avoid slow operations causing the
-               // loop to gather little entropy)
-               $minIterations = self::MIN_ITERATIONS;
-               // Duration of time to spend doing calculations (in seconds)
-               $duration = ( self::MSEC_PER_BYTE / 1000 ) * MWCryptHash::hashLength();
-               // Create a buffer to use to trigger memory operations
-               $bufLength = 10000000;
-               $buffer = str_repeat( ' ', $bufLength );
-               $bufPos = 0;
-
-               // Iterate for $duration seconds or at least $minIterations number of iterations
-               $iterations = 0;
-               $startTime = microtime( true );
-               $currentTime = $startTime;
-               while ( $iterations < $minIterations || $currentTime - $startTime < $duration ) {
-                       // Trigger some memory writing to trigger some bus activity
-                       // This may create variance in the time between iterations
-                       $bufPos = ( $bufPos + 13 ) % $bufLength;
-                       $buffer[$bufPos] = ' ';
-                       // Add the drift between this iteration and the last in as entropy
-                       $nextTime = microtime( true );
-                       $delta = (int)( ( $nextTime - $currentTime ) * 1000000 );
-                       $data .= $delta;
-                       // Every 100 iterations hash the data and entropy
-                       if ( $iterations % 100 === 0 ) {
-                               $data = sha1( $data );
-                       }
-                       $currentTime = $nextTime;
-                       $iterations++;
-               }
-               $timeTaken = $currentTime - $startTime;
-               $data = MWCryptHash::hash( $data );
-
-               wfDebug( __METHOD__ . ": Clock drift calculation " .
-                       "(time-taken=" . ( $timeTaken * 1000 ) . "ms, " .
-                       "iterations=$iterations, " .
-                       "time-per-iteration=" . ( $timeTaken / $iterations * 1e6 ) . "us)\n" );
-
-               return $data;
-       }
-
-       /**
-        * Return a rolling random state initially build using data from unstable sources
-        * @return string A new weak random state
-        */
-       protected function randomState() {
-               static $state = null;
-               if ( is_null( $state ) ) {
-                       // Initialize the state with whatever unstable data we can find
-                       // It's important that this data is hashed right afterwards to prevent
-                       // it from being leaked into the output stream
-                       $state = MWCryptHash::hash( $this->initialRandomState() );
-               }
-               // Generate a new random state based on the initial random state or previous
-               // random state by combining it with clock drift
-               $state = $this->driftHash( $state );
-
-               return $state;
-       }
-
-       /**
-        * @see self::wasStrong()
-        */
-       public function realWasStrong() {
-               if ( is_null( $this->strong ) ) {
-                       throw new MWException( __METHOD__ . ' called before generation of random data' );
-               }
-
-               return $this->strong;
-       }
-
-       /**
-        * @see self::generate()
-        */
-       public function realGenerate( $bytes, $forceStrong = false ) {
-
-               wfDebug( __METHOD__ . ": Generating cryptographic random bytes for " .
-                       wfGetAllCallers( 5 ) . "\n" );
-
-               $bytes = floor( $bytes );
-               static $buffer = '';
-               if ( is_null( $this->strong ) ) {
-                       // Set strength to false initially until we know what source data is coming from
-                       $this->strong = true;
-               }
-
-               if ( strlen( $buffer ) < $bytes ) {
-                       // If available make use of mcrypt_create_iv URANDOM source to generate randomness
-                       // On unix-like systems this reads from /dev/urandom but does it without any buffering
-                       // and bypasses openbasedir restrictions, so it's preferable to reading directly
-                       // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate
-                       // entropy so this is also preferable to just trying to read urandom because it may work
-                       // on Windows systems as well.
-                       if ( function_exists( 'mcrypt_create_iv' ) ) {
-                               $rem = $bytes - strlen( $buffer );
-                               $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM );
-                               if ( $iv === false ) {
-                                       wfDebug( __METHOD__ . ": mcrypt_create_iv returned false.\n" );
-                               } else {
-                                       $buffer .= $iv;
-                                       wfDebug( __METHOD__ . ": mcrypt_create_iv generated " . strlen( $iv ) .
-                                               " bytes of randomness.\n" );
-                               }
-                       }
-               }
-
-               if ( strlen( $buffer ) < $bytes ) {
-                       if ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
-                               $rem = $bytes - strlen( $buffer );
-                               $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong );
-                               if ( $openssl_bytes === false ) {
-                                       wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes returned false.\n" );
-                               } else {
-                                       $buffer .= $openssl_bytes;
-                                       wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes generated " .
-                                               strlen( $openssl_bytes ) . " bytes of " .
-                                               ( $openssl_strong ? "strong" : "weak" ) . " randomness.\n" );
-                               }
-                               if ( strlen( $buffer ) >= $bytes ) {
-                                       // openssl tells us if the random source was strong, if some of our data was generated
-                                       // using it use it's say on whether the randomness is strong
-                                       $this->strong = !!$openssl_strong;
-                               }
-                       }
-               }
-
-               // Only read from urandom if we can control the buffer size or were passed forceStrong
-               if ( strlen( $buffer ) < $bytes &&
-                       ( function_exists( 'stream_set_read_buffer' ) || $forceStrong )
-               ) {
-                       $rem = $bytes - strlen( $buffer );
-                       if ( !function_exists( 'stream_set_read_buffer' ) && $forceStrong ) {
-                               wfDebug( __METHOD__ . ": Was forced to read from /dev/urandom " .
-                                       "without control over the buffer size.\n" );
-                       }
-                       // /dev/urandom is generally considered the best possible commonly
-                       // available random source, and is available on most *nix systems.
-                       MediaWiki\suppressWarnings();
-                       $urandom = fopen( "/dev/urandom", "rb" );
-                       MediaWiki\restoreWarnings();
-
-                       // Attempt to read all our random data from urandom
-                       // php's fread always does buffered reads based on the stream's chunk_size
-                       // so in reality it will usually read more than the amount of data we're
-                       // asked for and not storing that risks depleting the system's random pool.
-                       // If stream_set_read_buffer is available set the chunk_size to the amount
-                       // of data we need. Otherwise read 8k, php's default chunk_size.
-                       if ( $urandom ) {
-                               // php's default chunk_size is 8k
-                               $chunk_size = 1024 * 8;
-                               if ( function_exists( 'stream_set_read_buffer' ) ) {
-                                       // If possible set the chunk_size to the amount of data we need
-                                       stream_set_read_buffer( $urandom, $rem );
-                                       $chunk_size = $rem;
-                               }
-                               $random_bytes = fread( $urandom, max( $chunk_size, $rem ) );
-                               $buffer .= $random_bytes;
-                               fclose( $urandom );
-                               wfDebug( __METHOD__ . ": /dev/urandom generated " . strlen( $random_bytes ) .
-                                       " bytes of randomness.\n" );
-
-                               if ( strlen( $buffer ) >= $bytes ) {
-                                       // urandom is always strong, set to true if all our data was generated using it
-                                       $this->strong = true;
-                               }
-                       } else {
-                               wfDebug( __METHOD__ . ": /dev/urandom could not be opened.\n" );
-                       }
-               }
-
-               // If we cannot use or generate enough data from a secure source
-               // use this loop to generate a good set of pseudo random data.
-               // This works by initializing a random state using a pile of unstable data
-               // and continually shoving it through a hash along with a variable salt.
-               // We hash the random state with more salt to avoid the state from leaking
-               // out and being used to predict the /randomness/ that follows.
-               if ( strlen( $buffer ) < $bytes ) {
-                       wfDebug( __METHOD__ .
-                               ": Falling back to using a pseudo random state to generate randomness.\n" );
-               }
-               while ( strlen( $buffer ) < $bytes ) {
-                       $buffer .= MWCryptHash::hmac( $this->randomState(), strval( mt_rand() ) );
-                       // This code is never really cryptographically strong, if we use it
-                       // at all, then set strong to false.
-                       $this->strong = false;
-               }
-
-               // Once the buffer has been filled up with enough random data to fulfill
-               // the request shift off enough data to handle the request and leave the
-               // unused portion left inside the buffer for the next request for random data
-               $generated = substr( $buffer, 0, $bytes );
-               $buffer = substr( $buffer, $bytes );
-
-               wfDebug( __METHOD__ . ": " . strlen( $buffer ) .
-                       " bytes of randomness leftover in the buffer.\n" );
-
-               return $generated;
-       }
-
-       /**
-        * @see self::generateHex()
-        */
-       public function realGenerateHex( $chars, $forceStrong = false ) {
-               // hex strings are 2x the length of raw binary so we divide the length in half
-               // odd numbers will result in a .5 that leads the generate() being 1 character
-               // short, so we use ceil() to ensure that we always have enough bytes
-               $bytes = ceil( $chars / 2 );
-               // Generate the data and then convert it to a hex string
-               $hex = bin2hex( $this->generate( $bytes, $forceStrong ) );
-
-               // A bit of paranoia here, the caller asked for a specific length of string
-               // here, and it's possible (eg when given an odd number) that we may actually
-               // have at least 1 char more than they asked for. Just in case they made this
-               // call intending to insert it into a database that does truncation we don't
-               // want to give them too much and end up with their database and their live
-               // code having two different values because part of what we gave them is truncated
-               // hence, we strip out any run of characters longer than what we were asked for.
-               return substr( $hex, 0, $chars );
-       }
-
-       /** Publicly exposed static methods **/
+use MediaWiki\MediaWikiServices;
 
+class MWCryptRand {
        /**
-        * Return a singleton instance of MWCryptRand
-        * @return MWCryptRand
+        * @return CryptRand
         */
        protected static function singleton() {
-               if ( is_null( self::$singleton ) ) {
-                       self::$singleton = new self;
-               }
-
-               return self::$singleton;
+               return MediaWikiServices::getInstance()->getCryptRand();
        }
 
        /**
@@ -385,7 +42,7 @@ class MWCryptRand {
         * @return bool Returns true if the source was strong, false if not.
         */
        public static function wasStrong() {
-               return self::singleton()->realWasStrong();
+               return self::singleton()->wasStrong();
        }
 
        /**
@@ -401,7 +58,7 @@ class MWCryptRand {
         * @return string Raw binary random data
         */
        public static function generate( $bytes, $forceStrong = false ) {
-               return self::singleton()->realGenerate( $bytes, $forceStrong );
+               return self::singleton()->generate( $bytes, $forceStrong );
        }
 
        /**
@@ -417,6 +74,6 @@ class MWCryptRand {
         * @return string Hexadecimal random data
         */
        public static function generateHex( $chars, $forceStrong = false ) {
-               return self::singleton()->realGenerateHex( $chars, $forceStrong );
+               return self::singleton()->generateHex( $chars, $forceStrong );
        }
 }
diff --git a/includes/utils/MWFileProps.php b/includes/utils/MWFileProps.php
new file mode 100644 (file)
index 0000000..e60b9ab
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+/**
+ * MimeMagic helper functions for detecting and dealing with MIME types.
+ *
+ * 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
+ */
+
+/**
+ * MimeMagic helper wrapper
+ *
+ * @since 1.28
+ */
+class MWFileProps {
+       /** @var MimeMagic */
+       private $magic;
+
+       /**
+        * @param MimeMagic $magic
+        */
+       public function __construct( MimeMagic $magic ) {
+               $this->magic = $magic;
+       }
+
+       /**
+        * Get an associative array containing information about
+        * a file with the given storage path.
+        *
+        * Resulting array fields include:
+        *   - fileExists
+        *   - size (filesize in bytes)
+        *   - mime (as major/minor)
+        *   - media_type (value to be used with the MEDIATYPE_xxx constants)
+        *   - metadata (handler specific)
+        *   - sha1 (in base 36)
+        *   - width
+        *   - height
+        *   - bits (bitrate)
+        *   - file-mime
+        *   - major_mime
+        *   - minor_mime
+        *
+        * @param string $path Filesystem path to a file
+        * @param string|bool $ext The file extension, or true to extract it from the filename.
+        *             Set it to false to ignore the extension.
+        * @return array
+        * @since 1.28
+        */
+       public function getPropsFromPath( $path, $ext ) {
+               $fsFile = new FSFile( $path );
+
+               $info = $this->newPlaceholderProps();
+               $info['fileExists'] = $fsFile->exists();
+               if ( $info['fileExists'] ) {
+                       $info['size'] = $fsFile->getSize(); // bytes
+                       $info['sha1'] = $fsFile->getSha1Base36();
+
+                       # MIME type according to file contents
+                       $info['file-mime'] = $this->magic->guessMimeType( $path, false );
+                       # Logical MIME type
+                       $ext = ( $ext === true ) ? FileBackend::extensionFromPath( $path ) : $ext;
+                       $info['mime'] = $this->magic->improveTypeFromExtension( $info['file-mime'], $ext );
+
+                       list( $info['major_mime'], $info['minor_mime'] ) = File::splitMime( $info['mime'] );
+                       $info['media_type'] = $this->magic->getMediaType( $path, $info['mime'] );
+
+                       # Height, width and metadata
+                       $handler = MediaHandler::getHandler( $info['mime'] );
+                       if ( $handler ) {
+                               $info['metadata'] = $handler->getMetadata( $fsFile, $path );
+                               /** @noinspection PhpMethodParametersCountMismatchInspection */
+                               $gis = $handler->getImageSize( $fsFile, $path, $info['metadata'] );
+                               if ( is_array( $gis ) ) {
+                                       $info = $this->extractImageSizeInfo( $gis ) + $info;
+                               }
+                       }
+               }
+
+               return $info;
+       }
+
+       /**
+        * Exract image size information
+        *
+        * @param array $gis
+        * @return array
+        */
+       private 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;
+       }
+
+       /**
+        * Empty place holder props for non-existing files
+        *
+        * Resulting array fields include:
+        *   - fileExists
+        *   - size (filesize in bytes)
+        *   - mime (as major/minor)
+        *   - media_type (value to be used with the MEDIATYPE_xxx constants)
+        *   - metadata (handler specific)
+        *   - sha1 (in base 36)
+        *   - width
+        *   - height
+        *   - bits (bitrate)
+        *   - file-mime
+        *   - major_mime
+        *   - minor_mime
+        *
+        * @return array
+        * @since 1.28
+        */
+       public function newPlaceholderProps() {
+               return FSFile::placeholderProps() + [
+                       'metadata' => '',
+                       'width' => 0,
+                       'height' => 0,
+                       'bits' => 0,
+                       'media_type' => MEDIATYPE_UNKNOWN
+               ];
+       }
+}
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 abba5a1..95b4463 100644 (file)
@@ -21,6 +21,7 @@
  * @author Aaron Schulz
  */
 use Wikimedia\Assert\Assert;
+use MediaWiki\MediaWikiServices;
 
 /**
  * Class for getting statistically unique IDs
@@ -368,7 +369,7 @@ class UIDGenerator {
                // Counter values would not survive accross script instances in CLI mode.
                $cache = null;
                if ( ( $flags & self::QUICK_VOLATILE ) && PHP_SAPI !== 'cli' ) {
-                       $cache = ObjectCache::getLocalServerInstance();
+                       $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
                }
                if ( $cache ) {
                        $counter = $cache->incrWithInit( $bucket, $cache::TTL_INDEFINITE, $count, $count );
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 db71c5c..3e28759 100644 (file)
@@ -2303,7 +2303,7 @@ class Language {
 
        /**
         * Takes a number of seconds and returns an array with a set of corresponding intervals.
-        * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
+        * For example 65 will be turned into [ minutes => 1, seconds => 5 ].
         *
         * @since 1.20
         *
index b31b10f..13ba7e8 100644 (file)
@@ -1086,11 +1086,11 @@ class LanguageConverter {
                        //  -{zh-hans:<span style="font-size:120%;">xxx</span>;zh-hant:\
                        //      <span style="font-size:120%;">yyy</span>;}-
                        // we should split it as:
-                       //  array(
+                       //  [
                        //        [0] => 'zh-hans:<span style="font-size:120%;">xxx</span>'
                        //        [1] => 'zh-hant:<span style="font-size:120%;">yyy</span>'
                        //        [2] => ''
-                       //       )
+                       //  ]
                        $pat = '/;\s*(?=';
                        foreach ( $this->mVariants as $variant ) {
                                // zh-hans:xxx;zh-hant:yyy
index 1e0bb00..87d2127 100644 (file)
@@ -35,7 +35,7 @@ class LanguageKm extends Language {
         */
        function commafy( $_ ) {
                /* NO-op for Khmer. Cannot use
-                * $separatorTransformTable = array( ',' => '' )
+                * $separatorTransformTable = [ ',' => '' ]
                 * That would break when parsing and doing strstr '' => 'foo';
                 */
                return $_;
index 236ae4f..f8b50fc 100644 (file)
@@ -35,7 +35,7 @@ class LanguageMy extends Language {
         */
        function commafy( $_ ) {
                /* NO-op. Cannot use
-                * $separatorTransformTable = array( ',' => '' )
+                * $separatorTransformTable = [ ',' => '' ]
                 * That would break when parsing and doing strstr '' => 'foo';
                 */
                return $_;
index 699185e..5ad5bd3 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:",
        "preview": "Eu dilèë",
        "showpreview": "Peuleumah hasé",
        "showdiff": "Peuleumah neuubah",
-       "anoneditwarning": "Droëneuh   hana teudapeuta tamong. Alamat IP Droëneuh   teucatat lam tarèh (riwayat away) ôn nyoë.",
+       "anoneditwarning": "<strong>Peuneugah:</strong> Droëneuh hana lom neutamong. Alamat IP-neuh jeuët deuh bak ureuëng la'én meunyö neumeuandam. Meunyö Droëneuh <strong>[$1 neutamong]</strong> atawa <strong>[$2 neudapeuta]</strong>, neuandamneuh jeuët teutuléh ateuëh nan Droëneuh ngön na lom meunapha'at nyang la'én.",
        "missingcommenttext": "Neupasoë beunalah di yup.",
        "summary-preview": "Eu dilèë neuringkaih:",
        "blockedtitle": "Ureueng ngui geutheun",
        "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:",
        "tooltip-t-recentchangeslinked": "Neuubah barô lam laman nyang meupawôt nibak laman nyoë",
        "tooltip-feed-rss": "Umpeuën RSS keu laman nyoë",
        "tooltip-feed-atom": "Umpeuën Atom keu miëng nyoë",
-       "tooltip-t-contributions": "Dapeuta beuneuri ureuëng ngui nyoë",
+       "tooltip-t-contributions": "Dapeuta beuneuri {{GENDER:$1|ureuëng ngui nyoë}}",
        "tooltip-t-emailuser": "Peu'ét surat-e keu ureuëng ngui nyoë",
        "tooltip-t-upload": "Peutamong beureukaih",
        "tooltip-t-specialpages": "Dapeuta ban dum miëng kusuih",
        "exif-orientation": "Orientasi",
        "exif-xresolution": "Resolusi linteuëng",
        "exif-yresolution": "Rèsolusi buju",
+       "exif-datetime": "Uroë buleuën ngön watèë neuubah beureukaih",
+       "exif-make": "Pabrék kamèra",
+       "exif-model": "Moden kamèra",
        "exif-software": "Software geungui",
        "exif-exifversion": "Versi Exif",
        "exif-colorspace": "Ruweuëng wareuna",
        "tag-filter": "Saréng [[Special:Tags|tag]]:",
        "tag-filter-submit": "Saréng",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Tag}}]]: $2)",
+       "logentry-delete-delete": "$1 {{GENDER:$2|geusampôh}} miëng $3",
        "logentry-newusers-create": "$1 {{GENDER:$2|geupeugöt}} akun ureuëng ngui",
        "searchsuggest-search": "Mita",
        "duration-seconds": "{{PLURAL:$1|deutik}}",
index ed1e509..092ecc4 100644 (file)
        "htmlform-submit": "Ninviar",
        "htmlform-reset": "Desfer cambios",
        "htmlform-selectorother-other": "Atros",
-       "sqlite-has-fts": "$1, con soporte de busca de texto integro",
-       "sqlite-no-fts": "$1, sin soporte de busca de texto integro",
        "logentry-delete-delete": "$1 borró a pachina $3",
        "logentry-delete-restore": "$1 restauró a pachina $3",
        "logentry-delete-event": "$1 modificó a visibilidat de {{PLURAL:$5|un evento d'o rechistro|$5 eventos d'o rechistro}} en $3: $4",
index 1492094..04b7369 100644 (file)
@@ -67,7 +67,8 @@
                        "Hhaboh162002",
                        "بدارين",
                        "باسم",
-                       "Moud hosny"
+                       "Moud hosny",
+                       "ديفيد"
                ]
        },
        "tog-underline": "سطر تحت الوصلات:",
        "tog-watchlisthideliu": "أخف تعديلات المستخدمين المسجلين في قائمة المراقبة",
        "tog-watchlistreloadautomatically": "أعد تحميل قائمة المراقبة بصفة آلية حينما يتغير مرشح ما (يتطلب جافاسكربت)",
        "tog-watchlisthideanons": "أخف تعديلات المستخدمين المجهولين في قائمة المراقبة",
-       "tog-watchlisthidepatrolled": " أخف التعديلات المراجعة في قائمة المراقبة",
+       "tog-watchlisthidepatrolled": "أخف التعديلات المراجعة في قائمة المراقبة",
        "tog-watchlisthidecategorization": "أخف تصنيف الصفحات",
        "tog-ccmeonemails": "أرسل إلي نسخا من الرسائل الإلكترونية التي أرسلها إلى المستخدمين الآخرين",
        "tog-diffonly": "لا تعرض محتوى الصفحة أسفل الفرق",
        "faq": "الأسئلة المتكررة",
        "faqpage": "Project:أسئلة متكررة",
        "actions": "أفعال",
-       "namespaces": "Ù\81ضاءات Ø§Ù\84تسÙ\85Ù\8aØ©",
+       "namespaces": "Ù\86طاÙ\82ات",
        "variants": "المتغيرات",
        "navigation-heading": "قائمة التصفح",
        "errorpagetitle": "خطأ",
        "history": "تاريخ الصفحة",
        "history_short": "تاريخ",
        "updatedmarker": "عدلت منذ زيارتي الأخيرة",
-       "printableversion": "بتنسق للطباعة",
+       "printableversion": "نسخة للطباعة",
        "permalink": "رابط دائم",
        "print": "اطبع",
        "view": "مطالعة",
        "talk": "نقاش",
        "views": "معاينة",
        "toolbox": "أدوات",
+       "tool-link-userrights": "تغيير مجموعات {{GENDER:$1|المستخدم|المستخدمة}}",
+       "tool-link-emailuser": "أرسل رسالة {{GENDER:$1|لهذا المستخدم|لهذه المستخدمة}}",
        "userpage": "طالع صفحة المستخدم",
        "projectpage": "طالع صفحة المشروع",
        "imagepage": "طالع صفحة الملف",
        "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": "إعادة ضبط كلمة السر لم تتم التعامل معها. ربما لا موفر تم ضبطه؟",
        "searchprofile-advanced-tooltip": "ابحث في النطاقات المخصصة",
        "search-result-size": "$1 ({{PLURAL:$2|لا كلمات|كلمة واحدة|كلمتان|$2 كلمات|$2 كلمة}})",
        "search-result-category-size": "{{PLURAL:$1|لا أعضاء|عضو واحد|عضوان|$1 أعضاء|$1 عضوًا|$1 عضو}} ({{PLURAL:$2|لا تصانيف فرعية|تصنيف فرعي واحد|تصنيفان فرعيان|$2 تصنيفات فرعية|$2 تصنيفًا فرعيًا|$2 تصنيف فرعي}} و{{PLURAL:$3|لا ملفات|ملف واحد|ملفان|$3 ملفات|$3 ملفًا|$3 ملف}})",
-       "search-redirect": "(تحويلة $1)",
+       "search-redirect": "(تحويلة من $1)",
        "search-section": "(قسم $1)",
        "search-category": "(التصنيف $1)",
        "search-file-match": "(يطابق محتوى الملف)",
        "prefs-watchlist": "قائمة المراقبة",
        "prefs-editwatchlist": "تعديل قائمة المراقبة",
        "prefs-editwatchlist-label": "عدل قائمة مراقبتك:",
-       "prefs-editwatchlist-edit": "أعرض Ù\88Ø£حذف عناوين من قائمة مراقبتك",
+       "prefs-editwatchlist-edit": "اعرض Ù\88احذف عناوين من قائمة مراقبتك",
        "prefs-editwatchlist-raw": "عدل قائمة المراقبة الخام",
        "prefs-editwatchlist-clear": "امسح قائمة المراقبة",
        "prefs-watchlist-days": "عدد الأيام للعرض في قائمة المراقبة:",
        "upload-dialog-disabled": "رفع الملفات باستخدام هذا الحوار معطلة على هذه الويكي.",
        "upload-dialog-title": "رفع الملف",
        "upload-dialog-button-cancel": "إلغاء",
+       "upload-dialog-button-back": "رجوع",
        "upload-dialog-button-done": "تم",
        "upload-dialog-button-save": "احفظ",
        "upload-dialog-button-upload": "رفع",
        "apisandbox-results-fixtoken-fail": "فشل جلب توكين \"$1\"",
        "apisandbox-alert-page": "هناك حقول غير صالحة في هذه الصفحة.",
        "apisandbox-alert-field": "قيمة هذا الحقل غير صالحة.",
+       "apisandbox-continue": "استمرار",
+       "apisandbox-continue-clear": "إفراغ",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} س [https://www.mediawiki.org/wiki/API:Query#Continuing_queries يستمر] في الطلب الأخير؛ {{int:apisandbox-continue-clear}} سيفرغ المعاملات المرتبطة بالاستمرار.",
        "booksources": "مصادر كتاب",
        "booksources-search-legend": "البحث عن مصادر الكتب",
        "booksources-isbn": "ردمك:",
        "allmessagesname": "الاسم",
        "allmessagesdefault": "النص الافتراضي",
        "allmessagescurrent": "النص الحالي",
-       "allmessagestext": "Ù\87Ø°Ù\87 Ù\82ائÙ\85Ø© Ø¨Ø±Ø³Ø§Ø¦Ù\84 Ø§Ù\84Ù\86ظاÙ\85 Ø§Ù\84Ù\85تÙ\88Ù\81رة Ù\81Ù\8a Ù\86طاÙ\82 Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8a.\nÙ\8aرجÙ\89 Ø²Ù\8aارة :\n[https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation MediaWiki Localisation] Ù\88 [https://translatewiki.net translatewiki.net]\nإازا Ù\83Ù\86ت ØªØ±ØºØ¨ Ù\81Ù\8a Ø§Ù\84Ù\85ساÙ\87Ù\85Ø© Ø¨ØªØ¹Ø±Ù\8aب Ù\85Ù\8aدÙ\8aا Ù\88Ù\8aÙ\83Ù\8a",
+       "allmessagestext": "Ù\87Ø°Ù\87 Ù\82ائÙ\85Ø© Ø¨Ø±Ø³Ø§Ø¦Ù\84 Ø§Ù\84Ù\86ظاÙ\85 Ø§Ù\84Ù\85تÙ\88Ù\81رة Ù\81Ù\8a Ù\86طاÙ\82 Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8a.\nÙ\85Ù\86 Ù\81ضÙ\84Ù\83 Ø²Ø± [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation ØªØ±Ø¬Ù\85Ø© Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8a] Ù\88 [https://translatewiki.net ØªØ±Ø§Ù\86سÙ\84Ù\8aت Ù\88Ù\8aÙ\83Ù\8a Ø¯Ù\88ت Ù\86ت] Ù\84Ù\88 Ù\83Ù\86ت ØªØ±ØºØ¨ Ù\81Ù\8a Ø§Ù\84Ù\85ساÙ\87Ù\85Ø© Ù\81Ù\8a ØªØ±Ø¬Ù\85Ø© Ù\85Ù\8aدÙ\8aاÙ\88Ù\8aÙ\83Ù\8a Ø§Ù\84أساسÙ\8aØ©.",
        "allmessagesnotsupportedDB": "هذه الصفحة لا يمكن استخدامها لأن '''$wgUseDatabaseMessages''' تم تعطيله.",
        "allmessages-filter-legend": "المرشح",
        "allmessages-filter": "رشح حسب حالة التخصيص:",
        "exif-lens": "العدسة المستخدمة",
        "exif-serialnumber": "الرقم التسلسلي للكاميرا",
        "exif-cameraownername": "مالك الكاميرا",
-       "exif-label": "عÙ\84اÙ\85ة",
+       "exif-label": "اÙ\84تسÙ\85Ù\8aة",
        "exif-datetimemetadata": "آخر تعديل للبيانات التعريفية",
        "exif-nickname": "الاسم غير الرسمي للصورة",
        "exif-rating": "التقييم (من 5)",
        "htmlform-cloner-create": "إضافة المزيد",
        "htmlform-cloner-delete": "إزالة",
        "htmlform-cloner-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": "القيمة التي حددتها ليست تاريخا متعرف عليه. جرب استخدام صيغة YYYY-MM-DD.",
+       "htmlform-time-invalid": "القيمة التي حددتها ليست وقتا متعرف عليه. جرب استخدام صيغة HH:MM:SS.",
+       "htmlform-datetime-invalid": "القيمة التي حددتها ليست وقتا وتاريخا متعرف عليه. جرب استخدام صيغة YYYY-MM-DD HH:MM:SS.",
+       "htmlform-date-toolow": "القيمة التي حددتها هي قبل أول تاريخ مسموح به $1.",
+       "htmlform-date-toohigh": "القيمة التي حددتها هي بعد آخر تاريخ مسموح به $1.",
+       "htmlform-time-toolow": "القيمة التي حددتها هي قبل أول وقت مسموح به $1.",
+       "htmlform-time-toohigh": "القيمة التي حددتها هي بعد آخر وقت مسموح به $1.",
+       "htmlform-datetime-toolow": "القيمة التي حددتها هي قبل أول تاريخ ووقت مسموح بهما $1.",
+       "htmlform-datetime-toohigh": "القيمة التي حددتها هي بعد آخر تاريخ ووقت مسموح بهما $1.",
        "htmlform-title-badnamespace": "[[:$1]] ليس في نطاق \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" ليس عنوان صفحة يمكن إنشاؤه",
        "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": "{{GENDER:$2|استعاد|استعادت}} $1 صفحة $3",
        "logentry-delete-event": "{{GENDER:$2|غيّر|غيّرت}} $1 إمكانية مشاهدة {{PLURAL:$5||حدث|حدثين|$5 أحداث|$5 حدثًا|$5 حدث}} في سجل $3: $4",
        "feedback-external-bug-report-button": "أرسل تقرير علة تقنية",
        "feedback-dialog-title": "أرسل تغذية راجعة",
        "feedback-dialog-intro": "أنت يمكنك استخدام الاستمارة السهلة بالأسفل لإرسال تعليقك. تعليقك ستتم إضافته للصفحة \"$1\"، مع اسم المستخدم الخاص بك.",
-       "feedback-error-title": "خطأ",
        "feedback-error1": "خطأ: لا يمكن التعرف عليها من API",
        "feedback-error2": "خطأ: فشل في تحرير",
        "feedback-error3": "خطأ : لا توجد استجابة من API",
        "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 941b518..6ccf4bf 100644 (file)
@@ -37,6 +37,7 @@
        "tog-watchdefault": "মই সম্পাদনা কৰা সকলো পৃষ্ঠা মোৰ লক্ষ্য-তালিকাত যোগ কৰক",
        "tog-watchmoves": "মই স্থানান্তৰ কৰা সকলো পৃষ্ঠা আৰু ফাইল মোৰ লক্ষ্য-তালিকাত যোগ কৰক",
        "tog-watchdeletion": "মই বিলোপ কৰা সকলো পৃষ্ঠা মোৰ লক্ষ্য-তালিকাত যোগ কৰক",
+       "tog-watchuploads": "মই আপল'ড কৰা নতুন ফাইলসমূহ মোৰ লক্ষ্যপৃষ্ঠাত যোগ কৰক",
        "tog-watchrollback": "মই পূৰ্ববত কৰা পৃষ্ঠা মোৰ লক্ষ্য-তালিকাত যোগ কৰা হওক",
        "tog-minordefault": "সকলো সম্পাদনা অগুৰুত্বপূৰ্ণ বুলি নিজে নিজে চিহ্নিত কৰক",
        "tog-previewontop": "সম্পাদনা বাকছৰ ওপৰত খচৰা দেখুৱাওক",
@@ -46,7 +47,7 @@
        "tog-enotifminoredits": "অগুৰুত্বপূৰ্ণ সম্পাদনা হ'লেও মোলৈ ই-মেইল পঠাব",
        "tog-enotifrevealaddr": "জাননী ই-মেইল বোৰত মোৰ ই-মেইল ঠিকনা দেখুৱাব",
        "tog-shownumberswatching": "লক্ষ্য কৰি থকা সদস্য সমূহৰ সংখ্যা দেখুৱাওক",
-       "tog-oldsig": "বৰ্তমানৰ স্বাক্ষৰ:",
+       "tog-oldsig": "à¦\86পà§\8bনাৰ à¦¬à§°à§\8dতমানৰ à¦¸à§\8dবাà¦\95à§\8dষৰ:",
        "tog-fancysig": "স্বাক্ষৰ ৱিকিটেক্সট হিচাপে ব্যৱহাৰ কৰক (স্বয়ংক্ৰিয় সংযোগ অবিহনে)",
        "tog-uselivepreview": "তাৎক্ষণিক প্ৰাক্‌দৰ্শন ব্যৱহাৰ কৰক",
        "tog-forceeditsummary": "সম্পাদনাৰ সাৰাংশ নিদিলে মোক জনাব",
@@ -54,6 +55,7 @@
        "tog-watchlisthidebots": "মোৰ লক্ষ্য-তালিকাত ব'টে কৰা সম্পাদনা নেদেখুৱাব",
        "tog-watchlisthideminor": "মোৰ লক্ষ্য-তালিকাত অগুৰুত্বপূৰ্ণ সম্পাদনা নেদেখুৱাব",
        "tog-watchlisthideliu": "প্ৰবেশ কৰা সদস্যৰ সম্পাদনাসমূহ আঁতৰাই অনুসৰণ-তালিকা দেখুৱাওক",
+       "tog-watchlistreloadautomatically": "পৰিশোধক সলনি হ'লেই লক্ষ্যপৃষ্ঠা স্বয়ংক্ৰিয়ভাৱে ৰিল'ড কৰক (জাভাস্ক্ৰিপ্টৰ প্ৰয়োজন)",
        "tog-watchlisthideanons": "বেনামী সদস্যৰ সম্পাদনাসমূহ আঁতৰাই অনুসৰণ-তালিকা দেখুৱাওক",
        "tog-watchlisthidepatrolled": "পৰীক্ষিত সম্পাদনাসমূহ লক্ষ্য-তালিকাৰ পৰা লুকুৱাই ৰাখক",
        "tog-watchlisthidecategorization": "পৃষ্ঠাবোৰৰ শ্ৰেণীকৰণ লুকুৱাওক",
@@ -62,7 +64,7 @@
        "tog-showhiddencats": "নিহিত শ্ৰেণীসমূহ দেখুৱাওক",
        "tog-norollbackdiff": "পূৰ্বৱত কৰা পাছত পাৰ্থক্য নেদেখুৱাব",
        "tog-useeditwarning": "সালসলনি সংৰক্ষণ নকৰাকৈ সম্পাদনা পৃষ্ঠা ত্যাগৰ সময়ত মোক সাৱধান কৰক",
-       "tog-prefershttps": "পà§\8dৰৱà§\87শ à¦\95ৰà§\8bà¦\81তà§\87 à¦¸à¦¦à¦¾à¦¯à¦¼ সুৰক্ষিত সংযোগ ব্যৱহাৰ কৰক",
+       "tog-prefershttps": "পà§\8dৰৱà§\87শ à¦\95ৰাৰ à¦¸à¦®à¦¯à¦¼à¦¤ সুৰক্ষিত সংযোগ ব্যৱহাৰ কৰক",
        "underline-always": "সদায়",
        "underline-never": "কেতিয়াও নহয়",
        "underline-default": "ব্ৰাউজাৰ ডিফল্ট",
        "october-date": "অক্টোবৰ $1",
        "november-date": "নৱেম্বৰ $1",
        "december-date": "ডিচেম্বৰ $1",
+       "period-am": "পূৰ্বাহ্ন",
+       "period-pm": "অপৰাহ্ন",
        "pagecategories": "{{PLURAL:$1|শ্ৰেণী|শ্ৰেণীসমূহ}}",
        "category_header": "\"$1\" শ্ৰেণীৰ পৃষ্ঠাসমূহ",
        "subcategories": "উপশ্ৰেণীসমূহ",
        "newwindow": "(নতুন ৱিণ্ড'ত খোল খায়)",
        "cancel": "বাতিল কৰক",
        "moredotdotdot": "অধিক...",
-       "morenotlisted": "এই তালিকা সম্পূৰ্ণ নহয়।",
+       "morenotlisted": "এই তালিকা সম্পূৰ্ণ নহ'ব পাৰে।",
        "mypage": "পৃষ্ঠা",
        "mytalk": "বাৰ্তালাপ",
-       "anontalk": "à¦\8fà¦\87 IP-ত à¦¯à§\8bà¦\97াযà§\8bà¦\97 à¦\95ৰক",
+       "anontalk": "বাৰà§\8dতা à¦¦à¦¿à¦¯à¦¼ক",
        "navigation": "দিকদৰ্শন",
        "and": "&#32;আৰু",
        "qbfind": "বিচৰা হওক",
        "talk": "আলোচনা",
        "views": "দৰ্শন",
        "toolbox": "সঁজুলিসমূহ",
+       "tool-link-userrights": "{{GENDER:$1|সদস্য}} গোটসমূহ সলাওক",
+       "tool-link-emailuser": "এই {{GENDER:$1|সদস্যজনক}} ইমেইল কৰক",
        "userpage": "সদস্য পৃষ্ঠা চাওক",
        "projectpage": "প্ৰকল্প পৃষ্ঠা চাওক",
        "imagepage": "নথি পৃষ্ঠা চাওক",
        "laggedslavemode": "সাৱধানবাণী: ইয়াত সাম্প্ৰতিক সাল-সলনি নাথাকিব পাৰে",
        "readonly": "তথ্যকোষ বন্ধ কৰা আছে",
        "enterlockreason": "বন্ধ কৰাৰ কাৰণ দিয়ক, লগতে কেতিয়ামানে খোলা হব তাকো জনাব।",
-       "readonlytext": "হয়তো নিয়মীয়া পৰিচৰ্যাৰ বাবে তথ্যকোষত নতুন সম্পাদনা আৰু আন সাল-সলনি বন্ধ কৰা হৈছে। কিছু সময় পিছত এয়া সাধাৰণ অৱস্থালৈ আহিব।\n\nযিজন প্ৰশাসকে বন্ধ কৰিছে তেওঁ এই কাৰণ দিছে: $1",
+       "readonlytext": "হয়তà§\8b à¦¨à¦¿à¦¯à¦¼à¦®à§\80য়া à¦ªà§°à¦¿à¦\9aৰà§\8dযাৰ à¦¬à¦¾à¦¬à§\87 à¦¤à¦¥à§\8dযà¦\95à§\8bষত à¦¨à¦¤à§\81ন à¦¸à¦®à§\8dপাদনা à¦\86ৰà§\81 à¦\86ন à¦¸à¦¾à¦²-সলনি à¦¬à¦¨à§\8dধ à¦\95ৰা à¦¹à§\88à¦\9bà§\87। à¦\95িà¦\9bà§\81 à¦¸à¦®à¦¯à¦¼ à¦ªà¦¿à¦\9bত à¦\8fয়া à¦¸à¦¾à¦§à¦¾à§°à¦£ à¦\85ৱসà§\8dথালà§\88 à¦\86হিব।\n\nযিà¦\9cন à¦ªà§\8dৰণালà§\80 à¦ªà§\8dৰশাসà¦\95à§\87 à¦¬à¦¨à§\8dধ à¦\95ৰিà¦\9bà§\87 à¦¤à§\87à¦\93à¦\81 à¦\8fà¦\87 à¦\95াৰণ à¦¦à¦¿à¦\9bà§\87: $1",
        "missing-article": "\"$1\" $2 লেখাটো তথ্যকোষত পোৱা নগ’ল ।\n\nবিলোপ কৰা কোনো পৃষ্ঠাৰ সংযোগৰ বাবে সাধাৰণতে এনে ঘটে ।\n\nযদি এনে হোৱা নাই তেন্তে আপুনি ছফ্টৱেৰত কিবা সমস্যা পাইছে ।\nঅনুগ্ৰহ কৰি এই সম্পৰ্কে ইউ.আৰ.এল. সহ কোনো [[Special:ListUsers/sysop|প্ৰশাসক]]ক জনাওক ।",
        "missingarticle-rev": "(সংস্কৰণ#: $1)",
        "missingarticle-diff": "(তফাৎ: $1, $2)",
        "title-invalid-talk-namespace": "অনুৰোধ কৰা পৃষ্ঠাৰ শিৰোনামে এটা আলোচনা পৃষ্ঠা সূচাইছে যিটো থাকিব নোৱাৰে।",
        "title-invalid-characters": "অনুৰোধ কৰা পৃষ্ঠাৰ শিৰোনামত অবৈধ চিহ্ন আছে: \"$1\"।",
        "title-invalid-magic-tilde": "অনুৰোধ কৰা পৃষ্ঠাৰ শিৰোনামত অবৈধ যাদুকৰী টাইল্ড শৃংখল আছে (<nowiki>~~~</nowiki>)।",
-       "title-invalid-too-long": "অনুৰোধ কৰা পৃষ্ঠাৰ শিৰোনাম অতি দীঘল। UTF-8 এন্‌ক'ডিঙত ই {PLURAL:$1|বাইট}}তকৈ দীঘল হ'ব নালাগে।",
+       "title-invalid-too-long": "অনুৰোধ কৰা পৃষ্ঠাৰ শিৰোনাম অতি দীঘল। UTF-8 এন্‌ক'ডিঙত ই $1 {{PLURAL:$1|বাইটতকৈ}} দীঘল হ'ব নালাগে।",
        "title-invalid-leading-colon": "অনুৰোধ কৰা পৃষ্ঠাৰ শিৰোনামৰ আৰম্ভণিত এটা অবৈধ ক'ল'ন আছে।",
        "perfcached": "তলত দিয়া তথ্যখিনি আগতে জমা কৰি থোৱা (cached) আৰু সাম্প্ৰতিক নহ'ব পাৰে। এই তথ্যখিনিত সৰ্বোচ্চ {{PLURAL:$1|এটা ফলাফল|$1টা ফলাফল}} উপলব্ধ।",
        "perfcachedts": "তলত দিয়া তথ্য খিনি আগতে জমা কৰি থোৱা (cached) আৰু শেষবাৰৰ কাৰণে $1 ত নবীকৰণ কৰা হৈছিল। সৰ্বাধিক {{PLURAL:$4|এটা ফলাফল|$4 টা ফলাফল}} এই কেশ্বত পাব।",
        "viewsource": "উৎস চাওক",
        "viewsource-title": "$1ৰ উৎস চাওক",
        "actionthrottled": "কাৰ্য লেহেম কৰা হৈছে",
-       "actionthrottledtext": "সà§\8dপাম à§°à§\8bধ à¦\95ৰিবলà§\88 à¦\8fà¦\87 à¦\95à§\8dৰিয়াতà§\8b à¦\95ম à¦¸à¦®à¦¯à¦¼à§° à¦­à¦¿à¦¤à§°à¦¤ à¦¬à¦¹à§\81 à¦¬à§\87à¦\9bি à¦¬à¦¾à§° à¦\95ৰাতà§\8b à§°à§\8bধ à¦\95ৰা à¦¹à§\88à¦\9bà§\87, আৰু আপুনি ইতিমধ্যে সেই সীমা অতিক্ৰম কৰিলে।\nঅনুগ্ৰহ কৰি কিছু সময় পাছত চেষ্টা কৰক।",
+       "actionthrottledtext": "দà§\81ৰà§\8dবাà¦\95à§\8dয à§°à§\8bধ à¦\95ৰিবলà§\88 à¦\8fà¦\87 à¦\95à§\8dৰিয়াতà§\8b à¦\95ম à¦¸à¦®à¦¯à¦¼à§° à¦­à¦¿à¦¤à§°à¦¤ à¦¬à¦¹à§\81 à¦¬à§\87à¦\9bি à¦¬à¦¾à§° à¦\95ৰাà¦\9fà§\8b à¦¨à¦¿à¦·à§\87ধ আৰু আপুনি ইতিমধ্যে সেই সীমা অতিক্ৰম কৰিলে।\nঅনুগ্ৰহ কৰি কিছু সময় পাছত চেষ্টা কৰক।",
        "protectedpagetext": "সম্পাদনা ৰোধ কৰিবলৈ এই পৃষ্ঠাটো সুৰক্ষিত কৰা হৈছে।",
        "viewsourcetext": "আপুনি এই পৃষ্ঠাটোৰ উৎস চাব আৰু প্ৰতিলিপি কৰিব পাৰে।",
        "viewyourtext": "আপুনি <strong>আপোনাৰ সম্পাদনাসমূহ</strong>ৰ উৎস চাব আৰু এই পৃষ্ঠালৈ প্ৰতিলিপি কৰিব পাৰে।",
        "virus-scanfailed": "স্কেন অসফল (কোড $1)",
        "virus-unknownscanner": "অজ্ঞাত এন্টিভাইৰাচ:",
        "logouttext": "'''আপুনি প্ৰস্থান কৰিলে।'''\n\nমন কৰিব যে যেতিয়ালৈকে আপোনাৰ ব্ৰাউজাৰৰ অস্থায়ী-স্মৃতি (cache) খালী নকৰে, তেতিয়ালৈকে কিছুমান পৃষ্ঠাত আপুনি প্ৰৱেশ কৰা বুলি দেখুৱাই থাকিব পাৰে।",
+       "cannotlogoutnow-title": "এতিয়া প্ৰস্থান কৰিব নোৱাৰি",
+       "cannotlogoutnow-text": "$1 ব্যৱহাৰ কৰাৰ সময়ত প্ৰস্থান কৰিব নোৱাৰি।",
        "welcomeuser": "আদৰিছোঁ, $1!",
        "welcomecreation-msg": "== আদৰিছোঁ, $1! ==\nআপোনাৰ সদস্যভুক্তি হৈ গ’ল ।\n[[Special:Preferences|{{SITENAME}}ৰ পছন্দসমূহ]]ত আপোনাৰ পছন্দমতে ব্যক্তিগতকৰণ কৰি ল’বলৈ নাপাহৰে যেন ।",
        "yourname": "সদস্যনাম:",
        "yourpasswordagain": "গুপ্তশব্দ আকৌ এবাৰ লিখক",
        "createacct-yourpasswordagain": "গুপ্তশব্দ নিশ্চিত কৰক",
        "createacct-yourpasswordagain-ph": "গুপ্তশব্দ আকৌ লিখক",
-       "remembermypassword": "মোৰ প্ৰৱেশ এই কম্পিউটাৰত মনত ৰাখিব (সৰ্বাধিক $1 {{PLURAL:$1|দিনলৈ|দিনলৈ}})",
        "userlogin-remembermypassword": "মোক লগ্‌-ইন কৰাই ৰাখক",
        "userlogin-signwithsecure": "নিৰাপদ সংযোগ ব্যৱহাৰ কৰক",
+       "cannotlogin-title": "প্ৰৱেশ কৰিব নোৱাৰি",
+       "cannotlogin-text": "প্ৰৱেশ কৰা সম্ভৱ নহয়",
+       "cannotloginnow-title": "এতিয়া প্ৰৱেশ কৰিব নোৱাৰি",
+       "cannotloginnow-text": "$1 ব্যৱহাৰ কৰাৰ সময়ত প্ৰৱেশ কৰিব নোৱাৰি।",
        "yourdomainname": "আপোনাৰ ডমেইন:",
        "password-change-forbidden": "আপুনি এই ৱিকিত গুপ্তশব্দ সলাব নোৱাৰে।",
        "externaldberror": "কোনো প্ৰামাণ্যকৰণ তথ্যকোষৰ ত্ৰুটি ঘটিছে নতুবা আপোনাৰ বৰ্হি-একাউণ্ট নৱীকৰণ কৰাৰ অনুমতি নাই ।",
        "passwordreset-emailtext-user": "{{SITENAME}}ত $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": "এখন গুপ্তশব্দ উদ্ধাৰ ইমেইল সৃষ্টি কৰা হ'ল, কিন্তু {{GENDER:$2|সদস্যজনলৈ}} পঠিয়াব পৰা নগ'ল। সেইখন তলত দেখুওৱা হৈছে: $1",
        "changeemail": "ই-মেইল ঠিকনা সলনি নাইবা বিলোপ কৰক",
        "changeemail-header": "একাউণ্টৰ ই-মেইল ঠিকনা সলনি কৰক",
        "changeemail-no-info": "এই পৃষ্ঠাটোত প্ৰৱেশাধিকাৰ পাবলৈ আপুনি লগ্‌ ইন কৰিব লাগিব।",
        "minoredit": "এইটো এটা অগুৰুত্বপূৰ্ণ সম্পাদনা",
        "watchthis": "এই পৃষ্ঠাটো লক্ষ্য কৰক",
        "savearticle": "পৃষ্ঠা সাঁচক",
+       "savechanges": "সাঁচি থওক",
        "preview": "খচৰা",
        "showpreview": "খচৰা চাওক",
        "showdiff": "সালসলনিবোৰ দেখুৱাওক",
        "newarticle": "(নতুন)",
        "newarticletext": "আপুনি বিচৰা প্ৰবন্ধটো বিচাৰি পোৱা নগ'ল।\n\nইচ্ছা কৰিলে আপুনিয়েই এই প্ৰবন্ধটো লিখা আৰম্ভ কৰিব পাৰে। [$1 ইয়াত] সহায় পাব।\n\nআপুনি যদি ইয়ালৈ ভুলতে আহিছে, তেনেহলে আপোনাৰ ব্ৰাওজাৰৰ '''BACK''' বুটামত টিপা মাৰক।",
        "anontalkpagetext": "----''এইখন আলোচনা পৃষ্ঠা বেনামী সদস্যৰ বাবে, যিয়ে নিজা একাউণ্ট  সৃষ্টি কৰা নাই বা যিয়ে সেই একাউণ্ট ব্যৱহাৰ নকৰে।\nএতেকে আমি তেখেতসকলক আই-পি ঠিকনাৰে চিনাক্ত কৰিবলৈ বাধ্য।\nসেই একেই আই-পি ঠিকনা অনেকেই ব্যৱহাৰ কৰিব পাৰে।\nআপুনি যদি এজন বেনামী সদস্য আৰু যদি আপুনি অনুভৱ কৰে যে আপোনাৰ প্ৰতি অপ্ৰাসঙ্গিক মন্তব্য কৰা হৈছে, তেনেহলে আন বেনামী সদস্যৰ পৰা পৃথক কৰিবলৈ \n[[Special:CreateAccount|একাউন্ট সৃষ্টি কৰক]] বা [[Special:UserLogin|প্ৰৱেশ কৰক]] ।''",
-       "noarticletext": "এই পৃষ্ঠাত বৰ্তমান কোনো পাঠ্য নাই ।\nআপুনি আন পৃষ্ঠাত [[Special:Search/{{PAGENAME}}| এই শিৰোনামা সন্ধান কৰিব পাৰে]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} সম্পৰ্কীয় অভিলেখ সন্ধান কৰিব পাৰে],\nবা [{{fullurl:{{FULLPAGENAME}}|action=edit}} এই পৃষ্ঠা সম্পাদনা কৰিব পাৰে]</span>",
+       "noarticletext": "এই পৃষ্ঠাত বৰ্তমান কোনো পাঠ্য নাই ।\nআপুনি আন পৃষ্ঠাত [[Special:Search/{{PAGENAME}}|এই শিৰোনামা সন্ধান কৰিব পাৰে]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} সম্পৰ্কীয় অভিলেখ সন্ধান কৰিব পাৰে],\nবা [{{fullurl:{{FULLPAGENAME}}|action=edit}} এই পৃষ্ঠা সৃষ্টি কৰিব পাৰে]</span>",
        "noarticletext-nopermission": "এই পৃষ্ঠাত বৰ্তমান কোনো পাঠ্য নাই।\nআপুনি আন পৃষ্ঠাত [[Special:Search/{{PAGENAME}}|এই শিৰোনামা সন্ধান কৰিব পাৰে]],\nবা <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} সম্পৰ্কীয় অভিলেখ সন্ধান কৰিব পাৰে]</span>, কিন্তু এই পৃষ্ঠা সৃষ্টি কৰিবলৈ আপোনাৰ অনুমতি নাই।",
        "missing-revision": "\"{{FULLPAGENAME}}\" নামৰ পৃষ্ঠাৰ #$1 সংশোধনৰ অস্তিত্ব নাই।\n\nসাধাৰণতে বিলোপ কৰা এখন পৃষ্ঠাৰ পুৰণা ইতিহাস লিংক অনুসৰণ কৰিলে এনে হয়।\n[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} বিলোপন ল'গ]ত অধিক তথ্য পাব।",
        "userpage-userdoesnotexist": "\"<nowiki>$1</nowiki>\" নামৰ সদস্য একাউন্ট নিবন্ধিত নহয় ।\nঅনুগ্ৰহ কৰি চাওক আপুনি এই পৃষ্ঠা সৃষ্টি/সম্পাদনা কৰিব বিচাৰিছে নেকি ।",
        "undo-nochange": "সম্পাদনাটো ইতিমধ্যেই বাতিল কৰা হৈছে।",
        "undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|আলোচনা]]) সম্পাদিত $1 সংশোধনটি বাতিল কৰক",
        "undo-summary-username-hidden": "এজন গোপন ব্যৱহাৰকাৰীয়ে কৰা $1 সংশোধন বাতিল কৰক",
-       "cantcreateaccounttitle": "একাউণ্ট সৃষ্টি কৰিব নোৱাৰি",
        "cantcreateaccount-text": "আই পি ঠিকনা ('''$1''')ৰ পৰা একাউণ্ট সৃষ্টিত [[User:$3|$3]]’য়ে বাধা প্ৰদান কৰিছে ।\n\n$3 য়ে আগবঢ়োৱা ইয়াৰ কাৰণ হৈছে ''$2''",
        "cantcreateaccount-range-text": "[[User:$3|$3]]য়ে <strong>$1</strong> পৰিসীমাৰ আই পি ঠিকনাৰ পৰা একাউণ্ট সৃষ্টি বাৰণ কৰিছে যাৰ ভিতৰত আপোনাৰ আই ই ঠিকনাও (<strong>$4</strong>) আছে।\n\n $3য়ে <em>$2</em> বুলি কাৰণ দৰ্শাইছে",
        "viewpagelogs": "এই পৃষ্ঠাৰ অভিলেখ চাওক ।",
        "contributions": "{{GENDER:$1|সদস্যৰ}} বৰঙণিসমূহ",
        "contributions-title": "$1ৰ বৰঙণিসমূহ",
        "mycontris": "বৰঙণিসমূহ",
+       "anoncontribs": "বৰঙণি",
        "contribsub2": "{{GENDER:$3|$1}} ($2)ৰ কাৰণে",
        "nocontribs": "এই গুণসমূহৰ লগত মিল থকা কোনো সালসলনি পোৱা নগ’ল ।",
        "uctop": "(বৰ্তমান)",
        "javascripttest": "জাভাস্ক্ৰিপ্ট পৰীক্ষা।",
        "javascripttest-pagetext-unknownaction": "অজ্ঞাত কাৰ্য \"$1\"।",
        "javascripttest-qunit-intro": "mediawiki.org-ত [$1 পৰীক্ষা নথিকৰণ] চাওক।",
-       "tooltip-pt-userpage": "আপোনাৰ সদস্য পৃষ্ঠা",
+       "tooltip-pt-userpage": "{{GENDER:|আপোনাৰ সদস্য}} পৃষ্ঠা",
        "tooltip-pt-anonuserpage": "যি আই.পি. ঠিকনাৰ পৰা আপুনি সম্পাদনা কৰিছে তাৰ সদস্য পৃষ্ঠা",
-       "tooltip-pt-mytalk": "আপোনাৰ আলোচনা পৃষ্ঠা",
+       "tooltip-pt-mytalk": "{{GENDER:|আপোনাৰ}} আলোচনা পৃষ্ঠা",
        "tooltip-pt-anontalk": "এই আই.পি. ঠিকনাৰ পৰা কৰা সম্পাদনাসমূহৰ আলোচনা",
-       "tooltip-pt-preferences": "আপোনাৰ পছন্দসমূহ",
+       "tooltip-pt-preferences": "{{GENDER:|আপোনাৰ}} পছন্দসমূহ",
        "tooltip-pt-watchlist": "আপুনি সালসলনিৰ গতিবিধি লক্ষ্য কৰি থকা পৃষ্ঠাসমূহৰ সুচী",
-       "tooltip-pt-mycontris": "আপোনাৰ বৰঙণিৰ তালিকা",
+       "tooltip-pt-mycontris": "{{GENDER:|আপোনাৰ}} বৰঙণিসমূহ",
        "tooltip-pt-login": "বাধ্যতামূলক নহ'লেও প্ৰৱেশ কৰাটো বাঞ্চনীয়",
        "tooltip-pt-logout": "প্ৰস্থান",
        "tooltip-pt-createaccount": "আপোনাক এটা একাউণ্ট সৃষ্টি কৰি প্ৰৱেশ কৰিবলৈ অনুৰোধ জনোৱা হৈছে, কিন্তু এয়া বাধ্যতামূলক নহয়",
        "tooltip-t-recentchangeslinked": "সংযুক্ত পৃষ্ঠাসমূহৰ শেহতীয়া সালসলনিসমূহ",
        "tooltip-feed-rss": "এই পৃষ্ঠাৰ বাবে আৰ-এচ-এচ ভুক্তি",
        "tooltip-feed-atom": "এই পৃষ্ঠাৰ বাবে এটম ভুক্তি",
-       "tooltip-t-contributions": "এই সদস্যজনৰ অৰিহনাসমূহৰ সূচী চাওক",
+       "tooltip-t-contributions": "{{GENDER:$1|এই সদস্যজনৰ}} বৰঙণিসমূহৰ তালিকা চাওক",
        "tooltip-t-emailuser": "এই সদস্যজনলৈ ই-মেইল পঠাওক",
        "tooltip-t-info": "এই পৃষ্ঠাৰ বিষয়ে অধিক তথ্য",
        "tooltip-t-upload": "ফাইল আপল'ডৰ বাবে",
        "htmlform-chosen-placeholder": "এটা বিকল্প বাছনি কৰক",
        "htmlform-cloner-create": "আৰু যোগ কৰক",
        "htmlform-cloner-delete": "আঁতৰাওক",
-       "sqlite-has-fts": "$1 সম্পূৰ্ণ-পাঠ অনুসন্ধান সমৰ্থন সহ",
-       "sqlite-no-fts": "$1 সম্পূৰ্ণ-পাঠ সন্ধান সমৰ্থন অবিহনে",
        "logentry-delete-delete": "$3 পৃষ্ঠাটো $1ৰদ্বাৰা {{GENDER:$2|বিলোপ কৰা হ'ল}}",
        "logentry-delete-restore": "$1-এ $3 পৃষ্ঠাটো {{GENDER:$2|পুনৰ্সংৰক্ষণ কৰিলে}}",
        "logentry-delete-event": "$3: $4 -ত {{PLURAL:$5|এটা লগ ঘটনা|$5 লগ ঘটনাসমূহ}} -ৰ $1 পৰিৱৰ্তন কৰা দৃশ্যমানতা",
        "special-characters-group-khmer": "খেমাৰ",
        "special-characters-title-endash": "en দেছ্‌",
        "special-characters-title-emdash": "em দেছ‌",
-       "special-characters-title-minus": "বিয়োগ চিন",
-       "api-error-blacklisted": "অনুগ্ৰহ কৰি অন্য এটা বৰ্ণনামূলক শিৰোনাম নিৰ্বাচন কৰক"
+       "special-characters-title-minus": "বিয়োগ চিন"
 }
index f12c554..6e8cdb6 100644 (file)
@@ -42,7 +42,7 @@
        "tog-enotifminoredits": "Mandame tamién un corréu cuando heba ediciones menores de les páxines y ficheros",
        "tog-enotifrevealaddr": "Amosar la mio direición de corréu nos correos de notificación",
        "tog-shownumberswatching": "Amosar el númberu d'usuarios que tán vixilando la páxina",
-       "tog-oldsig": "Firma esistente:",
+       "tog-oldsig": "La to firma actual:",
        "tog-fancysig": "Tratar la firma como testu wiki (ensin enllaz automáticu)",
        "tog-uselivepreview": "Usar vista previa en tiempu real",
        "tog-forceeditsummary": "Avisame cuando grabe col resume d'edición en blanco",
        "category-file-count-limited": "{{PLURAL:$1El ficheru siguiente ta|Los $1 ficheeros siguientes tán}} na categoría actual.",
        "listingcontinuesabbrev": "cont.",
        "index-category": "Páxines indexaes",
-       "noindex-category": "Páxines non indexaes",
+       "noindex-category": "Páxines sin indexar",
        "broken-file-category": "Páxines con enllaces frañíos a ficheros",
        "about": "Tocante a",
        "article": "Páxina de conteníu",
        "newwindow": "(s'abre nuna ventana nueva)",
        "cancel": "Encaboxar",
        "moredotdotdot": "Más...",
-       "morenotlisted": "Esta llista nun ta completa.",
+       "morenotlisted": "Esta llista puede tar incompleta.",
        "mypage": "Páxina",
        "mytalk": "Alderique",
        "anontalk": "Alderique",
        "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?",
        "searchprofile-advanced-tooltip": "Buscar nos espacios de nomes personalizaos",
        "search-result-size": "$1 ({{PLURAL:$2|1 pallabra|$2 pallabres}})",
        "search-result-category-size": "{{PLURAL:$1|1 miembru|$1 miembros}} ({{PLURAL:$2|1 subcategoría|$2 subcategories}}, {{PLURAL:$3|1 ficheru|$3 ficheros}})",
-       "search-redirect": "(redireición de $1)",
+       "search-redirect": "(redireición dende $1)",
        "search-section": "(seición $1)",
        "search-category": "(categoría $1)",
        "search-file-match": "(casa col conteníu del ficheru)",
        "upload-dialog-disabled": "Nesta wiki tán desactivaes les xubíes de ficheros por aciu d'esti diálogu.",
        "upload-dialog-title": "Xubir ficheru",
        "upload-dialog-button-cancel": "Encaboxar",
+       "upload-dialog-button-back": "Anterior",
        "upload-dialog-button-done": "Fecho",
        "upload-dialog-button-save": "Guardar",
        "upload-dialog-button-upload": "Xubir",
        "apisandbox-results-fixtoken-fail": "Nun pudo recuperase'l token «$1».",
        "apisandbox-alert-page": "Los campos d'esta páxina nun son válidos.",
        "apisandbox-alert-field": "El valor d'esti campu nun ye válidu.",
+       "apisandbox-continue": "Siguir",
+       "apisandbox-continue-clear": "Llimpiar",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} [https://www.mediawiki.org/wiki/API:Query#Continuing_queries siguirá] cola última solicitú; {{int:apisandbox-continue-clear}} llimpiará los parámetros rellacionaos con siguir.",
        "booksources": "Fontes de llibros",
        "booksources-search-legend": "Busca de fontes de llibros",
        "booksources-search": "Buscar",
        "htmlform-cloner-create": "Amestar más",
        "htmlform-cloner-delete": "Desaniciar",
        "htmlform-cloner-required": "Necesítase polo menos un valor.",
+       "htmlform-date-placeholder": "AAAA-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "AAAA-MM-DD HH:MM:SS",
+       "htmlform-date-invalid": "El valor que disti nun ye una data reconocible. Prueba usando'l formatu AAAA-MM-DD.",
+       "htmlform-time-invalid": "El valor que disti nun ye una hora reconocible. Prueba usando'l formatu HH:MM:SS.",
+       "htmlform-datetime-invalid": "Nun se reconoció la fecha y hora nel formatu proporcionáu. Prueba a usar el formatu AAAA-MM-DD HH:MM:SS.",
+       "htmlform-date-toolow": "El valor que disti ye anterior a la fecha más antigua permitida, $1.",
+       "htmlform-date-toohigh": "El valor especificáu ye posterior a la mayor data permitida, $1.",
+       "htmlform-time-toolow": "El valor qu'especificasti ye anterior a la hora más antigua permitida, $1.",
+       "htmlform-time-toohigh": "El valor qu'especificasti ye posterior a la hora más nueva permitida, $1.",
+       "htmlform-datetime-toolow": "El valor qu'especificasti ye anterior a la fecha y hora más antigua permitida, $1.",
+       "htmlform-datetime-toohigh": "El valor qu'especificasti ye posterior a la fecha y hora más nueva permitida, $1.",
        "htmlform-title-badnamespace": "[[:$1]] nun ta nel espaciu de nomes \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "«$1» nun ye un títulu de páxina que pueda crease",
        "htmlform-title-not-exists": "$1 nun esiste.",
        "htmlform-user-not-exists": "<strong>$1</strong> nun esiste.",
        "htmlform-user-not-valid": "<strong>$1</strong> nun ye un nome d'usuariu válidu.",
-       "sqlite-has-fts": "$1 con sofitu pa busca de testu completu",
-       "sqlite-no-fts": "$1 ensin sofitu pa busca de testu completu",
        "logentry-delete-delete": "$1 {{GENDER:$2|desanició}} la páxina $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|restauró}} la páxina $3",
        "logentry-delete-event": "$1 {{GENDER:$2|camudó}} la visibilidá {{PLURAL:$5|d'un socesu del rexistru|de $5 socesos del rexistru}} en $3: $4",
        "feedback-external-bug-report-button": "Rexistrar una xera técnica",
        "feedback-dialog-title": "Unviar opinión",
        "feedback-dialog-intro": "Puedes usar el formulariu fácil de más abaxo pa unviar comentarios. Estos amestaránse a la páxina «$1», xunto col to nome d'usuariu.",
-       "feedback-error-title": "Error",
        "feedback-error1": "Fallu: Resultáu de la API non reconocíu",
        "feedback-error2": "Fallu: Falló la edición",
        "feedback-error3": "Fallu: Ensin respuesta de la API",
        "unlinkaccounts-success": "Desenllazóse la cuenta.",
        "authenticationdatachange-ignored": "Nun se xestionó'l cambéu de los datos d'autentificacion. ¿Seique, nun se configuró un fornidor?",
        "userjsispublic": "Atención: les subpáxines JavaScript nun tendríen de contener datos acutaos porque son visibles pa otros usuarios.",
-       "usercssispublic": "Atención: les subpáxines CSS nun tendríen de contener datos acutaos porque son visibles pa otros usuarios."
+       "usercssispublic": "Atención: les subpáxines CSS nun tendríen de contener datos acutaos porque son visibles pa otros usuarios.",
+       "restrictionsfield-badip": "Direición o rangu IP inválidu: $1",
+       "restrictionsfield-label": "Rangos d'IP permitíos:",
+       "restrictionsfield-help": "Una única direición IP o rangu CIDR per llinia. P'activar toos, utiliza<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index add8bea..428dafc 100644 (file)
        "yourpasswordagain": "Parolu təkrar daxil edin:",
        "createacct-yourpasswordagain": "Parolu təsdiqlə",
        "createacct-yourpasswordagain-ph": "Parolu təkrar daxil edin",
-       "remembermypassword": "Məni bu kompyuterdə xatırla (maksimum $1 {{PLURAL:$1|gün|gün}})",
        "userlogin-remembermypassword": "Sistemdə qal",
        "userlogin-signwithsecure": "Etibarlı bağlantıdan istifadə edin",
        "yourdomainname": "Sizin domeniniz:",
        "listgrouprights-addgroup-self-all": "Bütün qrupları öz hesabına əlavə edə bilər",
        "listgrouprights-removegroup-self-all": "Bütün qrupları öz hesabından çıxara bilər",
        "listgrouprights-namespaceprotection-namespace": "Adlar fəzası",
+       "restricted-displaytitle-ignored": "İmtina edilmiş görüntü başlıqlarına malik səhifələr",
        "mailnologin": "Ünvan yoxdur",
        "emailuser": "İstifadəçiyə e-məktub göndər",
        "defemailsubject": "\"$1\" adlı istifadəçidən {{SITENAME}} e-məktubu",
        "htmlform-selectorother-other": "Digər",
        "htmlform-no": "Xeyr",
        "htmlform-yes": "Bəli",
-       "sqlite-has-fts": "$1 tam mətn axtarma ilə",
-       "sqlite-no-fts": "$1 tam mətn axtarma olmadan",
        "logentry-delete-delete": "$1 $3 səhifəsini {{GENDER:$2|sildi}}",
        "logentry-suppress-delete": "$1 $3 səhifəsini {{GENDER:$2|gizlətdi}}",
        "revdelete-content-hid": "gizli mətn",
        "feedback-bugnew": "Mən yoxladım. Yeni xəta barədə xəbər ver",
        "feedback-cancel": "İmtina",
        "feedback-close": "Oldu",
-       "feedback-error-title": "Xəta",
        "feedback-error2": "Xəta: Redaktə qeydə alınmadı",
        "feedback-message": "Mesaj:",
        "feedback-subject": "Mövzu:",
index 1dd05c7..a88feea 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.",
        "upload-form-label-infoform-categories": "Категориялар",
        "upload-form-label-infoform-date": "Дата",
        "upload-form-label-own-work-message-generic-local": "Тейәлгән файл  {{SITENAME}} лицензия сәйәсәтенә ярашлы икәнен раҫлайым.",
-       "upload-form-label-not-own-work-message-generic-local": "{{SITENAME}} ҡағиҙәләренә ярашлы файлды тейәй алмаһағыҙ, диалог теҙерәһен ябығыҙ ҙа тейәү !с!н башҡа ысулды һайлағыҙ.",
+       "upload-form-label-not-own-work-message-generic-local": "{{SITENAME}} ҡағиҙәләренә ярашлы файлды тейәй алмаһағыҙ, диалог тәҙрәһен ябығыҙ ҙа тейәү өсөн башҡа ысулды һайлағыҙ.",
        "upload-form-label-not-own-work-local-generic-local": "Ошонда эшләп ҡарағыҙ[[Special:Upload|килешеү буйынса тейәү бите]].",
        "upload-form-label-own-work-message-generic-foreign": "Был файлды дөйөм репозиторийға күсереүемде аңлайым. Быны ҡулланыусы килешеүе һәм лицензия сәйәсәтенә ярашлы эшләүемде раҫлайым.",
        "upload-form-label-not-own-work-message-generic-foreign": "{{SITENAME}} ҡағиҙәләренә ярашлы файлды тейәй алмаһағыҙ, диалог теҙерәһен ябығыҙ ҙа тейәү өсөн башҡа ысулды һайлағыҙ.",
        "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|үҙгәртте}}",
        "feedback-external-bug-report-button": "Техник эш еберергә",
        "feedback-dialog-title": "Баһалама ебәрергә",
        "feedback-dialog-intro": "Баһалама ебәреү өсөн түбәндәге форманы файҙаланығыҙ. Һеҙҙең исем менән комментарий «$1» битендә буласаҡ.",
-       "feedback-error-title": "Хата",
        "feedback-error1": "Хата: API-нан беленмәгән хата",
        "feedback-error2": "Хата: Мөхәррирләү хатаһы",
        "feedback-error3": "Хата: API-нан яуап юҡ",
index 32ddf70..f60b28d 100644 (file)
        "newwindow": "(адкрываецца ў новым акне)",
        "cancel": "Скасаваць",
        "moredotdotdot": "Далей…",
-       "morenotlisted": "Гэта ня поўны сьпіс.",
+       "morenotlisted": "Гэты сьпіс можа быць няпоўным.",
        "mypage": "Старонка",
        "mytalk": "Гутаркі",
        "anontalk": "Гутаркі",
        "talk": "Абмеркаваньне",
        "views": "Рэжымы",
        "toolbox": "Інструмэнты",
+       "tool-link-userrights": "Зьмяніць групы {{GENDER:$1|ўдзельніка|ўдзельніцы}}",
+       "tool-link-emailuser": "Даслаць {{GENDER:$1|удзельніку|удзельніцы}} ліст электроннай поштай",
        "userpage": "Паказаць старонку ўдзельніка",
        "projectpage": "Паказаць старонку праекту",
        "imagepage": "Паказаць старонку файла",
        "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». Магчыма, ён ужо быў дададзены?",
        "passwordreset-emailelement": "Імя ўдзельніка: \n$1\n\nЧасовы пароль: \n$2",
        "passwordreset-emailsentemail": "Калі гэты адрас электроннай пошты далучаны да вашага рахунку, тады будзе дасланы ліст пра скідваньне паролю.",
        "passwordreset-emailsentusername": "Калі ёсьць адрас электроннай пошты, злучаны з гэтым імем удзельніка, тады будзе дасланы ліст пра скідваньне паролю.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Электронны ліст|Электронныя лісты}} скіданьня паролю {{PLURAL:$1|быў дасланы|былі дасланыя}}. {{PLURAL:$1|Імя ўдзельніка і пароль|Сьпіс імёнаў удзельнікаў і паролі}} паказаныя ніжэй.",
-       "passwordreset-emailerror-capture2": "Не атрымалася даслаць {{GENDER:$2|удзельніку|удзельніцы}} ліст электроннай поштай: $1 {{PLURAL:$3|Імя ўдзельніка і пароль|Сьпіс імёнаў удзельнікаў і паролі}} паказаныя ніжэй.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|1=Электронны ліст|Электронныя лісты}} скіданьня паролю {{PLURAL:$1|1=быў дасланы|былі дасланыя}}. {{PLURAL:$1|1=Імя ўдзельніка і пароль|Сьпіс імёнаў удзельнікаў і паролі}} паказаныя тут.",
+       "passwordreset-emailerror-capture2": "Не атрымалася даслаць {{GENDER:$2|удзельніку|удзельніцы}} ліст электроннай поштай: $1 {{PLURAL:$3|1=Імя ўдзельніка і пароль|Сьпіс імёнаў удзельнікаў і паролі}} паказаныя тут.",
        "passwordreset-nocaller": "Мусіць быць пададзены той, хто робіць выклік",
        "passwordreset-nosuchcaller": "Аўтар выкліку не існуе: $1",
        "passwordreset-ignored": "Скіданьне паролю не адбылося. Магчыма, ня быў наладжаны пастаўшчык?",
        "resettokens-text": "Тут вы можаце скінуць токены, якія даюць вам доступ да пэўных прыватных зьвестак, асацыяваных з вашым рахункам.\n\nКалі вы выпадкова падзяліліся токенамі зь іншымі, або калі ваш рахунак быў скампрамэтаваны, скарыстайцеся гэтай магчымасьцю і скіньце токены.",
        "resettokens-no-tokens": "Няма токенаў для скіданьня.",
        "resettokens-tokens": "Токены:",
-       "resettokens-token-label": "$1 (бягучае значэньне: $2)",
+       "resettokens-token-label": "$1 (цяперашняе значэньне: $2)",
        "resettokens-watchlist-token": "Токен стужкі (Atom/RSS) [[Special:Watchlist|зьменаў у вашым сьпісе назіраньня]]",
        "resettokens-done": "Токены скінутыя.",
        "resettokens-resetbutton": "Скінуць вылучаныя токены",
        "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": "Калі ласка, увядзіце камэнтар ніжэй.",
        "subject-preview": "Папярэдні прагляд загалоўку:",
        "previewerrortext": "Адбылася памылка пры спробе папярэдняга прагляду вашых зьменаў.",
        "blockedtitle": "Удзельнік заблякаваны",
-       "blockedtext": "'''Ваш рахунак ўдзельніка ці IP-адрас быў заблякаваны.'''\n\nБлякаваньне выканаў $1.\nПрычына гэтага: ''$2''.\n\n* Пачатак блякаваньня: $8\n* Сканчэньне блякаваньня: $6\n* Быў заблякаваны: $7\n\nВы можаце скантактавацца з $1 ці адным зь іншых [[{{MediaWiki:Grouppage-sysop}}|адміністратараў]], каб абмеркаваць блякаваньне. Заўважце, што Вы ня зможаце ўжыць магчымасьць «даслаць ліст па электроннай пошце», пакуль не пазначыце сапраўдны адрас электроннай пошты ў Вашых [[Special:Preferences|наладах]], і калі гэта Вам не было забаронена.\nВаш IP-адрас — $3, ідэнтыфікатар блякаваньня — #$5.\nКалі ласка, улучайце ўсю вышэйпададзеную інфармацыю ва ўсе запыты, што Вы будзеце рабіць.",
-       "autoblockedtext": "Ваш IP-адрас быў аўтаматычна заблякаваны, таму што ён ужываўся іншым удзельнікам, які быў заблякаваны $1.\nПрычына гэтага:\n\n:''$2''\n\n* Блякаваньне пачалося: $8\n* Блякаваньне скончыцца: $6\n* Быў заблякаваны: $7\n\nВы можаце скантактавацца з $1 ці з адным зь іншых [[{{MediaWiki:Grouppage-sysop}}|адміністратараў]], каб абмеркаваць блякаваньне.\n\nЗаўважце, што Вы ня зможаце ужываць магчымасьць «даслаць ліст праз электронную пошту», пакуль ня будзе пазначаны дзейны адрас электроннай пошты ў Вашых [[Special:Preferences|наладах удзельніка]], і калі гэта Вам не было забаронена.\n\nВаш цяперашні IP-адрас — $3, ідэнтыфікатар блякаваньня — #$5.\nКалі ласка, улучайце ўсю вышэйпададзеную інфармацыю ва ўсе запыты, што Вы будзеце рабіць.",
+       "blockedtext": "<strong>Ваш рахунак удзельніка ці IP-адрас быў заблякаваны.</strong>\n\nБлякаваньне выканаў $1.\nПрычына гэтага: <em>$2</em>.\n\n* Пачатак блякаваньня: $8\n* Сканчэньне блякаваньня: $6\n* Быў заблякаваны: $7\n\nВы можаце скантактавацца з $1 ці адным зь іншых [[{{MediaWiki:Grouppage-sysop}}|адміністратараў]], каб абмеркаваць блякаваньне. Заўважце, што Вы ня зможаце ўжыць магчымасьць «даслаць ліст па электроннай пошце», пакуль не пазначыце сапраўдны адрас электроннай пошты ў Вашых [[Special:Preferences|наладах]], і калі гэта Вам не было забаронена.\nВаш IP-адрас — $3, ідэнтыфікатар блякаваньня — #$5.\nКалі ласка, улучайце ўсю вышэйпададзеную інфармацыю ва ўсе запыты, што Вы будзеце рабіць.",
+       "autoblockedtext": "Ваш IP-адрас быў аўтаматычна заблякаваны, таму што ён ужываўся іншым удзельнікам, які быў заблякаваны $1.\nПрычына гэтага:\n\n:<em>$2</em>\n\n* Блякаваньне пачалося: $8\n* Блякаваньне скончыцца: $6\n* Быў заблякаваны: $7\n\nВы можаце скантактавацца з $1 ці з адным зь іншых [[{{MediaWiki:Grouppage-sysop}}|адміністратараў]], каб абмеркаваць блякаваньне.\n\nЗаўважце, што Вы ня зможаце ўжываць магчымасьць «даслаць ліст праз электронную пошту», пакуль ня будзе пазначаны дзейны адрас электроннай пошты ў Вашых [[Special:Preferences|наладах удзельніка]], і калі гэта Вам не было забаронена.\n\nВаш цяперашні IP-адрас — $3, ідэнтыфікатар блякаваньня — #$5.\nКалі ласка, улучайце ўсю вышэйпададзеную інфармацыю ва ўсе запыты, што Вы будзеце рабіць.",
        "blockednoreason": "прычына не пазначана",
        "whitelistedittext": "Вам трэба $1, каб рэдагаваць старонкі.",
        "confirmedittext": "Вы мусіце пацьвердзіць Ваш адрас электроннай пошты перад рэдагаваньнем старонак. Калі ласка, пазначце і пацьвердзіце адрас электроннай пошты праз Вашы [[Special:Preferences|налады]].",
        "nosuchsectiontitle": "Немагчыма знайсьці сэкцыю",
-       "nosuchsectiontext": "Вы спрабуеце рэдагаваць сэкцыю, якой не існуе.\nЯна магла быць перанесена, альбо выдалена пад час Вашага прагляду старонкі.",
+       "nosuchsectiontext": "Вы спрабавалі рэдагаваць сэкцыю, якой не існуе.\nЯна магла быць перанесеная альбо выдаленая, пакуль вы праглядалі старонку.",
        "loginreqtitle": "Патрабуецца ўваход у сыстэму",
        "loginreqlink": "ўвайсьці",
        "loginreqpagetext": "Вы мусіце $1, каб праглядаць іншыя старонкі.",
        "upload-dialog-disabled": "Загрузка файлаў з дапамогай гэтага дыялёгу адключаная ў гэтай вікі.",
        "upload-dialog-title": "Загрузка файла",
        "upload-dialog-button-cancel": "Адмяніць",
+       "upload-dialog-button-back": "Назад",
        "upload-dialog-button-done": "Зроблена",
        "upload-dialog-button-save": "Захаваць",
        "upload-dialog-button-upload": "Загрузіць",
        "apisandbox-results-fixtoken-fail": "Памылка пры атрыманьні токену «$1».",
        "apisandbox-alert-page": "Палі на гэтай старонцы няслушныя.",
        "apisandbox-alert-field": "Значэньне гэтага поля зьяўляецца няслушным.",
+       "apisandbox-continue": "Працягнуць",
+       "apisandbox-continue-clear": "Ачысьціць",
        "booksources": "Крыніцы кніг",
        "booksources-search-legend": "Пошук кніг",
        "booksources-isbn": "ISBN:",
        "tag-filter-submit": "Фільтар",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|1=Метка|Меткі}}]]: $2)",
        "tag-mw-contentmodelchange": "зьмена мадэлі зьместу",
+       "tag-mw-contentmodelchange-description": "Рэдагаваньні, якія [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel зьмяняюць мадэль зьместу] старонкі",
        "tags-title": "Меткі",
        "tags-intro": "На гэтай старонцы знаходзіцца сьпіс метак, якімі праграмнае забесьпячэньне можа пазначыць рэдагаваньне, і іх значэньне.",
        "tags-tag": "Назва меткі",
        "htmlform-cloner-create": "Дадаць больш",
        "htmlform-cloner-delete": "Выдаліць",
        "htmlform-cloner-required": "Патрабуецца як мінімум яшчэ адно значэньне.",
+       "htmlform-date-placeholder": "ГГГГ-ММ-ДД",
+       "htmlform-time-placeholder": "ГГ:ХХ:СС",
+       "htmlform-datetime-placeholder": "ГГГГ-ММ-ДД ГГ:ХХ:СС",
+       "htmlform-date-invalid": "Уведзенае вамі значэньне не зьяўляецца датай. Паспрабуйце ўжыць фармат ГГГГ-ММ-ДД.",
+       "htmlform-time-invalid": "Уведзенае вамі значэньне не зьяўляецца часам. Паспрабуйце ўжыць фармат ГГ:ХХ:СС.",
+       "htmlform-datetime-invalid": "Уведзенае вамі значэньне не зьяўляецца датай і часам. Паспрабуйце ўжыць фармат ГГГГ-ММ-ДД ГГ:ХХ:СС.",
+       "htmlform-date-toolow": "Уведзенае вамі значэньне меней за самую раньнюю дазволеную дату $1.",
+       "htmlform-date-toohigh": "Значэньне, якое вы выбралі, болей за самую позьнюю дазволеную дату $1.",
+       "htmlform-time-toolow": "Значэньне, якое вы выбралі, меней за самы раньні дазволены час $1.",
+       "htmlform-time-toohigh": "Значэньне, якое вы выбралі, болей за самы позьні дазволены час $1.",
+       "htmlform-datetime-toolow": "Значэньне, якое вы выбралі, меней за самую раньнюю дазволеную дату і час $1.",
+       "htmlform-datetime-toohigh": "Значэньне, якое вы выбралі, болей за найпазьнейшую дазволеную дату і час $1.",
        "htmlform-title-badnamespace": "[[:$1]] знаходзіцца не ў прасторы назваў «{{ns:$2}}».",
        "htmlform-title-not-creatable": "«$1» — немагчымы загаловак для старонкі",
        "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 {{GENDER:$2|выдаліў|выдаліла}} старонку $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|аднавіў|аднавіла}} старонку $3",
        "logentry-delete-event": "$1 {{GENDER:$2|зьмяніў|зьмяніла}} бачнасьць $5 {{PLURAL:$5|1=падзеі ў журнале|падзеяў у журнале}} на $3: $4",
        "feedback-external-bug-report-button": "Аформіць тэхнічную задачу",
        "feedback-dialog-title": "Адаслаць водгук",
        "feedback-dialog-intro": "Свой водгук Вы можаце адаслаць праз простую форму зьнізу. Ваш камэнтар будзе дададзены на старонку «$1» разам з Вашым іменем.",
-       "feedback-error-title": "Памылка",
        "feedback-error1": "Памылка: невядомы вынік з API",
        "feedback-error2": "Памылка рэдагаваньня",
        "feedback-error3": "Памылка: няма адказу ад API",
        "sessionprovider-nocookies": "Файлы-кукі могуць быць адключаныя. Упэўніцеся, што ў вас уключаныя файлы-кукі і пачніце спачатку.",
        "randomrootpage": "Выпадковая карэнная старонка",
        "log-action-filter-block": "Тып блякаваньня:",
+       "log-action-filter-contentmodel": "Тып мадыфікацыі contentmodel:",
        "log-action-filter-delete": "Тып выдаленьня:",
        "log-action-filter-import": "Тып імпарту:",
+       "log-action-filter-managetags": "Тып дзеяньня кіраваньня меткамі:",
        "log-action-filter-move": "Тып пераносу:",
+       "log-action-filter-newusers": "Тып стварэньня рахунку:",
+       "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": "Зьмяненьне блякаваньня",
        "log-action-filter-block-unblock": "Разблякаваць",
        "log-action-filter-contentmodel-change": "Зьмена мадэлі зьместу",
+       "log-action-filter-contentmodel-new": "Стварэньне старонкі зь нестандартнай мадэльлю зьместу",
        "log-action-filter-delete-delete": "Выдаленьне старонкі",
        "log-action-filter-delete-restore": "Аднаўленьне старонкі",
        "log-action-filter-delete-event": "Выдаленьне журналу",
index 1ab171e..a3019e0 100644 (file)
        "talk": "Размовы",
        "views": "Віды",
        "toolbox": "Прылады",
+       "tool-link-userrights": "Змяніць групы {{GENDER:$1|ўдзельніка|ўдзельніцы}}",
+       "tool-link-emailuser": "Напісаць ліст {{GENDER:$1|удзельніку|удзельніцы}}",
        "userpage": "Паказаць старонку ўдзельніка",
        "projectpage": "Паказаць старонку праекта",
        "imagepage": "Гл. старонку файла",
        "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\". Магчыма, яна ўжо дададзена?",
        "upload-foreign-cant-upload": "Гэта вікі не настроена для ўкладання файлаў у запытанае старонняе сховішча файлаў.",
        "upload-dialog-title": "Укласці файл",
        "upload-dialog-button-cancel": "Нічога",
+       "upload-dialog-button-back": "Назад",
        "upload-dialog-button-done": "Гатова",
        "upload-dialog-button-save": "Запісаць",
        "upload-dialog-button-upload": "Укласці",
        "apisandbox-submit-invalid-fields-title": "Некаторыя палі недапушчальныя",
        "apisandbox-submit-invalid-fields-message": "Калі ласка, выпраўце адзначаныя палі і паспрабуйце ізноў.",
        "apisandbox-results": "Вынікі",
+       "apisandbox-request-url-label": "URL-адрас запыту:",
+       "apisandbox-request-time": "Час запыту: {{PLURAL:$1|$1 мс}}",
+       "apisandbox-results-fixtoken": "Папраўце токен і паўтарыце адпраўку",
        "apisandbox-alert-page": "Палі на гэтай старонцы недапушчальныя.",
        "apisandbox-alert-field": "Значэнне гэтага поля недапушчальнае.",
        "booksources": "Кнігі",
        "rollbacklinkcount": "адкаціць $1 {{PLURAL:$1|праўку|праўкі|правак}}",
        "rollbacklinkcount-morethan": "адкаціць больш за $1 {{PLURAL:$1|праўку|праўкі|правак}}",
        "rollbackfailed": "Не ўдалося адкаціць",
+       "rollback-missingparam": "У запыце адсутнічаюць абавязковыя параметры.",
+       "rollback-missingrevision": "Не ўдалося атрымаць звесткі версіі.",
        "cantrollback": "Немагчыма адкаціць праўку; апошні аўтар гэта адзіны аўтар на гэтай старонцы.",
        "alreadyrolled": "Немагчыма адкаціць апошнюю праўку ў [[:$1]], аўтарства [[User:$2|$2]] ([[User talk:$2|Talk]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]);\nз таго часу нехта іншы правіў або адкатваў гэтую старонку.\n\nАпошняя праўка старонкі была аўтарства [[User:$3|$3]] ([[User talk:$3|Talk]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "Тлумачэнне праўкі было: <em>$1</em>.",
        "revertpage": "Праўкі аўтарства [[Special:Contributions/$2|$2]] ([[User talk:$2|размова]]) адкочаныя; вернута апошняя версія аўтарства [[User:$1|$1]]",
        "revertpage-nouser": "Праўкі (імя ўдзельніка схавана) адкочаны да версіі {{GENDER:$1|[[User:$1|$1]]}}",
        "rollback-success": "Адкочаны праўкі $1; вернута апошняя версія $2.",
+       "rollback-success-notify": "Адкочаны праўкі $1;\nвернута апошняя версія $2. [$3 Паказаць змены]",
        "sessionfailure-title": "Памылка сеансу",
        "sessionfailure": "Магчыма, ёсць праблемы з вашым сеансам працы ў сістэме. Таму вам было адмоўлена ў выкананні дзеяння, каб засцерагчыся ад захопу сеанса.\n\nВярніцеся на папярэднюю старонку, перазагрузіце яе і тады паспрабуйце зноў.",
        "changecontentmodel": "Змяніць мадэль змесціва старонкі",
        "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-change-revertlink": "адкаціць",
        "logentry-contentmodel-change-revert": "адкат",
        "protectlogpage": "Журнал аховы",
        "export-download": "Прапанаваць запісаць у файл",
        "export-templates": "Разам з шаблонамі",
        "export-pagelinks": "Разам са старонкамі, на якія ёсць спасылкі (макс. кольк. крокаў):",
+       "export-manual": "Дадаць старонкі ўручную:",
        "allmessages": "Сістэмныя паведамленні",
        "allmessagesname": "Назва",
        "allmessagesdefault": "Прадвызначаны тэкст",
        "thumbnail-temp-create": "Не ўдаецца стварыць часовы файл эскіза",
        "thumbnail-dest-create": "Не ўдаецца захаваць эскіз па месцы прызначэння",
        "thumbnail_invalid_params": "Няправільныя параметры драбніцы",
+       "thumbnail_toobigimagearea": "Файл з памерамі большымі, чым $1",
        "thumbnail_dest_directory": "Немагчыма стварыць мэтавую тэчку",
        "thumbnail_image-type": "Дадзены тып выявы не падтрымліваецца",
        "thumbnail_gd-library": "Няпоўная канфігурацыя бібліятэкі GD, адсутнічае функцыя $1",
        "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": "Не дазволена",
        "confirmemail_body_set": "Нехта (магчыма, вы) з IP-адрасам $1\nпаказаў дадзены адрас электроннай пошты для ўліковага запісу «$2» у праекце {{SITENAME}}.\n\nКаб пацвердзіць, што акаўнт сапраўды належыць вам, і ўключыць магчымасць адпраўкі лістоў з сайта {{SITENAME}}, адкрыйце гэтую спасылку ў браўзеры:\n\n$3\n\nКалі рахунак вам *не належыць*, адкрыйце ніжэй паказаную спасылку, каб адмовіцца ад пацверджання адрасу эл.пошты:\n\n$5\n\nКод пацверджання дзейсны да $4.",
        "confirmemail_invalidated": "Пацверджанне эл.пошты скасаванае",
        "invalidateemail": "Адмовіцца ад пацверджання эл.пошты",
+       "notificationemail_subject_changed": "Адрас электроннай пошты на пляцоўцы {{SITENAME}} зменены",
        "scarytranscludedisabled": "[Устаўлянне з іншых вікі не дазволена]",
        "scarytranscludefailed": "[Не ўдалося атрымаць шаблон для $1]",
        "scarytranscludefailed-httpstatus": "[Не ўдалося атрымаць шаблон для $1: HTTP $2]",
        "tag-filter": "Фільтр [[Special:Tags|бірак]]:",
        "tag-filter-submit": "Фільтр",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Тэг|Тэгі}}]]: $2)",
+       "tag-mw-contentmodelchange": "змена мадэлі змесціва",
        "tags-title": "Біркі",
        "tags-intro": "Тут пералічаныя біркі, якімі праграмы могуць пазначыць праўку, а таксама іх значэнні.",
        "tags-tag": "Назва біркі",
        "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 {{GENDER:$2|сцёр|сцёрла}} старонку $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|аднавіў|аднавіла}} старонку $3",
        "logentry-delete-event": "$1 {{GENDER:$2|змяніў|змяніла}} бачнасць {{PLURAL:$5|запісу журнала|$5 запісаў журнала}} $3: $4",
        "feedback-cancel": "Адмена",
        "feedback-close": "Зроблена.",
        "feedback-dialog-title": "Даслаць водгук",
-       "feedback-error-title": "Памылка",
        "feedback-error1": "Памылка. Невядомы вынік з API",
        "feedback-error2": "Памылка. Збой праўкі",
        "feedback-error3": "Памылка. Няма адказу ад API",
        "expand_templates_generate_xml": "Паказаць дрэва сінтаксічнага аналізу XML",
        "expand_templates_generate_rawhtml": "Паказаць зыходны код HTML",
        "expand_templates_preview": "Перадпаказ",
+       "expand_templates_input_missing": "Трэба ўвесці хоць які-небудзь тэкст.",
        "pagelanguage": "Змяніць мову старонкі",
        "pagelang-name": "Старонка",
        "pagelang-language": "Мова",
        "mediastatistics-header-unknown": "Невядомыя",
        "mediastatistics-header-bitmap": "Растравыя выявы",
        "mediastatistics-header-drawing": "Рысункі (вектарныя выявы)",
+       "mediastatistics-header-text": "Тэкст",
+       "mediastatistics-header-archive": "Сціснутыя фарматы",
        "mediastatistics-header-total": "Усе файлы",
        "json-error-state-mismatch": "Недапушчальны або некарэктны JSON",
        "json-error-syntax": "Памылка сінтаксісу",
+       "headline-anchor-title": "Спасылка на гэты раздзел",
        "special-characters-group-latin": "Лацінскія",
        "special-characters-group-latinextended": "Лацінскія дадатковыя",
        "special-characters-group-ipa": "IPA",
index c8eb76b..67965f5 100644 (file)
@@ -64,7 +64,7 @@
        "tog-enotifminoredits": "Уведомяване по е-пощата при малки промени на страници или файлове",
        "tog-enotifrevealaddr": "Показване на електронния ми адрес в известяващите писма",
        "tog-shownumberswatching": "Показване на броя на потребителите, наблюдаващи дадена страница",
-       "tog-oldsig": "Текущ подпис:",
+       "tog-oldsig": "Ð\92аÑ\88иÑ\8fÑ\82 Ñ\82екущ подпис:",
        "tog-fancysig": "Без превръщане на подписа в препратка към потребителската страница",
        "tog-uselivepreview": "Използване на бърз предварителен преглед",
        "tog-forceeditsummary": "Предупреждаване при празно поле за резюме на редакцията",
@@ -81,7 +81,7 @@
        "tog-showhiddencats": "Показване на скритите категории",
        "tog-norollbackdiff": "Не показвай разликата между редакциите след отмяна на редакции",
        "tog-useeditwarning": "Предупреждаване при опит за напускане на страница, отворена в режим на редактиране, без да са запазени промените",
-       "tog-prefershttps": "Да се използва винаги защитена връзка след влизане",
+       "tog-prefershttps": "Да се използва винаги защитена връзка при влизане",
        "underline-always": "Винаги",
        "underline-never": "Никога",
        "underline-default": "Според настройките на облика или браузъра",
        "newwindow": "(отваря се в нов прозорец)",
        "cancel": "Отказ",
        "moredotdotdot": "Още…",
-       "morenotlisted": "Този Ñ\81пиÑ\81Ñ\8aк Ð½Ðµ Ðµ пълен.",
+       "morenotlisted": "Ð\92Ñ\8aзможно Ðµ Ñ\82ози Ñ\81пиÑ\81Ñ\8aк Ð´Ð° Ðµ Ð½Ðµпълен.",
        "mypage": "Страница",
        "mytalk": "Беседа",
        "anontalk": "Беседа",
        "yourpasswordagain": "Парола (повторно):",
        "createacct-yourpasswordagain": "Потвърждаване на паролата",
        "createacct-yourpasswordagain-ph": "Въвежда се паролата (повторно)",
-       "remembermypassword": "Запомняне на паролата на този компютър (най-много за $1 {{PLURAL:$1|ден|дни}})",
        "userlogin-remembermypassword": "Запомняне",
        "userlogin-signwithsecure": "Използване на защитена връзка",
        "yourdomainname": "Домейн:",
        "botpasswords-label-cancel": "Отказване",
        "botpasswords-label-delete": "Изтриване",
        "botpasswords-label-resetpassword": "Възстановяване на парола",
-       "botpasswords-label-restrictions": "Ограничения на употребата:",
        "botpasswords-created-title": "Паролата на бота е създадена",
        "botpasswords-created-body": "Паролата на бот „$1“ на потребител „$2“ е създадена.",
        "botpasswords-updated-title": "Паролата на бота е обновена",
        "mergehistory-empty": "Няма редакции, които могат да бъдат слети.",
        "mergehistory-done": "$3 {{PLURAL:$3|версия|версии}} от $1 {{PLURAL:$3|беше успешно слята|бяха успешно слети}} с редакционната история на [[:$2]].",
        "mergehistory-fail": "Невъзможно е да се извърши сливане на редакционните истории; проверете страницата и времевите параметри.",
+       "mergehistory-fail-invalid-source": "Изходната страница е невалидна.",
+       "mergehistory-fail-invalid-dest": "Целевата страница е невалидна.",
+       "mergehistory-fail-permission": "Нямате права за обединяване на историята.",
+       "mergehistory-fail-self-merge": "Изходната и целевата страница се еднакви.",
        "mergehistory-no-source": "Изходната страница $1 не съществува.",
        "mergehistory-no-destination": "Целевата страница $1 не съществува.",
        "mergehistory-invalid-source": "Изходната страница трябва да притежава коректно име.",
        "searchprofile-advanced-tooltip": "Търсене в избрани именни пространства",
        "search-result-size": "$1 ({{PLURAL:$2|една дума|$2 думи}})",
        "search-result-category-size": "{{PLURAL:$1|1 член|$1 члена}} ({{PLURAL:$2|1 подкатегория|$2 подкатегории}}, {{PLURAL:$3|1 файл|$3 файла}})",
-       "search-redirect": "(пренасочване $1)",
+       "search-redirect": "(пренасочване от $1)",
        "search-section": "(раздел $1)",
        "search-category": "(категория $1)",
        "search-suggest": "Вероятно имахте предвид: $1",
        "grant-editmywatchlist": "редактиране на списъка ви за наблюдение",
        "grant-editpage": "Редактиране на съществуващи страници",
        "grant-editprotected": "Редактиране на защитени страници",
+       "grant-uploadeditmovefile": "Качване, заменяне и прехвърляне на файлове",
        "grant-uploadfile": "Качване на нови файлове",
        "grant-basic": "Основни права",
        "grant-viewdeleted": "Преглед на изтрити файлове и страници",
        "action-viewmywatchlist": "преглед на списъка ви за наблюдение",
        "action-viewmyprivateinfo": "преглеждане на личните данни",
        "action-editmyprivateinfo": "редактиране на личната си информация",
+       "action-managechangetags": "създаване и (де)активиране на етикети",
+       "action-applychangetags": "прилагане на етикетите заедно с промените ви",
        "action-purge": "почисти кеша на тази страница",
        "nchanges": "$1 {{PLURAL:$1|промяна|промени}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|от последното посещение}}",
        "apihelp-no-such-module": "Модул \"$1\" не беше намерен.",
        "apisandbox": "Пясъчник за API",
        "apisandbox-fullscreen": "Разшири полето",
+       "apisandbox-submit": "Направи запитване",
        "apisandbox-reset": "Изчистване",
        "apisandbox-retry": "Повторен опит",
+       "apisandbox-loading": "Зареждане на информация за API-модул \"$1\"...",
+       "apisandbox-load-error": "Възникна грешка при зареждането на информация за API-модул \"$1\": $2",
+       "apisandbox-no-parameters": "Този API-модул няма параметри.",
+       "apisandbox-helpurls": "Връзки за помощ",
        "apisandbox-examples": "Примери",
+       "apisandbox-dynamic-parameters": "Допълнителни параметри",
        "apisandbox-dynamic-parameters-add-label": "Добавяне на параметър:",
        "apisandbox-dynamic-parameters-add-placeholder": "Име на параметъра",
+       "apisandbox-dynamic-error-exists": "Параметър с име \"$1\" вече съществува.",
        "apisandbox-results": "Резултати",
        "apisandbox-request-url-label": "URL-адрес на заявката:",
        "booksources": "Източници на книги",
        "booksources-text": "По-долу е списъкът от връзки към други сайтове, продаващи нови и използвани книги или имащи повече информация за книгите, които търсите:",
        "booksources-invalid-isbn": "Предоставеният ISBN изглежда е невалиден; проверете за грешки и копирайте от оригиналния източник.",
        "specialloguserlabel": "Изпълнител:",
-       "speciallogtitlelabel": "Цел (заглавие или потребител):",
+       "speciallogtitlelabel": "Цел (заглавие или {{ns:user}}:потребителско име за потребител):",
        "log": "Дневници",
        "logeventslist-submit": "Показване",
        "all-logs-page": "Всички публични дневници",
        "movenotallowedfile": "Нямате права да премествате файлове.",
        "cant-move-user-page": "Нямате нужните права на достъп, за да местите потребителски страници (можете да местите само подстраници).",
        "cant-move-to-user-page": "Нямате нужните права на достъп, за да извършвате преместване на страници върху потребителски страници (можете да местите само върху подстраници от потребителското пространство).",
+       "cant-move-category-page": "Нямате необходимите права за преместване на страници на категории.",
+       "cant-move-to-category-page": "Нямате необходимите права за преместване на страница в страница на категория.",
        "newtitle": "Ново заглавие:",
        "move-watch": "Наблюдаване на страницата",
        "movepagebtn": "Преместване",
        "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-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": "Заглавие",
        "exif-ycbcrpositioning-1": "Центрирани",
        "exif-dc-contributor": "Сътрудници",
        "exif-dc-date": "Дата(и)",
+       "exif-dc-publisher": "Издател",
+       "exif-dc-relation": "Свързани медии",
        "exif-dc-rights": "Права",
+       "exif-dc-source": "Източник медия",
        "exif-dc-type": "Вид медия",
        "exif-isospeedratings-overflow": "По-голяма от 65535",
        "exif-iimcategory-ace": "Изкуствa, култура и забавление",
        "watchlistedit-raw-done": "Списъкът ви за наблюдение беше обновен.",
        "watchlistedit-raw-added": "{{PLURAL:$1|1 страница беше добавена|$1 страници бяха добавени}}:",
        "watchlistedit-raw-removed": "{{PLURAL:$1|Една страница беше премахната|$1 страници бяха премахнати}}:",
+       "watchlistedit-clear-title": "Изчистване на списъка за наблюдение",
        "watchlistedit-clear-legend": "Изчистване на списъка за наблюдение",
        "watchlistedit-clear-explain": "Всички заглавия ще бъдат премахнати от списъка ви за наблюдение",
        "watchlistedit-clear-titles": "Заглавия:",
        "tags-actions-header": "Действия",
        "tags-active-yes": "Да",
        "tags-active-no": "Не",
-       "tags-source-extension": "Дефиниран от разширение",
+       "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": "Деактивиране на етикета",
        "tags-edit-chosen-placeholder": "Избиране на няколко етикета",
        "tags-edit-reason": "Причина:",
        "tags-edit-revision-submit": "Прилагане на промените към {{PLURAL:$1|тази редакция|$1 редакции}}",
+       "tags-edit-success": "Промените са приложени.",
+       "tags-edit-failure": "Промените не могат да бъдат приложени:\n$1",
        "tags-edit-nooldid-title": "Не е зададена версия",
        "comparepages": "Сравняване на страници",
        "compare-page1": "Страница 1",
        "htmlform-chosen-placeholder": "Избиране",
        "htmlform-cloner-create": "Добавяне на още",
        "htmlform-cloner-delete": "Премахване",
+       "htmlform-title-badnamespace": "[[:$1]] не е в именното пространство \"{{ns:$2}}\".",
        "htmlform-title-not-exists": "$1 не съществува.",
-       "sqlite-has-fts": "$1 с поддръжка на пълнотекстово търсене",
-       "sqlite-no-fts": "$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",
        "feedback-bugornote": "Ако сте готови подробно да опишете технически проблем, моля [$1 докладвайте го тук].\nВ противен случай, можете да използвате лесния формуляр по-долу. Коментарът ви ще бъде добавен към страницата \"[$3 $2]\", наред с вашето потребителско име.",
        "feedback-cancel": "Отказване",
        "feedback-close": "Готово",
-       "feedback-error-title": "Грешка",
        "feedback-error1": "Грешка: Неразпознат резултат от API",
        "feedback-error2": "Грешка: Неуспешна редакция",
        "feedback-error3": "Грешка: Няма отговор от API",
        "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 65e1021..1a60e1a 100644 (file)
@@ -58,7 +58,7 @@
        "tog-enotifminoredits": "পাতা এবং ফাইলগুলোতে অনুল্লেখ্য সম্পাদনার জন্যও আমাকে ই-মেইল করা হোক",
        "tog-enotifrevealaddr": "বিজ্ঞপ্তি মেইলে আমার ই-মেইল ঠিকানা প্রকাশ করা হোক",
        "tog-shownumberswatching": "নজরদারী করছে, এমন ব্যবহারকারীর সংখ্যা দেখানো হোক",
-       "tog-oldsig": "বর্তমান স্বাক্ষর:",
+       "tog-oldsig": "à¦\86পনার à¦¬à¦°à§\8dতমান à¦¸à§\8dবাà¦\95à§\8dষর:",
        "tog-fancysig": "স্বাক্ষরকে উইকিটেক্সট হিসেবে মনে করুন (কোন সয়ংক্রিয় লিঙ্ক ছাড়া)",
        "tog-uselivepreview": "তাৎক্ষণিক প্রাকদর্শন ব্যবহার করো",
        "tog-forceeditsummary": "খালি সম্পাদনা সারাংশ প্রবেশ করানোর সময় আমাকে জানানো হোক",
@@ -75,7 +75,7 @@
        "tog-showhiddencats": "লুকায়িত বিষয়শ্রেণীসমূহ দেখাও",
        "tog-norollbackdiff": "রোলব্যাকের পরে পার্থক্য দেখিও না",
        "tog-useeditwarning": "অসংরক্ষিত পরিবর্তনসহ কোনো পাতা ত্যাগের সময় সাবধান করো",
-       "tog-prefershttps": "যà¦\96নà¦\87 à¦ªà§\8dরবà§\87শ à¦\95রবà§\87ন সবসময় নিরাপদ সংযোগ ব্যবহার করুন",
+       "tog-prefershttps": "পà§\8dরবà§\87শ à¦\95রার à¦¸à¦®à¦¯à¦¼ সবসময় নিরাপদ সংযোগ ব্যবহার করুন",
        "underline-always": "সব সময়",
        "underline-never": "কখনো নয়",
        "underline-default": "স্কিন অথবা ব্রাউজারে যেমনভাবে নির্দিষ্ট করা আছে",
        "newwindow": "(নতুন উইন্ডোতে খুলবে)",
        "cancel": "বাতিল",
        "moredotdotdot": "আরও...",
-       "morenotlisted": "à¦\8fà¦\9fি à¦\8fà¦\95à¦\9fি à¦\85সমà§\8dপà§\82রà§\8dণ à¦¤à¦¾à¦²à¦¿à¦\95া।",
+       "morenotlisted": "à¦\8fà¦\87 à¦¤à¦¾à¦²à¦¿à¦\95াà¦\9fি à¦\85সমà§\8dপà§\82রà§\8dণ à¦¹à¦¤à§\87 à¦ªà¦¾à¦°à§\87।",
        "mypage": " পাতা",
        "mytalk": "আলোচনা",
        "anontalk": "আলাপ",
        "talk": "আলোচনা",
        "views": "দৃষ্টিকোণ",
        "toolbox": "সরঞ্জাম",
+       "tool-link-userrights": "{{GENDER:$1|ব্যবহারকারী}} দল পরিবর্তন করুন",
+       "tool-link-emailuser": "এই {{GENDER:$1|ব্যবহারকারী}}কে ইমেইল পাঠান",
        "userpage": "ব্যাবহারকারীর পাতা দেখুন",
        "projectpage": "মেটা-পাতা দেখুন",
        "imagepage": "ফাইল পাতা দেখুন",
        "userlogin-remembermypassword": "আমাকে প্রবেশ অবস্থায় রাখো",
        "userlogin-signwithsecure": "নিরাপদ সংযোগ ব্যবহার করুন",
        "cannotlogin-title": "প্রবেশ করতে পারবেন না",
+       "cannotlogin-text": "প্রবেশ করা সম্ভব নয়।",
        "cannotloginnow-title": "এখন প্রবেশ করা যাবে না",
        "cannotloginnow-text": "$1 ব্যবহার করার সময় প্রবেশ করা সম্ভব নয়।",
        "cannotcreateaccount-title": "অ্যাকাউন্ট তৈরি করা যাবে না",
+       "cannotcreateaccount-text": "সরাসরি অ্যাকাউন্ট সৃষ্টিকরণ এই উইকিতে সক্রিয় নয়।",
        "yourdomainname": "আপনার ডোমেইন:",
        "password-change-forbidden": "আপনি এই উইকিতে পাসওয়ার্ড পরিবর্তন করতে পারবেন না।",
        "externaldberror": "হয় কোন বহিঃস্থ যাচাইকরণ ডাটাবেজ ত্রুটি ঘটেছে অথবা আপনার বহিঃস্থ অ্যাকাউন্ট হালনাগাদ করার অনুমতি নেই।",
        "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": "বট পাসওয়ার্ড",
        "botpasswords-disabled": "বট পাসওয়ার্ড নিষ্ক্রিয় করা।",
        "botpasswords-no-central-id": "বট পাসওয়ার্ড ব্যবহার করার জন্য, আপনাকে একটি কেন্দ্রীভূত অ্যাকাউন্টে প্রবেশ করতে হবে।",
+       "botpasswords-existing": "বিদ্যমান বট শব্দচাবি",
        "botpasswords-createnew": "একটি নতুন বট পাসওয়ার্ড তৈরি করুন",
+       "botpasswords-editexisting": "একটি বিদ্যমান বট পাসওয়ার্ড পরিবর্তন করুন",
        "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\" নামের বট যুক্ত করা যায়নি। আগে থেকেই তালিকায় রয়েছে?",
        "botpasswords-deleted-title": "বট পাসওয়ার্ড অপসারণ করা হয়েছে",
        "botpasswords-deleted-body": "ব্যবহারকারী \"$2\"-এর \"$1\" নামের বটের জন্য বট পাসওয়ার্ড মুছে ফেলা হয়েছিল।",
        "botpasswords-no-provider": "BotPasswordsSessionProvider উপলব্ধ নয়।",
+       "botpasswords-restriction-failed": "বট পাসওয়ার্ডের সীমাবদ্ধতা এই প্রবেশ প্রতিরোধ করেছে।",
+       "botpasswords-not-exist": "ব্যবহারকারী \"$1\"-এর \"$2\" নামক বট পাসওয়ার্ডটি নেই।",
        "resetpass_forbidden": "পাসওয়ার্ড পরিবর্তন করা সম্ভব নয়",
        "resetpass_forbidden-reason": "পাসওয়ার্ড পরিবর্তন করা যাবে না: $1",
        "resetpass-no-info": "এই পাতাটিতে সরাসরি প্রবেশাধিকার পেতে আপনাকে অবশ্যই প্রবেশ করতে হবে।",
        "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-nocaller": "একটি আহ্বানকারী প্রদান করা আবশ্যক",
        "passwordreset-nosuchcaller": "আহ্বানকারীর অস্তিত্ব নেই: $1",
+       "passwordreset-ignored": "পাসওয়ার্ড পুনঃধার্যকরণ করা যায়নি। হয়তো কোন প্রদানকারী কনফিগার করা হয়েনি?",
        "passwordreset-invalideamil": "ভুল ইমেইল ঠিকানা",
        "passwordreset-nodata": "একটি ব্যবহারকারীর নাম বা একটি ইমেল ঠিকানা দুটির একটিও সরবরা দেয়া হয়নি",
        "changeemail": "ই-মেইল ঠিকানা পরিবর্তন বা বাতিল",
        "changeemail-no-info": "এই পাতাটিতে সরাসরি প্রবেশাধিকার পেতে আপনাকে অবশ্যই প্রবেশ করতে হবে।",
        "changeemail-oldemail": "বর্তমান ই-মেইল ঠিকানা:",
        "changeemail-newemail": "নতুন ই-মেইল ঠিকানা:",
+       "changeemail-newemail-help": "আপনার ইমেইল ঠিকানা অপসরণ করতে চাইলে এই জায়গাটি ফাঁকা ছেড়ে দেওয়া উচিত। ইমেইল ঠিকানা অপসরণ করলে আপনি ভুলে যাওয়া পাসওয়ার্ড পুনঃধার্য করতে পারবেন না এবং এই উইকি থেকে আপনাকে কোন ইমেইল পাঠানো হবে না।",
        "changeemail-none": "(কিছু নাই)",
        "changeemail-password": "আপনার {{SITENAME}} পাসওয়ার্ড:",
        "changeemail-submit": "ই-মেইল পরিবর্তন",
        "permissionserrors": "অনুমতি ত্রুটিসমূহ",
        "permissionserrorstext": "আপনার এটা করার অনুমতি নেই, নিচের {{PLURAL:$1|টি কারণের|টি কারণের}} জন্য:",
        "permissionserrorstext-withaction": "আপনার $2 অনুমতি নেই, যার {{PLURAL:$1|কারণ|কারণসমূহ}} হল:",
+       "contentmodelediterror": "আপনি এই পুনর্বিবেচনা সম্পাদনা করতে পারবেন না কারণ এর বিষয়বস্তু মডেল <code>$1</code>, যা বর্তমান বিষয়বস্তু মডেল <code>$2</code>-এর থেকে ভিন্ন।",
        "recreate-moveddeleted-warn": "'''সতর্কীকরণ: আপনি এমন একটি পাতা পুনরায় তৈরি করছেন যা পূর্বে অপসারণ করা হয়েছিল।'''\n\nআপনি পাতাটি সম্পাদনা চালিয়ে যাওয়া ঠিক হবে কিনা, তা বিবেচনা করুন।\nআপনার সুবিধার্থে পাতাটির অপলুপ্তি লগ এখানে দেয়া হলো:",
        "moveddeleted-notice": "এই পাতাটি অপসারণ করা হয়েছে।\nসূত্র হিসেবে নিচে এ পাতার অবলুপ্তি লগ দেওয়া হলো।",
        "moveddeleted-notice-recent": "দুঃখিত, এই পাতাটি সাম্প্রতি অপসারিত হয়েছে (সর্বশেষ ২৪ ঘণ্টায়)।\nসূত্র হিসেবে নিচে এই পাতা অপসারণ ও স্থানান্তর লগ দেয়া হয়েছে।",
        "invalid-content-data": "ভুল কন্টেন্ট ডাটা",
        "content-not-allowed-here": "\"$1\" কন্টেন্টটি [[$2]] পাতায় অনুমোদিত নয়",
        "editwarning-warning": "এই পাতাটি ত্যাগ করলে আপনার আপনার করা পরিবর্তনগুলো হারিয়ে যেতে পারে।\nআপনি যদি প্রবেশ করা থাকেন, আপনি এই সতর্কীকরণ বার্তাটি আপনার পছন্দের \"সম্পাদনা\" অনুচ্ছেদ থেকে নিস্ক্রিয় করতে পারেন।",
+       "editpage-invalidcontentmodel-title": "বিষয়বস্তু মডেল সমর্থিত নয়",
+       "editpage-invalidcontentmodel-text": "এই \"$1\" বিষয়বস্তু মডেলটি অসমর্থিত।",
        "editpage-notsupportedcontentformat-title": "উল্লেখিত পদ্ধতি সমর্থনযোগ্য নয়।",
        "editpage-notsupportedcontentformat-text": "$1 লেখার ফরম্যাট, $2 কন্টেন্ট মডেলের উপযোগী নয়।",
        "content-model-wikitext": "উইকিটেক্সট",
        "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-toobig": "ইতিহাস থেকে আগের পাতাগুলো একীকরণ সম্ভব নয়, কারণ এর ফলে সর্বোচ্চ $1 টি {{PLURAL:$1|সংস্করণ}} স্থানান্তরের সীমানা অতিক্রম করবে।",
        "searchprofile-advanced-tooltip": "স্বনির্ধারিত নামস্থানে অনুসন্ধান করো",
        "search-result-size": "$1 ({{PLURAL:$2|১টি শব্দ|$2টি শব্দ}})",
        "search-result-category-size": "{{PLURAL:$1 |১টি সদস্য |$1টি সদস্য}} ({{PLURAL:$2 |১টি উপবিষয়শ্রেণী|$2টি উপবিষয়শ্রেণী}}, {{PLURAL:$3 |১টি ফাইল |$3টি ফাইল}})",
-       "search-redirect": "(পুনর্নিদেশনা $1)",
+       "search-redirect": "($1 থেকে পুনর্নির্দেশিত)",
        "search-section": "(অনুচ্ছেদ $1)",
        "search-category": "(বিষয়শ্রেণী $1)",
        "search-file-match": "(নথির বিষয়বস্তু মিলে যায়)",
        "right-changetags": "নির্দিষ্ট সংস্করণ এবং দীর্ঘ সম্পাদনাগুলোতে [[Special:Tags|ট্যাগ]] সংযোজন ও অপসারণ করুন",
        "right-deletechangetags": "ডাটাবেজ থেকে [[Special:Tags|ট্যাগ]] অপসারণ করা",
        "grant-group-email": "ইমেইল পাঠান",
+       "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-privateinfo": "ব্যক্তিগত তথ্যে প্রবেশাধিকার",
        "grant-sendemail": "অন্য ব্যবহারকারীকে ইমেইল পাঠান",
        "upload-dialog-disabled": "এই ডায়ালগ ব্যবহার করে ফাইল আপলোড করা এই উইকিতে নিষ্ক্রিয় করা হয়েছে।",
        "upload-dialog-title": "ফাইল আপলোড করুন",
        "upload-dialog-button-cancel": "বাতিল",
+       "upload-dialog-button-back": "পিছনে",
        "upload-dialog-button-done": "সম্পন্ন",
        "upload-dialog-button-save": "সংরক্ষণ",
        "upload-dialog-button-upload": "আপলোড",
        "apisandbox-dynamic-parameters": "অতিরিক্ত প্যারামিটার",
        "apisandbox-dynamic-parameters-add-label": "প্যারামিটার যোগ করুন:",
        "apisandbox-dynamic-parameters-add-placeholder": "প্যারামিটারের নাম",
+       "apisandbox-dynamic-error-exists": "\"$1\" নামক একটি প্যারামিটার আগে থেকেই বিদ্যমান।",
        "apisandbox-results": "ফলাফল",
        "apisandbox-sending-request": "API অনুরোধ পাঠানো হচ্ছে...",
        "apisandbox-loading-results": "API ফলাফল গ্রহণ করা হচ্ছে...",
        "tags-delete-reason": "কারণ:",
        "tags-delete-submit": "অপরিবর্তনীয় এই ট্যাগ অপসারন করো",
        "tags-delete-not-found": "\"$1\" ট্যাগ বিদ্যমান নয়।",
+       "tags-delete-no-permission": "আপনার পরিবর্তন ট্যাগ মুছে ফেলার অনুমতি নেই।",
        "tags-activate-title": "সক্রিয় ট্যাগ",
        "tags-activate-question": "আপনি ট্যাগ \"$1\" সক্রিয় করতে চলেছেন।",
        "tags-activate-reason": "কারণ:",
        "htmlform-cloner-create": "আরও যোগ করুন",
        "htmlform-cloner-delete": "অপসারণ",
        "htmlform-cloner-required": "অন্তত একটি মূল্য আবশ্যক।",
+       "htmlform-date-placeholder": "বববব-মম-দদ",
+       "htmlform-time-placeholder": "ঘঘ:মম:সস",
+       "htmlform-datetime-placeholder": "বববব-মম-দদ ঘঘ:মম:সস",
        "htmlform-title-badnamespace": "[[:$1]] \"{{ns:$2}}\" নামস্থানে খুঁজে পাওয়া যায়নি।",
        "htmlform-title-not-creatable": "\"$1\" সৃষ্টিযোগ্য পাতার শিরোনাম নয়",
        "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 পাতাটি অপসারিত হয়েছে",
        "logentry-delete-restore": "$1 কর্তৃক $3 পাতাটি {{GENDER:$2|ফিরিয়ে আনা}} হয়েছে",
        "logentry-delete-event": "$1 {{PLURAL:$5|একটি লগ ইভেন্টের|$5 লগ ইভেন্টসমূহের}} দৃশ্যমানতা {{GENDER:$2|পরিবর্তন}} করেছেন $3: $4",
        "feedback-external-bug-report-button": "প্রযুক্তিগত কাজ ফাইল করুন",
        "feedback-dialog-title": "প্রতিক্রিয়া জমা দিন",
        "feedback-dialog-intro": "আপনি আপনার প্রতিক্রিয়া জানাতে নীচের সহজ ফরম ব্যবহার করতে পারেন। আপনার মন্তব্য আপনার ব্যবহারকারী নামসহ, \"$1\" পাতায় যোগ করা হবে।",
-       "feedback-error-title": "ত্রুটি",
        "feedback-error1": "ত্রুটি: এপিআই হতে অজানা ফলাফল এসেছে",
        "feedback-error2": "ত্রুটি: সম্পাদনা ব্যর্থ",
        "feedback-error3": "ত্রুটি: এপিআই হতে কোন সাড়া নেই",
        "log-action-filter-upload-upload": "নতুন আপলোড",
        "log-action-filter-upload-overwrite": "পুনঃআপলোড",
        "authmanager-authn-no-primary": "সরবরাহকৃত পরিচয়পত্রের অনুমোদন যাচাই করা যায়নি।",
+       "authmanager-authn-autocreate-failed": "একটি স্থানীয় অ্যাকাউন্টের স্বয়ংক্রিয়-সৃষ্টি ব্যর্থ হয়েছে: $1",
        "authmanager-create-disabled": "অ্যাকাউন্ট সৃষ্টিকরণ নিষ্ক্রিয় করা হয়েছে।",
        "authmanager-create-from-login": "আপনার একাউন্ট তৈরি করতে, নীচের ক্ষেত্রগুলি পূরণ করুন।",
        "authmanager-authplugin-setpass-failed-title": "পাসওয়ার্ড পরিবর্তন ব্যর্থ হয়েছে",
        "authmanager-authplugin-setpass-bad-domain": "অবৈধ ডোমেইন।",
        "authmanager-autocreate-noperm": "স্বয়ংক্রিয় অ্যাকাউন্ট সৃষ্টি মঞ্জুরিপ্রাপ্ত নয়।",
        "authmanager-userdoesnotexist": "ব্যবহারকারী অ্যাকাউন্ট \"$1\" অনিবন্ধিত।",
+       "authmanager-username-help": "প্রমাণীকরণের জন্য ব্যবহারকারী নাম।",
+       "authmanager-password-help": "প্রমাণীকরণের জন্য পাসওয়ার্ড।",
+       "authmanager-domain-help": "বহিঃস্থ প্রমাণীকরণের জন্য ডোমেইন।",
+       "authmanager-retype-help": "নিশ্চিত করার জন্য আবার পাসওয়ার্ড লিখুন।",
        "authmanager-email-label": "ইমেইল",
        "authmanager-email-help": "ইমেইল ঠিকানা",
        "authmanager-realname-label": "প্রকৃত নাম",
        "authmanager-realname-help": "ব্যবহারকারীর প্রকৃত নাম",
+       "authmanager-provider-password": "পাসওয়ার্ড-ভিত্তিক প্রমাণীকরণ।",
+       "authmanager-provider-password-domain": "পাসওয়ার্ড ও ডোমেইন-ভিত্তিক প্রমাণীকরণ।",
        "authmanager-provider-temporarypassword": "অস্থায়ী পাসওয়ার্ড",
        "authprovider-confirmlink-success-line": "$1: সংযোগ করা সফল হয়েছে।",
        "authprovider-resetpass-skip-label": "উপেক্ষা করো",
        "credentialsform-provider": "পরিচয়পত্রের ধরন:",
        "credentialsform-account": "অ্যাকাউন্টের নাম:",
        "linkaccounts": "অ্যাকাউন্ট সংযোগ করুন",
+       "linkaccounts-success-text": "অ্যাকাউন্টটি সংযোগ করা হয়েছে।",
        "linkaccounts-submit": "অ্যাকাউন্ট সংযুক্ত করুন",
        "unlinkaccounts": "অ্যাকাউন্ট সংযোগ বিচ্ছিন্ন করুন",
        "unlinkaccounts-success": "অ্যাকাউন্টের সংযোগ বিচ্ছিন্ন করা হয়েছে।",
+       "authenticationdatachange-ignored": "প্রমাণীকরণ উপাত্তের পরিবর্তন পরিচালনা করা হয়নি। হয়তো কোন প্রদানকারী কনফিগার করা হয়নি?",
        "userjsispublic": "অনুগ্রহ করে লক্ষ্য করুন: জাভাস্ক্রিপ্টের উপপাতাগুলিতে গোপনীয় তথ্য থাকা উচিত নয় যেহেতু অন্যান্য ব্যবহারকারীও এগুলি দেখতে পান।",
-       "usercssispublic": "অনুগ্রহ করে লক্ষ্য করুন: সিএসএসের উপপাতাগুলিতে গোপনীয় তথ্য থাকা উচিত নয় যেহেতু অন্যান্য ব্যবহারকারীও এগুলি দেখতে পান।"
+       "usercssispublic": "অনুগ্রহ করে লক্ষ্য করুন: সিএসএসের উপপাতাগুলিতে গোপনীয় তথ্য থাকা উচিত নয় যেহেতু অন্যান্য ব্যবহারকারীও এগুলি দেখতে পান।",
+       "restrictionsfield-badip": "আইপি ঠিকানা অথবা পরিসীমা অবৈধ: $1",
+       "restrictionsfield-label": "অনুমোদিত আইপি পরিসীমা:",
+       "restrictionsfield-help": "লাইন প্রতি একটি আইপি ঠিকানা বা CIDR পরিসীমা। সবকিছু সক্রিয় করতে<br><code>0.0.0.0/0</code><br><code>::/0</code><br>ব্যবহার করুন"
 }
index 5bea10a..c0c6e29 100644 (file)
        "htmlform-cloner-create": "Ouzhpennañ muioc'h",
        "htmlform-cloner-delete": "Dilemel",
        "htmlform-cloner-required": "Un dalvoudenn a zo ret da vihanañ.",
-       "sqlite-has-fts": "$1 gant enklask eus an destenn a-bezh embreget",
-       "sqlite-no-fts": "$1 hep enklask eus an destenn a-bezh embreget",
        "logentry-delete-delete": "Diverket eo bet ar bajenn $3 gant $1",
        "logentry-delete-restore": "Assavet eo bet ar bajenn $3 gant $1",
        "logentry-delete-event": "Kemmet eo bet gwelusted {{PLURAL:$5|un darvoud eus ar marilh|$5 darvoud eus ar marilh}} d'an $3 gant $1 : $4",
index 159f7e7..bd7643b 100644 (file)
@@ -51,7 +51,7 @@
        "tog-enotifminoredits": "Također mi pošalji e-poštu za male izmjene na stranicama i datotekama",
        "tog-enotifrevealaddr": "Otkrij adresu moje e-pošte u porukama obavještenja",
        "tog-shownumberswatching": "Prikaži broj korisnika koji prate",
-       "tog-oldsig": "Postojeći potpis:",
+       "tog-oldsig": "Vaš postojeći potpis:",
        "tog-fancysig": "Smatraj potpis kao wikitekst (bez automatskog linka)",
        "tog-uselivepreview": "Koristi pregled uživo",
        "tog-forceeditsummary": "Opomeni me pri unosu praznog sažetka",
@@ -68,7 +68,7 @@
        "tog-showhiddencats": "Prikaži skrivene kategorije",
        "tog-norollbackdiff": "Ne prikazuj razliku nakon izvršenog vraćanja",
        "tog-useeditwarning": "Upozori me kad napuštam stranicu za izmjene bez sačuvanih promjena",
-       "tog-prefershttps": "Uvijek koristi sigurnu konekciju kada sam prijavljen.",
+       "tog-prefershttps": "Uvijek koristi sigurnu vezu dok sam prijavljen",
        "underline-always": "Uvijek",
        "underline-never": "Nikad",
        "underline-default": "Prema predodređenim postavkama teme ili preglednika",
        "october-date": "$1. oktobar",
        "november-date": "$1. novembar",
        "december-date": "$1. decembar",
+       "period-am": "AM",
+       "period-pm": "PM",
        "pagecategories": "{{PLURAL:$1|Kategorija|Kategorije}}",
        "category_header": "Članci u kategoriji \"$1\"",
        "subcategories": "Podkategorije",
        "newwindow": "(otvara se u novom prozoru)",
        "cancel": "Odustani",
        "moredotdotdot": "Više...",
-       "morenotlisted": "Ovaj spisak nije potpun.",
+       "morenotlisted": "Ovaj spisak možda nije potpun.",
        "mypage": "Stranica",
        "mytalk": "Razgovor",
        "anontalk": "Razgovor",
        "talk": "Razgovor",
        "views": "Pregledi",
        "toolbox": "Alati",
+       "tool-link-userrights": "Promijeni {{GENDER:$1|korisničke}} grupe",
+       "tool-link-emailuser": "Pošalji e-poruku {{GENDER:$1|korisniku|korisnici}}",
        "userpage": "Pogledaj korisničku stranicu",
        "projectpage": "Pogledaj stranicu projekta",
        "imagepage": "Pogledajte stranicu datoteke",
        "confirmable-confirm": "Da li {{GENDER:$1|ste}} sigurni?",
        "confirmable-yes": "Da",
        "confirmable-no": "Ne",
-       "thisisdeleted": "Pogledati ili vratiti $1?",
+       "thisisdeleted": "Pogledaj ili vrati $1?",
        "viewdeleted": "Pogledaj $1?",
-       "restorelink": "{{PLURAL:$1|$1 izbrisana izmjena|$1 izbrisanih izmjena}}",
+       "restorelink": "{{PLURAL:$1|jednu obrisanu izmjenu|$1 obrisane izmjene|$1 obrisanih izmjena}}",
        "feedlinks": "Fid:",
        "feed-invalid": "Nedozvoljen tip potpisa",
        "feed-unavailable": "RSS izvori nisu dostupni",
        "laggedslavemode": "'''Upozorenje''': Stranica, možda, nije ažurirana.",
        "readonly": "Baza je zaključana",
        "enterlockreason": "Unesite razlog za zaključavanje, uključujući procjenu vremena otključavanja",
-       "readonlytext": "Baza je trenutno zaključana za nove unose i ostale izmjene, vjerovatno zbog rutinskog održavanja, posle čega će biti vraćena u uobičajeno stanje.\n\nAdministrator koji ju je zaključao je ponudio ovo objašnjenje: $1",
+       "readonlytext": "Baza podataka trenutno je zaključana za nove unose i druge izmjene, vjerovatno zbog rutinskog održavanja, nakon čega će biti vraćena u uobičajeno stanje.\n\nSistemski administrator koji ju je zaključao naveo je sljedeće objašnjenje: $1",
        "missing-article": "U bazi podataka nije pronađen tekst stranice tražen pod nazivom \"$1\" $2.\n\nDo ovoga dolazi kad se prati pomjeranje ili historija linka za stranicu koja je pobrisana.\n\n\nU slučaju da se ne radi o gore navedenom moguće je da ste pronašli grešku u programu.\nMolimo Vas da ovo prijavite [[Special:ListUsers/sysop|administratoru]] s navođenjem tačne adrese stranice.",
        "missingarticle-rev": "(revizija#: $1)",
        "missingarticle-diff": "(Razlika: $1, $2)",
        "userlogin-resetpassword-link": "Zaboravili ste lozinku?",
        "userlogin-helplink2": "Pomoć pri prijavljivanju",
        "userlogin-loggedin": "Već ste prijavljeni kao {{GENDER:$1|$1}}.\nKoristite donji obrazac da biste se prijavili kao drugi korisnik.",
+       "userlogin-reauth": "Morate se ponovo prijaviti da bismo potvrdili da ste zaista {{GENDER:$1|$1}}.",
        "userlogin-createanother": "Napravi još jedan račun",
        "createacct-emailrequired": "Adresa e-pošte",
        "createacct-emailoptional": "Adresa e-pošte (opcionalno)",
        "noname": "Niste izabrali ispravno korisničko ime.",
        "loginsuccesstitle": "Prijavljen",
        "loginsuccess": "'''Sad ste prijavljeni na {{SITENAME}} kao \"$1\".'''",
-       "nosuchuser": "Ne postoji korisnik s imenom \"$1\".\nKorisnička imena razlikuju velika i mala slova.\nProvjerite Vaš unos ili [[Special:CreateAccount|napravite novi korisnički račun]].",
+       "nosuchuser": "Ne postoji korisnik s imenom \"$1\".\nKorisnička imena razlikuju velika i mala slova.\nProvjerite jeste li ga tačno upisali ili [[Special:CreateAccount|otvorite novi račun]].",
        "nosuchusershort": "Ne postoji korisnik s imenom \"$1\".\nProvjerite jeste li dobro ukucali.",
        "nouserspecified": "Morate izabrati korisničko ime.",
        "login-userblocked": "Ovaj korisnik je blokiran. Prijava nije dopuštena.",
        "wrongpasswordempty": "Lozinka koju ste unijeli je bila prazna.\nMolimo Vas da pokušate ponovno.",
        "passwordtooshort": "Lozinka mora imati najmanje {{PLURAL:$1|1 znak|$1 znaka|$1 znakova}}.",
        "passwordtoolong": "Lozinke ne mogu biti duže od {{PLURAL:$1|jednog znaka|$1 znaka|$1 znakova}}.",
+       "passwordtoopopular": "Ovo je često korištena lozinka i ne može se koristiti. Molimo Vas da izaberete jaču lozinku.",
        "password-name-match": "Vaša lozinka mora biti različita od Vašeg korisničkog imena.",
        "password-login-forbidden": "Korištenje ovih korisničkih imena i šifara je zabranjeo.",
        "mailmypassword": "Poništi lozinku",
        "noemail": "Ne postoji adresa e-pošte za korisnika \"$1\".",
        "noemailcreate": "Morate da navedete validnu e-mail adresu",
        "passwordsent": "Nova lozinka poslana je na adresu e-pošte korisnika \"$1\".\nMolimo Vas da se prijavite nakon što je primite.",
-       "blocked-mailpassword": "Da bi se spriječila nedozvoljena akcija, Vašoj IP adresi je onemogućeno uređivanje stranica kao i mogućnost zahtijevanje nove lozinke.",
+       "blocked-mailpassword": "Vašoj IP-adresi onemogućeno je uređivanje. Da bi se spriječila zloupotreba, s nje nije moguće zahtijevati novu lozinku.",
        "eauthentsent": "Na navedenu adresu e-pošte poslana je poruka s potvrdom.\nPrije nego što pošaljemo daljnje poruke, pratite uputstva s e-pošte da biste potvrdili da je račun zaista Vaš.",
        "throttled-mailpassword": "Već Vam je poslana e-poruka za promjenu lozinke u {{PLURAL:$1|posljednjih sat vremena|posljednja $1 sata|posljednjih $1 sati}}.\nDa bi se spriječila zloupotreba, može se poslati samo jedna e-poruka za promjenu lozinke {{PLURAL:$1|svakih sat vremena|svaka $1 sata|svakih $1 sati}}.",
        "mailerror": "Greška pri slanju e-pošte: $1",
        "passwordreset-emailtext-ip": "Neko (vjerovatno Vi, s IP adrese $1) je zatražio podsjetnik Vaših detalja računa za {{SITENAME}} ($4). Sljedeći {{PLURAL:$3|račun korisnika je|računi korisnika su}} povezani s ovom e-mail adresom:\n\n$2\n\n{{PLURAL:$3|Ova privremena šifra|Ove privremene šifre}} će isteći za {{PLURAL:$5|jedan dan|$5 dana}}.\nTrebate se prijaviti i odabrati novu šifru. Ako je neko drugi napravio ovaj zahtjev, ili ako ste se sjetili Vaše početne šifre, a ne želite je promijeniti, možete zanemariti ovu poruku i nastaviti koristiti staru šifru.",
        "passwordreset-emailtext-user": "Korisnik $1 na {{SITENAME}} je zatražio podsjetnik o detaljima Vašeg računa za {{SITENAME}} ($4). Sljedeći {{PLURAL:$3|korisnički račun je|korisnički računi su}} povezani s ovom e-mail adresom:\n\n$2\n\n{{PLURAL:$3|Ova privremena šifra|Ove privremene šifre}} će isteći za {{PLURAL:$5|jedan dan|$5 dana}}.\nTrebate se prijaviti i odabrati novu šifru. Ako je neko drugi napravio ovaj zahtjev, ili ako ste se sjetili Vaše originalne šifre, a ne želite je više promijeniti, možete zanemariti ovu poruku i nastaviti koristiti staru šifru.",
        "passwordreset-emailelement": "Korisničko ime: \n$1\n\nPrivremena šifra: \n$2",
-       "passwordreset-emailsentemail": "Ako je ovo adresa e-pošte s kojom ste registrirali ovaj račun, podsjetnik šifre će vam biti poslan na vašu adresu e-pošte.",
+       "passwordreset-emailsentemail": "Ako je ova adresa e-pošte povezana s Vašim računom, podsjetnik o lozinci bit će Vam poslan na adresu e-pošte.",
        "changeemail": "Promjena ili uklanjanje e-adrese",
        "changeemail-header": "Ispunite sljedeći formular da biste promijenili adresu e-pošte. Ako želite ukloniti postojeću adresu e-pošte s vašeg korisničkog računa, pri ispunjavanju formulara, polje nove adrese e-pošte ostavite prazno.",
        "changeemail-no-info": "Morate biti prijavljeni za direktan pristup ovoj stranici.",
        "accmailtext": "Nasumično odabrana šifra za [[User talk:$1|$1]] je poslata na adresu $2.\n\nŠifra/lozinka za ovaj novi račun može biti promijenjena na stranici ''[[Special:ChangePassword|izmjene šifre]]'' nakon prijave.",
        "newarticle": "(Novi)",
        "newarticletext": "Došli ste na stranicu koja još nema sadržaja.\n*Ako želite unijeti sadržaj, počnite tipkati u prozor ispod ovog teksta.\n*Ako Vam treba pomoć, idite na [$1 stranicu za pomoć].\n*Ako ste ovamo dospjeli slučajno, kliknite na dugme \"Nazad\" (''Back'') u Vašem internetskom pregledniku.",
-       "anontalkpagetext": "----''Ovo je stranica za razgovor za anonimnog korisnika koji još nije napravio nalog ili ga ne koristi.\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo identifikovali njega ili nju.\nTakvu adresu može dijeliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene nebitne primjedbe, molimo Vas da [[Special:CreateAccount|napravite nalog]] ili se [[Special:UserLogin|prijavite]] da biste izbjegli buduću zabunu sa ostalim anonimnim korisnicima.''",
+       "anontalkpagetext": "----\n<em>Ovo je stranica za razgovor s anonimnim korisnikom koji još nije napravio račun ili ga ne koristi.</em>\nZbog toga moramo koristiti brojčanu IP-adresu kako bismo ga prepoznali.\nTakvu adresu može dijeliti više korisnika.\nAko ste anonimni korisnik i smatrate da su Vam upućene nebitne primjedbe, molimo Vas da [[Special:CreateAccount|napravite račun]] ili se [[Special:UserLogin|prijavite]] da biste izbjegli buduću zabunu s ostalim anonimnim korisnicima.",
        "noarticletext": "Na ovoj stranici trenutno nema teksta.\nMožete [[Special:Search/{{PAGENAME}}|tražiti naslov ove stranice]] na drugim stranicama,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tražiti u povezanim zapisnicima] ili [{{fullurl:{{FULLPAGENAME}}|action=edit}} napraviti ovu stranicu]</span>.",
        "noarticletext-nopermission": "Trenutno nema teksta na ovoj stranici.\nMožete [[Special:Search/{{PAGENAME}}|tražiti ovaj naslov stranice]] na drugim stranicama ili <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti povezane zapisnike]</span>, ali nemate dozvolu da napravite ovu stranicu.",
        "missing-revision": "Uređivanje broj $1 na stranici \"{{FULLPAGENAME}}\" ne postoji.\n\nOvo se obično dešava kad pratite zastarjelu vezu na stranicu koja je obrisana.\nViše informacija možete pronaći u [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} protokolu brisanja].",
        "previewnote": "<strong>Ne zaboravite da je ovo samo pregled.</strong>\nVaše izmjene još nisu sačuvane!",
        "continue-editing": "Idi na područje uređivanja",
        "previewconflict": "Ovaj pregled prikazuje kako će tekst u gornjem polju\nizgledati ako kliknete \"Sačuvaj članak\".",
-       "session_fail_preview": "<strong>Izvinjavamo se! Nismo mogli obraditi vašu izmjenu zbog gubitka podataka o prijavi.\nMolimo pokušajte ponovno.\nAko i dalje ne bude radilo, pokušajte se [[Special:UserLogout|odjaviti]] i ponovno prijaviti.</strong>",
+       "session_fail_preview": "Izvinjavamo se! Nismo mogli obraditi Vašu izmjenu zbog gubitka podataka o prijavi.\n\nMožda ste odjavljeni. <strong>Provjerite jeste li prijavljeni i pokušajte ponovo</strong>.\nAko i dalje ne radi, pokušajte se [[Special:UserLogout|odjaviti]] i ponovo prijaviti i provjerite dozvoljava li Vaš preglednik kolačiće s ovog sajta.",
        "session_fail_preview_html": "'''Žao nam je! Nismo mogli da obradimo vašu izmjenu zbog gubitka podataka.'''\n\n''Zbog toga što {{SITENAME}} ima omogućen izvorni HTML, predpregled je sakriven kao predostrožnost protiv JavaScript napada.''\n\n'''Ako ste pokušali da napravite pravu izmjenu, molimo pokušajte ponovo. Ako i dalje ne radi, pokušajte da se [[Special:UserLogout|odjavite]] i ponovo prijavite.'''",
        "token_suffix_mismatch": "'''Vaša izmjena nije prihvaćena jer je Vaš web preglednik ubacio znakove interpunkcije u token uređivanja.\nIzmjena je odbačena da bi se spriječilo uništavanje teksta stranice.\nTo se događa ponekad kad korisite problematični anonimni proxy koji je baziran na web-u.'''",
        "edit_form_incomplete": "'''Neki dijelovi uređivačkog obrasca nisu došli do servera; dvaput provjerite da su vaše izmjene nepromjenjene i pokušajte ponovno.'''",
        "nohistory": "Ne postoji historija izmjena za ovu stranicu.",
        "currentrev": "Trenutna verzija",
        "currentrev-asof": "Trenutna verzija na dan $2 u $3",
-       "revisionasof": "Verzija od $1",
+       "revisionasof": "Verzija na dan $2 u $3",
        "revision-info": "Izmjena od $1 od {{GENDER:$6|$2}}$7",
        "previousrevision": "← Starija izmjena",
        "nextrevision": "Novija izmjena →",
        "revdelete-submit": "Primijeni na odabrane {{PLURAL:$1|reviziju|revizije}}",
        "revdelete-success": "'''Vidljivost izmjene je ažurirana.'''",
        "revdelete-failure": "'''Vidljivost revizije nije mogla biti ažurirana:'''\n$1",
-       "logdelete-success": "'''Vidljivost evidencije uspješno postavljena.'''",
+       "logdelete-success": "Postavljena je vidljivost unosa u zapisniku.",
        "logdelete-failure": "'''Zapisnik vidljivosti nije mogao biti postavljen:'''\n$1",
        "revdel-restore": "Promijeni dostupnost",
        "pagehist": "Historija stranice",
        "difference-multipage": "(Razlika između stranica)",
        "lineno": "Red $1:",
        "compareselectedversions": "Uporedi označene verzije",
-       "showhideselectedversions": "Pokaži/sakrij odabrane verzije",
+       "showhideselectedversions": "Prikaži/sakrij izabrane izmjene",
        "editundo": "poništi",
        "diff-empty": "(Nema razlike)",
        "diff-multi-sameuser": "({{PLURAL:$1|Nije prikazana jedna međurevizija|Nisu prikazane $1 međurevizije}} istog korisnika)",
        "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",
        "editusergroup": "Uredi {{GENDER:$1|korisničke}} grupe",
        "editinguser": "Mijenjate korisnička prava korisnika <strong>[[User:$1|$1]]</strong> $2",
        "userrights-editusergroup": "Uredi korisničke grupe",
-       "saveusergroups": "Sačuvaj korisničke grupe",
+       "saveusergroups": "Sačuvaj {{GENDER:$1|korisničke}} grupe",
        "userrights-groupsmember": "Član:",
        "userrights-groupsmember-auto": "Uključeni član od:",
        "userrights-groups-help": "Možete promijeniti grupe kojima ovaj korisnik pripada:\n* Označeni kvadratić znači da je korisnik u toj grupi.\n* Neoznačen kvadratić znači da korisnik nije u toj grupi.\n* Oznaka * (zvjezdica) označava da Vi ne možete izbrisati ovu grupu ako je dodate i obrnutno.",
        "right-override-export-depth": "Izvoz stranica uključujući povezane stranice do dubine od 5 linkova",
        "right-sendemail": "Slanje e-maila drugim korisnicima",
        "right-passwordreset": "Pogledaj e-mailove za obnavljanje šifre",
-       "right-managechangetags": "Napravi i briši [[Special:Tags|oznake]] iz baze podataka",
+       "right-managechangetags": "Napravi i (de)aktiviraj [[Special:Tags|oznake]]",
        "right-applychangetags": "Primijeni [[Special:Tags|oznake]] na nečije izmjene",
        "right-changetags": "Dodavanje ili uklanjanje raznih [[Special:Tags|oznaka]] na pojedinačnim verzijama i unosima zapisnika",
        "grant-group-page-interaction": "Upravljanje stranicama",
        "uploadstash-clear": "Očisti sakrivene datoteke",
        "uploadstash-nofiles": "Nemate sakrivenih datoteka.",
        "uploadstash-badtoken": "Izvršavanje ove akcije je bilo neuspješno, možda zato što su vaša uređivačka odobrenja istekla. Pokušajte ponovo.",
-       "uploadstash-errclear": "Brisanje sakrivenih datoteka je bilo neuspješno.",
+       "uploadstash-errclear": "Brisanje datoteka nije uspjelo.",
        "uploadstash-refresh": "Osvježi spisak datoteka",
        "invalid-chunk-offset": "Neispravna polazna tačka",
        "img-auth-accessdenied": "Pristup onemogućen",
        "log-title-wildcard": "Traži naslove koji počinju ovim tekstom",
        "showhideselectedlogentries": "Pokaži/sakrij izabrane zapise u evidenciji",
        "log-edit-tags": "Uredi oznake izabranih zapisničkih unosa",
+       "checkbox-select": "Izaberi: $1",
+       "checkbox-all": "Sve",
+       "checkbox-none": "Ništa",
+       "checkbox-invert": "Obrni",
        "allpages": "Sve stranice",
        "nextpage": "Sljedeća stranica ($1)",
        "prevpage": "Prethodna stranica ($1)",
        "watchlistanontext": "Morate biti prijavljeni kako biste vidjeli ili uređivali svoj spisak praćenih članaka.",
        "watchnologin": "Niste prijavljeni",
        "addwatch": "Dodaj na spisak praćenja",
-       "addedwatchtext": "Stranica \"[[:$1]]\" i njena stranica za razgovor dodani su na vaš [[Special:Watchlist|spisak praćenja]].",
+       "addedwatchtext": "Stranica \"[[:$1]]\" i njena stranica za razgovor dodani su na Vaš [[Special:Watchlist|spisak praćenja]].",
+       "addedwatchtext-talk": "\"[[:$1]]\" i njoj pridružena stranica dodane su na Vaš [[Special:Watchlist|spisak praćenja]].",
        "addedwatchtext-short": "Stranica \"$1\" je dodana na vaš spisak praćenja.",
        "removewatch": "Ukloni sa spiska praćenja",
-       "removedwatchtext": "Stranica \"[[:$1]]\" i njena stranica za razgovor uklonjeni su s [[Special:Watchlist|Vašeg spiska praćenja]].",
+       "removedwatchtext": "Stranica \"[[:$1]]\" i njena stranica za razgovor uklonjeni su s Vašeg [[Special:Watchlist|spiska praćenja]].",
+       "removedwatchtext-talk": "\"[[:$1]]\" i njoj pridružena stranica uklonjene su s Vašeg [[Special:Watchlist|spiska praćenja]].",
        "removedwatchtext-short": "Stranica \"$1\" je uklonjena sa vašeg spiska praćenja.",
        "watch": "Prati članak",
        "watchthispage": "Prati ovu stranicu",
        "enotif_body": "Poštovani $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nSažetak urednika: $PAGESUMMARY $PAGEMINOREDIT\n\nKontaktirajte urednika:\ne-pošta: $PAGEEDITOR_EMAIL\nwiki: $PAGEEDITOR_WIKI\n\nNeće biti drugih obavještenja u slučaju daljnjih izmjena osim ako prijavljeni ponovno posjetite stranicu. Također možete poništiti oznake obavijesti za sve praćene stranice koje imate na vašem spisku praćenja.\n\nVaš prijateljski {{SITENAME}} sistem obavještavanja\n\n--\nZa promjenu vaših postavki email obavijesti, posjetite\n{{canonicalurl:{{#special:Preferences}}}}\n\nZa promjenu postavki vašeg praćenja, posjetite\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nDa obrišete stranicu sa vašeg spiska praćenja, posjetite\n$UNWATCHURL\n\nPovratne informacije i daljnja pomoć:\n$HELPPAGE",
        "created": "napravljena",
        "changed": "promijenjena",
-       "deletepage": "Obrišite stranicu",
+       "deletepage": "Obriši stranicu",
        "confirm": "Potvrdite",
        "excontent": "sadržaj je bio: \"$1\"",
        "excontentauthor": "sadržaj je bio: \"$1\", a jedini urednik \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|razgovor]])",
        "exbeforeblank": "sadržaj prije brisanja je bio: \"$1\"",
        "delete-confirm": "Brisanje \"$1\"",
        "delete-legend": "Obriši",
-       "historywarning": "<strong>Upozorenje</strong>: Stranica koju želite da obrišete ima historiju sa otprilike $1 {{PLURAL:$1|revizijom|revizije|revizija}}:",
+       "historywarning": "<strong>Upozorenje:</strong> Stranica koju želite obrisati ima historiju sa $1 {{PLURAL:$1|izmjenom|izmjene|izmjena}}:",
        "historyaction-submit": "Prikaži",
-       "confirmdeletetext": "Brisanjem ćete obrisati stranicu ili sliku zajedno sa historijom iz baze podataka, ali će se iste moći vratiti kasnije.\nMolim potvrdite svoju namjeru, da razumijete posljedice i da ovo radite u skladu sa [[{{MediaWiki:Policy-url}}|pravilima]].",
+       "confirmdeletetext": "Obrisat ćete stranicu i njenu historiju.\nPotvrdite svoju namjeru, da razumijete posljedice i da ovo radite u skladu s [[{{MediaWiki:Policy-url}}|pravilima]].",
        "actioncomplete": "Radnja je izvršena",
        "actionfailed": "Akcija nije uspjela",
        "deletedtext": "Stranica \"$1\" je obrisana.\nPogledajte $2 za zapisnik nedavnih brisanja.",
        "restriction-level-all": "svi nivoi",
        "undelete": "Pregled obrisanih stranica",
        "undeletepage": "Pregled i vraćanje obrisanih stranica",
-       "undeletepagetitle": "'''Sljedeći sadržaj prikazuje obrisane revizije od [[:$1|$1]]'''.",
+       "undeletepagetitle": "<strong>Sljedeći sadržaj prikazuje obrisane izmjene stranice [[:$1|$1]]<strong>.",
        "viewdeletedpage": "Pregled obrisanih stranica",
        "undeletepagetext": "{{PLURAL:$1|Slijedeća $1 stranica je obrisana|Slijedeće $1 stranice su obrisane|Slijedećih $1 je obrisano}} ali su još uvijek u arhivi i mogu biti vraćene.\nArhiva moše biti periodično čišćena.",
-       "undelete-fieldset-title": "Vraćanje revizija",
-       "undeleteextrahelp": "Da biste vratili cijelu historiju stranice, ostavite sve kućice neoznačene i kliknite na <strong><em>{{int:undeletebtn}}</em></strong>.\nDa biste vratili određene stranice, izaberite verzije koje želite vratiti i kliknite na <strong><em>{{int:undeletebtn}}</em></strong>.",
+       "undelete-fieldset-title": "Vraćanje izmjena",
+       "undeleteextrahelp": "Da biste vratili cijelu historiju stranice, ostavite sve kućice neoznačene i kliknite na <strong><em>{{int:undeletebtn}}</em></strong>.\nDa biste vratili određene izmjene, označite ih i kliknite na <strong><em>{{int:undeletebtn}}</em></strong>.",
        "undeleterevisions": "$1 {{PLURAL:$1|izmjena je obrisana|izmjena je obrisano}}",
-       "undeletehistory": "Ako vratite stranicu, sve će verzije biti vraćene u njenu historiju.\nAko je u međuvremenu napravljena nova verzija s istim nazivom, vraćene verzije će se pojaviti njenoj ranijoj historiji.",
-       "undeleterevdel": "Vraćanje obrisanog se neće izvršiti ako bi rezultiralo da zaglavlje stranice ili revizija datoteke bude djelimično obrisano.\nU takvim slučajevima, morate ukloniti označene ili otkriti sakrivene najskorije obrisane revizije.",
+       "undeletehistory": "Ako vratite stranicu, sve će izmjene biti vraćene u njenu historiju.\nAko je u međuvremenu napravljena nova izmjena s istim nazivom, vraćene izmjene pojavit će se u njenoj ranijoj historiji.",
+       "undeleterevdel": "Vraćanje neće biti izvršeno ako je rezultat toga djelomično brisanje posljednje izmjene.\nU takvim slučajevima morate isključiti ili otkriti najnoviju obrisanu izmjenu.",
        "undeletehistorynoadmin": "Ova stranica je obrisana.\nRazlog za brisanje se nalazi ispod, zajedno s detaljima o korisniku koji je mijenjao stranicu prije brisanja.\nTekst obrisanih verzija dostupan je samo administratorima.",
        "undelete-revision": "Obrisana izmjena stranice $1 (dana $4, u $5) koju je {{GENDER:$3|napravio|napravila}} $3:",
        "undeleterevision-missing": "Nepoznata ili nedostajuća revizija.\nMožda ste unijeli pogrešan link, ili je revizija vraćena ili uklonjena iz arhive.",
        "undeletebtn": "Vrati",
        "undeletelink": "pogledaj/vrati",
        "undeleteviewlink": "pogledaj",
-       "undeleteinvert": "Izmijeni odabir",
+       "undeleteinvert": "Obrni izbor",
        "undeletecomment": "Razlog:",
        "undeletedrevisions": "{{PLURAL:$1|vraćena $1 verzija|vraćene $1 verzije|vraćeno $1 verzija}}",
        "undeletedrevisions-files": "{{PLURAL:$1|1 verzija|$1 verzije|$1 verzija}} i {{PLURAL:$2|1 datoteka|$2 datoteke|$2 datoteka}} vraćeno",
        "undeletedfiles": "{{PLURAL:$1|1 datoteka vraćena|$1 datoteke vraćene|$1 datoteka vraćeno}}",
-       "cannotundelete": "Vraćanje nije uspjelo:\n$1",
+       "cannotundelete": "Vraćanje jedne ili svih stavki nije uspjelo:\n$1",
        "undeletedpage": "'''$1 je vraćena'''\n\nProvjerite [[Special:Log/delete|zapis brisanja]] za zapise najskorijih brisanja i vraćanja.",
        "undelete-header": "Pogledajte [[Special:Log/delete|zapisnik brisanja]] za nedavno obrisane stranice.",
        "undelete-search-title": "Pretraga obrisanih stranica",
        "sp-contributions-newbies-sub": "Za nove korisnike",
        "sp-contributions-newbies-title": "Doprinosi novih korisnika",
        "sp-contributions-blocklog": "zapisnik blokiranja",
-       "sp-contributions-suppresslog": "obrisani doprinosi korisnika",
-       "sp-contributions-deleted": "obrisani doprinosi korisnika",
+       "sp-contributions-suppresslog": "obrisani {{GENDER:$1|korisnički}} doprinosi",
+       "sp-contributions-deleted": "obrisani {{GENDER:$1|korisnički}} doprinosi",
        "sp-contributions-uploads": "postavljanja",
        "sp-contributions-logs": "zapisnici",
        "sp-contributions-talk": "razgovor",
        "whatlinkshere-hideredirs": "$1 preusmjerenja",
        "whatlinkshere-hidetrans": "$1 uključenja",
        "whatlinkshere-hidelinks": "$1 linkove",
-       "whatlinkshere-hideimages": "Veze do datoteke $1",
+       "whatlinkshere-hideimages": "$1 linkova do datoteke",
        "whatlinkshere-filters": "Filteri",
        "autoblockid": "Automatska blokada #$1",
        "block": "Blokiraj korisnika",
        "move-page": "Premjesti $1",
        "move-page-legend": "Premjesti stranicu",
        "movepagetext": "Korištenjem ovog formulara možete preimenovati stranicu, premještajući cijelu historiju na novo ime.\nČlanak pod starim imenom postat će stranica koja preusmjerava na članak pod novim imenom. \nMožete automatski izmijeniti preusmjerenje do izvornog naslova.\nAko se ne odlučite na to, provjerite [[Special:DoubleRedirects|dvostruka]] ili [[Special:BrokenRedirects|neispravna preusmjeravanja]].\nDužni ste provjeriti da svi linkovi i dalje nastave voditi na prave stranice.\n\nImajte na umu da članak '''neće''' biti premješten ako već postoji članak pod imenom na koje ga namjeravate preusmjeriti osim u slučaju stranice za preusmjeravanje koja nema nikakvih starih izmjena.\nTo znači da možete vratiti stranicu na prethodno mjesto ako pogriješite, ali ne možete zamijeniti postojeću stranicu.\n\n'''Pažnja!'''\nOvo može biti drastična i neočekivana promjena kad su u pitanju popularne stranice.\nMolimo da dobro razmislite prije no što premjestite stranicu.",
-       "movepagetext-noredirectfixer": "Koristeći donji obrazac, preimenovat ćete stranicu i premjestiti cijelu njenu historiju na novi naziv.\nStari naziv postat će preusmjerenje na novi naziv.\nMolimo da provjerite postoje li [[Special:DoubleRedirects|dvostruka]] ili [[Special:BrokenRedirects|nedovršena preusmjerenja]].\nVi ste za to odgovorni te morate provjeriti jesu li linkovi ispravni i vode li tamo kamo bi trebali voditi.\n\nImajte na umu da stranica '''neće''' biti premještena ako već postoji stranica s tim imenom, osim ako je prazna ili je preusmjerenje ili nema ranije historije.\nOvo znači da možete preimenovati stranicu nazad gdje je ranije bila preimenovana ako ste pogriješili, ali ne možete ponovo preimenovati postojeću stranicu.\n\n'''Pažnja!'''\nImajte na umu da premještanje popularnog članka može biti\ndrastična i neočekivana promjena za korisnike; molimo da budete sigurni da ste shvatili posljedice prije no što nastavite.",
+       "movepagetext-noredirectfixer": "Koristeći donji obrazac, preimenovat ćete stranicu i premjestiti cijelu njenu historiju na novi naziv.\nStari naziv postat će preusmjerenje na novi naziv.\nMolimo da provjerite postoje li [[Special:DoubleRedirects|dvostruka]] ili [[Special:BrokenRedirects|nedovršena preusmjerenja]].\nVi ste za to odgovorni te morate provjeriti jesu li linkovi ispravni i vode li tamo kamo bi trebali voditi.\n\nImajte na umu da stranica '''neće''' biti premještena ako već postoji stranica s tim imenom, osim ako je prazna ili je preusmjerenje ili nema ranije historije.\nOvo znači da možete preimenovati stranicu nazad gdje je ranije bila preimenovana ako ste pogriješili, ali ne možete ponovo preimenovati postojeću stranicu.\n\n<strong>Napomena:</strong>\nImajte na umu da premještanje popularnog članka može biti\ndrastična i neočekivana promjena za korisnike; molimo da budete sigurni da ste shvatili posljedice prije no što nastavite.",
        "movepagetalktext": "Ako označite ovu kutijicu, odgovarajuća stranica za razgovor, ako postoji, automatski će biti premještena na novi naziv, osim ako već postoji sadržaj na odredišnoj stranici za razgovor.\n\nU tom slučaju, morat ćete ručno premjestiti ili prekopirati stranicu ako to želite.",
-       "moveuserpage-warning": "'''Upozorenje:''' Premještate korisničku stranicu. Molimo da zapamtite da će se samo stranica premjestiti a korisnik se ''neće'' preimenovati.",
+       "moveuserpage-warning": "<strong>Upozorenje:</strong> Premještate korisničku stranicu. Imajte u vidu da će samo stranica biti premještena, a sam korisnik <em>neće</em> biti preimenovan.",
        "movecategorypage-warning": "<strong>Upozorenje:</strong> Premještate stranicu kategorije. Imajte na umu da će samo stranica biti premještena i da sve stranice u staroj kategoriji <em>neće</em> biti ponovo kategorirane u novu kategoriju.",
        "movenologintext": "Morate biti registrovani korisnik i [[Special:UserLogin|prijavljeni]] da biste premjestili stranicu.",
        "movenotallowed": "Nemate dopuštenje za premještanje stranica.",
        "cant-move-category-page": "Nemate dopuštene da premještate stranice kategorija.",
        "cant-move-to-category-page": "Nemate dopuštenje da premjestite stranicu na stranicu kategorije.",
        "newtitle": "Novi naslov:",
-       "move-watch": "Prati ovu stranicu",
+       "move-watch": "Prati izvornu i odredišnu stranicu",
        "movepagebtn": "Premjesti stranicu",
        "pagemovedsub": "Premještanje uspjelo",
        "movepage-moved": "'''\"$1\" je premještena na \"$2\"'''",
        "movepage-moved-noredirect": "Pravljenje preusmjerenja je onemogućeno.",
        "articleexists": "Stranica pod tim imenom već postoji ili je ime koje ste izabrali neispravno. Molimo Vas da izaberete drugo ime.",
        "cantmove-titleprotected": "Ne možete premjestiti stranicu na ovu lokaciju jer je novi naslov zaštićen od pravljenja.",
-       "movetalk": "Premjestite i stranicu za razgovor ako je moguće.",
+       "movetalk": "Premjesti i stranicu za razgovor",
        "move-subpages": "Premjesti sve podstranice (do $1)",
        "move-talk-subpages": "Premjesti podstranice stranica za razgovor (do $1)",
        "movepage-page-exists": "Stranica $1 već postoji i ne može biti automatski zamijenjena.",
        "revertmove": "vrati",
        "delete_and_move_text": "==Potebno brisanje==\nOdredišna stranica \"[[:$1]]\" već postoji.\nDa li je želite obrisati kako bi ste mogli izvršiti premještanje?",
        "delete_and_move_confirm": "Da, obriši stranicu",
-       "delete_and_move_reason": "Obrisano da bi se napravio prostor za premještanje iz \"[[$1]]\"",
+       "delete_and_move_reason": "Obrisano da se oslobodi mjesto za premještanje iz \"[[$1]]\"",
        "selfmove": "Izvorni i ciljani naziv su isti; strana ne može da se premjesti preko same sebe.",
        "immobile-source-namespace": "Ne mogu premjestiti stranice u imenski prostor \"$1\"",
        "immobile-target-namespace": "Ne mogu se premjestiti stranice u imenski prostor \"$1\"",
        "tags-actions-header": "Radnje",
        "tags-active-yes": "Da",
        "tags-active-no": "Ne",
-       "tags-source-extension": "Definirano preko proširenja",
+       "tags-source-extension": "Definirano softverom",
        "tags-source-manual": "Ručno postavili korisnici ili botovi",
        "tags-source-none": "Više se ne koristi",
        "tags-edit": "uređivanje",
        "htmlform-title-not-exists": "$1 ne postoji.",
        "htmlform-user-not-exists": "<strong>$1</strong> ne postoji.",
        "htmlform-user-not-valid": "<strong>$1</strong> nije ispravno korisničko ime.",
-       "sqlite-has-fts": "$1 sa podrškom pretrage cijelog teksta",
-       "sqlite-no-fts": "$1 bez podrške pretrage cijelog teksta",
        "logentry-delete-delete": "$1 {{GENDER:$2|obrisao|obrisala}} je stranicu $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|vratio|vratila}} je stranicu $3",
        "logentry-delete-event": "$1 je {{GENDER:$2|promijenio|promijenila}} vidljivost {{PLURAL:$5|događaja|$5 događaja}} u evidenciji na $3: $4",
        "feedback-external-bug-report-button": "Podnesi tehnički zadatak",
        "feedback-dialog-title": "Pošalji povratne informacije",
        "feedback-dialog-intro": "Možete koristiti jednostavni formular ispod kako biste poslali povratne informacije. Vaš komentar će biti dodan stranici \"$1\" zajedno s vašim korisničkim imenom.",
-       "feedback-error-title": "Greška",
        "feedback-error1": "Greška: Neprepoznati rezultat od API",
        "feedback-error2": "Greška: Uređivanje nije uspjelo",
        "feedback-error3": "Greška: Nema odgovora od API",
index 9e9e6b6..1c3ddf7 100644 (file)
        "databaseerror-query": "Consulta: $1",
        "databaseerror-function": "Funció: $1",
        "databaseerror-error": "Error:$1",
+       "transaction-duration-limit-exceeded": "Per evitar una alta demora de resposta, s'ha interromput aquesta transacció perquè la durada d'escriptura ($1) ha sobrepassat el límit de $2 segons.\nSi esteu canviant molts elements alhora, intenteu fer-ho amb diverses operacions més petites.",
        "laggedslavemode": "Avís: La pàgina podria mancar de modificacions recents.",
        "readonly": "La base de dades està bloquejada",
        "enterlockreason": "Escriviu una raó pel bloqueig, així com una estimació de quan tindrà lloc el desbloqueig",
        "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?",
        "htmlform-title-not-exists": "$1 no existeix.",
        "htmlform-user-not-exists": "<strong>$1</strong> no existeix.",
        "htmlform-user-not-valid": "<strong>$1</strong> no és nom d'usuari vàlid.",
-       "sqlite-has-fts": "$1, amb suport de cerca de text íntegre",
-       "sqlite-no-fts": "$1, sense supor de cerca de text íntegre",
        "logentry-delete-delete": "$1 {{GENDER:$2|ha esborrat}} la pàgina $3",
        "logentry-delete-restore": "$1 ha restaurat $3",
        "logentry-delete-event": "$1 {{GENDER:$2|ha canviat}} la visibilitat {{PLURAL:$5|d'un esdeveniment al registre|de $5 esdeveniments al registre}} de $3: $4",
index 2a856d9..fafca4a 100644 (file)
                        "LNDDYL",
                        "唐吉訶德的侍從",
                        "Ztl8702",
-                       "Macofe"
+                       "Macofe",
+                       "GnuDoyng"
                ]
        },
-       "tog-underline": "下劃綫鏈接",
-       "tog-hideminor": "囥起最近改變其過幼修改",
-       "tog-hidepatrolled": "囥起最近改變其巡邏修改",
-       "tog-newpageshidepatrolled": "共巡邏視頁趁新建頁列表𡅏囥起去",
+       "tog-underline": "Â-hĕk-siáng lièng-giék",
+       "tog-hideminor": "Káung kī cī-bŏng gì guó-éu siŭ-gāi",
+       "tog-hidepatrolled": "Káung kī cī-bŏng ī giēng-că gì siŭ-gāi",
+       "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": "加添我開其頁面共我上傳其文件遘我其監視單",
@@ -37,7 +38,7 @@
        "tog-enotifminoredits": "就㑚講是過幼編輯,也着發電子郵件乞我",
        "tog-enotifrevealaddr": "敆通知郵件臺中顯示我其電子郵件地址",
        "tog-shownumberswatching": "顯示監視用戶其數量",
-       "tog-oldsig": "存在其簽名",
+       "tog-oldsig": "Nṳ̄ còng-câi gì chiĕng-miàng:",
        "tog-fancysig": "共簽名當成維基文本(無自動鏈接)",
        "tog-uselivepreview": "使即時預覽",
        "tog-forceeditsummary": "提醒我行遘蜀萆空白其編輯總結",
        "tog-watchlisthidebots": "囥起監視單其機器人其修改",
        "tog-watchlisthideminor": "囥起監視單其過幼修改",
        "tog-watchlisthideliu": "共已經登錄其用戶其編輯趁監視單𡅏囥起咯",
+       "tog-watchlistreloadautomatically": "Sìng-tō̤ dèu-giông gāi-biéng sèng-hâiu cê̤ṳ-dông gĕng-sĭng gáng-sê-dăng (JavaScript diŏh kŭi lā̤)",
        "tog-watchlisthideanons": "共匿名其用戶其編輯趁監視單𡅏囥起咯",
        "tog-watchlisthidepatrolled": "共巡查其編輯趁監視單𡅏囥起咯",
+       "tog-watchlisthidecategorization": "Káung kī hiĕk gì lôi-biék",
        "tog-ccmeonemails": "共我發乞其他用戶其電子郵件其備份發乞我。",
        "tog-diffonly": "伓使敆下底其顯示𣍐蜀様其地方顯示頁面內容",
        "tog-showhiddencats": "㪗藏類別",
-       "tog-norollbackdiff": "敆回滾其時候,無叕𣍐蜀様其地方",
+       "tog-norollbackdiff": "Cék-hèng huòi-gūng ī-hâiu ng-sāi hiēng-sê chă-biék",
        "tog-useeditwarning": "我編輯頁面其時候離開,起動警告我蜀下",
-       "tog-prefershttps": "躒入以後始終使安全連接",
+       "tog-prefershttps": "Láuk-diē ī-hâiu sṳ̄-cṳ̆ng sāi ăng-cuòng lièng-giék",
        "underline-always": "直頭",
        "underline-never": "頭𡅏無",
        "underline-default": "皮膚或者瀏覽器默認其",
        "editfont-monospace": "蜀様寬其字體",
        "editfont-sansserif": "無襯線其字體",
        "editfont-serif": "有襯線其字體",
-       "sunday": "禮拜",
-       "monday": "拜一",
-       "tuesday": "拜二",
-       "wednesday": "拜三",
-       "thursday": "拜四",
-       "friday": "拜五",
-       "saturday": "拜六",
+       "sunday": "Lā̤-bái",
+       "monday": "Bái-ék",
+       "tuesday": "Bái-nê",
+       "wednesday": "Bái-săng",
+       "thursday": "Bái-sé",
+       "friday": "Bái-ngô",
+       "saturday": "Bái-lĕ̤k",
        "sun": "禮拜",
-       "mon": "拜一",
-       "tue": "拜二",
+       "mon": "B1",
+       "tue": "Bái-nê",
        "wed": "拜三",
        "thu": "拜四",
        "fri": "拜五",
        "sat": "拜六",
-       "january": "一月",
-       "february": "二月",
-       "march": "三月",
-       "april": "四月",
-       "may_long": "五月",
-       "june": "六月",
-       "july": "七月",
-       "august": "八月",
-       "september": "九月",
-       "october": "十月",
-       "november": "十一月",
-       "december": "十二月",
+       "january": "Ék-nguŏk",
+       "february": "Nê-nguŏh",
+       "march": "Săng-nguŏk",
+       "april": "Sé-nguŏk",
+       "may_long": "Ngô-nguŏk",
+       "june": "Lĕ̤k-nguŏk",
+       "july": "Chék-nguŏk",
+       "august": "Báik-nguŏk",
+       "september": "Gāu-nguŏk",
+       "october": "Sĕk-nguŏk",
+       "november": "Sĕk-ék-nguŏk",
+       "december": "Sĕk-nê-nguŏk",
        "january-gen": "一月",
-       "february-gen": "二月",
+       "february-gen": "Nê-nguŏk",
        "march-gen": "三月",
        "april-gen": "四月",
        "may-gen": "五月",
        "october-gen": "十月",
        "november-gen": "十一月",
        "december-gen": "十二月",
-       "jan": "一月",
-       "feb": "二月",
-       "mar": "三月",
-       "apr": "四月",
-       "may": "五月",
-       "jun": "六月",
-       "jul": "七月",
-       "aug": "八月",
-       "sep": "九月",
-       "oct": "十月",
-       "nov": "十一月",
-       "dec": "十二月",
+       "jan": "Ék-nguŏk",
+       "feb": "Nê-nguŏk",
+       "mar": "Săng-nguŏk",
+       "apr": "Sé-nguŏk",
+       "may": "Ngô-nguŏk",
+       "jun": "Lĕ̤k-nguŏk",
+       "jul": "Chék-nguŏk",
+       "aug": "Báik-nguŏk",
+       "sep": "Gāu-nguŏk",
+       "oct": "Sĕk-nguŏk",
+       "nov": "Sĕk-ék-nguŏk",
+       "dec": "Sĕk-nê-nguŏk",
        "january-date": "一月$1號",
        "february-date": "二月$1號",
        "march-date": "三月$1號",
        "october-date": "十月$1號",
        "november-date": "十一月$1號",
        "december-date": "十二月$1號",
-       "pagecategories": "{{PLURAL:$1}}類別",
+       "period-am": "AM",
+       "period-pm": "PM",
+       "pagecategories": "{{PLURAL:$1}} Lôi-biék",
        "category_header": "「$1」類別下底其頁面",
        "subcategories": "子類別",
        "category-media-header": "「$1」類別下底其媒體",
        "category-empty": "''茲類別下底現在無文章也無媒體。''",
-       "hidden-categories": "{{PLURAL:$1}}乞囥起其類別",
+       "hidden-categories": "{{PLURAL:$1}} bĭk ké̤ṳk káung kī gì lôi-biék",
        "hidden-category-category": "已經囥起其類別",
-       "category-subcat-count": "{{PLURAL:$2|茲萆分類僅包括下底蜀萆子分類|茲分類有 {{PLURAL:$1|子分類|$1 萆子分類}},總計 $2 萆。}}",
+       "category-subcat-count": "{{PLURAL:$2|Ciā lôi-biék nâ bău-guák â-dā̤ siŏh bĭk cṳ̄-lôi-biék.|Ciā lôi-biék bău-guák â-dā̤ $1 bĭk cṳ̄-lôi-biék, gê̤ṳng-cūng $2 bĭk.}}",
        "category-subcat-count-limited": "茲蜀萆類別下底有子類別{{PLURAL:$1}}",
-       "category-article-count": "{{PLURAL:$2|茲蜀萆類別儷有下底蜀頁。|共總有$2頁,下底其茲$1頁敆茲蜀萆類別𡅏。}}",
+       "category-article-count": "{{PLURAL:$2|Ciā lôi-biék nâ bău-guák â-dā̤ siŏh bĭk hiĕk-miêng.|Ciā lôi-biék bău-guák â-dā̤ $1 bĭk hiĕk-miêng, gê̤ṳng-cūng $2 bĭk.}}",
        "category-article-count-limited": "下底$1頁敆茲蜀萆類別𡅏{{PLURAL:$1}}",
        "category-file-count": "茲蜀萆類別共總有$2萆文件,下底茲$1萆文件都敆茲蜀萆類別𡅏。",
        "category-file-count-limited": "下底其茲$1萆文件都敆茲蜀萆類別𡅏。{{PLURAL:$1}}",
        "listingcontinuesabbrev": "(繼續前斗)",
        "index-category": "索引其頁面",
-       "noindex-category": "未索引其頁面",
+       "noindex-category": "Muôi sáuk-īng gì hiĕk",
        "broken-file-category": "獃其文件鏈接其頁面",
        "about": "關於",
        "article": "文章",
        "newwindow": "(敆新窗口打開)",
        "cancel": "取消",
        "moredotdotdot": "更価...",
-       "morenotlisted": "茲蜀萆單單𣍐完整。",
+       "morenotlisted": "Ciā dăng-dăng mâ̤ uòng-cīng.",
        "mypage": "頁面",
        "mytalk": "我其討論",
-       "anontalk": "茲隻IP其討論頁",
-       "navigation": "引導",
-       "and": "&#32;",
+       "anontalk": "Páng-gōng",
+       "navigation": "Īng-dô̤:",
+       "and": "&#32;gâe̤ng",
        "qbfind": "討",
        "qbbrowse": "覷蜀覷",
        "qbedit": "修改",
        "faq": "真稠碰著其問題",
        "faqpage": "Project:稠問其問題",
        "actions": "動作",
-       "namespaces": "命名空間",
-       "variants": "變體",
-       "navigation-heading": "導航菜單",
+       "namespaces": "Miàng-kŭng-găng",
+       "variants": "Biéng-tā̤",
+       "navigation-heading": "Dô̤-hòng chái-dăng",
        "errorpagetitle": "鄭咯",
        "returnto": "轉去$1。",
-       "tagline": "來源:{{SITENAME}}",
-       "help": "幫助",
-       "search": "尋討",
-       "searchbutton": "",
+       "tagline": "Lài-nguòng: {{SITENAME}}",
+       "help": "Bŏng-cô",
+       "search": "Sìng-tō̤",
+       "searchbutton": "Tō̤",
        "go": "去",
-       "searcharticle": "",
-       "history": "頁面歷史",
-       "history_short": "歷史",
+       "searcharticle": "Kó̤",
+       "history": "Hiĕk-miêng lĭk-sṳ̄",
+       "history_short": "Lĭk-sṳ̄",
        "updatedmarker": "趁我最後蜀回訪問開始更新",
-       "printableversion": "會拍印其版本",
-       "permalink": "永久鏈接",
+       "printableversion": "Â̤ páh-éng gì bēng-buōng",
+       "permalink": "Īng-giū lièng-giék",
        "print": "拍印",
-       "view": "覷蜀覷",
+       "view": "Ché̤ṳ-siŏh-ché̤ṳ",
        "view-foreign": "敆$1𡅏看",
-       "edit": "修改",
+       "edit": "Siŭ-gāi",
        "edit-local": "編輯當地描述",
        "create": "創建",
        "create-local": "添加當地描述",
        "unprotectthispage": "改變茲蜀頁其保護狀態",
        "newpage": "新頁",
        "talkpage": "討論茲頁",
-       "talkpagelinktext": "討論",
+       "talkpagelinktext": "páng-gōng",
        "specialpage": "特殊頁",
-       "personaltools": "個人其傢私花",
+       "personaltools": "Gó̤-ìng gì gă-sĭ-huă",
        "articlepage": "覷蜀覷內容頁面",
-       "talk": "討論",
-       "views": "覷蜀覷",
-       "toolbox": "傢私花",
+       "talk": "Tō̤-lâung",
+       "views": "Ché̤ṳ-siŏh-ché̤ṳ",
+       "toolbox": "Gă-sĭ-huă",
        "userpage": "覷蜀覷用戶頁面",
        "projectpage": "看工程頁",
        "imagepage": "覷蜀覷文件頁面",
        "viewhelppage": "看幫助頁",
        "categorypage": "看分類頁",
        "viewtalkpage": "看討論",
-       "otherlanguages": "其它其語言",
-       "redirectedfrom": "(趁$1重定向過來)",
+       "otherlanguages": "Gì-tă ngṳ̄-ngiòng",
+       "redirectedfrom": "(téng $1 tṳ̀ng-déng-hióng guó-lì)",
        "redirectpagesub": "重定向頁",
        "redirectto": "重定向遘",
-       "lastmodifiedat": "茲蜀頁是着$1 $2其辰候最後修改其。",
+       "lastmodifiedat": "Cī siŏh hiĕh sê diŏh $1 $2 sèng-hâiu có̤i-âu siŭ-gāi gì.",
        "viewcount": "茲蜀頁已經乞訪問$1回了。{{PLURAL:$1}}",
        "protectedpage": "保護頁",
-       "jumpto": "跳遘:",
-       "jumptonavigation": "引導:",
-       "jumptosearch": "尋討",
+       "jumpto": "Tiéu gáu:",
+       "jumptonavigation": "Īng-dô̤:",
+       "jumptosearch": "Sìng-tō̤",
        "view-pool-error": "對不住,服務器茲蜀萆時候已弳過載了。\n過価用戶敆𡅏覷茲蜀頁。\n起動等仂久再來覷茲蜀頁。\n\n$1",
        "generic-pool-error": "對不住,現刻時服務器過載了。\n實在過価用戶敆𡅏訪問茲蜀萆資源。\n起動汝等蜀刻再訪問茲蜀萆資源。",
        "pool-timeout": "等待鎖定其時間遘了",
        "pool-queuefull": "隊列池已經滿了",
        "pool-errorunknown": "𣍐曉什乇綻咯",
-       "aboutsite": "關於{{SITENAME}}",
-       "aboutpage": "Project:關於",
+       "poolcounter-usage-error": "Ê̤ṳng-huák chó̤-nguô: $1",
+       "aboutsite": "Guăng-ṳ̀ {{SITENAME}}",
+       "aboutpage": "Project:Guăng-ṳ̀",
        "copyright": "內容會使敆$1下底會使獲得遘,若無會給出其它提示。",
-       "copyrightpage": "{{ns:project}}:版權",
-       "currentevents": "大樹下",
-       "currentevents-url": "Project:大樹下",
-       "disclaimers": "無負責聲明",
-       "disclaimerpage": "Project:無負責聲明",
+       "copyrightpage": "{{ns:project}}:Bēng-guòng",
+       "currentevents": "Duâi Ché̤ṳ Â",
+       "currentevents-url": "Project:Duâi Ché̤ṳ Â",
+       "disclaimers": "Mò̤-hô-cáik sĭng-mìng",
+       "disclaimerpage": "Project:Mò̤-hô-cáik sĭng-mìng",
        "edithelp": "修改保護",
-       "mainpage": "頭頁",
-       "mainpage-description": "頭頁",
+       "helppage-top-gethelp": "Bŏng-cô",
+       "mainpage": "Tàu Hiĕk",
+       "mainpage-description": "Tàu Hiĕk",
        "policy-url": "Project:政策",
-       "portal": "廳中",
-       "portal-url": "Project:社區門戶",
-       "privacy": "隱私政策",
-       "privacypage": "Project:隱私政策",
+       "portal": "Tiăng-dŏng",
+       "portal-url": "Project:Tiăng-dŏng",
+       "privacy": "Ṳ̄ng-sṳ̆ céng-cháik",
+       "privacypage": "Project:Ṳ̄ng-sŭ céng-cháik",
        "badaccess": "權限錯誤",
        "badaccess-group0": "汝𣍐使做汝要求其茲蜀萆動作。",
        "badaccess-groups": "汝卜做其動作着{{PLURAL:$2|茲蜀群組|茲蜀組裡勢}}其用戶乍有能耐使:$1",
        "versionrequired": "需要版本$1其MediaWiki",
        "versionrequiredtext": "需要MediaWiki其版本$1來使茲蜀頁。\n覷[[Special:Version|版本頁面]]。",
        "ok": "好",
-       "retrievedfrom": "趁「$1」退過來",
+       "retrievedfrom": "Lài-nguòng: \"$1\"",
        "youhavenewmessages": "汝有$1($2)。",
        "youhavenewmessagesfromusers": "汝有趁$3用戶($2)來其$1萆信息{{PLURAL:$3}}",
        "youhavenewmessagesmanyusers": "汝有趁雅価用戶($2)其$1信息",
        "newmessageslinkplural": "{{PLURAL:$1|蜀條新其消息|999=新其消息}}",
        "newmessagesdifflinkplural": "最後{{PLURAL:$1|回改變|999=回改變}}",
        "youhavenewmessagesmulti": "汝有趁$1來其新信息",
-       "editsection": "修改",
+       "editsection": "siŭ-gāi",
        "editold": "修改",
        "viewsourceold": "看源代碼",
-       "editlink": "修改",
-       "viewsourcelink": "看源代碼",
-       "editsectionhint": "修改段:$1",
-       "toc": "目錄",
+       "editlink": "siŭ-gāi",
+       "viewsourcelink": "Káng nguòng-dâi-mā",
+       "editsectionhint": "Siŭ-gāi dâung: $1",
+       "toc": "Mŭk-liŏh",
        "showtoc": "顯示",
        "hidetoc": "囥起",
        "collapsible-collapse": "掩",
        "feed-invalid": "無乇使其下標填充類型",
        "feed-unavailable": "𣍐使聚合訂閱",
        "site-rss-feed": "$1 RSS 訂閱",
-       "site-atom-feed": "$1原子訂閱",
+       "site-atom-feed": "$1 Atom déng-iŏk",
        "page-rss-feed": "「$1」RSS訂閱",
-       "page-atom-feed": "「$1」原子訂閱",
-       "red-link-title": "$1(無許頁)",
+       "page-atom-feed": "$1 Atom déng-iŏk",
+       "red-link-title": "$1 (mò̤ hī hiĕh)",
        "sort-descending": "降序排序",
        "sort-ascending": "升序排序",
-       "nstab-main": "頁面",
+       "nstab-main": "Ùng-ciŏng",
        "nstab-user": "用戶頁",
        "nstab-media": "媒體頁",
-       "nstab-special": "特殊頁面",
+       "nstab-special": "Dĕk-sṳ̀-hiĕk",
        "nstab-project": "工程頁",
-       "nstab-image": "文件",
+       "nstab-image": "Ùng-giông",
        "nstab-mediawiki": "消息",
        "nstab-template": "模板",
        "nstab-help": "幫助頁",
-       "nstab-category": "類別",
+       "nstab-category": "Lôi-biék",
+       "mainpage-nstab": "Tàu Hiĕk",
        "nosuchaction": "無茲蜀種行動",
        "nosuchactiontext": "茲蜀種URL指定其行動是𣍐合法其。",
        "nosuchspecialpage": "無總款其特殊頁",
        "perfcached": "下底其數據乞緩存固加可能伓是最新其。{{PLURAL:$1|$1條結果}}會敆緩存臺中討著。",
        "perfcachedts": "下底其數據已經緩存過了,最後更新遘$1。{{PLURAL:$4|$4條結果}}會敆緩存臺中討著。",
        "querypage-no-updates": "茲蜀頁其更新乞禁止了。\n數據嚽塊現刻時𣍐更新了。",
-       "viewsource": "看源代碼",
+       "viewsource": "Káng nguòng-dâi-mā",
        "viewsource-title": "覷\"$1\"其源代碼",
        "actionthrottled": "行動乞取消咯",
        "protectedpagetext": "茲頁已經乞保護起咯,𣍐使修改或者其它行動。",
        "yourpasswordagain": "重新拍囇密碼:",
        "createacct-yourpasswordagain": "確定密碼",
        "createacct-yourpasswordagain-ph": "再拍入蜀回密碼",
-       "remembermypassword": "共我敆茲蜀萆瀏覽器其登錄記錄記定幾日(最価$1日){{PLURAL:$1}}",
        "userlogin-remembermypassword": "記𡅏我躒入其狀態",
        "userlogin-signwithsecure": "使安全其連接",
        "yourdomainname": "汝其域名:",
        "nocookiesnew": "用戶賬號已經開好了,不過汝固未躒入。\n{{SITENAME}}使cookie來記錄已經躒入其用戶。\n汝其cookie固未開起來。\n起動汝開啟cookie,仱再使汝新其賬號共密碼來躒入。",
        "nocookieslogin": "{{SITENAME}}使cookies來記錄已經登錄其用戶。\n但是汝禁用了cookie。\n起動汝開起cookie,然後再試蜀試。",
        "noname": "汝未指定蜀萆合法其用戶名。",
-       "loginsuccesstitle": "躒入成功",
+       "loginsuccesstitle": "Láuk-diē sìng-gŭng",
        "loginsuccess": "'''汝現在已經「$1」其成功躒入{{SITENAME}}了。'''",
-       "nosuchuser": "無總款其用戶名「$1」。\n用户名是大小写敏感其。\n检查汝其拼写,或者覷蜀覷[[Special:CreateAccount|開新賬戶]]。",
+       "nosuchuser": "Că mò̤ ming-chĭng sê \"$1\" gì ê̤ṳng-hô.\nÊ̤ṳng-hô-miàng ô buŏng duâi-siēu-siā.\nChiāng giēng-că nṳ̄ gì pĭng-siā, hĕ̤k-chiā [[Special:CreateAccount|kŭi sĭng dióng-hô]].",
        "nosuchusershort": "無總款其用戶名「$1」。\n檢查汝其拼寫。",
        "nouserspecified": "汝著指定蜀萆用戶名。",
        "login-userblocked": "茲隻用戶已經乞封鎖去了。登錄是𣍐允許其。",
        "accountcreated": "賬戶創建了",
        "accountcreatedtext": "[[{{ns:User}}:$1|$1]]([[{{ns:User talk}}:$1|talk]])用戶已經創建。",
        "createaccount-title": "{{SITENAME}}其開賬戶",
-       "login-abort-generic": "汝其登錄𣍐成功——放棄去了",
+       "login-abort-generic": "Nṳ̄ láuk-diē sék-bâi - Sák gó̤ lāu",
        "loginlanguagelabel": "語言:$1",
-       "pt-login": "躒入",
+       "pt-login": "Láuk-diē",
        "pt-login-button": "躒入",
-       "pt-createaccount": "開新賬號",
+       "pt-createaccount": "Kŭi sĭng dióng-hô̤",
        "pt-userlogout": "躒出",
        "php-mail-error-unknown": "PHP其mail()函數,𣍐曉什乇綻去。",
        "changepassword": "改變密碼",
        "passwordreset-domain": "域名:",
        "passwordreset-email": "電批地址:",
        "passwordreset-emailsentemail": "蜀萆密碼重新設置其電批已經寄出去了。",
-       "passwordreset-emailsent-capture": "蜀萆密碼重新設置其電批已經寄出去了,內容就是生下底總款。",
        "changeemail": "修改電批其地址",
        "changeemail-header": "修改賬戶電子郵件地址",
        "changeemail-oldemail": "現刻時其電批地址:",
        "subject": "主題/標題:",
        "minoredit": "過幼修改",
        "watchthis": "監視茲頁",
-       "savearticle": "保存茲頁",
+       "savearticle": "Bō̤-còng ciā hiĕk",
        "preview": "預覽",
        "showpreview": "顯示預覽",
        "showdiff": "看改變其部分",
        "blockednoreason": "無掏出原因",
        "whitelistedittext": "汝必須$1乍會使修改頁面。",
        "loginreqtitle": "需要登錄",
-       "loginreqlink": "躒入",
+       "loginreqlink": "láuk-diē",
        "loginreqpagetext": "起動汝$1以後再看其它頁面。",
        "accmailtitle": "密碼寄出了",
        "accmailtext": "共[[User talk:$1|$1]]用戶隨機生成其密碼已經發遘$2了。汝登錄以後會使敆[[Special:ChangePassword|修改密碼]]頁面修改茲蜀萆密碼。",
        "templatesused": "{{PLURAL:$1}}茲頁裏勢使其模板:",
        "templatesusedpreview": "茲萆預覽使其{{PLURAL:$1|模板}}:",
        "templatesusedsection": "茲蜀段使其{{PLURAL:$1|模板}}:",
-       "template-protected": "(保護)",
-       "template-semiprotected": "(半保護)",
+       "template-protected": "(bō̤-hô)",
+       "template-semiprotected": "(buáng bō̤-hô)",
        "permissionserrorstext-withaction": "因為下底其{{PLURAL:$1|原因}},汝無能耐 $2 :",
        "recreate-moveddeleted-warn": "'''注意:汝敆𡅏重新創建舊底已經乞刪唻其頁面。'''\n\n汝應該考慮蜀下繼續去編輯茲蜀頁到底是伓是合適其。茲蜀頁其刪除記錄共移動記錄都敆嚽塊:",
        "edit-conflict": "編輯衝突",
        "content-model-text": "純文本",
        "content-model-javascript": "JavaScript",
        "content-model-css": "CSS",
-       "undo-summary": "取消[[Special:Contributions/$2|$2]]([[User talk:$2|Tō̤-lâung]])其$1修改",
-       "cantcreateaccounttitle": "無能獃開賬戶",
+       "undo-summary": "Chṳ̄-siĕu [[Special:Contributions/$2|$2]]([[User talk:$2|Páng-gōng]])sū có̤ gì siŭ-gāi $1",
        "viewpagelogs": "看茲頁其歷史",
        "nohistory": "茲頁無修改歷史。",
        "currentrev": "最新版本",
-       "revisionasof": "$1其版本",
-       "previousrevision": "←加舊其版本",
+       "revisionasof": "$1 gì bēng-buōng",
+       "previousrevision": "← Gá-gô gì bēng-buōng",
        "nextrevision": "加新其版本→",
        "currentrevisionlink": "最新版本",
-       "cur": "",
+       "cur": "dāng",
        "next": "下",
-       "last": "",
+       "last": "sèng",
        "page_first": "頭",
        "page_last": "尾",
        "histlegend": "差別揀選:選擇卜比並其版本,再擪「回車」('''Enter''')或者擪底底其'''比並揀選版本'''。<br />\n說明:(伶)=共第一新其版本比並,(前)=共前蜀版本比並,~=過幼修改。",
        "difference-title": "「$1」調整以後𣍐蜀樣其地方",
        "difference-title-multipage": "「$1」共「$2」臺中𣍐蜀樣其地方",
        "difference-multipage": "(臺中𣍐蜀様其地方)",
-       "lineno": "第$1行:",
+       "lineno": "Dâ̤ $1 hòng:",
        "compareselectedversions": "比並揀選版本",
        "showhideselectedversions": "顯/藏揀選其調整",
-       "editundo": "取消",
-       "searchresults": "討結果",
-       "searchresults-title": "尋討「$1」其結果",
-       "prevn": "前{{PLURAL:$1}}$1萆",
-       "nextn": "後{{PLURAL:$1}}$1萆",
-       "shown-title": "每頁顯示$1{{PLURAL:$1|萆結果}}",
-       "viewprevnext": "看($1 {{int:pipe-separator}} $2)($3)。",
-       "searchprofile-articles": "內容頁",
-       "searchprofile-images": "多媒體",
-       "searchprofile-everything": "所有乇",
-       "searchprofile-advanced": "高級",
-       "searchprofile-articles-tooltip": "敆$1𡅏尋討",
-       "searchprofile-images-tooltip": "尋討文件",
-       "search-result-size": "$1 ({{PLURAL:$2|$2萆單詞}})",
-       "search-redirect": "(重定向 $1)",
+       "editundo": "Chṳ̄-siĕu",
+       "searchresults": "Sìng-tō̤ giék-guō",
+       "searchresults-title": "Sìng-tō̤ \"$1\" gì giék-guō",
+       "prevn": "sèng $1 bĭk",
+       "nextn": "âu $1 bĭk",
+       "shown-title": "Mūi hiĕk hiēng-sê $1{{PLURAL:$1|bĭk giék-guō}}",
+       "viewprevnext": "Káng ($1 {{int:pipe-separator}} $2) ($3)",
+       "searchprofile-articles": "Nô̤i-ṳ̀ng hiĕk",
+       "searchprofile-images": "Dŏ̤-mùi-tā̤",
+       "searchprofile-everything": "Sū-iū-nó̤h",
+       "searchprofile-advanced": "Gŏ̤-ngék",
+       "searchprofile-articles-tooltip": "Găk $1 lā̤ sìng-tō̤",
+       "searchprofile-images-tooltip": "Sìng-tō̤ ùng-giông",
+       "search-result-size": "$1 ({{PLURAL:$2|$2 bĭk dăng-sṳ̀}})",
+       "search-redirect": "(dêng-hióng $1)",
        "search-suggest": "汝其意思是伓是:$1",
        "searchrelated": "相關其",
        "searchall": "全部",
        "showingresults": "顯示趁#<b>$2</b>開始其{{PLURAL:$1|'''$1'''萆結果}}。",
-       "search-nonefound": "討毋着",
+       "search-nonefound": "Tō̤ mâ̤ diŏh.",
        "preferences": "設定",
        "mypreferences": "我其設定",
        "prefs-edits": "修改數量:",
        "grouppage-sysop": "{{ns:project}}:管理員",
        "grouppage-bureaucrat": "{{ns:project}}:官僚組",
        "grouppage-suppress": "{{ns:project}}:巡查員",
-       "newuserlogpage": "開賬戶日誌",
+       "newuserlogpage": "Kŭi dióng-hô nĭk-cé",
        "action-edit": "修改茲蜀頁",
-       "recentchanges": "這般其改變",
+       "recentchanges": "Cī-bŏng gì gāi-biéng",
        "recentchanges-summary": "敆維基茲頁跟蹤這般其改變。",
-       "recentchanges-label-newpage": "茲蜀萆修改創建新其蜀頁",
-       "recentchanges-label-minor": "嚽是蜀萆過幼修改",
-       "recentchanges-label-bot": "茲蜀萆修改是機器人做其",
-       "rclistfrom": "顯示由$3 $2開始其新其改變",
-       "rcshowhideminor": "$1過幼修改",
-       "rcshowhidebots": "$1機器人",
-       "rcshowhideliu": "$1已註冊其用戶",
-       "rcshowhideanons": "$1無名用戶",
-       "rcshowhidemine": "$1我其修改",
-       "rclinks": "顯示$2日以內產生其$1回改變<br />$3",
-       "diff": "",
-       "hist": "",
+       "recentchanges-label-newpage": "Cī siŏh bĭk siŭ-gāi cháung-gióng lāu sĭng hiĕk",
+       "recentchanges-label-minor": "Cuòi sê siŏh bĭk guó-éu siŭ-gāi",
+       "recentchanges-label-bot": "Cuòi sê gĭ-ké-nè̤ng siŭ-gāi gì",
+       "rclistfrom": "Hiēng-sê téng $3 $2 gáu dāng gì sĭng gāi-biéng",
+       "rcshowhideminor": "$1 guó-éu siŭ-gāi",
+       "rcshowhidebots": "$1 gĭ-ké-nè̤ng",
+       "rcshowhideliu": "$1 ī dĕng-gé gì ê̤ṳng-hô",
+       "rcshowhideanons": "$1 ù-mìng-sê",
+       "rcshowhidemine": "$1 nguāi gì siŭ-gāi",
+       "rclinks": "Hiēng-sê có̤i-gê̤ṳng $2 gĕ̤ng ī-nô̤i gì $1 huòi gāi-biéng<br />$3",
+       "diff": "chă",
+       "hist": "sṳ̄",
        "hide": "掩",
        "show": "現",
        "minoreditletter": "~",
        "newpageletter": "!",
        "boteditletter": "^",
+       "rc-change-size-new": "Siŭ-gāi ī-hâiu biéng có̤ $1 cê-ciék",
        "rc-enhanced-hide": "囥起細節",
        "recentchangeslinked": "相關其改變",
        "recentchangeslinked-feed": "相關其改變",
-       "recentchangeslinked-toolbox": "相關其改變",
+       "recentchangeslinked-toolbox": "Sŏng-guăng gì gāi-biéng",
        "recentchangeslinked-page": "頁面名:",
-       "upload": "上傳文件",
+       "upload": "Siông-diòng ùng-giông",
        "uploadbtn": "上傳文件",
        "reuploaddesc": "取消上傳,轉去上傳頁面",
        "uploadnologin": "未登錄",
        "listfiles_name": "名",
        "listfiles_user": "用戶",
        "listfiles_size": "尺寸",
-       "file-anchor-link": "文件",
-       "filehist": "文件歷史",
-       "filehist-current": "現刻時",
-       "filehist-datetime": "日期/時間",
-       "filehist-user": "用戶",
-       "filehist-dimensions": "維度",
-       "filehist-comment": "評論",
-       "imagelinks": "文件使用方法",
-       "linkstoimage": "下底{{PLURAL:$1|$1頁鏈接}}遘茲文件:",
+       "file-anchor-link": "Ùng-giông",
+       "filehist": "Ùng-giông lĭk-sṳ̄",
+       "filehist-current": "hiêng-káik-sì",
+       "filehist-datetime": "Nĭk-gĭ/Sì-găng",
+       "filehist-user": "Ê̤ṳng-hô",
+       "filehist-dimensions": "Chióh-cháung",
+       "filehist-comment": "Suók-mìng",
+       "imagelinks": "Ùng-giông sāi-ê̤ṳng cìng-huóng",
+       "linkstoimage": "Â-dā̤ {{PLURAL:$1|$1 hiĕk}} lièng gáu ciā ùng-giông:",
        "nolinkstoimage": "無鏈接遘茲蜀萆文件其頁面。",
        "uploadnewversion-linktext": "上傳蜀萆新版本其茲萆文件。",
        "shared-repo-name-wikimediacommons": "Wikimedia Commons",
        "unwatchedpages": "無監視其頁面",
        "listredirects": "重定向其單單",
        "unusedtemplateswlh": "其它鏈接",
-       "randompage": "隨便罔看",
+       "randompage": "Sùi-biêng muōng ché̤ṳ",
        "randomredirect": "隨便重定向",
        "statistics": "統計",
        "statistics-header-users": "用戶統計",
        "withoutinterwiki": "無跨語言其鏈接",
        "withoutinterwiki-summary": "下底其頁面無鏈接遘其它語言其版本。",
        "fewestrevisions": "修改最少其頁面",
-       "nbytes": "$1{{PLURAL:$1}}å­\97ç¯\80",
+       "nbytes": "$1{{PLURAL:$1}} bÄ­k dÄ\83ng-sá¹³Ì\80",
        "nlinks": "$1隻{{PLURAL:$1|鏈接}}",
        "nmembers": "$1隻成員{{PLURAL:$1}}",
        "wantedcategories": "卜挃其類別",
        "longpages": "長頁",
        "protectedpages": "保護頁",
        "listusers": "用戶單",
-       "newpages": "新頁",
+       "newpages": "Sĭng hiĕk",
        "newpages-username": "用戶名:",
        "ancientpages": "最舊其頁面",
        "move": "移動",
        "allpagesfrom": "使下底其乇開始顯示頁:",
        "allarticles": "所有文章",
        "allinnamespace": "所有頁面($1命名空間)",
-       "allpagessubmit": "",
+       "allpagessubmit": "Kó̤",
        "allpagesprefix": "按頭部顯示頁面:",
        "allpagesbadtitle": "給出其頁面其標題是𣍐合法其,或者有蜀萆跨語言或跨維基其前綴。伊可能包括蜀萆或者価萆𣍐使廮標題裏勢其字符。",
        "categories": "類別",
        "deletionlog": "刪除日誌",
        "deletecomment": "原因:",
        "rollback": "再修改轉去",
-       "rollbacklink": "",
+       "rollbacklink": "duōng",
        "rollbackfailed": "轉𣍐去",
        "cantrollback": "𣍐使恢復修改;最後其貢獻者是茲蜀頁其唯一其作者。",
        "alreadyrolled": "𣍐使回滾最後蜀回[[User:$2|$2]] ([[User talk:$2|討論]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]])其[[:$1]]編輯;\n有其他儂已經編輯過了或者茲蜀頁已經乞回滾過了。\n\n最後蜀回茲蜀頁其修改是[[User:$3|$3]] ([[User talk:$3|討論]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]])改其。",
        "undeleteviewlink": "看",
        "undeletecomment": "原因:",
        "undelete-search-submit": "尋討",
-       "namespace": "命名空間:",
-       "invert": "反選",
-       "blanknamespace": "(主要)",
+       "namespace": "Miàng-kŭng-găng:",
+       "invert": "Huāng-sōng",
+       "blanknamespace": "(cuō-iéu)",
        "contributions": "{{GENDER:$1|User}}用戶貢獻",
        "contributions-title": "$1其用戶貢獻",
        "mycontris": "我其貢獻",
        "sp-contributions-search": "尋討貢獻",
        "sp-contributions-username": "IP地址或者用戶名:",
        "sp-contributions-submit": "尋討",
-       "whatlinkshere": "甚乇鏈遘嚽塊",
+       "whatlinkshere": "Diē-nē̤ lièng gáu cē̤-nē̤",
        "whatlinkshere-title": "鏈接遘$1其頁面",
        "whatlinkshere-page": "頁面:",
        "linkshere": "下底其頁面鏈接遘'''[[:$1]]''':",
        "anononlyblock": "囇無名用戶",
        "createaccountblock": "防止開賬戶",
        "ipblocklist-empty": "茲張封鎖單單是空其。",
-       "blocklink": "封鎖",
+       "blocklink": "hŭng-sō̤",
        "unblocklink": "開封",
        "change-blocklink": "修改封鎖情況",
-       "contribslink": "貢獻",
+       "contribslink": "góng-hióng",
        "blocklogpage": "封鎖日誌",
        "blocklogentry": "封鎖[[$1]],遘$2時候過時,$3",
        "block-log-flags-anononly": "囇無名用戶",
        "allmessagescurrent": "現時其文字",
        "allmessagestext": "茲是敆MediaWiki命名空間裏勢系統消息其蜀萆單單。\n如果汝卜想貢獻通用其MediaWiki基本地化服務,起動汝訪問[https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation MediaWiki本地化]共[https://translatewiki.net translatewiki.net]。",
        "allmessagesnotsupportedDB": "茲蜀頁𣍐使其,因為'''$wgUseDatabaseMessages'''已經乞禁止去了。",
-       "thumbnail-more": "放大",
+       "thumbnail-more": "Huóng-duâi",
        "tooltip-pt-userpage": "汝其用戶頁",
        "tooltip-pt-mytalk": "汝其討論頁",
        "tooltip-pt-preferences": "汝其設定",
        "tooltip-pt-watchlist": "汝監視其頁面有改過其單單",
        "tooltip-pt-mycontris": "汝其貢獻其單單",
-       "tooltip-pt-login": "希望汝先躒入;不過儂家無逼汝總款做。",
+       "tooltip-pt-login": "Gióng-ngiê nṳ̄ sĕng láuk-diē; bók-guó nàng-gă mò̤ ăng nṳ̄ cūng-kuāng có̤.",
        "tooltip-pt-logout": "躒出",
-       "tooltip-ca-talk": "茲蜀頁其討論",
-       "tooltip-ca-edit": "汝會使修改茲蜀頁。起動敆保存以前使預覽按鈕",
-       "tooltip-ca-addsection": "開始蜀萆新其部分",
+       "tooltip-pt-createaccount": "Gióng-ngiê nṳ̄ sĕng kŭi dióng-hô gái láuk-diē; bók-guó nàng-gă mò̤ ăng nṳ̄ cūng-kuāng có̤.",
+       "tooltip-ca-talk": "Nô̤i-ṳ̀ng gì tō̤-lâung",
+       "tooltip-ca-edit": "Siŭ-gāi cī hiĕk",
+       "tooltip-ca-addsection": "Gă sĭng dâung",
        "tooltip-ca-viewsource": "茲蜀頁乞保護起去。\n汝會使看伊其源代碼。",
-       "tooltip-ca-history": "覷茲頁舊底其版本",
+       "tooltip-ca-history": "Ché̤ṳ cī hiĕk gó̤-dā̤ gì bēng-buōng",
        "tooltip-ca-protect": "保護茲蜀頁",
        "tooltip-ca-delete": "刪掉茲蜀頁",
        "tooltip-ca-move": "移動茲蜀頁",
-       "tooltip-ca-watch": "將茲蜀頁加遘汝其監視單",
+       "tooltip-ca-watch": "Ciŏng cī siŏh hiĕk gă diē nṳ̄ gì gáng-sê-dăng",
        "tooltip-ca-unwatch": "共茲頁趁監視單𡅏移開去",
-       "tooltip-search": "尋討 {{SITENAME}} [alt-f]",
-       "tooltip-search-fulltext": "敆茲幾頁𡅏尋討茲文字",
-       "tooltip-p-logo": "覷蜀覷頭頁",
-       "tooltip-n-mainpage": "覷蜀覷頭頁",
-       "tooltip-n-mainpage-description": "覷蜀覷頭頁",
-       "tooltip-n-recentchanges": "維基百科最近其改變其單單",
-       "tooltip-n-randompage": "隨便罔看",
-       "tooltip-t-whatlinkshere": "鏈遘嚽塊其所有維基頁面其單單",
-       "tooltip-t-recentchangeslinked": "鏈遘茲頁其頁面其最近修改",
+       "tooltip-search": "Sìng-tō̤ {{SITENAME}} [alt-f]",
+       "tooltip-search-go": "Nâ ô dè̤ng-miàng gì hiĕk còng-câi, cô kó̤ hiā hiĕk",
+       "tooltip-search-fulltext": "Sìng-tō̤ sāi-ê̤ṳng ciā ùng-cê gì hiĕk-miêng",
+       "tooltip-p-logo": "Ché̤ṳ-siŏh-ché̤ṳ tàu-hiĕk",
+       "tooltip-n-mainpage": "Ché̤ṳ-siŏh-ché̤ṳ tàu-hiĕk",
+       "tooltip-n-mainpage-description": "Ché̤ṳ-siŏh-ché̤ṳ tàu-hiĕk",
+       "tooltip-n-portal": "Guăng-ṳ̀ ciā gĕ̤ng-tiàng, nṳ̄ â̤ có̤ gì, kó̤ diē-nē̤ tō̤ nó̤h",
+       "tooltip-n-recentchanges": "Cī-bŏng diŏh wiki ô gāi-biéng gì dăng-dăng",
+       "tooltip-n-randompage": "Sùi-biêng muōng ché̤ṳ",
+       "tooltip-n-help": "Sìng-tō̤ bŏng-cô gì sū-câi",
+       "tooltip-t-whatlinkshere": "Cuòng-buô lièng-gáu cŭ-uái gì wiki hiĕk-miêng dăng-dăng",
+       "tooltip-t-recentchangeslinked": "鏈遘茲頁其頁面其最近修改\nCī hiĕk lièng gáu bĕk hiĕk gì cī-bŏng gì gāi-biéng",
        "tooltip-t-contributions": "茲蜀用戶其貢獻單單",
        "tooltip-t-emailuser": "向茲蜀隻用戶寄電批",
-       "tooltip-t-upload": "上傳文件",
-       "tooltip-t-specialpages": "特殊頁其單單",
-       "tooltip-t-print": "茲蜀頁其會拍印其版本",
-       "tooltip-t-permalink": "茲頁茲版本其永久鏈接",
-       "tooltip-ca-nstab-main": "看蜀看內容頁",
+       "tooltip-t-upload": "Siông-diòng ùng-giông",
+       "tooltip-t-specialpages": "Cuòng-buô dĕk-sṳ̀-hiĕk dăng-dăng",
+       "tooltip-t-print": "Cī hiĕk gì â̤ páh-éng bēng-buōng",
+       "tooltip-t-permalink": "茲頁茲版本其永久鏈接\nCī hiĕk cī bēng-buōng gì īng-giū lièng-giék",
+       "tooltip-ca-nstab-main": "Káng iĕk gì nô̤i-ṳ̀ng",
        "tooltip-ca-nstab-user": "覷蜀覷用戶頁",
        "tooltip-ca-nstab-special": "茲是蜀萆特殊頁,汝𣍐使修改茲蜀頁。",
        "tooltip-ca-nstab-project": "看工程頁",
-       "tooltip-ca-nstab-image": "看文件頁",
+       "tooltip-ca-nstab-image": "Ché̤ṳ ùng-giông hiĕk",
        "tooltip-ca-nstab-template": "覷蜀覷模板",
        "tooltip-minoredit": "共茲標記成過幼修改",
        "tooltip-save": "保存汝其改變 [alt-s]",
        "tooltip-watch": "共茲蜀頁加遘汝其監視單[alt-w]",
        "anonymous": "{{SITENAME}}其無名{{PLURAL:$1|用戶}}",
        "lastmodifiedatby": "茲頁最後是$3著$1$2改變其。",
+       "pageinfo-toolboxlink": "Hiĕk-miêng séng-sék",
        "deletedrevision": "刪掉舊其版本$1",
        "previousdiff": "← 舊其修改",
        "nextdiff": "新其修改 →",
        "file-nohires": "無更高決斷",
+       "show-big-image-size": "$1 × $2 chiông-só",
        "ilsubmit": "尋討",
        "bydate": "按日期",
-       "metadata": "元數據",
+       "metadata": "Nguòng-só-gé̤ṳ",
        "exif-componentsconfiguration-0": "無存在",
        "exif-meteringmode-0": "𣍐八",
        "exif-lightsource-0": "𣍐八",
        "exif-subjectdistancerange-0": "𣍐八",
-       "namespacesall": "所有",
+       "namespacesall": "cuòng-buô",
        "monthsall": "囫圇年",
        "confirmemail": "確定電批地址",
        "confirmemail_invalid": "確認碼無效。\n可能已經過期了。",
        "watchlisttools-view": "看相關改變",
        "watchlisttools-edit": "看共修改監視單",
        "watchlisttools-raw": "修改原始監視單",
-       "specialpages": "特殊頁",
-       "searchsuggest-search": ""
+       "specialpages": "Dĕk-sṳ̀-hiĕk",
+       "searchsuggest-search": "Tō̤"
 }
index 088c746..decdb5b 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» пароль кхиамца кхоьллина.",
        "upload-http-error": "Даьлла гӀалат HTTP: $1",
        "upload-dialog-title": "Файл чуяккхар",
        "upload-dialog-button-cancel": "Цаоьшу",
+       "upload-dialog-button-back": "Юха",
        "upload-dialog-button-done": "Кийчча ю",
        "upload-dialog-button-save": "Ӏалашъян",
        "upload-dialog-button-upload": "Чуяккха",
        "htmlform-chosen-placeholder": "Харжа кеп",
        "htmlform-cloner-create": "ТӀетоха кхин",
        "htmlform-cloner-delete": "ДӀаяккха",
+       "htmlform-datetime-placeholder": "ШШШШ-ББ-ДД СС:ММ:СС",
        "htmlform-title-not-exists": "«$1» яц.",
        "htmlform-user-not-exists": "<strong>$1</strong> яц.",
        "htmlform-user-not-valid": "<strong>$1</strong> — декъашхочун магийна йоцу цӀе.",
        "feedback-bugornote": "Хьайн техникин халонах лаьцна яздан хӀума делахь, дехар до, [$1 хаам бе тхоьга].\nДацахь хьан йиш ю хӀокху атта кепаца «[$3 $2]» агӀонг къамел тӀетоха хьан декъашхочун цӀарца, кхин лелош йолу браузер билгал еш.",
        "feedback-cancel": "Цаоьшу",
        "feedback-close": "Кийчча ю",
-       "feedback-error-title": "ГӀалат",
        "feedback-message": "Хаам:",
        "feedback-subject": "Къамел:",
        "feedback-submit": "Дахьийта",
        "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аьрбийн шординарш",
+       "special-characters-group-arabicextended": "Ӏаьрбийн алсам",
        "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": "Ð\93Ñ\83жаÑ\80аÑ\82ойн",
+       "special-characters-group-gujarati": "Ð\93Ñ\83джаÑ\80аÑ\82и",
        "special-characters-group-devanagari": "Деванагари",
        "special-characters-group-thai": "Тайхойн",
        "special-characters-group-lao": "Лаохойн",
index 83a72b4..964df5c 100644 (file)
@@ -62,7 +62,7 @@
        "tog-enotifminoredits": "Posílat e-maily i při malých editacích stránek a souborů",
        "tog-enotifrevealaddr": "Prozradit mou e-mailovou adresu v upozorňujících e-mailech",
        "tog-shownumberswatching": "Zobrazovat počet sledujících uživatelů",
-       "tog-oldsig": "Stávající podpis:",
+       "tog-oldsig": "Váš stávající podpis:",
        "tog-fancysig": "Používat v podpisu wikitext (bez automatického odkazu)",
        "tog-uselivepreview": "Používat rychlý náhled",
        "tog-forceeditsummary": "Upozornit, když nevyplním shrnutí editace",
@@ -79,7 +79,7 @@
        "tog-showhiddencats": "Zobrazit skryté kategorie",
        "tog-norollbackdiff": "Po vrácení změny nezobrazovat porovnání rozdílů",
        "tog-useeditwarning": "Upozornit, když budu opouštět editaci bez uložení změn",
-       "tog-prefershttps": "Po přihlášení používat vždy zabezpečené spojení",
+       "tog-prefershttps": "Po přihlášení vždy používat zabezpečené připojení",
        "underline-always": "Vždy",
        "underline-never": "Nikdy",
        "underline-default": "Podle nastavení prohlížeče nebo vzhledu",
        "newwindow": "(otevře se v novém okně)",
        "cancel": "Storno",
        "moredotdotdot": "Další…",
-       "morenotlisted": "Tento seznam není úplný.",
+       "morenotlisted": "Tento seznam může být neúplný.",
        "mypage": "Stránka",
        "mytalk": "Diskuse",
        "anontalk": "Diskuse",
        "talk": "Diskuse",
        "views": "Zobrazení",
        "toolbox": "Nástroje",
+       "tool-link-userrights": "Změnit uživatelské skupiny {{GENDER:$1|tohoto uživatele|této uživatelky}}",
+       "tool-link-emailuser": "Poslat e-mail {{GENDER:$1|tomuto uživateli|této uživatelce}}",
        "userpage": "Prohlédnout si uživatelskou stránku",
        "projectpage": "Prohlédnout si stránku projektu",
        "imagepage": "Prohlédnout si stránku o souboru",
        "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?",
        "passwordreset-emailelement": "Uživatelské jméno: \n$1\n\nDočasné heslo: \n$2",
        "passwordreset-emailsentemail": "Pokud je u vašeho účtu nastavena tato e-mailová adresa, bude vám zaslán e-mail pro získání nového hesla.",
        "passwordreset-emailsentusername": "Pokud je u tohoto účtu nastavena e-mailová adresa, bude vám zaslán e-mail pro získání nového hesla.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Byl odeslán e-mail|Byly odeslány e-maily}} pro získání nového hesla. {{PLURAL:$1|Uživatelské jméno a heslo jsou zobrazeny|Seznam uživatelských jmen a hesel je zobrazen}} níže.",
-       "passwordreset-emailerror-capture2": "{{GENDER:$2|Uživateli|Uživatelce}} se nepodařilo odeslat e-mail: $1 {{PLURAL:$3|Uživatelské jméno a heslo jsou zobrazeny|Seznam uživatelských jmen a hesel je zobrazen}} níže.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Byl odeslán e-mail|Byly odeslány e-maily}} pro získání nového hesla. Zde {{PLURAL:$1|jsou zobrazeny uživatelské jméno a heslo|je zobrazen seznam uživatelských jmen a hesel}}.",
+       "passwordreset-emailerror-capture2": "{{GENDER:$2|Uživateli|Uživatelce}} se nepodařilo odeslat e-mail: $1 Zde {{PLURAL:$3|jsou zobrazeny uživatelské jméno a heslo|je zobrazen seznam uživatelských jmen a hesel}}.",
        "passwordreset-nocaller": "Musí být uveden volající",
        "passwordreset-nosuchcaller": "Volající neexistuje: $1",
        "passwordreset-ignored": "Žádost o nové heslo nebyla zpracována. Možná není nakonfigurován žádný poskytovatel?",
        "searchprofile-advanced-tooltip": "Nastavit jmenné prostory, ve kterých se má hledat",
        "search-result-size": "$1 ({{PLURAL:$2|1 slovo|$2 slova|$2 slov}})",
        "search-result-category-size": "{{PLURAL:$1|1 položka|$1 položky|$1 položek}} ({{PLURAL:$2|1 podkategorie|$2 podkategorie|$2 podkategorií}}, {{PLURAL:$3|1 soubor|$3 soubory|$3 souborů}})",
-       "search-redirect": "(přesměrování $1)",
+       "search-redirect": "(přesměrování $1)",
        "search-section": "(část $1)",
        "search-category": "(kategorie $1)",
        "search-file-match": "(odpovídá obsahu souboru)",
        "upload-dialog-disabled": "Načítání souborů pomocí tohoto dialogu je na této wiki vypnuto.",
        "upload-dialog-title": "Načtení souboru",
        "upload-dialog-button-cancel": "Storno",
+       "upload-dialog-button-back": "Zpět",
        "upload-dialog-button-done": "Hotovo",
        "upload-dialog-button-save": "Uložit",
        "upload-dialog-button-upload": "Načíst",
        "apisandbox-results-fixtoken-fail": "Nepodařilo se načíst token „$1“.",
        "apisandbox-alert-page": "Pole na této stránce nejsou platná.",
        "apisandbox-alert-field": "Hodnota tohoto pole není platná.",
+       "apisandbox-continue": "Pokračovat",
+       "apisandbox-continue-clear": "Vymazat",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} bude [https://www.mediawiki.org/wiki/API:Query#Continuing_queries pokračovat] v posledním požadavku; {{int:apisandbox-continue-clear}} vymaže parametry související s pokračováním.",
        "booksources": "Zdroje knih",
        "booksources-search-legend": "Vyhledat knižní zdroje",
        "booksources-search": "Hledat",
        "htmlform-cloner-create": "Přidat další",
        "htmlform-cloner-delete": "Odstranit",
        "htmlform-cloner-required": "Je povinná nejméně jedna hodnota.",
+       "htmlform-date-placeholder": "RRRR-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "RRRR-MM-DD HH:MM:SS",
+       "htmlform-date-invalid": "Uvedená hodnota není platné datum. Zkuste použít formát RRRR-MM-DD.",
+       "htmlform-time-invalid": "Uvedená hodnota není platný čas. Zkuste použít formát HH:MM:SS.",
+       "htmlform-datetime-invalid": "Uvedená hodnota není platné datum a čas. Zkuste použít formát RRRR-MM-DD HH:MM:SS.",
+       "htmlform-date-toolow": "Uvedená hodnota je před nejdřívějším dovoleným datem $1.",
+       "htmlform-date-toohigh": "Uvedená hodnota je po nejpozdějším dovoleném datu $1.",
+       "htmlform-time-toolow": "Uvedená hodnota je před nejdřívějším dovoleným časem $1.",
+       "htmlform-time-toohigh": "Uvedená hodnota je po nejpozdějším dovoleném čase $1.",
+       "htmlform-datetime-toolow": "Uvedená hodnota je před nejdřívějším dovoleným datem a časem $1.",
+       "htmlform-datetime-toohigh": "Uvedená hodnota je po nejpozdějším dovoleném datu a času $1.",
        "htmlform-title-badnamespace": "Stránka [[:$1]] není ve jmenném prostoru „{{ns:$2}}“.",
        "htmlform-title-not-creatable": "Pod názvem „$1“ nelze vytvořit stránku",
        "htmlform-title-not-exists": "Stránka $1 neexistuje.",
        "htmlform-user-not-exists": "Uživatel <strong>$1</strong> neexistuje.",
        "htmlform-user-not-valid": "<strong>$1</strong> není platné uživatelské jméno.",
-       "sqlite-has-fts": "$1 s podporou plnotextového vyhledávání",
-       "sqlite-no-fts": "$1 bez podpory plnotextového vyhledávání",
        "logentry-delete-delete": "$1 {{GENDER:$2|smazal|smazala}} stránku $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|obnovil|obnovila}} stránku $3",
        "logentry-delete-event": "$1 {{GENDER:$2|změnil|změnila}} viditelnost {{PLURAL:$5|protokolovacího záznamu|$5 protokolovacích záznamů}} ke stránce $3: $4",
        "feedback-external-bug-report-button": "Založit technický úkol",
        "feedback-dialog-title": "Odeslat názor",
        "feedback-dialog-intro": "Pomocí níže zobrazeného jednoduchého formuláře můžete odeslat svůj názor. Váš komentář se spolu s vaším uživatelským jménem přidá na stránku „$1“.",
-       "feedback-error-title": "Chyba",
        "feedback-error1": "Chyba: Nerozpoznaný výsledek z API",
        "feedback-error2": "Chyba: Editace se nezdařila",
        "feedback-error3": "Chyba: API nevrátilo žádnou odpověď",
        "unlinkaccounts-success": "Propojení účtu bylo zrušeno.",
        "authenticationdatachange-ignored": "Změna autentizačních údajů nebyla zpracována. Možná není nakonfigurován žádný poskytovatel?",
        "userjsispublic": "Uvědomte si prosím, že podstránky s JavaScriptem by neměly obsahovat tajné údaje, protože jsou viditelné ostatním uživatelům.",
-       "usercssispublic": "Uvědomte si prosím, že podstránky s CSS by neměly obsahovat tajné údaje, protože jsou viditelné ostatním uživatelům."
+       "usercssispublic": "Uvědomte si prosím, že podstránky s CSS by neměly obsahovat tajné údaje, protože jsou viditelné ostatním uživatelům.",
+       "restrictionsfield-badip": "Neplatná IP adresa nebo rozsah: $1",
+       "restrictionsfield-label": "Povolené rozsahy IP adres:",
+       "restrictionsfield-help": "Jedna IP adresa nebo CIDR rozsah na řádek. Všechno povolíte pomocí<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index c6bd656..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",
        "htmlform-cloner-create": "Tilføj flere",
        "htmlform-cloner-delete": "Fjern",
        "htmlform-cloner-required": "Der kræves mindst en værdi.",
-       "sqlite-has-fts": "$1 med fuld-tekst søgnings support",
-       "sqlite-no-fts": "$1 uden fuld-tekst søgnings support",
        "logentry-delete-delete": "$1 {{GENDER:$2|slettede}} siden $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|gendannede}} siden $3",
        "logentry-delete-event": "$1 {{GENDER:$2|ændrede}} synligheden af {{PLURAL:$5|en loghændelse|$5 loghændelser}} for siden $3: $4",
index d5f52f2..fa332e7 100644 (file)
        "talk": "Diskussion",
        "views": "Ansichten",
        "toolbox": "Werkzeuge",
+       "tool-link-userrights": "{{GENDER:$1|Benutzergruppen}} ändern",
+       "tool-link-emailuser": "E-Mail an {{GENDER:$1|diesen Benutzer|diese Benutzerin}} senden",
        "userpage": "Benutzerseite anzeigen",
        "projectpage": "Projektseite anzeigen",
        "imagepage": "Dateiseite anzeigen",
        "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?",
        "passwordreset-emailelement": "Benutzername: \n$1\n\nTemporäres Passwort: \n$2",
        "passwordreset-emailsentemail": "Falls diese E-Mail-Adresse mit deinem Benutzerkonto verknüpft ist, wird eine Passwort-Zurücksetzungs-E-Mail versandt.",
        "passwordreset-emailsentusername": "Falls es eine E-Mail-Adresse gibt, die mit diesem Benutzernamen verknüpft ist, wird eine Passwort-Zurücksetzungs-E-Mail versandt.",
-       "passwordreset-emailsent-capture2": "Die Passwort-Zurücksetzungs-{{PLURAL:$1|E-Mail wurde|E-Mails wurden}} versandt. {{PLURAL:$1|Der Benutzername und das Passwort|Die Liste der Benutzernamen und Passwörter}} wird unten angezeigt.",
-       "passwordreset-emailerror-capture2": "Das Senden der E-Mail an {{GENDER:$2|den Benutzer|die Benutzerin}} ist fehlgeschlagen: $1 {{PLURAL:$3|Der Benutzername und das Passwort|Die Liste der Benutzernamen und Passwörter}} wird unten angezeigt.",
+       "passwordreset-emailsent-capture2": "Die Passwort-Zurücksetzungs-{{PLURAL:$1|E-Mail wurde|E-Mails wurden}} versandt. {{PLURAL:$1|Der Benutzername und das Passwort|Die Liste der Benutzernamen und Passwörter}} wird hier angezeigt.",
+       "passwordreset-emailerror-capture2": "Das Senden der E-Mail an {{GENDER:$2|den Benutzer|die Benutzerin}} ist fehlgeschlagen: $1 {{PLURAL:$3|Der Benutzername und das Passwort|Die Liste der Benutzernamen und Passwörter}} wird hier angezeigt.",
        "passwordreset-nocaller": "Es muss ein Rufer angegeben werden",
        "passwordreset-nosuchcaller": "Rufer ist nicht vorhanden: $1",
        "passwordreset-ignored": "Die Passwortzurücksetzung konnte nicht verarbeitet werden. Vielleicht wurde kein Dienstanbieter konfiguriert?",
        "upload-dialog-disabled": "Dateiuploads mit diesem Dialog sind auf diesem Wiki deaktiviert.",
        "upload-dialog-title": "Datei hochladen",
        "upload-dialog-button-cancel": "Abbrechen",
+       "upload-dialog-button-back": "Zurück",
        "upload-dialog-button-done": "Schließen",
        "upload-dialog-button-save": "Speichern",
        "upload-dialog-button-upload": "Hochladen",
        "apisandbox-results-fixtoken-fail": "Der „$1“-Token konnte nicht abgerufen werden.",
        "apisandbox-alert-page": "Felder auf dieser Seite sind nicht gültig.",
        "apisandbox-alert-field": "Der Wert dieses Feldes ist nicht gültig.",
+       "apisandbox-continue": "Fortfahren",
+       "apisandbox-continue-clear": "Löschen",
+       "apisandbox-continue-help": "Mit „{{int:apisandbox-continue}}“ kann man die letzte Anfrage [https://www.mediawiki.org/wiki/API:Query#Continuing_queries fortfahren]; „{{int:apisandbox-continue-clear}}“ löscht fortsetzungsbezogene Parameter.",
        "booksources": "ISBN-Suche",
        "booksources-search-legend": "Suche nach Bezugsquellen für Bücher",
        "booksources-search": "Suchen",
        "htmlform-cloner-create": "Weitere hinzufügen",
        "htmlform-cloner-delete": "Entfernen",
        "htmlform-cloner-required": "Es ist mindestens ein Wert erforderlich.",
+       "htmlform-date-placeholder": "JJJJ-MM-TT",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "JJJJ-MM-TT HH:MM:SS",
+       "htmlform-date-invalid": "Der eingegebene Wert ist kein erkanntes Datum. Versuche die Verwendung des Formats JJJJ-MM-TT.",
+       "htmlform-time-invalid": "Der eingegebene Wert ist keine erkannte Zeit. Versuche die Verwendung des Formats HH:MM:SS.",
+       "htmlform-datetime-invalid": "Der eingegebene Wert ist kein erkanntes Datum und keine Zeit. Versuche die Verwendung des Formats JJJJ-MM-TT HH:MM:SS.",
+       "htmlform-date-toolow": "Der eingegebene Wert liegt vor dem frühesten erlaubten Datum $1.",
+       "htmlform-date-toohigh": "Der eingegebene Wert liegt nach dem spätesten erlaubten Datum $1.",
+       "htmlform-time-toolow": "Der eingegebene Wert liegt vor der frühesten erlaubten Zeit $1.",
+       "htmlform-time-toohigh": "Der eingegebene Wert liegt nach der spätesten erlaubten Zeit $1.",
+       "htmlform-datetime-toolow": "Der eingegebene Wert liegt vor dem frühesten erlaubten Datum und der Zeit $1.",
+       "htmlform-datetime-toohigh": "Der eingegebene Wert liegt nach dem spätesten erlaubten Datum und der Zeit $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.",
        "htmlform-user-not-exists": "<strong>$1</strong> ist nicht vorhanden.",
        "htmlform-user-not-valid": "<strong>$1</strong> ist kein gültiger Benutzername.",
-       "sqlite-has-fts": "Version $1 mit Unterstützung für die Volltextsuche",
-       "sqlite-no-fts": "Version $1 ohne Unterstützung für die Volltextsuche",
        "logentry-delete-delete": "$1 {{GENDER:$2|löschte}} Seite $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|stellte}} Seite $3 wieder her",
        "logentry-delete-event": "$1 {{GENDER:$2|änderte}} die Sichtbarkeit {{PLURAL:$5|eines Logbucheintrags|von $5 Logbucheinträgen}} auf $3: $4",
        "feedback-external-bug-report-button": "Eine technische Aufgabe einreichen",
        "feedback-dialog-title": "Rückmeldung senden",
        "feedback-dialog-intro": "Du kannst das einfache Formular unten verwenden, um deine Rückmeldung einzureichen. Dein Kommentar wird zusammen mit deinem Benutzernamen zur Seite „$1“ hinzugefügt.",
-       "feedback-error-title": "Fehler",
        "feedback-error1": "Fehler: Unbekanntes Ergebnis der API",
        "feedback-error2": "Fehler: Bearbeitung gescheitert",
        "feedback-error3": "Fehler: Keine Antwort von der API",
        "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 fa0653f..c6df7b5 100644 (file)
@@ -24,7 +24,8 @@
                        "Macofe",
                        "Matma Rex",
                        "Kumkumuk",
-                       "Gırd"
+                       "Gırd",
+                       "Velg"
                ]
        },
        "tog-underline": "Bınê gırey de xete bance:",
@@ -42,6 +43,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 +53,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 +61,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 +70,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-empty": "''Ena kategoriye de hewna qet nuştey ya zi medya çıniyê.''",
        "hidden-categories": "{{PLURAL:$1|Kategoriya nımıtiye|Kategoriyê nımıtey}}",
        "hidden-category-category": "Kategoriyê nımıtey",
-       "category-subcat-count": "{| border=\"1\" cellpadding=\"2\" cellspacing=\"0\" align=\"left\" style=\"margin-left:1em; background:khaki; border: 1px #aaa solid; border-collapse: collapse; font-size: 250%;\"\n| align=\"center\" |{{PLURAL:$2|Na kategori de $1 bınkategoriy est ê.|$2 kategoriyan ra $1 kategoriyê bınêni asenê.}} \n|-\n| align=\"center\" |(K) Kategoriye (D) Dosya (P) Peli (M)  Medya\n|}",
+       "category-subcat-count": "{{PLURAL:$2|Na kategoriye de tenya na bınkategoriye esta.|Na kategoriye de, $2 ra pêro piya, {{PLURAL:$1|bınkategoriye esta|$1 bınkategoriy estê}}.}}",
        "category-subcat-count-limited": "Na kategoriye de {{PLURAL:$1|na kategoriya bınêne esta|nê $1 kategoriyê bınêni estê}}.",
-       "category-article-count": "{| border=\"1\" cellpadding=\"2\" cellspacing=\"0\" align=\"left\" style=\"margin-left:1em; background:khaki; border: 1px #aaa solid; border-collapse: collapse; font-size: 250%;\"\n| align=\"center\" |{{PLURAL:$2|Na kategori de teyna ena perr esta.|pêro piya $2 ra  {{PLURAL:$1|ena perra na kategori de ya|$1 perri na kategori de yê.}}}}\n|}",
+       "category-article-count": "{{PLURAL:$2|Na kategoriye de teyna ena pele esta.|Ebe $2 ra pêro piya {{PLURAL:$1|ena pele na kategoriye dera|$1 enê peli na kategoriye derê}}.}}",
        "category-article-count-limited": "{{PLURAL:$1|Pela cêrêne|$1 Pelê cêrêni}} na kategoriye derê.",
        "category-file-count": "{{PLURAL:$2|Na kategori tenya dosya ya cêri muhtewa kena.|Na kategori de $2 ra pêro piya {{PLURAL:$1|1 dosya est a|$1 dosyey est ê}}.}}",
        "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ıtekelne",
        "moredotdotdot": "Vêşi...",
-       "morenotlisted": "Vêşi lista nêbi...",
+       "morenotlisted": "Na lista qay kemi ya.",
        "mypage": "Pele",
        "mytalk": "Mesac",
        "anontalk": "Werênayış",
        "specialpage": "Pela xısusiye",
        "personaltools": "Hacetê şexsiy",
        "articlepage": "Pera zerreki bıvin",
-       "talk": "Werênayış",
+       "talk": "Behs",
        "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-description": "Pela seri",
        "policy-url": "Project:Terzê hereketi",
        "portal": "Portalê cemaeti",
-       "portal-url": "Project:Portalê cemaeti",
+       "portal-url": "Project:Portalë Å\9fëlıgi",
        "privacy": "Politikaya nımıteyiye",
        "privacypage": "Project:Xısusiyetê nımıtışi",
        "badaccess": "Xeta mısadey",
        "nstab-template": "Şablon",
        "nstab-help": "Pela peşti",
        "nstab-category": "Kategoriye",
-       "mainpage-nstab": "Pela seri",
+       "mainpage-nstab": "Pera esas",
        "nosuchaction": "Fealiyeto wınasi çıniyo",
        "nosuchactiontext": "URL ra kar qebul nêbı.\nŞıma belka URL şaş nuşt, ya zi gıreyi şaş ra ameyi.\nKeyepelê {{SITENAME}} eşkeno xeta eşkera bıkero.",
        "nosuchspecialpage": "Pela xasa wınasiye çıniya",
        "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-emailtext-ip": "Jeweri, {{SITENAME}} ra (ma heta şımayê, $1 IP adresi ra) ($4) teferuatê hesabdê şıma  va wa biyaro xo viri. Karbero ke cêrdeyo {{PLURAL:$3|hesaba|eno hesaba}} ena e-posta adresiya aleqey cı esto:\n\n$2\n\n{{PLURAL:$3|ena parola idaretena|ena parola idareten}} {{PLURAL:$5|jew roc|$5  roca}}rêya.\nEna parolaya deqewe de u xorê ju parolaya newi bıweçine. Parolaya şıma emaya şıma viri se  yana  ena e-posta şıma nê weştase u şıma qayıl niye parolaya xo bıvurnese, ena mesacer peygoş bıkerê.",
        "passwordreset-emailtext-user": "$1 enê karberi, {{SITENAME}}  ra ($4) teferuatê hesab dê şıma  va wa biyaro xo viri. Karbero ke cêrdeyo {{PLURAL:$3|hesaba|eno hesaba}} ena e-posta adresiya aleqey cı esto:\n\n$2\n\n{{PLURAL:$3|ena parola idaretena|ena parola idareten}} {{PLURAL:$5|jew roc|$5  roca}}rêya.\nEna parolaya deqewe de u xorê ju parolaya newi bıweçine. Parolaya şıma emaya şıma viri se  yana  ena e-posta şıma nê weştase u şıma qayıl niye parolaya xo bıvurnese, ena mesacer peygoş bıkerê.",
        "passwordreset-emailelement": "Nameyê karberi: \n$1\n\nParolaya vêrdiye: \n$2",
-       "passwordreset-emailsentemail": "Eke na seba hesabê şıma yew adresa e-posteyê qeydına, yew e-posteyê parola nênkerdışi rışiyeno.",
+       "passwordreset-emailsentemail": "Eger kı ena e-posta aitê şımaya se, jew e-posta do bırışi yo ena hesab.",
        "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:",
        "hr_tip": "Xeta verardiye (teserrufın bıgureyne/bıxebetne)",
        "summary": "Xulasa:",
        "subject": "Mewzu:",
-       "minoredit": "Vurriyayışo werdiyo",
+       "minoredit": "No yew vurnayışo werdiyo",
        "watchthis": "Na pele seyr ke",
        "savearticle": "Qeyd ke",
        "savechanges": "Vurnayışan qeyd ke",
        "newarticle": "(Newe)",
        "newarticletext": "To yew gıre tıkna be ra yew pela ke hewna çıniya.\nSeba afernayışê pele ra, qutiya metnê cêrêni bıgurene (seba melumati qaytê [$1 pela peşti] ke).\nEke be ğeletine ameya tiya, wa gocega <strong>peyser</strong>i programê xo de bıtıkne.",
        "anontalkpagetext": "----''Na per, perêk kı karbero hesab a nêkerdeyan o, ya zi karbero hesab akerdeyan o labele pê hesabê xo nêkewto de. No sebeb ra ma IP adres xebetneno û ney IP adresan herkes nêşeno bıvino. Eke şıma qayil niye ina bo xorê [[Special:CreateAccount|yew hesab bıvıraze]] veya xut [[Special:UserLogin|hesab akere]].''",
-       "noarticletext": "Ena pele de hewna theba çıniyo.\nTı şenê zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|qandê  sernameyê ena pele cı geyre]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre],\nya zi [{{fullurl:{{FULLPAGENAME}}|action=edit}} ena pele vıraze]</span>.{{MediaWiki mesaca pera newi}}",
+       "noarticletext": "Ena perrer de hewna theba çıni yo.\nTı şenê zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|qandê  sernameyê ena pele cı geyre]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre],\nya zi [{{fullurl:{{FULLPAGENAME}}|action=edit}} ena pele vıraze]</span>.{{MediaWiki mesaca pera newi}}",
        "noarticletext-nopermission": "Ena pele de hewna theba çıniyo.\nTı şenay zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|seba sernameyê na pele cı geyre]], ya zi <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre]</span>, ema destur çıniyo ke na pele vırazê.",
        "missing-revision": "Rewizyonê name dê pela da #$1 \"{{FULLPAGENAME}}\" dı çıniyo.\n\nNo normal de tarix dê pelanê besterneyan dı ena xırabin asena.\nDetayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} tiya dı] aseno.",
        "userpage-userdoesnotexist": "Hesabê karberi \"<nowiki>$1</nowiki>\" qeyd nêbiyo.\nKerem ke, tı ke wazenay na pele bafernê/bıvurnê, qontrol ke.",
        "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",
        "viewprevnext": "($1 {{int:pipe-separator}} $2) ($3) bıvênên",
        "searchmenu-exists": "''Ena 'Wikipediya de ser \"[[:$1]]\" yew pel esto'''",
        "searchmenu-new": "<strong>Na wiki de pela \"[[:$1]]\" vıraze!</strong> {{PLURAL:$2|0=|Sewbina pela ke şıma geyrayê cı aye bıvênê.|Yew zi neticanê cıgeyrayışê xo bıvênê.}}",
-       "searchprofile-articles": "Pelê zerreki",
-       "searchprofile-images": "Multimedya",
-       "searchprofile-everything": "Heme çi",
-       "searchprofile-advanced": "Raverşiyaye",
+       "searchprofile-articles": "Perrê muhteway",
+       "searchprofile-images": "Zafınmedya",
+       "searchprofile-everything": "Pêro çi",
+       "searchprofile-advanced": "Herayen",
        "searchprofile-articles-tooltip": "$1 de cı geyre",
        "searchprofile-images-tooltip": "Dosya cı geyre",
        "searchprofile-everything-tooltip": "Tedeesteyan hemine cı geyre (pelanê mınaqeşeyi zi tey)",
        "searchprofile-advanced-tooltip": "Cayê nameyanê xısusiyan de cı geyre",
        "search-result-size": "$1 ({{PLURAL:$2|1 çeku|$2 çekuy}})",
        "search-result-category-size": "{{PLURAL:$1|1 eza|$1 ezayan}} ({{PLURAL:$2|1 kategoriyê bini|$2 kategirayanê binan}}, {{PLURAL:$3|1 dosya|$3 dosyayan}})",
-       "search-redirect": "($1 ra ardış)",
+       "search-redirect": "($1 ra kırışiyè)",
        "search-section": "(qısmê $1)",
        "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)",
        "grant-basic": "Heqê basiti",
        "grant-viewdeleted": "Besteryaya peran u dosyaya bıasne",
        "grant-viewmywatchlist": "Lista serykerdışê xo bıvêne",
-       "newuserlogpage": "Cıkewtışê hesabvıraştışi",
-       "newuserlogpagetext": "Ena log de viraştişê karberî esta.",
+       "newuserlogpage": "Roceka karberanê newa",
+       "newuserlogpagetext": "No yew qeydê afernayışanê karberio.",
        "rightslog": "Qeydê heqanê karberi",
        "rightslogtext": "Ena listeyê loganê ke heqqa karbaranî mucneno.",
        "action-read": "ena pela wanayış",
        "recentchanges-noresult": "Goreyê kriteranê kıfşkerdeyan ra qet yew vurnayış nêvêniya.",
        "recentchanges-feed-description": "Ena feed dı vurnayişanê tewr peniyan teqip bık.",
        "recentchanges-label-newpage": "Enê vurnayışi ra yu pera newi vıraziya ya",
-       "recentchanges-label-minor": "Vurriyayışo werdiyo",
+       "recentchanges-label-minor": "No yew vurnayışo werdiyo",
        "recentchanges-label-bot": "Eno vurnayış terefê yew boti ra vıraziyo",
        "recentchanges-label-unpatrolled": "Eno vurnayış hewna dewriya nêbiyo",
        "recentchanges-label-plusminus": "Ebadê pele de bazê bayti de vayeyê cı",
        "recentchanges-submit": "Bımocne",
        "rcnotefrom": "Cêr de <strong>$2</strong> ra nata {{PLURAL:$5|vurnayışiyê}} asenê (tewr vêşi <strong>$1</strong> asenê) <strong>$3, $4</strong>",
        "rclistfrom": "$3 $2 ra tepiya vurnayışanê neweyan bımocne",
-       "rcshowhideminor": "vurriyayışê werdi $1",
+       "rcshowhideminor": "Vurriyayışê werdiy $1",
        "rcshowhideminor-show": "Bımocne",
        "rcshowhideminor-hide": "Bınımne",
        "rcshowhidebots": "botan $1",
        "randomredirect": "Serçarnayışo rastameye",
        "randomredirect-nopages": "Cayê nameyê \"$1\" de serşıkıtışi çıniyê.",
        "statistics": "İstatistiki",
-       "statistics-header-pages": "İstatistikê pele",
+       "statistics-header-pages": "İstatıstıkê perrer",
        "statistics-header-edits": "İstatistikê vurnayışan",
        "statistics-header-users": "İstatistikê karberi",
        "statistics-header-hooks": "Yewbina istatistiki",
-       "statistics-articles": "Pelê zerreki",
+       "statistics-articles": "Meqaley",
        "statistics-pages": "Peli",
        "statistics-pages-desc": "Wiki de peley pêro, kategoriy, hetenayışi wesaire...",
        "statistics-files": "Dosyayê bar biye",
        "speciallogtitlelabel": "Meqsed (sername ya zi {{ns:user}}:karberi rê nameyê karberi):",
        "log": "Qeydi",
        "logeventslist-submit": "Bımocne",
-       "all-logs-page": "Umumi qeydi pêro",
+       "all-logs-page": "Qeydê umumi pêro",
        "alllogstext": "qey {{SITENAME}}i mocnayişê heme rocaneyani.\ntipa rocaneyi, nameyê karberi (herfa pil u qıci re hessas a), ya zi peli (reyna hessasiyê herfa pil u qıciyi) bıweçine u esayiş qıc kerê.",
        "logempty": "Qeydan dı malumato unasin çıni yo.",
-       "log-title-wildcard": "sername yê ke pê ney nuşteyi destkenêpê bıgêr.",
+       "log-title-wildcard": "Sernameyê ke be nê nuşteyi ra destkenê pê, cıgeyre",
        "showhideselectedlogentries": "Qeydê weçinayışê bımocne/bınımne dekerê",
        "log-edit-tags": "Etiketanê weçinayê qeydan bıvurnê",
        "checkbox-select": "Weçinaye: $1",
        "emailsubject": "Mewzu:",
        "emailmessage": "Mesac:",
        "emailsend": "Bırışe",
-       "emailccme": "kopyayekê mesaji mı re bıerşaw",
+       "emailccme": "Ju kopya ya mesaci bırş mı rê?",
        "emailccsubject": "$2 kopyaya mesaj a ke şıma erşawıto/a $1:",
        "emailsent": "E-poste rışna",
        "emailsenttext": "e-mailê şıma erşawiya/ruşiya",
        "usermessage-summary": "Mesacê sistemi caverde.",
        "usermessage-editor": "Xeberdarê sistemi",
        "usermessage-template": "MediaWiki:UserMessage",
-       "watchlist": "Lista seyrkerdışi",
-       "mywatchlist": "Lista seyrkerdışi",
+       "watchlist": "Listey pawıteyan",
+       "mywatchlist": "Listey pawıteyan",
        "watchlistfor2": "Qandê $1 ($2)",
        "nowatchlist": "listeya temaşa kerdıişê şıma de yew madde zi çina.",
        "watchlistanontext": "qey vurnayişê maddeya listeya temaşakerdiş ronıştış akerê",
        "watchlist-hide": "Bınımne",
        "watchlist-submit": "Bımocne",
        "wlshowtime": "Periyoda zemani asenayışi:",
-       "wlshowhideminor": "vurriyayışê werdi",
+       "wlshowhideminor": "vurriyayışê werdiy",
        "wlshowhidebots": "boti",
        "wlshowhideliu": "karberê qeydıni",
        "wlshowhideanons": "karberê anonimi",
        "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": "Tenya iştıraqanê karberanê neweyan bımocne",
        "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-blocklog": "qeydê kılitkerdışi",
+       "sp-contributions-deleted": "iştırakê {{GENDER:$1|karberi}} esterdi",
        "sp-contributions-uploads": "Barkerdışi",
        "sp-contributions-logs": "qeydi",
        "sp-contributions-talk": "werênayış",
        "sp-contributions-search": "Dekerdena cı geyrê",
        "sp-contributions-username": "Adresa IPy ya zi nameyê karberi:",
        "sp-contributions-toponly": "Tenya rewizyonanê tewr peyniyan bimocne",
+       "sp-contributions-hideminor": "Vurriyayışanê werdiyan bınımne",
        "sp-contributions-submit": "Cı geyre",
        "whatlinkshere": "Linkê tedeestey",
        "whatlinkshere-title": "Per da \"$1\" rê perê ke gre danê",
        "contribslink": "iştıraki",
        "emaillink": "e-poste bırışe",
        "autoblocker": "Şıma otomatikmen kılit biy, çıke adresa şımaya ''IP''y terefê \"[[User:$1|$1]]\" gureniyena.\nSebebê kılitbiyayışê $1'i \"$2\"o",
-       "blocklogpage": "Qeydê astengi",
+       "blocklogpage": "Qeydê kılitkerdışi",
        "blocklog-showlog": "verniyê no/na karberi cıwa ver geriyayo/ya.",
        "blocklog-showsuppresslog": "verniyê no/na karberi cıwa ver geriyayo/ya.",
        "blocklogentry": "[[$1]] biyo bloqe, sebeb: $3, hetana $2 do bıramo.",
        "tooltip-pt-anonuserpage": "pelê karberê IPyi",
        "tooltip-pt-mytalk": "Pela {{GENDER:|toya}} werênayışi",
        "tooltip-pt-anontalk": "vurnayiş ê ke no Ipadresi ra biyo muneqeşa bıker",
-       "tooltip-pt-preferences": "Tercihê {{GENDER:|şıma}}",
+       "tooltip-pt-preferences": "Tercihê {{GENDER:|to}}",
        "tooltip-pt-watchlist": "Lista pelanê ke to gırewtê seyrkerdış",
        "tooltip-pt-mycontris": "Yew lista iştırakanê {{GENDER:|şıma}}",
        "tooltip-pt-login": "Mayê şıma ronıştış akerdışi rê dawet keme; labelê ronıştış mecburi niyo",
        "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",
        "htmlform-chosen-placeholder": "Opsiyon weçine",
        "htmlform-cloner-create": "Tayêna cı ke",
        "htmlform-cloner-delete": "Wedare",
-       "sqlite-has-fts": "$1 tam-metn destegê cı geyrayışiya piya",
-       "sqlite-no-fts": "$1 tam-metn bê destegê cı geyrayışi",
        "logentry-delete-delete": "$1 pela $3 {{GENDER:$2|esterıte}}",
        "logentry-delete-restore": "$1 pela $3 {{GENDER:$2|peyser arde}}",
        "logentry-delete-event": "$1 $3: $4 de asayışê {{PLURAL:$5|cıkerdışi|cıkerdışan}} {{GENDER:$2|vurna}}",
        "special-characters-title-minus": "işaretê kemiye",
        "mw-widgets-dateinput-placeholder-day": "SSSS-AA-RR",
        "mw-widgets-dateinput-placeholder-month": "SSSS-AA",
-       "mw-widgets-titleinput-description-redirect": "berd be $1"
+       "mw-widgets-titleinput-description-redirect": "berd be $1",
+       "log-action-filter-newusers": "Babetê hesabvıraştışi:"
 }
index 7dd59f3..0f6d5c0 100644 (file)
@@ -14,7 +14,7 @@
        "tog-hideminor": "अहिलका मामूली सम्पादनलाई लुकाउन्या",
        "tog-hidepatrolled": "गस्ती(patrolled)सम्पादनलाई लुकाउन्या",
        "tog-newpageshidepatrolled": "गस्ती गरिया पानानलाई नयाँ पाना  सूचीबठेई लुकाउन्या",
-       "tog-hidecategorization": "पà¥\83षà¥\8dठहरà¥\82à¤\95à¥\8b à¤¶à¥\8dरà¥\87णà¥\80à¤\95रण à¤¹à¤\9fाया",
+       "tog-hidecategorization": "पनà¥\8dनाà¤\85नà¥\8b à¤¶à¥\8dरà¥\87णà¥\80à¤\95रण à¤²à¥\81à¤\95ाऽ",
        "tog-extendwatchlist": "निगरानी सूचीलाई सबै परिवर्तन धेकुन्या गरी बढुन्या , ऐईलका बाहेक",
        "tog-usenewrc": "पानाका अहिलका  परिवर्तन र अवलोकन सूचीका आधारमी सामूहिक परिवर्तनहरू",
        "tog-numberheadings": "शीर्षकहरूलाई स्वत:अङ्कित गर",
@@ -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": "खाली सम्पादन शीर्षक प्रविष्टि गरेपछा मलाई सोधन्या",
        "tog-watchlistreloadautomatically": "जज्ज्याँलै फिल्टर बदेलिन्छ इच्छासूची आफुइ रिलोड अर: (जावास्क्रिप्ट चायीन्छ)",
        "tog-watchlisthideanons": "अज्ञात प्रयोगकर्ताहरूबाट गरिएको सम्पादन ध्यान सूचीबठेई लुकाउन्या",
        "tog-watchlisthidepatrolled": "बोट सम्पादनहरू ध्यान सूचीबठेई लुकाउन्या",
-       "tog-watchlisthidecategorization": "पà¥\83षà¥\8dठहरà¥\82à¤\95à¥\8b à¤¶à¥\8dरà¥\87णà¥\80à¤\95रण à¤²à¥\81à¤\95à¥\8cनà¥\8dया",
+       "tog-watchlisthidecategorization": "पनà¥\8dनाà¤\85नà¥\8b à¤¶à¥\8dरà¥\87णà¥\80à¤\95रण à¤²à¥\81à¤\95ाऽ",
        "tog-ccmeonemails": "मुईले अन्य प्रयोगकर्ताहरूलाई पठाउन्या इ-मेलको प्रतिलिपि मुईलाई पठाउन्या",
        "tog-diffonly": "तलका पानाहरुको भिन्नहरू सामग्री नदेखाउन्या",
        "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": "खोल अथवा ब्राउजर पैलीकाजसो",
        "qbpageoptions": "ये पानो",
        "qbmyoptions": "मेरो पानो",
        "faq": "भौत सोधिन्या प्रश्नहरू",
-       "faqpage": "Project:भà¥\8cत à¤¸à¥\8bधिà¤\8fà¤\95ा à¤ªà¥\8dरशà¥\8dनहरà¥\81",
+       "faqpage": "Project:भà¥\8cत à¤¸à¥\8bधियाà¤\95ा à¤ªà¥\8dरशà¥\8dनहरà¥\82",
        "actions": "कार्यहरू",
        "namespaces": "नेमस्पेस",
        "variants": "बहुरुपअन",
        "talk": "कुरणिकाआनी",
        "views": "अवलोकन गरऽ",
        "toolbox": "औजारअन",
+       "tool-link-userrights": "परिवर्तन{{GENDER:$1|प्रयोगकर्ता}}समूहहरू",
+       "tool-link-emailuser": "{{GENDER:$1|प्रयोगकर्ता}}लाई एइ इमेलमी पठाऽ",
        "userpage": "प्रयोगकर्ता पाना हेर्न्या",
        "projectpage": "प्रोजेक्ट पानो हेर्न्या",
        "imagepage": "चित्र पानो हेर",
        "databaseerror-query": "अनुरोध: $1",
        "databaseerror-function": "फङ्सन : $1",
        "databaseerror-error": "गल्ती: $1",
+       "transaction-duration-limit-exceeded": "To avoid creating high replication lag, this transaction was aborted because the write duration ($1) exceeded the $2 second limit.\nIf you are changing many items at once, try doing multiple smaller operations instead.",
        "laggedslavemode": "<strong>चेतावनी:</strong> पानामी हालका अद्यतनहरू नहुनस्कदान ।",
        "readonly": "डेटाबेस बन्द गरिया छ",
        "enterlockreason": "ताल्चा मार्नुको कारण दिया, साथै ताल्चा हटाउने समयको अवधि अनुमान लगा।",
        "title-invalid-interwiki": "अनुरोध गरियाको शिर्षकमी अन्तर विकि लिङ्क छ जइलाई शिर्षकमी प्रयोग गद्द नाइपाइनो ।",
        "title-invalid-talk-namespace": "निवेदन गरियाको पानाको शिर्षकले उपलब्ध नभएका कुरडी पानालाई सन्दर्भको रूपमी राख्याको छ ।",
        "title-invalid-characters": "निवेदन गरियाको यै पानाको शिर्षकमी अवैध अक्षर रयाको छः \"$1\" ।",
+       "title-invalid-relative": "शीर्षक एउटा सन्दर्भित मार्ग राख्दछ। सन्दर्भित पृष्ठको शीर्षक (./, ../)अमान्य छ, किनकि त्यो प्राय रूपले पहुँच बाहिर हुन्छ जब त्यसलाई प्रायोगकर्ताको ब्राउजरबाट प्रयोगमी ल्याउने प्रयास गर्ने गरिन्छ।",
        "title-invalid-magic-tilde": "अनुरोध अरिया: पन्ना: शीर्षकमी अमान्य म्याजिक टिल्ड शृङ्खला छ (<nowiki>~~~</nowiki>)।",
        "title-invalid-too-long": "अनुरोध अरिया: पन्ना: शीर्षक भौत लामु छ। यो UTF-8 इनकोडिङमी $1 {{PLURAL:$1|byte|bytes}} है लामु हुनु हुनैन।",
        "title-invalid-leading-colon": "निवेदन गरिया पृष्ठको शिर्षकको शुरूमी अवैध कोलोन रया छ ।",
        "viewsource": "स्रोत हेर",
        "viewsource-title": " $1 को स्रोत हेर",
        "actionthrottled": "कार्य रोकिईयो",
-       "actionthrottledtext": "स्पामको रोकथामको लागि , तमीलाई यो कार्य नापै समयमी मैथै पटक गद्दाबठे सिमित गरियाको छ, र तमीले आफ्नो सिमा पार गरिसक्याछौ ।\nकृपया केही मिनेट पछि पुन: प्रयास गर  ।",
+       "actionthrottledtext": "स्पाम रोकथाम खिलाइ, तमलाई येइ काम थोक्काइ बगतमी झिक्कै फेर अद्दा बठेइ सीमित अरीरैछ, रे तमले आफुनी सीमा पार अरिसकिराइछऽ।\nकृपया केइ मिनट पछा दोसर्‍याँ प्रयास अर्याऽ।",
        "protectedpagetext": "यो पृष्ठ सम्पादन हुनबठे बचाउन सम्पादनमी तथा अन्य कार्यमी रोक लगाइया छ।",
        "viewsourcetext": "तम ये पृष्ठको स्रोत हेद्दु सकुन्छौ और उईको नक्कल उताद्दु सकुन्छौ |",
        "viewyourtext": "यै पानामी रह्याका '''तमरा सम्पादनहरू''' हेद्द या प्रतिलिपी गद्द सक्द्या हौ :",
+       "protectedinterface": "यो पृष्ठले सफ्टवेयरको लागि अन्तरमोहडा पाठ प्रदान गर्दछ , र यसलाई दुरुपयोग हुनबाट बचाउन सुरक्षा प्रदान गरिएको छ।\nसम्पूर्ण विकिहरूका लागि अनुवादमी परिवर्तन गर्नको लागि [https://translatewiki.net/ translatewiki.net], प्रयोग गर्नुहोस् ,  मिडियाविकि स्थानियकरण परियोजना ।",
        "editinginterface": "<strong>चेतावनी:</strong> तमी यै पानालाई सम्पादन गद्द लाग्याछौ, जनले सफ्टवेयरको लागि \nइन्टरफेस सामग्रीहरू प्रदान गरन्छ।\nयै पानामी गरियाको परिवर्तनले यै विकिमी अरु प्रयोगकर्तानको इन्टरफेसको प्रदर्शनमी प्रभाव पडन्छ ।",
        "translateinterface": "सप्पै विकिइनखिलाइ अनुवाद थप्दाइ या बदेल्लाइ, कृपया [https://translatewiki.net/ translatewiki.net]को प्रयोग अर:, मिडियाविकि क्षेत्रीयकरण परियोजना:।",
+       "cascadeprotected": "यो पृष्ठ सम्पादन गर्नबाट सुरक्षित गरिएकोछ किनभनें {{PLURAL:$1|पृष्ठ |पृष्ठहरू}}मा सुरक्षित गर्नुका साथै प्रपात (\"cascading\") विकल्प खुल्ला राखिएको छ:\n$2",
        "namespaceprotected": "तमलाई '''$1'''  नेमस्पेसमी रह्याका पानाहरू सम्पादन गद्या अनुमति छैन ।",
        "customcssprotected": "तमलाई यो  पानो सम्पादन गद्दे अनुमति छैन, किनकी यैमी कुनै अर्को प्रयोगकर्ताको व्यक्तिगत अभिरुचीहरू संग्रहित छन् ।",
        "customjsprotected": "तमलाई यो जाभास्कृप्ट पानो सम्पादन गद्दे अनुमति छैन, किनकी यैमी कुनै अर्को प्रयोगकर्ताको व्यक्तिगत अभिरुचीहरू संग्रहित छन् ।",
        "createacct-yourpasswordagain-ph": "आँजि पासवर्ड भरऽ",
        "userlogin-remembermypassword": "मुलाई अघाडी झान्या काम गराइराख्या",
        "userlogin-signwithsecure": "सुक्षित जडान प्रयोग गद्द्या",
-       "cannotlogin-title": "à¤\85à¤\88ल à¤­à¤¿à¤¤à¤° à¤\9dान à¤¨à¤¾à¤\87à¤\81 à¤ªà¤¾à¤\88नो",
+       "cannotlogin-title": "भितर à¤\9dान à¤¨à¤¾à¤\87à¤\81सà¤\95ियो",
        "cannotlogin-text": "येइमी लगइन सम्भव नाइथिन।",
        "cannotloginnow-title": "अईल भितर झान नाइँ पाईनो",
        "cannotloginnow-text": "भितर जान असंभव छ जब प्रयोग $1|",
        "changepassword-success": "तमरो पासवर्ड सफलतापूर्वक परिवर्तन भयो!",
        "changepassword-throttled": "तमले अलै भौत फेर प्रवेशका निम्ति प्रयास गरया छौ।\nकृपया $1 थोक्कै जागी मात्र प्रयास गर।",
        "botpasswords": "बोट पासवर्ड",
+       "botpasswords-createnew": "नौलो बोट पासवर्ड बनाऽ",
+       "botpasswords-editexisting": "भयाऽ बोट पासवर्ड सम्पादन अरऽ",
        "botpasswords-label-appid": "बोट नाम:",
        "botpasswords-label-create": "सृजना गर",
        "botpasswords-label-update": "नयाँ बनाउनु",
        "botpasswords-label-resetpassword": "पासवर्ड पूर्वनिर्धारित गर",
        "botpasswords-label-grants": "अनुदान आवेदन:",
        "botpasswords-label-grants-column": "प्रदान भयो",
+       "botpasswords-bad-appid": "बोट नाउँ \"$1\" नाइमाणीनो।",
+       "botpasswords-insert-failed": "बोट नाउँ \"$1\" थप्दाइ असफल। कि यो पैली थपीसकीरैछ?",
+       "botpasswords-update-failed": "बोट नाउँ \"$1\" अपडेट अद्दाइ असफल। कि यो मेट्याऽ हो?",
        "botpasswords-created-title": "बोट को पासवर्ड बन्यो",
+       "botpasswords-created-body": "प्रयोगकर्ता \"$2\" को बोट नाउँ \"$1\" खिलाइ बोट पासवर्ड बनायियो।",
        "botpasswords-updated-title": "बोट को पासवर्ड अपडेट भयो",
        "botpasswords-deleted-title": "बोट को पासवर्ड मेटियो",
        "resetpass_forbidden": "पासवर्ड परिवर्तन गर्न नाइँमिल्लो",
        "continue-editing": "सम्पादन क्षेत्रमी जाओ",
        "editing": "$1 सम्पादन अरीन्नाछ़",
        "creating": "$1 बनाइँदै",
-       "editingsection": "$1 (à¤\96णà¥\8dड) à¤¸à¤®à¥\8dपादन à¤\97रिदà¥\88",
+       "editingsection": "$1 (à¤\96णà¥\8dड) à¤¸à¤®à¥\8dपादन à¤\85रà¥\80नà¥\8dनाà¤\9b़",
        "editingcomment": "$1 सम्पादन गर्दै(नयाँ खण्ड)",
        "editconflict": "सम्पादन बाँझ्यो: $1",
        "yourtext": "तमरा पाठहरू",
        "grouppage-user": "{{ns:project}}:प्रयोगकर्ताहरू",
        "grouppage-autoconfirmed": "{{ns:project}}:स्वतःपुष्टि भयाऽ प्रयोगकर्ताअन",
        "grouppage-bot": "{{ns:project}}:बोटअन",
-       "grouppage-sysop": "{{ns:project}}:पà¥\8dरबनà¥\8dधà¤\95हरà¥\82",
+       "grouppage-sysop": "{{ns:project}}:वà¥\8dयवसà¥\8dथापà¤\95à¤\85न",
        "grouppage-bureaucrat": "{{ns:project}}:प्रशासकअन",
        "grouppage-suppress": "{{ns:project}}:लुकौन्या",
        "right-read": "पृष्ठहरू पढ",
        "protect-default": "सब्बै प्रयोगकर्तानहरूलाई अनुमति दिन्या",
        "protect-level-autoconfirmed": "नौला तथा दर्ता भयाका प्रयोगकर्तानलाई मात्र अनुमति दिन्या",
        "protect-cascade": "यै पानामी संलग्न सुरक्षित पानाहरू (लामबद्द सुरक्षा)",
+       "protect-expiry-options": "२ घण्टाहरू:2 hours,१ दिन :1 day,३ दिनहरू:3 days,१ हप्ता:1 week,२ हप्ताहरू:2 weeks,१ महिना:1 month,३ महिनाहरू:3 months,६ महिनाहरू:6 months,१ वर्ष:1 year,अनगिन्ती:infinite",
+       "restriction-type": "अनुमति:",
        "pagesize": "(अक्षरहरू)",
        "undeletepage": "मेट्याका पानाहरू हेद्या र पूर्वरुपमी फर्काउन्या",
        "undeleterevisions": "$1 {{PLURAL:$1|संशोधन|संशोधनहरू}} संग्रहित",
        "whatlinkshere-links": "← लिंकहरू",
        "whatlinkshere-hideredirs": "$1 पुन:निर्देशित हुन्छ",
        "whatlinkshere-hidetrans": "$1 सम्मील",
-       "whatlinkshere-hidelinks": "$1 लिङ्क",
+       "whatlinkshere-hidelinks": "$1 लिङ्कहरू",
        "whatlinkshere-hideimages": "$1 फाइलआ लिङ्कअन",
        "whatlinkshere-filters": "छानियाका",
        "ipbreason-dropdown": "* ब्लक गर्नुका समान्य कारणहरू\n** झूटो सूचना दियाको\n** पानानबठे सामाग्रीहरू हटायाको\n** बाहिरी जालक्षेत्र (sites)सित नचाहिंदो लिङ्क गर्याको \n** पानानमी बकवास/गाली-गलौच हाल्याको\n** भै धेकाउने व्यवहार/उत्पीडन (सताउने कार्य) गर्याको\n** धेरै गलत खाताहरू बनायाको\n** प्रयोगकर्ता नाम अस्वीकार्य",
        "import-logentry-upload-detail": "$1 {{PLURAL:$1|संशोधन|संशोधनहरू}} आयात भयो",
        "tooltip-pt-userpage": "{{GENDER:|तमरो प्रयोगकर्ता}} पान्नो",
        "tooltip-pt-anonuserpage": "तमी जो IP ठेगानाको रुपमी सम्पादन गद्दै छौ , त्यैको प्रयोगकर्ता पानो निम्न छ :",
-       "tooltip-pt-mytalk": "{{GENDER:|तमरà¥\8b}} à¤\95à¥\81रडà¥\80à¤\95ानà¥\80 à¤ªà¤¾नो",
+       "tooltip-pt-mytalk": "{{GENDER:|तमरà¥\8b}} à¤\95à¥\81रणिà¤\95ाà¤\86नà¥\80 à¤ªà¤¾à¤¨à¥\8dनो",
        "tooltip-pt-preferences": "{{GENDER:|तमरी}} अभिरुचि",
        "tooltip-pt-watchlist": "पृष्ठहरूको सूची जैका फेरबदलहरुलाई तमले पहरा गरिराखेका छौ ।",
        "tooltip-pt-mycontris": "{{GENDER:|तमरा}} योगदानअनऐ सूची",
        "newimages-summary": "यै खास पानाले अन्तिम अपलोड गर्याका फाइलहरू धेकाउँन्छ ।",
        "days": "{{PLURAL:$1|$1 दिन|$1 दिनहरू}}",
        "metadata": "मेटाडेटा",
-       "metadata-help": "यà¥\88 à¤«à¤¾à¤\87लमि à¤\85तिरिà¤\95à¥\8dत à¤\9cानà¤\95ारà¥\80हरà¥\81 à¤\9bनà¥\8d, à¤¯à¥\88लाà¤\88 à¤¬à¤¨à¤¾à¤\89न à¤¸à¤®à¥\8dभवतà¤\83 à¤¡à¤¿à¤\9cिà¤\9fल à¤\95à¥\8dयामà¥\87रा और स्क्यानर प्रयोग गरियाको हुनसकन्छ । यदि यै फाइललाई खास अवस्थाबठे फेरबदल गरियाको हो भण्या यै फाइलले  सब्बै विवरण प्रतिबिम्बित गद्द सक्यानाइथी ।",
+       "metadata-help": "यà¥\88 à¤«à¤¾à¤\87लमि à¤\85तिरिà¤\95à¥\8dत à¤\9cानà¤\95ारà¥\80हरà¥\82 à¤\9bनà¥\8d, à¤¯à¥\88लाà¤\88 à¤¬à¤£à¥\81à¤\89न à¤¸à¤®à¥\8dभवतà¤\83 à¤¡à¤¿à¤\9cिà¤\9fल à¤\95à¥\8dयामरा और स्क्यानर प्रयोग गरियाको हुनसकन्छ । यदि यै फाइललाई खास अवस्थाबठे फेरबदल गरियाको हो भण्या यै फाइलले  सब्बै विवरण प्रतिबिम्बित गद्द सक्यानाइथी ।",
        "metadata-fields": "Image metadata fields listed in this message will be included on image page display when the metadata table is collapsed.\nOthers will be hidden by default.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "exif-orientation": "अभिविन्यास",
        "exif-xresolution": "क्षैतिज संकल्प(resolution)",
index e338987..1d12d07 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.",
        "searchprofile-advanced-tooltip": "Sērca int i spâsi di nòm fât só mzûra.",
        "search-result-size": "$1 ({{PLURAL:$2|'na parôla|$2 parôli}})",
        "search-result-category-size": "{{PLURAL:$1|1 utèint|$1 utèint}} ({{PLURAL:$2|1 sotcategoréia|$2 sotcategoréi}},{{PLURAL:$3|1 file|$3 files}})",
-       "search-redirect": "(redirect $1)",
+       "search-redirect": "(redirect from $1)",
        "search-section": "(sesiòun $1)",
        "search-category": "(categoréia $1)",
        "search-file-match": "(relasiòun dèinter al file)",
        "badsig": "Erōr int la fîrma mìa standard, verifichêr i tag HTML.",
        "badsiglength": "La fîrma siēlta l'é trôp lònga, l'an dēv mìa andêr d'ed sōver di $1 {{PLURAL:$1|carâter}}.",
        "yourgender": "Cme arfêres a té?",
-       "gender-unknown": "Indiferèint",
+       "gender-unknown": "Al progrâma, int al numinêret e tōti 'l vôlti ch' al pōl, al druvarà dal parôli sèinsa gèner.",
        "gender-male": "L'é registrê in sém a {{SITENAME}}",
        "gender-female": "L'é registrêda in sém a {{SITENAME}}",
        "prefs-help-gender": "L'impustasiòun ed cla preferèinsa ché l'é a siēlta. Al progrâma al drōva cól valōr ché per parlêr cun tè e numinêret cun chiêter cun al druvêr al gèner ed gramâtica gióst. Cl'infurmasiòun ché la srà póblica.",
        "userrights": "Gestiòun di permès relatîv a j utèint",
        "userrights-lookup-user": "Gestiòun di gróp utèint",
        "userrights-user-editname": "Mèt dèinter al nòm utèint:",
-       "editusergroup": "Mudéfica gróp utèint",
-       "editinguser": "Mudéfica i dirét utèint ed l' utèint <strong>[[User:$1|$1]]</strong> $2",
+       "editusergroup": "Mudéfica i gróp {{GENDER:$1|utèint}}",
+       "editinguser": "Mudéfica i dirét utèint ed j {{GENDER:$1|utèint}}<strong>[[User:$1|$1]]</strong> $2",
        "userrights-editusergroup": "Mudéfica gróp utèint",
-       "saveusergroups": "Sêlva gróp utèint",
+       "saveusergroups": "Sêlva i gróp{{GENDER:$1|utèint}}",
        "userrights-groupsmember": "Al fà pêrt {{PLURAL:$1|al gróp|ai gróp}}:",
        "userrights-groupsmember-auto": "Al fà pêrt ed sicûr a:",
        "userrights-groups-help": "L'é pusébil mudifichêr i gróp in dó fà pêrt l'utèint. \n*'Na caşèla sernîda la sègna a che gróp al fà pêrt l'utèint. \n*'Na caşèla mìa serrnîda la sègna che l'utèin al fà mìa pêrt al gróp. \n*Al sègn * al sègna ch' an n'é m'a pusébil scanşlêr che l'utèin al fà pêrt al gróp dōp avèirel sgnê (o invicivêrsa).",
        "userrights-changeable-col": "Gróp ch'es pōlen mudifichêr.",
        "userrights-unchangeable-col": "Gróp ch'an 's pōlen mìa mudifichêr.",
        "userrights-conflict": "Cuntrâst ed mudéfica di dirét utèint! Cuntròla e cunfērma al tó mudéfichi.",
-       "userrights-removed-self": "T'é tôt via cun sucès i tō dirét. E dòunca, an 't prê pió andêr dèinter a cla pàgina ché.",
+       "userrights-removed-self": "T'é tôt via i tō dirét. E dòunca, an 't prê pió andêr dèinter a cla pàgina ché.",
        "group": "Gróp:",
        "group-user": "Utèint",
        "group-autoconfirmed": "Utèint cunvalidê da per ló",
        "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 a8dd103..3136f05 100644 (file)
        "talk": "Discussion",
        "views": "Views",
        "toolbox": "Tools",
+       "tool-link-userrights": "Change {{GENDER:$1|user}} groups",
+       "tool-link-emailuser": "Email this {{GENDER:$1|user}}",
        "userpage": "View user page",
        "projectpage": "View project page",
        "imagepage": "View file page",
        "signupend": "",
        "signupend-https": "",
        "mailerror": "Error sending mail: $1",
-       "acct_creation_throttle_hit": "Visitors to this wiki using your IP address have created {{PLURAL:$1|1 account|$1 accounts}} in the last day, which is the maximum allowed in this time period.\nAs a result, visitors using this IP address cannot create any more accounts at the moment.",
+       "acct_creation_throttle_hit": "Visitors to this wiki using your IP address have created {{PLURAL:$1|1 account|$1 accounts}} in the last $2, which is the maximum allowed in this time period.\nAs a result, visitors using this IP address cannot create any more accounts at the moment.",
        "emailauthenticated": "Your email address was confirmed on $2 at $3.",
        "emailnotauthenticated": "Your email address is not yet confirmed.\nNo email will be sent for any of the following features.",
        "noemailprefs": "Specify an email address in your preferences for these features to work.",
        "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?",
        "passwordreset-emailelement": "Username:\n$1\n\nTemporary password:\n$2",
        "passwordreset-emailsentemail": "If this email address is associated with your account, then a password reset email will be sent.",
        "passwordreset-emailsentusername": "If there is an email address associated with this username, then a password reset email will be sent.",
-       "passwordreset-emailsent-capture2": "The password reset {{PLURAL:$1|email has|emails have}} been sent. The {{PLURAL:$1|username and password|list of usernames and passwords}} is shown below.",
-       "passwordreset-emailerror-capture2": "Emailing the {{GENDER:$2|user}} failed: $1 The {{PLURAL:$3|username and password|list of usernames and passwords}} is shown below.",
+       "passwordreset-emailsent-capture2": "The password reset {{PLURAL:$1|email has|emails have}} been sent. The {{PLURAL:$1|username and password|list of usernames and passwords}} is shown here.",
+       "passwordreset-emailerror-capture2": "Emailing the {{GENDER:$2|user}} failed: $1 The {{PLURAL:$3|username and password|list of usernames and passwords}} is shown here.",
        "passwordreset-nocaller": "A caller must be provided",
        "passwordreset-nosuchcaller": "Caller does not exist: $1",
        "passwordreset-ignored": "The password reset was not handled. Maybe no provider was configured?",
        "searchprofile-advanced-tooltip": "Search in custom namespaces",
        "search-result-size": "$1 ({{PLURAL:$2|1 word|$2 words}})",
        "search-result-category-size": "{{PLURAL:$1|1 member|$1 members}} ({{PLURAL:$2|1 subcategory|$2 subcategories}}, {{PLURAL:$3|1 file|$3 files}})",
-       "search-redirect": "(redirect $1)",
+       "search-redirect": "(redirect from $1)",
        "search-section": "(section $1)",
        "search-category": "(category $1)",
        "search-file-match": "(matches file content)",
        "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",
        "apisandbox-results-fixtoken-fail": "Failed to fetch \"$1\" token.",
        "apisandbox-alert-page": "Fields on this page are not valid.",
        "apisandbox-alert-field": "The value of this field is not valid.",
+       "apisandbox-continue": "Continue",
+       "apisandbox-continue-clear": "Clear",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} will [https://www.mediawiki.org/wiki/API:Query#Continuing_queries continue] the last request; {{int:apisandbox-continue-clear}} will clear continuation-related parameters.",
        "booksources": "Book sources",
        "booksources-summary": "",
        "booksources-search-legend": "Search for book sources",
        "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.",
        "htmlform-user-not-exists": "<strong>$1</strong> does not exist.",
        "htmlform-user-not-valid": "<strong>$1</strong> isn't a valid username.",
        "rawmessage": "$1",
-       "sqlite-has-fts": "$1 with full-text search support",
-       "sqlite-no-fts": "$1 without full-text search support",
        "logentry-delete-delete": "$1 {{GENDER:$2|deleted}} page $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|restored}} page $3",
        "logentry-delete-event": "$1 {{GENDER:$2|changed}} visibility of {{PLURAL:$5|a log event|$5 log events}} on $3: $4",
        "feedback-external-bug-report-button": "File a technical task",
        "feedback-dialog-title": "Submit feedback",
        "feedback-dialog-intro": "You can use the easy form below to submit your feedback. Your comment will be added to the page \"$1\", along with your username.",
-       "feedback-error-title": "Error",
        "feedback-error1": "Error: Unrecognized result from API",
        "feedback-error2": "Error: Edit failed",
        "feedback-error3": "Error: No response from API",
        "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 f0a61b3..83a8456 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?",
        "titlematches": "Trovitaj laŭ titolo",
        "textmatches": "Trovitaj laŭ enhavo",
        "notextmatches": "Neniu trovita laŭ enhavo",
-       "prevn": "{{PLURAL:$1|$1 antaŭa|$1 antaŭaj}}",
-       "nextn": "{{PLURAL:$1|$1 sekva|$1 sekvaj}}",
+       "prevn": "{{PLURAL:$1|$1 antaŭa|$1 antaŭaj}}n",
+       "nextn": "{{PLURAL:$1|$1 sekva|$1 sekvaj}}n",
        "prev-page": "antaŭa paĝo",
        "next-page": "sekva paĝo",
        "prevn-title": "{{PLURAL:$1|Antaŭa $1 rezulto|Antaŭaj $1 rezultoj}}",
        "nextn-title": "{{PLURAL:$1|Posta $1 rezulto|Postaj $1 rezultoj}}",
        "shown-title": "Montri {{PLURAL:$1|$1 rezulton|$1 rezultojn}} en paĝo",
-       "viewprevnext": "Montri ($1 {{int:pipe-separator}} $2) ($3).",
+       "viewprevnext": "Montri ($1 {{int:pipe-separator}} $2) ($3)",
        "searchmenu-exists": "'''Estas paĝo nomita \"[[:$1]]\" en ĉi tiu vikio'''",
        "searchmenu-new": "<strong>Krei la paĝon \"[[:$1]]\" en ĉi tiu vikio!</strong>{{PLURAL:$2|0=|Vidu ankaŭ la paĝon trovitan per via serĉo.|Vidu ankaŭ la trovitajn serĉrezultojn.}}",
        "searchprofile-articles": "Enhavaj paĝoj",
        "prefs-watchlist": "Atentaro",
        "prefs-editwatchlist": "Redakti atentaron",
        "prefs-editwatchlist-label": "Redakti erojn de via atentaro:",
-       "prefs-editwatchlist-edit": "Montri kaj forigi erojn de vi atentaro",
+       "prefs-editwatchlist-edit": "Rigardi kaj forigi titolojn el via atentaro",
        "prefs-editwatchlist-raw": "Redakti krudan atentaron",
        "prefs-editwatchlist-clear": "Malplenigi vian atentaron",
        "prefs-watchlist-days": "Kiom da tagoj montriĝu en la atentaro:",
        "upload-dialog-disabled": "Alŝutoj de dosiero per ĉi tiun dialogon estas malfunkciigita sur ĉi tiu vikio.",
        "upload-dialog-title": "Alŝuti dosieron",
        "upload-dialog-button-cancel": "Nuligi",
+       "upload-dialog-button-back": "Reen",
        "upload-dialog-button-done": "Farite",
        "upload-dialog-button-save": "Konservi",
        "upload-dialog-button-upload": "Alŝuti",
        "watchlist-hide": "Kaŝi",
        "watchlist-submit": "Montri",
        "wlshowtime": "Vidigenda tempodaŭro:",
-       "wlshowhideminor": "Etaj redaktoj",
-       "wlshowhidebots": "robotoj",
-       "wlshowhideliu": "registritaj uzantoj",
-       "wlshowhideanons": "anonimaj uzantoj",
+       "wlshowhideminor": "etajn redaktojn",
+       "wlshowhidebots": "robotojn",
+       "wlshowhideliu": "registritajn uzantojn",
+       "wlshowhideanons": "anonimajn uzantojn",
        "wlshowhidepatr": "patrolitaj redaktoj",
-       "wlshowhidemine": "miaj redaktoj",
-       "wlshowhidecategorization": "paĝa enkategoriigo.",
+       "wlshowhidemine": "miajn redaktojn",
+       "wlshowhidecategorization": "kategoriigon de paĝoj",
        "watchlist-options": "Opcioj por atentaro",
        "watching": "Aldonata al la atentaro...",
        "unwatching": "Malatentante...",
        "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",
        "isredirect": "alidirektilo",
        "istemplate": "inkludo",
        "isimage": "ligilo al dosiero",
-       "whatlinkshere-prev": "{{PLURAL:$1|antaŭa|antaŭaj $1}}",
-       "whatlinkshere-next": "{{PLURAL:$1|posta|postaj $1}}",
+       "whatlinkshere-prev": "{{PLURAL:$1|antaŭan|antaŭajn $1}}",
+       "whatlinkshere-next": "{{PLURAL:$1|postan|postajn $1}}",
        "whatlinkshere-links": "← ligiloj",
        "whatlinkshere-hideredirs": "$1 alidirektilojn",
-       "whatlinkshere-hidetrans": "$1 transinkluzivaĵojn",
+       "whatlinkshere-hidetrans": "$1 inkludojn",
        "whatlinkshere-hidelinks": "$1 ligilojn",
        "whatlinkshere-hideimages": "$1 dosieraj ligoj",
        "whatlinkshere-filters": "Filtriloj",
        "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",
        "htmlform-cloner-create": "Aldoni plian",
        "htmlform-cloner-delete": "Forigi",
        "htmlform-cloner-required": "Almenaŭ unu valoro estas nepra.",
+       "htmlform-date-placeholder": "JJJJ-MM-TT",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "JJJJ-MM-TT HH:MM:SS",
        "htmlform-title-badnamespace": "[[:$1]] ne  estas en \"{{ns:$2}}\" nomspaco.",
        "htmlform-title-not-creatable": "\"$1\" estas nekreebla titolo por paĝo",
        "htmlform-title-not-exists": "$1 ne ekzistas.",
        "htmlform-user-not-exists": "<strong>$1</strong> ne ekzistas.",
        "htmlform-user-not-valid": "<strong>$1</strong> ne estas valida salutnomo.",
-       "sqlite-has-fts": "$1 kun tut-teksta subteno",
-       "sqlite-no-fts": "$1 sen tut-teksta subteno",
        "logentry-delete-delete": "$1 forigis paĝon $3",
        "logentry-delete-restore": "$1 restarigis paĝon $3",
        "logentry-delete-event": "$1 ŝanĝis videblecon de {{PLURAL:$5|protokola evento|$5 protokolaj eventoj}} je $3: $4",
        "logentry-newusers-autocreate": "Uzantokonto $1 estis {{GENDER:$2|kreita}} aŭtomate",
        "logentry-protect-move_prot": "$1 {{GENDER:$2|movis}} protektajn agordojn el $4 al $3",
        "logentry-protect-unprotect": "$1 {{GENDER:$2|forigis}} protekton el $3",
-       "logentry-protect-protect": "$1 {{GENDER:$2|protektis}} $3 $4",
+       "logentry-protect-protect": "$1 {{GENDER:$2|protektis}} la paĝon $3 $4",
        "logentry-protect-protect-cascade": "$1 {{GENDER:$2|protektis}} $3 $4 [rikure]",
        "logentry-protect-modify": "$1 {{GENDER:$2|ŝanĝis}} la nivelon de protekto de $3 $4",
        "logentry-protect-modify-cascade": "$1 {{GENDER:$2|ŝanĝis}} la nivelon de protekto de $3 $4 [rikure]",
        "feedback-external-bug-report-button": "Krei teĥnikan taskon",
        "feedback-dialog-title": "Sendi prijuĝajn rimarkojn",
        "feedback-dialog-intro": "Vi povas uzi suban simplan formularon por sendi viajn prijuĝajn rimarkojn. Via komento estos aldonita al la paĝo \"$1\" kun via uzanto-nomo.",
-       "feedback-error-title": "Eraro",
        "feedback-error1": "Eraro: Nerekonita rezulto de API",
        "feedback-error2": "Eraro: La redakto malsukcesis",
        "feedback-error3": "Eraro: Neniu respondo de API",
index 74b31c0..700f4e0 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?",
        "upload-dialog-disabled": "En este wiki están desactivadas las subidas de archivos mediante este cuadro de diálogo.",
        "upload-dialog-title": "Subir archivo",
        "upload-dialog-button-cancel": "Cancelar",
+       "upload-dialog-button-back": "Volver",
        "upload-dialog-button-done": "Hecho",
        "upload-dialog-button-save": "Guardar",
        "upload-dialog-button-upload": "Subir",
        "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",
        "htmlform-cloner-create": "Añadir más",
        "htmlform-cloner-delete": "Eliminar",
        "htmlform-cloner-required": "Se requiere al menos un valor.",
+       "htmlform-date-placeholder": "AAAA-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "AAAA-MM-DD HH:MM:SS",
+       "htmlform-date-invalid": "No se reconoció la fecha en el formato proporcionado. Prueba a usar el formato AAAA-MM-DD.",
+       "htmlform-time-invalid": "No se reconoció la hora en el formato proporcionado. Prueba a usar el formato HH:MM:SS.",
+       "htmlform-datetime-invalid": "No se reconoció la fecha y la hora en el formato proporcionado. Prueba a usar el formato AAAA-MM-DD HH:MM:SS.",
+       "htmlform-date-toolow": "El valor especificado es anterior a la fecha más antigua permitida, $1.",
+       "htmlform-date-toohigh": "El valor especificado es posterior a la fecha límite permitida, $1.",
+       "htmlform-time-toolow": "El valor especificado es anterior a la hora más antigua permitida, $1.",
+       "htmlform-time-toohigh": "El valor especificado es posterior a la hora límite permitida, $1.",
+       "htmlform-datetime-toolow": "El valor especificado es anterior a la fecha y hora más antigua permitidas, $1.",
+       "htmlform-datetime-toohigh": "El valor especificado es posterior a la fecha y hora límite permitidas, $1.",
        "htmlform-title-badnamespace": "[[:$1]] no está en el espacio de nombres \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" no es un título de página que se pueda crear",
        "htmlform-title-not-exists": "$1 no existe.",
        "htmlform-user-not-exists": "<strong>$1</strong> no existe.",
        "htmlform-user-not-valid": "<strong>$1</strong> no es un nombre de usuario válido.",
-       "sqlite-has-fts": "$1 con soporte para búsqueda de texto completo",
-       "sqlite-no-fts": "$1 sin soporte para búsqueda de texto completo",
        "logentry-delete-delete": "$1 {{GENDER:$2|borró}} la página $3",
        "logentry-delete-restore": "$1 restauró la página «$3»",
        "logentry-delete-event": "$1 {{GENDER:$2|modificó}} la visibilidad de {{PLURAL:$5|un evento|$5 eventos}} del registro en $3: $4",
        "logentry-patrol-patrol": "$1 {{GENDER:$2|marcó}} la revisión $4 de la página $3 como verificada",
        "logentry-patrol-patrol-auto": "$1 {{GENDER:$2|marcó}} automáticamente la revisión $4 de la página $3 como verificada",
        "logentry-newusers-newusers": "La cuenta de usuario $1 ha sido {{GENDER:$2|creada}}",
-       "logentry-newusers-create": "Se ha {{GENDER:$2|creado}} la cuenta de usuario $1",
+       "logentry-newusers-create": "Se ha {{GENDER:$2|creado}} la cuenta de {{GENDER:$4|usuario|usuaria}} $1",
        "logentry-newusers-create2": "La cuenta de usuario $3 ha sido {{GENDER:$2|creada}} por $1",
        "logentry-newusers-byemail": "La cuenta de usuario $3 ha sido {{GENDER:$2|creada}} por $1 y la contraseña ha sido enviada por correo",
-       "logentry-newusers-autocreate": "La cuenta $1 se {{GENDER:$2|creó}} automáticamente",
+       "logentry-newusers-autocreate": "Se ha {{GENDER:$2|creado}} automáticamente la cuenta de {{GENDER:$4|usuario|usuaria}} $1",
        "logentry-protect-move_prot": "$1 {{GENDER:$2|trasladó}} las preferencias de protección de $4 a $3",
        "logentry-protect-unprotect": "$1 {{GENDER:$2|eliminó}} la protección de $3",
-       "logentry-protect-protect": "$1 {{GENDER:$2|protegió}} $3 $4",
+       "logentry-protect-protect": "$1 {{GENDER:$2|protegió}} $3 $4",
        "logentry-protect-protect-cascade": "$1 {{GENDER:$2|protegió}} a $3 $4 [en cascada]",
        "logentry-protect-modify": "$1 {{GENDER:$2|cambió}} el nivel de protección de $3 $4",
        "logentry-protect-modify-cascade": "$1 {{GENDER:$2|cambió}} el nivel de protección de $3 $4 [en cascada]",
        "feedback-external-bug-report-button": "Enviar una tarea técnica",
        "feedback-dialog-title": "Enviar comentarios",
        "feedback-dialog-intro": "Puedes usar el formulario sencillo debajo para enviar tus comentarios. Ellos se agregarán a la página \"$1\", junto con tu nombre de usuario.",
-       "feedback-error-title": "Error",
        "feedback-error1": "Error: No se reconoce resultado de API",
        "feedback-error2": "Error: Falló la edición",
        "feedback-error3": "Error: No hay respuesta de la API",
index d544a4e..530e319 100644 (file)
        "newwindow": "(avaneb uues aknas)",
        "cancel": "Loobu",
        "moredotdotdot": "Veel...",
-       "morenotlisted": "See loend pole täielik.",
+       "morenotlisted": "See loend võib olla ebatäielik.",
        "mypage": "Minu lehekülg",
        "mytalk": "Arutelu",
        "anontalk": "Arutelu",
        "eauthentsent": "Määratud e-posti aadressile on saadetud kinnituse e-kiri.\nEnne kui su kontole ükskõik milline muu e-kiri saadetakse, pead e-kirjas olevat juhist järgides kinnitama, et konto on tõepoolest sinu.",
        "throttled-mailpassword": "Parooli lähtestamise e-kiri saadetud viimase {{PLURAL:$1|tunni|$1 tunni}} jooksul.\nVäärtarvitamise vältimiseks saadetakse {{PLURAL:$1|tunni|$1 tunni}} jooksul ainult üks lähtestamise e-kiri.",
        "mailerror": "Viga kirja saatmisel: $1",
-       "acct_creation_throttle_hit": "Selle viki külastajad, kes kasutavad sinu IP-aadressi, on viimase ööpäeva jooksul loonud {{PLURAL:$1|ühe konto|$1 kontot}}, mis on selles ajavahemikus ülemmääraks.\nSeetõttu ei saa seda IP-aadressi kasutades hetkel rohkem kontosid luua.",
+       "acct_creation_throttle_hit": "Selle viki külastajad, kes kasutavad sinu IP-aadressi, on viimase $2 jooksul loonud {{PLURAL:$1|ühe konto|$1 kontot}}, mis on selles ajavahemikus ülemmääraks.\nSeetõttu ei saa seda IP-aadressi kasutades hetkel rohkem kontosid luua.",
        "emailauthenticated": "Sinu e-posti aadressi kinnitamisaeg: $2 kell $3.",
        "emailnotauthenticated": "Sinu e-posti aadress pole veel kinnitatud.\nJärgmiste funktsioonidega seotud e-kirju ei saadeta.",
        "noemailprefs": "Järgmiste võimaluste toimimiseks on vaja määrata e-posti aadress.",
        "searchprofile-advanced-tooltip": "Otsi kohandatud nimeruumidest",
        "search-result-size": "$1 ({{PLURAL:$2|1 sõna|$2 sõna}})",
        "search-result-category-size": "{{PLURAL:$1|1 lehekülg|$1 lehekülge}} ({{PLURAL:$2|1 alamkategooria|$2 alamkategooriat}}, {{PLURAL:$3|1 fail|$3 faili}})",
-       "search-redirect": "(ümbersuunamine $1)",
+       "search-redirect": "(ümbersuunamine lehelt $1)",
        "search-section": "(alaosa $1)",
        "search-category": "(kategooria \"$1\")",
        "search-file-match": "(vastab faili sisule)",
        "tags-actions-header": "Toimingud",
        "tags-active-yes": "Jah",
        "tags-active-no": "Ei",
-       "tags-source-extension": "Määratletud tarkvaralisas",
+       "tags-source-extension": "Määratletud tarkvaraliselt",
        "tags-source-manual": "Kasutaja või robot rakendab käsitsi",
        "tags-source-none": "Pole enam kasutuses",
        "tags-edit": "muuda",
        "htmlform-title-not-exists": "Lehekülge $1 pole olemas.",
        "htmlform-user-not-exists": "Kasutajat <strong>$1</strong> pole olemas.",
        "htmlform-user-not-valid": "<strong>$1</strong> pole sobiv kasutajanimi.",
-       "sqlite-has-fts": "$1 koos täistekstiotsingu toega",
-       "sqlite-no-fts": "$1 ilma täistekstiotsingu toeta",
        "logentry-delete-delete": "$1 {{GENDER:$2|kustutas}} lehekülje $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|taastas}} lehekülje $3",
        "logentry-delete-event": "$1 {{GENDER:$2|muutis}} leheküljel $3 {{PLURAL:$5|ühe|$5}} logisündmuse nähtavust: $4",
        "feedback-external-bug-report-button": "Koosta tehniline tööülesanne",
        "feedback-dialog-title": "Tagasiside saatmine",
        "feedback-dialog-intro": "Selle lihtsa vormi abil saad tagasisidet saata. Leheküljele \"$1\" lisatakse sinu kommentaar, mille juures on sinu kasutajanimi.",
-       "feedback-error-title": "Tõrge",
        "feedback-error1": "Tõrge: Tundmatu API tulemus",
        "feedback-error2": "Tõrge: Redigeerimine ebaõnnestus",
        "feedback-error3": "Tõrge: API ei vasta",
        "mw-widgets-dateinput-placeholder-month": "AAAA-KK",
        "mw-widgets-titleinput-description-new-page": "lehekülge pole veel",
        "mw-widgets-titleinput-description-redirect": "ümbersuunamine leheküljele \"$1\"",
-       "randomrootpage": "Juhuslik juurlehekülg"
+       "randomrootpage": "Juhuslik juurlehekülg",
+       "userjsispublic": "Pea silmas, et JavaScripti alamleheküljed ei tohiks sisaldada konfidentsiaalseid andmeid, kuna neid näevad teised kasutajad."
 }
index 523b2ef..be79578 100644 (file)
@@ -43,6 +43,7 @@
        "tog-watchdefault": "Aldatzen ditudan orrialdeak eta fitxategiak nire jarraipen-zerrendara gehitu",
        "tog-watchmoves": "Izena aldatutako orrialdeak eta fitxategiak jarraipen-zerrendara gehitu",
        "tog-watchdeletion": "Ezabatzen ditudan orrialdeak eta fitxategiak nire jarraipen-zerrendara gehitu",
+       "tog-watchuploads": "Gehitu igotzen ditudan fitxategiak nire jarraipen zerrendara",
        "tog-watchrollback": "Nire jarraipen zerrendan rollbacka egin dudan orrialdeak erakutsi",
        "tog-minordefault": "Lehenetsi bezala aldaketa txiki bezala markatu guztiak",
        "tog-previewontop": "Aurrebista aldaketa koadroaren aurretik erakutsi",
@@ -52,7 +53,7 @@
        "tog-enotifminoredits": "Orrialde edo fitxategietan aldaketak txikiak direnean ere e-posta jaso",
        "tog-enotifrevealaddr": "Jakinarazpen mezuetan nire e-posta helbidea erakutsi",
        "tog-shownumberswatching": "Jarraitzen duen erabiltzaile kopurua erakutsi",
-       "tog-oldsig": "Egungo sinadura:",
+       "tog-oldsig": "Zure egungo sinadura:",
        "tog-fancysig": "Sinadura wikitestu gisa tratatu (lotura automatikorik gabe)",
        "tog-uselivepreview": "Zuzeneko aurrebista erabili",
        "tog-forceeditsummary": "Aldaketaren laburpena zuri uzterakoan ohartarazi",
        "newwindow": "(leiho berrian irekitzen da)",
        "cancel": "Utzi",
        "moredotdotdot": "Gehiago...",
-       "morenotlisted": "Zerrenda hau ez dago osorik.",
+       "morenotlisted": "Zerrenda hau agian ez dago osorik.",
        "mypage": "Orrialdea",
        "mytalk": "Eztabaida",
        "anontalk": "Eztabaida",
        "talk": "Eztabaida",
        "views": "Ikustaldiak",
        "toolbox": "Tresnak",
+       "tool-link-userrights": "Erabiltzaile {{GENDER:$1|taldea}} aldatu",
+       "tool-link-emailuser": "{{GENDER:$1|Erabiltzale}} honi e-posta bidali",
        "userpage": "Lankide orrialdea ikusi",
        "projectpage": "Proiektuaren orrialdea ikusi",
        "imagepage": "Ikusi fitxategiaren orria",
        "createacct-reason-ph": "Zergatik ari zaren beste erabiltzaile kontu bat",
        "createacct-submit": "Kontua sortu",
        "createacct-another-submit": "Kontu bat sortu",
+       "createacct-continue-submit": "Jarraitu kontua sortzen",
+       "createacct-another-continue-submit": "Jarraitu kontua sortzen",
        "createacct-benefit-heading": "{{SITENAME}} zu bezalako pertsonek egiten dute.",
        "createacct-benefit-body1": "{{PLURAL:$1|edizio bat|$1 edizio}}",
        "createacct-benefit-body2": "{{PLURAL:$1|Orrialde 1|$1 orrialde}}",
        "eauthentsent": "Egiaztapen mezu bat bidali da zehaztutako e-posta helbidera.\nHelbide horretara beste edozein mezu bidali aurretik, bertan azaltzen diren argibideak jarraitu behar dituzu, kontua zurea dela egiaztatzeko.",
        "throttled-mailpassword": "Pasahitz gogorarazle bat bidali da jada azken {{PLURAL:$1|orduan|$1 orduetan}}.\nBandalismoa sahiesteko pasahitz eskaera bat baino ezin da egin {{PLURAL:$1|orduan|$1 orduan}} behin.",
        "mailerror": "Errorea mezua bidaltzerakoan: $1",
-       "acct_creation_throttle_hit": "Sentitzen dugu, {{PLURAL:$1|erabiltzaile kontu bat sortu duzu|$1 erabiltzaile kontu sortu dituzu}} dagoeneko.\nOndorioz, ezin duzu kontu gehiago sortu.",
+       "acct_creation_throttle_hit": "Sentitzen dugu, {{PLURAL:$1|erabiltzaile kontu bat sortu duzu|$1 erabiltzaile kontu sortu dituzu}} dagoeneko azken $2(e)tan.\nOndorioz, ezin duzu kontu gehiago sortu.",
        "emailauthenticated": "Zure e-posta helbidea autentifikatu da $2an $3(e)tan.",
        "emailnotauthenticated": "Zure posta helbidea egiaztatu gabe dago. \nEz da mezurik bidaliko hurrengo ezaugarrientzako.",
        "noemailprefs": "Zehaztu e-posta helbide bat ezaugarri hauek erabili ahal izateko.",
        "botpasswords-label-update": "Eguneratu",
        "botpasswords-label-cancel": "Utzi",
        "botpasswords-label-delete": "Ezabatu",
+       "botpasswords-label-resetpassword": "Pasahitza berrezarri",
        "resetpass_forbidden": "Ezin dira pasahitzak aldatu",
        "resetpass-no-info": "Orrialde honetara zuzenean sartzeko izena eman behar duzu.",
        "resetpass-submit-loggedin": "Pasahitza aldatu",
        "continue-editing": "Edizio-eremura joan",
        "previewconflict": "Aurreikuspenak aldaketen koadroan idatzitako testua erakusten du, gorde ondoren agertuko den bezala.",
        "session_fail_preview": "'''Sentitzen dugu! Ezin izan da zure aldaketa prozesatu, saioko datu batzuen galera dela-eta. Mesedez, saiatu berriz. Arazoak jarraitzen badu, saiatu [[Special:UserLogout|saioa amaitu]] eta berriz hasten.'''",
-       "session_fail_preview_html": "'''Sentitzen dugu! Ezin izan dugu zure aldaketa burutu, saio datu galera bat medio.'''\n\n''Wiki honek HTML kodea onartzen duenez, aurreikuspena ezgaituta dago JavaScript erasoak saihestu asmoz.''\n\n'''Aldaketa saiakera hau zuzena baldin bada, saiatu berriro mesedez. Arazoak jarraitzen badu, saiatu saioa itxi eta berriz hasten.'''",
+       "session_fail_preview_html": "<strong>Sentitzen dugu! Ezin izan dugu zure aldaketa burutu, saio datu galera bat medio.</strong>\n\n<em>Wiki honek HTML kodea onartzen duenez, aurreikuspena ezgaituta dago JavaScript erasoak saihestu asmoz.</em>\n\n<strong>Aldaketa saiakera hau zuzena baldin bada, saiatu berriro mesedez. Arazoak jarraitzen badu, saiatu  [[Special:UserLogout|saioa itxi]] eta berriz hasten.</strong>",
        "token_suffix_mismatch": "'''Zure aldaketa ezeztatua izan da zure bezeroak puntuazio-karaktereak itxuragabetu dituelako.\nAldaketa ezeztatua izan da testuaren galtzea galarazteko.\nHau batzuetan gertatzen da buggyan oinarritutako web proxy zerbitzua erabiltzean.'''",
        "edit_form_incomplete": "'''Aldaketa formularioaren atal batzuk ez dira iritsi zerbitzarira; bi aldiz ziurtatu zure aldaketak osorik daudela eta berriro saiatu.'''",
        "editing": "«$1» aldatzen",
        "copyrightwarning": "Kontuan izan ezazu {{SITENAME}} webgunean egindako ekarpen guztiak $2 lizentziaren pean argitaratzen direla (xehetasunetarako, ikus $1). Zuk idatzitakoa libreki aldatua eta banatua izatea nahi ez baduzu, ez ezazu hemen jarri.<br />\nEra berean, hitzematen ari zara hau zuk zeuk idatzia dela, edo jabari publikotik nahiz askea den beste ituri batetik kopiatu duzula.\n'''Ez erabili copyright eskubideek babestutako lanik, baimenik gabe!'''",
        "copyrightwarning2": "Mesedez, kontuan izan ezazu {{SITENAME}} webgunean egindako ekarpen guztiak beste erabiltzaileek aldatu edo ezabatu ditzaketela. Zuk idatzitakoa libreki aldatua izatea nahi ez baduzu, ez ezazu hemen jarri.<br />\nEra berean, hitzematen ari zara hau zuk zeuk idatzia dela, edo jabari publikotik nahiz askea den beste ituri batetik kopiatu duzula (xehetasunetarako, ikus $1).\n'''Ez erabili copyright eskubideek babestutako lanik, baimenik gabe!'''",
        "longpageerror": "'''Errorea: Bidali duzun testuak {{PLURAL:$1|kilobyte 1eko|$1 kilobyteko}} luzera du, eta {{PLURAL:$2|kilobyte 1eko|$2 kilobyteko}} maximoa baino luzeagoa da.'''\nEzin da gorde.",
-       "readonlywarning": "'''Oharra: Datu-basea blokeatu egin da mantenu lanak burutzeko, beraz ezingo dituzu orain zure aldaketak gorde.'''\nTestua fitxategi baten kopiatu dezakezu, eta beranduago erabiltzeko gorde.\n\nBlokeatu zuen administratzaileak honako azalpena eman zuen: $1",
+       "readonlywarning": "<strong>Oharra: Datu-basea blokeatu egin da mantenu lanak burutzeko, beraz ezingo dituzu orain zure aldaketak gorde.</strong>I\nTestua fitxategi baten kopiatu dezakezu, eta beranduago erabiltzeko gorde.\n\nBlokeatu zuen administratzaileak honako azalpena eman zuen: $1",
        "protectedpagewarning": "'''Oharra:  Orri hau blokeatua dago administratzaileek soilik eraldatu ahal dezaten.'''\nAzken erregistroa ondoren ikusgai dago erreferentzia gisa:",
        "semiprotectedpagewarning": "'''Oharra''': Orrialde hau erregistratutako erabiltzaileek bakarrik aldatzeko babestuta dago.\nErregistroko azken sarrera azpian jartzen da erreferentzia gisa:",
        "cascadeprotectedwarning": "'''Oharra:''' Orrialde hau blokeatua izan da eta administratzaileek baino ez dute berau aldatzeko ahalmena, honako {{PLURAL:$1|orrialdeko|orrialdeetako}} kaskada-babesean txertatuta dagoelako:",
        "rows": "Lerroak:",
        "columns": "Zutabeak:",
        "searchresultshead": "Bilaketa",
-       "stub-threshold": "<a href=\"#\" class=\"stub\">stub link</a> formaturako atalasea (byteak):",
+       "stub-threshold": "<a href=\"#\" class=\"stub\">stub link</a> formaturako atalasea ($1):",
        "stub-threshold-sample-link": "adibidea",
        "stub-threshold-disabled": "Ezgaitua",
        "recentchangesdays": "Aldaketa berrietan erakutsi beharreko egun kopurua:",
        "gender-female": "Wiki orrialdeak editatzen dituen emakumea",
        "prefs-help-gender": "Hobespen hau jartzea aukerazkoa da.\nSoftwareak bere balioak erabiltzen ditu zu aipatzeko eta beste batzuek genero gramatikala erabiltzeko aukera izan dezaten.\nInformazio hau publikoa da.",
        "email": "E-posta",
-       "prefs-help-realname": "* Benetako izena (aukerakoa): zehaztea erabakiz gero, zure lanarentzako atribuzio bezala balioko du.",
+       "prefs-help-realname": "Benetako izena aukerakoa da. \nZehaztea erabakiz gero, zure lanarentzako atribuzio bezala balioko du.",
        "prefs-help-email": "E-posta helbidea aukerakoa da, baina zure pasahitza ahaztekotan berriro zure e-postara bidaltzeko aukera ematen dizu.",
        "prefs-help-email-others": "Besteak e-mail bidez zurekin harremanetan jartzea ahalbidetu dezakezu, zure lankide- edo eztabaida-orrietako loturaren bidez. Beste lankideak zurekin harremanetan jartzerakoan ez da ikusiko zure e-mail helbidea.",
        "prefs-help-email-required": "E-mail helbidea derrigorrezkoa da.",
        "upload-copy-upload-invalid-domain": "Domeinu honetan ezin dira igoerak kopiatu.",
        "upload-dialog-title": "Igo fitxategia",
        "upload-dialog-button-cancel": "Utzi",
+       "upload-dialog-button-back": "Atzera",
        "upload-dialog-button-done": "Egina",
        "upload-dialog-button-save": "Gorde",
        "upload-dialog-button-upload": "Igo",
        "apisandbox-helpurls": "Laguntza estekak",
        "apisandbox-examples": "Adibideak",
        "apisandbox-dynamic-parameters": "Parametro gehigarriak",
+       "apisandbox-dynamic-parameters-add-label": "Gehitu parametroa:",
+       "apisandbox-dynamic-parameters-add-placeholder": "Parametroaren izena",
+       "apisandbox-dynamic-error-exists": "$1 parametro izena dagoeneko existitzen da",
        "apisandbox-results": "Emaitzak",
        "booksources": "Iturri liburuak",
        "booksources-search-legend": "Liburuen bilaketa",
        "logempty": "Ez dago emaitzarik erregistroan.",
        "log-title-wildcard": "Testu honekin hasten diren izenburuak bilatu",
        "showhideselectedlogentries": "Erakutsi/ezkutatu aukeratutako log sarrerak",
+       "checkbox-select": "Aukeratu:$1",
        "checkbox-all": "Denak",
        "checkbox-none": "Bat ere ez",
        "allpages": "Orri guztiak",
        "watchlistanontext": "Mesedez saioa hasi zure jarraipen zerrendako orrialdeak ikusi eta aldatu ahal izateko.",
        "watchnologin": "Saioa hasi gabe",
        "addwatch": "Jarraipen zerrendara gehitu",
-       "addedwatchtext": "«[[:$1]]» orria zure [[Special:Watchlist|jarraipen zerrendara]] erantsi da. \n\nOrri honetan aurrerantzean egindako aldaketak zerrenda horretan agertuko dira.",
+       "addedwatchtext": "\"[[:$1]]\" eta haren eztabaida orria zure [[Special:Watchlist|jarraipen zerrendara]] erantsi da. \n\nOrri honetan aurrerantzean egindako aldaketak zerrenda horretan agertuko dira.",
        "addedwatchtext-short": "$1 orria zure jarraipen zerrendara gehitu da.",
        "removewatch": "Kendu zure jarraipen zerrendatik",
        "removedwatchtext": "\"[[:$1]]\" orrialdea zure [[Special:Watchlist|jarraipen zerrendatik]] kendu da.",
        "import-upload": "Igo XML datuak",
        "import-token-mismatch": "Sesio data galdu da. Saia saitez berriro ere, mesedez.",
        "import-invalid-interwiki": "Ezin da esandako wikitik inportatu.",
-       "import-error-edit": "\"$1\" orrialdea ez da inportatu ez duzula baimenik aldatzeko.",
+       "import-error-edit": "\"$1\" orrialdea ez da inportatu aldatzeko baimenik ez duzulako.",
        "import-error-create": "\"$1\" orrialdea ez da inportatu ez duzula baimenik sortzeko.",
        "import-error-interwiki": "\"$1\" orrialdea ez da inportatu bere izena kanpo loturetarako gordeta dagoelako (interwiki).",
        "import-error-special": "\"$1\" orrialdea ez da inportatu izen-tarte berezi bati dagokiolako eta horretan orrialderik ezin delako egon.",
        "tags-actions-header": "Ekintzak",
        "tags-active-yes": "Bai",
        "tags-active-no": "Ez",
-       "tags-source-extension": "Luzapenak definitua",
+       "tags-source-extension": "Softwareak definitua",
        "tags-source-none": "Ez da gehiago erabiltzen",
        "tags-edit": "aldatu",
        "tags-delete": "ezabatu",
        "tags-create-tag-name": "Etiketaren izena:",
        "tags-create-reason": "Arrazoia:",
        "tags-create-submit": "Sortu",
+       "tags-create-no-name": "Etiketatutako izen bat zehaztu behar duzu.",
        "tags-create-already-exists": "\"$1\" etiketa badago.",
        "tags-create-warnings-below": "Etiketaren sorrerarekin jarraitu nahi duzu?",
        "tags-delete-title": "Etiketa ezabatu",
        "htmlform-chosen-placeholder": "Aukeratu",
        "htmlform-cloner-create": "Gehitu gehiago",
        "htmlform-cloner-delete": "Kendu",
+       "htmlform-date-placeholder": "UUUU-HH-EE",
+       "htmlform-time-placeholder": "OO:MM:SS",
+       "htmlform-datetime-placeholder": "UUUU-HH-EE OO:MM:SS",
+       "htmlform-date-invalid": "Jarri duzun balioak ez du data ezagunik adierazten. Saiatu UUUU-HH-EE formatua erabiltzen.",
+       "htmlform-time-invalid": "Jarri duzun balioak ez du denbora ezagunik adierazten. Saiatu OO:MM:SS formatua erabiltzen.",
+       "htmlform-datetime-invalid": "Jarri duzun balioak ez da data eta denbora bezala ezagutzen. Saitu UUUU-HH-EE OO:MM:SS formatua erabiltzen.",
        "htmlform-title-not-creatable": "\"$1\" ez da sor daitekeen orrialde baten izenburua",
        "htmlform-title-not-exists": "$1 ez da existitzen.",
        "htmlform-user-not-exists": "<strong>$1</strong> ez da existitzen.",
        "htmlform-user-not-valid": "<strong>$1</strong> erabiltzaile izena ezin da erabili.",
-       "sqlite-has-fts": "$1 testu osoan bilatzeko laguntzarekin",
-       "sqlite-no-fts": "$1 testu osoan bilatzeko laguntzarik gabe",
        "logentry-delete-delete": "$1 {{GENDER:$2|wikilariak}} «$3» orria ezabatu du",
        "logentry-delete-restore": "$1(e)k $3 orrialdea {{GENDER:$2|berrezarri}} du",
        "logentry-delete-event": "$1 wikilariak ikusgaitasuna {{{{GENDER:$2|}}|aldatu}} {{PLURAL:$5|dio erregistroko sarrera bati|die erregistroko $5 sarrerari}}, $3 orrian: $4",
        "feedback-cancel": "Utzi",
        "feedback-close": "Egina",
        "feedback-dialog-title": "Feedbacka bidali",
-       "feedback-error-title": "Errorea",
        "feedback-error1": "Akatsa: APIaren emaitza ez ezagunak",
        "feedback-error2": "Akatsa: Aldaketa ez da egin",
        "feedback-error3": "Akatsa: APIaren erantzunik gabe",
        "mw-widgets-titleinput-description-new-page": "orri hori oraindik ez da existitzen",
        "mw-widgets-titleinput-description-redirect": "$1ra birzuzendu",
        "sessionprovider-generic": "$1 sesio",
+       "log-action-filter-block": "Blokeatze mota:",
+       "log-action-filter-delete": "Ezabatze mota:",
+       "log-action-filter-import": "Inportazio mota:",
+       "log-action-filter-move": "Mugimendu mota:",
+       "log-action-filter-newusers": "Kontu sortze-mota:",
+       "log-action-filter-patrol": "Patruilatze mota:",
+       "log-action-filter-protect": "Babes mota:",
+       "log-action-filter-suppress": "Ezabatze mota:",
+       "log-action-filter-upload": "Igoera mota:",
        "log-action-filter-all": "Denak",
        "log-action-filter-block-block": "Blokeatu",
        "log-action-filter-block-reblock": "Blokeoa aldatu",
        "log-action-filter-block-unblock": "blokeoa kendu",
+       "log-action-filter-delete-revision": "Berrikuspen ezabaketa",
+       "log-action-filter-import-interwiki": "Transwiki inportazioa",
+       "log-action-filter-managetags-create": "Etiketa sorkuntza",
+       "log-action-filter-managetags-delete": "Etiketa ezabaketa",
+       "log-action-filter-managetags-activate": "Etiketa aktibazioa",
+       "log-action-filter-managetags-deactivate": "Etiketa desaktibazioa",
        "authmanager-userdoesnotexist": "\"$1\" erabiltzaile kontua ez dago erregistratua.",
        "authmanager-email-label": "Emaila",
        "authmanager-email-help": "Helbide elektronikoa",
        "authmanager-realname-label": "Benetako izena",
        "authmanager-realname-help": "Erabiltzailearen benetako izena",
        "authprovider-resetpass-skip-label": "Utzi",
-       "authform-wrongtoken": "Token okerra"
+       "authform-wrongtoken": "Token okerra",
+       "credentialsform-account": "Kontuaren izena:"
 }
index dbe5d53..0fbbe20 100644 (file)
        "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|یک مورد سیاهه|$5 مورد سیاهه}} را در $3 {{GENDER:$2|تغییر داد}}: $4",
index e417ee4..d74dd1e 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?",
        "upload-foreign-cant-upload": "Tätä wikiä ei ole konfiguroitu tallentamaan tiedostoja pyydettyyn ulkoiseen tiedostovarastoon.",
        "upload-dialog-title": "Tiedoston tallennus",
        "upload-dialog-button-cancel": "Peru",
+       "upload-dialog-button-back": "Takaisin",
        "upload-dialog-button-done": "Valmis",
        "upload-dialog-button-save": "Tallenna",
        "upload-dialog-button-upload": "Tallenna",
        "htmlform-cloner-create": "Lisää enemmän",
        "htmlform-cloner-delete": "Poista",
        "htmlform-cloner-required": "Vähintään yksi arvo on pakollinen.",
+       "htmlform-date-placeholder": "VVVV-KK-PP",
+       "htmlform-time-placeholder": "TT:MM:SS",
+       "htmlform-datetime-placeholder": "VVVV-KK-PP TT:MM:SS",
        "htmlform-title-badnamespace": "Sivu [[:$1]] ei ole nimiavaruudessa ”{{ns:$2}}”.",
        "htmlform-title-not-creatable": "”$1” ei kelpaa sivun nimeksi.",
        "htmlform-title-not-exists": "Sivua $1 ei ole olemassa.",
        "htmlform-user-not-exists": "Käyttäjää <strong>$1</strong> ei ole olemassa.",
        "htmlform-user-not-valid": "<strong>$1</strong> ei ole kelvollinen käyttäjänimi.",
-       "sqlite-has-fts": "$1, jossa on tuki kokotekstihaulle",
-       "sqlite-no-fts": "$1, jossa ei ole tukea kokotekstihaulle",
        "logentry-delete-delete": "$1 {{GENDER:$2|poisti}} sivun $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|palautti}} sivun $3",
        "logentry-delete-event": "$1 {{GENDER:$2|muutti}} {{PLURAL:$5|lokitapahtuman|$5 lokitapahtuman}} näkyvyyttä kohteessa $3: $4",
        "feedback-external-bug-report-button": "Lähetä tekninen tehtävä",
        "feedback-dialog-title": "Lähetä palautetta",
        "feedback-dialog-intro": "Voit käyttää tätä helppoa lomaketta palautteesi lähettämiseen. Kommenttisi lisätään sivulle \"$1\" käyttäjätunnuksesi kera.",
-       "feedback-error-title": "Virhe",
        "feedback-error1": "Virhe: Ohjelmointirajapinnan vastausta ei tunnistettu",
        "feedback-error2": "Virhe: Muokkaus epäonnistui",
        "feedback-error3": "Virhe: Ohjelmointirajapinta ei vastaa",
        "linkaccounts-submit": "Linkitä tunnuksia",
        "unlinkaccounts": "Poista tunnusten linkityksiä",
        "unlinkaccounts-success": "Tunnuksen linkitys poistettiin.",
-       "authenticationdatachange-ignored": "Varmennustietojen muutosta ei käsitelty. Ehkä palveluntarjoajaa ei määritelty?"
+       "authenticationdatachange-ignored": "Varmennustietojen muutosta ei käsitelty. Ehkä palveluntarjoajaa ei määritelty?",
+       "restrictionsfield-badip": "Virheellinen IP-osoite tai alue: $1",
+       "restrictionsfield-label": "Sallitut IP-alueet:"
 }
index 21f2e7c..ff2eb88 100644 (file)
        "tog-enotifminoredits": "M’avertir par courriel également lors des modifications mineures des pages ou des fichiers",
        "tog-enotifrevealaddr": "Afficher mon adresse électronique dans les courriels de notification",
        "tog-shownumberswatching": "Afficher le nombre d’utilisateurs en cours",
-       "tog-oldsig": "Signature existante :",
+       "tog-oldsig": "Votre signature existante :",
        "tog-fancysig": "Traiter la signature comme du wikitexte (sans lien automatique)",
        "tog-uselivepreview": "Utiliser l’aperçu rapide",
        "tog-forceeditsummary": "M’avertir lorsque je n’ai pas spécifié de résumé de modification",
        "tog-showhiddencats": "Afficher les catégories cachées",
        "tog-norollbackdiff": "Ne pas afficher le diff après avoir révoqué",
        "tog-useeditwarning": "M’avertir quand je quitte une page en cours de modification sans avoir sauvegardé",
-       "tog-prefershttps": "Toujours utiliser une connexion sécurisée pour se connecter",
+       "tog-prefershttps": "Utilisez toujours une connexion sécurisée pour vous connecter",
        "underline-always": "Toujours",
        "underline-never": "Jamais",
        "underline-default": "Valeur par défaut du thème ou du navigateur",
        "newwindow": "(ouvre dans une nouvelle fenêtre)",
        "cancel": "Annuler",
        "moredotdotdot": "Plus...",
-       "morenotlisted": "Cette liste n’est pas complète.",
+       "morenotlisted": "Cette liste peut être incomplète.",
        "mypage": "Page",
        "mytalk": "Discussion",
        "anontalk": "Discussion",
        "talk": "Discussion",
        "views": "Affichages",
        "toolbox": "Outils",
+       "tool-link-userrights": "Modifier les groupes de {{GENDER:$1|l’utilisateur|l’utilisatrice}}",
+       "tool-link-emailuser": "Envoyer un courriel à {{GENDER:$1|l’utilisateur|l’utilisatrice}}",
        "userpage": "Voir la page utilisateur",
        "projectpage": "Voir la page du projet",
        "imagepage": "Voir la page du fichier",
        "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é ?",
        "prefs-emailconfirm-label": "Confirmation du courriel :",
        "youremail": "Courriel :",
        "username": "{{GENDER:$1|Nom d'utilisateur|Nom d'utilisatrice}} :",
-       "prefs-memberingroups": "{{GENDER:$2|Membre}} {{PLURAL:$1|du groupe|des groupes}}:",
+       "prefs-memberingroups": "{{GENDER:$2|Membre}} {{PLURAL:$1|du groupe|des groupes}} :",
        "prefs-registration": "Date d'inscription :",
        "yourrealname": "Nom réel :",
        "yourlanguage": "Langue :",
        "upload-dialog-disabled": "Les téléversements de fichier utilisant cette boîte de dialogue sont désactivés sur ce wiki.",
        "upload-dialog-title": "Téléverser un fichier",
        "upload-dialog-button-cancel": "Annuler",
+       "upload-dialog-button-back": "Retour",
        "upload-dialog-button-done": "Terminé",
        "upload-dialog-button-save": "Enregistrer",
        "upload-dialog-button-upload": "Téléverser",
        "apisandbox-results-fixtoken-fail": "Impossible de récupérer le jeton \"$1\"",
        "apisandbox-alert-page": "Les champs de cette page ne sont pas valides.",
        "apisandbox-alert-field": "La valeur de ce champ n'est pas valide.",
+       "apisandbox-continue": "Continuer",
+       "apisandbox-continue-clear": "Effacer",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} [https://www.mediawiki.org/wiki/API:Query#Continuing_queries continuera] la dernière requête ; {{int:apisandbox-continue-clear}} effacera les paramètres relatifs à la continuation.",
        "booksources": "Ouvrages de référence",
        "booksources-search-legend": "Rechercher parmi des ouvrages de référence",
        "booksources-isbn": "ISBN :",
        "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",
        "namespacesall": "Tous",
        "monthsall": "tous",
        "confirmemail": "Confirmer l’adresse de courriel",
-       "confirmemail_noemail": "Vous n’avez pas défini une adresse de courriel valide dans vos [[Special:Preferences|préférences]].",
+       "confirmemail_noemail": "Vous n’avez pas défini une adresse de courriel valide dans vos [[Special:Preferences|préférences utilisateur]].",
        "confirmemail_text": "Ce wiki nécessite la vérification de votre adresse de courriel avant de pouvoir utiliser toute fonction de messagerie.\nUtilisez le bouton ci-dessous pour envoyer un courriel de confirmation à votre adresse.\nLe courriel inclura un lien comportant un code à usage unique et limité dans le temps ;\nchargez ce lien dans votre navigateur pour confirmer que votre adresse de courriel est valide.",
        "confirmemail_pending": "Un code de confirmation vous a déjà été envoyé par courriel ;\nsi vous venez de créer votre compte, veuillez attendre quelques minutes que le courriel arrive avant de demander un nouveau code.",
        "confirmemail_send": "Envoyer un code de confirmation",
        "confirmemail_success": "Votre adresse de courriel a été confirmée.\nVous pouvez maintenant vous [[Special:UserLogin|{{MediaWiki:Loginreqlink}}]] et profiter du wiki.",
        "confirmemail_loggedin": "Votre adresse de courriel est maintenant confirmée.",
        "confirmemail_subject": "Confirmation d’adresse de courriel pour {{SITENAME}}",
-       "confirmemail_body": "Quelqu’un, probablement vous, à partir de l’adresse IP $1,\na enregistré un compte « $2 » avec cette adresse de courriel\nsur le site {{SITENAME}}.\n\nPour confirmer que ce compte vous appartient vraiment et afin\nd’activer les fonctions de messagerie sur {{SITENAME}},\nveuillez suivre ce lien dans votre navigateur :\n\n$3\n\nSi vous n’avez *pas* enregistré ce compte, n’ouvrez pas ce lien ;\nvous pouvez suivre l’autre lien ci-dessous pour annuler la\nconfirmation de votre adresse courriel :\n\n$5\n\nCe code de confirmation expirera le $4.",
+       "confirmemail_body": "Quelqu’un, probablement vous, à partir de l’adresse IP $1,\na créé un compte « $2 » avec cette adresse de courriel sur le site {{SITENAME}}.\n\nPour confirmer que ce compte vous appartient vraiment et afin\nd’activer les fonctions de messagerie sur {{SITENAME}},\nveuillez suivre ce lien dans votre navigateur :\n\n$3\n\nSi vous n’avez *pas* créé ce compte, suivez le lien ci-dessous \npour annuler la confirmation de votre adresse courriel :\n\n$5\n\nCe code de confirmation expirera le $4.",
        "confirmemail_body_changed": "Quelqu’un, probablement vous, à partir de l’adresse IP $1,\na modifié l’adresse de courriel associée au compte « $2 » de {{SITENAME}}\nen cette adresse.\n\nPour confirmer que ce compte vous appartient vraiment et afin\nde réactiver les fonctions de messagerie sur {{SITENAME}},\nveuillez suivre ce lien dans votre navigateur :\n\n$3\n\nSi ce compte ne vous appartient *pas*, n’ouvrez pas ce lien ;\nvous pouvez suivre l’autre lien ci-dessous pour annuler la\nconfirmation de votre adresse courriel :\n\n$5\n\nCe code de confirmation expirera le $4.",
        "confirmemail_body_set": "Quelqu’un, probablement vous, depuis l’adresse IP $1, a modifié l’adresse de courriel du compte « $2 » en celle-ci sur {{SITENAME}}.\n\nPour confirmer que ce compte vous appartient et réactiver les fonctions de courriel sur {{SITENAME}}, ouvrez ce lien dans votre navigateur Web :\n\n$3\n\nCe code de confirmation expirera le $4.\n\nSi le compte ne vous appartient *pas*, suivez plutôt ce lien pour annuler la confirmation de l’adresse de courriel :\n\n$5",
        "confirmemail_invalidated": "Confirmation de l’adresse courriel annulée",
        "scarytranscludefailed": "[La récupération de modèle a échoué pour $1]",
        "scarytranscludefailed-httpstatus": "[Échec de la récupération du modèle pour  $1 : HTTP  $2 ]",
        "scarytranscludetoolong": "[L'URL est trop longue]",
-       "deletedwhileediting": "'''Attention''' : cette page a été supprimée après que vous ayez commencé à la modifier !",
+       "deletedwhileediting": "<strong>Attention</strong> : cette page a été supprimée après que vous ayez commencé à la modifier !",
        "confirmrecreate": "L’utilisat{{GENDER:$1|eur|rice}} [[User:$1|$1]] ([[User talk:$1|Discussion]]) a supprimé cette page, alors que vous aviez commencé à la modifier, pour le motif suivant :\n: <em>$2</em>\nVeuillez confirmer que vous désirez réellement recréer cette page.",
        "confirmrecreate-noreason": "L’utilisat{{GENDER:$1|eur|rice}} [[User:$1|$1]] ([[User talk:$1|Discussion]]) a supprimé cette page, alors que vous aviez commencé à la modifier. Veuillez confirmer que vous désirez réellement recréer cette page.",
        "recreate": "Recréer",
        "watchlistedit-clear-removed": "{{PLURAL:$1|Un titre a été|$1 titres ont été}} retirés :",
        "watchlistedit-too-many": "Il y a trop de pages à afficher ici.",
        "watchlisttools-clear": "Effacer la liste de suivi",
-       "watchlisttools-view": "Liste de suivi",
+       "watchlisttools-view": "Afficher les modifications significatives",
        "watchlisttools-edit": "Voir et modifier la liste de suivi",
        "watchlisttools-raw": "Modifier la liste de suivi en mode brut",
        "iranian-calendar-m1": "Farvardin",
        "hebrew-calendar-m12-gen": "eloul",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|discussion]])",
        "timezone-local": "Local",
-       "duplicate-defaultsort": "Attention : la clé de tri par défaut « $2 » écrase la précédente clé « $1 ».",
+       "duplicate-defaultsort": "<strong>Attention :</strong> la clé de tri par défaut « $2 » écrase la précédente clé « $1 ».",
        "duplicate-displaytitle": "<strong>Attention :</strong> Le titre d'affichage « $2 » remplace l'ancien titre d'affichage « $1 ».",
        "restricted-displaytitle": "<strong>Avertissement :</strong> le titre d’affichage \"$1\" a été ignoré car il n'est pas équivalent au titre effectif de la page.",
        "invalid-indicator-name": "<strong>Erreur :</strong> L’attribut <code>name</code> des indicateurs d’état de la page ne doit pas être vide.",
        "version-license-not-found": "Aucune information détaillée de la licence n'a été trouvée pour cette extension.",
        "version-credits-title": "Remerciements pour $1",
        "version-credits-not-found": "Aucune information détaillée des remerciements n'a été trouvée pour cette extension.",
-       "version-poweredby-credits": "Ce wiki fonctionne grâce à '''[https://www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2.",
+       "version-poweredby-credits": "Ce wiki fonctionne grâce à <strong>[https://www.mediawiki.org/ MediaWiki]</strong>, copyright © 2001-$1 $2.",
        "version-poweredby-others": "autres",
        "version-poweredby-translators": "traducteurs de translatewiki.net",
        "version-credits-summary": "Nous tenons à remercier les personnes suivantes pour leur contribution à  [[Special:Version|MediaWiki]].",
-       "version-license-info": "MediaWiki est un logiciel libre, vous pouvez le redistribuer ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation ; soit la version 2 de la Licence, ou (à votre choix) toute version ultérieure.\n\nMediaWiki est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE, sans même la garantie implicite de COMMERCIALISATION ou D'ADAPTATION À UN USAGE PARTICULIER. Voir la Licence Publique Générale GNU pour plus de détails.\n\nVous devriez avoir reçu [{{SERVER}}{{SCRIPTPATH}}/COPYING une copie de la Licence Publique Générale GNU] avec ce programme, sinon, écrivez à la Free Software Foundation, Inc., 51, rue Franklin, cinquième étage, Boston, MA 02110-1301, États-Unis ou [//www.gnu.org/licenses/old-licenses/gpl-2.0.html lisez-la en ligne].",
+       "version-license-info": "MediaWiki est un logiciel libre, vous pouvez le redistribuer ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation ; soit la version 2 de la Licence, ou (à votre choix) toute version ultérieure.\n\nMediaWiki est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE, sans même la garantie implicite de COMMERCIALISATION ou D'ADAPTATION À UN USAGE PARTICULIER. Voir la Licence Publique Générale GNU pour plus de détails.\n\nVous devriez avoir reçu [{{SERVER}}{{SCRIPTPATH}}/COPYING une copie de la Licence Publique Générale GNU] avec ce programme, sinon, écrivez à la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, États-Unis ou [//www.gnu.org/licenses/old-licenses/gpl-2.0.html lisez-la en ligne].",
        "version-software": "Logiciels installés",
        "version-software-product": "Produit",
        "version-software-version": "Version",
        "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",
        "tags-create-no-name": "Vous devez spécifier un nom de balise.",
-       "tags-create-invalid-chars": "Les noms de balise ne doivent pas contenir de virgules (<code>,</code>) ou des barres obliques (<code>/</code>).",
+       "tags-create-invalid-chars": "Les noms de balise ne doivent pas contenir de virgules (<code>,</code>) ni de barres obliques (<code>/</code>).",
        "tags-create-invalid-title-chars": "Les noms de balise ne doivent pas contenir de caractères qui ne peuvent pas être utilisés dans les titres des pages.",
        "tags-create-already-exists": "La balise « $1 » existe déjà.",
        "tags-create-warnings-above": "{{PLURAL:$2|L'avertissement suivant|Les avertissements suivants}} ont été rencontrés lors de la tentative de création de la balise « $1 » :",
        "htmlform-cloner-create": "Ajouter encore",
        "htmlform-cloner-delete": "Supprimer",
        "htmlform-cloner-required": "Une valeur au moins est obligatoire.",
+       "htmlform-date-placeholder": "AAAA-MM-JJ",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "AAAA-MM-JJ HH:MM:SS",
+       "htmlform-date-invalid": "La valeur que vous avez spécifiée n’est pas une date reconnue. Essayez en utilisant le format AAAA-MM-JJ.",
+       "htmlform-time-invalid": "La valeur que vous avez spécifiée n’est pas une heure reconnue. Essayez en utilisant le format HH:MM:SS.",
+       "htmlform-datetime-invalid": "La valeur que vous avez spécifiée n’est pas un horodatage reconnu. Essayez en utilisant le format AAAA-MM-JJ HH:MM:SS.",
+       "htmlform-date-toolow": "La valeur que vous avez spécifiée est antérieure à la date autorisée la plus ancienne, qui est $1.",
+       "htmlform-date-toohigh": "La valeur que vous avez spécifiée est postérieure à la date autorisée la plus lointaine, qui est $1.",
+       "htmlform-time-toolow": "La valeur que vous avez spécifiée est antérieure à la plus petite heure autorisée, qui est $1.",
+       "htmlform-time-toohigh": "La valeur que vous avez spécifiée est postérieure à la plus grande heure autorisée, qui est $1.",
+       "htmlform-datetime-toolow": "La valeur que vous avez spécifiée est antérieure à l’horodatage autorisé le plus ancien, qui est $1.",
+       "htmlform-datetime-toohigh": "La valeur que vous avez spécifiée est postérieure à l’horodatage autorisé le plus lointain, qui est $1.",
        "htmlform-title-badnamespace": "[[:$1]] n'est pas dans l’espace de noms « {{ns:$2}} ».",
        "htmlform-title-not-creatable": "« $1 » n’est pas un titre de page pouvant être créée",
        "htmlform-title-not-exists": "$1 n’existe pas",
        "htmlform-user-not-exists": "<strong>$1</strong> n’existe pas.",
        "htmlform-user-not-valid": "<strong>$1</strong> n’est pas un nom d’utilisateur valide.",
-       "sqlite-has-fts": "$1 avec recherche en texte intégral prise en charge",
-       "sqlite-no-fts": "$1 sans recherche en texte intégral prise en charge",
        "logentry-delete-delete": "$1 {{GENDER:$2|a supprimé}} la page $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|a restauré}} la page $3",
        "logentry-delete-event": "$1 {{GENDER:$2|a modifié}} la visibilité {{PLURAL:$5|d'un événement du journal|de $5 événements du journal}} sur $3: $4",
        "feedback-external-bug-report-button": "Signaler un bogue technique",
        "feedback-dialog-title": "Soumettre un commentaire",
        "feedback-dialog-intro": "Vous pouvez utiliser le simple formulaire ci-dessous pour faire parvenir vos commentaires. Votre commentaire sera ajouté à la page « $1 », ainsi que votre nom d’utilisateur.",
-       "feedback-error-title": "Erreur",
        "feedback-error1": "Erreur : Résultat de l'IPA non reconnu",
        "feedback-error2": "Erreur : la modification a échoué",
        "feedback-error3": "Erreur : aucune réponse de l'API",
        "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 d28d4eb..051e76b 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",
        "talk": "Conversa",
        "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|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",
        "searchprofile-advanced-tooltip": "Procurar nos espazos de nomes elixidos",
        "search-result-size": "$1 ({{PLURAL:$2|1 palabra|$2 palabras}})",
        "search-result-category-size": "{{PLURAL:$1|1 membro|$1 membros}} ({{PLURAL:$2|1 subcategoría|$2 subcategorías}}, {{PLURAL:$3|1 ficheiro|$3 ficheiros}})",
-       "search-redirect": "(redirixido desde \"$1\")",
+       "search-redirect": "(redirixido desde $1)",
        "search-section": "(sección \"$1\")",
        "search-category": "(categoría $1)",
        "search-file-match": "(coincide co contido do ficheiro)",
        "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",
        "recentchanges-page-removed-from-category-bundled": "[[:$1]] eliminada da categoría [[Special:WhatLinksHere/$1||esta páxina está incluída noutras páxinas]]",
        "autochange-username": "Cambio automático de MediaWiki",
        "upload": "Subir un ficheiro",
-       "uploadbtn": "Subir un ficheiro",
+       "uploadbtn": "Subir o ficheiro",
        "reuploaddesc": "Cancelar a carga e volver ao formulario de carga",
        "upload-tryagain": "Enviar a descrición do ficheiro modificada",
        "uploadnologin": "Non accedeu ao sistema",
        "upload-dialog-disabled": "As cargas de ficheiros usando esta pantalla están desactivadas neste wiki.",
        "upload-dialog-title": "Subir un ficheiro",
        "upload-dialog-button-cancel": "Cancelar",
+       "upload-dialog-button-back": "Volver",
        "upload-dialog-button-done": "Feito",
        "upload-dialog-button-save": "Gardar",
        "upload-dialog-button-upload": "Subir",
        "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}}.",
        "statistics-edits-average": "Media de edicións por páxina",
        "statistics-users": "[[Special:ListUsers|Usuarios]] rexistrados",
        "statistics-users-active": "Usuarios activos",
-       "statistics-users-active-desc": "Usuarios que teñen levado a cabo unha acción {{PLURAL:$1|no último día|nos últimos $1 días}}",
+       "statistics-users-active-desc": "Usuarios que levaron a cabo unha acción {{PLURAL:$1|no último día|nos últimos $1 días}}",
        "pageswithprop": "Páxinas cunha propiedade de páxina",
        "pageswithprop-legend": "Páxinas cunha propiedade de páxina",
        "pageswithprop-text": "Esta páxina lista aquelas páxinas que utilizan unha propiedade de páxina determinada.",
        "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\".",
        "apisandbox-results-fixtoken-fail": "Erro ó recuperar o identificador \"$1\".",
        "apisandbox-alert-page": "Os campos nesta páxina non son válidos.",
        "apisandbox-alert-field": "O valor deste campo non é válido.",
+       "apisandbox-continue": "Continuar",
+       "apisandbox-continue-clear": "Limpar",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} [https://www.mediawiki.org/wiki/API:Query#Continuing_queries continuará] a última petición; {{int:apisandbox-continue-clear}} limpará os parámetros relativos á continuación.",
        "booksources": "Fontes bibliográficas",
        "booksources-search-legend": "Procurar fontes bibliográficas",
        "booksources-search": "Procurar",
        "htmlform-cloner-create": "Engadir máis",
        "htmlform-cloner-delete": "Eliminar",
        "htmlform-cloner-required": "Necesítase, polo menos, un valor.",
+       "htmlform-date-placeholder": "AAAA-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "AAAA-MM-DD HH:MM:SS",
+       "htmlform-date-invalid": "O valor especificado non é unha data recoñedica. Probe usando o formato AAAA-MM-DD.",
+       "htmlform-time-invalid": "O valor especificado non é unha hora recoñedica. Probe usando o formato HH:MM:SS.",
+       "htmlform-datetime-invalid": "O valor especificado non é unha data e hora recoñedica. Probe usando o formato AAAA-MM-DD HH:MM:SS.",
+       "htmlform-date-toolow": "O valor especificado é anterior á data máis antiga permitida: $1",
+       "htmlform-date-toohigh": "O valor especificado é posterior á data máis nova permitida: $1",
+       "htmlform-time-toolow": "O valor especificado é anterior ó tempo máis antigo permitido: $1",
+       "htmlform-time-toohigh": "O valor especificado é posterior ó tempo máis novo permitido: $1",
+       "htmlform-datetime-toolow": "O valor especificado é anterior á data e tampo máis antigo permitido: $1",
+       "htmlform-datetime-toohigh": "O valor especificado é posterior á data e tempo máis novo permitido: $1",
        "htmlform-title-badnamespace": "\"[[:$1]]\" non está no espazo de nomes \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" non é un título de páxina que se poida crear",
        "htmlform-title-not-exists": "\"$1\" non existe.",
        "htmlform-user-not-exists": "\"<strong>$1</strong>\" non existe.",
        "htmlform-user-not-valid": "\"<strong>$1</strong>\" non é un nome de usuario válido.",
-       "sqlite-has-fts": "$1 con soporte para procuras de texto completo",
-       "sqlite-no-fts": "$1 sen soporte para procuras de texto completo",
        "logentry-delete-delete": "$1 {{GENDER:$2|borrou}} a páxina \"$3\"",
        "logentry-delete-restore": "$1 {{GENDER:$2|restaurou}} a páxina \"$3\"",
        "logentry-delete-event": "$1 {{GENDER:$2|mudou}} a visibilidade {{PLURAL:$5|dunha entrada|de $5 entradas}} do rexistro de $3: $4",
        "feedback-external-bug-report-button": "Enviar unha tarefa técnica",
        "feedback-dialog-title": "Enviar comentarios",
        "feedback-dialog-intro": "Pode usar o formulario simple de abaixo para enviar os seus comentarios sobre o editor visual. O seu comentario será engadido á páxina \"$1\", xunto co seu nome de usuario.",
-       "feedback-error-title": "Erro",
        "feedback-error1": "Erro: Resultado da API non recoñecido",
        "feedback-error2": "Erro: Fallo de edición",
        "feedback-error3": "Erro: Non hai resposta da API",
        "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 432338d..2aa61b6 100644 (file)
        "print": "Cetaki",
        "view": "Bilohi",
        "view-foreign": "Bilohi to $1",
-       "edit": "Momoli'o",
+       "edit": "Boli'o",
        "edit-local": "Ubawa deskripsi lokal",
        "create": "Mohutu",
        "create-local": "Duhengi deskripsi lokal",
        "lastmodifiedat": "Halaman botiye biloli'o pulitiyo $1, $2.",
        "viewcount": "Halaman botiye ma hilu'o {{PLURAL:$1|$1 kali}}.<br />",
        "protectedpage": "Halaman udaha-daha",
-       "jumpto": "Lumanti'a ode",
-       "jumptonavigation": "Navigasi",
+       "jumpto": "Lumanti'a ode:",
+       "jumptonavigation": "navigasi",
        "jumptosearch": "lolohe",
        "view-pool-error": "Ma'apu, server onggo sibuk sa'ati boti.\nNgohuntuwa pengguna mocoba momilehe halaman boti.\nWulatipo ngope'e to'u yi'o dipo mocoba momilehe halaman boti pooli.\n\n\n$1",
        "generic-pool-error": "Ma'apu, server onggo sibuk sa'ati boti.\nNgohuntuwa pengguna mocoba momilohe halaman boti.\nWulatipo ngope'e to'u  yi'u dipo mocoba momilehe halaman boti pooli.",
        "newmessageslinkplural": "{{PLURAL:$1|tuwawu tahuli bohu|999=tahuli bohu}}",
        "newmessagesdifflinkplural": "{{PLURAL:$1|iluba|999=u iluba}} pulitiyo",
        "youhavenewmessagesmulti": "Yio lootapu tahuli bohu to $1",
-       "editsection": "boli'a",
+       "editsection": "boli'o",
        "editold": "boli'a",
        "viewsourceold": "Bilohi bungoliyo",
        "editlink": "boli'a",
        "nstab-template": "Templat",
        "nstab-help": "Halaman tulungi",
        "nstab-category": "Kategori",
-       "mainpage-nstab": "Halamani bungaliyo",
+       "mainpage-nstab": "Halaman Bungaliyo",
        "nosuchaction": "Diya'a huhutu boyito",
        "nosuchactiontext": "Huhutu u hepohile lo URL ja valid.\nYi'o lotalawa lopomaso lo URL, meyalo lodudu'a pranala u ja banari.\nUtiye olo kira-kira tuwotiyo woluwo bug to pilaakasi u hepomake {{SITENAME}}",
        "nosuchspecialpage": "Diya'a halaman istimewa boyito",
        "yourpasswordagain": "Ulangiya tahe u'unti",
        "createacct-yourpasswordagain": "Konfirmasi tahe u'unti",
        "createacct-yourpasswordagain-ph": "Tuwota pooli tahe u'unti",
-       "remembermypassword": "Eelayi tahe u'unti'u to komputer botiye (to delomo $1 {{PLURAL:$1|huyi}})",
        "userlogin-remembermypassword": "Hulima'o wa'u tuwo-tuwoto",
        "userlogin-signwithsecure": "Popohunawa server aamani",
        "cannotloginnow-title": "Ja mowali tumuwoto log sa'ati botiya",
        "tooltip-invert": "Centang kotak botiye u mopowanto'o halaman yiloboli'a to delomo huwali lo tanggulo tilulawoto (wawu huwali lo tanggulo a'ayita wanu dicentang)",
        "namespace_association": "Huwali lo tanggulo a'aayita",
        "tooltip-namespace_association": "Centang halaman botiye u mopowayito huwali lo tanggulo lo'iyawa meyalo subjek u a'ayita wolo huwali lo tanggulo u tilulawoto.",
-       "blanknamespace": "Bungaliyo",
+       "blanknamespace": "(Bungaliyo)",
        "contributions": "Kontribusi {{GENDER:$1|Ta ohu'uwo}}",
        "mycontris": "Kontribusi",
        "anoncontribs": "Kontribusi",
index 75ce391..6220a07 100644 (file)
        "views": "𐍃𐌹𐌿𐌽𐌴𐌹𐍃",
        "toolbox": "𐍃𐌰𐍂𐍅𐌰𐌽𐍃",
        "projectpage": "𐌰𐌽𐌳𐌷𐌿𐌻𐌴𐌹 𐍆𐌰𐌿𐍂𐌰𐍅𐌰𐌿𐍂𐍀𐌰𐌻𐌰𐌿𐍆",
+       "mediawikipage": "𐌰𐌽𐌳𐌷𐌿𐌻𐌴𐌹 𐍅𐌰𐌿𐍂𐌳𐌰𐌻𐌰𐌿𐍆",
        "viewhelppage": "𐌰𐌽𐌳𐌷𐌿𐌻𐌴𐌹 𐌷𐌹𐌻𐍀𐌰𐌻𐌰𐌿𐍆",
        "otherlanguages": "𐌰𐌽𐌸𐌰𐍂𐌰𐌹𐌼 𐍂𐌰𐌶𐌳𐍉𐌼",
        "redirectedfrom": "(𐌹𐍃 {{GENDER:𐍄𐌹𐌿𐌷𐌰𐌽𐍃|𐍄𐌹𐌿𐌷𐌰𐌽𐌰}} 𐌷𐌹𐌳𐍂𐌴 𐍆𐍂𐌰𐌼 $1)",
        "disclaimers": "𐌲𐌰𐍂𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃 𐍂𐌰𐌹𐌷𐍄𐌰𐌹𐍃",
        "disclaimerpage": "Project:𐌲𐌰𐌼𐌰𐌹𐌽𐌰 𐌲𐌰𐍂𐌰𐌹𐌳𐌴𐌹𐌽𐍃 𐍂𐌰𐌹𐌷𐍄𐌰𐌹𐍃",
        "edithelp": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐌹𐌷𐌹𐌻𐍀𐌰",
+       "helppage-top-gethelp": "𐌷𐌹𐌻𐍀𐌰",
        "mainpage": "𐌰𐌽𐌰𐍃𐍄𐍉𐌳𐌴𐌹𐌽𐌹𐌻𐌰𐌿𐍆𐍃",
        "mainpage-description": "𐌰𐌽𐌰𐍃𐍄𐍉𐌳𐌴𐌹𐌽𐌹𐌻𐌰𐌿𐍆𐍃",
-       "portal": "ð\90\8c±ð\90\8c°ð\90\8c¿ð\90\8d\82ð\90\8c²ð\90\8d\83 ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c¹",
-       "portal-url": "Project:ð\90\8c±ð\90\8c°ð\90\8c¿ð\90\8d\82ð\90\8c²ð\90\8d\83 ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c¹",
+       "portal": "ð\90\8c²ð\90\8c°ð\90\8cµð\90\8c¿ð\90\8c¼ð\90\8c¸ð\90\8d\83 ð\90\8c²ð\90\8c°ð\90\8c¼ð\90\8c°ð\90\8c¹ð\90\8c½ð\90\8c³ð\90\8c¿ð\90\8c¸ð\90\8c°ð\90\8c¹ð\90\8d\83",
+       "portal-url": "Project:ð\90\8c²ð\90\8c°ð\90\8cµð\90\8c¿ð\90\8c¼ð\90\8c¸ð\90\8d\83 ð\90\8c²ð\90\8c°ð\90\8c¼ð\90\8c°ð\90\8c¹ð\90\8c½ð\90\8c³ð\90\8c¿ð\90\8c¸ð\90\8c°ð\90\8c¹ð\90\8d\83",
        "privacy": "𐌲𐌰𐍂𐌴𐌳𐌴𐌹𐌽𐍉𐍃 𐍃𐌿𐌽𐌳𐍂𐍉𐍅𐌹𐍃𐌰𐌽𐌰",
        "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": "𐌺𐌿𐌽𐌹",
        "error": "𐌰𐌹𐍂𐌶𐌴𐌹",
        "databaseerror-error": "𐌰𐌹𐍂𐌶𐌴𐌹: $1",
        "missing-article": "𐌳𐌰𐍄𐌰𐌱𐌴𐍃 𐌽𐌹 𐌱𐌹𐌲𐌰𐍄 𐌱𐍉𐌺𐍉𐍃 𐌻𐌰𐌿𐌱𐌹𐍃 𐌸𐌹𐌶𐌴𐌹 𐍃𐌺𐌿𐌻𐌳𐌴𐌳𐌹 𐌱𐌹𐌲𐌹𐍄𐌰𐌽, 𐌷𐌰𐌹𐍄𐌰𐌽𐍃 \"$1\" $2. \n\n𐌸𐌰𐍄𐌰 𐌿𐍆𐍄𐌰 𐍅𐌰𐌹𐍂𐌸𐌹𐌸 𐌾𐌰𐌱𐌰𐌹 𐌻𐌰𐌹𐍃𐍄𐌾𐌰𐌳𐌰 𐍆𐌰𐌹𐍂𐌽𐌾𐌰 𐌳𐌹𐍆𐍆 𐌸𐌰𐌿 𐍃𐍀𐌹𐌻𐌻𐌰𐌲𐌰𐍅𐌹𐍃𐍃 𐍃𐌴𐌹 𐍆𐍂𐌰𐌵𐌹𐍃𐍄𐌹𐌳𐌰 𐌹𐍃𐍄. 𐌽𐌹𐌱𐌰𐌹 𐌹𐍃𐍄, 𐌼𐌰𐌷𐍄𐍃 𐌹𐍃𐍄 𐌴𐌹 𐌱𐌹𐌲𐌴𐍄𐌴𐌹𐍃 𐌰𐌹𐍂𐌶𐌴𐌹𐌽 𐌹𐌽 𐍃𐌰𐌿𐍆𐍄𐍅𐌰𐌹𐍂𐌰. \n\n𐌱𐌹𐌳𐌾𐌰𐌼 𐌸𐌿𐌺, 𐌼𐌴𐍂𐌴𐌹 𐌸𐌰𐍄𐌰 𐌳𐌿 [[Special:ListUsers/sysop\n|𐍂𐌴𐌹𐌺]] 𐌲𐌹𐍆𐌿𐌷 𐌲𐌰𐍅𐌹𐍃𐍃.",
+       "cannotdelete-title": "𐌽𐌹 𐌼𐌰𐌲 𐍆𐍂𐌰𐌵𐌹𐍃𐍄𐌾𐌰𐌽 𐌻𐌰𐌿𐌱𐌰 \"$1\"",
        "badtitle": "𐌿𐌽𐍂𐌰𐌹𐌷𐍄𐌰𐍄𐌰 𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹",
        "badtitletext": "𐍆𐍂𐌰𐌹𐌷𐌰𐌽𐍃 𐌻𐌰𐌿𐍆𐍃 𐍅𐌰𐍃 𐌿𐌽𐌲𐌰𐌼𐌰𐌲𐌰𐌽𐌳𐍃, 𐌻𐌰𐌿𐍃, 𐌰𐌹𐌸𐌸𐌰𐌿 𐌿𐌽𐍂𐌰𐌹𐌷𐍄𐌰𐌱𐌰 𐌲𐌰𐍅𐌹𐌳𐌰𐌽𐍃 𐌼𐌹𐌸𐍂𐌰𐌶𐌳𐌰 𐌸𐌰𐌿 𐌼𐌹𐌸-𐍅𐌹𐌺𐌹 𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹. 𐌼𐌰𐌲𐌹 𐌷𐌰𐌱𐌰𐌽 𐌰𐌹𐌽𐌰 𐌸𐌰𐌿 𐌼𐌰𐌽𐌰𐌲𐌹𐌶𐍉𐍃 𐌱𐍉𐌺𐍉𐍃 𐌱𐍂𐌿𐌺𐌹𐌳𐍉𐍃 𐌹𐌽 𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌾𐌰𐌼.",
        "viewsource": "𐍃𐌰𐌹𐍈 𐌱𐍂𐌿𐌽𐌽𐌰𐌽",
+       "mycustomjsprotected": "𐌽𐌹 𐌷𐌰𐌱𐌰𐌹𐍃 𐌰𐌽𐌳𐌻𐌴𐍄 𐌳𐌿 𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽 𐌸𐌰𐌽𐌰 JavaScript 𐌻𐌰𐌿𐍆.",
        "yourname": "𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰𐌼𐍉:",
        "userlogin-yourname": "𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰𐌼𐍉",
        "userlogin-yourname-ph": "𐌼𐌴𐌻𐌴𐌹 𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰𐌼𐍉 𐌸𐌴𐌹𐌽",
        "createacct-yourpasswordagain": "𐌲𐌰𐍃𐌹𐌲𐌻𐌴𐌹 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳",
        "createacct-yourpasswordagain-ph": "𐌼𐌴𐌻𐌴𐌹 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳 𐌰𐍆𐍄𐍂𐌰",
        "userlogin-remembermypassword": "𐌲𐌰𐍆𐌰𐍃𐍄 𐌼𐌹𐌺 {{GENDER:𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰𐌽𐌰|𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰}}",
+       "cannotloginnow-title": "𐌽𐌿 𐌽𐌹 𐌼𐌰𐌲𐍄 𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽",
        "login": "𐌰𐍄𐌲𐌰𐌲𐌲",
        "nav-login-createaccount": "𐌰𐍄𐌲𐌰𐌲𐌲 / 𐍃𐌺𐌰𐍀𐌴𐌹 𐌺𐌰𐍅𐍄𐍃𐌾𐍉𐌽",
        "userlogin": "𐌰𐍄𐌲𐌰𐌲𐌲 / 𐍃𐌺𐌰𐍀𐌴𐌹 𐌺𐌰𐍅𐍄𐍃𐌾𐍉𐌽",
        "pt-login": "𐌰𐍄𐌲𐌰𐌲𐌲",
        "pt-login-button": "𐌰𐍄𐌲𐌰𐌲𐌲",
        "pt-createaccount": "𐍃𐌺𐌰𐍀𐌴𐌹 𐌺𐌰𐍅𐍄𐍃𐌾𐍉𐌽",
+       "pt-userlogout": "𐌰𐍆𐌻𐌴𐌹𐌸",
        "passwordreset": "𐌰𐍆𐍄𐍂𐌰 𐍃𐌰𐍄𐌴𐌹 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳",
        "bold_sample": "𐍃𐍅𐌹𐌽𐌸𐍉𐍃 𐌱𐍉𐌺𐍉𐍃",
        "bold_tip": "𐍃𐍅𐌹𐌽𐌸𐍉𐍃 𐌱𐍉𐌺𐍉𐍃",
        "showpreview": "𐌰𐍄𐌰𐌿𐌲𐌴𐌹 𐍆𐌰𐌿𐍂𐌰𐍃𐌹𐌿𐌽",
        "showdiff": "𐌰𐍄𐌰𐌿𐌲𐌴𐌹 𐌹𐌽𐌼𐌰𐌹𐌳𐌹𐌽𐌹𐌽𐍃",
        "loginreqlink": "𐌰𐍄𐌲𐌰𐌲𐌲",
-       "newarticle": "(ð\90\8c½ð\90\8c¹ð\90\8c¿ð\90\8c¾ð\90\8c°ð\90\8d\84ð\90\8c°)",
+       "newarticle": "(ð\90\8c½ð\90\8c¹ð\90\8d\85ð\90\8c¹)",
        "newarticletext": "𐌻𐌰𐌹𐍃𐍄𐌹𐌳𐌴𐍃 𐌲𐌰𐍅𐌹𐍃 𐌳𐌿 𐌻𐌰𐌿𐌱𐌰 𐍃𐌰𐌴𐌹 𐌽𐌹𐍃𐍄. 𐌳𐌿 𐍃𐌺𐌰𐍀𐌾𐌰𐌽 𐌸𐌰𐌽𐌰 𐌻𐌰𐌿𐍆, 𐌰𐌽 𐌰𐍃𐍄𐍉𐌳𐌴𐌹 𐌼𐌴𐌻𐌾𐌰𐌽 𐌹𐌽 𐌰𐍂𐌺𐌰𐌹 𐌿𐍆 (𐍃𐌰𐌹𐍈 [$1 𐌷𐌹𐌻𐍀𐌰𐌻𐌰𐌿𐍆] 𐌼𐌰𐌽𐌰𐌲𐌹𐌶𐌹𐌽 𐌺𐌿𐌽𐌸𐌾𐌰). 𐌾𐌰𐌱𐌰𐌹 𐌹𐍃 𐌷𐌴𐍂 𐌹𐌽 𐌰𐌹𐍂𐌶𐌴𐌹𐌽𐍃, 𐌲𐌰𐌲𐌲 𐌳𐌿 <𐍃𐍄𐍂𐍉𐌽𐌲>𐌹𐌱𐌿𐌺𐌰𐌷𐌰𐌿𐌱𐌹𐌳𐌹𐌻𐍉𐌽.",
        "noarticletext": "𐌽𐌿 𐌽𐌹 𐍃𐌹𐌽𐌳 𐌱𐍉𐌺𐍉𐍃 𐌹𐌽 𐌸𐌰𐌼𐌼𐌰 𐌻𐌰𐌿𐌱𐌰.\n𐌼𐌰𐌲𐍄 [[Special:Search/{{PAGENAME}}|𐍃𐍉𐌺𐌾𐌰𐌽 𐌸𐌰𐍄𐌰 𐌻𐌰𐌿𐌱𐌰-𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹]] 𐌹𐌽 𐌰𐌽𐌸𐌰𐍂𐌰𐌹𐌼 𐌻𐌰𐌿𐌱𐌰𐌼,  <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 𐍃𐍉𐌺𐌾𐌰𐌽 𐌲𐌰𐌷𐌰𐌷𐌾𐍉 𐌲𐌰𐍆𐌰𐍃𐍄𐍉𐍃], 𐌰𐌹𐌸𐌸𐌰𐌿 [{{fullurl:{{FULLPAGENAME}}|action=edit}} 𐍃𐌺𐌰𐍀𐌾𐌰𐌽 𐌸𐌰𐌽𐌰 𐌻𐌰𐌿𐍆.]</ span>",
        "noarticletext-nopermission": "𐌽𐌿 𐌽𐌹 𐍃𐌹𐌽𐌳 𐌱𐍉𐌺𐍉𐍃 𐌹𐌽 𐌸𐌰𐌼𐌼𐌰 𐌻𐌰𐌿𐌱𐌰.\n𐌼𐌰𐌲𐍄 [[Special:Search/{{PAGENAME}}|𐍃𐍉𐌺𐌾𐌰𐌽 𐌸𐌰𐍄𐌰 𐌻𐌰𐌿𐌱𐌰-𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹]] 𐌹𐌽 𐌰𐌽𐌸𐌰𐍂𐌰𐌹𐌼 𐌻𐌰𐌿𐌱𐌰𐌼, 𐌸𐌰𐌿 <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 𐍃𐍉𐌺𐌾𐌰𐌽 𐌲𐌰𐌷𐌰𐌷𐌾𐍉 𐌲𐌰𐍆𐌰𐍃𐍄𐍉𐍃]</span>, 𐌹𐌸 𐌽𐌹 𐌷𐌰𐌱𐌰𐌹𐍃 𐌰𐌽𐌳𐌻𐌴𐍄 𐍃𐌺𐌰𐍀𐌾𐌰𐌽 𐌸𐌰𐌽𐌰 𐌻𐌰𐌿𐍆.",
        "updated": "(𐌰𐌽𐌰𐌽𐌹𐍅𐌹𐌸)",
        "previewnote": "<strong>𐌲𐌰𐌼𐌹𐌽𐌸𐌴𐌹 𐌸𐌰𐍄𐌴𐌹 𐌸𐌰𐍄𐌰 𐌹𐍃𐍄 𐌸𐌰𐍄𐌰𐌹𐌽𐌴𐌹 𐍆𐌰𐌿𐍂𐌰𐍃𐌹𐌿𐌽𐍃.</strong>\n𐌸𐌴𐌹𐌽𐍉𐍃 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃 𐌽𐌰𐌿𐌷 𐌽𐌹 𐌲𐌰𐍆𐌰𐍃𐍄𐌰𐌽𐍉𐍃 𐍃𐌹𐌽𐌳!",
-       "editing": "{{GENDER:𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽𐌳𐍃|𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽𐌳𐌴𐌹}} $1",
-       "creating": "{{GENDER:𐍃𐌺𐌰𐍀𐌾𐌰𐌽𐌳𐍃|𐍃𐌺𐌰𐍀𐌾𐌰𐌽𐌳𐌴𐌹}} $1",
+       "editing": "{{GENDER:𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽𐌳𐍃|𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽𐌳𐌴𐌹|𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽𐌳𐍃}} $1",
+       "creating": "{{GENDER:𐍃𐌺𐌰𐍀𐌾𐌰𐌽𐌳𐍃|𐍃𐌺𐌰𐍀𐌾𐌰𐌽𐌳𐌴𐌹|𐍃𐌺𐌰𐍀𐌾𐌰𐌽𐌳𐍃\n}} $1",
        "editingsection": "{{GENDER:𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽𐌳𐍃|𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽𐌳𐌴𐌹}} $1 (𐌳𐌰𐌹𐌻)",
        "editingcomment": "{{GENDER:𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽𐌳𐍃|𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽𐌳𐌴𐌹}} $1 (𐌽𐌹𐌿𐌾𐌰 𐌳𐌰𐌹𐌻)",
        "yourdiff": "𐌲𐌰𐍃𐌺𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃",
        "hiddencategories": "𐍃𐌰 𐌻𐌰𐌿𐍆𐍃 𐌹𐍃𐍄 𐌲𐌰𐌳𐌰𐌹𐌻𐌰 {{PLURAL:$1|1 𐌰𐌽𐌰𐌻𐌰𐌿𐌲𐌽𐌹𐍃 𐌺𐌿𐌽𐌾𐌹𐍃|$1 𐌰𐌽𐌰𐌻𐌰𐌿𐌲𐌽𐌰𐌹𐌶𐌴 𐌺𐌿𐌽𐌾𐌴}}:‎",
        "permissionserrorstext-withaction": "𐌽𐌹 𐌷𐌰𐌱𐌰𐌹𐍃 𐌰𐌽𐌳𐌻𐌴𐍄 𐌳𐌿 $2, 𐌹𐌽 {{PLURAL:$1|𐌹𐍆𐍄𐌿𐌼𐌰𐌹𐌶𐍉𐍃 𐍅𐌰𐌹𐌷𐍄𐌰𐌹𐍃|𐌹𐍆𐍄𐌿𐌼𐌰𐌹𐌶𐍉 𐍅𐌰𐌹𐌷𐍄𐌴}}:",
        "moveddeleted-notice": "𐍃𐌰 𐌻𐌰𐌿𐍆𐍃 𐌿𐍃𐌽𐌿𐌼𐌰𐌽𐍃 𐌹𐍃𐍄. 𐌿𐍃𐌽𐌿𐌼𐍄𐍃 𐌾𐌰𐌷 𐌲𐌰𐍆𐌰𐍃𐍄𐌰𐌹𐌽𐍃 𐌼𐌹𐌸𐍃𐌰𐍄𐌴𐌹𐌽𐌰𐌹𐍃 𐌿𐍆 𐍃𐌹𐌽𐌳 𐌿𐍃𐍄𐌰𐌹𐌺𐌽𐌴𐌹𐌽𐌰𐌹.",
+       "postedit-confirmation-created": "𐌻𐌰𐌿𐍆𐍃 𐌲𐌰𐍃𐌺𐌰𐍀𐌰𐌽𐍃 𐌹𐍃𐍄.",
+       "edit-already-exists": "𐌽𐌹 𐍅𐌰𐍃 𐌼𐌰𐌷𐍄𐍃 𐍃𐌺𐌰𐍀𐌾𐌰𐌽 𐌸𐌰𐌽𐌰 𐌻𐌰𐌿𐍆. \n𐌾𐌿 𐌹𐍃𐍄.",
        "post-expand-template-inclusion-warning": "'''𐌷𐍅𐍉𐍄𐌾𐌰𐌽𐌳𐍃:''' 𐍆𐌰𐌿𐍂𐌰𐌼𐌴𐌻𐌴𐌹𐌽𐍃 𐍃𐌹𐌽𐌳 𐌿𐍆𐌰𐍂𐌼𐌹𐌺𐌹𐌻𐍃. 𐍃𐌿𐌼𐍃 𐍆𐌰𐌿𐍂𐌴𐌼𐌴𐌻𐌴𐌹𐌽𐍉𐍃 𐌽𐌹 𐌼𐌰𐌲 𐍅𐌹𐍃𐌰𐌽 𐌸𐌰𐍂",
        "post-expand-template-inclusion-category": "𐍃𐌴𐌹𐌳𐍉𐌽𐍃 𐌸𐌰𐍂 𐍆𐌰𐌿𐍂𐌰𐌼𐌴𐌻𐌴𐌹𐌽𐍃 𐍃𐌹𐌽𐌳 𐌿𐍆𐌰𐍂𐌼𐌹𐌺𐌹𐌻𐍃",
        "viewpagelogs": "𐌰𐍄𐌰𐌿𐌲𐌴𐌹 𐌲𐌰𐍆𐌰𐍃𐍄𐌰𐌹𐌽𐌹𐌽𐍃 𐌸𐌰𐌼𐌼𐌰 𐌻𐌰𐌿𐌱𐌰",
        "history-feed-item-nocomment": "$1 𐌰𐍄 $2",
        "rev-delundel": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹 𐌰𐌽𐌰𐍃𐌹𐌿𐌽",
        "revdel-restore": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹 𐌰𐌽𐌰𐍃𐌹𐌿𐌽",
-       "revertmerge": "ð\90\8c¿ð\90\8c½ð\90\8c²ð\90\8c°ð\90\8d\84ð\90\8c¹ð\90\8c»ð\90\8d\89ð\90\8d\83",
+       "revertmerge": "ð\90\8c¿ð\90\8c½ð\90\8c²ð\90\8c°ð\90\8c±ð\90\8c»ð\90\8c°ð\90\8c½ð\90\8c³",
        "history-title": "𐌰𐍆𐍄𐍂𐌰𐍃𐌹𐌿𐌽𐌹𐍃𐍀𐌹𐌻𐌻 𐌻𐌰𐌿𐌱𐌹𐍃 \"$1\"",
        "difference-title": "𐌲𐌰𐍃𐌺𐌰𐌹𐌳𐌴𐌹𐌽𐍃 𐌼𐌹𐌸 𐌰𐍆𐍄𐍂𐌰𐍃𐌹𐌿𐌽𐍉𐌼 𐌻𐌰𐌿𐌱𐌹𐍃 \"$1\"",
+       "difference-multipage": "(𐌰𐌽𐌸𐌰𐍂𐌻𐌴𐌹𐌺𐌾𐌰 𐌼𐌹𐌸 𐌻𐌰𐌿𐌱𐌰𐌼)",
        "lineno": "𐍃𐍄𐍂𐌹𐌺𐍃 $1:",
        "editundo": "𐍃𐌺𐌰𐍀𐌴𐌹 𐌰𐍆𐍄𐍂𐌰",
        "diff-multi-sameuser": "({{PLURAL:$1|𐌰𐌹𐌽𐌰 𐌼𐌹𐌳𐌿𐌼𐌰𐌲𐌰𐌱𐍉𐍄𐌴𐌹𐌽𐍃|$1 𐌼𐌹𐌳𐌿𐌼𐌰 𐌲𐌰𐌱𐍉𐍄𐌴𐌹𐌽𐍉𐍃}} 𐍆𐍂𐌰𐌼 𐍃𐌰𐌼𐌹𐌽 𐌱𐍂𐌿𐌺𐌾𐌹𐌽 𐌽𐌹 𐌰𐍄𐌰𐌿𐌲𐌹𐌳𐌰/𐌰𐍄𐌰𐌿𐌲𐌹𐌳𐍉𐍃)",
        "search-showingresults": "{{ZPLURAL:$4|𐍄𐌰𐌿𐌹 <strong>$1 𐍅𐌰𐌹𐌷𐍄𐌰𐌹𐍃 <strong>$3|𐍄𐍉𐌾𐌰 <strong>$1 - $2 𐍅𐌰𐌹𐌷𐍄𐌰𐌹𐍃 <strong>$3}}",
        "search-nonefound": "𐌽𐌹 𐍄𐌰𐌿𐌹 𐍅𐌰𐍃 𐍃𐌰𐌼𐌰𐌽𐌰 𐍃𐍅𐌰 𐍃𐍉𐌺𐌴𐌹𐌽.",
        "powersearch-legend": "𐍃𐍉𐌺𐌴𐌹",
-       "preferences": "ð\90\8c¼ð\90\8c´ð\90\8c¹ð\90\8c½ð\90\8d\89ð\90\8d\83 ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8c¼ð\90\8c°ð\90\8c¹ð\90\8c³ð\90\8c´ð\90\8c¹ð\90\8c½ð\90\8c´ð\90\8c¹𐍃",
-       "mypreferences": "ð\90\8c²ð\90\8c°ð\90\8c»ð\90\8c´ð\90\8c¹ð\90\8cºð\90\8c°ð\90\8c½ð\90\8c³ð\90\8c´ð\90\8c¹ð\90\8c½𐍃 𐍅𐌰𐌹𐌷𐍄𐍃",
+       "preferences": "ð\90\8c¼ð\90\8c´ð\90\8c¹ð\90\8c½ð\90\8d\89ð\90\8d\83 ð\90\8d\86ð\90\8d\82ð\90\8c¹ð\90\8c¾ð\90\8d\89ð\90\8c½ð\90\8d\89ð\90\8d\83 ð\90\8d\85ð\90\8c°ð\90\8c¹ð\90\8c·ð\90\8d\84𐍃",
+       "mypreferences": "ð\90\8c¼ð\90\8c´ð\90\8c¹ð\90\8c½ð\90\8d\89ð\90\8d\83 ð\90\8d\86ð\90\8d\82ð\90\8c¹ð\90\8c¾ð\90\8d\89ð\90\8c½ð\90\8d\89𐍃 𐍅𐌰𐌹𐌷𐍄𐍃",
        "prefs-skin": "𐍆𐌹𐌻𐌻",
        "skin-preview": "𐍆𐌰𐌿𐍂𐌰𐍃𐌰𐌹𐍈",
        "saveprefs": "𐌲𐌰𐍆𐌰𐍃𐍄",
        "minoreditletter": "l",
        "newpageletter": "N",
        "boteditletter": "b",
-       "recentchangeslinked": "ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c¹𐌳𐌰𐌽𐍉𐍃 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃",
-       "recentchangeslinked-feed": "Máideinlieks",
-       "recentchangeslinked-toolbox": "ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c¹𐌳𐌰𐌽𐍉𐍃 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃",
-       "recentchangeslinked-title": "ð\90\8c¹ð\90\8c½ð\90\8c¼ð\90\8c°ð\90\8c¹ð\90\8c³ð\90\8c´ð\90\8c¹ð\90\8c½ð\90\8d\89ð\90\8d\83 ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c¹𐌳𐌰𐌽𐍉𐍃 𐌼𐌹𐌸 \"$1\"",
-       "recentchangeslinked-summary": "𐍃𐍉 𐌹𐍃𐍄 𐌻𐌴𐌹𐍃𐍄𐌰 𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐌴 𐌰𐍆𐍄𐌿𐌼𐌹𐍃𐍄𐍃 𐍃𐌺𐍉𐍀 𐌰𐌽𐌰 𐍃𐌴𐌹𐌳𐍉𐌽𐍃 𐌻𐌴𐌹𐌽𐌺𐍉𐌽𐌳 𐌿𐍃 𐌿𐍃𐍃𐌹𐌽𐌳𐌰𐌹 𐍃𐌴𐌹𐌳𐍉𐌽 (𐌰𐌹𐌸𐌸𐌰𐌿 𐌻𐌹𐌸𐌰𐌿𐍃 𐌿𐍃𐍃𐌹𐌽𐌳𐌰𐌹𐌶𐍉𐍃 𐌷𐌰𐌽𐍃𐍉𐍃). 𐍃𐌴𐌹𐌳𐍉𐌽𐍃 [[Special:Watchlist|𐍅𐌹𐍄𐌰𐌽𐌳𐌻𐌴𐌹𐍃𐍄𐍉𐍃 𐌸𐌴𐌹𐌽𐍉𐍃]] 𐍃𐌹𐌽𐌳 '''𐌳𐌹𐌲𐍂𐍃𐍄𐌰𐍆𐍃'''.",
+       "recentchangeslinked": "ð\90\8c²ð\90\8c°ð\90\8c±ð\90\8c¿ð\90\8c½𐌳𐌰𐌽𐍉𐍃 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃",
+       "recentchangeslinked-feed": "𐌲𐌰𐌱𐌿𐌽𐌳𐌰𐌽𐍉𐍃 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃",
+       "recentchangeslinked-toolbox": "ð\90\8c²ð\90\8c°ð\90\8c±ð\90\8c¿ð\90\8c½𐌳𐌰𐌽𐍉𐍃 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃",
+       "recentchangeslinked-title": "ð\90\8c¹ð\90\8c½ð\90\8c¼ð\90\8c°ð\90\8c¹ð\90\8c³ð\90\8c´ð\90\8c¹ð\90\8c½ð\90\8d\89ð\90\8d\83 ð\90\8c²ð\90\8c°ð\90\8c±ð\90\8c¿ð\90\8c½𐌳𐌰𐌽𐍉𐍃 𐌼𐌹𐌸 \"$1\"",
+       "recentchangeslinked-summary": "A𐌸𐌰𐍄𐌰 𐌹𐍃𐍄 𐍅𐌹𐌺𐍉 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉 𐌽𐌹𐌿𐌾𐌰𐌱𐌰 𐌲𐌰𐍄𐌰𐍅𐌹𐌳𐍉𐍃 𐌻𐌰𐌿𐌱𐌰𐌼 𐌲𐌰𐌱𐌿𐌽𐌳𐌰𐌹 𐍃𐌿𐌼𐌰𐌼𐌼𐌰 𐌻𐌰𐌿𐌱𐌰 (𐌸𐌰𐌿 𐌲𐌰𐌳𐌰𐌹𐌻𐌰𐌼 𐍃𐌿𐌼𐌹𐍃 𐌺𐌿𐌽𐌾𐌹𐍃). <strong>𐌻𐌰𐌿𐌱𐍉𐍃 𐌰𐌽𐌰 [[Special:Watchist|your]] 𐍃𐌹𐌽𐌳 </strong>𐍃𐍅𐌹𐌽𐌸𐌰𐌹.",
        "recentchangeslinked-page": "𐌻𐌰𐌿𐌱𐌰𐌽𐌰𐌼𐍉:",
        "recentchangeslinked-to": "𐌰𐍄𐌰𐌿𐌲𐌴𐌹 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃 𐌻𐌰𐌿𐌱𐌴 𐌸𐌰𐌹𐌴𐌹 𐌲𐌰𐍅𐌹𐌳𐌰𐌽𐌰𐌹 𐌳𐌿 𐌲𐌹𐌱𐌰𐌽𐌰𐌼𐌼𐌰 𐌻𐌰𐌿𐌱𐌰.",
        "upload": "𐌿𐍃𐌷𐌻𐌰𐌸𐌰𐌽 𐍆𐌴𐌹𐌻𐌰𐌽𐍃",
        "randompage": "𐌸𐌿𐍃 𐌿𐌽𐌺𐌿𐌽𐌸𐍃 𐌻𐌰𐌿𐍆𐍃",
        "statistics": "𐍂𐌰𐌸𐌾𐍉𐌽𐍃",
        "brokenredirects-edit": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹",
-       "brokenredirects-delete": "(𐍄𐌰𐌹𐍂𐌰𐌽)",
+       "brokenredirects-delete": "𐍆𐍂𐌰𐌵𐌹𐍃𐍄𐌴𐌹",
        "nbytes": "$1 {{PLURAL:$1|𐌱𐌹𐍄|𐌱𐌰𐍄𐌰}}",
-       "ncategories": "$1 {{PLURAL:$1|ð\90\8cºð\90\8c¿ð\90\8c½ð\90\8c¾ð\90\8c°|ð\90\8cºð\90\8c¿ð\90\8c½ð\90\8c¾ð\90\8d\89ð\90\8d\83}}",
+       "ncategories": "$1 {{PLURAL:$1|ð\90\8cºð\90\8c¿ð\90\8c½ð\90\8c¹|ð\90\8cºð\90\8c¿ð\90\8c½ð\90\8c¾ð\90\8c°}}",
        "nlinks": "$1 {{PLURAL:$1|𐌲𐌰𐍅𐌹𐍃𐍃|𐌲𐌰𐍅𐌹𐍃𐍃𐌴𐌹𐍃}}",
        "nmembers": "$1 {{PLURAL:$1|𐌲𐌰𐌳𐌰𐌹𐌻𐌰|𐌲𐌰𐌳𐌰𐌹𐌻𐌰𐌽𐍃}}",
        "wantedpages": "𐌲𐌰𐌹𐍂𐌽𐌹𐌳𐌰𐌹 𐌻𐌰𐌿𐌱𐍉𐍃",
        "longpages": "𐌻𐌰𐌲𐌲𐌰𐌹 𐌻𐌰𐌿𐌱𐍉𐍃",
        "listusers": "𐍂𐌴𐌲𐌹𐍃𐍄𐍂𐌴𐍂𐌰𐌳𐌴 𐌱𐍂𐌿𐌺𐌾𐌰𐌽𐌳𐍃",
        "newpages": "𐌽𐌹𐌿𐌾𐌰𐌹 𐌻𐌰𐌿𐌱𐍉𐍃",
-       "move": "ð\90\8c½ð\90\8c°ð\90\8c¼ð\90\8c¾ð\90\8c°ð\90\8c½ ð\90\8c°ð\90\8d\86ð\90\8d\84ð\90\8d\82ð\90\8c°",
+       "move": "ð\90\8c¼ð\90\8c¹ð\90\8c¸ð\90\8d\83ð\90\8c°ð\90\8d\84ð\90\8c´ð\90\8c¹",
        "movethispage": "𐌼𐌹𐌸𐍃𐌰𐍄𐌴𐌹 𐌸𐌰𐌽𐌰 𐌻𐌰𐌿𐍆",
        "booksources": "𐌱𐍉𐌺𐌰𐌱𐍂𐌿𐌽𐌽𐌰𐌽𐍃",
        "booksources-search-legend": "𐍃𐍉𐌺𐌴𐌹 𐌱𐍉𐌺𐌰𐌱𐍂𐌿𐌽𐌽𐌰𐌽𐍃",
        "log": "𐌻𐍉𐌲𐌱𐍉𐌺𐍉𐍃",
        "all-logs-page": "𐌰𐌻𐌻𐌰 𐌻𐍉𐌲𐍉𐍃",
        "allpages": "𐌰𐌻𐌻𐌰𐌹 𐌻𐌰𐌿𐌱𐍉𐍃",
-       "nextpage": "ð\90\8c¹ð\90\8d\86ð\90\8d\84ð\90\8c¿ð\90\8c¼ð\90\8c° ð\90\8d\83ð\90\8c´ð\90\8c¹ð\90\8c³ð\90\8d\89 ($1)",
+       "nextpage": "ð\90\8c¹ð\90\8d\86ð\90\8d\84ð\90\8c¿ð\90\8c¼ð\90\8c° ð\90\8c»ð\90\8c°ð\90\8c¿ð\90\8d\86ð\90\8d\83 ($1)",
        "prevpage": "𐌰𐍆𐍄𐌿𐌼𐌹𐍃𐍄𐍃 𐌻𐌰𐌿𐍆𐍃 ($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": "𐌻𐌰𐌹𐍃𐍄𐌰𐌻𐌴𐌹𐍃𐍄𐌰",
        "unwatch": "𐌽𐌹𐍅𐌰𐍂𐌰𐌽",
        "watchlist-details": "{{PLURAL:$1|$1 𐌻𐌰𐌿𐍆𐍃|$1 𐌻𐌰𐌿𐌱𐍉𐍃}} 𐌰𐌽𐌰 𐌸𐌴𐌹𐌽𐌰𐌹 𐍅𐌹𐍄𐌰𐍅𐌹𐌺𐍉𐌽, 𐌽𐌹 𐍃𐌿𐌽𐌳𐍂𐍉 𐍂𐌰𐌷𐌽𐌾𐌰𐌽𐌳𐌰 𐌲𐌰𐍅𐌰𐌿𐍂𐌳𐌾𐌰𐌻𐌰𐌿𐌱𐍉𐍃.",
        "watching": "Wita...",
-       "unwatching": "Niwita...",
+       "unwatching": "𐌿𐌽𐍅𐌹𐍄𐌰𐌽𐌳𐍉...",
        "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": "𐌰𐌽𐌸𐌰𐍂/𐌼𐌰𐌹𐍃 𐌼𐌹𐍄𐍉𐌽𐍃:",
        "prot_1movedto2": "[[$1]] 𐌼𐌹𐌸𐍃𐌰𐍄𐌹𐌸 𐌳𐌿 [[$2]]",
        "protect-level-sysop": "𐌰𐌽𐌳𐌻𐌴𐍄𐌹𐌸 𐌸𐌰𐍄𐌰𐌹𐌽𐌴𐌹 𐍂𐌴𐌹𐌺𐍃",
        "protect-expiring": "𐌿𐍃𐍄𐌹𐌿𐌷𐌹𐌸 $1 (UTC)",
-       "restriction-type": "Freihals:",
+       "restriction-type": "𐌰𐌽𐌳𐌻𐌴𐍄",
        "restriction-edit": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹",
-       "restriction-move": "ð\90\8d\83ð\90\8cºð\90\8c¹ð\90\8c¿ð\90\8c±ð\90\8c°ð\90\8c½",
+       "restriction-move": "ð\90\8c¼ð\90\8c¹ð\90\8c¸ð\90\8d\83ð\90\8c°ð\90\8d\84ð\90\8c´ð\90\8c¹",
        "undeletebtn": "𐌰𐍆𐍄𐍂𐌰 𐌲𐌰𐌱𐍉𐍄𐌾𐌰𐌽",
        "undeletelink": "𐍃𐌰𐌹𐍈𐌰𐌽/𐌰𐍆𐍄𐍂𐌰𐌲𐌰𐍃𐌰𐍄𐌾𐌰𐌽",
        "undeleteviewlink": "𐍃𐌰𐌹𐍈𐌹𐍃",
        "blocklogentry": "𐌰𐍆𐌳𐍂𐌰𐌿𐍃𐌹𐌸 [[$1]] 𐍆𐌰𐌿𐍂 $2 $3",
        "newtitle": "𐌽𐌹𐌿𐌾𐌹 𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹:",
        "move-watch": "𐌰𐍄𐍅𐌹𐍄 𐌱𐍂𐌿𐌽𐌽𐌰𐌻𐌰𐌿𐌱𐌰 𐌾𐌰𐌷 𐌼𐌿𐌽𐌳𐍂𐌴𐌹𐌻𐌰𐌿𐌱𐌰",
-       "movepagebtn": "ð\90\8d\83ð\90\8cºð\90\8c¹ð\90\8c¿ð\90\8c±ð\90\8c° ð\90\8d\83ð\90\8c´ð\90\8c¹ð\90\8c³ð\90\8d\89",
+       "movepagebtn": "ð\90\8c¼ð\90\8c¹ð\90\8c¸ð\90\8d\83ð\90\8c°ð\90\8d\84ð\90\8c´ð\90\8c¹ ð\90\8c»ð\90\8c°ð\90\8c¿ð\90\8d\86",
        "movelogpage": "𐌼𐌹𐌸𐍃𐌰𐍄𐌴𐌹 𐌲𐌰𐍆𐌰𐍃𐍄𐌰𐌹𐌽",
        "movereason": "𐍆𐌰𐌹𐍂𐌹𐌽𐌰:",
        "revertmove": "𐍂𐌰𐌹𐌳𐌾𐌰𐌽",
        "export": "𐌿𐍄𐌱𐌰𐌹𐍂 𐌻𐌰𐌿𐌱𐌰𐌽𐍃",
+       "allmessages-filter-translate": "𐌲𐌰𐍃𐌺𐌴𐌹𐍂𐌴𐌹",
        "thumbnail-more": "\n𐌼𐌹𐌺𐌹𐌻𐌴𐌹",
        "tooltip-pt-userpage": "{{GENDER:|Your user}} 𐌻𐌰𐌿𐍆𐍃",
        "tooltip-pt-mytalk": "{{GENDER:|𐌸𐌴𐌹𐌽𐍃}} 𐌻𐌰𐌿𐍆𐍃 𐌲𐌰𐍅𐌰𐌿𐍂𐌳𐌾𐌹𐍃",
        "table_pager_limit_submit": "Affgaggan",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|𐌲𐌰𐍅𐌰𐌿𐍂𐌳𐌾𐌰]])",
        "version-other": "Anþar",
+       "version-poweredby-translators": "translatewiki.net 𐌲𐌰𐍃𐌺𐌴𐌹𐍂𐌾𐌰𐌽𐍃",
        "specialpages": "𐌿𐍃𐍃𐌹𐌽𐌳𐌰𐌹 𐌻𐌰𐌿𐌱𐍉𐍃",
        "tag-filter": "[[Special:Tags|𐍄𐌰𐌹𐌺𐌽𐍉𐍃]] 𐍆𐌹𐌻𐌷𐌰",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|𐍃𐍉𐌺𐌴𐌹𐌽𐌹𐍅𐌰𐌿𐍂𐌳|𐍃𐍉𐌺𐌴𐌹𐌽𐌹𐍅𐌰𐌿𐍂𐌳𐌰}}]]: $2)",
        "tags-source-header": "𐌱𐍂𐌿𐌽𐌽𐌰",
-       "tags-actions-header": "ð\90\8c³ð\90\8c´ð\90\8c³ð\90\8c´ð\90\8c¹ð\90\8d\83",
+       "tags-actions-header": "ð\90\8d\84ð\90\8d\89ð\90\8c¾ð\90\8c°",
        "tags-source-none": "𐌽𐌹 𐌾𐌿 𐌱𐍂𐌿𐌺𐌾𐌰𐌳𐌰",
        "tags-delete": "𐌿𐍃𐌽𐌹𐌼",
        "tags-activate": "𐌲𐌰𐌵𐌹𐌿𐌴𐌹",
index 489479b..7639217 100644 (file)
@@ -21,7 +21,8 @@
                        "80686",
                        "아라",
                        "Macofe",
-                       "Xð"
+                       "Xð",
+                       "Terfili"
                ]
        },
        "tog-underline": "Links unterstryche:",
        "yourpasswordagain": "Passwort no mol yygee:",
        "createacct-yourpasswordagain": "Passwort bstetige",
        "createacct-yourpasswordagain-ph": "Gib s Passwort nomol yy",
-       "remembermypassword": "Uf däm Computer duurhaft aamälde (Maximal fir $1 {{PLURAL:$1|Tag|Täg}})",
        "userlogin-remembermypassword": "Aagmäldet blyybe",
        "userlogin-signwithsecure": "Sicheri Verbindig bruuche",
        "yourdomainname": "Dyyni Domäne",
        "passwordreset-emailtext-user": "Dr Benutzer $1 bi {{SITENAME}} het e Zrucksetzig vu Dym Passwort bi {{SITENAME}} aagforderet ($4). \n\n{{PLURAL:$3|Des Benutzerkonto isch|Die Benutzerkonte sin}} mit däre E-Mail-Adräss verchnipft: \n\n$2 \n\n{{PLURAL:$3|Des temporär Passwort lauft|Die temporäre Passwerter laufe}} in {{PLURAL:$5|eim Tag|$5 Täg}} ab.\nDu sottsch di aamälden un e nej Passwort vergee. Wänn eber ander die Aafrog gstellt het oder Du di wider an Dyy alt Passwort chasch erinnere un s nimi wettsch ändere, chasch die Nochricht ignorieren un alsfurt Dyy alt Passwort bruche.",
        "passwordreset-emailelement": "Benutzername: \n$1\n\nTemporär Passwort: \n$2",
        "passwordreset-emailsentemail": "We das di bestätigti E-Mail-Adrässen vo dym Wiki-Konto isch, de wird es E-Mail verschickt, für ds Passwort zrüggzsetze.",
-       "passwordreset-emailsent-capture": "E Passwort-Zrucksetzigs-Mail isch vergschickt worde, un isch unte aazeigt.",
-       "passwordreset-emailerror-capture": "Die unten angezeigte Passwortzrucksetzigsmail, wu unten aazeigt wird, isch generiert wore, aber dr Versand an {{GENDER:$2|dr Benutzer|d Benutzeri}} het nit funktioniert: $1",
        "changeemail": "E-Mail-Adrässen änderen oder lösche",
        "changeemail-header": "Füll das Formular uus, für dyni E-Mail-Adrässe z ändere. We du möchtisch, das dys Wiki-Konto nümm mit eren E-Mail-Adrässe verbunden isch, de chasch ds Fäld für’ne nöüi E-Mail-Adrässe läär la und ds Formular abschicke.",
-       "changeemail-passwordrequired": "Du muesch dys Passwort agä, für d Änderig z bestätige.",
        "changeemail-no-info": "Du muesch aagmolde sy zum uff die Syte diräkt zuegryfe z chönne.",
        "changeemail-oldemail": "Aktuelli E-Mail-Adräss",
        "changeemail-newemail": "Nöii E-Mail-Adräss:",
        "newarticle": "(Nej)",
        "newarticletext": "Du bisch eme Link nogange zuen ere Syte, wu s nid git.\nZum die Syte aalege, chasch do in däm Chaschte unte aafange schrybe (lueg [$1 Hilfe] fir meh Informatione).\nWänn do nid hesch welle aane goh, no druck in Dyynem Browser uf '''Zruck'''.",
        "anontalkpagetext": "----''Des isch e Diskussionssyte vun eme anonyme Benutzer, wu kei Zuegang aagleit het oder wu ne nit bruucht. Sälleweg mien mir di numerisch IP-Adräss bruuche zum ihn oder si z identifiziere. So ne IP-Adräss cha au vu mehrere Benutzer teilt wäre. Wenn Du ne anonyme Benutzer bisch un s Gfiel hesch, ass do irrelevanti Kommentar an di grichtet wäre, derno [[Special:CreateAccount|leg e Konto aa]] oder [[Special:UserLogin|mäld di aa]] zum in Zuekumft Verwirrige mit andere anonyme Benutzer z vermyyde.''",
-       "noarticletext": "Uf däre Syte het s no kei Täxt. Du chasch uf andere Syte [[Special:Search/{{PAGENAME}}|dä Yytrag sueche]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} dr Logbuechyytrag sueche, wo dezue ghert],\noder [{{fullurl:{{FULLPAGENAME}}|action=edit}} die Syte bearbeite]</span>.",
+       "noarticletext": "Uf däre Syte het s no kei Täxt. \nDu chasch uf andere Syte [[Special:Search/{{PAGENAME}}|dä Yytrag sueche]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} dr Logbuechyytrag sueche, wo dezue ghert],\noder [{{fullurl:{{FULLPAGENAME}}|action=edit}} die Syte erstelle]</span>.",
        "noarticletext-nopermission": "In däre Syte het s zur Zyt no kei Text.\nDu chasch dää Titel uf andre Syte [[Special:Search/{{PAGENAME}}|sueche]]\noder <span class=\"plainlinks\">in dr zuegherige [{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbiecher sueche].</span> Du derfsch aber die Syte nit aalege.",
        "missing-revision": "D Version $1 vu dr Syte mit Name „{{FULLPAGENAME}}“ git s nit.\n\nDää Fähler chunnt normalerwyys dur e veraltete Link zue dr Versionsgschicht vun ere Syte, wu in dr Zwischezyt glescht woren isch.\nEinzelheite chasch im [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} Lesch-Logbuech] bschaue.",
        "userpage-userdoesnotexist": "S Benutzerkonto „<nowiki>$1</nowiki>“ git s nit. Bitte prief, eb Du die Syte wirkli wit aalege/bearbeite.",
        "undo-nochange": "Schyns isch die Bearbeitig scho rugggängig gmacht wore.",
        "undo-summary": "D Änderig $1 vu [[Special:Contributions/$2|$2]] ([[User talk:$2|Diskussion]]) isch ruckgängig gmacht wore.",
        "undo-summary-username-hidden": "Änderig $1 vun eme versteckte Benutzer ruckgängig gmacht.",
-       "cantcreateaccounttitle": "Benutzerkonto cha nid aagleit wäre.",
        "cantcreateaccount-text": "S Aalege vu me Benutzerkonto vu dr IP-Adräss '''($1)''' isch dur [[User:$3|$3]] gsperrt wore.\n\nGrund vu dr Sperri: ''$2''",
        "cantcreateaccount-range-text": "S Aalege vu Benutzerkonte vu IP-Adrässen im Berych <strong>$1</strong>, wu s Dyni IP-Adräss (<strong>$4</strong>) din het, isch vu [[User:$3|$3]] gsperrt wore.\n\nDr Grund, wu vu $3 aagee woren isch: <em>$2</em>",
        "viewpagelogs": "Logbüecher für die Syten azeige",
        "contributions": "{{GENDER:$1|Benutzer-Byträg}}",
        "contributions-title": "Benutzerbyytreg vu „$1“",
        "mycontris": "Myyni Byyträg",
+       "anoncontribs": "Byyträg",
        "contribsub2": "Vu {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Ds Benutzerkonto «$1» isch nid registriert.",
        "nocontribs": "S sin keini Benutzerbyytreg mit däne Kriterie gfunde wore.",
        "javascripttest": "JavaScript-Tescht",
        "javascripttest-pagetext-unknownaction": "Unbekannti Aktion «$1».",
        "javascripttest-qunit-intro": "Lueg d [$1 Dokumentation zue Tescht] uf mediawiki.org",
-       "tooltip-pt-userpage": "Dyyni Benutzersyte",
+       "tooltip-pt-userpage": "{{GENDER:|Dyyni}} Benutzersyte",
        "tooltip-pt-anonuserpage": "D Benutzersyte vo der IP-Adress wo du mit schaffsch",
-       "tooltip-pt-mytalk": "Dyyni Diskussionssyte",
+       "tooltip-pt-mytalk": "{{GENDER:|Dyyni}}  Diskussionssyte",
        "tooltip-pt-anontalk": "Diskussione über Änderige vo dere IP-Adress",
-       "tooltip-pt-preferences": "Myni Ystellige",
+       "tooltip-pt-preferences": "{{GENDER:|Dyni}} Ystellige",
        "tooltip-pt-watchlist": "Lischte vo de beobachtete Syte.",
-       "tooltip-pt-mycontris": "Lischt vu Dyyne Byyträg",
+       "tooltip-pt-mycontris": "E Lischt vu {{GENDER:|Dyyne}} Byyträg",
        "tooltip-pt-login": "Aamälde",
        "tooltip-pt-logout": "Abmälde",
        "tooltip-pt-createaccount": "Du chasch gärn e Benutzerkonto aalege un Di aamälde. Du muesch s aber nit",
        "tooltip-t-recentchangeslinked": "Letschti Änderige vo de Syte, wo vo do verlinkt sin",
        "tooltip-feed-rss": "RSS-Feed für selli Syte",
        "tooltip-feed-atom": "Atom-Feed für selli Syte",
-       "tooltip-t-contributions": "Lischte vo de Byträg vo däm Benutzer",
+       "tooltip-t-contributions": "E Lischt vo de Byträg vo {{GENDER:$1|däm Benutzer}}",
        "tooltip-t-emailuser": "Schick däm Benutzer e E-Bost",
        "tooltip-t-info": "Meh Informationen über die Syte",
        "tooltip-t-upload": "Dateien ufelade",
        "htmlform-title-not-exists": "$1 git’s nid.",
        "htmlform-user-not-exists": "<strong>$1</strong> git’s nid.",
        "htmlform-user-not-valid": "<strong>$1</strong> isch ke gültige Name.",
-       "sqlite-has-fts": "$1 mit Unterstitzig vu dr Volltextsuechi",
-       "sqlite-no-fts": "$1 ohni Unterstitzig vu dr Volltextsuechi",
        "logentry-delete-delete": "{{GENDER:$2|Dr|D|}} $1 het d Syte $3 glöscht",
        "logentry-delete-restore": "{{GENDER:$2|Der $1|D $1|$1}} het d Syte $3 wider härgstellt",
        "logentry-delete-event": "{{GENDER:$2|Der $1|D $1|$1}} het d Sichtbarkeit {{PLURAL:$5|vumene Logbuechyytrag|vo $5 Logbuechyyträg}} gänderet uff $3: $4",
        "mw-widgets-dateinput-placeholder-month": "JJJJ-MM",
        "mw-widgets-titleinput-description-new-page": "d Syte git’s no nid",
        "mw-widgets-titleinput-description-redirect": "Wyterleitig uf $1",
-       "api-error-blacklisted": "Bitte due en andre, ussagechräftigere Titel usswääle.",
        "randomrootpage": "Zuefelligi Stammsyte"
 }
index 8241917..2d1d825 100644 (file)
        "minoredit": "આ એક નાનો સુધારો છે",
        "watchthis": "આ પાનાને ધ્યાનમાં રાખો",
        "savearticle": "પાનું સાચવો",
+       "savechanges": "પરિવર્તન સાચવો",
        "publishpage": "પાનું પ્રકાશિત કરો",
        "publishchanges": "ફેરફારો પ્રકાશિત કરો",
        "preview": "પૂર્વાવલોકન",
        "searchprofile-advanced-tooltip": "સ્થાનીય નામસ્થળોમાં શોધો",
        "search-result-size": "$1 ({{PLURAL:$2|૧ શબ્દ|$2 શબ્દો}})",
        "search-result-category-size": "{{PLURAL:$1|1 સભ્ય|$1 સભ્યો}} ({{PLURAL:$2|1 ઉપ શ્રેણી|$2 ઉપ શ્રેણીઓ}}, {{PLURAL:$3|1 ફાઇલ|$3 ફાઇલો}})",
-       "search-redirect": "(અન્યત્ર પ્રસ્થાન $1)",
+       "search-redirect": "($1થી વાળેલું)",
        "search-section": "(વિભાગ $1)",
        "search-category": "(શ્રેણી $1)",
        "search-suggest": "શું તમે $1 કહેવા માંગો છો?",
        "file-thumbnail-no": "ફાઇલનું નામ <strong>$1</strong>થી શરૂ થાય છે.\nલાગે છે કે આ ઘટાડેલા કદનું ચિત્ર  ''(thumbnail)'' છે..\nજો તમારી સાથે પૂર્ણ ઘનત્વ ધરાવતી ચિત્રની ફાઇલ હોય તો જ આ ફાઇલ ચડાવશો, અન્યથા ફાઇલનું નામ બદલશો.",
        "fileexists-forbidden": "આ નામની ફાઇલ પહેલેથી મોજુદ છે અને તેના ઉપર લેખન કરી શકાશે નહી.\nતેમ છતાં પણ તમે ફાઇલ ચડાવવા માંગતા હોવ તો ફાઇલનું નામ બદલો અને નવા નામે ફરીથી ચડાવો.\n[[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "સર્વસામાન્ય ફાઇલ સંગ્રહમાં આ નામની ફાઇલ મોજુદ છે.\nતેમ છતાં પણ તમે ફાઇલ ચડાવવા માંગતા હોવ તો ફાઇલનું નામ બદલો અને નવા નામે ફરીથી ચડાવો.\n[[File:$1|thumb|center|$1]]",
-       "file-exists-duplicate": "આ ફાઇલ {{PLURAL:$1|ફાઇલ|ફાઇલો}} ની પ્રત છે.",
+       "file-exists-duplicate": "આ ફાઇલ નીચેની {{PLURAL:$1|ફાઇલ|ફાઇલો}}ની નકલ છે:",
        "file-deleted-duplicate": "ફાઇલ ([[:$1]]) ને સમાન ફાઇલ પહેલાં ભૂંસાડી દેવાઇ છે.\nઆ ફાઇલને ચડાવત પહેલાં હટાવ ઇતિહાસ ચકાસી લો.",
        "uploadwarning": "ફાઇલ ચઢાવ ચેતવણી",
        "uploadwarning-text": "કૃપયા ફાઈલ સંબધી વર્ણન સુધારો અને ફરી પ્રયત્ન કરો",
        "emailsubject": "વિષય:",
        "emailmessage": "સંદેશો:",
        "emailsend": "મોકલો",
-       "emailccme": "મારા ઈ-મેલની પ્રત મને મોકલો",
+       "emailccme": "મારા ઈમેલની પ્રત મને મોકલો",
        "emailccsubject": "$1ને તમે મોકલેલા સંદેશાની પ્રત: $2",
        "emailsent": "ઈ-મેલ મોકલી દેવાયો",
        "emailsenttext": "તમારો ઈ-મેલ મોકલી દેવાયો છે",
        "rollbacklinkcount-morethan": "$1 {{PLURAL:$1|ફેરફાર|ફેરફારો}} કરતાં ઓછું પાછું લાવો",
        "rollbackfailed": "ઉલટાવવું નિષ્ફળ",
        "cantrollback": "આ ફેરફારો ઉલટાવી નહી શકાય\nછેલ્લો ફેરફાર આ પાના ના રચયિતા દ્વારા જ થયો હતો",
-       "alreadyrolled": "[[User:$2|$2]] ([[User talk:$2|talk]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]) દ્વારા થયેલ[[:$1]]ના  ફેરફારો ઉલટાવી ન શકાયા;\nકોઇક અન્ય સભ્યએ આ પાનાપર ફેરફાર કરી દીધા છે.\n\nઆ પાના પર ના છેલ્લા ફેરફારો [[User:$3|$3]] ([[User talk:$3|talk]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]) દ્વારા કરવામાં આવ્યાં હતાં.",
+       "alreadyrolled": "[[User:$2|$2]] ([[User talk:$2|talk]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]) દ્વારા થયેલ [[:$1]]ના ફેરફારો ઉલટાવી ન શકાયા;\nકોઇક અન્ય સભ્યે આ પાના પર ફેરફાર કર્યો છે અથવા ફેરફારો ઉલ્ટાવ્યા છે.\n\nઆ પાના પરના છેલ્લા ફેરફારો [[User:$3|$3]] ([[User talk:$3|talk]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]) દ્વારા કરવામાં આવ્યા હતા.",
        "editcomment": "ફેરફાર સારાંશ હતી: <em>$1</em>.",
        "revertpage": "[[Special:Contributions/$2|$2]] ([[User talk:$2|talk]]) દ્વારા કરેલ ફેરફારોને  [[User:$1|$1]] દ્વારા કરેલા છેલ્લા સુધારા સુધી ઉલટાવાયા.",
        "revertpage-nouser": "ગુપ્ત સભ્ય વડે કરાયેલ ફેરફારને {{GENDER:$1|[[User:$1|$1]]}} વડે કરેલ છેલ્લા પુનરાવર્તન પર પાછા લઇ જવાયું.",
        "articleexists": "આ નામનું પાનું અસ્તિત્વમાં છે, અથવાતો તમે પસંદ કરેલું નામ અસ્વિકાર્ય છો.\nકૃપા કરી અન્ય નામ પસંદ કરો.",
        "cantmove-titleprotected": "આ સ્થાને તમે પાનું નહીં હટાવી શકો કેમ કે નવું શીર્ષક રચના કરવા પહેલેથી આરક્ષીત છે",
        "movetalk": "સંલગ્ન ચર્ચાનું પાનું પણ ખસેડો",
-       "move-subpages": "($1 સુધી) ઉપ-પાના હટાવાયા",
+       "move-subpages": "પેટાપાનાં પણ ખસેડો ($1 સુધીના)",
        "move-talk-subpages": "ઉપપાનને ચર્ચાના પાના પર ખસેડો ( $1 સુધે)",
        "movepage-page-exists": "પાનું  $1 પહેલેથી અસ્તિત્વમાં છે તેના પર સ્વયં ચલિત રીતે નવું લેખન ન થાય.",
        "movepage-page-moved": "પાના $1 ને $2 પર ખસેડાયું",
        "importuploaderrortemp": "આયાતી ફાઈલ ચઢાવવું અસફળ.\nહંગામી ફોલ્ડરા ગાયબ છે.",
        "import-parse-failure": "XML આયાત પદચ્છેદ અસફળ",
        "import-noarticle": "આયાત કરવા માટે કોઇ પાનું નથી!",
-       "import-nonewrevisions": "બધા àª«à«\87રફરà«\8b àªªàª¹à«\87લા àª\86યાત àª\95રાયા àª\9bà«\87.",
+       "import-nonewrevisions": "àª\95à«\8bàª\87 àª«à«\87રફારà«\8b àª\86યાત àª\95રાયા àª¨àª¥à«\80 (બધાàª\82 àªªàª¹à«\87લà«\87થà«\80 àª¹àª¾àª\9cર àª¹àª¤àª¾, àª\85થવા àª\95à«\8dષતિàª\93નà«\87 àª\95ારણà«\87 àª\85વàª\97ણાયા àª\9bà«\87).",
        "xml-error-string": "$1  લીટી ક્ર્માંક $2, સ્તંભ  $3 (બાઇટ  $4): $5",
        "import-upload": "XML માહિતી ચઢાવો",
        "import-token-mismatch": "સત્ર સમાપ્ત\nફરી પ્રયત્ન કરો",
        "htmlform-chosen-placeholder": "વિકલ્પ પસંદ કરો",
        "htmlform-cloner-create": "વધુ ઉમેરો",
        "htmlform-cloner-delete": "હટાવો",
-       "sqlite-has-fts": "$1 પૂર્ણ શબ્દ શોધ સહીત",
-       "sqlite-no-fts": "$1 પૂર્ણ શબ્દ  શોધ વિકલ્પ વગર",
        "logentry-delete-delete": "$1 દ્વારા પાનું $3 {{GENDER:$2|દૂર કરવામાં આવ્યું}}",
        "logentry-delete-restore": "$1 {{GENDER:$2|પુનઃસંગ્રહ}} પાનું $3",
        "logentry-delete-event": "$1 એ {{PLURAL:$5|લૉગ ઘટના|$5 લૉગ ઘટનાઓ}} ની દ્રશ્યતા $3 પર {{GENDER:$2|બદલેલ}} છે: $4",
index e76347f..458f34f 100644 (file)
@@ -61,7 +61,7 @@
        "tog-enotifwatchlistpages": "לשלוח אליי דוא\"ל כאשר משתנה דף או קובץ ברשימת המעקב שלי",
        "tog-enotifusertalkpages": "לשלוח אליי דוא\"ל כאשר נעשה שינוי בדף שיחת המשתמש שלי",
        "tog-enotifminoredits": "לשלוח אליי דוא\"ל גם על עריכות משניות בדפים וקבצים",
-       "tog-enotifrevealaddr": "×\97ש×\99פת כתובת הדוא\"ל שלי בהתראות דוא\"ל",
+       "tog-enotifrevealaddr": "×\9c×\97ש×\95×£ ×\90ת כתובת הדוא\"ל שלי בהתראות דוא\"ל",
        "tog-shownumberswatching": "הצגת מספר המשתמשים העוקבים",
        "tog-oldsig": "החתימה הנוכחית שלך:",
        "tog-fancysig": "התייחסות לחתימה כקוד ויקי (ללא קישור אוטומטי)",
        "actions": "פעולות",
        "namespaces": "מרחבי שם",
        "variants": "גרסאות שפה",
-       "navigation-heading": "תפר×\99×\98 ×\94× ×\99×\95×\95×\98",
+       "navigation-heading": "תפריט ניווט",
        "errorpagetitle": "שגיאה",
        "returnto": "חזרה לדף $1.",
        "tagline": "מתוך {{SITENAME}}",
        "updatedmarker": "עודכן מאז ביקורך האחרון",
        "printableversion": "גרסה להדפסה",
        "permalink": "קישור קבוע",
-       "print": "×\92רס×\94 ×\9c×\94×\93פס×\94",
+       "print": "הדפסה",
        "view": "צפייה",
        "view-foreign": "הצגה ב{{GRAMMAR:תחילית|$1}}",
        "edit": "עריכה",
        "deletethispage": "מחיקת דף זה",
        "undeletethispage": "שחזור דף זה",
        "undelete_short": "שחזור {{PLURAL:$1|עריכה אחת|$1 עריכות}}",
-       "viewdeleted_short": "צפ×\99×\99×\94 ×\91{{PLURAL:$1|ער×\99×\9b×\94 ×\9e×\97×\95ק×\94 ×\90×\97ת|Ö¾$1 ×¢×¨×\99×\9b×\95ת מחוקות}}",
+       "viewdeleted_short": "×\94צ×\92ת {{PLURAL:$1|ער×\99×\9b×\94 ×\9e×\97×\95ק×\94 ×\90×\97ת|$1 ×¢×¨×\99×\9b×\94 מחוקות}}",
        "protect": "הגנה",
        "protect_change": "שינוי",
-       "protectthispage": "הגנה על דף זה",
+       "protectthispage": "×\94פע×\9cת ×\94×\92× ×\94 ×¢×\9c ×\93×£ ×\96×\94",
        "unprotect": "שינוי הגנה",
-       "unprotectthispage": "ש×\99× ×\95×\99 ×\94×\94×\92× ×\94 ×©ל דף זה",
+       "unprotectthispage": "ש×\99× ×\95×\99 ×\94×\94×\92× ×\94 ×¢ל דף זה",
        "newpage": "דף חדש",
        "talkpage": "שיחה על דף זה",
        "talkpagelinktext": "שיחה",
        "talk": "שיחה",
        "views": "צפיות",
        "toolbox": "כלים",
+       "tool-link-userrights": "שינוי הרשאות ה{{GENDER:$1|משתמש|משתמשת}}",
+       "tool-link-emailuser": "שליחת דוא\"ל ל{{GENDER:$1|משתמש|משתמשת}}",
        "userpage": "צפייה בדף המשתמש",
        "projectpage": "צפייה בדף המיזם",
        "imagepage": "צפייה בדף הקובץ",
        "jumpto": "קפיצה אל:",
        "jumptonavigation": "ניווט",
        "jumptosearch": "חיפוש",
-       "view-pool-error": "×\9eצ×\98ער×\99×\9d, ×\94שרת×\99×\9d ×¢×\9e×\95ס×\99×\9d ×\9bר×\92×¢.\n×\99×\95תר ×\9e×\93×\99 ×\9eשת×\9eש×\99×\9d ×\9eנס×\99×\9d ×\9cצפ×\95ת ×\91×\93×£ ×\96×\94.\n×\90× ×\90 ×\94×\9eת×\99× ×\95 ×\96×\9e×\9f ×\9e×\94 ×\9cפנ×\99 ×©×ª× ×¡×\95 ×©×\95×\91 ×\9cצפ×\95ת ×\91×\93×£.\n\n$1",
-       "generic-pool-error": "×\9eצ×\98ער×\99×\9d, ×\94שרת×\99×\9d ×¢×\9e×\95ס×\99×\9d ×\9bר×\92×¢.\n×\99×\95תר ×\9e×\93×\99 ×\9eשת×\9eש×\99×\9d ×\9eנס×\99×\9d ×\9cצפ×\95ת ×\91×\9eש×\90×\91 ×\94×\96×\94.\n×\90× ×\90 ×\94×\9eת×\99× ×\95 ×\96×\9e×\9f ×\9e×\94 ×\9cפנ×\99 ×©×ª× ×¡×\95 ×©×\95×\91 ×\9cצפ×\95ת ×\91×\9eש×\90×\91 ×\94×\96×\94.",
+       "view-pool-error": "×\9eצ×\98ער×\99×\9d, ×\94שרת×\99×\9d ×¢×\9e×\95ס×\99×\9d ×\9bר×\92×¢.\n×\99×\95תר ×\9e×\93×\99 ×\9eשת×\9eש×\99×\9d ×\9eנס×\99×\9d ×\9cצפ×\95ת ×\91×\93×£ ×\94×\96×\94.\n× ×\90 ×\9c×\94×\9eת×\99×\9f ×\96×\9e×\9f ×\9e×\94 ×\95×\9c×\90×\97ר ×\9e×\9b×\9f ×\9cנס×\95ת ×©×\95×\91.\n\n$1",
+       "generic-pool-error": "×\9eצ×\98ער×\99×\9d, ×\94שרת×\99×\9d ×¢×\9e×\95ס×\99×\9d ×\9bר×\92×¢.\n×\99×\95תר ×\9e×\93×\99 ×\9eשת×\9eש×\99×\9d ×\9eנס×\99×\9d ×\9cצפ×\95ת ×\91×\9eש×\90×\91 ×\94×\96×\94.\n× ×\90 ×\9c×\94×\9eת×\99×\9f ×\96×\9e×\9f ×\9e×\94 ×\95×\9c×\90×\97ר ×\9e×\9b×\9f ×\9cנס×\95ת ×©×\95×\91.",
        "pool-timeout": "זמן ההמתנה לסיום הנעילה עבר",
        "pool-queuefull": "התור מלא",
        "pool-errorunknown": "שגיאה בלתי ידועה",
        "portal-url": "Project:שער הקהילה",
        "privacy": "מדיניות הפרטיות",
        "privacypage": "Project:מדיניות הפרטיות",
-       "badaccess": "ש×\92×\99×\90×\94 ×\91×\94רש×\90×\95ת",
+       "badaccess": "ש×\92×\99×\90ת ×\94רש×\90×\94",
        "badaccess-group0": "אין {{GENDER:|לך|לך|לכם}} הרשאה לבצע את הפעולה ש{{GENDER:|ביקשת|ביקשת|ביקשתם}}.",
        "badaccess-groups": "הפעולה ש{{GENDER:|ביקשת|ביקשת|ביקשתם}} לבצע מוגבלת למשתמשים ב{{PLURAL:$2|קבוצה הבאה|אחת הקבוצות הבאות}}: $1.",
        "versionrequired": "נדרשת גרסה $1 של מדיה־ויקי",
-       "versionrequiredtext": "×\92רס×\94 $1 ×©×\9c ×\9e×\93×\99×\94Ö¾×\95×\99ק×\99 × ×\93רשת ×\9cש×\99×\9e×\95ש ×\91×\93×£ ×\96×\94. ×\9c×\9e×\99×\93×¢ × ×\95סף, ×¨×\90×\95 ×\90ת [[Special:Version|×\93×£ הגרסה]].",
+       "versionrequiredtext": "×\92רס×\94 $1 ×©×\9c ×ª×\95×\9bנת ×\9e×\93×\99×\94Ö¾×\95×\99ק×\99 × ×\93רשת ×\9cש×\99×\9e×\95ש ×\91×\93×£ ×\94×\96×\94.\n{{GENDER:|ר×\90×\94|ר×\90×\99|ר×\90×\95}} [[Special:Version|×\9e×\99×\93×¢ ×¢×\9c הגרסה]].",
        "ok": "אישור",
        "pagetitle": "$1 – {{SITENAME}}",
        "backlinksubtitle": "→ $1",
-       "retrievedfrom": "×\9eק×\95ר: $1",
+       "retrievedfrom": "×\90×\95×\97×\96ר ×\9eת×\95×\9a \"$1\"",
        "youhavenewmessages": "יש לך $1 ($2).",
        "youhavenewmessagesfromusers": "יש לך $1 {{PLURAL:$3|ממשתמש אחר|מ־$3 משתמשים}} ($2).",
        "youhavenewmessagesmanyusers": "יש לך $1 ממשתמשים רבים ($2).",
        "confirmable-no": "לא",
        "thisisdeleted": "להציג או לשחזר $1?",
        "viewdeleted": "להציג $1?",
-       "restorelink": "{{PLURAL:$1|×\92רס×\94 ×\9e×\97×\95ק×\94 ×\90×\97ת|$1 ×\92רס×\90ות מחוקות}}",
+       "restorelink": "{{PLURAL:$1|ער×\99×\9b×\94 ×\9e×\97×\95ק×\94 ×\90×\97ת|$1 ×¢×¨×\99×\9bות מחוקות}}",
        "feedlinks": "הזנה:",
        "feed-invalid": "סוג הזנת המנוי שגוי.",
        "feed-unavailable": "הזנות אינן זמינות",
        "nstab-category": "קטגוריה",
        "mainpage-nstab": "עמוד ראשי",
        "nosuchaction": "אין פעולה כזו",
-       "nosuchactiontext": "הפעולה שצוינה בכתובת ה־URL אינה תקינה.\nייתכן שטעית בהקלדת ה־URL, או שהשתמשת בקישור לא נכון.\nייתכן גם שהבעיה נוצרה כתוצאה מבאג בתוכנה המשמשת את {{SITENAME}}.",
+       "nosuchactiontext": "הפעולה שצוינה בכתובת ה־URL אינה תקינה.\nייתכן שטעית בהקלדת הכתובת, או שהשתמשת בקישור לא נכון.\nייתכן גם שהבעיה נוצרה כתוצאה מבאג בתוכנה המשמשת את {{SITENAME}}.",
        "nosuchspecialpage": "אין דף מיוחד בשם זה",
        "nospecialpagetext": "<strong>ביקשת דף מיוחד שאינו קיים.</strong>\n\nרשימה של הדפים המיוחדים הקיימים ניתן למצוא בדף [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "שגיאה",
        "databaseerror-function": "פונקציה: $1",
        "databaseerror-error": "שגיאה: $1",
        "transaction-duration-limit-exceeded": "כדי למנוע עיכובי העתקה גדולים, פעולה זו הופסקה כיוון שמשך הכתיבה ($1) עבר את המגבלה של {{PLURAL:$2|שנייה אחת|$2 שניות}}.\nאם הפעולה דורשת שינוי של פריטים רבים בו־זמנית, ניתן לנסות לבצע מספר פעולות קטנות יותר.",
-       "laggedslavemode": "'''אזהרה:''' הדף עשוי שלא להכיל עדכונים אחרונים.",
+       "laggedslavemode": "<strong>אזהרה:</strong> הדף עשוי שלא להכיל עדכונים אחרונים.",
        "readonly": "בסיס הנתונים נעול",
        "enterlockreason": "יש להקליד סיבה לנעילה, כולל הערכה למועד שחרור הנעילה",
-       "readonlytext": "×\91ס×\99ס × ×ª×\95× ×\99×\9d ×\96×\94 ×©×\9c ×\94×\90תר × ×¢×\95×\9c ×\91ר×\92×¢ ×\96×\94 ×\9cצ×\95ר×\9a הזנת נתונים ושינויים. ככל הנראה מדובר בתחזוקה שוטפת, שלאחריה יחזור האתר לפעולתו הרגילה.\n\nמנהל המערכת שנעל את בסיס הנתונים סיפק את ההסבר הבא: $1",
-       "missing-article": "×\91ס×\99ס ×\94נת×\95× ×\99×\9d ×\9c×\90 ×\9eצ×\90 ×\90ת ×\94×\98קס×\98 ×©×\9c ×\94×\93×£ ×©×\94×\95×\90 ×\94×\99×\94 ×\90×\9e×\95ר ×\9c×\9eצ×\95×\90, ×\91ש×\9d \"$1\" $2.\n\n×\94×\93×\91ר × ×\92ר×\9d ×\91×\93ר×\9a ×\9b×\9c×\9c ×¢×\9cÖ¾×\99×\93×\99 ×§×\99ש×\95ר ×\99ש×\9f ×\9c×\94ש×\95×\95×\90ת ×\92רס×\90×\95ת ×©×\9c ×\93×£ ×©× ×\9e×\97ק ×\90×\95 ×\9c×\92רס×\94 ×©×\9c ×\93×£ ×\9b×\96×\94.\n\n×\90×\9d ×\96×\94 ×\90×\99× ×\95 ×\94×\9eקר×\94, ×\96×\94×\95 ×\9bנר×\90×\94 ×\91×\90×\92 ×\91ת×\95×\9b× ×\94.\n×\90× ×\90 ×\93×\95×\95×\97×\95 על כך ל[[Special:ListUsers/sysop|מפעיל מערכת]], תוך שמירת פרטי כתובת ה־URL.",
+       "readonlytext": "×\91ס×\99ס ×\94נת×\95× ×\99×\9d × ×¢×\95×\9c ×\9bר×\92×¢ ×\9cהזנת נתונים ושינויים. ככל הנראה מדובר בתחזוקה שוטפת, שלאחריה יחזור האתר לפעולתו הרגילה.\n\nמנהל המערכת שנעל את בסיס הנתונים סיפק את ההסבר הבא: $1",
+       "missing-article": "×\91ס×\99ס ×\94נת×\95× ×\99×\9d ×\9c×\90 ×\9eצ×\90 ×\90ת ×\94×\98קס×\98 ×©×\9c ×\94×\93×£ ×©×\94×\95×\90 ×\94×\99×\94 ×\90×\9e×\95ר ×\9c×\9eצ×\95×\90, ×\91ש×\9d \"$1\" $2.\n\n×\96×\94 × ×\92ר×\9d ×\91×\93ר×\9aÖ¾×\9b×\9c×\9c ×¢×§×\91 ×\9c×\97×\99צ×\94 ×¢×\9c ×§×\99ש×\95ר ×\99ש×\9f ×\9c×\92רס×\94 ×©×\9c ×\93×£ ×©× ×\9e×\97ק.\n\n×\90×\9d ×\96×\94 ×\90×\99× ×\95 ×\94×\9eקר×\94, ×\96×\94×\95 ×\9bנר×\90×\94 ×\91×\90×\92 ×\91ת×\95×\9b× ×\94.\n× ×\90 ×\9c×\93×\95×\95×\97 על כך ל[[Special:ListUsers/sysop|מפעיל מערכת]], תוך שמירת פרטי כתובת ה־URL.",
        "missingarticle-rev": "(מספר גרסה: $1)",
        "missingarticle-diff": "(השוואת הגרסאות: $1, $2)",
        "readonly_lag": "בסיס הנתונים ננעל אוטומטית כדי לאפשר לבסיסי הנתונים המשניים להתעדכן מהבסיס הראשי.",
        "internalerror": "שגיאה פנימית",
        "internalerror_info": "שגיאה פנימית: $1",
        "internalerror-fatal-exception": "שגיאה חמורה מסוג \"$1\"",
-       "filecopyerror": "×\94עתקת \"$1\" ×\9cÖ¾\"$2\" × ×\9bש×\9c×\94.",
-       "filerenameerror": "ש×\99× ×\95×\99 ×\94ש×\9d ×©×\9c \"$1\" ×\9cÖ¾\"$2\" × ×\9bש×\9c.",
-       "filedeleteerror": "×\9e×\97×\99קת \"$1\" × ×\9bש×\9c×\94.",
-       "directorycreateerror": "×\99צ×\99רת ×\94ת×\99ק×\99×\99×\94 \"$1\" × ×\9bש×\9c×\94.",
+       "filecopyerror": "×\9c×\90 × ×\99ת×\9f ×\94×\99×\94 ×\9c×\94עת×\99ק ×\90ת ×\94ק×\95×\91×¥ \"$1\" ×\9cק×\95×\91×¥ \"$2\".",
+       "filerenameerror": "×\9c×\90 × ×\99ת×\9f ×\94×\99×\94 ×\9cשנ×\95ת ×\90ת ×©×\9d ×\94ק×\95×\91×¥ \"$1\" ×\9cש×\9d \"$2\".",
+       "filedeleteerror": "×\9c×\90 × ×\99ת×\9f ×\94×\99×\94 ×\9c×\9e×\97×\95ק ×\90ת ×\94ק×\95×\91×¥ \"$1\".",
+       "directorycreateerror": "×\9c×\90 × ×\99ת×\9f ×\94×\99×\94 ×\9c×\99צ×\95ר ×\90ת ×\94ת×\99ק×\99×\99×\94 \"$1\".",
        "directoryreadonlyerror": "התיקייה \"$1\" היא לקריאה בלבד.",
        "directorynotreadableerror": "התיקייה \"$1\" אינה ניתנת לקריאה.",
        "filenotfound": "הקובץ \"$1\" לא נמצא.",
-       "unexpected": "ערך לא צפוי: \"$1\"=\"$2\"",
+       "unexpected": "ערך לא צפוי: \"$1\"=\"$2\".",
        "formerror": "שגיאה: לא ניתן היה לשלוח את הטופס.",
        "badarticleerror": "לא ניתן לבצע את הפעולה הזאת בדף זה.",
        "cannotdelete": "לא ניתן היה למחוק את הדף או הקובץ \"$1\".\nייתכן שהוא כבר נמחק על־ידי משתמש אחר.",
        "delete-hook-aborted": "המחיקה הופסקה על־ידי מבנה Hook.\nלא ניתן הסבר.",
        "no-null-revision": "לא ניתן היה ליצור גרסת־דמה בדף \"$1\"",
        "badtitle": "כותרת שגויה",
-       "badtitletext": "כותרת הדף המבוקש הייתה בלתי־תקינה, ריקה, או קישור שגוי לשפה אחרת או למיזם אחר.\nייתכן שהיא מכילה תו אחד או יותר שאינו יכול לשמש בכותרות.",
+       "badtitletext": "כותרת הדף המבוקש הייתה בלתי תקינה, ריקה, או קישור שגוי לשפה אחרת או למיזם אחר.\nייתכן שהיא מכילה תו אחד או יותר שאינו יכול לשמש בכותרות.",
        "title-invalid-empty": "כותרת הדף המבוקש ריקה או מכילה רק שם של מרחב שם.",
-       "title-invalid-utf8": "כותרת הדף המבוקש מכילה רצף UTF-8 בלתי־תקין.",
+       "title-invalid-utf8": "כותרת הדף המבוקש מכילה רצף UTF-8 בלתי תקין.",
        "title-invalid-interwiki": "כותרת הדף המבוקש מכילה קישור בינוויקי, שלא ניתן להשתמש בו בכותרות.",
        "title-invalid-talk-namespace": "כותרת הדף המבוקש מפנה לדף שיחה שאינו יכול להתקיים.",
-       "title-invalid-characters": "כותרת הדף המבוקש מכילה תווים בלתי־תקינים: \"$1\".",
-       "title-invalid-relative": "×\91×\9b×\95תרת ×\99ש × ×ª×\99×\91 ×\99×\97ס×\99. ×\9b×\95תרת ×\93פ×\99×\9d ×\99×\97ס×\99×\95ת (./, ../) ×\90×\99× ×\9f ×ª×§×\99× ×\95ת, ×\9b×\99×\95×\95×\9f ×©×\9cעת×\99×\9d ×§×¨×\95×\91×\95ת ×\94×\9f ×\9c×\90 ×\99×\94×\99×\95 ×\91× ×\95ת־השגה כשתטופלנה על־ידי הדפדפן של המשתמש.",
-       "title-invalid-magic-tilde": "כותרת הדף המבוקש מכילה רצף טילדות מיוחד (<nowiki>~~~</nowiki>).",
-       "title-invalid-too-long": "כותרת הדף המבוקש ארוכה מדי. היא צריכה להיות לכל היותר באורך {{PLURAL:$1|בית אחד|$1 בתים}} בקידוד UTF-8.",
-       "title-invalid-leading-colon": "כותרת הדף המבוקש מכילה תו נקודתיים בלתי־תקין בתחילתה.",
-       "perfcached": "המידע הבא הוא עותק שמור בזיכרון המטמון של המידע, ועשוי שלא להיות מעודכן. לכל היותר {{PLURAL:$1|תוצאה אחת נשמרת|$1 תוצאות נשמרות}} בזיכרון המטמון.",
-       "perfcachedts": "המידע הבא הוא עותק שמור בזיכרון המטמון של המידע, שעודכן לאחרונה ב־$1. לכל היותר {{PLURAL:$4|תוצאה אחת נשמרת|$4 תוצאות נשמרות}} בזיכרון המטמון.",
+       "title-invalid-characters": "כותרת הדף המבוקש מכילה תווים בלתי תקינים: \"$1\".",
+       "title-invalid-relative": "×\91×\9b×\95תרת ×\99ש × ×ª×\99×\91 ×\99×\97ס×\99. ×\9b×\95תרת ×\93פ×\99×\9d ×\99×\97ס×\99×\95ת (./, ../) ×\90×\99× ×\9f ×ª×§×\99× ×\95ת, ×\9eש×\95×\9d ×©×\9cעת×\99×\9d ×§×¨×\95×\91×\95ת ×\94×\9f ×\99×\94×\99×\95 ×\91×\9cת×\99Ö¾× ×\99תנ×\95ת ×\9cהשגה כשתטופלנה על־ידי הדפדפן של המשתמש.",
+       "title-invalid-magic-tilde": "כותרת הדף המבוקש מכילה רצף טילדות מיוחד שאינו תקין (<nowiki>~~~</nowiki>).",
+       "title-invalid-too-long": "כותרת הדף המבוקש ארוכה מדי. היא צריכה להיות לכל היותר באורך של {{PLURAL:$1|בייט אחד|$1 בייטים}} בקידוד UTF-8.",
+       "title-invalid-leading-colon": "כותרת הדף המבוקש מכילה תו נקודתיים בלתי תקין בתחילתה.",
+       "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×\9e×\94×\9c×\9a $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\" נכשלה. האם הוא כבר נוסף?",
        "passwordreset-emailelement": "שם משתמש:\n$1\n\nסיסמה זמנית:\n$2",
        "passwordreset-emailsentemail": "אם כתובת הדואר האלקטרוני הזאת משויכת לחשבון שלך, אז יישלח דואר אלקטרוני לאיפוס הסיסמה.",
        "passwordreset-emailsentusername": "אם יש כתובת דואר אלקטרוני שמשויכת לשם המשתמש הזה, אז יישלח דואר אלקטרוני לאיפוס הסיסמה.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|×\93×\95×\90\"×\9c ×\90×\99פ×\95ס ×\94ס×\99ס×\9e×\94 × ×©×\9c×\97\94×\95×\93×¢×\95ת ×\93×\95×\90\"×\9c ×©×\9c ×\90×\99פ×\95ס ×\94ס×\99ס×\9e×\94 × ×©×\9c×\97×\95}}. {{PLURAL:$1|ש×\9d ×\94×\9eשת×\9eש×\99×\9d ×\95×\94ס×\99ס×\9e×\94 ×\9e×\95צ×\92×\99×\9d|רש×\99×\9e×\94 ×©×\9c ×©×\9e×\95ת ×\94×\9eשת×\9eש×\99×\9d ×\95×\94ס×\99ס×\9e×\90×\95ת ×\9e×\95צ×\92ת}} ×\9c×\94×\9cן.",
-       "passwordreset-emailerror-capture2": "×\9c×\90 × ×\99ת×\9f ×\94×\99×\94 ×\9cש×\9c×\95×\97 ×\93×\95×\90\"×\9c ×\9c{{GENDER:$2|×\9eשת×\9eש|×\9eשת×\9eשת}}: $1 {{PLURAL:$3|ש×\9d ×\94×\9eשת×\9eש ×\95×\94ס×\99ס×\9e×\94 ×\9e×\95צ×\92×\99×\9d|רש×\99×\9e×\94 ×©×\9c ×©×\9e×\95ת ×\94×\9eשת×\9eש×\99×\9d ×\95×\94ס×\99ס×\9e×\90×\95ת ×\9e×\95צ×\92ת}} ×\9c×\94×\9cן.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|×\93×\95×\90\"×\9c ×\90×\99פ×\95ס ×\94ס×\99ס×\9e×\94 × ×©×\9c×\97\94×\95×\93×¢×\95ת ×\93×\95×\90\"×\9c ×©×\9c ×\90×\99פ×\95ס ×\94ס×\99ס×\9e×\94 × ×©×\9c×\97×\95}}. {{PLURAL:$1|ש×\9d ×\94×\9eשת×\9eש×\99×\9d ×\95×\94ס×\99ס×\9e×\94 ×\9e×\95צ×\92×\99×\9d|רש×\99×\9e×\94 ×©×\9c ×©×\9e×\95ת ×\94×\9eשת×\9eש×\99×\9d ×\95×\94ס×\99ס×\9e×\90×\95ת ×\9e×\95צ×\92ת}} ×\9b×\90ן.",
+       "passwordreset-emailerror-capture2": "×\9c×\90 × ×\99ת×\9f ×\94×\99×\94 ×\9cש×\9c×\95×\97 ×\93×\95×\90\"×\9c ×\9c{{GENDER:$2|×\9eשת×\9eש|×\9eשת×\9eשת}}: $1 {{PLURAL:$3|ש×\9d ×\94×\9eשת×\9eש ×\95×\94ס×\99ס×\9e×\94 ×\9e×\95צ×\92×\99×\9d|רש×\99×\9e×\94 ×©×\9c ×©×\9e×\95ת ×\94×\9eשת×\9eש×\99×\9d ×\95×\94ס×\99ס×\9e×\90×\95ת ×\9e×\95צ×\92ת}} ×\9b×\90ן.",
        "passwordreset-nocaller": "לא סופק הקורא הנדרש",
        "passwordreset-nosuchcaller": "הקורא אינו קיים: $1",
        "passwordreset-ignored": "איפוס הסיסמה לא בוצע. ייתכן שלא הוגדר ספק.",
        "searchprofile-advanced-tooltip": "חיפוש במרחבי שם מותאמים אישית",
        "search-result-size": "$1 ({{PLURAL:$2|מילה אחת|$2 מילים}})",
        "search-result-category-size": "{{PLURAL:$1|פריט אחד|$1 פריטים}} ({{PLURAL:$2|קטגוריית משנה אחת|$2 קטגוריות משנה}}, {{PLURAL:$3|קובץ אחד|$3 קבצים}})",
-       "search-redirect": "(הפניה $1)",
+       "search-redirect": "(הפניה מהדף $1)",
        "search-section": "(פסקה $1)",
        "search-category": "(קטגוריה $1)",
        "search-file-match": "(התאמה בתוכן הקובץ)",
        "upload-dialog-disabled": "אין אפשרות להעלות קבצים באמצעות תיבת הדו־שיח הזאת באתר זה.",
        "upload-dialog-title": "העלאת קובץ",
        "upload-dialog-button-cancel": "ביטול",
+       "upload-dialog-button-back": "חזרה",
        "upload-dialog-button-done": "בוצע",
        "upload-dialog-button-save": "שמירה",
        "upload-dialog-button-upload": "העלאה",
        "apisandbox-results-fixtoken-fail": "קבלת האסימון \"$1\" נכשלה.",
        "apisandbox-alert-page": "שדות בדף זה אינם תקינים.",
        "apisandbox-alert-field": "הערך של שדה זה אינו תקין.",
+       "apisandbox-continue": "המשך",
+       "apisandbox-continue-clear": "ניקוי",
+       "apisandbox-continue-help": "\"{{int:apisandbox-continue}}\" [https://www.mediawiki.org/wiki/API:Query#Continuing_queries ימשיך] את הבקשה האחרונה; \"{{int:apisandbox-continue-clear}}\" ינקה את הפרמטרים הקשורים להמשך.",
        "booksources": "משאבי ספרות חיצוניים",
        "booksources-search-legend": "חיפוש משאבי ספרות חיצוניים",
        "booksources-isbn": "מסת\"ב (ISBN):",
        "watcherrortext": "אירעה שגיאה בעת שינוי הגדרות רשימת המעקב של \"$1\".",
        "enotif_reset": "סימון כל הדפים כאילו נצפו",
        "enotif_impersonal_salutation": "משתמש ב{{GRAMMAR:תחילית|{{SITENAME}}}}",
-       "enotif_subject_deleted": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} נמחק על־ידי $2",
-       "enotif_subject_created": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} נוצר על־ידי $2",
-       "enotif_subject_moved": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} הועבר על־ידי $2",
-       "enotif_subject_restored": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} שוחזר על־ידי $2",
-       "enotif_subject_changed": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} שונה על־ידי $2",
-       "enotif_body_intro_deleted": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} נמחק ב־$PAGEEDITDATE על ידי $2, ראו $3.",
-       "enotif_body_intro_created": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} נוצר ב־$PAGEEDITDATE על ידי $2, ראו $3 לגרסה הנוכחית.",
-       "enotif_body_intro_moved": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} הועבר ב־$PAGEEDITDATE על ידי $2, ראו $3 לגרסה הנוכחית.",
-       "enotif_body_intro_restored": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} שוחזר ב־$PAGEEDITDATE על ידי $2, ראו $3 לגרסה הנוכחית.",
-       "enotif_body_intro_changed": "הדף $1 ב{{grammar:תחילית|{{SITENAME}}}} שונה ב־$PAGEEDITDATE על ידי $2, ראו $3 לגרסה הנוכחית.",
-       "enotif_lastvisited": "ראו $1 לכל השינויים מאז ביקורכם האחרון.",
+       "enotif_subject_deleted": "הדף \"$1\" ב{{grammar:תחילית|{{SITENAME}}}} נמחק על־ידי $2",
+       "enotif_subject_created": "הדף \"$1\" ב{{grammar:תחילית|{{SITENAME}}}} נוצר על־ידי $2",
+       "enotif_subject_moved": "הדף \"$1\" ב{{grammar:תחילית|{{SITENAME}}}} הועבר על־ידי $2",
+       "enotif_subject_restored": "הדף \"$1\" ב{{grammar:תחילית|{{SITENAME}}}} שוחזר על־ידי $2",
+       "enotif_subject_changed": "הדף \"$1\" ב{{grammar:תחילית|{{SITENAME}}}} שוּנה על־ידי $2",
+       "enotif_body_intro_deleted": "הדף \"$1\" באתר {{SITENAME}} נמחק ב־$PAGEEDITDATE על־ידי $2; ראו $3.",
+       "enotif_body_intro_created": "הדף \"$1\" באתר {{SITENAME}} נוצר ב־$PAGEEDITDATE על־ידי $2; ראו $3 לגרסה הנוכחית של הדף.",
+       "enotif_body_intro_moved": "הדף \"$1\" באתר {{SITENAME}} הועבר ב־$PAGEEDITDATE על־ידי $2; ראו $3 לגרסה הנוכחית של הדף.",
+       "enotif_body_intro_restored": "הדף \"$1\" באתר {{SITENAME}} שוחזר ב־$PAGEEDITDATE על־ידי $2; ראו $3 לגרסה הנוכחית של הדף.",
+       "enotif_body_intro_changed": "הדף \"$1\" באתר {{SITENAME}} שוּנה ב־$PAGEEDITDATE על־ידי $2; ראו $3 לגרסה הנוכחית של הדף.",
+       "enotif_lastvisited": "ראו $1 לכל השינויים מאז ביקורכם האחרון בדף.",
        "enotif_lastdiff": "ראו $1 לשינוי זה.",
        "enotif_anon_editor": "משתמש אנונימי $1",
-       "enotif_body": "×\9c×\9b×\91×\95×\93 $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nתקצ×\99ר ×\94ער×\99×\9b×\94: $PAGESUMMARY $PAGEMINOREDIT\n\n×\91×\90פשר×\95ת×\9b×\9d ×\9c×\99צ×\95ר ×§×©×¨ ×¢×\9d ×\94×¢×\95ר×\9a:\n×\91×\93×\95×\90ר ×\94×\90×\9cק×\98ר×\95× ×\99: $PAGEEDITOR_EMAIL\n×\91×\90תר: $PAGEEDITOR_WIKI\n\n×\9c×\90 ×ª×\94×\99×\99× ×\94 ×\94×\95×\93×¢×\95ת ×¢×\9c ×¤×¢×\95×\9c×\95ת × ×\95ספ×\95ת ×¢×\93 ×©×ª×\91קר×\95 ×\91×\93×£ ×\9bש×\90ת×\9d ×\9e×\97×\95×\91ר×\99×\9d ×\9c×\97ש×\91×\95×\9f. ×\91×\90פשר×\95ת×\9b×\9d ×\92×\9d ×\9c×\90פס ×\90ת ×\93×\92×\9c×\99 ×\94×\94×\95×\93×¢×\95ת ×\91×\9b×\9c ×\94×\93פ×\99×\9d ×©×\91רש×\99×\9eת ×\94×\9eעק×\91.\n\n×\9eער×\9bת ×\94×\94×\95×\93×¢×\95ת ×©×\9c {{SITENAME}}\n\n--\n×\9b×\93×\99 ×\9cשנ×\95ת ×\90ת ×\94×\94×\92×\93ר×\95ת ×©×\9c ×\94×\95×\93×¢×\95ת ×\94×\93×\95×\90\"×\9c ×\94נש×\9c×\97×\95ת ×\90×\9c×\99×\9b×\9d, ×\91קר×\95 ×\91×\93×£\n{{canonicalurl:{{#special:Preferences}}}}\n\n×\9b×\93×\99 ×\9cשנ×\95ת ×\90ת ×\94×\92×\93ר×\95ת ×¨×©×\99×\9eת ×\94×\9eעק×\91, ×\91קר×\95 ×\91×\93×£\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\n×\9b×\93×\99 ×\9c×\9e×\97×\95ק ×\90ת ×\94×\93×£ ×\9eרש×\99×\9eת ×\94×\9eעק×\91 ×©×\9c×\9b×\9d, ×\91קר×\95 ×\91×\93×£\n$UNWATCHURL\n\nלמשוב ולעזרה נוספת:\n$HELPPAGE",
+       "enotif_body": "×\9c×\9b×\91×\95×\93 $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nתקצ×\99ר ×\94ער×\99×\9b×\94: $PAGESUMMARY $PAGEMINOREDIT\n\n×\91×\90פשר×\95ת×\9b×\9d ×\9c×\99צ×\95ר ×§×©×¨ ×¢×\9d ×\94×¢×\95ר×\9a:\n×\91×\93×\95×\90ר ×\90×\9cק×\98ר×\95× ×\99: $PAGEEDITOR_EMAIL\n×\91×\90תר: $PAGEEDITOR_WIKI\n\n×\9c×\90 ×ª×§×\91×\9c×\95 ×\94×\95×\93×¢×\95ת ×¢×\9c ×¤×¢×\95×\9c×\95ת × ×\95ספ×\95ת ×¢×\93 ×©×ª×\91קר×\95 ×\91×\93×£ ×\94×\96×\94 ×\9bש×\90ת×\9d ×\9e×\97×\95×\91ר×\99×\9d ×\9c×\97ש×\91×\95×\9f. ×\91×\90פשר×\95ת×\9b×\9d ×\92×\9d ×\9c×\90פס ×\90ת ×\93×\92×\9c×\99 ×\94×\94×\95×\93×¢×\95ת ×¢×\91×\95ר ×\9b×\9c ×\94×\93פ×\99×\9d ×©×\91רש×\99×\9eת ×\94×\9eעק×\91 ×©×\9c×\9b×\9d.\n\n×\91×\91ר×\9b×\94, ×\9eער×\9bת ×\94×\94×\95×\93×¢×\95ת ×©×\9c {{SITENAME}}.\n\n--\n×\9b×\93×\99 ×\9cשנ×\95ת ×\90ת ×\94×\94×\92×\93ר×\95ת ×©×\9c ×\94×\95×\93×¢×\95ת ×\94×\93×\95×\90\"×\9c ×\94נש×\9c×\97×\95ת ×\90×\9c×\99×\9b×\9d, ×\91קר×\95 ×\91×\93×£:\n{{canonicalurl:{{#special:Preferences}}}}\n\n×\9b×\93×\99 ×\9cשנ×\95ת ×\90ת ×\94×\94×\92×\93ר×\95ת ×©×\9c ×¨×©×\99×\9eת ×\94×\9eעק×\91 ×©×\9c×\9b×\9d, ×\91קר×\95 ×\91×\93×£:\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\n×\9b×\93×\99 ×\9c×\94ס×\99ר ×\90ת ×\94×\93×£ ×\94×\96×\94 ×\9eרש×\99×\9eת ×\94×\9eעק×\91 ×©×\9c×\9b×\9d, ×\91קר×\95 ×\91×\93×£:\n$UNWATCHURL\n\nלמשוב ולעזרה נוספת:\n$HELPPAGE",
        "created": "נוצר",
-       "changed": "שונה",
+       "changed": "שוּנה",
        "deletepage": "מחיקת הדף",
        "confirm": "אישור",
        "excontent": "התוכן היה: \"$1\"",
        "tags-update-remove-not-allowed-multi": "לא ניתן להסיר את {{PLURAL:$2|התגית הבאה|התגיות הבאות}} ידנית: $1",
        "tags-edit-title": "עריכת תגיות",
        "tags-edit-manage-link": "ניהול תגיות",
-       "tags-edit-revision-selected": "{{PLURAL:$1|הגרסה שנבחרה|הגרסאות שנבחרו}} מתוך [[:$2]]:",
+       "tags-edit-revision-selected": "{{PLURAL:$1|הגרסה שנבחרה|הגרסאות שנבחרו}} מתוך הדף [[:$2]]:",
        "tags-edit-logentry-selected": "{{PLURAL:$1|פעולת היומן שנבחרה|פעולות היומן שנבחרו}}:",
-       "tags-edit-revision-legend": "×\94×\95ספ×\94 ×©×\9c ×ª×\92×\99×\95ת {{PLURAL:$1|×\9c×\92רס×\94 ×\94×\96×\90ת|×\9c×\9b×\9c $1 ×\94×\92רס×\90×\95ת}} ×\90×\95 ×\94סרת×\9f",
+       "tags-edit-revision-legend": "×\94×\95ספ×\94 ×\90×\95 ×\94סר×\94 ×©×\9c ×ª×\92×\99×\95ת {{PLURAL:$1|×\9e×\92רס×\94 ×\96×\95\9eÖ¾$1 ×\92רס×\90×\95ת}}",
        "tags-edit-logentry-legend": "הוספה של תגיות {{PLURAL:$1|לרשומת היומן הזאת|לכל $1 רשומות היומן}} או הסרתן",
-       "tags-edit-existing-tags": "ת×\92×\99×\95ת ×§×\99×\99×\9eות:",
+       "tags-edit-existing-tags": "×\94ת×\92×\99×\95ת ×\94× ×\95×\9b×\97×\99ות:",
        "tags-edit-existing-tags-none": "<em>אין</em>",
        "tags-edit-new-tags": "תגיות חדשות:",
        "tags-edit-add": "הוספת התגיות הבאות:",
        "tags-edit-remove": "הסרת התגיות הבאות:",
        "tags-edit-remove-all-tags": "(הסרת כל התגיות)",
-       "tags-edit-chosen-placeholder": "×\91×\97×\99רת ×ª×\92×\99×\95ת ×\9eס×\95×\99×\9eות",
+       "tags-edit-chosen-placeholder": "× ×\90 ×\9c×\91×\97×\95ר ×ª×\92×\99ות",
        "tags-edit-chosen-no-results": "לא נמצאו תגיות מתאימות",
        "tags-edit-reason": "סיבה:",
-       "tags-edit-revision-submit": "×\94×\97×\9cת ×©×\99× ×\95×\99×\99×\9d {{PLURAL:$1|×\9c×\92רס×\94 ×\94×\96×\90ת|ל־$1 גרסאות}}",
-       "tags-edit-logentry-submit": "×\94×\97×\9cת ×©×\99× ×\95×\99×\99×\9d {{PLURAL:$1|×\9cרש×\95×\9eת ×\94×\99×\95×\9e×\9f ×\94×\96×\90ת|×\9cÖ¾$1 ×¨×©×\95×\9eת ×\94יומן}}",
+       "tags-edit-revision-submit": "×\94×\97×\9cת ×\94ש×\99× ×\95×\99×\99×\9d {{PLURAL:$1|×\9c×\92רס×\94 ×\96×\95|ל־$1 גרסאות}}",
+       "tags-edit-logentry-submit": "×\94×\97×\9cת ×\94ש×\99× ×\95×\99×\99×\9d {{PLURAL:$1|×\9cרש×\95×\9eת ×\94×\99×\95×\9e×\9f ×\94×\96×\95\9cÖ¾$1 ×¨×©×\95×\9e×\95ת יומן}}",
        "tags-edit-success": "השינויים הוחלו.",
        "tags-edit-failure": "החלת השינויים נכשלה:\n$1",
        "tags-edit-nooldid-title": "גרסת היעד אינה תקינה",
        "htmlform-cloner-create": "הוספה",
        "htmlform-cloner-delete": "הסרה",
        "htmlform-cloner-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": "הערך שכתבת אינו תאריך מוכר. ניתן להשתמש בפורמט YYYY-MM-DD.",
+       "htmlform-time-invalid": "הערך שכתבת אינו שעה מוכרת. ניתן להשתמש בפורמט HH:MM:SS.",
+       "htmlform-datetime-invalid": "הערך שכתבת אינו תאריך ושעה מוכרים. ניתן להשתמש בפורמט YYYY-MM-DD HH:MM:SS.",
+       "htmlform-date-toolow": "הערך שכתבת הוא לפני התאריך המוקדם ביותר האפשרי, $1.",
+       "htmlform-date-toohigh": "הערך שכתבת הוא אחרי התאריך המאוחר ביותר האפשרי, $1.",
+       "htmlform-time-toolow": "הערך שכתבת הוא לפני השעה המוקדמת ביותר האפשרית, $1.",
+       "htmlform-time-toohigh": "הערך שכתבת הוא אחרי השעה המאוחרת ביותר האפשרית, $1.",
+       "htmlform-datetime-toolow": "הערך שכתבת הוא לפני התאריך והשעה המוקדמים ביותר האפשריים, $1.",
+       "htmlform-datetime-toohigh": "הערך שכתבת הוא אחרי התאריך והשעה המאוחרים ביותר האפשריים, $1.",
        "htmlform-title-badnamespace": "[[:$1]] אינו במרחב השם \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" אינו שם של דף שאפשר ליצור",
        "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 {{GENDER:$2|מחק|מחקה}} את הדף $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|שחזר|שחזרה}} את הדף $3",
        "logentry-delete-event": "$1 {{GENDER:$2|שינה|שינתה}} את מצב התצוגה של {{PLURAL:$5|פעולת יומן|$5 פעולות יומן}} של $3: $4",
        "feedback-external-bug-report-button": "דיווח על משימה טכנית",
        "feedback-dialog-title": "שליחת המשוב",
        "feedback-dialog-intro": "באפשרותך להשתמש בטופס הפשוט שלהלן כדי לשלוח משוב. ההערה שלך תתווסף לדף \"$1\", יחד עם שם המשתמש שלך.",
-       "feedback-error-title": "שגיאה",
        "feedback-error1": "שגיאה: תוצאה לא מזוהה מה־API",
        "feedback-error2": "שגיאה: העריכה נכשלה",
        "feedback-error3": "שגיאה: אין תשובה מה־API",
        "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 cb933de..097d801 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\" को जोड़ने में विफल हुआ। क्या यह पहले से है?",
        "table_pager_limit": "एक पृष्ठपर $1 आइटम दर्शायें",
        "table_pager_limit_label": "आइटम प्रति पृष्ठ:",
        "table_pager_limit_submit": "जायें",
-       "table_pager_empty": "रिà¥\9bलà¥\8dà¤\9f नहीं",
+       "table_pager_empty": "à¤\95à¥\8bहà¥\80 à¤ªà¤°à¤¿à¤£à¤¾à¤® नहीं",
        "autosumm-blank": "पृष्ठ को खाली किया",
        "autosumm-replace": "पृष्ठ को '$1' से बदल रहा है।",
        "autoredircomment": "[[$1]] को अनुप्रेषित",
        "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 ने $3 पृष्ठ की लॉग {{PLURAL:$5|प्रविष्टि|प्रविष्टियों}} की दृश्यता {{GENDER:$2|बदली}}: $4",
        "feedback-external-bug-report-button": "तकनीकी कार्य को जोड़ना",
        "feedback-dialog-title": "प्रतिपुष्टि भेजिए",
        "feedback-dialog-intro": "आप नीचे दिए गए सरल फ़ॉर्म का प्रयोग करके अपनी प्रतिपुष्टि भेज सकते हैं। आपकी टिप्पणी पृष्ठ \"$1\" से आपके सदस्यनाम के आगे जोड़ दी जाएगी।",
-       "feedback-error-title": "त्रुटि",
        "feedback-error1": "त्रुटि: न पहचाना गया परिणाम एपीआई से",
        "feedback-error2": "त्रुटि: संपादन विफल रहा है",
        "feedback-error3": "त्रुटि: एपीआई से कोई प्रतिक्रिया नहीं",
index ef1b90a..3335a30 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",
        "yourpasswordagain": "Ponovno upišite lozinku",
        "createacct-yourpasswordagain": "Potvrdi zaporku",
        "createacct-yourpasswordagain-ph": "Unesite zaporku ponovno",
-       "remembermypassword": "Zapamti moju lozinku na ovom računalu (najduže $1 {{PLURAL:$1|dan|dana}})",
        "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:",
        "upload-copy-upload-invalid-domain": "Kopije postavljenih datoteka nisu dostupne s ove domene.",
        "upload-dialog-title": "Postavi datoteku",
        "upload-dialog-button-cancel": "Odustani",
+       "upload-dialog-button-back": "Natrag",
        "upload-dialog-button-done": "Gotovo",
        "upload-dialog-button-save": "Spremi",
        "upload-dialog-button-upload": "Postavi",
        "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",
+       "watchlist": "Popis praćenja",
        "mywatchlist": "Popis praćenja",
        "watchlistfor2": "Za $1 $2",
        "nowatchlist": "Na Vašem popisu praćenja nema nijednog članka.",
        "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",
        "export-download": "Ponudi opciju snimanja u datoteku",
        "export-templates": "Uključi predloške",
        "export-pagelinks": "Uključi povezane stranice do dubine od:",
-       "allmessages": "Sve sistemske poruke",
+       "allmessages": "Sve poruke sustava",
        "allmessagesname": "Ime",
        "allmessagesdefault": "Prvotni tekst",
        "allmessagescurrent": "Trenutačni tekst",
        "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 suradnika  {{GENDER:$1|this user}}",
+       "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",
        "htmlform-cloner-create": "Dodaj još",
        "htmlform-cloner-delete": "Ukloni",
        "htmlform-cloner-required": "Potrebna je barem jedna vrijednost.",
-       "sqlite-has-fts": "$1 s podrškom pretraživanja cijelog teksta",
-       "sqlite-no-fts": "$1 bez podrške pretraživanja cijelog teksta",
+       "htmlform-date-placeholder": "GGGG-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "GGGG-MM-DD HH:MM:SS",
+       "htmlform-time-invalid": "Unesena vrijednost nije prepoznati format vremena. Pokušajte koristiti format HH:MM:SS.",
+       "htmlform-datetime-toohigh": "Uneseni datum i vrijeme su veći od $1",
        "logentry-delete-delete": "$1 je {{GENDER:$2|obrisao|obrisala}} stranicu $3",
        "logentry-delete-restore": "$1 je {{GENDER:$2|vratio|vratila}} stranicu $3",
        "logentry-delete-event": "$1 je {{GENDER:$2|promijenio|promijenila}} vidljivost {{PLURAL:$5|zapisa u evidenciji|$5 zapisa u evidenciji}} na $3: $4",
        "feedback-external-bug-report-button": "Arhiviraj tehnički zadatak",
        "feedback-dialog-title": "Slanje povratnih informacija",
        "feedback-dialog-intro": "Da biste poslali povratnu informaciju, rabite jednostavan obrazac. Vaš će komentar biti dodan na stranici \"$1\" s Vašim suradničkim imenom.",
-       "feedback-error-title": "Pogrješka",
        "feedback-error1": "Pogreška: neprepoznati rezultat API funkcije",
        "feedback-error2": "Pogreška: uređivanje nije uspjelo",
        "feedback-error3": "Pogreška: nema odgovora API funkcije",
        "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 d8780e1..cdd7d39 100644 (file)
@@ -72,7 +72,7 @@
        "tog-enotifminoredits": "Kapjak értesítést e-mailben a lapok és fájlok apró változtatásairól",
        "tog-enotifrevealaddr": "Jelenjen meg az e-mail címem a figyelmeztető e-mailekben",
        "tog-shownumberswatching": "A lapot figyelő szerkesztők számának megjelenítése",
-       "tog-oldsig": "A jelenlegi aláírás:",
+       "tog-oldsig": "A jelenlegi aláírásod:",
        "tog-fancysig": "Az aláírás wikiszöveg (nem lesz automatikusan hivatkozásba rakva)",
        "tog-uselivepreview": "Élő előnézet használata",
        "tog-forceeditsummary": "Figyelmeztessen, ha nem adok meg szerkesztési összefoglalót",
        "newwindow": "(új ablakban nyílik meg)",
        "cancel": "Mégse",
        "moredotdotdot": "Tovább…",
-       "morenotlisted": "A lista nem teljes.",
+       "morenotlisted": "A lista hiányos lehet.",
        "mypage": "Lapom",
        "mytalk": "Vitalap",
        "anontalk": "Vitalap",
        "talk": "Vitalap",
        "views": "Nézetek",
        "toolbox": "Eszközök",
+       "tool-link-userrights": "{{GENDER:$1|Felhasználócsoportok}} módosítása",
+       "tool-link-emailuser": "E-mail küldése ennek a {{GENDER:$1|felhasználónak}}",
        "userpage": "Felhasználó lapjának megtekintése",
        "projectpage": "Projektlap megtekintése",
        "imagepage": "A fájl leírólapjának megtekintése",
        "missingarticle-rev": "(változat azonosítója: $1)",
        "missingarticle-diff": "(eltérés: $1, $2)",
        "readonly_lag": "Az adatbázis automatikusan le lett zárva, amíg a mellékkiszolgálók utolérik a főkiszolgálót.",
+       "nonwrite-api-promise-error": "A „Promise-Non-Write-API-Action” (ígéret nem író API-műveletre) HTTP-fejléc szerepelt a kérésben, de a kérés egy író API-modulra irányult.",
        "internalerror": "Belső hiba",
        "internalerror_info": "Belső hiba: $1",
        "internalerror-fatal-exception": "Végzetes kivétel: „$1”",
        "createacct-email-ph": "Add meg e-mail címed",
        "createacct-another-email-ph": "Add meg az emailcímet",
        "createaccountmail": "Átmeneti, véletlenszerű jelszó beállítása és kiküldése a megadott e-mail címre",
+       "createaccountmail-help": "A jelszó megismerése nélkül készíthető valaki másnak fiók.",
        "createacct-realname": "Igazi neved (nem kötelező)",
        "createaccountreason": "Indoklás:",
        "createacct-reason": "Indoklás",
        "createacct-reason-ph": "Miért hozol létre egy másik fiókot",
+       "createacct-reason-help": "A fióklétrehozási naplóban megjelenő üzenet",
        "createacct-submit": "Felhasználói fiók létrehozása",
        "createacct-another-submit": "Fiók létrehozása",
        "createacct-continue-submit": "Fiók létrehozásának folytatása",
        "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?",
+       "botpasswords-update-failed": "A(z) „$1” nevű botfiók frissítése sikertelen. Lehet, hogy törölted?",
        "botpasswords-created-title": "Botjelszó létrehozva",
        "botpasswords-created-body": "\"$2\" felhasználó \"$1\" bot jelszava létrehozva.",
        "botpasswords-updated-title": "Botjelszó frissítve",
        "botpasswords-updated-body": "\"$2\" felhasználó \"$1\" bot jelszava módosítva.",
        "botpasswords-deleted-title": "Botjelszó törölve",
        "botpasswords-deleted-body": "\"$2\" felhasználó \"$1\" bot jelszava törölve.",
+       "botpasswords-newpassword": "A bejelentkezéshez használható új felhasználóneved <strong>$1</strong>, jelszavad <strong>$2</strong>. <em>Ezeket jegyezd fel a későbbiekre.</em> <br> (Régebbi botoknál, amik megkövetelhetik, hogy a bejelentkezési név megegyezzen magával a felhasználónévvel, használhatod a(z) <strong>$3</strong> felhasználónevet is <strong>$4</strong> jelszóval.)",
        "botpasswords-no-provider": "A BotPasswordsSessionProvider nem áll rendelkezésre.",
+       "botpasswords-restriction-failed": "A botjelszó-korlátozások megakadályozzák ezt a bejelentkezést.",
+       "botpasswords-invalid-name": "A megadott felhasználónév nem tartalmazza a botjelszó-elválasztót („$1”).",
+       "botpasswords-not-exist": "A(z) „$1” felhasználó nem rendelkezik „$2” nevű botjelszóval.",
        "resetpass_forbidden": "A jelszavak nem változtathatók meg",
        "resetpass_forbidden-reason": "A jelszavakat nem változtathatóak meg: $1",
        "resetpass-no-info": "Be kell jelentkezned, hogy közvetlenül elérd ezt a lapot.",
        "passwordreset-emailelement": "Felhasználónév: \n$1\n\nIdeiglenes jelszó: \n$2",
        "passwordreset-emailsentemail": "Ha ez az e-mail-cím van a fiókodhoz társítva, egy jelszó-visszaállító e-mailt küldünk.",
        "passwordreset-emailsentusername": "Ha ehhez a felhasználónévhez tartozik e-mail cím, akkor egy jelszó-visszaállító levelet küld a rendszer.",
-       "passwordreset-emailsent-capture2": "A jelszóvisszaállító {{PLURAL:$1|e-mailt|e-maileket}} elküldtük. A felhasználói {{PLURAL:$1|név és a jelszó|nevek és jelszavak listája}} lentebb látható.",
+       "passwordreset-emailsent-capture2": "A jelszóvisszaállító {{PLURAL:$1|e-mailt|e-maileket}} elküldtük. A {{PLURAL:$1|felhasználónév és a jelszó|felhasználónevek és jelszavak listája}} itt látható.",
+       "passwordreset-emailerror-capture2": "Az e-mail-küldés {{GENDER:$2|sikertelen}}: $1. A {{PLURAL:$3|felhasználónév és a jelszó|felhasználónevek és jelszavak listája}} itt látható.",
+       "passwordreset-nocaller": "A hívó megadása kötelező",
+       "passwordreset-nosuchcaller": "A hívó nem létezik: $1",
+       "passwordreset-ignored": "A jelszó-visszaállítás nem lett kezelve. Talán nincs konfigurálva szolgáltató?",
        "passwordreset-invalideamil": "Érvénytelen e-mail cím",
+       "passwordreset-nodata": "Se felhasználónevet, sem e-mail-címet nem adtál meg",
        "changeemail": "E-mail cím megváltoztatása vagy eltávolítása",
        "changeemail-header": "Töltsd ki ezt az űrlapot az e-mail-címed megváltoztatásához. Ha nem szeretnél semmilyen e-mail-címet kapcsolni a fiókodhoz, hagyd üresen az új e-mail-cím mezőjét az űrlap elküldésekor.",
        "changeemail-no-info": "A lap közvetlen eléréséhez be kell jelentkezned.",
        "accmailtext": "A(z) [[User talk:$1|$1]] fiókhoz egy véletlenszerűen generált jelszót küldünk a(z) $2 címre.\n\nAz új fiók jelszava a ''[[Special:ChangePassword|jelszó megváltoztatása]]'' lapon módosítható a bejelentkezés után.",
        "newarticle": "(Új)",
        "newarticletext": "Egy olyan lapra mutató hivatkozást követtél, ami még nem létezik.\nA lap létrehozásához csak gépeld be a szövegét a lenti szövegdobozba. Ha kész vagy, az „Előnézet megtekintése” gombbal ellenőrizheted, hogy úgy fog-e kinézni, ahogy szeretnéd, és a „Lap mentése” gombbal tudod elmenteni. (További információkat a [$1 súgólapon] találsz).\nHa tévedésből jutottál ide, kattints a böngésződ '''vissza''' vagy '''back''' gombjára.",
-       "anontalkpagetext": "----''Ez egy olyan anonim szerkesztő vitalapja, aki még nem regisztrált, vagy csak nem jelentkezett be.\nEzért az IP-címét használjuk az azonosítására.\nUgyanazon az IP-címen számos szerkesztő osztozhat az idők folyamán.\nHa úgy látod, hogy az üzenetek, amiket ide kapsz, nem neked szólnak, [[Special:CreateAccount|regisztrálj]] vagy ha már regisztráltál, [[Special:UserLogin|jelentkezz be]], hogy ne keverjenek össze másokkal.''",
+       "anontalkpagetext": "----\n<em>Ez egy olyan anonim felhasználó vitalapja, aki még nem regisztrált, vagy csak nem jelentkezett be.</em>\nEzért az IP-címét kell használnunk az azonosítására.\nUgyanazon az IP-címen számos szerkesztő osztozhat az idők folyamán.\nHa anonim felhasználó vagy, és úgy látod, hogy az üzenetek, amiket kapsz, nem neked szólnak, [[Special:CreateAccount|regisztrálj]] vagy [[Special:UserLogin|jelentkezz be]], hogy ne keverjenek össze másokkal.",
        "noarticletext": "Ez a lap jelenleg nem tartalmaz szöveget.\n[[Special:Search/{{PAGENAME}}|Rákereshetsz erre a címszóra]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} megtekintheted a kapcsolódó naplókat],\nvagy [{{fullurl:{{FULLPAGENAME}}|action=edit}} létrehozhatod a lapot].</span>",
        "noarticletext-nopermission": "Ez a lap jelenleg nem tartalmaz szöveget.\n[[Special:Search/{{PAGENAME}}|Rákereshetsz a lap címére]] más lapok tartalmában, vagy <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} megtekintheted a kapcsolódó naplófájlokat]</span>.",
        "missing-revision": "A(z) \"{{FULLPAGENAME}}\" nevű oldal #$1 változata nem létezik.\n\nEzt általában egy elavult, törölt oldalra mutató laptörténeti hivatkozás használata okozza. Részletek a [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} törlési naplóban] találhatóak.",
        "invalid-content-data": "Érvénytelen tartalom adat",
        "content-not-allowed-here": "\"$1\" tartalom nem engedélyezett a [[$2]] oldalon",
        "editwarning-warning": "A lap elhagyásával az összes itt végzett változtatás elveszhet.\nHa be vagy jelentkezve letilthatod ezt a figyelmeztetést a beállításaid „{{int:prefs-editing}}” szakaszában.",
+       "editpage-invalidcontentmodel-title": "A tartalommodell nem támogatott",
+       "editpage-invalidcontentmodel-text": "A(z) „$1” tartalommodell nem támogatott.",
        "editpage-notsupportedcontentformat-title": "Nem támogatott tartalom formátum",
        "editpage-notsupportedcontentformat-text": "$2 tartalommodell nem támogatja $1 tartalomformátumot.",
        "content-model-wikitext": "wikiszöveg",
        "content-json-empty-object": "Üres objektum",
        "content-json-empty-array": "Üres tömb",
        "deprecated-self-close-category": "Érvénytelen önzáró HTML-címkéket használó lapok",
+       "deprecated-self-close-category-desc": "A lap érvénytelen önzáró HTML-címkéket használ (pl. <code>&lt;b/></code> vagy <code>&lt;span/></code>). Ezeknek a működése hamarosan meg fog változni a HTML5 szabvánnyal összhangban lévőre, ezért a wikiszövegben való használatuk elavult.",
        "duplicate-args-warning": "<strong>Figyelmeztetés:</strong> A(z) [[:$1]] lap dupla értékkel hívja meg a(z) [[:$2]] sablont („$3” paraméter). Csak az utolsó érték lesz felhasználva.",
        "duplicate-args-category": "Dupla paramétermegadást tartalmazó lapok",
        "duplicate-args-category-desc": "Az oldal olyan sablon hívásokat tartalmaz, amely ugyanazt a paramétert használja, például <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> or <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "undo-summary": "Visszavontam [[Special:Contributions/$2|$2]] ([[User talk:$2|vita]]) szerkesztését (oldid: $1)",
        "undo-summary-username-hidden": "A rejtett felhasználó által végzett $1 változat visszavonása",
        "cantcreateaccount-text": "Erről az IP-címről ('''$1''') nem lehet regisztrálni, mert [[User:$3|$3]] blokkolta az alábbi indokkal:\n\n:''$2''",
-       "cantcreateaccount-range-text": "A regisztrációt a(z) <strong>$1</strong> IP-címtartományban, amelybe a te IP-címed (<strong>$4</strong>) is tartozik, [[User:$3|$3]] blokkolta.",
+       "cantcreateaccount-range-text": "A regisztrációt a(z) <strong>$1</strong> IP-címtartományban, amelybe a te IP-címed (<strong>$4</strong>) is tartozik, [[User:$3|$3]] blokkolta.\n\n$3 a következő indoklást adta: <em>$2</em>",
        "viewpagelogs": "A lap a rendszernaplókban",
        "nohistory": "A lap nem rendelkezik laptörténettel.",
        "currentrev": "Aktuális változat",
        "revdelete-submit": "Alkalmazás a kiválasztott {{PLURAL:$1|változatra|változatokra}}",
        "revdelete-success": "A változat láthatósága sikeresen frissítve.",
        "revdelete-failure": "'''Nem sikerült frissíteni a változat láthatóságát:'''\n$1",
-       "logdelete-success": "'''Az esemény láthatóságának beállítása sikeresen elvégezve.'''",
+       "logdelete-success": "A naplóbejegyzés láthatósága beállítva.",
        "logdelete-failure": "'''Nem sikerült módosítani a naplóbejegyzés láthatóságát:'''\n$1",
        "revdel-restore": "Láthatóság megváltoztatása",
        "pagehist": "Laptörténet",
        "mergehistory-fail-bad-timestamp": "Érvénytelen időbélyeg.",
        "mergehistory-fail-invalid-source": "Érvénytelen forráslap.",
        "mergehistory-fail-invalid-dest": "Érvénytelen céllap.",
+       "mergehistory-fail-no-change": "A laptörténet-összefésülő nem fésült össze egy változatot sem. Ellenőrizd a lap és idő paramétereket.",
        "mergehistory-fail-permission": "Nincsen jogod a laptörténetek egyesítéséhez.",
        "mergehistory-fail-self-merge": "A forrás- és céllap megegyezik.",
+       "mergehistory-fail-timestamps-overlap": "A forrásváltozatok átfedésben vannak vagy későbbiek a célváltozatoknál.",
        "mergehistory-fail-toobig": "Nem lehetséges a laptörténetek egyesítése, mivel több mint $1 {{PLURAL:$1|változást}} kellene áthelyezni.",
        "mergehistory-no-source": "Nem létezik forráslap $1 néven.",
        "mergehistory-no-destination": "Nem létezik céllap $1 néven.",
        "diff-multi-sameuser": "({{PLURAL:$1|Egy közbenső módosítás|$1 közbenső módosítás}} ugyanattól a szerkesztőtől nincs mutatva)",
        "diff-multi-otherusers": "({{PLURAL:$1|Egy közbenső módosítás|$1 közbenső módosítás}}, amit {{PLURAL:$2|egy másik szerkesztő végzett|$2 másik szerkesztő végzett}}, nincs mutatva)",
        "diff-multi-manyusers": "({{PLURAL:$1|Egy közbeeső változat|$1 közbeeső változat}} nincs mutatva, amit $2 szerkesztő módosított)",
-       "difference-missing-revision": "A(z) \"{{PAGENAME}}\" nevű oldal #$1 $2 változata nem létezik.\n\nEzt általában egy elavult, törölt oldalra mutató laptörténeti hivatkozás használata okozza. Részletek a [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} törlési naplóban] találhatóak.",
+       "difference-missing-revision": "Az összehasonlítandó változatok {{PLURAL:$2|egyike ($1) nem található|($1) nem találhatóak}}.\n\nEzt általában egy elavult, törölt oldalra mutató laptörténeti hivatkozás használata okozza. Részletek a [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} törlési naplóban] találhatóak.",
        "searchresults": "A keresés eredménye",
        "searchresults-title": "Keresési eredmények: „$1”",
        "titlematches": "Címbeli egyezések",
        "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",
        "grant-group-high-volume": "Nagy mennyiségű szerkesztés végrehajtása",
        "grant-group-customization": "Személyre szabás és beállítások",
        "grant-group-administration": "Adminisztratív műveletek végrehajtása",
+       "grant-group-private-information": "Privát adataid elérése",
        "grant-group-other": "egyéb műveletek",
        "grant-blockusers": "felhasználók blokkolása és blokk feloldása",
        "grant-createaccount": "fiókok létrehozása",
        "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",
        "recentchanges-page-added-to-category-bundled": "[[:$1]] hozzáadva a kategóriához, [[Special:WhatLinksHere/$1|ez a lap be van illesztve más lapokra]]",
        "recentchanges-page-removed-from-category": "[[:$1]] eltávolítva a kategóriából",
        "recentchanges-page-removed-from-category-bundled": "[[:$1]] eltávolítva a kategóriából, [[Special:WhatLinksHere/$1|ez a lap be van illesztve más lapokra]]",
+       "autochange-username": "MediaWiki automatikus módosítása",
        "upload": "Fájl feltöltése",
        "uploadbtn": "Fájl feltöltése",
        "reuploaddesc": "Visszatérés a feltöltési űrlaphoz.",
        "file-thumbnail-no": "A fájlnév a(z) <strong>$1</strong> karakterlánccal kezdődik.\nÚgy tűnik, hogy ez egy kisméretű kép ''(bélyegkép)''.\nHa rendelkezel a teljesméretű képpel, akkor töltsd fel azt, egyébként kérjük, hogy változtasd meg a fájlnevet.",
        "fileexists-forbidden": "Már létezik egy ugyanilyen nevű fájl, és nem lehet felülírni.\nHa még mindig fel szeretnéd tölteni a fájlt, menj vissza, és adj meg egy új nevet. [[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Egy ugyanilyen nevű fájl már létezik a közös fájlmegosztóban; kérlek menj vissza és válassz egy másik nevet a fájlnak, ha még mindig fel akarod tölteni! [[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "A feltöltendő fájl pontos másolata a(z) <strong>[[:$1]]</strong> jelenlegi változatának.",
+       "fileexists-duplicate-version": "A feltöltendő fájl pontos másolata a(z) <strong>[[:$1]]</strong> {{PLURAL:$2|egy régebbi változatának|régebbi változatainak}}.",
        "file-exists-duplicate": "Ez a következő {{PLURAL:$1|fájl|fájlok}} duplikátuma:",
        "file-deleted-duplicate": "Egy ehhez hasonló fájlt ([[:$1]]) korábban már töröltek. Ellenőrizd a fájl törlési naplóját, mielőtt újra feltöltenéd.",
        "file-deleted-duplicate-notitle": "Egy ugyanilyen fájlt korábban már töröltek, és címét eltávolították. Kérj meg valakit, aki meg tudja nézni a törölt fájlokat, hogy tekintse át a helyzetet, mielőtt újra feltöltenéd a fájlt.",
        "uploaded-script-svg": "A feltöltött SVG fájlodban szkriptelemet találtunk: \"$1\".",
        "uploaded-hostile-svg": "Nem biztonságos CSS kódot találtunk a feltöltött SVG fájlod stíluselemei között.",
        "uploaded-event-handler-on-svg": "Az alábbi eseménykezelő-attribútum beállítása nem megengedett az SVG fájlokban: <code>$1=$2</code>.",
+       "uploaded-href-attribute-svg": "href attribútumok SVG fájlokban csak http:// vagy https:// protokollal engedélyezettek, <code>&lt;$1 $2=\"$3\"&gt;</code> található.",
        "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>.",
        "upload-too-many-redirects": "Az URL túl sokszor volt átirányítva",
        "upload-http-error": "HTTP-hiba történt: $1",
        "upload-copy-upload-invalid-domain": "Másolás nem engedélyezett ebből a tartományból.",
+       "upload-foreign-cant-upload": "A wiki nincs konfigurálva fájlok feltöltésére a kért külső fájltárolóba.",
+       "upload-foreign-cant-load-config": "A külső fájltárolóba való fájlfeltöltés konfigurációjának beolvasása sikertelen.",
        "upload-dialog-disabled": "Fájl feltöltés ezzel a párbeszéddel tiltott ezen a wikin.",
        "upload-dialog-title": "Fájl feltöltése",
        "upload-dialog-button-cancel": "Mégse",
        "backend-fail-read": "Nem sikerült olvasni ebből a fájlból: $1.",
        "backend-fail-create": "Nem sikerült írni ebbe a fájlba: $1.",
        "backend-fail-maxsize": "Nem lehet írni ezt a fájlt: $1, mert a mérete nagyobb, mint $2 bájt.",
-       "backend-fail-readonly": "A(z) „$1” tárolórendszer jelenleg csak olvasható. Ennek oka a következő: „$2”",
+       "backend-fail-readonly": "A(z) „$1” tárolórendszer jelenleg csak olvasható. Ennek oka a következő: <em>$2</em>",
        "backend-fail-synced": "A(z) „$1” fájl inkonzisztens állapotban van a tárolórendszerek között",
        "backend-fail-connect": "Nem sikerült csatlakozni a(z) „$1” tárolórendszerhez.",
        "backend-fail-internal": "Ismeretlen hiba keletkezett a(z) „$1” tárolórendszerben.",
        "filerevert-submit": "Visszaállítás",
        "filerevert-success": "<span class=\"plainlinks\">A(z) '''[[Media:$1|$1]]''' fájl visszaállítása a(z) [$4 verzióra, $3, $2] sikerült.</span>",
        "filerevert-badversion": "A megadott időbélyegzésű fájlnak nincs helyi változata.",
+       "filerevert-identical": "A fájl jelenlegi verziója már azonos a kiválasztottal.",
        "filedelete": "$1 törlése",
        "filedelete-legend": "Fájl törlése",
        "filedelete-intro": "Törölni készülsz a(z) '''[[Media:$1|$1]]''' médiafájlt, a teljes fájltörténetével együtt.",
        "apisandbox-api-disabled": "API le van tiltva ezen az oldalon.",
        "apisandbox-intro": "Ezen az oldalon kísérletezhetsz a <strong>MediaWiki web service API</strong>-val.\nA használattal kapcsolatos további részletek az [[mw:API:Main page|API-dokumentációnál]] találhatók. Példa: [https://www.mediawiki.org/wiki/API#A_simple_example olvasd el a főoldal tartalomjegyzékét]. További példákért válassz egy tevékenységet!\n\nFigyelj rá, hogy bár ez csak egy „homokozó”, ettől még az általad végzett műveletek módosíthatják a wikit!",
        "apisandbox-fullscreen": "Panel kinyitása",
+       "apisandbox-fullscreen-tooltip": "A homokozópanel kinyitása a böngészőablak kitöltéséhez.",
        "apisandbox-unfullscreen": "Lap mutatása",
+       "apisandbox-unfullscreen-tooltip": "A homokozópanel méretének csökkentése a MediaWiki navigációs hivatkozásainak megjelenítéséhez.",
        "apisandbox-submit": "Kérés végrehajtása",
        "apisandbox-reset": "Törlés",
        "apisandbox-retry": "Újra",
        "apisandbox-dynamic-parameters-add-placeholder": "Paraméter neve",
        "apisandbox-dynamic-error-exists": "A(z) „$1” nevű paraméter már létezik.",
        "apisandbox-deprecated-parameters": "Elavult paraméterek",
+       "apisandbox-fetch-token": "A token automatikus kitöltése",
        "apisandbox-submit-invalid-fields-title": "Egyes mezők érvénytelenek",
        "apisandbox-submit-invalid-fields-message": "Javítsd ki a jelzett mezőket, és próbáld újra.",
        "apisandbox-results": "Eredmények",
        "apisandbox-sending-request": "API-kérés küldése…",
        "apisandbox-loading-results": "API-válaszok fogadása…",
+       "apisandbox-results-error": "Hiba történt az API-lekérdezés válaszának betöltése közben: $1.",
        "apisandbox-request-url-label": "Kérő URL:",
        "apisandbox-request-time": "Kérés hossza: $1 ms",
+       "apisandbox-results-fixtoken": "Token javítása és újrapróbálkozás",
+       "apisandbox-results-fixtoken-fail": "A(z) „$1” token lekérése sikertelen.",
+       "apisandbox-alert-page": "Hibás mezők vannak ezen a lapon.",
        "apisandbox-alert-field": "Ennek a mezőnek az értéke érvénytelen.",
        "booksources": "Könyvforrások",
        "booksources-search-legend": "Könyvforrások keresése",
        "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",
        "trackingcategories-msg": "Nyomkövető kategória",
        "trackingcategories-name": "Üzenetnév",
        "trackingcategories-desc": "Kategóriába kerülés feltétele",
+       "restricted-displaytitle-ignored": "Lapok figyelmen kívül hagyott megjelenítendő lapcímmel",
+       "restricted-displaytitle-ignored-desc": "A lapon figyelmen kívül hagyott <code><nowiki>{{DISPLAYTITLE}}</nowiki></code> van, mivel a megadott cím nem egyezik a lap tényleges címével.",
        "noindex-category-desc": "A lapot nem indexelik a keresőrobotok, mert tartalmazza a <code><nowiki>__NOINDEX__</nowiki></code> varázsszót, és egy olyan névtérben található, ahol ez engedélyezett.",
        "index-category-desc": "A lapot akkor is indexelik a keresőrobotok, ha egyébként nem tennék, mert tartalmazza az <code><nowiki>__INDEX__</nowiki></code> varázsszót, és egy olyan névtérben található, ahol ez engedélyezett.",
        "post-expand-template-inclusion-category-desc": "A lap mérete nagyobb a <code>$wgMaxArticleSize</code> változóban tárolt értéknél a sablonok kibontása után, így néhány sablon nem került kibontásra.",
        "watchnologin": "Nem vagy bejelentkezve",
        "addwatch": "Hozzáadás a figyelőlistához",
        "addedwatchtext": "A(z) „[[:$1]]” lapot és vitalapját hozzáadtam a [[Special:Watchlist|figyelőlistádhoz]].",
+       "addedwatchtext-talk": "A(z) „[[:$1]]” lapot és a hozzá tartozó tartalmi lapot hozzáadtam a [[Special:Watchlist|figyelőlistádhoz]].",
        "addedwatchtext-short": "Az oldal: \"$1\" hozzá lett adva a figyelőlistádhoz.",
        "removewatch": "Eltávolítás a figyelőlistáról",
        "removedwatchtext": "A(z) „[[:$1]]” lapot és vitalapját eltávolítottam a [[Special:Watchlist|figyelőlistáról]].",
+       "removedwatchtext-talk": "A(z) „[[:$1]]” lapot és a hozzá tartozó tartalmi lapot eltávolítottam a [[Special:Watchlist|figyelőlistáról]].",
        "removedwatchtext-short": "Az oldal: \"$1\" el lett távolítva a figyelőlistádról.",
        "watch": "Lap figyelése",
        "watchthispage": "Lap figyelése",
        "delete-toobig": "Ennek a lapnak a laptörténete több mint {{PLURAL:$1|egy|$1}} változatot őriz. A szervert kímélendő az ilyen lapok törlése nem engedélyezett.",
        "delete-warning-toobig": "Ennek a lapnak a laptörténete több mint {{PLURAL:$1|egy|$1}} változatot őriz. Törlése fennakadásokat okozhat a wiki adatbázis-műveleteiben; óvatosan járj el.",
        "deleteprotected": "Nem tudod törölni a lapot, mivel le van védve.",
-       "deleting-backlinks-warning": "'''Figyelem:'''  [[Special:WhatLinksHere/{{FULLPAGENAME}}|Más lapok]] hivatkoznak a törlendő oldalra.",
+       "deleting-backlinks-warning": "<strong>Figyelem:</strong> [[Special:WhatLinksHere/{{FULLPAGENAME}}|Más lapok]] hivatkoznak a törlendő oldalra (vagy beillesztik azt).",
        "rollback": "Szerkesztések visszaállítása",
        "rollbacklink": "visszaállítás",
        "rollbacklinkcount": "$1 szerkesztés visszaállítása",
        "rollbacklinkcount-morethan": "több mint $1 szerkesztés visszaállítása",
        "rollbackfailed": "A visszaállítás nem sikerült",
+       "rollback-missingparam": "Kötelező paraméterek hiányoznak a kérésből.",
+       "rollback-missingrevision": "A lapváltozat adatainak betöltése sikertelen.",
        "cantrollback": "Nem lehet visszaállítani: az utolsó szerkesztést végző felhasználó az egyetlen, aki a lapot szerkesztette.",
        "alreadyrolled": "[[:$1]] utolsó, [[User:$2|$2]] ([[User talk:$2|vita]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]) általi szerkesztését nem lehet visszavonni:\nidőközben valaki már visszavonta vagy szerkesztette a lapot.\n\nAz utolsó szerkesztést [[User:$3|$3]] ([[User talk:$3|vita]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]) végezte.",
        "editcomment": "A szerkesztési összefoglaló <em>$1</em> volt.",
        "revertpage": "Visszaállítottam a lap korábbi változatát: [[Special:Contributions/$2|$2]]  ([[User talk:$2|vita]]) szerkesztéséről [[User:$1|$1]] szerkesztésére",
        "revertpage-nouser": "Visszaállítottam a lap korábbi változatát (szerkesztőnév eltávolítva) szerkesztéséről [[User:$1|$1]] szerkesztésére",
        "rollback-success": "$1 szerkesztéseit visszaállítottam $2 utolsó változatára.",
+       "rollback-success-notify": "$1 szerkesztései visszaállítva;\nhelyreállítva $2 utolsó változata. [$3 Változtatások megtekintése]",
        "sessionfailure-title": "Munkamenethiba",
        "sessionfailure": "Úgy látszik, hogy probléma van a bejelentkezési munkameneteddel;\nez a művelet a munkamenet eltérítése miatti óvatosságból megszakadt.\nKérjük, hogy nyomd meg a „vissza” gombot, és töltsd le újra az oldalt, ahonnan jöttél, majd próbáld újra.",
        "changecontentmodel": "A lap tartalommodelljének megváltoztatása",
        "changecontentmodel-success-text": "A(z) [[:$1]] lap tartalommodellje sikeresen megváltoztatva.",
        "changecontentmodel-cannot-convert": "A(z) [[:$1]] lap nem alakítható át $2 típusúvá.",
        "changecontentmodel-nodirectediting": "A(z) $1 tartalommodell nem támogatja a közvetlen szerkesztést",
+       "changecontentmodel-emptymodels-title": "Nincs elérhető tartalommodell",
+       "changecontentmodel-emptymodels-text": "A(z) [[:$1]] lapon lévő tartalom nem alakítható át semmilyen típusúvá.",
        "log-name-contentmodel": "Tartalommodell-változások naplója",
        "log-description-contentmodel": "Egy lap tartalommodelljéhez kapcsolódó események",
+       "logentry-contentmodel-new": "$1 {{GENDER:$2|létrehozta}} a(z) $3 lapot nem alapértelmezett „$5” tartalommodellel.",
        "logentry-contentmodel-change": "$1 {{GENDER:$2|megváltoztatta}} a(z) $3 lap tartalommodeljét erről: „$4” erre: „$5”",
        "logentry-contentmodel-change-revertlink": "visszaállítás",
        "logentry-contentmodel-change-revert": "visszaállítás",
        "undeletehistorynoadmin": "Ezt a szócikket törölték. A törlés okát alább az összegzésben\nláthatod, az oldalt a törlés előtt szerkesztő felhasználók részleteivel együtt. Ezeknek\na törölt változatoknak a tényleges szövege csak az adminisztrátorok számára hozzáférhető.",
        "undelete-revision": "$1 $4, $5-kori törölt változata (szerző: $3).",
        "undeleterevision-missing": "Érvénytelen vagy hiányzó változat. Lehet, hogy rossz hivatkozásod van, ill. a\nváltozatot visszaállították vagy eltávolították az archívumból.",
+       "undeleterevision-duplicate-revid": "{{PLURAL:$1|Egy|$1}} változat nem állítható vissza, mert a <code>rev_id</code>-{{PLURAL:$1|je|jük}} már használatban van.",
        "undelete-nodiff": "Nem található korábbi változat.",
        "undeletebtn": "Helyreállítás",
        "undeletelink": "megtekintés/helyreállítás",
        "undeletedrevisions": "$1 változat helyreállítva",
        "undeletedrevisions-files": "{{PLURAL:$1|egy|$1}} változat és {{PLURAL:$2|egy|$2}} fájl visszaállítva",
        "undeletedfiles": "{{PLURAL:$1|egy|$1}} fájl visszaállítva",
-       "cannotundelete": "Lap visszaállítása sikertelen: $1",
+       "cannotundelete": "Egy vagy több visszaállítás sikertelen:\n$1",
        "undeletedpage": "'''$1 helyreállítva'''\n\nLásd a [[Special:Log/delete|törlési naplót]] a legutóbbi törlések és helyreállítások listájához.",
        "undelete-header": "A legutoljára törölt lapokat lásd a [[Special:Log/delete|törlési naplóban]].",
        "undelete-search-title": "Törölt lapok keresése",
        "sp-contributions-newbies-sub": "Új szerkesztők lapjai",
        "sp-contributions-newbies-title": "Új szerkesztők közreműködései",
        "sp-contributions-blocklog": "Blokkolási napló",
-       "sp-contributions-suppresslog": "elrejtett szerkesztők közreműködései",
-       "sp-contributions-deleted": "törölt szerkesztések",
+       "sp-contributions-suppresslog": "elrejtett {{GENDER:$1|felhasználók}} közreműködései",
+       "sp-contributions-deleted": "törölt {{GENDER:$1|szerkesztések}}",
        "sp-contributions-uploads": "feltöltések",
        "sp-contributions-logs": "naplók",
        "sp-contributions-talk": "vitalap",
        "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.",
        "tooltip-feed-rss": "A lap tartalma RSS hírcsatorna formájában",
        "tooltip-feed-atom": "A lap tartalma Atom hírcsatorna formájában",
        "tooltip-t-contributions": "A {{GENDER:$1|felhasználó}} közreműködéseinek listája",
-       "tooltip-t-emailuser": "Írj levelet ennek a felhasználónak!",
+       "tooltip-t-emailuser": "Írj e-mailt ennek a {{GENDER:$1|felhasználónak}}",
        "tooltip-t-info": "További információk erről a lapról",
        "tooltip-t-upload": "Képek vagy egyéb fájlok feltöltése",
        "tooltip-t-specialpages": "Az összes speciális lap listája",
        "confirmemail_body_set": "Valaki, valószínűleg te, ezt az email címet adta meg\n„$2” nevű {{SITENAME}}-fiókjához a következő IP-címről: $1.\n\nHa meg szeretnéd erősíteni, hogy a fiók valóban hozzád tartozik, így aktiválva a(z) {{SITENAME}} e-mailes funkcióit, nyisd meg az alábbi linket a böngésződben:\n\n$3\n\nHa a fiók *nem* hozzád tartozik, kövesd az alábbi linket a\nmegerősítés visszavonásához:\n\n$5\n\nEz a megerősítő e-mail $4-ig érvényes.",
        "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]",
        "scarytranscludetoolong": "[Az URL túl hosszú]",
        "deletedwhileediting": "'''Figyelmeztetés:''' A lapot a szerkesztés megkezdése után törölték!",
-       "confirmrecreate": "Miután elkezdted szerkeszteni, [[User:$1|$1]] ([[User talk:$1|vita]]) törölte ezt a lapot a következő indokkal:\n: ''$2''\nKérlek erősítsd meg, hogy tényleg újra akarod-e írni a lapot.",
-       "confirmrecreate-noreason": "[[User:$1|$1]] ([[User talk:$1|vita]]) törölte ezt a lapot, miután elkezdtél szerkeszteni. Erősítsd meg, hogy tényleg ismét létre szeretnéd hozni a lapot.",
+       "confirmrecreate": "Miután elkezdted szerkeszteni, [[User:$1|$1]] ([[User talk:$1|vita]]) törölte ezt a lapot a következő indokkal:\n: <em>$2</em>\nKérlek erősítsd meg, hogy tényleg újra létre akarod-e hozni a lapot.",
+       "confirmrecreate-noreason": "[[User:$1|$1]] ([[User talk:$1|vita]]) törölte ezt a lapot, miután elkezdted szerkeszteni. Erősítsd meg, hogy tényleg ismét létre szeretnéd hozni a lapot.",
        "recreate": "Újraírás",
        "confirm_purge_button": "OK",
        "confirm-purge-top": "Törlöd az oldal gyorsítótárban (cache) található változatát?",
        "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-license": "Licenc",
        "version-libraries-description": "Leírás",
        "version-libraries-authors": "Szerzők",
-       "redirect": "Átirányítás fájl, szerkesztő, oldal vagy oldalváltozat 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": "Á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), 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:",
-       "redirect-user": "Felhasználóazonosító",
+       "redirect-user": "Felhasználóazonosító",
        "redirect-page": "Lapazonosító",
-       "redirect-revision": "Oldal felülvizsgálata",
+       "redirect-revision": "Lapváltozat",
        "redirect-file": "Fájlnév",
+       "redirect-logid": "Naplóazonosító",
        "redirect-not-exists": "Érték nem található",
        "fileduplicatesearch": "Duplikátumok keresése",
        "fileduplicatesearch-summary": "Fájlok duplikátumainak keresése hash értékük alapján.",
        "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",
        "htmlform-title-not-exists": "$1 nem létezik.",
        "htmlform-user-not-exists": "<strong>$1</strong> nem létezik.",
        "htmlform-user-not-valid": "<strong>$1</strong> nem egy érvényes felhasználónév.",
-       "sqlite-has-fts": "$1 teljes szöveges keresés támogatással",
-       "sqlite-no-fts": "$1 teljes szöveges keresés támogatása nélkül",
        "logentry-delete-delete": "$1 törölte a következő lapot: $3",
        "logentry-delete-restore": "$1 helyreállította a következő lapot: $3",
        "logentry-delete-event": "$1 megváltoztatta {{PLURAL:$5|egy napló bejegyzés|$5 napló bejegyzés}} láthatóságát a(z) $3 című lapon: $4",
        "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 fa83ac9..f9f9e50 100644 (file)
@@ -43,7 +43,7 @@
        "tog-enotifminoredits": "Notificar me etiam de modificationes minor de paginas e files",
        "tog-enotifrevealaddr": "Revelar mi adresse de e-mail in messages de notification",
        "tog-shownumberswatching": "Monstrar le numero de usatores que observa le pagina",
-       "tog-oldsig": "Signatura existente:",
+       "tog-oldsig": "Tu signatura existente:",
        "tog-fancysig": "Tractar signatura como wikitexto (sin ligamine automatic)",
        "tog-uselivepreview": "Usar previsualisation dynamic",
        "tog-forceeditsummary": "Avisar me si io non entra un summario de modification",
@@ -60,7 +60,7 @@
        "tog-showhiddencats": "Monstrar categorias celate",
        "tog-norollbackdiff": "Non monstrar differentias post exequer un revocation",
        "tog-useeditwarning": "Advertir me quando io quita un pagina de modification sin publicar le cambiamentos",
-       "tog-prefershttps": "Sempre usar un connexion secur in session aperte",
+       "tog-prefershttps": "Sempre usar un connexion secur durante session aperte",
        "underline-always": "Sempre",
        "underline-never": "Nunquam",
        "underline-default": "Como definite per tu navigator o apparentia",
        "newwindow": "(se aperi in un nove fenestra)",
        "cancel": "Cancellar",
        "moredotdotdot": "Plus...",
-       "morenotlisted": "Iste lista non es complete.",
+       "morenotlisted": "Iste lista pote esser incomplete.",
        "mypage": "Pagina",
        "mytalk": "Discussion",
        "anontalk": "Discussion",
        "eauthentsent": "Un message de confirmation ha essite inviate al adresse de e-mail specificate.\nPro permitter que le systema invia altere messages a iste adresse, tu debe sequer le instructiones in iste message pro confirmar que le adresse es realmente tue.",
        "throttled-mailpassword": "Un message pro le reinitialisation del contrasigno ha jam essite inviate intra le ultime {{PLURAL:$1|hora|$1 horas}}.\nPro prevenir le abuso, solmente un message pro le reinitialisation del contrasigno essera inviate per {{PLURAL:$1|hora|$1 horas}}.",
        "mailerror": "Error de inviar e-mail: $1",
-       "acct_creation_throttle_hit": "Le visitatores de iste wiki usante tu adresse IP ha create {{PLURAL:$1|1 conto|$1 contos}} durante le ultime die, e isto es le maximo permittite in iste periodo de tempore.\nA causa de isto, le visitatores usante iste adresse IP non pote crear nove contos al momento.",
+       "acct_creation_throttle_hit": "Le visitatores de iste wiki usante tu adresse IP ha create {{PLURAL:$1|1 conto|$1 contos}} durante le ultime $2, e isto es le maximo permittite in iste periodo de tempore.\nA causa de isto, le visitatores usante iste adresse IP non pote crear nove contos al momento.",
        "emailauthenticated": "Tu adresse de e-mail ha essite confirmate le $2 a $3.",
        "emailnotauthenticated": "Tu non ha ancora confirmate tu adresse de e-mail.\nNulle e-mail essera inviate pro le sequente functiones.",
        "noemailprefs": "Es necessari specificar un adresse de e-mail in tu preferentias pro poter executar iste functiones.",
        "botpasswords-label-resetpassword": "Reinitialisar le contrasigno",
        "botpasswords-label-grants": "Concessiones applicabile:",
        "botpasswords-help-grants": "Cata concession da accesso al derectos de usator listate que un conto de usator jam ha. Vide le [[Special:ListGrants|tabula de concessiones]] pro plus information.",
-       "botpasswords-label-restrictions": "Restrictiones de uso:",
        "botpasswords-label-grants-column": "Concedite",
        "botpasswords-bad-appid": "Le nomine del robot \"$1\" non es valide.",
        "botpasswords-insert-failed": "Le addition del nomine de robot \"$1\" ha fallite. Esque illo ha jam essite addite?",
        "passwordreset-emailelement": "Nomine de usator: \n$1\n\nContrasigno temporari: \n$2",
        "passwordreset-emailsentemail": "Si iste adresse es associate a tu conto, alora un e-mail pro reinitialisar le contrasigno essera inviate.",
        "passwordreset-emailsentusername": "Si il ha un adresse de e-mail associate a iste conto, alora un e-mail pro reinitialisar le contrasigno essera inviate.",
-       "passwordreset-emailsent-capture2": "Le {{PLURAL:$1|message|messages}} de e-mail pro reinitialisation de contrasigno ha essite inviate. Le {{PLURAL:$1|nomine de usator e contrasigno|lista de nomines de usator e contrasignos}} appare hic infra.",
-       "passwordreset-emailerror-capture2": "Le invio de e-mail al {{GENDER:$2|usator}} ha fallite: $1 Le {{PLURAL:$3|nomine de usator e contrasigno|lista de nomines de usator e contrasignos}} appare hic infra.",
+       "passwordreset-emailsent-capture2": "Le {{PLURAL:$1|message|messages}} de e-mail pro reinitialisation de contrasigno ha essite inviate. Le {{PLURAL:$1|nomine de usator e contrasigno|lista de nomines de usator e contrasignos}} es monstrate hic.",
+       "passwordreset-emailerror-capture2": "Le invio de e-mail al {{GENDER:$2|usator}} ha fallite: $1 Le {{PLURAL:$3|nomine de usator e contrasigno|lista de nomines de usator e contrasignos}} es monstrate hic.",
        "passwordreset-nocaller": "Un appellator debe esser fornite",
        "passwordreset-nosuchcaller": "Appellator non existe: $1",
        "passwordreset-ignored": "Le reinitialisation del contrasigno non ha essite realisate. Es possibile que nulle fornitor ha essite configurate?",
        "upload-dialog-disabled": "Le incargamento de files con iste dialogo es disactivate in iste wiki.",
        "upload-dialog-title": "Incargar file",
        "upload-dialog-button-cancel": "Cancellar",
+       "upload-dialog-button-back": "Retornar",
        "upload-dialog-button-done": "Facite",
        "upload-dialog-button-save": "Salveguardar",
        "upload-dialog-button-upload": "Incargar",
        "htmlform-cloner-create": "Adder plus",
        "htmlform-cloner-delete": "Remover",
        "htmlform-cloner-required": "Al minus un valor es requirite.",
+       "htmlform-date-placeholder": "AAAA-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "AAAA-MM-DD HH:MM:SS",
+       "htmlform-date-invalid": "Le valor specificate non es recognoscite como data. Tenta usar le formato AAAA-MM-DD.",
+       "htmlform-time-invalid": "Le valor specificate non es recognoscite como hora. Tenta usar le formato HH:MM:SS.",
+       "htmlform-datetime-invalid": "Le valor specificate non es recognoscite como data e hora. Tenta usar le formato AAAA-MM-DD HH:MM:SS.",
        "htmlform-title-badnamespace": "[[:$1]] non es in le spatio de nomines \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" non es un titulo de pagina creabile",
        "htmlform-title-not-exists": "$1 non existe.",
        "htmlform-user-not-exists": "<strong>$1</strong> non existe.",
        "htmlform-user-not-valid": "<strong>$1</strong> non es un nomine de usator valide.",
-       "sqlite-has-fts": "$1 con supporto de recerca de texto integre",
-       "sqlite-no-fts": "$1 sin supporto de recerca de texto integre",
        "logentry-delete-delete": "$1 {{GENDER:$2|deleva}} le pagina $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|restaurava}} le pagina $3",
        "logentry-delete-event": "$1 {{GENDER:$2|cambiava}} le visibilitate de {{PLURAL:$5|un entrata|$5 entratas}} de registro in $3: $4",
        "feedback-external-bug-report-button": "Signalar un problema technic",
        "feedback-dialog-title": "Submitter commentario",
        "feedback-dialog-intro": "Usa le formulario sequente pro submitter tu commentario, le qual apparera in le pagina \"$1\" insimul a tu nomine de usator.",
-       "feedback-error-title": "Error",
        "feedback-error1": "Error: Resultato del API non recognoscite",
        "feedback-error2": "Error: Modification fallite",
        "feedback-error3": "Error: Nulle responsa del API",
index 2d8d4ef..9814fea 100644 (file)
@@ -76,7 +76,7 @@
        "tog-enotifminoredits": "Kirimkan saya surel juga pada perubahan kecil",
        "tog-enotifrevealaddr": "Tampilkan alamat surel saya pada surel notifikasi",
        "tog-shownumberswatching": "Tunjukkan jumlah pemantau",
-       "tog-oldsig": "Tanda tangan sekarang:",
+       "tog-oldsig": "Tanda tangan Anda yang sudah ada:",
        "tog-fancysig": "Perlakukan tanda tangan sebagai teks wiki (tanpa suatu pranala otomatis)",
        "tog-uselivepreview": "Gunakan pratayang langsung",
        "tog-forceeditsummary": "Ingatkan saya bila kotak ringkasan suntingan masih kosong",
        "newwindow": "(buka di jendela baru)",
        "cancel": "Batalkan",
        "moredotdotdot": "Lainnya...",
-       "morenotlisted": "Daftar ini belum lengkap.",
+       "morenotlisted": "Daftar ini mungkin tidak lengkap.",
        "mypage": "Halaman",
        "mytalk": "Pembicaraan",
        "anontalk": "Pembicaraan",
        "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?",
        "right-suppressrevision": "Menampilkan, menyembunyikan dan membatalkan penyembunyian revisi tertentu atas suatu halaman dari pengguna",
        "right-viewsuppressed": "Lihat revisi yang disembunyikan dari semua pengguna",
        "right-suppressionlog": "Melihat log privat",
-       "right-block": "Memblokir penyuntingan oleh pengguna lain",
+       "right-block": "Blokir pengguna lain dari penyuntingan",
        "right-blockemail": "Memblokir pengiriman surel oleh pengguna",
        "right-hideuser": "Memblokir nama pengguna dan menyembunyikannya dari publik",
        "right-ipblock-exempt": "Mengabaikan pemblokiran IP, pemblokiran otomatis, dan rentang pemblokiran",
        "action-undelete": "membatalkan penghapusan halaman ini",
        "action-suppressrevision": "meninjau dan mengembalikan revisi yang disembunyikan ini",
        "action-suppressionlog": "melihat log privat ini",
-       "action-block": "memblokir pengguna ini dari penyuntingan",
+       "action-block": "Blokir pengguna ini dari penyuntingan",
        "action-protect": "mengganti tingkat pelindungan halaman ini",
        "action-rollback": "mengembalikan dengan cepat suntingan-suntingan pengguna terakhir yang menyunting halaman tertentu",
        "action-import": "mengimpor halaman ini dari wiki lain",
        "htmlform-title-not-exists": "$1 tidak ada.",
        "htmlform-user-not-exists": "<strong>$1</strong> tidak ada.",
        "htmlform-user-not-valid": "<strong>$1</strong> bukan merupakan nama pengguna sah.",
-       "sqlite-has-fts": "$1 dengan dukungan pencarian teks lengkap",
-       "sqlite-no-fts": "$1 tanpa dukungan pencarian teks lengkap",
        "logentry-delete-delete": "$1 {{GENDER:$2|menghapus}} halaman $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|mengembalikan}} halaman $3",
        "logentry-delete-event": "$1 {{GENDER:$2|mengubah}} tampilan {{PLURAL:$5|$5 log peristiwa}} di $3: $4",
        "feedback-external-bug-report-button": "Kirim tugas teknis",
        "feedback-dialog-title": "Kirimkan saran dan tanggapan",
        "feedback-dialog-intro": "Anda bisa menggunakan formulir sederhana di bawah untuk mengirimkan saran dan masukan. Komentar Anda akan ditambahkan pada laman \"$1\" bersama nama pengguna Anda.",
-       "feedback-error-title": "Kesalahan",
        "feedback-error1": "Galat: Hasil tidak dikenal dari API",
        "feedback-error2": "Galat: Penyuntingan gagal",
        "feedback-error3": "Error: API tidak merespons",
index be841af..46a2e24 100644 (file)
        "htmlform-title-not-exists": "Awan ti $1.",
        "htmlform-user-not-exists": "Awan ti <strong>$1</strong>.",
        "htmlform-user-not-valid": "Saan nga umiso a nagan ti agar-aramat ti <strong>$1</strong>.",
-       "sqlite-has-fts": "Ti $1 nga addaan iti suporta ti panagbiruk ti napno a teksto",
-       "sqlite-no-fts": "Ti $1 nga awan iti suporta ti panagbiruk ti napno a teksto",
        "logentry-delete-delete": "{{GENDER:$2|Inikkat}} ni $1 ti panid ti $3",
        "logentry-delete-restore": "Ni $1 ket {{GENDER:$2|insublina}} ti panid ti $3",
        "logentry-delete-event": "Ni $1 ket {{GENDER:$2|binaliwanna}} ti panagkita {{PLURAL:$5|iti listaan ti pasamak |dagiti $5 a listaan ti pasamak }} iti $3: $4",
index fd1d4c4..0e3003a 100644 (file)
        "listgrouprights-namespaceprotection-restrictedto": "Réttindi sem leyfa notanda að breyta",
        "listgrants-rights": "Réttindi",
        "trackingcategories-name": "Heiti skilaboða",
+       "restricted-displaytitle-ignored": "Síður með hunsaða sýnda titla",
        "trackingcategories-nodesc": "Enginn lýsing tiltæk.",
        "trackingcategories-disabled": "Flokkurinn er óvirkur",
        "mailnologin": "Ekkert netfang til að senda á",
        "htmlform-title-not-exists": "$1 er ekki til",
        "htmlform-user-not-exists": "<strong>$1</strong> er ekki til.",
        "htmlform-user-not-valid": "<strong>$1</strong> er ekki gilt notandanafn.",
-       "sqlite-has-fts": "$1 með fullum texta leitar stuðningi",
-       "sqlite-no-fts": "$1 án fullum texta leitar stuðningi",
        "logentry-delete-delete": "$1 {{GENDER:$2|eyddi}} síðunni $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|endurvakti}} $3",
        "logentry-delete-event": "$1 {{GENDER:$2|breytti}} sýnileika {{PLURAL:$5|færslu|$5 færslna}} á $3: $4",
        "feedback-external-bug-report-button": "Senda inn tæknilegar lýsingar/verkefni",
        "feedback-dialog-title": "Senda umsögn",
        "feedback-dialog-intro": "Þú getur þú notað einfalt eyðublað hér fyrir neðan til að senda inn umsögn. Athugasemdinni þinni verður bætt við síðuna \"$1\" ásamt notandanafni þínu.",
-       "feedback-error-title": "Villa",
        "feedback-error1": "Villa: Óþekkt útkoma frá API",
        "feedback-error2": "Villa: Breytingin mistókst",
        "feedback-error3": "Villa: Ekkert svar frá API",
index a752071..d22d1a7 100644 (file)
                        "Matteocng",
                        "Einreiher",
                        "Anto",
-                       "Saracrovetto"
+                       "Saracrovetto",
+                       "Tosky",
+                       "Selven"
                ]
        },
        "tog-underline": "Sottolinea i collegamenti:",
        "talk": "Discussione",
        "views": "Visite",
        "toolbox": "Strumenti",
+       "tool-link-userrights": "Modifica gruppi {{GENDER:$1|utente}}",
+       "tool-link-emailuser": "Invia una email a questo {{GENDER:$1|utente}}",
        "userpage": "Visualizza la pagina utente",
        "projectpage": "Visualizza la pagina di servizio",
        "imagepage": "Visualizza la pagina del file",
        "createacct-yourpasswordagain-ph": "Inserisci nuovamente la password",
        "userlogin-remembermypassword": "Mantienimi collegato",
        "userlogin-signwithsecure": "Usa una connessione sicura",
-       "cannotlogin-text": "Accesso non è possibile.",
+       "cannotlogin-title": "Non è possibile effettuare l'accesso",
+       "cannotlogin-text": "L'accesso non è possibile.",
        "cannotloginnow-title": "Impossibile accedere ora",
        "cannotloginnow-text": "L'accesso non è possibile quando si sta usando $1.",
        "cannotcreateaccount-title": "Impossibile creare l'utenza",
        "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": "Reimposta password",
        "passwordreset-text-one": "Compila questo modulo per reimpostare la tua password.",
        "passwordreset-text-many": "{{PLURAL:$1|Compila uno dei campi per ricevere una password temporanea tramite email.}}",
-       "passwordreset-disabled": "La reimpostazione delle password è stata disabilitata su questa wiki",
+       "passwordreset-disabled": "La reimpostazione delle password è stata disabilitata per questo wiki",
        "passwordreset-emaildisabled": "Le funzionalità di posta elettronica sono state disabilitate su questa wiki.",
        "passwordreset-username": "Nome utente:",
        "passwordreset-domain": "Dominio:",
        "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?",
        "searchprofile-advanced-tooltip": "Cerca nei namespace personalizzati",
        "search-result-size": "$1 ({{PLURAL:$2|una parola|$2 parole}})",
        "search-result-category-size": "{{PLURAL:$1|1 utente|$1 utenti}} ({{PLURAL:$2|1 sottocategoria|$2 sottocategorie}}, {{PLURAL:$3|1 file|$3 files}})",
-       "search-redirect": "(redirect $1)",
+       "search-redirect": "(reindirizzamento da $1)",
        "search-section": "(sezione $1)",
        "search-category": "(categoria $1)",
        "search-file-match": "(corrispondenza nel contenuto del file)",
        "upload-http-error": "Si è verificato un errore HTTP: $1",
        "upload-copy-upload-invalid-domain": "Non è consentito il caricamento di copie da questo dominio.",
        "upload-foreign-cant-upload": "Questo wiki non è configurato per caricare i file nel repository di file esterno richiesto.",
+       "upload-foreign-cant-load-config": "Impossibile caricare il file di configurazione per caricarlo nel repositorio esterno.",
        "upload-dialog-disabled": "Il caricamento di file tramite questa finestra di dialogo è disabilitato in questo wiki.",
        "upload-dialog-title": "Carica file",
        "upload-dialog-button-cancel": "Annulla",
+       "upload-dialog-button-back": "Indietro",
        "upload-dialog-button-done": "Fatto",
        "upload-dialog-button-save": "Salva",
        "upload-dialog-button-upload": "Carica",
        "uploadstash-errclear": "La pulizia dei file non è riuscita.",
        "uploadstash-refresh": "Aggiorna l'elenco dei file",
        "uploadstash-thumbnail": "vedi miniatura",
+       "uploadstash-exception": "Impossibile memorizzare il caricamento in stash ($1): \"$2\".",
        "invalid-chunk-offset": "Offset della parte non valido.",
        "img-auth-accessdenied": "Accesso negato",
        "img-auth-nopathinfo": "PATH_INFO mancante.\nIl server non è impostato per passare questa informazione.\nPotrebbe essere basato su CGI e non può supportare img_auth.\nVedi https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization",
        "listfiles_date": "Data",
        "listfiles_name": "Nome",
        "listfiles_user": "Utente",
-       "listfiles_size": "Dimensione in byte",
+       "listfiles_size": "Dimensione",
        "listfiles_description": "Descrizione",
        "listfiles_count": "Versioni",
        "listfiles-show-all": "Includi le vecchie versioni delle immagini",
        "apisandbox-results-fixtoken-fail": "Impossibile recuperare il token \"$1\".",
        "apisandbox-alert-page": "I campi su questa pagina non sono validi.",
        "apisandbox-alert-field": "Il valore di questo campo non è valido.",
+       "apisandbox-continue": "Continua",
+       "apisandbox-continue-clear": "Pulisci",
        "booksources": "Fonti librarie",
        "booksources-search-legend": "Ricerca di fonti librarie",
        "booksources-isbn": "Codice ISBN:",
        "htmlform-cloner-create": "Aggiungi altro",
        "htmlform-cloner-delete": "Rimuovi",
        "htmlform-cloner-required": "È obbligatorio almeno un valore.",
+       "htmlform-date-placeholder": "AAAA-MM-GG",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "AAAA-MM-GG HH:MM:SS",
+       "htmlform-date-invalid": "Il valore specificato non è riconosciuto come data. Prova a utilizzare il formato AAAA-MM-GG.",
+       "htmlform-time-invalid": "Il valore specificato non è riconosciuto come orario. Prova a utilizzare il formato HH:MM:SS.",
+       "htmlform-datetime-invalid": "Il valore specificato non è riconosciuto come data e ora. Prova a utilizzare il formato AAAA-MM-GG HH:MM:SS.",
+       "htmlform-date-toolow": "Il valore specificato è precedente alla prima data consentita del $1.",
+       "htmlform-date-toohigh": "Il valore specificato è successivo all'ultima data consentita del $1.",
+       "htmlform-time-toolow": "Il valore specificato è precedente al primo orario consentito del $1.",
+       "htmlform-time-toohigh": "Il valore specificato è successivo all'ultimo orario consentito di $1.",
+       "htmlform-datetime-toolow": "Il valore specificato è precedente alla prima data e ora consentita del $1.",
+       "htmlform-datetime-toohigh": "Il valore specificato è successivo all'ultima data e ora consentita del $1.",
        "htmlform-title-badnamespace": "[[:$1]] non si trova nel namespace \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" è il titolo di una pagina non creabile",
        "htmlform-title-not-exists": "$1 non esiste.",
        "htmlform-user-not-exists": "<strong>$1</strong> non esiste.",
        "htmlform-user-not-valid": "<strong>$1</strong> non è un nome utente valido.",
-       "sqlite-has-fts": "$1 con la possibilità di ricerca completa nel testo",
-       "sqlite-no-fts": "$1 senza la possibilità di ricerca completa nel testo",
        "logentry-delete-delete": "$1 {{GENDER:$2|ha cancellato}} la pagina $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|ha ripristinato}} la pagina \"$3\"",
        "logentry-delete-event": "$1 {{GENDER:$2|ha modificato}} la visibilità di {{PLURAL:$5|un'azione del registro|$5 azioni del registro}} di \"$3\": $4",
        "feedback-external-bug-report-button": "Documenta un problema tecnico",
        "feedback-dialog-title": "Invia un feedback",
        "feedback-dialog-intro": "Usa il modulo sottostante per inviare il tuo feedback. Il tuo commento apparirà nella pagina \"$1\", assieme al tuo nome utente.",
-       "feedback-error-title": "Errore",
        "feedback-error1": "Errore: Dalla API è arrivato un risultato non riconosciuto",
        "feedback-error2": "Errore: Non è stato possibile eseguire la modifica",
        "feedback-error3": "Errore: Nessuna risposta dalla API",
        "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.",
+       "authenticationdatachange-ignored": "Il cambiamento dei dati di autenticazione non è stato gestito. Forse non è stato configurato nessun provider?",
+       "userjsispublic": "Ricorda: le sottopagine JavaScript non devono contenere dati riservati poichè sono visualizzabili da altri utenti.",
+       "usercssispublic": "Ricorda: le sottopagine CSS non devono contenere dati riservati poichè sono visualizzabili da altri utenti.",
+       "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 5f451b9..72b6873 100644 (file)
        "talk": "議論",
        "views": "表示",
        "toolbox": "ツール",
+       "tool-link-emailuser": "この{{GENDER:$1|利用者}}にメールを送信",
        "userpage": "利用者ページを表示",
        "projectpage": "プロジェクトのページを表示",
        "imagepage": "ファイルのページを表示",
        "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」の追加に失敗しました。既に追加されていないか確認してください。",
        "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 が $3 の{{PLURAL:$5|記録項目|記録項目$5件}}の閲覧レベルを{{GENDER:$2|変更しました}}: $4",
index 0fa8f88..f4e4631 100644 (file)
        "htmlform-no": "Ora",
        "htmlform-yes": "Iya",
        "htmlform-chosen-placeholder": "Pilih pilihan",
-       "sqlite-has-fts": "$1 mawa sengkuyungan golèkan tèks jangkep",
-       "sqlite-no-fts": "$1 tanpa sengkuyungan golèkan tèks jangkep",
        "logentry-delete-delete": "$1 {{GENDER:$2|mbusak}} kaca $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|mbalèkaké}} kaca $3",
        "logentry-delete-event": "$1 {{GENDER:$2|ngganti}} parupané {{PLURAL:$5|sak prastawa log|$5 prastawa log}} ana ing $3: $4",
        "feedback-bugornote": "Yèn Sampéyan siap njelasaké masalah tèhnis kanthi rinci mangga [$1 laporaké bug].\nUtawa, Sampéyan bisa nganggo pormulir gampang ngisor. Tanggepan Sampéyan bakal ditambahaké nèng kaca \"[$3 $2]\", bebarengan karo jeneng panganggo Sampéyan lan pramban sing Sampéyan anggo.",
        "feedback-cancel": "Batal",
        "feedback-close": "Rampung",
-       "feedback-error-title": "Cacad",
        "feedback-error1": "Kasalahan: Asil ora dikenal saka API",
        "feedback-error2": "Cacad: Gagal mbesut",
        "feedback-error3": "Kasalahan: Ora ana tanggepan saka API",
        "feedback-submit": "Kirim",
        "feedback-thanks": "Nuwun! Lebon saran Sampéyan wis dipasang nèng kacané \"[$2 $1]\".",
        "searchsuggest-search": "Golèk",
-       "searchsuggest-containing": "ngisi...",
+       "searchsuggest-containing": "ngemu...",
        "api-error-badaccess-groups": "Sampéyan ora dililakaké ngunggah berkas nèng wiki iki.",
        "api-error-badtoken": "Kasalahan njero: Token èlèk.",
        "api-error-copyuploaddisabled": "Ngunggah saka URL dipatèni nèng sasana iki.",
index e9e2f92..a85c60b 100644 (file)
        "password-change-forbidden": "თქვენ არ შეგიძლიათ ამ ვიკიში პაროლის შეცვლა.",
        "externaldberror": "საგარეო მონაცემთა ბაზაში აუტენტიფიკაციის შეცდომაა, ან თქვენ არ გაქვთ საკმარისი უფლებები საგარეო ანგარიშში ცვლილებების შესატანად.",
        "login": "შესვლა",
-       "login-security": "á\83\93á\83\90á\83\90á\83\93á\83\90á\83¡á\83¢á\83£á\83 á\83\94á\83\97 á\83\97á\83¥á\83\95á\83\94á\83\9cá\83\98 á\83\90á\83\95á\83\97á\83\94á\83\9cá\83¢á\83£á\83 á\83\9dá\83\91ა",
+       "login-security": "á\83\93á\83\90á\83\90á\83\93á\83\90á\83¡á\83¢á\83£á\83 á\83\94á\83\97 á\83\98á\83\93á\83\94á\83\9cá\83¢á\83\98á\83¤á\83\98á\83\99á\83\90á\83ªá\83\98ა",
        "nav-login-createaccount": "შესვლა / რეგისტრაცია",
        "userlogin": "შესვლა/ანგარიშის შექმნა",
        "userloginnocreate": "შესვლა",
        "userlogin-resetpassword-link": "დაგავიწყდათ პაროლი?",
        "userlogin-helplink2": "დახმარება:შესვლა",
        "userlogin-loggedin": "თქვენ უკვე შეხვედით როგორც {{GENDER:$1|$1}}.\nგამოიყენეთ ფორმა ქვემოთ, რათა შეხვიდეთ სხვა ანგარიშიდან.",
-       "userlogin-reauth": "á\83\97á\83¥á\83\95á\83\94á\83\9c á\83\99á\83\95á\83\9aá\83\90á\83\95 á\83£á\83\9cá\83\93á\83\90 á\83¨á\83\94á\83®á\83\95á\83\98á\83\93á\83\94á\83\97 á\83¡á\83\98á\83¡á\83¢á\83\94á\83\9bá\83\90á\83¨á\83\98 á\83 á\83\90á\83\97á\83\90 á\83¨á\83\94á\83\9bá\83\9dá\83¬á\83\9bá\83\93á\83\94á\83¡ á\83 á\83\9dá\83\9b á\83®á\83\90á\83 á\83\97 $1",
+       "userlogin-reauth": "á\83\97á\83¥á\83\95á\83\94á\83\9c á\83£á\83\9cá\83\93á\83\90 á\83\92á\83\90á\83\98á\83\90á\83 á\83\9dá\83\97 á\83\90á\83\95á\83¢á\83\9dá\83 á\83\98á\83\96á\83\90á\83ªá\83\98á\83\90, á\83 á\83\90á\83\97á\83\90 á\83\99á\83\98á\83\93á\83\94á\83\95 á\83\94á\83 á\83\97á\83®á\83\94á\83\9a á\83\9bá\83\9dá\83®á\83\93á\83\94á\83¡ á\83\97á\83¥á\83\95á\83\94á\83\9cá\83\98 á\83\98á\83\93á\83\94á\83\9cá\83¢á\83\98á\83¤á\83\98á\83ªá\83\98á\83 á\83\94á\83\91á\83\90 á\83\90á\83\9cá\83\92á\83\90á\83 á\83\98á\83¨á\83\97á\83\90á\83\9c â\80\9e{{GENDER:$1|$1}}â\80\9c.",
        "userlogin-createanother": "სხვა ანგარიშის შექმნა",
        "createacct-emailrequired": "ელ. ფოსტის მისამართი",
        "createacct-emailoptional": "ელ. ფოსტის მისამართი (არასავალდებულო)",
        "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\" დამატება შეუძლებელია. უკვე დამატებულია?",
        "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 {{GENDER:$2|წაშალა}} გვერდი: „$3“",
        "logentry-delete-restore": "მომხმარებელმა $1 {{GENDER:$2|აღადგინა}} გვერდი $3",
        "logentry-delete-event": "მომხმარებელმა $1 {{GENDER:$2|შეცვალა}} {{PLURAL:$5|ჟურნალის ჩანაწერის|$5 ჟურნალის ჩანაწერების}} ხილვადობა $3-ზე: $4",
index 43dc0de..1580a47 100644 (file)
        "hidden-categories": "{{PLURAL:$1|Kategoriya wedariyaiye|Kategoriyê wedariyaey}}",
        "hidden-category-category": "Kategoriyê wedariyaey",
        "category-subcat-count": "{{PLURAL:$2|Na kategoriye de ana kategoriya bınêne esta.|Na kategoriye de $2 ra pêro pia, {{PLURAL:$1|ana kategoriya bınêne esta|ani $1 kategoriyê bınêni estê.}}, be $2 ra pia.}}",
-       "category-subcat-count-limited": "Na kategoriye de {{PLURAL:$1|ana kategoriya bınêne esta|ani $1 kategoriyê bınêni estê}}.",
+       "category-subcat-count-limited": "Na kategoriya de {{PLURAL:$1|ana kategoriya bınêne esta|ani $1 kategoriyê bınêni estê}}.",
        "category-article-count": "{{PLURAL:$2|Na kategoriye de teyna ana pele esta.|Na kategoriye de $2 ra pêro pia, {{PLURAL:$1|ana pele esta|ani $1 peli estê.}}, be $2 ra pêro pia}}",
        "category-article-count-limited": "{{PLURAL:$1|Ana pele kategoriya peyêne dera|Ani $1 peli kategoriya peyêne derê}}.",
        "category-file-count": "{{PLURAL:$2|Na kategoriye de teyna ana dosya esta.|Na kategoriye de $2 ra pêro pia, {{PLURAL:$1|ana dosya esta|ani $1 dosyey estê.}}}}",
index 08b84e8..e6e7170 100644 (file)
@@ -15,7 +15,8 @@
                        "Batyrbek.kz",
                        "Matma Rex",
                        "Nemo bis",
-                       "Mormegil"
+                       "Mormegil",
+                       "Mirgulkali"
                ]
        },
        "tog-underline": "Сілтеменің астын сызу:",
        "talk": "Талқылау",
        "views": "Көрініс",
        "toolbox": "Құралдар",
+       "tool-link-userrights": "{{GENDER:$1|Қатысушы}} топтарын өзгерту",
+       "tool-link-emailuser": "Бұл {{GENDER:$1|қатысушыға}} хат жіберу",
        "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": "Бұл бетке тікелей ену үшін жүйеге кіруіңіз керек.",
        "revdelete-text-file": "Жойылған файл нұсқалары әлі де бет тарихында көрінетін болады, бірақ олардың мағлұмат бөлшектері жалпыға қатынаулы болмайды.",
        "logdelete-text": "Жойылған журнал оқиғалары әлі де бет тарихында көрінетін болады, бірақ олардың мағлұмат бөлшектері жалпыға қатынаулы болмайды.",
        "revdelete-text-others": "Қосымша тиымдар қойылғанша басқа әкімшілер, жасырын мағлұматқа қатынай және оны қалпына келтіре алады.",
-       "revdelete-confirm": "Сіз осыны істеу ниетіңізде салдары қандай болатынын түсінінің және сіз  [[{{MediaWiki:Policy-url}}|ережеге]] сәйкес бұны істегеніңізді құптаңыз.",
+       "revdelete-confirm": "Сіз осыны істеу ниетіңіздің салдары қандай болатынын түсінініңіз және сіз [[{{MediaWiki:Policy-url}}|ережеге]] сәйкес бұны істегеніңізді құптаңыз.",
        "revdelete-suppress-text": "Жасыру <strong>тек</strong> төмендегідей жағдайларда қолданылады:\n* потенциялды ғайбат ақпарат\n* Орынсыз жеке ақпарат\n*: <em>мекенжай және телефон номерлері, жеке сәйкестендіру нөмерлері, тағы сол сияқтылар.</em>",
        "revdelete-legend": "Көрініс тиымдарын қою:",
        "revdelete-hide-text": "Түзету мәтінін жасыр",
        "trackingcategories-msg": "Санатты қадағалау",
        "trackingcategories-name": "Хабарлама атауы",
        "trackingcategories-desc": "Санаттарды қосу шарттары",
+       "restricted-displaytitle-ignored": "Еленбеген көретілетін атауларымен беттер",
        "noindex-category-desc": "Бұл бет роботтар арқылы индекстелмеген, себебі онда <code><nowiki>__NOINDEX__</nowiki></code> деген сиқырлы сөзі бар және бұл жалауша рұқсат етілген есім кеңістігінде орналасқан.",
        "index-category-desc": "Бұл бетте <code><nowiki>__INDEX__</nowiki></code> деген код бар (және бұл жалауша рұқсат етілген есім кеңістігінде орналасқан), демек мұнда қалыпты жағдайда роботтар арқылы индекстелмейді.",
        "post-expand-template-inclusion-category-desc": "Беттің мөлшері барлық үлгілерді кеңейткен соң мынадан <code>$wgMaxArticleSize</code> үлкенірек болады, сондықтан біраз үлгілер кеңейтілмейді.",
        "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 $3 бетіндегі {{PLURAL:$5|журнал оқиғасы|$5 журнал оқиғасы}} көрінісін {{GENDER:$2|өзгертті}}: $4",
        "feedback-external-bug-report-button": "Техникалық тапсырманы жіберу",
        "feedback-dialog-title": "Пікірді жіберу",
        "feedback-dialog-intro": "Сіз пікіріңізді жіберу үшін төмендегі пішінді пайдалана аласыз. Сіздің пікіріңіз «$1» бетіне сіздің қатысушы есіміңізбен қосылады.",
-       "feedback-error-title": "Қате",
        "feedback-error1": "Қате: API-дан танылмаған нәтиже",
        "feedback-error2": "Қате: Өңдеме сәтсіздікке ұшырады",
        "feedback-error3": "Қате: API-дан жауап жоқ",
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 50d114f..1dc2caf 100644 (file)
@@ -90,7 +90,7 @@
        "tog-enotifminoredits": "문서나 파일의 사소한 편집도 이메일로 알림",
        "tog-enotifrevealaddr": "알림 메일에 내 이메일 주소를 밝히기",
        "tog-shownumberswatching": "주시하는 사용자 수 보이기",
-       "tog-oldsig": "현재 서명:",
+       "tog-oldsig": "당신의 기존 서명:",
        "tog-fancysig": "서명을 위키텍스트로 취급 (자동으로 링크를 걸지 않음)",
        "tog-uselivepreview": "실시간 미리 보기 사용하기",
        "tog-forceeditsummary": "편집 요약을 쓰지 않았을 때 내게 물어보기",
        "talk": "토론",
        "views": "보기",
        "toolbox": "도구",
+       "tool-link-userrights": "{{GENDER:$1|사용자}} 그룹 변경",
+       "tool-link-emailuser": "이 {{GENDER:$1|사용자}}에게 이메일 보내기",
        "userpage": "사용자 문서 보기",
        "projectpage": "프로젝트 문서 보기",
        "imagepage": "파일 문서 보기",
        "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\" 봇 이름을 추가하는데 실패했습니다. 이미 등록되지 않았는지 확인하기 바랍니다.",
        "file-thumbnail-no": "파일 이름이 <strong>$1</strong>으로 시작합니다.\n이 파일은 그림의 크기를 줄인 (섬네일) 파일인 것 같습니다.\n더 해상도가 좋은 파일이 있다면 그 파일을 올리거나 아니면 올리려는 파일 이름을 바꾸세요.",
        "fileexists-forbidden": "같은 이름의 파일이 이미 있고, 덮어쓸 수 없습니다.\n그래도 파일을 올리시려면, 뒤로 돌아가서 다른 이름으로 시도해 주시기 바랍니다.\n[[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "같은 이름의 파일이 이미 위키미디어 공용에 있습니다.\n그래도 파일을 올리려면 뒤로 돌아가서 다른 이름으로 시도해 주시기 바랍니다.\n[[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "업로드한 항목은 <strong>[[:$1]]</strong>의 현재 판과 완전히 중복입니다.",
+       "fileexists-duplicate-version": "업로드한 항목은 <strong>[[:$1]]</strong>의 {{PLURAL:$2|과거의 판}}과 완전히 중복입니다.",
        "file-exists-duplicate": "현재 올리고 있는 {{PLURAL:$1|파일}}이 아래 파일과 중복됩니다:",
        "file-deleted-duplicate": "이 파일과 같은 파일 ([[:$1]])이 이전에 삭제된 적이 있습니다. 파일을 다시 올리기 전에 문서의 삭제 기록을 확인해 주시기 바랍니다.",
        "file-deleted-duplicate-notitle": "이 파일과 같은 파일이 이전에 삭제된 적이 있으며, 제목은 숨겨져 있습니다.\n다시 올리기 전에 상확은 검토하기 위해 숨겨진 파일 데이터를 볼 수 있는 누군가에게 물어봐야 합니다.",
        "upload-dialog-disabled": "이 대화창을 이용한 파일 올리기는 이 위키에서 비활성화되어 있습니다.",
        "upload-dialog-title": "파일 올리기",
        "upload-dialog-button-cancel": "취소",
+       "upload-dialog-button-back": "뒤로",
        "upload-dialog-button-done": "완료",
        "upload-dialog-button-save": "저장",
        "upload-dialog-button-upload": "올리기",
        "apisandbox-results-fixtoken-fail": "\"$1\" 토크을 가져오는데 실패했습니다.",
        "apisandbox-alert-page": "이 문서에 있는 필드가 유효하지 않습니다.",
        "apisandbox-alert-field": "이 필드의 값이 유효하지 않습니다.",
+       "apisandbox-continue": "계속",
+       "apisandbox-continue-clear": "지우기",
        "booksources": "책 찾기",
        "booksources-search-legend": "책 원본 검색",
        "booksources-isbn": "ISBN:",
        "htmlform-cloner-create": "더 추가",
        "htmlform-cloner-delete": "제거",
        "htmlform-cloner-required": "적어도 하나의 값이 필요합니다.",
+       "htmlform-date-placeholder": "YYYY-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "YYYY-MM-DD HH:MM:SS",
        "htmlform-title-badnamespace": "[[:$1]] 문서는 \"{{ns:$2}}\" 이름공간에 없습니다.",
        "htmlform-title-not-creatable": "\"$1\"은 만들 수 없는 문서 제목입니다.",
        "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님이 $3의 {{PLURAL:$1|기록 $5개}}에 대해 보이기 설정을 {{GENDER:$2|바꾸었습니다}}: $4",
        "feedback-external-bug-report-button": "기술적 보고 제기",
        "feedback-dialog-title": "피드백 제출",
        "feedback-dialog-intro": "당신의 피드백을 제출하기 위해 아래 쉬운 양식을 사용할 수 있습니다. 당신의 의견은 당신의 사용자 이름과 함께, \"$1\" 문서에 추가됩니다.",
-       "feedback-error-title": "오류",
        "feedback-error1": "오류: API 실행 결과를 인식할 수 없음",
        "feedback-error2": "오류: 편집 실패",
        "feedback-error3": "오류: API가 응답하지 않음",
        "unlinkaccounts-success": "계정의 연결이 해제되었습니다.",
        "authenticationdatachange-ignored": "인증 데이터 변경을 처리하지 못했습니다. 제공자를 설정하지 않으셨습니까?",
        "userjsispublic": "주목해 주십시오: 자바스크립트의 하위 문서들은 다른 사용자들이 볼 수 있기 때문에 기밀 데이터를 포함해서는 안 됩니다.",
-       "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 e04e424..76f1cce 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}}.",
        "searchprofile-advanced-tooltip": "Sich an den Nummraim déi an de perséinlichen Astellungen festgeluecht sinn",
        "search-result-size": "$1 ({{PLURAL:$2|1 Wuert|$2 Wierder}})",
        "search-result-category-size": "{{PLURAL:$1|1 Säit|$1 Säiten}} ({{PLURAL:$2|1 Ënnerkategorie|$2 Ënnerkategorien}}, {{PLURAL:$3|1 Fichier|$3 Fichieren}})",
-       "search-redirect": "(Viruleedung $1)",
+       "search-redirect": "(Viruleedung vu(n) $1)",
        "search-section": "(Abschnitt $1)",
        "search-category": "(Kategorie $1)",
        "search-file-match": "(Inhalt vum Fichier passt)",
        "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",
        "upload-options": "Optioune vum Eroplueden",
        "watchthisupload": "Dëse Fichier iwwerwaachen",
        "filewasdeleted": "E Fichier mat dësem Numm gouf schonn eemol eropgelueden an duerno nees geläscht. Kuckt w.e.g op $1 no, ier Dir dee Fichier nach eng Kéier eropluet.",
+       "filename-thumb-name": "Dësen Numm gesäit aus wéi den Numm vun engem Miniaturbild. Luet w.e.g. keng Miniatur-Biller zréck op déi selwecht Wiki. Sollt et sech ëm een anert Bild handelen da sicht w.e.g. e Fichiersnumm dee méi verständlech ass an den net sou ufänkt wéi e Miniaturbild.",
        "filename-bad-prefix": "Den Numm vum Fichier fänkt mat '''„$1“''' un. Dësen Numm krut en automatesch vun der Kamera a seet näischt iwwer dat aus, wat drop ass. Gitt dem Fichier w.e.g. en Numm, deen den Inhalt besser beschreift, an deen net verwiesselt ka ginn.",
        "upload-proto-error": "Falsche Protokoll",
        "upload-proto-error-text": "D'URL muss mat <code>http://</code> oder <code>ftp://</code> ufänken.",
        "upload-dialog-disabled": "D'Eropluede vu Fichieren mat dësem Dialog ass op dëser Wiki desaktivéiert.",
        "upload-dialog-title": "Fichier eroplueden",
        "upload-dialog-button-cancel": "Ofbriechen",
+       "upload-dialog-button-back": "Zréck",
        "upload-dialog-button-done": "Fäerdeg",
        "upload-dialog-button-save": "Späicheren",
        "upload-dialog-button-upload": "Eroplueden",
        "apisandbox-request-time": "Dauer vun der Ufro: {{PLURAL:$1|$1 ms}}",
        "apisandbox-alert-page": "Felder op dëser Säit sinn net valabel.",
        "apisandbox-alert-field": "De wäert vun dësem Feld ass net valabel.",
+       "apisandbox-continue": "Virufueren",
+       "apisandbox-continue-clear": "Eidel maachen",
        "booksources": "Bicherreferenzen",
        "booksources-search-legend": "No Bicherreferenze sichen",
        "booksources-search": "Sichen",
        "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",
        "htmlform-cloner-create": "Méi derbäisetzen",
        "htmlform-cloner-delete": "Ewechhuelen",
        "htmlform-cloner-required": "Mindestens ee Wäert ass obligatoresch.",
+       "htmlform-date-placeholder": "JJJJ-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "JJJJ-MM-DD HH:MM:SS",
+       "htmlform-date-invalid": "De Wäert deen Dir aginn hutt gouf net als Datum erkannt. Probéiert de Format JJJJ-MM-DD ze benotzen.",
+       "htmlform-time-invalid": "De Wäert deen Dir aginn hutt gouf net als Zäit erkannt. Probéiert de Format HH:MM:SS ze benotzen.",
+       "htmlform-datetime-invalid": "De Wäert deen Dir aginn hutt gouf net als Datum an Zäit erkannt. Probéiert de Format JJJJ-MM-DD HH:MM:SS ze benotzen.",
+       "htmlform-date-toolow": "De Wäert deen Dir aginn hutt ass virun deem éischten erlaabten Datum vum $1.",
+       "htmlform-date-toohigh": "De wäert deen Dir aginn hutt ass nom leschten erlaabten Datum vum $1.",
+       "htmlform-time-toolow": "De Wäert deen Dir aginn hutt ass virun der éischter erlaabter Zäit vu(n) $1.",
+       "htmlform-time-toohigh": "De Wäert deen Dir aginn hutt ass no der leschter erlaabter Zäit vu(n) $1.",
+       "htmlform-datetime-toohigh": "De Wäert deen Dir uginn hutt ass nom leschten erlaabten Datum an der Zäit vu(n) $1.",
        "htmlform-title-badnamespace": "[[:$1]] ass net am Nummraum \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" ass kee Säitentitel deen ugeluecht ka ginn",
        "htmlform-title-not-exists": "$1 gëtt et net.",
        "htmlform-user-not-exists": "<strong>$1</strong> gëtt et net.",
        "htmlform-user-not-valid": "<strong>$1</strong> ass kee valabele Benotzernumm.",
-       "sqlite-has-fts": "$1 ënnerstëtzt d'Volltextsich",
-       "sqlite-no-fts": "$1 ënnerstëtzt d'Volltextsich net",
        "logentry-delete-delete": "$1 {{GENDER:$2|huet}} d'Säit $3 geläscht",
        "logentry-delete-restore": "$1 {{GENDER:$2|huet}} d'Säit $3 restauréiert",
        "logentry-delete-event": "$1 huet d'Visibilitéit vun {{PLURAL:$5|engem Evenement|$5 Evenementer}} am Logbuch op $3:$4 {{GENDER:$2|geännert}}",
        "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",
        "feedback-close": "Fäerdeg",
        "feedback-external-bug-report-button": "Eng technesch Aufgab notifizéieren",
        "feedback-dialog-title": "Feedback schécken",
-       "feedback-error-title": "Feeler",
        "feedback-error1": "Feeler: Resultat vum API gouf net erkannt",
        "feedback-error2": "Feeler: D'Ännerung gouf net gespäichert",
        "feedback-error3": "Feeler: Keng Äntwert vum API",
        "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 779c36c..8cc7e7d 100644 (file)
@@ -45,7 +45,7 @@
        "tog-enotifminoredits": "Versjik  mich 'ne e-mail bie klein bewirkinge op pagina's en bestenj op mien volglies",
        "tog-enotifrevealaddr": "Mien e-mailadres tuine in e-mailberichte",
        "tog-shownumberswatching": "'t Aantal gebroekers tuine die dees pagina volg",
-       "tog-oldsig": "Bestaonde ongerteikening:",
+       "tog-oldsig": "Dien bestaonde ongerteikening:",
        "tog-fancysig": "Es wikiteks behanjele (zonder autematische verwiezing)",
        "tog-uselivepreview": "\"live veurbesjouwing\" gebroeke",
        "tog-forceeditsummary": "'n Melding gaeve bie 'n laeg samevatting",
        "category-file-count-limited": "Dees categorie bevat {{PLURAL:$1|'t volgende bestandj|de volgende $1 bestenj}}.",
        "listingcontinuesabbrev": "wiejer",
        "index-category": "Geïndexeerde paazjes",
-       "noindex-category": "Óngeïndexeerde paazjes",
+       "noindex-category": "Neet-geïndexeerde pazjena's",
        "broken-file-category": "Pazjena's mit ónjuuste bestandjsverwiezinge",
        "about": "Informatie",
        "article": "Pagina",
        "newwindow": "(in nuuj venster)",
        "cancel": "Aafbraeke",
        "moredotdotdot": "Miè...",
-       "morenotlisted": "Deze lies is neet compleet.",
+       "morenotlisted": "Deze lies is mäögelik neet compleet.",
        "mypage": "Mien gebroekerspagina",
        "mytalk": "Euverlèk",
        "anontalk": "Euverlèk veur dit IP adres",
        "yourpassword": "Die wachwaord",
        "userlogin-yourpassword": "Wachwaord",
        "yourpasswordagain": "Wachwaord opnuuj intype",
-       "remembermypassword": "Mien wachwaord onthouwe veur later sessies (hoegstens $1 {{PLURAL:$1|daag|daag}})",
        "yourdomainname": "Die domein",
        "externaldberror": "d'r Is 'n fout opgetraoje biej 't aanmelje biej de database of doe höbs gén toesjtömming diene externe gebroeker biej te wèrke.",
        "login": "Aanmèlde",
        "htmlform-submit": "Slaon óp",
        "htmlform-reset": "Maak verangeringe óngedaon",
        "htmlform-selectorother-other": "Anges",
-       "sqlite-has-fts": "Zeuk versie $1 mit óngersteuning veur \"full-text\"",
-       "sqlite-no-fts": "Zeuk versie $1 zónger óngersteuning veur \"fulltext\"",
        "logentry-delete-delete": "$1 {{GENDER:$1|haet}} de pagina $3 gewösj",
        "logentry-delete-restore": "$1 haet de pagina $3 trögkgezat",
        "logentry-delete-event": "$1 haet de zichbaarheid van {{PLURAL:$5|'ne logbookregel|$5 logbookregels}} van $3 gewiezig: $4",
index 5194c8b..8c5b16c 100644 (file)
        "htmlform-title-not-exists": "$1 a no l'existe.",
        "htmlform-user-not-exists": "'''$1''' o no l'existe.",
        "htmlform-user-not-valid": "<strong>$1</strong> o no l'è un nomme utente vallido.",
-       "sqlite-has-fts": "$1 co-a poscibilitæ de riçerca completa into testo",
-       "sqlite-no-fts": "$1 sença a poscibilitæ de riçerca completa into testo",
        "logentry-delete-delete": "$1 {{GENDER:$2|o l'ha scassou}} a paggina $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|o|a}} l'ha ripristinou a paggina $3",
        "logentry-delete-event": "$1 {{GENDER:$2|o|a}} l'ha modificou a vixibilitæ de {{PLURAL:$5|un'açion do registro|$5 açioin do registro}} de \"$3\": $4",
index f3cf09b..18a05e5 100644 (file)
@@ -63,7 +63,7 @@
        "tog-enotifminoredits": "Siųsti man laišką, kai puslapio keitimas yra smulkus",
        "tog-enotifrevealaddr": "Rodyti mano el. pašto adresą priminimo laiškuose",
        "tog-shownumberswatching": "Rodyti stebinčių naudotojų skaičių",
-       "tog-oldsig": "Galiojantis parašas:",
+       "tog-oldsig": "Jūsų egzistuojantis parašas:",
        "tog-fancysig": "Laikyti parašą vikitekstu (be automatinių nuorodų)",
        "tog-uselivepreview": "Naudoti tiesioginę peržiūrą",
        "tog-forceeditsummary": "Klausti, kai palieku tuščią keitimo komentarą",
@@ -80,7 +80,7 @@
        "tog-showhiddencats": "Rodyti paslėptas kategorijas",
        "tog-norollbackdiff": "Nerodyti skirtumo atlikus atmetimą",
        "tog-useeditwarning": "Perspėti mane, kai palieku redagavimo puslapį, o jame yra neišsaugotų pakeitimų",
-       "tog-prefershttps": "Prisiregistruojant visada naudokite saugų ryšį",
+       "tog-prefershttps": "Visada naudoti saugų ryšį esant prisijungus",
        "underline-always": "Visada",
        "underline-never": "Niekada",
        "underline-default": "Pagal naršyklės nustatymus",
        "newwindow": "(atsidaro naujame lange)",
        "cancel": "Atšaukti",
        "moredotdotdot": "Daugiau...",
-       "morenotlisted": "Šis sąrašas nėra išsamus.",
+       "morenotlisted": "Šis sąrašas gali būti nepilnas.",
        "mypage": "Puslapis",
        "mytalk": "Aptarimas",
        "anontalk": "Aptarimas",
        "talk": "Aptarimas",
        "views": "Peržiūros",
        "toolbox": "Įrankiai",
+       "tool-link-userrights": "Keisti {{GENDER:$1|vartotojo|vartotojos}} grupes",
+       "tool-link-emailuser": "Siusti el. laišką {{GENDER:$1|šiam vartotojui|šiai vartotojai}}",
        "userpage": "Rodyti naudotojo puslapį",
        "projectpage": "Rodyti projekto puslapį",
        "imagepage": "Žiūrėti failo puslapį",
        "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?",
        "passwordreset-emailelement": "Naudotojo vardas: \n$1\n\nLaikinas slaptažodis: \n$2",
        "passwordreset-emailsentemail": "Jeigu šis el. pašto adresas yra susietas su jūsų paskyra, tada slaptažodžio atkūrimo laiškas bus išsiųstas.",
        "passwordreset-emailsentusername": "Jeigu buvo el. paštas susietas su šiuo naudotojo vardu, tai slaptažodžio atkūrimo el. laiškas bus išsiųstas.",
-       "passwordreset-emailsent-capture2": "Slaptažodžio keitimo {{PLURAL:$1|el. laiškas buvo išsiųstas|el. laiškai buvo išsiųsti}}. {{PLURAL:$1|vartotojo vardas ir slaptažodis rodomi|vartotojų vardų ir slaptažodžių sąrašas rodomas}} žemiau.",
-       "passwordreset-emailerror-capture2": "El. laiško siuntimas {{GENDER:$2|vartotojui}} nepavyko: $1 {{PLURAL:$3|vartotojo vardas ir slaptažodis rodomi|vartotojų vardų ir slaptažodžių sąrašas rodomas}} žemiau.",
+       "passwordreset-emailsent-capture2": "Slaptažodžio keitimo {{PLURAL:$1|el. laiškas buvo išsiųstas|el. laiškai buvo išsiųsti}}. {{PLURAL:$1|vartotojo vardas ir slaptažodis rodomi|vartotojų vardų ir slaptažodžių sąrašas rodomas}} čia.",
+       "passwordreset-emailerror-capture2": "El. laiško siuntimas {{GENDER:$2|vartotojui}} nepavyko: $1 {{PLURAL:$3|vartotojo vardas ir slaptažodis rodomi|vartotojų vardų ir slaptažodžių sąrašas rodomas}} čia.",
        "passwordreset-nocaller": "Skambinantysis turi būti nurodytas",
        "passwordreset-nosuchcaller": "Skambinantysis neegzistuoja: $1",
        "passwordreset-invalideamil": "Neteisingas el. pašto adresas",
        "invalid-content-data": "Neleistinas turinys.",
        "content-not-allowed-here": "Turinys \"$1\" puslapyje [[$2]] nėra leistinas.",
        "editwarning-warning": "Palikdamas šį puslapį jūs galite prarasti visus padarytus pakeitimus.\nJei esate prisijungęs, galite išjungti šį perspėjimą jūsų nustatymų skyrelyje \"{{int:prefs-editing}}\".",
+       "editpage-invalidcontentmodel-title": "Turinio modelis nepalaikomas",
+       "editpage-invalidcontentmodel-text": "Turinio modulis „$1“ nėra palaikomas.",
        "editpage-notsupportedcontentformat-title": "Turinio formatas nepalaikomas",
        "editpage-notsupportedcontentformat-text": "Turinio formatas $1 nepalaiko turinio modelio $2.",
        "content-model-wikitext": "vikitekstas",
        "upload-foreign-cant-upload": "Šis vikis nėra sukonfigūruotas failų įkėlimui į nurodytą išorinę failų talpyklą.",
        "upload-dialog-title": "Įkelti failą",
        "upload-dialog-button-cancel": "Atšaukti",
+       "upload-dialog-button-back": "Atgal",
        "upload-dialog-button-done": "Atlikta",
        "upload-dialog-button-save": "Išsaugoti",
        "upload-dialog-button-upload": "Įkelti",
        "blanknamespace": "(Pagrindinis)",
        "contributions": "{{GENDER:$1|Naudotojo}} indėlis",
        "contributions-title": "{{GENDER:$1|Naudotojo|Naudotojos}} $1 indėlis",
-       "mycontris": "Įnašai",
-       "anoncontribs": "Įnašai",
+       "mycontris": "Indėlis",
+       "anoncontribs": "Indėlis",
        "contribsub2": "Dėl {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "Naudotojo paskyra „$1“ neužregistruota.",
        "nocontribs": "Jokie keitimai neatitiko šių kriterijų.",
        "ipb-unblock": "Atblokuoti naudotojo vardą arba IP adresą",
        "ipb-blocklist": "Rodyti egzistuojančius blokavimus",
        "ipb-blocklist-contribs": "{{GENDER:$1|$1}} indėlis",
-       "ipb-blocklist-duration-left": "$1 kairėje",
+       "ipb-blocklist-duration-left": "liko $1",
        "unblockip": "Atblokuoti naudotoją",
        "unblockiptext": "Naudokite šią formą, kad atkurtumėte redagavimo galimybę\nankščiau užblokuotam IP adresui ar naudotojui.",
        "ipusubmit": "Atblokuoti šį adresą",
        "blocklink": "blokuoti",
        "unblocklink": "atblokuoti",
        "change-blocklink": "keisti blokavimo nustatymus",
-       "contribslink": "įnašai",
+       "contribslink": "indėlis",
        "emaillink": "siųsti el. laišką",
        "autoblocker": "Jūs buvote automatiškai užblokuotas, nes jūsų IP adresą neseniai naudojo „[[User:$1|$1]]“. Nurodyta naudotojo $1 blokavimo priežastis: „$2“.",
        "blocklogpage": "Blokavimų sąrašas",
        "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 peradresuoją į 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ė:",
        "tag-filter": "[[Special:Tags|Žymų]] filtras:",
        "tag-filter-submit": "Filtras",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Žyma|Žymos}}]]: $2)",
+       "tag-mw-contentmodelchange": "turinio modulio keitimas",
+       "tag-mw-contentmodelchange-description": "Pakeitimai, kurie [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel keičia puslapio turinio modelį]",
        "tags-title": "Žymos",
        "tags-intro": "Šiame puslapyje yra žymų, kuriomis programinė įranga gali pažymėti keitimus, sąrašas bei jų reikšmės.",
        "tags-tag": "Žymos pavadinimas",
        "tags-actions-header": "Veiksmai",
        "tags-active-yes": "Taip",
        "tags-active-no": "Ne",
-       "tags-source-extension": "Apibrėžta papildinio",
+       "tags-source-extension": "Apibrėžta programinės įrangos",
        "tags-source-manual": "Taikoma vartotojų ar robotų rankiniu būdu",
        "tags-source-none": "Nebevartojamas",
        "tags-edit": "taisyti",
        "htmlform-title-not-exists": "$1 neegzistuoja.",
        "htmlform-user-not-exists": "<strong>$1</strong> neegzistuoja.",
        "htmlform-user-not-valid": "<strong>$1</strong> nėra tinkamas naudotojo vardas.",
-       "sqlite-has-fts": "$1 su visatekstės paieškos palaikymu",
-       "sqlite-no-fts": "$1 be visatekstės paieškos palaikymo",
        "logentry-delete-delete": "$1 {{GENDER:$2|ištrynė}} puslapį $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|atkūrė}} puslapį $3",
        "logentry-delete-event": "$1 {{GENDER:$2|pakeitė}} matomumą {{PLURAL:$5|žurnalo įvykio|$5 žurnalo įvykių}} $3: $4",
        "feedback-external-bug-report-button": "Užpildyti techninę užduotį",
        "feedback-dialog-title": "Pateikti atsiliepimą",
        "feedback-dialog-intro": "Galite naudoti lengvą formą esančia žemiau, kad pateiktumėte savo atsiliepimus. Jūsų komentaras bus pridėtas prie puslapio \"$1\", kartu su jūsų vartotojo vardu.",
-       "feedback-error-title": "Klaida",
        "feedback-error1": "Klaida: Neatpažįstamas rezultatas iš API",
        "feedback-error2": "Klaida: Redagavimas nepavyko",
        "feedback-error3": "Klaida: Jokio atsakymo iš API",
index bc9db01..612b02a 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]].",
        "upload-http-error": "HTTP kļūda: $1",
        "upload-dialog-title": "Augšupielādēt failu",
        "upload-dialog-button-cancel": "Atcelt",
+       "upload-dialog-button-back": "Atpakaļ",
        "upload-dialog-button-done": "Gatavs",
        "upload-dialog-button-save": "Saglabāt",
        "upload-dialog-button-upload": "Augšupielādēt",
        "nolicense": "Neviena licence nav izvēlēta",
        "license-nopreview": "(Priekšskatījums nav pieejams)",
        "upload_source_url": "(derīgs, publiski pieejams URL)",
-       "upload_source_file": "(fails datorā)",
+       "upload_source_file": "(tavs izvēlētais fails tavā datorā)",
        "listfiles-delete": "dzēst",
        "listfiles-summary": "Šajā īpašajā lapā ir redzami visi augšupielādētie faili.",
        "listfiles_search_for": "Meklēt failu pēc vārda:",
        "apisandbox-results": "Rezultāti",
        "apisandbox-request-url-label": "Pieprasījuma URL:",
        "apisandbox-request-time": "Pieprasījuma izpildes laiks: {{PLURAL:$1|$1 ms}}",
+       "apisandbox-continue-clear": "Notīrīt",
        "booksources": "Grāmatu avoti",
        "booksources-search-legend": "Meklēt grāmatu avotus",
        "booksources-search": "Meklēt",
        "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",
        "year": "No gada (un senāki):",
        "sp-contributions-newbies": "Rādīt jauno lietotāju devumu",
        "sp-contributions-newbies-sub": "Jaunie lietotāji",
-       "sp-contributions-blocklog": "Bloķēšanas reģistrs",
+       "sp-contributions-blocklog": "bloķēšanas reģistrs",
        "sp-contributions-deleted": "dzēstais {{GENDER:$1|dalībnieka|dalībnieces}} devums",
        "sp-contributions-uploads": "augšupielādes",
        "sp-contributions-logs": "reģistri",
        "sp-contributions-talk": "diskusija",
-       "sp-contributions-userrights": "Lietotāju tiesību pārvaldība",
+       "sp-contributions-userrights": "dalībnieka tiesību pārvaldība",
        "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",
        "ipb-unblock-addr": "Atbloķēt $1",
        "ipb-unblock": "Atbloķēt lietotāju vai IP adresi",
        "ipb-blocklist": "Apskatīt esošos blokus",
-       "ipb-blocklist-contribs": "$1 devums",
+       "ipb-blocklist-contribs": "{{GENDER:$1|$1}} devums",
        "unblockip": "Atbloķēt lietotāju",
        "unblockiptext": "Šeit var atbloķēt iepriekš nobloķētu IP adresi vai lietotāja vārdu (atjaunot viņiem rakstīšanas piekļuvi).",
        "ipusubmit": "Noņemt šo bloku",
        "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",
        "newimages-legend": "Filtrs",
        "newimages-label": "Faila nosaukums (vai tā daļa):",
        "newimages-showbots": "Parādīt botu augšupielādētos failus",
+       "newimages-hidepatrolled": "Paslēpt pārbaudītās augšupielādes",
        "noimages": "Nav nekā ko redzēt.",
        "ilsubmit": "Meklēt",
        "bydate": "<b>pēc datuma</b>",
        "tags-actions-header": "Darbības",
        "tags-active-yes": "Jā",
        "tags-active-no": "Nē",
-       "tags-source-extension": "Nosaka paplašinājums",
+       "tags-source-extension": "Nosaka programmatūra",
        "tags-source-none": "Vairs netiek izmantots",
        "tags-edit": "labot",
        "tags-delete": "dzēst",
        "htmlform-chosen-placeholder": "Izvēlieties iespēju",
        "htmlform-cloner-create": "Pievienot vairāk",
        "htmlform-cloner-delete": "Noņemt",
-       "sqlite-has-fts": "$1 ar pilnteksta meklēšanas atbalstu",
-       "sqlite-no-fts": "$1 bez pilnteksta meklēšanas atbalsta",
        "logentry-delete-delete": "$1 {{GENDER:$2|izdzēsa}} lapu $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|atjaunoja}} lapu $3",
        "revdelete-content-hid": "saturs slēpts",
        "feedback-cancel": "Atcelt",
        "feedback-close": "Gatavs",
        "feedback-dialog-title": "Iesniegt atsauksmes",
-       "feedback-error-title": "Kļūda",
        "feedback-error1": "Kļūda: API neatpazīts rezultāts",
        "feedback-error2": "Kļūda: Labojums neizdevās",
        "feedback-error3": "Kļūda: Nav atbildes no API",
index cd0a50d..7834379 100644 (file)
@@ -9,7 +9,8 @@
                        "Ibero-kolxi",
                        "Reedy",
                        "The Evil IP address",
-                       "아라"
+                       "아라",
+                       "Velg"
                ]
        },
        "tog-underline": "Link'iş tude kogu3’uxaçki:",
        "nstab-template": "Şabloni",
        "nstab-help": "Meşvelaşi but’k’a",
        "nstab-category": "Kʼatʼegori",
+       "mainpage-nstab": "Dudi But'k'a",
        "error": "Çilata",
        "missing-article": "Datʼabeizik, na igoren \"$1\" $2 coxoni butʼkʼaşi tekstʼi var az*iru.\n\nMuşeni? Çunki am butʼkʼa, jileri na ren a butʼkʼaşi golaxteri versiyoni ren.\n\nEger sebebi aya na va renna, pʼrogramis ar çilata z*irit.\nMu iqʼven! Aya, a [[Special:ListUsers/sysop|adminis]], URL-ti çʼareli şekʼilite rapʼortʼi doçʼarit.",
        "missingarticle-rev": "(revizyoni#: $1)",
        "userlogin-yourname": "Skani maxmare-coxo",
        "yourpassword": "Pʼarola-skani:",
        "userlogin-yourpassword": "Pʼarola-skani",
-       "remembermypassword": "Parola-skani goişini (for a maximum of $1 {{PLURAL:$1|day|days}})",
        "yourdomainname": "Skani domaini:",
        "login": "Sitʼeşa amaxti",
        "nav-login-createaccount": "Sitʼeşa amaxti / hesabi dokʼidi",
        "createacct-reason": "Muşen",
        "mailmypassword": "Ağne pʼarola-çkimi moncğoni",
        "loginlanguagelabel": "Nena: $1",
+       "pt-login": "Sitʼeşa amaxti",
+       "pt-createaccount": "Hesabi dokʼidi",
        "oldpassword": "Mcveşi p'arola:",
        "newpassword": "Ağani P'arola:",
        "passwordreset-username": "Skani maxmare-coxo:",
        "rc-enhanced-expand": "Detayepe ko3ʼiri (JavaScript-i unon)",
        "rc-enhanced-hide": "Detayepe doşinaxi",
        "recentchangeslinked": "Alakʼali na renan oktirobape",
+       "recentchangeslinked-toolbox": "Alakʼali na renan oktirobape",
        "recentchangeslinked-title": "\"$1\" kʼala alakʼali na renan oktirobape",
        "recentchangeslinked-summary": "Tude na çʼars listʼe, kʼiti na noğiru butʼkʼaşa (varna kʼiti na noğiru kʼatʼegorişi makʼaturepeşa) kʼontʼaktʼi na ikips butʼkʼapes na ixvenu çodinaşi oktirobapeşi listʼe ren.\n[[Special:Watchlist|Gotxozu na ginon butʼkʼapeşi listʼes]] na renan butʼkʼape '''mçxu''' nçʼaraten niçʼaru.",
        "recentchangeslinked-page": "Butʼkʼaşi coxo:",
        "tooltip-pt-login": "Ginon na sitʼeşa amaxti, mecburi va re",
        "tooltip-pt-logout": "Siteşen Kogamaxti",
        "tooltip-ca-talk": "Butʼkʼaş doloxe na içʼaren çʼarape şeni mutxanepe mi3ʼvit",
-       "tooltip-ca-edit": "Am butʼkʼa kodogaktiren. Mu iqʼven! ipti \"Evvelişen i3ʼkʼedi\" tʼuşi ixmari do na çʼari çʼara ikʼontʼroli, ukʼule ikʼayitʼi.",
+       "tooltip-ca-edit": "Butʼkʼa doktiri",
        "tooltip-ca-addsection": "Ağani burme dokʼidi.",
        "tooltip-ca-viewsource": "Am butʼkʼa içven. Xvala odudeş kʼodi gaz*iren. Doloxe muşi va gaktirinen.",
        "tooltip-ca-history": "Am butʼkʼaşi golaxteri versiyonepe",
        "watchlisttools-raw": "Kʼobo gotxozu listʼe doktiri",
        "specialpages": "Doxmeli butʼkʼape",
        "rightsnone": "(Va ren)",
+       "searchsuggest-search": "Mgori",
        "special-characters-group-latin": "Lat'ini",
        "special-characters-group-greek": "Xorumi",
        "special-characters-group-arabic": "Arabuli"
index d9d8ea0..ed6dde8 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|पन्ना|पन्ना}}  मे शामिल अछि।",
        "permissionserrorstext-withaction": "अहाँक अनुमति नै अछि $2 लेल, एकर लेल {{PLURAL:$1|कारण|कारणसभ}}सँ:",
        "recreate-moveddeleted-warn": "'''चेतौनी''': अहाँ फेरसँ ओ पन्ना बना रहल छी जे पहिने मेटा देल गेल छै।'''\n\nअहाँ विचारू जे की ई सम्पादन केनाइ उचित अछि।\nऐ पन्नाक मेटाएल बला आ हटाएल वृत्तलेख एतए सुविधा लेल देल जा रहल अछि:",
        "moveddeleted-notice": "ई पन्ना मेटाएल गेल अछि।\nई पन्ना लेल मेटाएल आ स्थानान्तरणक लग सन्दर्भ लेल नीचाँ देल गेल अछि।",
-       "log-fulllog": "सभà¤\9fा à¤µà¥\83तà¥\8dतलà¥\87à¤\96 देखी",
+       "log-fulllog": "समà¥\8dपà¥\82रà¥\8dण à¤²à¥\8cà¤\97 देखी",
        "edit-hook-aborted": "सम्पादन नोकसीसँ खतम भेल।\nई कोनो कारण नै देलक।",
        "edit-gone-missing": "पन्ना अद्यतन नै भऽ सकल।\nलगैए जे ई मेटा देल गेल अछि।",
        "edit-conflict": "सम्पादन अन्तर्विरोध",
        "expansion-depth-exceeded-warning": "पन्ना विस्तार गहिराई पार केनए अछि",
        "parser-unstrip-loop-warning": "Unstrip लूप पाओल गेल",
        "parser-unstrip-recursion-limit": "Unstrip पुनरावर्तन सीमा पार कइर गेल($1)",
-       "converter-manual-rule-error": "म्यानुअल भाषा परिवर्तन नियम में त्रुटि",
+       "converter-manual-rule-error": "म्यानुअल भाषा परिवर्तन नियममे त्रुटि",
        "undo-success": "ई सम्पादन पूर्ववत बदलल जा सकैए।\nकृपा क' नीचाँक तुलनाक जाँच करू ई देखैले जे ई वएह भेल अछि जे अहाँ चाहै छलहुँ, आ तखन सम्पादन ख़तम करबा लेल नीचाँक परिवर्तन सुरक्षित करू ।",
        "undo-failure": "मध्यवर्ती विरोधी सम्पादनक कारण ऐ सम्पादनकेँ खतम नै कएल जा सकैए।",
        "undo-norev": "ई सम्पादन खतम नै कएला जा सकैए कारण ई अछि नै वा मेटा देल गेल अछि।",
        "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 क समकालिक तखुनका संशोधन",
        "revdelete-show-file-submit": "हँ",
        "revdelete-selected-text": "[[:$2]] {{PLURAL:$1|क|के}} चयनित अवतरण:",
        "revdelete-selected-file": "[[:$2]] {{PLURAL:$1|क|के}} चयनित फाइल अवतरण:",
-       "logdelete-selected": "{{PLURAL:$1|à¤\9aà¥\81नल à¤µà¥\83तà¥\8dतलà¥\87à¤\96 à¤\98à¤\9fना|à¤\9aà¥\81नल à¤µà¥\83तà¥\8dतलà¥\87à¤\96 घटनासभ}}:",
+       "logdelete-selected": "{{PLURAL:$1|à¤\9aà¥\81नल à¤²à¥\8cà¤\97 à¤\98à¤\9fना|à¤\9aà¥\81नल à¤²à¥\8cà¤\97 घटनासभ}}:",
        "revdelete-text-text": "हटाएल गेल अवतरण पृष्ठ इतिहासमें देखाएल जाएत मुदा वोकर सामग्री सार्वजनिक रूपसँ नै देखाएल जा सकएत अछि।",
        "revdelete-text-file": "हटाएल गेल अवतरण पृष्ठ इतिहासमें देखाएल जाएत मुदा वोकर सामग्री सार्वजनिक रूपसँ नै देखाएल जा सकएत अछि।",
        "logdelete-text": "हटाए गेल प्रवेश घटनासभ अखैनो भी लॉग में दिखाबल ज्यात, लेकिन ओकर सामग्री के कुछ भाग के सार्वजनीक करबाक के लेल दुर्गम भ्या जेत।",
        "revdelete-reasonotherlist": "दोसर कारण",
        "revdelete-edit-reasonlist": "मेटेबाक कारण बदली",
        "revdelete-offender": "संशोधन केनिहार:",
-       "suppressionlog": "दबाà¤\8fलà¤\97à¥\87ल à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
+       "suppressionlog": "नà¥\81à¤\95ाà¤\8fल à¤²à¥\8cà¤\97",
        "suppressionlogtext": "नीचाँ मेटाएल आ प्रतिबन्धक उल्लेख अछि जे संचालकसँ नुकाएल सामिग्री अछि।\nअखन स्थित प्रभावी प्रतिबन्ध आ अवरोध लेल देखू [[Special:BlockList|IP block list]] ।",
-       "mergehistory": "मिà¤\9cà¥\8dà¤\9dर à¤­à¥\87ल à¤ªà¤¨à¥\8dना à¤¸à¤­à¤\95 à¤\87तिहास",
+       "mergehistory": "पà¥\83षà¥\8dठà¤\95 à¤\87तिहास à¤\8fà¤\95तà¥\8dरित à¤\95रà¥\80",
        "mergehistory-header": "ई पन्ना अहाँकेँ एकटा स्रोत पन्नाक एकटा नव पन्नामे संशोधन इतिहासकेँ मिज्झर करबाक अनुमति दैत अछि।\nसुनिश्चित होउ जे ई परिवर्तन ऐतिहासिक पन्ना सांतत्य स्थापित करत।",
-       "mergehistory-box": "दà¥\82 à¤ªà¤¨à¥\8dनाà¤\95 à¤¸à¤\82शà¥\8bधन à¤®à¤¿à¤\9cà¥\8dà¤\9dर à¤\95रà¥\80।",
+       "mergehistory-box": "दà¥\81à¤\88 à¤ªà¥\83षà¥\8dठसभà¤\95 à¤\87तिहास à¤\8fà¤\95तà¥\8dरित à¤\95रà¥\80:",
        "mergehistory-from": "मूल पन्ना:",
        "mergehistory-into": "लक्ष्य पन्ना:",
-       "mergehistory-list": "मिà¤\9cà¥\8dà¤\9dर à¤¯à¥\8bà¤\97à¥\8dय सम्पादन इतिहास",
+       "mergehistory-list": "à¤\8fà¤\95तà¥\8dरितà¥\80à¤\95रण सम्पादन इतिहास",
        "mergehistory-merge": "[[:$1]] एकर संशोधन सभकेँ [[:$2]] मे मिलाएल जा सकैए।\nरेडियो बटन स्तम्भक प्रयोग मात्र संशोधनकेँ निर्धारित समए वा ओइसँ पहिने मिज्झर करबामे प्रयोग करू।\nमोन राखू जे उपर नीचाँक लागिक प्रयोग ऐ स्तम्भकेँ पुनर्स्थापित कऽ देत।",
-       "mergehistory-go": "मिà¤\9cà¥\8dà¤\9dर à¤¹à¥\8bà¤\87 à¤¯à¥\8bà¤\97à¥\8dय à¤¸à¤®à¥\8dपादन à¤¸à¤­à¤\95à¥\87à¤\81 à¤¦à¥\87à¤\96ाà¤\89",
-       "mergehistory-submit": "सà¤\82शà¥\8bधन à¤¸à¤­à¤\95à¥\87 à¤®à¤¿à¤\9cà¥\8dà¤\9dर करी",
-       "mergehistory-empty": "à¤\95à¥\8bनà¥\8b à¤¸à¤\82शà¥\8bधन à¤®à¤¿à¤\9cà¥\8dà¤\9dर à¤¨à¥\88 à¤\95à¤\8fल à¤\9cा à¤¸à¤\95à¥\88à¤\8f।",
+       "mergehistory-go": "à¤\8fà¤\95तà¥\8dरित à¤\95रà¥\88लà¥\87ल à¤²à¤¾à¤¯à¤\95 à¤¸à¤®à¥\8dपादन à¤¦à¥\87à¤\96ाबà¥\80",
+       "mergehistory-submit": "सà¤\82शà¥\8bधन à¤\8fà¤\95तà¥\8dरित करी",
+       "mergehistory-empty": "à¤\95à¥\8bनà¥\8b à¤­à¥\80 à¤\85वतरण à¤\8fà¤\95तà¥\8dरित à¤¨à¥\88 à¤\95à¤\8fल à¤\9cा à¤¸à¤\95ल।",
        "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": "स्रोत पन्ना एकटा मान्य शीर्षक हेबाक चाही।",
        "mergehistory-comment": "[[:$1]] केँ [[:$2]] मे मिलाएल गेल: $3",
        "mergehistory-same-destination": "स्रोत आ लक्ष्य पन्ना सभ एक्के नै भऽ सकैए",
        "mergehistory-reason": "कारण:",
-       "mergelog": "मिà¤\9cà¥\8dà¤\9dरबला à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
+       "mergelog": "à¤\8fà¤\95तà¥\8dरà¥\80à¤\95रण à¤²à¥\8cà¤\97",
        "revertmerge": "नै मिज्झर",
        "mergelogpagetext": "नीचाँ एक पन्ना इतिहासक दोसरमे अद्यतन मिश्रणक सूची अछि।",
        "history-title": "\"$1\" क संशोधन इतिहास",
        "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-createaccount": "नव प्रयोक्ता खातासभ बनाबी",
        "right-autocreateaccount": "बाहरी खातासँ स्वतः प्रवेश",
        "right-minoredit": "सम्पादन सभकेँ मामूली चिन्हित करी",
-       "right-move": "पनà¥\8dना à¤\98सà¤\95ाबी",
-       "right-move-subpages": "पà¥\83षà¥\8dठ à¤\89पपà¥\83षà¥\8dठसभ à¤¸à¤¹à¤¿à¤¤ à¤\98सà¤\95ाबी",
-       "right-move-rootuserpages": "मà¥\82ल à¤ªà¥\8dरयà¥\8bà¤\95à¥\8dता à¤ªà¤¨à¥\8dना à¤\98सà¤\95ाबी",
-       "right-move-categorypages": "शà¥\8dरà¥\87णà¥\80 à¤ªà¥\83षà¥\8dठ à¤\98सà¤\95ाबी",
-       "right-movefile": "सञ्चिका सभ घसकाबी",
+       "right-move": "पनà¥\8dना à¤¸à¥\8dथानानà¥\8dतरित à¤\95री",
+       "right-move-subpages": "पà¥\83षà¥\8dठ à¤\89पपà¥\83षà¥\8dठसभ à¤¸à¤¹à¤¿à¤¤ à¤¸à¥\8dथानानà¥\8dतरित à¤\95री",
+       "right-move-rootuserpages": "मà¥\82ल à¤ªà¥\8dरयà¥\8bà¤\95à¥\8dता à¤ªà¤¨à¥\8dना à¤¸à¥\8dथानानà¥\8dतरित à¤\95री",
+       "right-move-categorypages": "शà¥\8dरà¥\87णà¥\80 à¤ªà¥\83षà¥\8dठ à¤¸à¥\8dथानानà¥\8dतरित à¤\95री",
+       "right-movefile": "सञ्चिकासभ स्थानान्तरित करी",
        "right-suppressredirect": "पृष्ठ घसकेबाकाल पुनर्निर्देश नै छोडी",
        "right-upload": "सञ्चिकासभ उपारोपित करी",
        "right-reupload": "वर्तमान सञ्चिकासभक पुनर्लेखन करी",
        "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-undelete": "पन्ना फेरसँ आनी",
        "right-suppressrevision": "संचालकसँ नुकाएल संशोधनकेँ पुनरीक्षित करी आ फेरसँ आनी",
        "right-viewsuppressed": "कोनो प्रयोक्ताके नुकाएल संसोधन देखु",
-       "right-suppressionlog": "वà¥\8dयà¤\95à¥\8dतिà¤\97त à¤µà¥\83तà¥\8dतलà¥\87à¤\96 देखी",
+       "right-suppressionlog": "निà¤\9cà¥\80 à¤²à¥\8cà¤\97 देखी",
        "right-block": "दोसर प्रयोक्ताकेँ सम्पादनसँ रोकी",
        "right-blockemail": "प्रयोक्ताकेँ ई-पत्र पठेबासँ रोकी",
        "right-hideuser": "एकटा प्रयोक्तानामकेँ प्रतिबन्धित करू, लोकसँ एकरा नुका कऽ",
        "right-noratelimit": "दरक सीमासँ प्रभावित नै",
        "right-import": "दोसर विकीसँ पन्ना लिअ",
        "right-importupload": "पन्नासभकेँ संचिका उपारोपणसँ आनू",
-       "right-patrol": "दà¥\8bसराà¤\95 à¤¸à¤®à¥\8dपादनà¤\95à¥\87à¤\81 à¤¸à¤\82à¤\9aालित à¤¦à¥\87à¤\96ाà¤\89",
-       "right-autopatrol": "अपन सम्पादनकेँ स्वचालित रूपेँ संचालित देखाउ",
+       "right-patrol": "à¤\85नà¥\8dय à¤¸à¤¦à¤¸à¥\8dयसभà¤\95 à¤¸à¤®à¥\8dपादन à¤ªà¤°à¥\80à¤\95à¥\8dषित à¤\9aिनà¥\8dहित à¤\95रà¥\80",
+       "right-autopatrol": "अपन सम्पादन स्वचालित रूपसँ परीक्षित चिन्हित करी",
        "right-patrolmarks": "हालक परिवर्तनमे संचालन चेन्ह देखू",
-       "right-unwatchedpages": "बिना à¤¸à¤\82à¤\9aालित à¤ªà¤¨à¥\8dना à¤¸à¤­à¤\95 à¤¸à¥\82à¤\9aà¥\80à¤\95à¥\87à¤\81 à¤¦à¥\87à¤\96à¥\82",
-       "right-mergehistory": "पनà¥\8dनाà¤\95 à¤\87तिहास à¤¸à¤­à¤\95à¥\87à¤\81 à¤®à¤¿à¤\9cà¥\8dà¤\9dर à¤\95रà¥\82",
+       "right-unwatchedpages": "à¤\8fहन à¤ªà¥\83षà¥\8dठसभà¤\95 à¤¸à¥\82à¤\9aà¥\80 à¤¦à¥\87à¤\96à¥\80 à¤\9cà¥\87 à¤\95à¥\87à¤\95रà¥\8b à¤§à¥\8dयानसà¥\82à¤\9aà¥\80मà¥\87 à¤¨à¥\88 à¤\85à¤\9bि",
+       "right-mergehistory": "पनà¥\8dनाà¤\95 à¤\87तिहास à¤\8fà¤\95तà¥\8dरित à¤\95रà¥\80",
        "right-userrights": "सभटा प्रयोक्ता अधिकारकेँ सम्पादित करू",
        "right-userrights-interwiki": "दोसर विकीपर प्रयोक्ताक प्रयोक्ता अधिकारक सम्पादन करी",
        "right-siteadmin": "दत्तनिधिकेँ प्रतिबन्धित करू आ फेर प्रतिबन्ध हटाउ",
        "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": "पà¥\8dरयà¥\8bà¤\95à¥\8dता à¤\85धिà¤\95ार à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
+       "rightslog": "पà¥\8dरयà¥\8bà¤\95à¥\8dता à¤\85धिà¤\95ार à¤²à¥\8cà¤\97",
        "rightslogtext": "ई प्रयोक्ता अधिकार परिवर्तन सभक वृतलेख छी।",
        "action-read": "ई पन्ना पढी",
        "action-edit": "ई पन्नाक सम्पादित करी",
-       "action-createpage": "पृष्ठ बनाबी",
-       "action-createtalk": "वार्ता पन्ना बनाबी",
+       "action-createpage": "à¤\88 à¤ªà¥\83षà¥\8dठ à¤¬à¤¨à¤¾à¤¬à¥\80",
+       "action-createtalk": "à¤\88 à¤µà¤¾à¤°à¥\8dता à¤ªà¤¨à¥\8dना à¤¬à¤¨à¤¾à¤¬à¥\80",
        "action-createaccount": "ई प्रयोक्ता खाता बनाबी",
-       "action-history": "पन्नाक इतिहास मिज्झर करी",
+       "action-autocreateaccount": "स्वतः बाहरी सदस्य खाता बनाबी",
+       "action-history": "ई पृष्ठक इतिहास देखी",
        "action-minoredit": "ऐ सम्पादनके मामूली कही",
-       "action-move": "à¤\90 à¤ªà¥\83षà¥\8dठà¤\95à¥\87 à¤\98सà¤\95ाबी",
-       "action-move-subpages": "à¤\90 à¤ªà¤¨à¥\8dना à¤\86 à¤\8fà¤\95र à¤\89पपनà¥\8dनाà¤\95à¥\87 à¤\98सà¤\95ाबी",
-       "action-move-rootuserpages": "मà¥\82ल à¤ªà¥\8dरयà¥\8bà¤\95à¥\8dता à¤ªà¤¨à¥\8dना à¤\98सà¤\95ाबी",
-       "action-move-categorypages": "शà¥\8dरà¥\87णà¥\80 à¤ªà¥\83षà¥\8dठ à¤\98सà¤\95ाबी",
-       "action-movefile": "à¤\88 à¤¸à¤\82à¤\9aिà¤\95ाà¤\95à¥\87à¤\81 à¤\98सà¤\95ाà¤\89",
+       "action-move": "à¤\88 à¤ªà¥\83षà¥\8dठà¤\95à¥\87 à¤¸à¥\8dथानानà¥\8dतरित à¤\95री",
+       "action-move-subpages": "à¤\88 à¤ªà¤¨à¥\8dना à¤\86 à¤\8fà¤\95र à¤\89पपनà¥\8dनाà¤\95à¥\87 à¤¸à¥\8dथानानà¥\8dतरित à¤\95री",
+       "action-move-rootuserpages": "मà¥\82ल à¤ªà¥\8dरयà¥\8bà¤\95à¥\8dता à¤ªà¤¨à¥\8dना à¤¸à¥\8dथानानà¥\8dतरित à¤\95री",
+       "action-move-categorypages": "शà¥\8dरà¥\87णà¥\80 à¤ªà¥\83षà¥\8dठ à¤¸à¥\8dथानानà¥\8dतरित à¤\95री",
+       "action-movefile": "à¤\88 à¤¸à¤\9eà¥\8dà¤\9aिà¤\95ा à¤¸à¥\8dथानानà¥\8dतरण à¤\95रà¥\80",
        "action-upload": "ई संचिकाकेँ उपारोपित करू",
        "action-reupload": "ई संचिकाक पुनर्लेखन करू",
        "action-reupload-shared": "ई संचिकाकेँ साझी बखारीमे नजरि नै दिअ",
        "action-rollback": "कृपा कऽ अन्तिम प्रयोक्ताक सम्पादनकेँ प्रत्यावर्तित करू जे एक खास पन्नाकेँ सम्पादित केलन्हि",
        "action-import": "ऐ पन्नाकेँ दोसर विकीसँ आनू",
        "action-importupload": "ऐ पन्नाकेँ संचिका उपारोपणसँ आनू",
-       "action-patrol": "दà¥\8bसराà¤\95 à¤¸à¤®à¥\8dपादनà¤\95à¥\87à¤\81 à¤¸à¤\82à¤\9aालित à¤¦à¥\87à¤\96ाà¤\89",
-       "action-autopatrol": "अपन सम्पादनकेँ संचालित देखाउ",
+       "action-patrol": "à¤\85नà¥\8dय à¤¸à¤¦à¤¸à¥\8dयसभà¤\95 à¤¸à¤®à¥\8dपादन à¤ªà¤°à¥\80à¤\95à¥\8dषित à¤\95रà¥\80",
+       "action-autopatrol": "अपन सम्पादन स्वचालित रूपसँ परीक्षित करी",
        "action-unwatchedpages": "बिना संचालित पन्ना सभक सूचीकेँ देखू",
-       "action-mergehistory": "पनà¥\8dनाà¤\95 à¤\87तिहासà¤\95à¥\87à¤\81 à¤®à¤¿à¤\9cà¥\8dà¤\9dर à¤\95रà¥\82",
+       "action-mergehistory": "à¤\88 à¤ªà¥\83षà¥\8dठà¤\95 à¤\87तिहास à¤\8fà¤\95तà¥\8dरित à¤\95रà¥\80",
        "action-userrights": "सभटा प्रयोक्ता अधिकारकेँ सम्पादित करू",
        "action-userrights-interwiki": "दोसर विकीपर प्रयोक्ताक प्रयोक्ता अधिकारक सम्पादन करू",
        "action-siteadmin": "दत्तनिधिकेँ प्रतिबन्धित करू आ फेर प्रतिबन्ध हटाउ",
        "action-editmywatchlist": "काँच साकांक्षसूची संपादित करू",
        "action-viewmywatchlist": "अपन काँच साकांक्षसूची देखु",
        "action-viewmyprivateinfo": "अपन व्यक्तिगत जानकारी देखु",
-       "action-editmyprivateinfo": "à¤\85पन à¤µà¥\8dयà¤\95à¥\8dतिà¤\97त à¤\9cानà¤\95ारà¥\80 à¤¸à¤®à¥\8dपादित à¤\95रà¥\81",
+       "action-editmyprivateinfo": "à¤\85पन à¤µà¥\8dयà¤\95à¥\8dतिà¤\97त à¤\9cानà¤\95ारà¥\80 à¤¸à¤®à¥\8dपादित à¤\95रà¥\80",
        "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": "लगक परिवर्तनसभ",
        "recentchanges-legend": "नव परिवर्तन सम्बन्धी विकल्प",
-       "recentchanges-summary": "à¤\88 à¤ªà¤¨à¥\8dनापर à¤µà¤¿à¤\95à¥\80मà¥\87 à¤­à¥\87ल à¤¸à¤­ à¤¸à¤\81 à¤\85दà¥\8dयतन à¤ªà¤°à¤¿à¤µà¤°à¥\8dतनपर à¤¨à¤\9cरि à¤°à¤¾à¤\96à¥\80।",
+       "recentchanges-summary": "à¤\88 à¤µà¤¿à¤\95िमà¥\87 à¤­à¥\87ल à¤¸à¤¨à¥\8dनिà¤\95à¤\9f à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\88 à¤ªà¥\83षà¥\8dठपर à¤¦à¥\87à¤\96ल à¤\9cा à¤¸à¤\95à¥\88त à¤\85à¤\9bि।",
        "recentchanges-noresult": "इ अवधिके दौरान इ मापदंडके पूर्ण करेत समय कोनो परिवर्तन नै केएल गेल अछि।",
        "recentchanges-feed-description": "ई सूचना-तंत्रांशमे विकीमे भेल सभसँ लगक परिवर्तन ताकी।",
        "recentchanges-label-newpage": "ई सम्पादन एकटा नव पन्नाक निर्माण केलक।",
        "recentchanges-label-minor": "ई एकटा लघु सम्पादन छी",
        "recentchanges-label-bot": "ई सम्पादन यान्त्रिक छल।",
-       "recentchanges-label-unpatrolled": "à¤\90 à¤¸à¤®à¥\8dपादनà¤\95 à¤ªà¥\81नरà¥\80à¤\95à¥\8dषण à¤\85à¤\96न à¤§à¤°à¤¿ à¤¨à¥\88 à¤\95à¤\8fल à¤\97à¥\87ल à¤\85à¤\9bि।",
+       "recentchanges-label-unpatrolled": "à¤\88 à¤¸à¤®à¥\8dपादन à¤\85à¤\96न à¤\9cाà¤\81à¤\9aल à¤¨à¥\88 à¤\97à¥\87ल à¤\85à¤\9bि",
        "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 ।",
-       "uploadlogpage": "à¤\89पारà¥\8bपण à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
+       "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": "à¤\89पारà¥\8bपण à¤²à¥\8cà¤\97",
        "uploadlogpagetext": "नीचाँ अद्यतन सञ्चिका उपारोपणक वर्णन अछि।\nदेखी [[Special:NewFiles|नव सञ्चिकाक बखारी]] बेसी स्पष्ट समुच्चा दृश्य लेल।",
        "filename": "सञ्चिका नाम",
        "filedesc": "संक्षेप",
        "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.]",
        "mimetype": "माइम प्रकार:",
        "download": "अवारोपन",
        "unwatchedpages": "बिन ध्यान देल पन्ना",
-       "listredirects": "रसà¥\8dता à¤¬à¤¦à¤²à¥\87नक सूची",
+       "listredirects": "पà¥\81नरà¥\8dनिरà¥\8dदà¥\87शनसभक सूची",
        "listduplicatedfiles": "डुप्लिकेट के साथ फाइलसभ के सूची।",
        "listduplicatedfiles-entry": "[[:File:$1|$1]] राखैत अछि  [[$3|{{PLURAL:$2|एक प्रतिलिपि|$2 duplicates}}]] ।",
        "unusedtemplates": "बिना प्रयोगक नमूना सभ",
        "unusedtemplateswlh": "दोसर लागि सभ",
        "randompage": "अव्यवस्थित पृष्ठ",
        "randompage-nopages": "ऐमे दोसर पन्ना नै अछि {{PLURAL:$2|namespace|namespaces}}: $1 ।",
-       "randomincategory": "श्रेणी में यादृच्छिक (रैंडम) पन्ना",
+       "randomincategory": "श्रेणीमे यादृच्छिक पृष्ठ",
        "randomincategory-invalidcategory": "\"$1\" एक मान्य श्रेणी नाम नै अछि।",
+       "randomincategory-nopages": "[[:Category:$1|$1]] श्रेणीमे कोनो पृष्ठ नै अछि।",
        "randomincategory-category": "श्रेणी:",
-       "randomredirect": "मिज्झर बदलेनबला लागि",
+       "randomincategory-legend": "श्रेणीमे यादृच्छिक पृष्ठ",
+       "randomincategory-submit": "जाए",
+       "randomredirect": "कोनो एक पुनर्निर्देशन पर जाए",
        "randomredirect-nopages": "नामस्थान \"$1\" मे कोनो बदलेनबला लागि नै अछि।",
        "statistics": "सांख्यिकी",
        "statistics-header-pages": "पन्नाक तथ्याङ्क",
        "statistics-users": "पञ्जीकृत [[Special:ListUsers|प्रयोक्ता]]",
        "statistics-users-active": "सक्रिय प्रयोक्ता",
        "statistics-users-active-desc": "प्रयोक्ता जे अन्तिम {{PLURAL:$1|दिन|$1 दिन}} मे कोनो काज केने छथि",
-       "pageswithprop-submit": "जाऊ",
+       "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]] दिस जा रहल अछि।",
        "double-redirect-fixed-maintenance": "द्वितीयक बदलेन [[$1]] सँ [[$2]] कएल गेल।",
        "double-redirect-fixer": "बदलेन स्थायित्व",
-       "brokenredirects": "à¤\9fà¥\82à¤\9fल à¤¬à¤¦à¤²à¥\87न à¤¸à¤­",
+       "brokenredirects": "तà¥\81à¤\9fल à¤ªà¥\81नरà¥\8dनिरà¥\8dदà¥\87शन à¤ªà¥\83षà¥\8dठ",
        "brokenredirectstext": "ई बदलेन सभ नै अवस्थित पन्ना सभक दिस जाइत अछि।",
        "brokenredirects-edit": "सम्पादन करी",
        "brokenredirects-delete": "मेटाबी",
        "prefixindex": "उपसर्गक संग सभटा पृष्ठ",
        "prefixindex-namespace": "उपसर्ग भएल सभ पृष्ठ ($1 नामस्थान)",
        "prefixindex-strip": "सूची में उपसर्ग नुकाउ",
-       "shortpages": "पनà¥\8dना à¤¸à¤­ à¤\9bाà¤\81à¤\9fà¥\82",
+       "shortpages": "à¤\9bà¥\8bà¤\9f à¤ªà¥\83षà¥\8dठसभ",
        "longpages": "नमगर पन्ना सभ",
        "deadendpages": "एकदमसँ अन्त भऽ जाएबला पन्ना सभ",
        "deadendpagestext": "ई पन्ना सभ {{SITENAME}} क दोसर पन्नासँ लागिमे नै रहत।",
        "newpages-username": "प्रयोक्तानाम:",
        "ancientpages": "सभसँ पुरान पन्नासभ",
        "move": "स्थानान्तरण",
-       "movethispage": "à¤\88 à¤ªà¥\83षà¥\8dठà¤\95à¥\87 à¤\98सà¤\95ाबी",
+       "movethispage": "पà¥\83षà¥\8dठà¤\95 à¤¨à¤¾à¤® à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\95री",
        "unusedimagestext": "ई सभ संचिका अछि मुदा कोनो पन्नामे निवशित नै अछि।\nकृपा कऽ ई बुझू जे दोसर जालस्थल सभ सोझ सार्वत्रिक विभव संकेतबला कोनो संचिकासँ लागि बना सकैए, आ तँए सरिय प्रयोगक बादो अखनो एतए सूचित कएल जा सकैए।",
        "unusedcategoriestext": "ई संवर्ग पन्ना सभ अछि, ओना कोनो दोसर पन्ना वा संवर्ग ओकर प्रयोग करैत अछि।",
        "notargettitle": "बिन लक्ष्यक",
        "booksources-invalid-isbn": "देल आइ.एस.बी.एन. संख्या मान्य नै बुझाइत अछि; कृपा कऽ मूल स्रोतसँ द्वितीयक बनेबा काल भेल भ्रमकेँ जाँचू।",
        "specialloguserlabel": "कर्ता:",
        "speciallogtitlelabel": "प्रयोजन (शीर्षक अथवा {{ns:user}}:प्रयोगकर्तानाम):",
-       "log": "वृत्तलेख",
-       "all-logs-page": "सभ सार्वजनिक वृत्तलेख",
+       "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-submit": "देखाबी",
        "listusers-noresult": "कोनो प्रयोक्ता नै",
        "listusers-blocked": "(प्रतिबन्धित)",
-       "activeusers": "सक्रिय प्रयोक्ता सभक सूची",
+       "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": "सांच(सभ) के संपादन करए लेल",
-       "trackingcategories": "श्रेणीके ट्रयाक करु",
+       "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": "कार्य नै भेल",
        "deletedtext": "\"$1\" केँ मेटा देल गेल अछि।\nदेखू $2 हालक मेटाएल सामिग्रीक अभिलेख लेल।",
-       "dellogpage": "मà¥\87à¤\9fाà¤\8fल à¤¸à¤¾à¤®à¤¿à¤\97à¥\8dरà¥\80à¤\95 à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
+       "dellogpage": "मà¥\87à¤\9fाà¤\8fल à¤²à¥\8cà¤\97",
        "dellogpagetext": "नीचाँ एकदम लगक मेटाएल पन्नाकऽ सूची छी।",
-       "deletionlog": "मà¥\87à¤\9fाà¤\8fल à¤¸à¤¾à¤®à¤¿à¤\97à¥\8dरà¥\80à¤\95 à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
+       "deletionlog": "मà¥\87à¤\9fाà¤\8fल à¤²à¥\8cà¤\97",
        "reverted": "पुरान कोनो संशोधन धरि घुराउ",
        "deletecomment": "कारण:",
        "deleteotherreason": "दोसर/ अतिरिक्त कारण:",
        "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": "मात्र स्वत: स्थापित प्रयोक्ताकेँ अनुमति दएल जाए",
        "maximum-size": "अधिक आकार:",
        "pagesize": "(अष्टक)",
        "restriction-edit": "संपादन",
-       "restriction-move": "à¤\98सà¤\95ाà¤\89",
+       "restriction-move": "सà¥\8dथानानà¥\8dतरण",
        "restriction-create": "बनाउ",
        "restriction-upload": "उपारोपण",
        "restriction-level-sysop": "पूर्ण सुरक्षित",
        "restriction-level-autoconfirmed": "अर्ध-रक्षित",
        "restriction-level-all": "कोनो स्तर",
-       "undelete": "मà¥\87à¤\9fाà¤\8fल à¤ªà¤¨à¥\8dना à¤¦à¥\87à¤\96à¥\82",
-       "undeletepage": "दà¥\87à¤\96à¥\82 à¤\86 à¤«à¥\87रसà¤\81 à¤®à¥\87à¤\9fाà¤\8fल à¤ªà¤¨à¥\8dना à¤\86नà¥\82",
+       "undelete": "मà¥\87à¤\9fाà¤\8fल à¤ªà¤¨à¥\8dना à¤¦à¥\87à¤\96à¥\80",
+       "undeletepage": "मà¥\87à¤\9fाà¤\8fल à¤\97à¥\87ल à¤ªà¥\83षà¥\8dठ à¤¦à¥\87à¤\96à¥\80 à¤\86 à¤ªà¥\81नरà¥\8dसà¥\8dथापित à¤\95रà¥\80",
        "undeletepagetitle": "''' ई मेटाएल संशोधन लेने अछि [[:$1|$1]]एकर''' ।",
-       "viewdeletedpage": "मà¥\87à¤\9fाà¤\8fल à¤ªà¤¨à¥\8dना à¤¦à¥\87à¤\96à¥\82",
+       "viewdeletedpage": "मà¥\87à¤\9fाà¤\8fल à¤ªà¤¨à¥\8dना à¤¦à¥\87à¤\96à¥\80",
        "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": "मà¥\87à¤\9fाà¤\8fल à¤ªà¤¨à¥\8dनाà¤\95à¥\87à¤\81 à¤¤à¤¾à¤\95à¥\82",
-       "undelete-search-box": "मेटाएल पन्ना सभकेँ ताकू",
+       "undelete-search-title": "मà¥\87à¤\9fाà¤\8fल à¤\97à¥\87ल à¤ªà¥\83षà¥\8dठ à¤¤à¤¾à¤\95à¥\80",
+       "undelete-search-box": "मेटाएल पन्नासभ ताकी",
        "undelete-search-prefix": "से शुरु भेल पन्ना देखाबू.",
        "undelete-search-submit": "ताकू",
        "undelete-no-results": "एहेन पन्ना मेटाएल पेटारमे नै भेटल।",
        "namespace": "चेन्हासी समूह:",
        "invert": "उनटा चयन",
        "tooltip-invert": "ऐ बक्साकेँ सही करू पन्ना परिवर्तनकेँ नुकेबा लेल चयनित नामस्थानक भीतर (आ संग लागल नामस्थान जँ सही कएल अछि तखन)",
+       "tooltip-whatlinkshere-invert": "चुनल गेल नामस्थान पृष्ठसभ सँ लिङ्कसभ नुकाबैक लेल ई सन्दूकके चिन्हित करी",
        "namespace_association": "सम्बद्ध चेन्हासी",
        "tooltip-namespace_association": "ई बक्साकेँ सही करी जइसँ वार्ता आ विषय नामस्थान समाहित कएल जा सकए चुनल नामस्थानमे",
        "blanknamespace": "(मुख्य)",
        "sp-contributions-newbies": "मात्र नव खाताक योगदान देखाबी",
        "sp-contributions-newbies-sub": "नब प्रयोक्ताकऽ लेल",
        "sp-contributions-newbies-title": "नब प्रयोक्ताकऽ योगदान",
-       "sp-contributions-blocklog": "पà¥\8dरतिबनà¥\8dधित à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
-       "sp-contributions-suppresslog": "मेटाएल प्रयोक्ता योगदान सभ",
-       "sp-contributions-deleted": "प्रयोक्ताकऽ मेटाएल योगदान सभ",
+       "sp-contributions-blocklog": "पà¥\8dरतिबनà¥\8dधित à¤²à¥\8cà¤\97",
+       "sp-contributions-suppresslog": "{{GENDER:$1|प्रयोगकर्ता}} योगदान दबाबी",
+       "sp-contributions-deleted": "{{GENDER:$1|प्रयोगकर्ता}}क मेटाएल योगदान",
        "sp-contributions-uploads": "उपारोपण",
-       "sp-contributions-logs": "वà¥\83तà¥\8dतलà¥\87à¤\96 à¤¸à¤­",
+       "sp-contributions-logs": "लà¥\8cà¤\97",
        "sp-contributions-talk": "वार्त्ता",
        "sp-contributions-userrights": "प्रयोक्ता अधिकारकऽ प्रबन्धन",
        "sp-contributions-blocked-notice": "ई प्रयोक्ता अखन प्रतिबन्धित अछि।\nनव प्रतिबन्धित वृत्तलेख लेख सन्दर्भ नीचाँ देल अछि:",
        "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]] अप्रतिबन्धित कएल गेल",
        "contribslink": "योगदान",
        "emaillink": "ई-पत्र पठाउ",
        "autoblocker": "अहाँक अनिकेत \"[[User:$1|$1]]\" द्वारा प्रयोगक कारण स्वचालित रूपेँ प्रतिबन्धित भऽ गेल।\n$1 एकर प्रतिबन्धक कारण अछि : \"$2\"",
-       "blocklogpage": "पà¥\8dरतिबनà¥\8dधित à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
+       "blocklogpage": "पà¥\8dरतिबनà¥\8dधित à¤²à¥\8cà¤\97",
        "blocklog-showlog": "ऐ प्रयोक्ताकेँ पहिनहिये प्रतिबन्धित कऽ देल गेल अछि।\nप्रतिबन्धक वृत्तलेख सन्दर्भ लेल नीचाँ देल जा रहल अछि:",
        "blocklog-showsuppresslog": "ऐ प्रयोक्ताकेँ पहिनहिये प्रतिबन्धित आ अदृश्य कऽ देल गेल अछि।\nदबाएल वृत्तलेख सन्दर्भ लेल नीचाँ देल जा रहल अछि:",
        "blocklogentry": "प्रतिबन्धित [[$1]] एकर अन्तिम तिथि अछि $2 $3",
        "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": "पनà¥\8dना à¤\98सà¤\95ाà¤\89",
-       "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 जे अहाँ एकर परिणाम बुझै छी।",
+       "move-page": "$1 स्थानान्तरित करी",
+       "move-page-legend": "पà¥\83षà¥\8dठ à¤¸à¥\8dथानानà¥\8dतरण",
+       "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": "नव शीर्षकपर:",
-       "move-watch": "à¤\9cड़ि à¤ªà¤¨à¥\8dना à¤\86 à¤\9bà¥\80प à¤ªà¤¨à¥\8dना à¤¦à¥\87à¤\96à¥\82",
-       "movepagebtn": "पनà¥\8dना à¤\98सà¤\95ाबी",
+       "newtitle": "नव शीर्षक:",
+       "move-watch": "लिà¤\99à¥\8dà¤\95 à¤ªà¤¨à¥\8dना à¤\86 à¤²à¤\95à¥\8dषित à¤ªà¤¨à¥\8dना à¤¦à¥\87à¤\96à¥\80",
+       "movepagebtn": "नाम à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\95री",
        "pagemovedsub": "घसकल",
        "movepage-moved": "'''\"$1\" घसकाएल गेल \"$2\"''' पर",
        "movepage-moved-redirect": "एकटा पुनर्निर्देशन बनाओल गेल छै.",
        "movepage-moved-noredirect": "पुनर्निर्देशन नहि बनाओल गेल छै.",
        "articleexists": "ओइ नामक एकटा पन्ना पहिनहियेसँ अछि, वा जे नाम अहाँ चयन केने छी से वांछित नै अछि। \nकृपा कऽ दोसर नामक चयन करू।",
        "cantmove-titleprotected": "नब शीर्षक बनाबै  सें रोकहि के कारण, अहां अहि ठाम पर कोनो आन पृष्ठक ठाम बदलि नहि सकब.",
-       "movetalk": "सम्बन्धित वार्ता पृष्ठ सेहो घसकाबी",
+       "movetalk": "समà¥\8dबनà¥\8dधित à¤µà¤¾à¤°à¥\8dता à¤ªà¥\83षà¥\8dठ à¤¸à¥\87हà¥\8b à¤\98à¥\81सà¤\95ाबà¥\80",
        "move-subpages": "उपपृष्ठ सेहो लेल जाऊ ($1 धरि)",
        "move-talk-subpages": "वार्ता पृष्ठक उपपृष्ठ लेने जाऊ ($1 धरि)",
        "movepage-page-exists": "पन्ना $1 पहिनहियेसँ अछि आ स्वचालित रूपेँ मेटाएल नै जा सकैए।",
        "movepage-page-moved": "पन्ना $1 केँ $2 लग घसका देल गेल अछि।",
        "movepage-page-unmoved": "पन्ना $1 केँ $2 लग नै घसकाएल जा सकैए।",
        "movepage-max-pages": "बेसी सें बेसी $1 पृष्ठ बदलि के {{PLURAL:$1| क देल गेल अछि|क देल गेल अछि}}, आब आर पृष्ठ अपने आप नहि बदलत.",
-       "movelogpage": "स्थानान्तरण लग",
+       "movelogpage": "सà¥\8dथानानà¥\8dतरण à¤²à¥\8cà¤\97",
        "movelogpagetext": "नाम बदलल गेल लेख कऽ सूचि नीचां देल गेल अछि",
        "movesubpage": "{{PLURAL:$1|उप पन्ना|उप पन्ना}}",
        "movesubpagetext": "नीचां $1 {{PLURAL:$1| पन्ना देखाओल गएल अछि, जे अहि पन्नाकऽ उप पन्ना अछि|पन्ना देखावोल गएल अछि, जे अहि पन्नाकऽ उप पन्ना अछि}}।",
        "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": "नव संचिका विस्तारक अपन प्रकारसँ मेल नै खाइए।",
        "imageinvalidfilename": "लक्ष्यित संचिकाक नाम अवैध अछि",
        "fix-double-redirects": "मूल शीर्षक धरि जाहि बला सभटा पुनर्निर्देशनों के सेहो बदलु.",
-       "move-leave-redirect": "एकटा बदलेन के पांछा छोडि के जाऊ",
+       "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": "पूर्वनिर्धारित सन्देश पाठ",
        "allmessages-prefix": "उपसर्गक आधारपर छाँटू:",
        "allmessages-language": "भाषा:",
        "allmessages-filter-submit": "चलू",
-       "allmessages-filter-translate": "à¤\85नà¥\81वाद à¤\95रà¥\81",
+       "allmessages-filter-translate": "à¤\85नà¥\81वाद à¤\95रà¥\80",
        "thumbnail-more": "पैग",
        "filemissing": "संचिका हेराएल",
        "thumbnail_error": "लघुचित्र निर्माण कालमे भ्रम:$1",
        "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": "हालमे भेल परिवर्तनक परीक्षण अक्षम अछि",
        "markedaspatrollederror-noautopatrol": "अहाँ अपन कएल संशोधनकेँ संचालित नै कहि सकै छी।",
        "markedaspatrollednotify": "$1 पृष्ठ में कएल गएल ऐ परिवर्तन जाँचल गेल चिन्हासी कएल गेल।",
        "markedaspatrollederrornotify": "जाँचल चिन्हासी असफल भेल।",
-       "patrol-log-page": "सà¤\82à¤\9aालन à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
-       "patrol-log-header": "à¤\88 à¤¸à¤\82à¤\9aालित à¤¸à¤\82शà¥\8bधन à¤¸à¤­à¤\95 à¤µà¥\83तà¥\8dतलà¥\87à¤\96 छी।",
-       "log-show-hide-patrol": "$1 à¤¨à¤¿à¤°à¥\80à¤\95à¥\8dषण à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
+       "patrol-log-page": "परà¥\80à¤\95à¥\8dषण à¤²à¥\8cà¤\97",
+       "patrol-log-header": "à¤\88 à¤ªà¤°à¥\80à¤\95à¥\8dषित à¤\85वतरणसभà¤\95 à¤²à¥\8cà¤\97 छी।",
+       "log-show-hide-patrol": "$1 à¤¨à¤¿à¤°à¥\80à¤\95à¥\8dषण à¤²à¥\8cà¤\97",
        "log-show-hide-tag": "$1 ट्याग लग",
        "deletedrevision": "पुरान संशोधन $1 हटा देलौं",
        "filedeleteerror-short": "संचिका मेटेबामे भ्रम : $1",
        "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": "विस्तृत विवरण नुकाउ",
        "exif-orientation-1": "सामान्य",
        "exif-orientation-2": "अनुदैर्घ्य मिज्झर",
        "exif-orientation-3": "180° पर घुमायल गेल",
-       "exif-orientation-4": "à¤\85नà¥\81पà¥\8dरसà¥\8dथ à¤®à¤¿à¤\9cà¥\8dà¤\9dर",
+       "exif-orientation-4": "भरà¥\8dà¤\9fिà¤\95लà¥\80 à¤«à¥\8dलिप à¤\95रà¥\80",
        "exif-orientation-5": "90° सी.सी.डब्लू. घुमाओल गेल आ अनुप्रस्थ रूपेँ मिज्झर कएल गेल",
        "exif-orientation-6": "९०° सी.सी.डब्लू. घुमाएल गेल",
        "exif-orientation-7": "९०° सी.डब्लू. घुमाओल गेल आ अनुप्रस्थ रूपेँ मिज्झर कएल गेल",
        "exif-customrendered-0": "सामान्य प्रक्रिया",
        "exif-customrendered-1": "वैकल्पिक प्रक्रिया",
        "exif-exposuremode-0": "स्वयं देखबैत",
-       "exif-exposuremode-1": "सà¤\82à¤\9aालित à¤¦à¥\87à¤\96ाà¤\8fब",
+       "exif-exposuremode-1": "मà¥\88नà¥\8dयà¥\81à¤\85ल à¤\8fà¤\95à¥\8dपà¥\8bà¤\9cर",
        "exif-exposuremode-2": "स्वचालित कोष्ठक",
        "exif-whitebalance-0": "स्वचालित उज्जर सन्तुलन",
        "exif-whitebalance-1": "संचालित उज्जर सन्तुलन",
        "confirm-watch-top": "ऐ पन्नाकेँ अपन साकांक्ष सूचीमे जोड़ू",
        "confirm-unwatch-button": "ठीक अछि",
        "confirm-unwatch-top": "ऐ पन्नाकेँ हमर साकांक्ष सूचीसँ हटाउ",
+       "confirm-rollback-button": "ठीक अछि",
+       "confirm-rollback-top": "ई पृष्ठ सम्पादन पूर्ववत करी?",
        "quotation-marks": "\"$1\"",
        "imgmultipageprev": "← पहिलुका पृष्ठ",
        "imgmultipagenext": "अगुलका पृष्ठ →",
        "imgmultigoto": "$1 पृष्ठ पर जाए",
        "img-lang-default": "(डिफल्ट भाषा)",
        "img-lang-info": "ई चित्र को $1. $2 में ढालु",
-       "img-lang-go": "à¤\9cाà¤\8a",
+       "img-lang-go": "à¤\9cाà¤\8f",
        "ascending_abbrev": "asc",
        "descending_abbrev": "जानकारी",
        "table_pager_next": "अगला पृष्ठ",
        "table_pager_limit_label": "सामग्री प्रति पृष्ठ",
        "table_pager_limit_submit": "जाए",
        "table_pager_empty": "कोनो परिणाम नहि",
-       "autosumm-blank": "पà¥\83षà¥\8dठ à¤\95à¥\87 à¤\96ालà¥\80 à¤\95रल गेल",
+       "autosumm-blank": "पà¥\83षà¥\8dठ à¤\96ालà¥\80 à¤\95à¤\8fल गेल",
        "autosumm-replace": "\"$1\" सहित पाठ परिवर्तित भेल",
        "autoredircomment": "[[$1]] के अनुप्रेषित",
        "autosumm-new": "'$1' संग नब पृष्ठ बनाओल गेल",
        "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-legend": "साà¤\95ाà¤\82à¤\95à¥\8dष-सà¥\82à¤\9aà¥\80 à¤®à¥\87à¤\9fाà¤\89",
+       "watchlistedit-clear-title": "धà¥\8dयानसà¥\82à¤\9aà¥\80 à¤\96ालà¥\80 à¤\95रà¥\80",
+       "watchlistedit-clear-legend": "साà¤\95ाà¤\82à¤\95à¥\8dष-सà¥\82à¤\9aà¥\80 à¤®à¥\87à¤\9fाबà¥\80",
        "watchlistedit-clear-explain": "एही ठाम रहल सभ शिर्षक अहाँक साकांक्ष-सूची से मेटा जाएत",
        "watchlistedit-clear-titles": "शीर्षक",
        "watchlistedit-clear-submit": "साकांक्ष-सूची मेटाउ (ई स्थायी छि!)",
        "watchlistedit-clear-done": "अहाँक साकांक्ष-सूची मेटाओल गेल।",
        "watchlistedit-clear-removed": "{{PLURAL:$1|1 शीर्षक छल|$1शीर्षक सभ रहए}} हटाएल गेल:",
-       "watchlistedit-too-many": "à¤\8fतà¥\87à¤\95 à¤¬à¤¹à¥\81त à¤°à¤¾à¤¸ à¤ªà¤¨à¥\8dना à¤¸à¤­ à¤¦à¥\87à¤\96ावà¥\8bल à¤\9cाà¤\8fत।",
-       "watchlisttools-clear": "साà¤\95ाà¤\82à¤\95à¥\8dष-सà¥\82à¤\9aà¥\80 à¤®à¥\87à¤\9fाà¤\89",
+       "watchlistedit-too-many": "à¤\8fतय à¤¦à¤°à¥\8dशावà¥\88à¤\95 à¤²à¥\87ल à¤\85तà¥\8dयधिà¤\95 à¤ªà¥\83षà¥\8dठ à¤\85à¤\9bि।",
+       "watchlisttools-clear": "साà¤\95ाà¤\82à¤\95à¥\8dष-सà¥\82à¤\9aà¥\80 à¤®à¥\87à¤\9fाबà¥\80",
        "watchlisttools-view": "सम्बन्धित परिवर्तन सभकेँ देखी",
        "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]]।",
-       "redirect-submit": "जाऊ",
+       "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-user": "प्रयोक्ता आई॰डी॰",
        "redirect-page": "पन्ना आई॰डी॰",
        "redirect-revision": "पन्ना अवतरण संख्या",
        "redirect-file": "फाइल नाम",
+       "redirect-logid": "प्रवेश आइडी",
        "redirect-not-exists": "बैनर नैं मिल्ल",
        "fileduplicatesearch": "द्वितीयक संचिका ताकू",
        "fileduplicatesearch-summary": "हैश मानक आधारपर द्वितीयक संचिका ताकू।",
        "specialpages-group-maintenance": "सुस्थापन प्रतिवेदन",
        "specialpages-group-other": "दोसर विशेष पन्ना",
        "specialpages-group-login": "सम्प्रवेश/ सम्प्रवेश आवेदन",
-       "specialpages-group-changes": "हालà¤\95 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\86 à¤µà¥\83तà¥\8dतलà¥\87à¤\96",
+       "specialpages-group-changes": "सनà¥\8dनिà¤\95à¤\9f à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\86 à¤²à¥\8cà¤\97",
        "specialpages-group-media": "मीडिया प्रतिवेदन आ उपारोपण",
        "specialpages-group-users": "प्रयोक्ता आ अधिकार",
-       "specialpages-group-highuse": "बà¥\87सà¥\80 à¤ªà¥\8dरयà¥\8bà¤\97बला à¤ªà¤¨à¥\8dना à¤¸à¤­",
+       "specialpages-group-highuse": "à¤\85तà¥\8dयधिà¤\95 à¤\89पयà¥\8bà¤\97à¥\80 à¤ªà¥\83षà¥\8dठ",
        "specialpages-group-pages": "पन्ना सभक सूची",
        "specialpages-group-pagetools": "पन्नाक औजार सभ",
        "specialpages-group-wiki": "विकी दत्तांश आ औजार सभ",
        "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-display-header": "परिवरà¥\8dतन à¤¸à¥\82à¤\9aà¥\80 à¤¸à¤­à¤\95 à¤°à¥\82परà¤\82à¤\97",
+       "tags-display-header": "परिवरà¥\8dतन à¤¸à¥\82à¤\9aिसभमà¥\87 à¤ªà¥\8dरदरà¥\8dशन",
        "tags-description-header": "अर्थक पूर्ण विवरण",
        "tags-source-header": "स्रोत",
        "tags-active-header": "सक्रिय?",
        "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-title": "ट्याग सम्पादन",
        "tags-edit-manage-link": "ट्याग व्यवस्थापन",
        "tags-edit-revision-selected": "[[:$2]] {{PLURAL:$1|क|के}} चयनित अवतरण:",
-       "tags-edit-logentry-selected": "{{PLURAL:$1|à¤\9aà¥\81नल à¤µà¥\83तà¥\8dतलà¥\87à¤\96 à¤\98à¤\9fना|à¤\9aà¥\81नल à¤µà¥\83तà¥\8dतलà¥\87à¤\96 à¤\98à¤\9fना सभ}}:",
-       "tags-edit-existing-tags-none": "''कोनो नै''",
+       "tags-edit-logentry-selected": "{{PLURAL:$1|à¤\9aà¥\81नल à¤²à¥\8cà¤\97 à¤\98à¤\9fना|à¤\9aà¥\81नल à¤²à¥\8cà¤\97 à¤\98à¤\9fनासभ}}:",
+       "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": "ऐ बीचमे अहाँ गूगलसँ खोज कऽ सकै छी।",
        "htmlform-chosen-placeholder": "एकटा विकल्प चुनु",
        "htmlform-cloner-create": "आर जोडु",
        "htmlform-cloner-delete": "हटाउ",
-       "sqlite-has-fts": "$1 पूर्ण-पाठ खोज सहायता युक्त",
-       "sqlite-no-fts": "$1 बिन पूर्ण-पाठ खोज सहायताक",
        "logentry-delete-delete": "$1 पृष्ठ $3 {{GENDER:$2|मेटौलक}}",
        "logentry-delete-restore": "$1 {{GENDER:$2|restored}} page $3",
-       "logentry-delete-event": "$1 {{GENDER:$2|changed}} एकर दृश्य{{PLURAL:$5| एकटा वृत्तलेख|$5 वृत्तलेख}}  $3: $4 केँ",
-       "logentry-delete-revision": "$1 {{GENDER:$2|परिवर्तन कियल गैल}} एकर दृश्य{{PLURAL:$5| एकटा संशोधन|$5 संशोधन}}  पन्ना $3: $4 पर",
-       "logentry-delete-event-legacy": "$1 {{GENDER:$2|changed}}  $3 पर वृत्तलेख दृश्य",
-       "logentry-delete-revision-legacy": "$1 {{GENDER:$2|changed}}  $3 पर वृत्तलेख संशोधन",
+       "logentry-delete-event": "$1द्वारा $3 पृष्ठक लौग {{PLURAL:$5|प्रविष्टि|प्रविष्टिसभ}}क दृश्यता {{GENDER:$2|परिवर्तित केलक}}: $4",
+       "logentry-delete-revision": "$1 द्वारा $3 पृष्ठक {{PLURAL:$5|एक अवतरण|$5 अवतरणसभ}}क दृश्यता {{GENDER:$2|परिवर्तित}}: $4",
+       "logentry-delete-event-legacy": "$1द्वारा $3 पृष्ठ पर लौग क्रियासभक दृश्यता {{GENDER:$2|परिवर्तित केलक}}",
+       "logentry-delete-revision-legacy": "$1द्वारा $3 पृष्ठ पर अवतरणसभक दृश्यता {{GENDER:$2|परिवर्तित केलक}}",
        "logentry-suppress-delete": "$1 {{GENDER:$2|दबाएल}} page $3",
        "logentry-suppress-event": "$1 चोरिसँ {{GENDER:$2|परिवर्तन कियल गैल}} एकर दृश्य{{PLURAL:$5| एकटा वृत्तलेख|$5 वृत्तलेख}}  $3: $4 पर",
        "logentry-suppress-revision": "$1 चोरिसँ {{GENDER:$2|changed}} एकर दृश्य{{PLURAL:$5| एकटा संशोधन|$5 संशोधन}}  $3: $4 पर",
-       "logentry-suppress-event-legacy": "$1 नुका क {{GENDER:$2|परिवर्तन}}  $3 पर वृत्तलेख दृश्य",
+       "logentry-suppress-event-legacy": "$1द्वारा गुप्त रूपसँ $3 पृष्ठ पर लौग क्रियासभक दृश्यता {{GENDER:$2|परिवर्तित केलक}}",
        "logentry-suppress-revision-legacy": "$1 नुका कऽ {{GENDER:$2|changed}}  $3 पर संशोधन दृश्य",
        "revdelete-content-hid": "सामिग्री नुकाएल",
        "revdelete-summary-hid": "नुकाएल सारांश सम्पादन",
        "logentry-import-interwiki": "$1 {{GENDER:$2|आयात केल गेल}} $3 कोनो और विकि सँ",
        "logentry-merge-merge": "$1 {{GENDER:$2|विलय केल गेल}} $3 के $4 में (संशोधन $5 धरि)",
        "logentry-move-move": "$1द्वारा $3 पृष्ठ $4 पर {{GENDER:$2|स्थानान्तरित}} कएलक",
-       "logentry-move-move-noredirect": "$1 {{GENDER:$2|हटाएल}} पन्ना $3 सँ $4 घुमौआकेँ बिना छोड़ने",
-       "logentry-move-move_redir": "$1 {{GENDER:$2|हटाएल}} पन्ना $3 सँ $4 घुमौआक अतिरिक्त",
-       "logentry-move-move_redir-noredirect": "$1 {{GENDER:$2|हटाएल}} पन्ना $3 सँ $4 घुमौआक अतितिक्त घुमौआकेँ बिना छोड़ने",
+       "logentry-move-move-noredirect": "$1 द्वारा $3 पर पुनर्निर्देशन नै छोडि ओकरा $4 पर {{GENDER:$2|स्थानान्तरित}} केलक",
+       "logentry-move-move_redir": "$1 द्वारा $4 सँ पुनर्निर्देशन हटाए $3 क ओहिपर {{GENDER:$2|स्थानान्तरित}} केलक",
+       "logentry-move-move_redir-noredirect": "$1 द्वारा $4 सँ पुनार्निर्देश हटाए $3 पर पुनर्निर्देश नै छोडि $3 के $4 पर {{GENDER:$2|स्थानान्तरित}} केलक",
        "logentry-patrol-patrol": "$1 {{GENDER:$2|चिन्हित}} संशोधन $4 $3 पन्नाक निरीक्षित",
        "logentry-patrol-patrol-auto": "$1 स्वतः {{GENDER:$2|चिन्हित}} संशोधन $4 $3 पन्नाक निरीक्षित",
        "logentry-newusers-newusers": "$1 {{GENDER:$2|बनाएल}} एकटा प्रयोक्ता खाता",
        "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": "पन्ना उपर प्रतिक्रिया जोडु ...",
        "feedback-back": "पाछां",
        "feedback-bugcheck": "बहुत निक! जांच करु कि [ $1 known bugs] पहिले स त नै अछि ।",
        "feedback-bugnew": "हम जाँच केलौ। एक नव बग रिपोर्ट करी",
-       "feedback-cancel": "रदà¥\8dद à¤\95रà¥\81",
+       "feedback-cancel": "रदà¥\8dद à¤\95रà¥\80",
        "feedback-close": "भ गेल",
-       "feedback-error-title": "त्रुटि",
        "feedback-error1": "त्रुटि: नै पहचानल गेल परिणाम एपीआईसँ",
        "feedback-error2": "त्रुटि: संपादन विफल भेल",
        "feedback-error3": "त्रुटि:एपीआईसँग कोनो प्रतिक्रिया नै",
        "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 7f522e6..eca8513 100644 (file)
@@ -47,7 +47,7 @@
        "tog-enotifminoredits": "Испраќај ми е-пошта и за ситни промени во страниците и податотеките",
        "tog-enotifrevealaddr": "Откриј ја мојата е-поштенска адреса во пораките за известување",
        "tog-shownumberswatching": "Прикажи го бројот на корисници кои набљудуваат",
-       "tog-oldsig": "Ð\9fостоечки потпис:",
+       "tog-oldsig": "Ð\92аÑ\88иоÑ\82 Ð¿остоечки потпис:",
        "tog-fancysig": "Сметај го потписот за викитекст (без автоматска врска)",
        "tog-uselivepreview": "Користи преглед во живо",
        "tog-forceeditsummary": "Извести ме кога нема опис на промените",
        "newwindow": "(се отвора во нов прозорец)",
        "cancel": "Откажи",
        "moredotdotdot": "Повеќе...",
-       "morenotlisted": "Ð\9eвоÑ\98 Ñ\81пиÑ\81ок Ð½Ðµ Ðµ целосен.",
+       "morenotlisted": "Ð\9eвоÑ\98 Ñ\81пиÑ\81ок Ð¼Ð¾Ð¶Ðµ Ð´Ð° Ðµ Ð½Ðµцелосен.",
        "mypage": "Страница",
        "mytalk": "разговор",
        "anontalk": "Разговор",
        "talk": "Разговор",
        "views": "Посети",
        "toolbox": "Алатки",
+       "tool-link-userrights": "Смени ги {{GENDER:$1|корисничките}} групи",
+       "tool-link-emailuser": "Испрати е-пошта на {{GENDER:$1|корисников}}",
        "userpage": "Преглед на корисничката страница",
        "projectpage": "Преглед на проектната страница",
        "imagepage": "Преглед на страницата на податотеката",
        "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": "Менувањето на лозинката не успеа. Можеби не е поставен услужник?",
        "searchprofile-advanced-tooltip": "Пребарување во именски простори по избор",
        "search-result-size": "$1 ({{PLURAL:$2|еден збор|$2 збора}})",
        "search-result-category-size": "{{PLURAL:$1|1 член|$1 членови}} ({{PLURAL:$2|1 поткатегорија|$2 поткатегории}}, {{PLURAL:$3|1 податотека|$3 податотеки}})",
-       "search-redirect": "(пренасочување $1)",
+       "search-redirect": "(пренасочување од $1)",
        "search-section": "(пасус $1)",
        "search-category": "(категорија $1)",
        "search-file-match": "(се совпаѓа со содржината на податотеката)",
        "upload-dialog-disabled": "Подигањето на податотеки со помош на овој дијалог е оневозможено на ова вики.",
        "upload-dialog-title": "Подигни податотека",
        "upload-dialog-button-cancel": "Откажи",
+       "upload-dialog-button-back": "Назад",
        "upload-dialog-button-done": "Готово",
        "upload-dialog-button-save": "Зачувај",
        "upload-dialog-button-upload": "Подигни",
        "apisandbox-results-fixtoken-fail": "Не успеав да ја добијам шифрата „$1“.",
        "apisandbox-alert-page": "Полињата на страницава се неважечки.",
        "apisandbox-alert-field": "Вредноста на полево е неважечка.",
+       "apisandbox-continue": "Продолжи",
+       "apisandbox-continue-clear": "Исчисти",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} ќе [https://www.mediawiki.org/wiki/API:Query#Continuing_queries продолжи] со последното барање; „{{int:apisandbox-continue-clear}}“ ќе ги исчисти параметрите поврзани со продолжување.",
        "booksources": "Печатени извори",
        "booksources-search-legend": "Пребарување на извори за книга",
        "booksources-isbn": "ISBN:",
        "htmlform-cloner-create": "Додај уште",
        "htmlform-cloner-delete": "Отстрани",
        "htmlform-cloner-required": "Се бара барем една вредност.",
+       "htmlform-date-placeholder": "ГГГГ-ММ-ДД",
+       "htmlform-time-placeholder": "ЧЧ:ММ:СС",
+       "htmlform-datetime-placeholder": "ГГГГ-ММ-ДД ЧЧ:ММ:СС",
+       "htmlform-date-invalid": "Не можам да ја препознаам внесената вредност. Користете го форматот ГГГГ-ММ-ДД.",
+       "htmlform-time-invalid": "Не можам да ја препознаам внесената вредност за време. Користете го форматот ЧЧ:ММ:СС.",
+       "htmlform-datetime-invalid": "Не можам да ја препознаам внесената вредност за датум и време. Користете го форматот ГГГГ-ММ-ДД ММ:СС.",
+       "htmlform-date-toolow": "Укажаната вредност е пред најраниот допуштен датум — $1.",
+       "htmlform-date-toohigh": "Укажаната вредност е по најдоцниот допуштен датум — $1.",
+       "htmlform-time-toolow": "Укажаната вредност е пред најраното допуштено време — $1.",
+       "htmlform-time-toohigh": "Укажаната вредност е по најдоцното допуштено време — $1.",
+       "htmlform-datetime-toolow": "Укажаната вредност е пред најраниот допуштен датум и време — $1.",
+       "htmlform-datetime-toohigh": "Укажаната вредност е по најдоцниот допуштен датум и време — $1.",
        "htmlform-title-badnamespace": "[[:$1]] не се наоѓа во именскиот простор „{{ns:$2}}“.",
        "htmlform-title-not-creatable": "Насловот „$1“ не може да се создава",
        "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 {{GENDER:$2|ја избриша}} страницата $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|ја возобнови}} страницата $3",
        "logentry-delete-event": "$1 {{GENDER:$2|ја измени}} видливоста на {{PLURAL:$5|настан во дневникот|$5 настани во дневникот}} на $3: $4",
        "feedback-external-bug-report-button": "Поднеси техничка задача",
        "feedback-dialog-title": "Поднеси мислење",
        "feedback-dialog-intro": "Послужете се со едноставниот образец подолу за да го поднесете вашето мислење. Коментарот ќе ви биде додаден на страницата „$1“, заедно со вашето корисничко име.",
-       "feedback-error-title": "Грешка",
        "feedback-error1": "Грешка: Непрепознаен резултат од извршникот",
        "feedback-error2": "Грешка: Уредувањето не успеа",
        "feedback-error3": "Грешка: Извршникот не одговара",
        "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 533ebef..0d18c0d 100644 (file)
@@ -77,7 +77,7 @@
        "tog-enotifminoredits": "मला पानांच्या आणि संचिकांच्या छोट्या बदलांकरीता सुद्धा विरोप पाठवा",
        "tog-enotifrevealaddr": "सूचना विरोपात माझा विरोपाचा (ई-मेल ) पत्ता दाखवा",
        "tog-shownumberswatching": "पहारा देणाऱ्या सदस्यांचा आकडा दाखवा",
-       "tog-oldsig": "सध्याची सही:",
+       "tog-oldsig": "à¤\86पलà¥\80 à¤¸à¤§à¥\8dयाà¤\9aà¥\80 à¤¸à¤¹à¥\80:",
        "tog-fancysig": "सही विकिसंज्ञा म्हणून वापरा (आपोआप दुव्याशिवाय)",
        "tog-uselivepreview": "सजीव झलक दाखवा",
        "tog-forceeditsummary": "जर ’बदलांचा आढावा’ दिला नसेल तर मला सूचित करा",
@@ -94,7 +94,7 @@
        "tog-showhiddencats": "लपविलेले वर्ग दाखवा",
        "tog-norollbackdiff": "द्रुतमाघार घेतल्यास बदल वगळा",
        "tog-useeditwarning": "जर मी संपादित करीत असलेल्या पानावरील माझे संपादिलेले बदल जतन न केल्यास मला इशारा द्या",
-       "tog-prefershttps": "सनोंद प्रवेशतांना प्रत्येक वेळी  सुरक्षित अनुबंध वापरा",
+       "tog-prefershttps": "सनà¥\8bà¤\82द à¤ªà¥\8dरवà¥\87शित à¤\85सताà¤\82ना à¤ªà¥\8dरतà¥\8dयà¥\87à¤\95 à¤µà¥\87ळà¥\80  à¤¸à¥\81रà¤\95à¥\8dषित à¤\85नà¥\81बà¤\82ध à¤µà¤¾à¤ªà¤°à¤¾",
        "underline-always": "नेहमी",
        "underline-never": "कधीच नाही",
        "underline-default": "त्वचा अथवा न्याहाळक अविचल (स्कीन अथवा ब्राऊजर डिफॉल्ट)",
        "category-file-count-limited": "खालील {{PLURAL:$1|संचिका|$1 संचिका}} या वर्गात आहेत.",
        "listingcontinuesabbrev": "पुढे चला",
        "index-category": "अनुक्रमित पाने",
-       "noindex-category": "à¤\85नà¥\81à¤\95à¥\8dरम à¤¨à¤¸à¤²à¥\87लà¥\80 पाने",
+       "noindex-category": "विना-à¤\85नà¥\81à¤\95à¥\8dरमित पाने",
        "broken-file-category": "तुटलेल्या संचिका दुव्यांसह असलेली पाने",
        "about": "च्या विषयी",
        "article": "आशयाचे पान",
        "newwindow": "(नवीन खिडकीत उघडते.)",
        "cancel": "रद्द करा",
        "moredotdotdot": "अजून...",
-       "morenotlisted": "हà¥\80 à¤¯à¤¾à¤¦à¥\80 à¤ªà¥\82रà¥\8dण à¤¨à¤¾à¤¹à¥\80.",
+       "morenotlisted": "हà¥\80 à¤¯à¤¾à¤¦à¥\80 à¤\85पà¥\82रà¥\8dण à¤\85सà¥\82 à¤¶à¤\95तà¥\87.",
        "mypage": "पान",
        "mytalk": "चर्चा",
        "anontalk": "चर्चा पान",
        "talk": "चर्चा",
        "views": "दृष्ये",
        "toolbox": "साधने",
+       "tool-link-emailuser": "{{GENDER:$1|सदस्याला}} विपत्र पाठवा",
        "userpage": "सदस्य पृष्ठ",
        "projectpage": "प्रकल्प पान पहा",
        "imagepage": "संचिका पृष्ठ पहा",
        "createacct-yourpasswordagain-ph": "पुन्हा परवलीचा शब्द टाका",
        "userlogin-remembermypassword": "मला नोंदीकृतच(लॉग्ड-ईन) ठेवा",
        "userlogin-signwithsecure": "सुरक्षित अनुबंध(सेक्युअर कनेक्शन) वापरा",
+       "cannotlogin-title": "सनोंद प्रवेश करु शकत नाही",
        "cannotloginnow-title": "आता सनोंद प्रवेश घेऊ शकत नाही",
        "cannotloginnow-text": "$1 वापरत असतांना सनोंद प्रवेश करणे शक्य नाही.",
        "yourdomainname": "तुमचे क्षेत्र (डोमेन) :",
        "eauthentsent": "नमूद केलेल्या ई-मेल पत्त्यावर एक निश्चितता स्वीकारक ई-मेल पाठविला गेला आहे.\nखात्यावर कोणताही इतर ई-मेल पाठविण्यापूर्वी - तो ई-मेल पत्ता तुमचाच आहे, हे सुनिश्चित करण्यासाठी - तुम्हाला त्या ई-मेल मधील सूचनांचे पालन करावे लागेल.",
        "throttled-mailpassword": "मागील {{PLURAL:$1|तासात|$1 तासांत}} परवलीचा शब्द बदलण्यासाठीची सूचना विपत्राद्वारे पाठविलेली आहे. दुरुपयोग टाळण्यासाठी, {{PLURAL:$1|एका तासामध्ये|$1 तासांमध्ये}} फक्त एकदाच सूचना दिली जाईल.",
        "mailerror": "विपत्र पाठवण्यात त्रुटी: $1",
-       "acct_creation_throttle_hit": "à¤\86पला à¤\85à¤\82à¤\95पतà¥\8dता à¤µà¤¾à¤ªà¤°à¥\81न à¤¯à¤¾ à¤µà¤¿à¤\95िस à¤­à¥\87à¤\9f à¤¦à¥\87णाऱà¥\8dयाà¤\82नà¥\80 à¤\95ाल {{PLURAL:$1|१ à¤\96ातà¥\87|$1 à¤\96ातà¥\80}} à¤\89à¤\98डलà¥\80 à¤\86हà¥\87त à¤¤à¥\80 à¤¯à¤¾ à¤\95ालावधà¥\80तà¥\80ल à¤®à¤¹à¤¤à¥\8dतम à¤\86हà¥\87त.\n\nतà¥\8dयाà¤\9aा à¤ªà¤°à¤¿à¤ªà¤¾à¤\95 à¤®à¥\8dहणà¥\82न à¤¸à¤§à¥\8dया à¤¹à¤¾ à¤\85à¤\82à¤\95पतà¥\8dता à¤µà¤¾à¤ªà¤°à¥\81न à¤­à¥\87à¤\9f à¤¦à¥\87णाऱà¥\8dयास अधिक खाते उघडता येणार नाहीत.",
+       "acct_creation_throttle_hit": "à¤\86पला à¤\85à¤\82à¤\95पतà¥\8dता à¤µà¤¾à¤ªà¤°à¥\81न à¤¯à¤¾ à¤µà¤¿à¤\95िस à¤­à¥\87à¤\9f à¤¦à¥\87णाऱà¥\8dयाà¤\82नà¥\80 à¤®à¤¾à¤\97à¥\80ल $2 à¤®à¤§à¥\8dयà¥\87 {{PLURAL:$1|१ à¤\96ातà¥\87|$1 à¤\96ातà¥\80}} à¤\89à¤\98डलà¥\80 à¤\86हà¥\87त à¤¤à¥\80 à¤¯à¤¾ à¤\95ालावधà¥\80तà¥\80ल à¤®à¤¹à¤¤à¥\8dतम à¤\86हà¥\87त.\n\nतà¥\8dयाà¤\9aा à¤ªà¤°à¤¿à¤ªà¤¾à¤\95 à¤®à¥\8dहणà¥\82न à¤¸à¤§à¥\8dया à¤¹à¤¾ à¤\85à¤\82à¤\95पतà¥\8dता à¤µà¤¾à¤ªà¤°à¥\81न à¤­à¥\87à¤\9f à¤¦à¥\87णाऱà¥\8dयाला अधिक खाते उघडता येणार नाहीत.",
        "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\" हे सांगकाम्याचे नाव जोडण्यात अयशस्वी. ते पूर्वीच जोडले होते काय?",
        "searchprofile-advanced-tooltip": "पारंपरित(कस्टम) नामविश्वांमध्ये शोधा",
        "search-result-size": "$1 ({{PLURAL:$2|१ शब्द|$2 शब्द}})",
        "search-result-category-size": "{{PLURAL:$1|१ सदस्य|$1 सदस्य}} ({{PLURAL:$2|१ उपवर्ग|$2 उपवर्ग}}, {{PLURAL:$3|1 संचिका|$3 संचिका}})",
-       "search-redirect": "(पुनर्निर्देशन $1)",
+       "search-redirect": "($1 पासून पुनर्निर्देशन)",
        "search-section": "(विभाग $1)",
        "search-category": "(वर्ग $1)",
        "search-file-match": "(संचिका आशयाशी अनुरुपते)",
        "upload-dialog-disabled": "हा डायलॉग वापरून  या विकिवर संचिका अपभारण अक्षम केले आहे.",
        "upload-dialog-title": "संचिकेचे अपभारण करा",
        "upload-dialog-button-cancel": "रद्द करा",
+       "upload-dialog-button-back": "परत जा",
        "upload-dialog-button-done": "झाले",
        "upload-dialog-button-save": "जतन करा",
        "upload-dialog-button-upload": "अपभारण करा",
        "pageinfo-article-id": "पृष्ठ-परिचय",
        "pageinfo-language": "पानाच्या मजकूराची भाषा",
        "pageinfo-content-model": "पान आशय नमूना",
+       "pageinfo-content-model-change": "बदला",
        "pageinfo-robot-policy": "यंत्रमानवाद्वारे अनुक्रमन",
        "pageinfo-robot-index": "अनुमती दिली",
        "pageinfo-robot-noindex": "अनुमती दिल्या जात नाही",
        "tag-filter": "[[Special:Tags|खूणपताका]] गाळक:",
        "tag-filter-submit": "गाळक",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|खूणपताका}}]]: $2)",
-       "tags-title": "खुणा",
-       "tags-intro": "प्रणालीतून विशिष्ट संपादनांच्या अर्थासहित  खुणांची  यादी नमूद करणारे पान",
+       "tags-title": "à¤\96à¥\81णपताà¤\95ा",
+       "tags-intro": "प्रणालीतून, विशिष्ट संपादनांच्या अर्थासहित  खुणपताकांची  यादी नमूद करणारे पान",
        "tags-tag": "खूण नाव",
        "tags-display-header": "बदल सुचीवर कसे दिसेल",
        "tags-description-header": "अर्थाची पूर्ण माहिती",
        "tags-hitcount": "$1 {{PLURAL:$1|बदल|बदल}}",
        "tags-manage-blocked": "आपण प्रतिबंधित असतांना बदल खूणपताकांचे व्यवस्थापन करु शकत नाही.",
        "tags-create-heading": "नवीन बिल्ला तयार करा",
+       "tags-create-explanation": "अविचलरित्या, नविन तयार केलेल्या खुणपताका सदस्यांना व सांगकाम्यांना वापरासाठी उपलब्ध होतील.",
        "tags-create-tag-name": "खूणपताकेचे नाव:",
        "tags-create-reason": "कारण:",
        "tags-create-submit": "निर्मित करा",
        "htmlform-cloner-create": "अधिक जोडा",
        "htmlform-cloner-delete": "हटवा",
        "htmlform-cloner-required": "किमान एक किंमत हवी",
+       "htmlform-date-placeholder": "वववव-मम-दिदि",
+       "htmlform-time-placeholder": "ताता:मिमि:सेसे",
+       "htmlform-datetime-placeholder": "वववव-मम-दिदि ताता:मिमि:सेसे",
+       "htmlform-date-invalid": "आपण नमूद केलेली किंमत ही अनोळखी दिनांक आहे. वववव-मम-दिदि प्रारुपणाचा वापर करण्याबाबत विचार करा.",
+       "htmlform-time-invalid": "आपण नमूद केलेली किंमत ही अनोळखी आहे. ताता:मिमि:सेसे प्रारुपणाचा वापर करण्याबाबत विचार करा.",
+       "htmlform-datetime-invalid": "आपण नमूद केलेली किंमत ही अनोळखी दिनांक व वेळ आहे. वववव-मम-दिदि ताता:मिमि:सेसे प्रारुपणाचा वापर करण्याबाबत विचार करा.",
+       "htmlform-time-toohigh": "आपण नमूद केलेली किंमत ही $1च्या परवानगी असलेल्या वेळ मर्यादेबाहेर आहे.",
+       "htmlform-datetime-toohigh": "आपण नमूद केलेली किंमत ही $1च्या परवानगी असलेल्या दिनांक व वेळ मर्यादेबाहेर आहे.",
        "htmlform-title-badnamespace": "[[:$1]] हे \"{{ns:$2}}\" नामविश्वात नाही.",
        "htmlform-title-not-creatable": "\"$1\" हे पान तयार करण्यासाठीचे शीर्षक नाही",
        "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 {{GENDER:$2|वगळलेले पान}} $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|पुनर्स्थापित पृष्ठ}} $3",
        "logentry-delete-event": "$1 ने $3 वर{{PLURAL:$5|नोंद-प्रसंग|$5 नोंद प्रसंगांची}} दृष्यता{{GENDER:$2|बदलली}}:$4",
        "feedback-cancel": "रद्द करा",
        "feedback-close": "झाले",
        "feedback-dialog-title": "प्रतिक्रिया सादर करा",
-       "feedback-error-title": "चूक",
        "feedback-error1": "चूक: API कडून अनोळखी परिणाम",
        "feedback-error2": "त्रुटी: संपादन रद्द",
        "feedback-error3": "त्रुटी:एपीआय तर्फे काहीच प्रत्युत्तर नाही",
index fb7b918..e2b8e1b 100644 (file)
@@ -41,6 +41,8 @@
        "tog-watchdefault": "ကျွန်ုပ် တည်းဖြတ်ခဲ့သည့် စာမျက်နှာများနှင့် ဖိုင်များကို စောင့်ကြည့်စာရင်းသို့  ပေါင်းထည့်ပါ။",
        "tog-watchmoves": "ကျွန်ုပ်ရွှေ့လိုက်သော စာမျက်နှာများနှင့် ဖိုင်များကို စောင့်ကြည့်စာရင်းသို့ ပေါင်းထည့်ရန်",
        "tog-watchdeletion": "ဖျက်လိုက်သောစာမျက်နှာများနှင့် ဖိုင်များကို စောင့်ကြည့်စာရင်သို့ ပေါင်းထည့်ရန်",
+       "tog-watchuploads": "ကျွန်ုပ်တင်လိုက်သော ဖိုင်အသစ်များအား ကျွန်ုပ်၏ စောင့်ကြည့်စာရင်းသို့ ပေါင်းထည့်ရန်",
+       "tog-watchrollback": "နောက်ပြန်ပြင်ခြင်း ဆောင်ရွက်လိုက်သည့် စာမျက်နှာများအား ကျွန်ုပ်၏ စောင့်ကြည့်စာရင်းသို့ ပေါင်းထည့်ရန်",
        "tog-minordefault": "တည်းဖြတ်မှုအားလုံးသည် အရေးမကြီးသော တည်းဖြတ်မှုဟု ပုံသေသတ်မှတ်ရန်",
        "tog-previewontop": "တည်းဖြတ်သည့်အကွက်မတိုင်မီ နမူနာကို ပြရန်",
        "tog-previewonfirst": "ပထမတည်းဖြတ်မှုတွင် နမူနာကို ပြရန်",
@@ -49,7 +51,7 @@
        "tog-enotifminoredits": "စာမျက်နှာများနှင့် ဖိုင်များ၏ အရေးမကြီးသော တည်းဖြတ်မှုများကိုလည်း အီးမေးပို့ရန်",
        "tog-enotifrevealaddr": " အသိပေးချက်အီးမေးများတွင် ကျွန်ုပ်၏ အီးမေးလိပ်စာကို ဖော်ပြရန်",
        "tog-shownumberswatching": "စောင့်ကြည့်နေသော အသုံးပြုသူအရေအတွက်ကို ပြရန်",
-       "tog-oldsig": "á\80\9bá\80¾á\80­á\80\94á\80¾á\80\84á\80·á\80ºá\80\95á\80¼á\80®á\80¸á\80\9eá\80¬á\80¸ á\80\9cá\80\80á\80ºá\80\99á\80¾á\80\90á\80º -",
+       "tog-oldsig": "á\80\9eá\80\84á\80ºá\81\8f á\80\9bá\80¾á\80­á\80\94á\80¾á\80\84á\80·á\80ºá\80\95á\80¼á\80®á\80¸á\80\9eá\80¬á\80¸ á\80\9cá\80\80á\80ºá\80\99á\80¾á\80\90á\80º:",
        "tog-fancysig": "လက်မှတ်ကို ဝီကီလင့်အဖြစ် သတ်မှတ်ရန် (အလိုအလျောက်လင့်မပါဘဲနှင့်)",
        "tog-forceeditsummary": "တည်းဖြတ်အတိုချုပ် ဗလာဖြစ်နေလျှင် သတိပေးရန်",
        "tog-watchlisthideown": "ကျွန်ုပ်၏ တည်းဖြတ်မှုများကို စောင့်ကြည့်စာရင်းမှ ဝှက်ထားရန်",
@@ -63,7 +65,7 @@
        "tog-diffonly": "ကွဲပြားမှုများအောက်ရှိ စာမျက်နှာတွင်ပါဝင်သည်များကို မပြပါနှင့်",
        "tog-showhiddencats": "ဝှက်ထားသော ကဏ္ဍများကို ပြရန်",
        "tog-useeditwarning": "မသိမ်းရသေးသော ပြောင်းလဲမှုများ နှင့် တည်းဖြတ်ဆဲစာမျက်နှာမှ ထွက်သွားလျှင် သတိပေးပါ",
-       "tog-prefershttps": "log in ဝင်တိုင်း လုံခြုံသော ဆက်သွယ်မှုကို အသုံးပြုရန်",
+       "tog-prefershttps": "လော့ဂ်အင်ဝင်ချိန်တွင် လုံခြုံသော ဆက်သွယ်မှုကို အမြဲတမ်း အသုံးပြုရန်",
        "underline-always": "အမြဲ",
        "underline-never": "ဘယ်သောအခါမျှ",
        "underline-default": "ဘရောက်ဆာ သို့ Skin default အတိုင်း",
        "category-file-count-limited": "အောက်ပါ {{PLURAL:$1|စာမျက်နှာ|$1 စာမျက်နှာများ}} သည် လက်ရှိစာမျက်နှာတွင် ရှိသည်။",
        "listingcontinuesabbrev": "ပံ့ပိုး",
        "index-category": "အက္ခရာစဉ် စာမျက်နှာများ",
-       "noindex-category": "á\80¡á\80\80á\80¹á\80\81á\80\9bá\80¬á\80\85á\80\89á\80º á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\99á\80»á\80¬á\80¸á\80\99á\80\9bá\80¾á\80­",
+       "noindex-category": "á\80¡á\80\80á\80¹á\80\81á\80\9bá\80¬á\80\99á\80\85á\80\89á\80ºá\80\91á\80¬á\80¸á\80\9eá\80±á\80¬ á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\99á\80»á\80¬á\80¸",
        "broken-file-category": "ကျိုးပျက်နေသော ဖိုင်လင့်များပါသည့် စာမျက်နှာများ",
        "about": "အကြောင်း",
        "article": "စာမျက်နှာ",
        "newwindow": "(ဝင်းဒိုးအသစ်တခုကိုဖွင့်ရန်)",
        "cancel": "မ​လုပ်​တော့​",
        "moredotdotdot": "နောက်ထပ်...",
-       "morenotlisted": "ဤစာရင်းမှာ မပြည့်စုံပါ။",
+       "morenotlisted": "á\80¤á\80\85á\80¬á\80\9bá\80\84á\80ºá\80¸á\80\99á\80¾á\80¬ á\80\99á\80\95á\80¼á\80\8aá\80·á\80ºá\80\85á\80¯á\80¶á\80\94á\80­á\80¯á\80\84á\80ºá\80\95á\80«á\81\8b",
        "mypage": "စာမျက်နှာ",
        "mytalk": "ဆွေးနွေးချက်",
        "anontalk": "ဆွေးနွေးရန်",
        "directorycreateerror": "လမ်းညွှန် \"$1\" ကို ဖန်တီးမရနိုင်ပါ။",
        "filenotfound": "ဖိုင် \"$1\" ကို ရှာမတွေ့ပါ။",
        "formerror": "အမှား - ဖောင်သွင်းနိုင်ခြင်းမရှိပါ",
+       "badarticleerror": "ဤလုပ်ဆောင်မှုအား ဤစာမျက်နှာတွင် လုပ်ဆောင်၍ မရနိုင်ပါ။",
        "cannotdelete": "\"$1\" စာမျက်နှာ သို့မဟုတ် ဖိုင်ကို ဖျက်၍ မရပါ။\nတစ်စုံတစ်ဦးမှ ဖျက်နှင့်ပြီး ဖြစ်နိုင်ပါသည်။",
        "cannotdelete-title": "\"$1\" စာမျက်နှာကို ဖျက်၍ မရပါ",
+       "delete-hook-aborted": "ရှင်းလင်းပြချက် မပေးထားပါ။",
        "badtitle": "ညံ့ဖျင်းသော ခေါင်းစဉ်",
        "badtitletext": "တောင်းဆိုထားသော စာမျက်နှာ ခေါင်းစဉ်သည် တရားမဝင်ပါ (သို့) ဗလာဖြစ်နေသည် (သို့) အခြားဘာသာများ(inter-language or inter-wiki title)သို့ မှားယွင်းစွာ လင့်ချိတ်ထားသည်။",
        "viewsource": "ရင်းမြစ်ကို ကြည့်ရန်",
        "viewsource-title": "$1၏ ရင်းမြစ်ကို ကြည့်ရန်",
        "protectedpagetext": "ဤစာမျက်နှာအား တည်းဖြတ်ခြင်းနှင့် အခြားလုပ်ဆောင်မှုများ မလုပ်ဆောင်နိုင်အောင် ကာကွယ်ထားသည်။",
+       "viewsourcetext": "ဤစာမျက်နှာ၏ ရင်းမြစ်ကို ကြည့်ရှု၍ ကူးယူနိုင်သည်။",
+       "viewyourtext": "ဤစာမျက်နှာရှိ <strong>သင့်တည်းဖြတ်မှုများ</strong>၏ ရင်းမြစ်ကို ကြည့်ရှုပြီး ကူးယူနိုင်သည်။",
        "namespaceprotected": "'''$1''' စာညွှန်းဖြင့် စာမျက်နှာကို တည်းဖြတ်ရန် ခွင့်ပြုချက် မရှိပါ။",
        "mycustomcssprotected": "ဤ CSS စာမျက်နှာကို သင်တည်းဖြတ်ပြင်ဆင်ခွင့် မရှိပါ။",
        "mycustomjsprotected": "ဤ JavaScript စာမျက်နှာကို သင်တည်းဖြတ်ပြင်ဆင်ခွင့် မရှိပါ။",
        "deleteotherreason": "အခြားသော/နောက်ထပ် အကြောင်းပြချက် -",
        "deletereasonotherlist": "အခြား အကြောင်းပြချက်",
        "delete-edit-reasonlist": "ဖျက်ပစ်ရသော အကြောင်းရင်းများကို တည်းဖြတ်ရန်",
+       "deleting-backlinks-warning": "<strong>သတိပေးချက်။</strong> သင်ဖျက်ပစ်တော့မည့် စာမျက်နှာအား [[Special:WhatLinksHere/{{FULLPAGENAME}}|အခြားစာမျက်နှာများမှ]] ချိတ်ဆက်ထားခြင်း သို့မဟုတ် ထည့်သွင်းထားခြင်း ရှိနေသည်။",
        "rollbacklink": "နောက်ပြန် ပြန်သွားရန်",
        "rollbacklinkcount": "{{PLURAL:$1|တည်းဖြတ်မှု|တည်းဖြတ်မှုများ}} $1 ကို နောက်ပြန်ပြင်ရန်",
        "protectlogpage": "ကာကွယ်မှုများ၏ မှတ်တမ်း",
        "others": "အခြား",
        "pageinfo-language": "စာမျက်နှာ စာကိုယ် ဘာသာစကား",
        "pageinfo-toolboxlink": "စာမျက်နှာ အချက်အလက်များ",
+       "markaspatrolleddiff": "စောင့်ကြပ်စစ်ဆေးပြီးကြောင်း မှတ်သားရန်",
+       "markaspatrolledtext": "ဤစာမျက်နှာအား စောင့်ကြပ်စစ်ဆေးပြီးကြောင်း မှတ်သားရန်",
        "filedeleteerror-short": "ဖိုင်ဖျက်ရာတွင် အမှားအယွင်း - $1",
        "previousdiff": "← တည်းဖြတ်မူ အဟောင်း",
        "nextdiff": "ပိုသစ်သော တည်းဖြတ်မှု",
index d60887a..6e71765 100644 (file)
@@ -11,7 +11,8 @@
                        "아라",
                        "Fitoschido",
                        "Taresi",
-                       "Macofe"
+                       "Macofe",
+                       "Akapochtli"
                ]
        },
        "tog-underline": "Mokìnxòîkuilòtzàswis tzòwilistìn:",
        "returnto": "Ximocuepa īhuīc $1.",
        "tagline": "Īhuīcpa {{SITENAME}}",
        "help": "Tēpalēhuiliztli",
-       "search": "Mà motèmo",
+       "search": "Nican tictemoz",
        "searchbutton": "Tictēmōz",
        "go": "Xiyauh",
        "searcharticle": "Xiyauh",
        "unprotectthispage": "Xicpatla inīn tlaīxtli ītlapiyaliz",
        "newpage": "Yancuic tlaīxtli",
        "talkpage": "Xictlahto inīn tlaīxtli ītechcopa",
-       "talkpagelinktext": "Nenônòtzalistli",
+       "talkpagelinktext": "Nenonotzaliztli",
        "specialpage": "Nònkuâkìskàtlaìxtlapalli",
        "personaltools": "In tlein nitēquitiltilia",
        "articlepage": "Xiquitta in tlamantlaīxtli",
        "userlogin-yourname": "Tequihuihcātōcāitl",
        "yourpassword": "Motlahtōlichtacāyo",
        "yourpasswordagain": "Motlahtōlichtacāyo occeppa",
-       "remembermypassword": "Ticpiyāz motlacalaquiliz inīn chīuhpōhualhuazco (īxquich {{PLURAL:$1|tōnalli}})",
        "yourdomainname": "Moāxcāyō",
        "login": "Xicalaqui",
        "nav-login-createaccount": "Ximocalaqui / ximomachiyōmaca",
index 5a1a330..6d21b41 100644 (file)
        "htmlform-title-not-exists": "$1 nun esiste.",
        "htmlform-user-not-exists": "<strong>$1</strong> nun esiste.",
        "htmlform-user-not-valid": "<strong>$1</strong> nun è nu nomme buono.",
-       "sqlite-has-fts": "$1 cu supporto 'e ricerche full-text",
-       "sqlite-no-fts": "$1 senza supporto 'e ricerche full-text",
        "logentry-delete-delete": "$1 {{GENDER:$2|scancellaje}} 'a paggena $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|arrepigliaje}} 'a paggena $3",
        "logentry-delete-event": "$1 {{GENDER:$2|cagnaie}} 'a vesibbiletà 'e {{PLURAL:$5|n'azione d' 'o riggistro|$5 aziune d' 'o riggistro}} ncopp' 'a 'a $3: $4",
index e5f3e01..d403668 100644 (file)
@@ -49,7 +49,8 @@
                        "Matma Rex",
                        "SuperPotato",
                        "Nemo bis",
-                       "Telaneo"
+                       "Telaneo",
+                       "Jon Harald Søby"
                ]
        },
        "tog-underline": "Strek under lenker:",
        "newwindow": "(åpnes i et nytt vindu)",
        "cancel": "Avbryt",
        "moredotdotdot": "Mer …",
-       "morenotlisted": "Denne lista er ufullstendig.",
+       "morenotlisted": "Denne lista er muligens ufullstendig.",
        "mypage": "Min brukerside",
        "mytalk": "Diskusjon",
        "anontalk": "Brukerdiskusjon",
        "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",
        "createacct-yourpasswordagain-ph": "Gjenta passordet",
        "userlogin-remembermypassword": "Hold meg innlogget",
        "userlogin-signwithsecure": "Logg inn med sikker tjener",
+       "cannotlogin-title": "Kan ikke logge inn",
+       "cannotlogin-text": "Innlogging er ikke mulig.",
        "cannotloginnow-title": "Kan ikke logge inn nå",
        "cannotloginnow-text": "Å logge inn er ikke mulig ved bruk av $1.",
+       "cannotcreateaccount-title": "Kan ikke opprette kontoer",
+       "cannotcreateaccount-text": "Direkte kontooppretting er ikke slått på på denne wikien.",
        "yourdomainname": "Ditt domene",
        "password-change-forbidden": "Du kan ikke endre passord på denne wikien.",
        "externaldberror": "Det var en ekstern autentifiseringsfeil, eller du kan ikke oppdatere din eksterne konto.",
        "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?",
        "botpasswords-updated-body": "Robotpassordet for boten «$1» til brukeren «$2» ble oppdatert.",
        "botpasswords-deleted-title": "Robotpassord slettet",
        "botpasswords-deleted-body": "Robotpassordet for boten «$1» til brukeren «$2» ble slettet.",
-       "botpasswords-newpassword": "Det nye passordet for å logge inn med <strong>$1</strong> er <strong>$2</strong>. <em>Vennligst lagre dette for fremtidig referanse.</em>",
+       "botpasswords-newpassword": "Det nye passordet for å logge inn med <strong>$1</strong> er <strong>$2</strong>. <em>Vennligst lagre dette for fremtidig referanse.</em> <br /> (For gamle roboter som trenger samme innloggingsnavn og brukernavn kan du også bruke <strong>$3</strong> som brukernavn og <strong>$4</strong> som passord.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider er ikke tilgjengelig.",
        "botpasswords-restriction-failed": "Begrensninger for robotpassord tillater ikke denne innloggingen.",
        "botpasswords-invalid-name": "Det angitte brukernavnet inneholder ikke separasjonstegnet for robotpassord (\"$1\").",
        "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?",
        "invalid-content-data": "Ugyldig innhold",
        "content-not-allowed-here": "Innholdsmodellen «$1» er ikke tillatt på siden [[$2]]",
        "editwarning-warning": "Ved å forlate siden kan du miste alle endringer du har gjort.\nHvis du er innlogget, kan du slå av denne advarselen under \"{{int:prefs-editing}}\"-avsnittet i dine innstillinger.",
+       "editpage-invalidcontentmodel-title": "Innholdsmodellen støttes ikke",
+       "editpage-invalidcontentmodel-text": "Innholdsmodellen «$1» støttes ikke.",
        "editpage-notsupportedcontentformat-title": "Innholdsformatet er ikke støttet",
        "editpage-notsupportedcontentformat-text": "Innholdsformatet $1 er ikke støttet av innholdsmodellen $2.",
        "content-model-wikitext": "wikitekst",
        "upload-dialog-disabled": "Filopplastinger med denne dialogen er slått av for denne wikien.",
        "upload-dialog-title": "Last opp fil",
        "upload-dialog-button-cancel": "Avbryt",
+       "upload-dialog-button-back": "Tilbake",
        "upload-dialog-button-done": "Utført",
        "upload-dialog-button-save": "Lagre",
        "upload-dialog-button-upload": "Last opp",
        "listusers": "Brukerliste",
        "listusers-editsonly": "Vis bare brukere med redigeringer",
        "listusers-creationsort": "Sorter etter opprettelsesdato",
-       "listusers-desc": "Sorter i avtakende rekkefølge",
+       "listusers-desc": "Sorter i synkende rekkefølge",
        "usereditcount": "{{PLURAL:$1|én redigering|$1 redigeringer}}",
        "usercreated": "{{GENDER:$3|Opprettet}} $2 $1",
        "newpages": "Nye sider",
        "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.",
        "apisandbox-results-fixtoken-fail": "Henting av nøkkelen «$1» mislyktes.",
        "apisandbox-alert-page": "Felter på denne siden er ugyldige.",
        "apisandbox-alert-field": "Verdien til dette feltet er ugyldig.",
+       "apisandbox-continue": "Fortsett",
+       "apisandbox-continue-clear": "Tøm",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} vil [https://www.mediawiki.org/wiki/API:Query#Continuing_queries fortsette] forrige forespørsel; {{int:apisandbox-continue-clear}} vil tømme fortsettelsesrelaterte parametre.",
        "booksources": "Bokkilder",
        "booksources-search-legend": "Søk etter bokkilder",
        "booksources-search": "Søk",
        "listgrouprights-namespaceprotection-header": "Navneromsbegrensinger",
        "listgrouprights-namespaceprotection-namespace": "Navnerom",
        "listgrouprights-namespaceprotection-restrictedto": "Rettighet(er) som tillater at brukeren redigerer",
+       "listgrants": "Tildelinger",
        "listgrants-summary": "Følgende er en liste over tildelinger samt hvilke brukerrettigheter de gir tilgang til. Brukere kan autorisere applikasjoner til å bruke kontoen deres, med rettigheter begrenset til de gitt av tildelingene brukeren har godkjent. En applikasjon som handler på vegne av en bruker kan imidlertid aldri benytte seg av rettigheter brukeren ikke selv har.\nDet kan finnes [[{{MediaWiki:Listgrouprights-helppage}}|ytterligere informasjon]] om de ulike rettighetene.",
+       "listgrants-grant": "Tildeling",
        "listgrants-rights": "Rettigheter",
        "trackingcategories": "Sporingskategori",
        "trackingcategories-summary": "Denne siden lister sporingskategorier som er automatisk befolket av Mediawiki-programvaren. Navnene deres kan endres ved å redigere de tilhørende systembeskjedene i {{ns:8}}-navnerommet.",
        "trackingcategories-name": "Beskjednavn",
        "trackingcategories-desc": "Kategori-inklusjonskriterium",
        "restricted-displaytitle-ignored": "Sider med ignorerte visningstitler",
+       "restricted-displaytitle-ignored-desc": "Denne sidens <code><nowiki>{{DISPLAYTITLE}}</nowiki></code> er ignorert fordi den ikke tilsvarer sidens faktiske tittel.",
        "noindex-category-desc": "Denne siden indekseres ikke av roboter fordi den er merket med det magiske ordet <code><nowiki>__NOINDEX__</nowiki></code> og er i navnerom der dette flagget tillates.",
        "index-category-desc": "Denne siden er påført det magiske ordet <code><nowiki>__INDEX__</nowiki></code> (og er i et navnerom hvor flagget er tillatt), og vil derfor bli indeksert av roboter selv når det normalt ikke ville skjedd.",
        "post-expand-template-inclusion-category-desc": "Sidestørrelsen er større enn <code>$wgMaxArticleSize</code> etter at alle maler er utvidet, så noen maler ble ikke utvidet.",
        "watchnologin": "Ikke logget inn",
        "addwatch": "Legg til i overvåkningslisten",
        "addedwatchtext": "«[[:$1]]» og den tilhørende diskusjonssiden er lagt til i [[Special:Watchlist|overvåkningslisten]] din.",
+       "addedwatchtext-talk": "«[[:$1]]» og dens tilhørende side har blitt lagt til i [[Special:Watchlist|overvåkningslista di]].",
        "addedwatchtext-short": "Siden «$1» har blitt lagt til i overvåkningslisten din.",
        "removewatch": "Fjern fra overvåkningslisten",
        "removedwatchtext": "«[[:$1]]» og den tilhørende diskusjonssiden har blitt fjernet fra [[Special:Watchlist|overvåkningslisten din]].",
+       "removedwatchtext-talk": "«[[:$1]]» og dens tilhørende side har blitt fjernet fra [[Special:Watchlist|overvåkningslista di]].",
        "removedwatchtext-short": "Siden «$1» har blitt fjernet fra overvåkningslisten din.",
        "watch": "Overvåk",
        "watchthispage": "Overvåk denne siden",
        "rollbacklinkcount": "tilbakestill {{PLURAL:$1|én endring|$1 endringer}}",
        "rollbacklinkcount-morethan": "tilbakestill mer enn $1 {{PLURAL:$1|endring|endringer}}",
        "rollbackfailed": "Kunne ikke tilbakestille",
+       "rollback-missingparam": "Påkrevde parametere i forespørselen mangler.",
+       "rollback-missingrevision": "Kunne ikke laste revisjonsdata.",
        "cantrollback": "Kan ikke fjerne redigering; den siste brukeren er den eneste forfatteren.",
        "alreadyrolled": "Kan ikke fjerne den siste redigeringen på [[$1]] av [[User:$2|$2]] ([[User talk:$2|diskusjon]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]); en annen har allerede redigert siden eller fjernet redigeringen.\n\nDen siste redigeringen ble foretatt av [[User:$3|$3]] ([[User talk:$3|diskusjon]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "Redigeringskommentaren var: <em>$1</em>",
        "revertpage": "Tilbakestilte endringer av [[Special:Contributions/$2|$2]] ([[User talk:$2|brukerdiskusjon]]) til siste versjon av [[User:$1|$1]]",
        "revertpage-nouser": "Tilbakestilt endringer av skjult bruker til siste versjon av\n{{GENDER:$1|[[User:$1|$1]]}}",
        "rollback-success": "Tilbakestilte endringer av $1; endret til siste versjon av $2.",
+       "rollback-success-notify": "Tilbakestilte endringer av $1;\nendret tilbake til siste revisjon av $2. [$3 Vis endringer]",
        "sessionfailure-title": "Sesjonsfeil",
        "sessionfailure": "Det ser ut til å være et problem med innloggingen din, og den ble avbrutt av sikkerhetshensyn. Trykk ''Tilbake'' i nettleseren din, oppdater siden og prøv igjen.",
        "changecontentmodel": "Endre innholdsmodell for en side",
        "changecontentmodel-title-label": "Sidetittel",
        "changecontentmodel-model-label": "Ny innholdsmodell",
        "changecontentmodel-reason-label": "Begrunnelse:",
+       "changecontentmodel-submit": "Endre",
        "changecontentmodel-success-title": "Innholdsmodellen ble endret",
        "changecontentmodel-success-text": "Innholdstypen for [[:$1]] har blitt endret.",
        "changecontentmodel-cannot-convert": "Innholdet på [[:$1]] kan ikke konverteres til en type av $2.",
        "changecontentmodel-nodirectediting": "Innholdsmodellen $1 støtter ikke direkte redigering",
+       "changecontentmodel-emptymodels-title": "Ingen innholdsmodeller er tilgjengelige",
+       "changecontentmodel-emptymodels-text": "Innholdet på [[:$1]] kan ikke konverteres til noen type.",
        "log-name-contentmodel": "Logg over endringer i endringsloggen",
        "log-description-contentmodel": "Hendelseslogg relatert til innholdsmodellen for en side",
+       "logentry-contentmodel-new": "$1 {{GENDER:$2|opprettet}} siden $3 med den ikke-standard innholdsmodellen «$5»",
        "logentry-contentmodel-change": "$1 {{GENDER:$2|endret}} innholdsmodellen for siden $3 fra «$4» til «$5»",
        "logentry-contentmodel-change-revertlink": "tilbakestill",
        "logentry-contentmodel-change-revert": "tilbakestill",
        "undeletehistorynoadmin": "Denne artikkelen har blitt slettet. Grunnen for slettingen vises i oppsummeringen nedenfor, sammen med detaljer om brukerne som redigerte siden før den ble slettet. Teksten i disse slettede revisjonene er kun tilgjengelig for administratorer.",
        "undelete-revision": "Slettet revisjon av $1 (per $4 $5) av $3:",
        "undeleterevision-missing": "Ugyldig eller manglende revisjon. Du kan ha en ødelagt lenke, eller revisjonen har blitt fjernet fra arkivet.",
+       "undeleterevision-duplicate-revid": "{{PLURAL:$1|Én revisjon|$1 revisjoner}} kunne ikke gjenopprettes, fordi {{PLURAL:$1|dens|deres}} <code>rev_id</code> allerede er i bruk.",
        "undelete-nodiff": "Ingen tidligere revisjoner funnet.",
        "undeletebtn": "Gjenopprett",
        "undeletelink": "vis/gjenopprett",
        "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-deleted": "slettede brukerbidrag",
+       "sp-contributions-suppresslog": "undertrykte {{GENDER:$1|brukerbidrag}}",
+       "sp-contributions-deleted": "slettede {{GENDER:$1|brukerbidrag}}",
        "sp-contributions-uploads": "opplastinger",
        "sp-contributions-logs": "logger",
        "sp-contributions-talk": "diskusjon",
        "sp-contributions-username": "IP-adresse eller brukernavn:",
        "sp-contributions-toponly": "Vis kun endringer som er gjeldende revisjoner",
        "sp-contributions-newonly": "Bare vis bidrag som er sideopprettinger",
+       "sp-contributions-hideminor": "Skjul mindre endringer",
        "sp-contributions-submit": "Søk",
        "whatlinkshere": "Hva lenker hit",
        "whatlinkshere-title": "Sider som lenker til «$1»",
        "unblock": "Fjern blokkering av bruker",
        "blockip": "Blokker {{GENDER:$1|bruker}}",
        "blockip-legend": "Blokker bruker",
-       "blockiptext": "Bruk skjemaet under for å blokkere en IP-adresses tilgang til å redigere artikler. Dette må kun gjøres for å forhindre hærverk, og i overensstemmelse med [[{{MediaWiki:Policy-url}}|retningslinjene]]. Fyll ut en spesiell begrunnelse under.",
+       "blockiptext": "Bruk skjemaet under for å blokkere skrivetilgangen til en spesifikk IP-adresse eller et brukernavn.\nDette bør kun gjøres for å forhindre vandalisme, og i samsvar med [[{{MediaWiki:Policy-url}}|retningslinjene]].\nSkriv inn en spesifikk grunn nedenfor (for eksempel ved å angi hvilke sider som ble vandalisert).\nDu kan blokkere IP-intervaller med [https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR]-syntaks; det største tillatte intervallet er /$1 for IPv4 og /$2 for IPv6.",
        "ipaddressorusername": "IP-adresse eller brukernavn",
        "ipbexpiry": "Varighet:",
        "ipbreason": "Årsak:",
        "ipb-unblock": "Opphev blokkering av et brukernavn eller en IP-adresse",
        "ipb-blocklist": "Vis gjeldende blokkeringer",
        "ipb-blocklist-contribs": "Bidrag fra {{GENDER:$1|$1}}",
+       "ipb-blocklist-duration-left": "$1 igjen",
        "unblockip": "Opphev blokkering",
        "unblockiptext": "Bruk skjemaet under for å gjenopprette skriveadgangen for en tidligere blokkert adresse eller bruker.",
        "ipusubmit": "Opphev blokkering",
        "block-log-flags-hiddenname": "brukernavn skjult",
        "range_block_disabled": "Muligheten til å blokkere flere IP-adresser om gangen er slått av.",
        "ipb_expiry_invalid": "Ugyldig utløpstid.",
+       "ipb_expiry_old": "Utløpstiden har allerede vært.",
        "ipb_expiry_temp": "For å skjule brukernavnet må blokkeringen være permanent.",
        "ipb_hide_invalid": "Denne kontoen kan ikke skjules; den har mer enn {{PLURAL:$1|én redigering|$1 redigeringer}}.",
        "ipb_already_blocked": "«$1» er allerede blokkert",
        "lockdbsuccesstext": "Databasen er låst.<br />Husk å [[Special:UnlockDB|låse den opp]] når du er ferdig med vedlikeholdet.",
        "unlockdbsuccesstext": "Databasen er låst opp.",
        "lockfilenotwritable": "Kan ikke skrive til databasen. For å låse eller åpne databasen, må denne kunne skrives til av tjeneren.",
+       "databaselocked": "Databasen er allerede låst.",
        "databasenotlocked": "Databasen er ikke låst.",
        "lockedbyandtime": "(av $1 den $2, kl $3)",
        "move-page": "Flytt $1",
        "move-page-legend": "Flytt side",
-       "movepagetext": "Når du bruker skjemaet nedenfor døper du om en side og flytter hele historikken til det nye navnet.\nDen gamle tittelen blir en omdirigeringsside til den nye tittelen.\nDu kan oppdatere omdirigeringer som peker til den opprinnelige tittelen automatisk.\nOm du velger å ikke gjøre det, sjekk at flyttingen ikke fører til [[Special:DoubleRedirects|doble]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for at lenker fortsetter å peke til de sidene de er ment å peke til.\n\nLegg merke til at siden '''ikke''' kan flyttes hvis det allerede finnes en side med den nye tittelen, med mindre sistnevnte er tom eller er en omdirigeringsside uten historikk.\nDet betyr at du kan flytte en side tilbake dit den kom fra hvis du gjør en feil, og du kan ikke overskrive eksisterende sider ved et uhell.\n\n'''Advarsel!'''\nDette kan være en drastisk og uventet endring for en populær side;\nvær sikker på at du forstår konsekvensene av dette før du fortsetter.",
-       "movepagetext-noredirectfixer": "Skjemaet nedenfor vil gi en side ny tittel og flytte historikken dens til det nye navnet.\nDen gamle tittelen vil bli en omdirigering til den nye.\nSjekk om det blir [[Special:DoubleRedirects|doble]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for å sjekke at lenker fortsetter å gå dit de skal.\n\nMerk at sider '''ikke''' blir flyttet om det allerede finnes en side med den tittelen, med mindre siden er tom eller en omdirigering og ikke har noen redigeringshistorikk.\nDette betyr at du kan endre tittelen til en tittel siden hadde tidligere, og at du ikke kan skrive over en eksisterende side.\n\n'''Advarsel!'''\nDette kan være en drastisk og uventet endring for en populær side;\nvær sikker på at du forstår konsekvensene av dette før du fortsetter.",
-       "movepagetalktext": "Den tilhørende diskusjonssiden vil automatisk bli flyttet sammen med siden '''med mindre:'''\n*Det allerede finnes en diskusjonsside som ikke er tom under det nye navnet, eller\n*Du fjerner markeringen i boksen nedenfor.\n\nI disse tilfellene er du nødt til å flytte eller flette siden manuelt, om ønskelig.",
+       "movepagetext": "Når du bruker skjemaet nedenfor døper du om en side og flytter hele historikken til det nye navnet.\nDen gamle tittelen blir en omdirigeringsside til den nye tittelen.\nDu kan oppdatere omdirigeringer som peker til den opprinnelige tittelen automatisk.\nOm du velger å ikke gjøre det, sjekk at flyttingen ikke fører til [[Special:DoubleRedirects|doble]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for at lenker fortsetter å peke til de sidene de er ment å peke til.\n\nLegg merke til at siden <strong>ikke</strong> kan flyttes hvis det allerede finnes en side med den nye tittelen, med mindre sistnevnte er tom eller er en omdirigeringsside uten historikk.\nDet betyr at du kan flytte en side tilbake dit den kom fra hvis du gjør en feil, og du kan ikke overskrive eksisterende sider ved et uhell.\n\n<strong>Merk:</strong>\nDette kan være en drastisk og uventet endring for en populær side;\nvær sikker på at du forstår konsekvensene av dette før du fortsetter.",
+       "movepagetext-noredirectfixer": "Skjemaet nedenfor vil gi en side ny tittel og flytte historikken dens til det nye navnet.\nDen gamle tittelen vil bli en omdirigering til den nye.\nSjekk om det blir [[Special:DoubleRedirects|doble]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for å sjekke at lenker fortsetter å gå dit de skal.\n\nMerk at sider <strong>ikke</strong> blir flyttet om det allerede finnes en side med den tittelen, med mindre siden er en omdirigering og ikke har noen redigeringshistorikk.\nDette betyr at du kan endre tittelen til en tittel siden hadde tidligere, og at du ikke kan skrive over en eksisterende side.\n\n<strong>Merk:</strong>\nDette kan være en drastisk og uventet endring for en populær side;\nvær sikker på at du forstår konsekvensene av dette før du fortsetter.",
+       "movepagetalktext": "Om du merker av denne boksen vil den tilhørende diskusjonssiden også flyttes til den nye tittelen, med mindre en ikke-tom diskusjonsside allerede finnes der.\n\nOm det er tilfelle må du flytte eller flette siden manuelt om det er ønskelig.",
        "moveuserpage-warning": "'''Advarsel:''' Du er i ferd med å flytte en brukerside. Merk at kun siden vil bli flyttet; brukernavnet vil ''ikke'' bli endret.",
        "movecategorypage-warning": "<strong>Advarsel:</strong> Du er i ferd med å flytte en kategoriside. Merk at kun siden blir flyttet, og at sider i det gamle kategorinavnet <em>ikke</em> blir omkategorisert til det nye navnet.",
        "movenologintext": "Du må være registrert bruker og være [[Special:UserLogin|logget på]] for å flytte en side.",
        "movenosubpage": "Denne siden har ingen undersider.",
        "movereason": "Årsak:",
        "revertmove": "tilbakestill",
-       "delete_and_move_text": "==Sletting nødvendig==\nMålsiden «[[:$1]]» finnes allerede. Vil du slette den så denne siden kan flyttes dit?",
+       "delete_and_move_text": "Målsiden «[[:$1]]» finnes fra før.\nØnsker du å slette den for å muliggjøre flyttingen?",
        "delete_and_move_confirm": "Ja, slett siden",
        "delete_and_move_reason": "Slettet for å muliggjøre flytting fra \"[[$1]]\"",
        "selfmove": "Kilde- og destinasjonstittel er den samme; kan ikke flytte siden.",
        "move-leave-redirect": "La det være igjen en omdirigering",
        "protectedpagemovewarning": "'''Advarsel:''' Denne siden har blitt låst slik at kun brukere med administratorrettigheter kan flytte den.\nDet siste loggelementet er oppgitt under som referanse:",
        "semiprotectedpagemovewarning": "'''Merk:''' Denne siden har blitt låst slik at kun registrerte brukere kan flytte den.\nDet siste loggelementet er oppgitt under som referanse:",
-       "move-over-sharedrepo": "== Filen finnes ==\n[[:$1]] finnes på en delt kilde. Dersom du flytter en fil til dette navnet, vil du overstyre den delte filen.",
+       "move-over-sharedrepo": "[[:$1]] finnes på et delt fillager. Flytting av filen til denne tittelen vil overstyre den delte filen.",
        "file-exists-sharedrepo": "Det valgte filnavnet er allerede i bruk på en delt kilde.\nVennligst velg et annet navn.",
        "export": "Eksporter sider",
        "exporttext": "Du kan eksportere teksten og redigeringshistorikken for en bestemt side eller en gruppe sider i XML.\nDette kan senere importeres til en annen wiki som bruker MediaWiki ved hjelp av [[Special:Import|importsiden]].\n\nFor å eksportere sider, skriv inn titler i tekstboksen under, én tittel per linje, og velg om du vil ha kun nåværende versjon, eller alle versjoner i historikken.\n\nDersom du bare vil ha nåværende versjon, kan du også bruke en lenke, for eksempel [[{{#Special:Export}}/{{MediaWiki:Mainpage}}]] for siden «[[{{MediaWiki:Mainpage}}]]».",
        "export-download": "Lagre som fil",
        "export-templates": "Ta med maler",
        "export-pagelinks": "Inkluder lenkede sider med en dybde på:",
+       "export-manual": "Legg til sider manuelt:",
        "allmessages": "Systemmeldinger",
        "allmessagesname": "Navn",
        "allmessagesdefault": "Standardtekst",
        "import-nonewrevisions": "Ingen revisjoner ble importert: De var enten allerede på plass, eller hoppet over pga. feil.",
        "xml-error-string": "$1 på linje $2, kolonne $3 (byte: $4): $5",
        "import-upload": "Last opp XML-data",
-       "import-token-mismatch": "Sesjonsdata mistet. Venligst prøv igjen.",
+       "import-token-mismatch": "Sesjonsdata mistet.\n\nDu kan ha blitt logget ut. <strong>Sjekk at du fortsatt er logget inn og prøv igjen.</strong>\nOm det fortsatt ikke fungerer, prøv å [[Special:UserLogout|logge ut]] og logge inn igjen, og sjekk om netteleseren din tillater informasjonskapsler fra denne siden.",
        "import-invalid-interwiki": "Kan ikke importere fra angitt wiki.",
        "import-error-edit": "Siden «$1» ble ikke importert fordi du ikke har tillatelse til å redigere den.",
        "import-error-create": "Siden «$1» ble ikke importert fordi du ikke har tillatelse til å opprette den.",
        "tooltip-feed-rss": "RSS-mating for denne siden",
        "tooltip-feed-atom": "Atom-mating for denne siden",
        "tooltip-t-contributions": "En liste over bidrag fra {{GENDER:$1|denne brukeren}}",
-       "tooltip-t-emailuser": "Send en e-post til denne brukeren",
+       "tooltip-t-emailuser": "Send en e-post til {{GENDER:$1|denne brukeren}}",
        "tooltip-t-info": "Mer informasjon om denne siden",
        "tooltip-t-upload": "Last opp filer",
        "tooltip-t-specialpages": "Liste over alle spesialsider",
        "tooltip-ca-nstab-category": "Vis kategorisiden",
        "tooltip-minoredit": "Merk dette som en mindre endring",
        "tooltip-save": "Lagre endringene dine",
+       "tooltip-publish": "Publiser endringene dine",
        "tooltip-preview": "Forhåndsvis endringene dine, vennligst gjør dette før du lagrer!",
        "tooltip-diff": "Vis hvilke endringer du har gjort på teksten",
        "tooltip-compareselectedversions": "Se forskjellen mellom de to valgte revisjonene av denne siden",
        "pageinfo-article-id": "Side-ID",
        "pageinfo-language": "Språk for sideinnholdet",
        "pageinfo-content-model": "Modell for sideinnhold",
+       "pageinfo-content-model-change": "endre",
        "pageinfo-robot-policy": "Bot-indeksering",
        "pageinfo-robot-index": "Tillatt",
        "pageinfo-robot-noindex": "Ikke tillatt",
        "pageinfo-category-files": "Antall filer",
        "markaspatrolleddiff": "Merk som patruljert",
        "markaspatrolledtext": "Merk denne siden som patruljert",
+       "markaspatrolledtext-file": "Merk denne filversjonen som patruljert",
        "markedaspatrolled": "Merket som patruljert",
        "markedaspatrolledtext": "Den valgte revisjonen av [[:$1]] har blitt merket som patruljert.",
        "rcpatroldisabled": "Siste endringer-patruljering er slått av",
        "newimages-legend": "Filnavn",
        "newimages-label": "Filnavn (helt eller delvis):",
        "newimages-showbots": "Vis opplastinger av botter",
+       "newimages-hidepatrolled": "Skjul patruljerte opplastinger",
        "noimages": "Ingenting å se.",
        "ilsubmit": "Søk",
        "bydate": "etter dato",
        "confirmemail_body_set": "Noen med IP-adresse $1, mest sannsynlig deg, har satt e-postadressen for kontoen «$2» til denne adressen på {{SITENAME}}.\n\nFor å bekrefte at denne kontoen faktisk tilhører deg og for å slå på e-post-tjenestene fra {{SITENAME}}, må du åpne denne lenken i nettleseren din:\n\n$3\n\nOm kontoen *ikke* tilhører deg, følg denne lenken for å avbryte e-post-bekreftelsen:\n\n$5\n\nDenne bekreftelseskoden utløper $4.",
        "confirmemail_invalidated": "Bekreftelse av e-postadresse avbrutt",
        "invalidateemail": "Avbryt bekreftelse av e-postadresse",
+       "notificationemail_subject_changed": "Registrert epostadresse på {{SITENAME}} har blitt endret",
+       "notificationemail_subject_removed": "Registrert epostadresse på {{SITENAME}} har blitt fjernet",
+       "notificationemail_body_changed": "Noen, antageligvis du (fra IP-adressen $1), har endret epostadressen til kontoen «$2» til «$3» på {{SITENAME}}.\n\nOm det ikke var du som gjorde det, kontakt en sideadministrator umiddelbart.",
+       "notificationemail_body_removed": "Noen, antageligvis deg (fra IP-adressen $1), har fjernet epostadressen til kontoen «$2» på {{SITENAME}}.\n\nOm det ikke var du som gjorde det, kontakt en sideadministrator umiddelbart.",
        "scarytranscludedisabled": "[Interwiki-transkludering er slått av]",
        "scarytranscludefailed": "[Malen kunne ikke hentes for $1]",
        "scarytranscludefailed-httpstatus": "[Henting av mal for $1 feilet: HTTP $2]",
        "confirm-watch-top": "Legg denne siden til overvåkningslisten din?",
        "confirm-unwatch-button": "OK",
        "confirm-unwatch-top": "Fjern denne siden fra overvåkningslisten din?",
+       "confirm-rollback-button": "OK",
+       "confirm-rollback-top": "Tilbakestill redigeringer på denne siden?",
        "quotation-marks": "«$1»",
        "imgmultipageprev": "← forrige side",
        "imgmultipagenext": "neste side &rarr;",
        "hebrew-calendar-m11-gen": "Ab",
        "hebrew-calendar-m12-gen": "Elúl",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|diskusjon]])",
+       "timezone-local": "Lokal",
        "duplicate-defaultsort": "Advarsel: Standardsorteringen «$2» tar over for den tidligere sorteringen «$1».",
        "duplicate-displaytitle": "<strong>Advarsel:</strong> Visningstittel \"$2\" erstatter tidligere visningstittel \"$1\".",
+       "restricted-displaytitle": "<strong>Advarsel:</strong> Visningstittelen «$1» ble ignorert siden den ikke tilsvarer sidens faktiske tittel.",
        "invalid-indicator-name": "<p>Feil:</strong> Sidestatus-indikatornes <code>navn</code>-attributt kan ikke være tomt.",
        "version": "Versjon",
        "version-extensions": "Installerte utvidelser",
        "version-libraries-license": "Lisens",
        "version-libraries-description": "Beskrivelse",
        "version-libraries-authors": "Forfattere",
-       "redirect": "Omdiriger via filnavn, bruker eller versjonsid",
-       "redirect-summary": "Denne spesialsiden omdirigerer til en fil (hvis et filnavn angis), en side (hvis et redigeringsnummer angis) eller en brukerside (hvis en numerisk brukeridentifikator angis).\nEksempler:[[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/revision/328429]], or [[{{#Special:Redirect}}/user/101]].",
+       "redirect": "Omdiriger via filnavn, bruker-, side-, revisjons- eller logg-ID.",
+       "redirect-summary": "Denne spesialsiden omdirigerer til en fil (hvis et filnavn angis), en side (om revisjons- eller side-ID angis), en brukerside (om bruker-ID angis), eller en loggoppføring (om logg-ID angis). Bruk: [[{{#Special:Redirect}}/file/Eksempel.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]] eller [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Gå",
        "redirect-lookup": "Oppslag:",
        "redirect-value": "Verdi:",
        "redirect-page": "Side-ID",
        "redirect-revision": "Sideversjon",
        "redirect-file": "Filnavn",
+       "redirect-logid": "Logg-ID",
        "redirect-not-exists": "Verdi er ikke funnet",
        "fileduplicatesearch": "Søk etter duplikatfiler",
        "fileduplicatesearch-summary": "Søk etter duplikatfiler basert på dets hash-verdi.",
        "tag-filter": "Filter for [[Special:Tags|tagger]]:",
        "tag-filter-submit": "Filtrer",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Tagg|Tagger}}]]: $2)",
+       "tag-mw-contentmodelchange": "innholdsmodellendring",
+       "tag-mw-contentmodelchange-description": "Redigeringer som [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel endrer innholdsmodellen] til en side",
        "tags-title": "Tagger",
        "tags-intro": "Denne siden lister opp taggene programvaren kan merke en endring med, og hva de betyr.",
        "tags-tag": "Taggnavn",
        "tags-actions-header": "Handlinger",
        "tags-active-yes": "Ja",
        "tags-active-no": "Nei",
-       "tags-source-extension": "Definert av en utvidelse",
+       "tags-source-extension": "Definert av programvaren",
        "tags-source-manual": "Brukes manuelt av brukere og roboter",
        "tags-source-none": "Brukes ikke lenger",
        "tags-edit": "rediger",
        "tags-deactivate": "deaktiver",
        "tags-hitcount": "{{PLURAL:$1|én endring|$1 endringer}}",
        "tags-manage-no-permission": "Du har ikke tillatelse til å behandle tagger.",
+       "tags-manage-blocked": "Du kan ikke behandle endringstagger mens du er blokkert.",
        "tags-create-heading": "Opprett ny tagg",
        "tags-create-explanation": "Som standard vil nyopprettede tagger være tilgjengelige for brukere og roboter.",
        "tags-create-tag-name": "Taggnavn:",
        "tags-delete-not-found": "Taggen «$1» finnes ikke.",
        "tags-delete-too-many-uses": "Taggen «$1» brukes på mer enn $2 {{PLURAL:$2|revisjon|revisjoner}}, hvilket betyr at den ikke kan slettes.",
        "tags-delete-warnings-after-delete": "Taggen «$1» ble slettet, men følgende {{PLURAL:$2|advarsel|advarsler}} dukket opp:",
+       "tags-delete-no-permission": "Du har ikke tillatelse til å slette endringstagger.",
        "tags-activate-title": "Aktiver taggen",
        "tags-activate-question": "Du er i ferd med å aktivere taggen «$1».",
        "tags-activate-reason": "Årsak:",
        "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.",
+       "tags-update-blocked": "Du kan ikke legge til eller fjerne endringstagger mens du er blokkert.",
        "tags-update-add-not-allowed-one": "Merket «$1» kan ikke legges til manuelt.",
        "tags-update-add-not-allowed-multi": "{{PLURAL:$2|Det følgende merket|De følgende merkene}} kan ikke legges til manuelt: $1",
        "tags-update-remove-not-allowed-one": "Merket «$1» kan ikke fjernes.",
        "htmlform-cloner-create": "Legg til mer",
        "htmlform-cloner-delete": "Fjern",
        "htmlform-cloner-required": "Minst én verdi kreves.",
+       "htmlform-date-placeholder": "ÅÅÅÅ-MM-DD",
+       "htmlform-time-placeholder": "TT:MM:SS",
+       "htmlform-datetime-placeholder": "ÅÅÅÅ-MM-DD TT:MM:SS",
+       "htmlform-date-invalid": "Verdien du anga gjenkjennes ikke som en dato. Prøv formatet ÅÅÅÅ-MM-DD.",
+       "htmlform-time-invalid": "Verdien du anga gjenkjennes ikke som et tidspunkt. Prøv formatet TT:MM:SS.",
+       "htmlform-datetime-invalid": "Verdien du anga gjenkjennes ikke som en dato og et tidspunkt. Prøv formatet ÅÅÅÅ-MM-DD TT:MM:SS.",
+       "htmlform-date-toolow": "Verdien du anga er før den tidligste tillatte datoen $1.",
+       "htmlform-date-toohigh": "Verdien du anga er etter den siste tillatte datoen $1.",
+       "htmlform-time-toolow": "Verdien du anga er før det tidligste tillatte tidspunktet $1.",
+       "htmlform-time-toohigh": "Verdien du anga er etter det siste tillatte tidspunktet $1.",
+       "htmlform-datetime-toolow": "Verdien du anga er før den tidligste tillatte datoen og tidspunktet $1.",
+       "htmlform-datetime-toohigh": "Verdien du anga er etter den siste tillatte datoen og tidspunktet $1.",
        "htmlform-title-badnamespace": "[[:$1]] er ikke i «{{ns:$2}}»-navnerommet",
        "htmlform-title-not-creatable": "«$1» er ikke en opprettbar sidetittel",
        "htmlform-title-not-exists": "$1 forefinnes ikke.",
        "htmlform-user-not-exists": "<strong>$1</strong> eksisterer ikke.",
        "htmlform-user-not-valid": "<strong>$1</strong> er ikke et gyldig brukernavn.",
-       "sqlite-has-fts": "$1 med støtte for fulltekstsøk",
-       "sqlite-no-fts": "$1 uten støtte for fulltekstsøk",
        "logentry-delete-delete": "$1 {{GENDER:$2|slettet}} siden $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|gjenopprettet}} siden $3",
        "logentry-delete-event": "$1 {{GENDER:$2|endret}} synligheten av {{PLURAL:$5|en logghendelse|$5 logghendelser}} på $3: $4",
        "feedback-external-bug-report-button": "Registrer en teknisk sak",
        "feedback-dialog-title": "Send tilbakemelding",
        "feedback-dialog-intro": "Bruk det enkle skjemaet under om du vil gi tilbakemelding. Kommentaren din vil bli lagt ut på siden «$1» sammen med brukernavnet ditt.",
-       "feedback-error-title": "Feil",
        "feedback-error1": "Feil: Ukjent resultat fra API",
        "feedback-error2": "Feil: Redigering feilet",
        "feedback-error3": "Feil: Ingen respons fra API",
        "feedback-useragent": "Brukeragent",
        "searchsuggest-search": "Søk",
        "searchsuggest-containing": "inneholder …",
+       "api-error-autoblocked": "Din IP-adresse har blitt blokkert automatisk fordi den ble brukt av en blokkert bruker.",
        "api-error-badaccess-groups": "Du har ikke tillatelse til å laste opp filer til denne wikien.",
        "api-error-badtoken": "Intern feil: Ugyldig nøkkel.",
+       "api-error-blocked": "Du har blitt blokkert fra å redigere.",
        "api-error-copyuploaddisabled": "Opplasting ved URL er deaktivert på denne tjeneren.",
        "api-error-duplicate": "Det er allerede {{PLURAL:$1|en annen fil|flere andre filer}} på denne siden med samme innhold.",
        "api-error-duplicate-archive": "Det fantes {{PLURAL:$1|en annen fil|noen andre filer}} på siden som hadde samme innhold, men {{PLURAL:$1|den|de}} ble slettet.",
        "api-error-nomodule": "Intern feil: ingen opplastningsmodul har blitt valgt.",
        "api-error-ok-but-empty": "Intern feil: ingen svar fra server.",
        "api-error-overwrite": "Det er ikke tillatt å overskrive eksisterende filer.",
+       "api-error-ratelimited": "Du prøver å laste opp flere filer enn wikien tillater i et kort tidsrom.\nPrøv igjen om noen minutter.",
        "api-error-stashfailed": "Internal error: tjeneren greide ikke å lagre midlertidig fil.",
        "api-error-publishfailed": "Intern feil: Tjeneren greide ikke å publisere midlertidig fil.",
        "api-error-stasherror": "Det oppstod en feil mens filen ble lastet opp til stash.",
        "api-error-unknownerror": "Ukjent feil: «$1».",
        "api-error-uploaddisabled": "Opplastning har blitt deaktivert på denne wikien.",
        "api-error-verification-error": "Filen kan være korrupt, eller ha feil filendelse.",
+       "api-error-was-deleted": "En fil med dette navnet har tidligere blitt lastet opp og senere slettet.",
        "duration-seconds": "$1 {{PLURAL:$1|sekund|sekunder}}",
        "duration-minutes": "$1 {{PLURAL:$1|minutt|minutter}}",
        "duration-hours": "$1 {{PLURAL:$1|time|timer}}",
        "expand_templates_preview": "Forhåndsvisning",
        "expand_templates_preview_fail_html": "<em>Fordi {{SITENAME}} har slått på rå HTML og sesjonsdata ble tapt er forhåndsvisningen skjult for å beskytte mot JavaScript-angrep.</em>\n\n<strong>Om dette er et legitimt forsøk på å forhåndsvise, prøv på nytt.</strong> Om det fortsatt ikke fungerer, prøv å [[Special:UserLogout|logge ut]] og logge inn igjen, og sjekk at nettleseren din godtar nettkapsler fra dette nettstedet.",
        "expand_templates_preview_fail_html_anon": "<em>Fordi {{SITENAME}} har slått på rå HTML og du ikke er logget inn er forhåndsvisningen skjult for å beskytte mot JavaScript-angrep.</em>\n\n<strong>Om dette er et legitimt forsøk på å forhåndsvise, [[Special:UserLogin|logg inn]] og prøv igjen.</strong>",
+       "expand_templates_input_missing": "Du må angi noe inndata.",
        "pagelanguage": "Endre sidespråk",
        "pagelang-name": "Side",
        "pagelang-language": "Språk",
        "log-name-pagelang": "Logg for språkendringer",
        "log-description-pagelang": "Dette er en logg som viser endringer i sidespråk",
        "logentry-pagelang-pagelang": "$1 {{GENDER:$2|endret}} språk for $3 fra $4 til $5.",
-       "default-skin-not-found": "Ops! Standarddrakten for wikien din, definert i <code dir=\"ltr\">$wgDefaultSkin</code> som <code>$1</code>, er ikke tilgjengelig.\n\nInstallasjonen din ser ut til å inneholde følgende {{PLURAL:$4|drakt|drakter}}. Se [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] for informasjon om hvordan du kan slå {{PLURAL:$4|denne på|disse på og velge en standarddrakt}}.\n\n$2\n\n; Om du nettopp har installert MediaWiki:\n: Du har trolig installert fra git, eller direkte fra kildekoden med en annen metode. Dette er forventet. Prøv å installere noen drakter fra [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org sin draktbase] ved å\n:* laste ned [https://www.mediawiki.org/wiki/Download tarball-installereren], som kommer med flere drakter og utvidelser. Du kan kopiere og lime inn <code>skins/</code>-mappen fra denne.\n:* laste ned individuelle drakter fra [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* klone en av <code>mediawiki/skins/*</code>-lagrene via git inn i <code>skins/</code> -mappen av din MediaWiki-installasjon.\n: Å gjøre dette skal ikke forstyrre git-mappen din om du er en MediaWiki-utvikler.\n\n; Om du nettopp har oppgradert MediaWiki:\n: MediaWiki 1.24 og nyere slår ikke lenger på automatisk installerte drakter (se [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manual: Skin autodiscovery]). Du kan lime inn følgende {{PLURAL:$5|linje|linjer}} i <code>LocalSettings.php</code> for å slå på {{PLURAL:$5|den|alle}} nåværende installerte {{PLURAL:$5|drakten|drakter}}:\n\n<pre dir=\"ltr\">$3</pre>\n\n; Om du nettopp har endret <code>LocalSettings.php</code>:\n: Dobbelsjekk draktnavnene for skrivefeil.",
-       "default-skin-not-found-no-skins": "Ops! Standarddrakten for wikien din, definert i <code>$wgDefaultSkin</code> som <code>$1</code>, er ikke tilgjengelig.\n\nDu har ingen installerte drakter.\n\n;Om du nettopp har installert eller oppgradert MediaWiki:\n: Du installerte trolig fra git, eller direkte fra kildekoden med en annen metode. Dette er forventet. MediaWiki 1.24 og nyere inkluderer ingen drakter i hovedarkivet. Prøv å installere noen drakter fra [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.orgs draktmappe], ved å:\n:* laste ned [https://www.mediawiki.org/wiki/Download tarball-installereren], som kommer med mange drakter og tillegg. Du kan kopiere og lime inn <code>skins/</code>-mappen fra denne.\n:* laste ned individuelle drakt-tarballer fra [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* klone en av <code>mediawiki/skins/*</code>-arkivene via git til <code dir=\"ltr\">skins/</code>-mappa i din MediaWiki-installasjon.\n: Å gjøre dette vil ikke forstyrre ditt git-arkiv om du er en MediaWiki-utvikler. Se [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual:Skin configuration] for informasjon om hvordan du slår på drakter og velger en standarddrakt.",
+       "default-skin-not-found": "Ops! Standarddrakten for wikien din, definert i <code dir=\"ltr\">$wgDefaultSkin</code> som <code>$1</code>, er ikke tilgjengelig.\n\nInstallasjonen din ser ut til å inneholde følgende {{PLURAL:$4|drakt|drakter}}. Se [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] for informasjon om hvordan du kan slå {{PLURAL:$4|denne på|disse på og velge en standarddrakt}}.\n\n$2\n\n; Om du nettopp har installert MediaWiki:\n: Du har trolig installert fra git, eller direkte fra kildekoden med en annen metode. Dette er forventet. Prøv å installere noen drakter fra [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org sin draktbase] ved å\n:* laste ned [https://www.mediawiki.org/wiki/Download tarball-installereren], som kommer med flere drakter og utvidelser. Du kan kopiere og lime inn <code>skins/</code>-mappen fra denne.\n:* laste ned individuelle drakter fra [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Bruk Git for å laste ned drakter].\n: Å gjøre dette skal ikke forstyrre git-mappen din om du er en MediaWiki-utvikler.\n\n; Om du nettopp har oppgradert MediaWiki:\n: MediaWiki 1.24 og nyere slår ikke lenger på automatisk installerte drakter (se [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manual: Skin autodiscovery]). Du kan lime inn følgende {{PLURAL:$5|linje|linjer}} i <code>LocalSettings.php</code> for å slå på {{PLURAL:$5|den|alle}} installerte {{PLURAL:$5|drakten|drakter}}:\n\n<pre dir=\"ltr\">$3</pre>\n\n; Om du nettopp har endret <code>LocalSettings.php</code>:\n: Dobbelsjekk draktnavnene for skrivefeil.",
+       "default-skin-not-found-no-skins": "Ops! Standarddrakten for wikien din, definert i <code>$wgDefaultSkin</code> som <code>$1</code>, er ikke tilgjengelig.\n\nDu har ingen installerte drakter.\n\n;Om du nettopp har installert eller oppgradert MediaWiki:\n: Du installerte trolig fra git, eller direkte fra kildekoden med en annen metode. Dette er forventet. MediaWiki 1.24 og nyere inkluderer ingen drakter i hovedarkivet. Prøv å installere noen drakter fra [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.orgs draktmappe], ved å:\n:* laste ned [https://www.mediawiki.org/wiki/Download tarball-installereren], som kommer med mange drakter og tillegg. Du kan kopiere og lime inn <code>skins/</code>-mappen fra denne.\n:* laste ned individuelle drakt-tarballer fra [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Bruk Git for å laste ned drakter].\n: Å gjøre dette vil ikke forstyrre ditt git-arkiv om du er en MediaWiki-utvikler. Se [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual:Skin configuration] for informasjon om hvordan du slår på drakter og velger en standarddrakt.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (slått på)",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>slått av</strong>)",
        "mediastatistics": "Mediestatistikk",
        "special-characters-group-ipa": "IPA",
        "special-characters-group-symbols": "Symboler",
        "special-characters-group-greek": "Gresk",
+       "special-characters-group-greekextended": "Utvidet gresk",
        "special-characters-group-cyrillic": "Kyrillisk",
        "special-characters-group-arabic": "Arabisk",
        "special-characters-group-arabicextended": "Utvidet arabisk",
        "mw-widgets-dateinput-placeholder-month": "ÅÅÅÅ-MM",
        "mw-widgets-titleinput-description-new-page": "siden eksisterer ikke ennå",
        "mw-widgets-titleinput-description-redirect": "omdiriger til $1",
+       "sessionmanager-tie": "Kan ikke kombinere flere forespørselsautentiseringstyper: $1",
        "sessionprovider-generic": "$1 sesjoner",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "informasjons&shy;kapsel-baserte sesjoner",
-       "randomrootpage": "Tilfeldig rotside"
+       "sessionprovider-nocookies": "Informasjonskapsler er kanskje slått av. Sjekk at du har slått på informasjonskapsler og prøv igjen.",
+       "randomrootpage": "Tilfeldig rotside",
+       "log-action-filter-block": "Type blokkering:",
+       "log-action-filter-contentmodel": "Type innholdsmodellendring:",
+       "log-action-filter-delete": "Type sletting:",
+       "log-action-filter-import": "Type import:",
+       "log-action-filter-managetags": "Type tagghåndteringshandling:",
+       "log-action-filter-move": "Type flytting:",
+       "log-action-filter-newusers": "Type kontooppretting:",
+       "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-block-reblock": "Blokkeringsendring",
+       "log-action-filter-block-unblock": "Avblokkering",
+       "log-action-filter-contentmodel-change": "Endring av innholdsmodell",
+       "log-action-filter-contentmodel-new": "Oppretting av side med ikke-standard innholdsmodell",
+       "log-action-filter-delete-delete": "Sidesletting",
+       "log-action-filter-delete-restore": "Sidegjenoppretting",
+       "log-action-filter-delete-event": "Loggsletting",
+       "log-action-filter-delete-revision": "Revisjonssletting",
+       "log-action-filter-import-interwiki": "Transwiki-importering",
+       "log-action-filter-import-upload": "XML-opplastingsimportering",
+       "log-action-filter-managetags-create": "Taggopprettelse",
+       "log-action-filter-managetags-delete": "Taggsletting",
+       "log-action-filter-managetags-activate": "Taggaktivering",
+       "log-action-filter-managetags-deactivate": "Taggdeaktivering",
+       "log-action-filter-move-move": "Flytting uten overskriving av omdirigeringer",
+       "log-action-filter-move-move_redir": "Flytting med overskriving av omdirigeringer",
+       "log-action-filter-newusers-create": "Opprettelse av anonym bruker",
+       "log-action-filter-newusers-create2": "Opprettelse av registrert bruker",
+       "log-action-filter-newusers-autocreate": "Automatisk opprettelse",
+       "log-action-filter-newusers-byemail": "Opprettelse med passord sendt på epost",
+       "log-action-filter-patrol-patrol": "Manuell patruljering",
+       "log-action-filter-patrol-autopatrol": "Automatisk patruljering",
+       "log-action-filter-protect-protect": "Beskyttelse",
+       "log-action-filter-protect-modify": "Beskyttelsesendring",
+       "log-action-filter-protect-unprotect": "Avbeskyttelse",
+       "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.",
+       "authmanager-authn-no-primary": "De oppgitte akkreditivene kunne ikke autentiseres.",
+       "authmanager-authn-no-local-user": "De oppgitte akkreditivene tilhører ingen bruker på denne wikien.",
+       "authmanager-authn-no-local-user-link": "De oppgitte akkreditivene er gyldige men tilhører ingen brukere på denne wikien. Logg inn på en annen måte eller opprett en ny bruker, og du vil ha mulighet til å lenke dine tidligere akkreditiver med den kontoen.",
+       "authmanager-authn-autocreate-failed": "Autooprrettelse av lokal konto mislyktes: $1",
+       "authmanager-change-not-supported": "De oppgitte akkreditivene kan ikke endres, siden ingenting ville bruke dem.",
+       "authmanager-create-disabled": "Kontoopprettelse er deaktivert.",
+       "authmanager-create-from-login": "For å opprette kontoen din, fyll inn feltene nedenfor.",
+       "authmanager-create-not-in-progress": "Kontoopprettelse foregår ikke eller sesjonsdata er tapt. Start igjen fra begynnelsen.",
+       "authmanager-create-no-primary": "De oppgitte akkreditivene kunne ikke brukes for kontooppretting.",
+       "authmanager-link-no-primary": "De oppgitte akkreditivene kunne ikke brukes for kontolenking.",
+       "authmanager-link-not-in-progress": "Kontolenking foregår ikke eller sesjonsdata er tapt. Start igjen fra begynnelsen.",
+       "authmanager-authplugin-setpass-failed-title": "Passordendring mislyktes",
+       "authmanager-authplugin-setpass-failed-message": "Autentiseringspluginen avviste passordendringen.",
+       "authmanager-authplugin-create-fail": "Autentiseringspluginen avviste kontoopprettelsen.",
+       "authmanager-authplugin-setpass-denied": "Autentiseringspluginen tillater ikke endring av passord.",
+       "authmanager-authplugin-setpass-bad-domain": "Ugyldig domene.",
+       "authmanager-autocreate-noperm": "Automatisk kontoopprettelse tillates ikke.",
+       "authmanager-autocreate-exception": "Automatisk kontoopprettelse er midlertidig deaktivert på grunn av tidligere feil.",
+       "authmanager-userdoesnotexist": "Brukerkontoen «$1» er ikke registrert.",
+       "authmanager-userlogin-remembermypassword-help": "Hvorvidt passordet skal huskes lenger enn sesjonslengden.",
+       "authmanager-username-help": "Brukernavn for autentisering.",
+       "authmanager-password-help": "Passord for autentisering.",
+       "authmanager-domain-help": "Domene for ekstern autentisering.",
+       "authmanager-retype-help": "Passord igjen for å bekrefte.",
+       "authmanager-email-label": "Epost",
+       "authmanager-email-help": "Epostadresse",
+       "authmanager-realname-label": "Virkelig navn",
+       "authmanager-realname-help": "Brukerens virkelige navn",
+       "authmanager-provider-password": "Passordbasert autentisering",
+       "authmanager-provider-password-domain": "Passord- og domenebasert autentisering",
+       "authmanager-provider-temporarypassword": "Midlertidig passord",
+       "authprovider-confirmlink-message": "Basert på dine nylige innloggingsforsøk kan følgende kontoer lenkes til den wikikonto. Å lenke dem muliggjør innlogging via de kontoene. Vennligst velg hvilke som skal lenkes.",
+       "authprovider-confirmlink-request-label": "Kontoer som skal lenkes",
+       "authprovider-confirmlink-success-line": "$1: Lenking gjennomført.",
+       "authprovider-confirmlink-failed": "Konto kunne ikke lenkes fullstendig: $1",
+       "authprovider-confirmlink-ok-help": "Fortsett etter at feilmeldinger om lenking har blitt vist.",
+       "authprovider-resetpass-skip-label": "Hopp over",
+       "authprovider-resetpass-skip-help": "Hopp over nullstilling av passordet.",
+       "authform-nosession-login": "Autentiseringen lyktes, men nettleseren din kan ikke «huske» å være innlogget.\n\n$1",
+       "authform-nosession-signup": "Kontoen ble opprettet, men nettleseren kan ikke «huske» å være innlogget.\n\n$1",
+       "authform-newtoken": "Manglende nøkkel. $1",
+       "authform-notoken": "Mangler nøkkel",
+       "authform-wrongtoken": "Feil nøkkel",
+       "specialpage-securitylevel-not-allowed-title": "Ikke tillatt",
+       "specialpage-securitylevel-not-allowed": "Beklager, du har ikke tillatelse til å bruke denne siden fordi identiteten din ikke kunne bekreftes.",
+       "authpage-cannot-login": "Kunne ikke starte innlogging.",
+       "authpage-cannot-login-continue": "Kunne ikke fortsette innlogging. Sesjonen din har trolig fått et tidsavbrudd.",
+       "authpage-cannot-create": "Kunne ikke starte kontoopprettelse.",
+       "authpage-cannot-create-continue": "Kunne ikke fortsette kontoopprettelse. Sesjonen din har trolig fått et tidsavbrudd.",
+       "authpage-cannot-link": "Kunne ikke starte kontolenking.",
+       "authpage-cannot-link-continue": "Kunne ikke fortsette kontolenking. Sesjonen din har trolig fått et tidsavbrudd.",
+       "cannotauth-not-allowed-title": "Ingen tilgang",
+       "cannotauth-not-allowed": "Du har ikke tillatelse til å bruke denne siden",
+       "changecredentials": "Endre akkreditiver",
+       "changecredentials-submit": "Endre akkreditiver",
+       "changecredentials-invalidsubpage": "$1 er ikke en gyldig akkreditivtype.",
+       "changecredentials-success": "Akkreditivene dine har blitt endret.",
+       "removecredentials": "Fjern akkreditiver",
+       "removecredentials-submit": "Fjern akkreditiver",
+       "removecredentials-invalidsubpage": "$1 er ikke en gyldig akkreditivtype.",
+       "removecredentials-success": "Akkreditivene dine har blitt fjernet.",
+       "credentialsform-provider": "Akkreditivtype:",
+       "credentialsform-account": "Kontonavn:",
+       "cannotlink-no-provider-title": "Det er ingen kontoer som kan lenkes",
+       "cannotlink-no-provider": "Det er ingen kontoer som kan lenkes.",
+       "linkaccounts": "Lenk kontoer",
+       "linkaccounts-success-text": "Kontoen ble lenket.",
+       "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.",
+       "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 fd3ccea..0d7ce83 100644 (file)
@@ -28,6 +28,7 @@
        "tog-hideminor": "सामान्य सम्पादनहरूलाई नयाँ परिवर्तनहरूबाट लुकाउने",
        "tog-hidepatrolled": "गस्ती गरिएका सम्पादनहरूलाई नयाँ परिवर्तनहरूबाट लुकाउने",
        "tog-newpageshidepatrolled": "गस्ती गरिएका पृष्ठहरूलाई नयाँ पृष्ठ सूचीबाट लुकाउने",
+       "tog-hidecategorization": "पृष्ठहरूको श्रेणीकरण हटाउनुहोस्",
        "tog-extendwatchlist": "निगरानी सूचीलाई सबै परिवर्तनहरू देखाउने गरी बढाउने, हालैको परिवर्तनहरू बाहेक",
        "tog-usenewrc": "पृष्ठका भर्खरका परिवर्तन र अवलोकन सूचीको आधारमा सामूहिक परिवर्तनहरू",
        "tog-numberheadings": "शीर्षकहरूलाई स्वत:अङ्कित गर्नुहोस्",
@@ -38,6 +39,7 @@
        "tog-watchdefault": "मैले सम्पादन गरेको पृष्ठ र फाइल निगरानी सूचीमा थप्ने",
        "tog-watchmoves": "मैले सारेका पृष्ठहरू र फाइलहरूलाई निगरानी सूचीमा थप्ने",
        "tog-watchdeletion": "मैले हटाएका पृष्ठहरू र फाइलहरूलाई निगरानी सूचीमा थप्ने",
+       "tog-watchuploads": "मेरा नयाँ फाइलहरूलाई मेरो निगरानी सूचीमा राख्ने ।",
        "tog-watchrollback": "मैले रोलब्याक गरेका पृष्ठहरूलाई मेरो निगरानी सूचीमा थप्ने।",
        "tog-minordefault": "सबै सम्पादनहरूलाई पूर्वनिर्धारित रुपमा सामान्य चिनो लगाउने",
        "tog-previewontop": "सम्पादन सन्दुक अघि पूर्वरुप देखाउने",
        "tog-watchlisthidebots": "बोट सम्पादनहरू निगरानी सूचीबाट लुकाउने",
        "tog-watchlisthideminor": "सामान्य सम्पादनहरू निगरानी सूचीबाट लुकाउने",
        "tog-watchlisthideliu": "प्रवेश गरेका प्रयोगकर्ताहरूको सम्पादन निगरानी सूचीबाट लुकाउने",
+       "tog-watchlistreloadautomatically": "जहिले पनि छननी बदल्न निगरानी सूचीलाई आफै लोड गर्नुहोस् (जावास्क्रिप्ट अनिवार्य)",
        "tog-watchlisthideanons": "अज्ञात प्रयोगकर्ताहरूबाट गरिएको सम्पादन निगरानी सूचीबाट लुकाउने",
        "tog-watchlisthidepatrolled": "गस्ती गरिएका सम्पादनहरू मेरो निगरानी सूचीबाट लुकाउने",
+       "tog-watchlisthidecategorization": "पृष्ठहरूको श्रेणीकरण हटाउनुहोस्",
        "tog-ccmeonemails": "मैले अन्य प्रयोगकर्ताहरूलाई पठाउने इ-मेलको प्रतिलिपि मलाई पठाउने",
        "tog-diffonly": "तलका पृष्ठहरूको भिन्नहरू सामग्री नदेखाउने",
        "tog-showhiddencats": "लुकाइएको श्रेणीहरू देखाउने",
        "october-date": "अक्टोबर $1",
        "november-date": "नोभेम्बर $1",
        "december-date": "डिसेम्बर $1",
+       "period-am": "पूर्वाह्न",
+       "period-pm": "अपराह्न",
        "pagecategories": "{{PLURAL:$1|श्रेणी|श्रेणीहरू}}",
        "category_header": "\"$1\" श्रेणीमा भएका लेखहरू",
        "subcategories": "उपश्रेणीहरू",
        "newwindow": "(नयाँ विन्डोमा खुल्छ)",
        "cancel": "रद्द",
        "moredotdotdot": "थप...",
-       "morenotlisted": "यà¥\8b à¤¸à¥\82à¤\9aà¥\80 à¤ªà¥\82रà¥\8dण à¤¹à¥\88न।",
+       "morenotlisted": "यà¥\8b à¤¸à¥\82à¤\9aà¥\80 à¤ªà¥\82रà¥\8dण à¤\9bà¥\88न ।",
        "mypage": "पृष्ठ",
        "mytalk": "वार्ता",
        "anontalk": "वार्ता",
        "qbedit": "सम्पादन गर्ने",
        "qbpageoptions": "यो पेज",
        "qbmyoptions": "मेरो पेज",
-       "faq": "धà¥\88रà¥\88 à¤¸à¥\8bधिà¤\8fà¤\95ा à¤ªà¥\8dरशà¥\8dनहरà¥\81",
-       "faqpage": "Project:धà¥\88रà¥\88 à¤¸à¥\8bधिà¤\8fà¤\95ा à¤ªà¥\8dरशà¥\8dनहरà¥\81",
+       "faq": "धà¥\88रà¥\88 à¤¸à¥\8bधिà¤\8fà¤\95ा à¤ªà¥\8dरशà¥\8dनहरà¥\82",
+       "faqpage": "Project:धà¥\88रà¥\88 à¤¸à¥\8bधिà¤\8fà¤\95ा à¤ªà¥\8dरशà¥\8dनहरà¥\82",
        "actions": "कार्यहरु",
        "namespaces": "नेमस्पेस",
        "variants": "बहुरुपहरू",
        "tagline": "{{SITENAME}}बाट",
        "help": "सहयोग",
        "search": "खोज्ने",
+       "search-ignored-headings": " #<!-- leave this line exactly as it is --> <pre>\n# Headings that will be ignored by search.\n# Changes to this take effect as soon as the page with the heading is indexed.\n# You can force page reindexing by doing a null edit.\n# The syntax is as follows:\n#   * Everything from a \"#\" character to the end of the line is a comment.\n#   * Every non-blank line is the exact title to ignore, case and everything.\nReferences\nExternal links\nSee also\n #</pre> <!-- leave this line exactly as it is -->",
        "searchbutton": "खोज्नुहोस्",
        "go": "जाने",
        "searcharticle": "खोज्ने",
        "backlinksubtitle": "← $1",
        "retrievedfrom": " \"$1\" बाट निकालिएको",
        "youhavenewmessages": "तपाईंको लागि ($2) मा  $1 छ ।",
-       "youhavenewmessagesfromusers": "तपाईंको लागि  {{PLURAL:$3|प्रयोगकर्ता|$3 प्रयोगकर्ताहरू}} ($2) बाट $1",
+       "youhavenewmessagesfromusers": "तपाईंको लागि {{PLURAL:$3|प्रयोगकर्ता|$3 प्रयोगकर्ताहरू}} का $1 छन् । ($2)",
        "youhavenewmessagesmanyusers": "तपाईँलाई धेरै प्रयोगकर्ताहरू($2) बाट $1 छ ।",
        "newmessageslinkplural": "{{PLURAL:$1|एउटा नयाँ सन्देश|999=नयाँ सन्देशहरू}}",
        "newmessagesdifflinkplural": "अन्तिम {{PLURAL:$1|परिवर्तन|999=परिवर्तनहरू}}",
        "databaseerror-query": "क्वेरी: $1",
        "databaseerror-function": "फङ्सन : $1",
        "databaseerror-error": "त्रुटि: $1",
+       "transaction-duration-limit-exceeded": "To avoid creating high replication lag, this transaction was aborted because the write duration ($1) exceeded the $2 second limit.\nIf you are changing many items at once, try doing multiple smaller operations instead.",
        "laggedslavemode": "<strong>चेतावनी:</strong> पृष्ठमा हालका अद्यतनहरू नहुनसक्छन् ।",
        "readonly": "डेटाबेस बन्द गरिएको छ",
        "enterlockreason": "ताल्चा मार्नुको कारण दिनुहोस्, साथै ताल्चा हटाउने समयको अवधि अनुमान लगाउनुहोस्।",
        "missingarticle-rev": "(संशोधन #: $1)",
        "missingarticle-diff": "(परि: $1, $2)",
        "readonly_lag": "डेटाबेस स्वतः बन्द गरिएको छ जबकि अधिनस्थ डेटाबेस सर्वरले मूल पहिल्याउँदैछ।",
+       "nonwrite-api-promise-error": "'Promise-Non-Write-API-Action' लाई एचटीटीपी शीर्षक द्वारा पठाईयो तर एपीआईमा लेखन मोडल छ ।",
        "internalerror": "आन्तरिक त्रुटि",
        "internalerror_info": "आन्तरिक त्रुटि: $1",
        "internalerror-fatal-exception": "प्रकारको गम्भीर अपवाद \"$1\"",
        "viewsource": "स्रोत हेर्नुहोस",
        "viewsource-title": " $1 को स्रोत हेर्नुहोस",
        "actionthrottled": "कार्य रोकियो",
-       "actionthrottledtext": "सà¥\8dपाम à¤°à¥\8bà¤\95थामà¤\95à¥\8b à¤²à¤¾à¤\97ि , à¤¤à¤ªà¤¾à¤\88à¤\81लाई यो कार्य थोरै समयमा धेरै पटक गर्नबाट सिमित गरिएको छ, र तपाईंले आफ्नो सिमा पार गरिसक्नु भयो ।\nकृपया केही मिनेट पछि पुन: प्रयास गर्नुहोस्  ।",
+       "actionthrottledtext": "सà¥\8dपाम à¤°à¥\8bà¤\95थामà¤\95à¥\8b à¤²à¤¾à¤\97ि , à¤¤à¤ªà¤¾à¤\88à¤\82लाई यो कार्य थोरै समयमा धेरै पटक गर्नबाट सिमित गरिएको छ, र तपाईंले आफ्नो सिमा पार गरिसक्नु भयो ।\nकृपया केही मिनेट पछि पुन: प्रयास गर्नुहोस्  ।",
        "protectedpagetext": "यो पृष्ठ सम्पादन हुनबाट बचाउन सम्पादनमा तथा अन्यकार्यमा रोक लगाइएको छ।",
-       "viewsourcetext": "तपाà¤\88à¤\81लà¥\87 यस पृष्ठको स्रोत हेर्न र प्रतिलिपी गर्न सक्नुहुन्छ ।",
-       "viewyourtext": "यस à¤ªà¥\83षà¥\8dठमा à¤°à¤¹à¥\87à¤\95ा '''तपाà¤\88à¤\81का सम्पादनहरू''' हेर्न या प्रतिलिपी गर्न सक्नुहुन्छ :",
+       "viewsourcetext": "तपाà¤\88à¤\82 यस पृष्ठको स्रोत हेर्न र प्रतिलिपी गर्न सक्नुहुन्छ ।",
+       "viewyourtext": "यस à¤ªà¥\83षà¥\8dठमा à¤°à¤¹à¥\87à¤\95ा '''तपाà¤\88à¤\82का सम्पादनहरू''' हेर्न या प्रतिलिपी गर्न सक्नुहुन्छ :",
        "protectedinterface": "यो पृष्ठले सफ्टवेयरको लागि अन्तरमोहडा पाठ प्रदान गर्दछ , र यसलाई दुरुपयोग हुनबाट बचाउन सुरक्षा प्रादन गरिएको छ।\nसम्पूर्ण विकिहरूका लागि अनुवादमा परिवर्तन गर्नको लागि [https://translatewiki.net/ translatewiki.net], प्रयोग गर्नुहोस् ,  मिडियाविकि स्थानियकरण परियोजना ।",
        "editinginterface": "<strong>चेतावनी:</strong> तपाईं यस पृष्ठलाई सम्पादन गर्नुहुँदैछ, जसले सफ्टवेयरको लागि \nइन्टरफेस सामग्रीहरू प्रदान गर्दछ।\nयस पृष्ठमा गरिएकोपरिवर्तनले यस विकिमा अरु प्रयोगकर्ताको इन्टरफेसको प्रदर्शनमा प्रभाव पार्नेछ ।",
        "translateinterface": "सबै विकिहरूको लागी अनुवाद जोड्न वा परिवर्तन गर्नका लागि मीडियाविकि क्षेत्रीयकरण परियोजना [https://translatewiki.net/ ट्रान्सलेटविकि.नेट]को प्रयोग गर्नुहोस।",
        "changepassword-throttled": "तपाईंले भर्खरै धेरै पल्ट प्रवेश (लग इन)को निम्ति प्रयास गर्नुभएको छ। \nकृपया $1 पर्खेर मात्र प्रयास गर्नुहोस्।",
        "resetpass_forbidden": "पासवर्ड परिवर्तन गर्न मिल्दैन",
        "resetpass-no-info": "यो पृष्ठ सिधै हेर्नको लागि तपाईँले प्रवेश गर्नुपर्छ ।",
-       "resetpass-submit-loggedin": "पà¥\8dरवà¥\87शशव्द परिवर्तन गर्ने",
+       "resetpass-submit-loggedin": "पà¥\8dरवà¥\87सशब्द परिवर्तन गर्ने",
        "resetpass-submit-cancel": "रद्द गर्ने",
-       "resetpass-wrong-oldpass": "à¤\85सà¥\8dथायà¥\80 à¤\85थवा à¤¹à¤¾à¥\8dलिà¤\8fà¤\95à¥\8b à¤ªà¥\8dरवà¥\87श à¤¶à¤µà¥\8dद à¤\85मानà¥\8dय\nतपाà¤\88à¤\82लà¥\87 à¤\85à¤\98िबाà¤\9f à¤¨à¥\88à¤\82 à¤ªà¥\8dरवà¥\87श à¤¶à¤µà¥\8dद à¤¸à¤«à¤²à¤¤à¤¾ à¤ªà¥\82रà¥\8dवà¤\95 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रिसà¤\95à¥\8dनà¥\81 à¤­à¤\8fà¤\95à¥\8b à¤¹à¥\8b à¤µà¤¾ à¤¨à¤¯à¤¾à¤\81 à¤ªà¥\8dरवà¥\87श à¤¶à¤µ्दको निम्ति निवेदन गर्नुभएकोछ।",
+       "resetpass-wrong-oldpass": "à¤\85सà¥\8dथायà¥\80 à¤\85थवा à¤¹à¤¾à¤²à¤¿à¤\8fà¤\95à¥\8b à¤ªà¥\8dरवà¥\87सशबà¥\8dद à¤\85मानà¥\8dय\nतपाà¤\88à¤\82लà¥\87 à¤\85à¤\98िबाà¤\9f à¤¨à¥\88à¤\82 à¤ªà¥\8dरवà¥\87सशबà¥\8dद à¤¸à¤«à¤²à¤¤à¤¾ à¤ªà¥\82रà¥\8dवà¤\95 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रिसà¤\95à¥\8dनà¥\81 à¤­à¤\8fà¤\95à¥\8b à¤¹à¥\8b à¤µà¤¾ à¤¨à¤¯à¤¾à¤\81 à¤ªà¥\8dरवà¥\87सशब्दको निम्ति निवेदन गर्नुभएकोछ।",
        "resetpass-recycled": "कृपया वर्तमान पासर्वड भन्दा फरक पासर्वडलाई पुनः मिलाउनुहोस् ।",
        "resetpass-temp-emailed": "तपाईं अस्थाई इमेल कोडले प्रवेश गर्नुभएको छ।\nप्रवेश सफल पार्नका लागि, तपाईंले यहाँ एउटा नयाँ पासवर्ड राख्नु पर्नेछ:",
        "resetpass-temp-password": "अस्थाइ पासवर्ड",
        "passwordreset-capture-help": "यदि तपाईंले यो कोठामा दाग दिनुभयो भनें यो इमेल (अस्थायी पासवर्ड सहित) तपाईंलाई देखा पर्नेछ साथै प्रयोगकर्तालाई पनि पठाइनेछ।",
        "passwordreset-email": "इमेल ठेगाना:",
        "passwordreset-emailtitle": "{{SITENAME}}मा खाता विवरण",
-       "passwordreset-emailtext-ip": "कसैले (सायद तपाईंले, $1 आईपि ठेगानाबाट) {{SITENAME}} ($4)मा तपाईंको खाता विवरणको निम्ति एउटा अनुस्मारकको अनुरोध गरेको छ। निम्न प्रयोगकर्ता {{PLURAL:$3|खाता यस इमेल ठेगानासित सम्बन्धित छ|खाताहरू यस इमेल ठेगानासित सम्बन्धित छन्}}:\n\n$2\n\n{{PLURAL:$3|यो अस्थाई पासवर्डको|यी अस्थाई पासवर्डहरुको}} समय {{PLURAL:$5|एक दिन|$5 दिन}}मा सकिनेछ।\nतपाईंले प्रवेश गरेर अहिले नैं नयाँ पासवर्ड छान्नुहोस्। यदि अरु कसैले अनुरोध गरेको भए अथवा यदि तपाईंलाई मूल पासवर्ड याद भए अनि यसलाई परिवर्तन गर्न चाहनुहुन्न भने, तपाईंले यस सन्देशलाई अनदेखा गर्नुहोस् र पुरानै पासवर्डलाई चालू राख्नुहोस्।",
-       "passwordreset-emailtext-user": "{{SITENAME}} को $1 प्रयोगकर्ताले  {{SITENAME}} ($4)को लागि खाता विवरणको निम्ति एउटा अनुस्मारकको अनुरोध गरेको छ। निम्न प्रयोगकर्ता {{PLURAL:$3|खाता यस इमेल ठेगानासित सम्बन्धित छ|खाताहरू यस इमेल ठेगानासित सम्बन्धित छन्}}:\n\n$2\n\n{{PLURAL:$3|यो अस्थाई पासवर्डको|यी अस्थाई पासवर्डहरूको}} समय {{PLURAL:$5|एक दिन|$5 दिन}}मा सकिनेछ।\nतपाईंले प्रवेश गरेर अहिले नैं नयाँ पासवर्ड छान्नुहोस्। यदि अरु कसैले अनुरोध गरेको भए अथवा यदि तपाईंलाई मूल पासवर्ड याद भए अनि यसलाई परिवर्तन गर्न चाहनुहुन्न भनें, तपाईंले यस सन्देशलाई अनदेखा गर्नुहोस् र पुरानै पासवर्डलाई चालू राख्नुहोस्।",
+       "passwordreset-emailtext-ip": "कसैले (सायद तपाईंले, $1 आईपि ठेगानाबाट) {{SITENAME}} ($4)मा तपाईंको खाता विवरणको निम्ति एउटा अनुस्मारकको अनुरोध गरेको छ। निम्न प्रयोगकर्ता {{PLURAL:$3|खाता यस इमेल ठेगानासित सम्बन्धित छ|खाताहरू यस इमेल ठेगानासित सम्बन्धित छन्}}:\n\n$2\n\n{{PLURAL:$3|यो अस्थाई पासवर्डको|यी अस्थाई पासवर्डहरुको}} समय {{PLURAL:$5|एक दिन|$5 दिन}}मा सकिनेछ।\nतपाईंले प्रवेश गरेर अहिले नैं नयाँ पासवर्ड छान्नुहोस्। यदि अरु कसैले अनुरोध गरेको भए अथवा यदि तपाईंलाई मूल पासवर्ड याद भए अनि यसलाई परिवर्तन गर्न चाहनुहुन्न भने, तपाईंले यस सन्देशलाई अनदेखा गर्नुहोस् र पुरानै पासवर्डलाई चालू राख्नुहोस्।",
+       "passwordreset-emailtext-user": "{{SITENAME}} को $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": "पासवर्ड परिवर्तनको लागि इमेल पठाइएको छ।",
        "changeemail": "इमेल ठेगाना परिवर्तन गर्नुहोस",
        "minoredit": "यो सानो सम्पादन हो",
        "watchthis": "यो पृष्ठ अवलोकन गर्नुहोस्",
        "savearticle": "सङ्ग्रह गर्ने",
+       "savechanges": "परिवर्तन सङ्ग्रह गर्नुहोस्",
        "preview": "पूर्वावलोकन",
        "showpreview": "पूर्वालोकन देखाउनुहोस्",
        "showdiff": "परिवर्तन देखाउनुहोस्",
        "nextn": "अर्को {{PLURAL:$1|$1}}",
        "prev-page": "अघिल्लो पृष्ठ",
        "next-page": "अर्को पृष्ठ",
-       "prevn-title": "पहिलà¥\87à¤\95à¥\8b  $1 {{PLURAL:$1|नतिà¤\9cा|नतिà¤\9cाहरà¥\81}}",
-       "nextn-title": "यस à¤ªà¤\9bिà¤\95à¥\8b $1 {{PLURAL:$1|नतिà¤\9cा |नतिà¤\9cाहरà¥\81}}",
+       "prevn-title": "पहिलà¥\87à¤\95à¥\8b  $1 {{PLURAL:$1|नतिà¤\9cा|नतिà¤\9cाहरà¥\82}}",
+       "nextn-title": "यस à¤ªà¤\9bिà¤\95à¥\8b $1 {{PLURAL:$1|नतिà¤\9cा |नतिà¤\9cाहरà¥\82}}",
        "shown-title": "देखाउने $1 {{PLURAL:$1|नतिजा|नतिजाहरू}} प्रति पृष्ठ",
        "viewprevnext": "हेर्नुहोस् ($1 {{int:pipe-separator}} $2) ($3)",
        "searchmenu-exists": "''' \"[[:$1]]\" नाम गरेको पृष्ठ  यो विकीमा रहेको छ'''",
        "userrights-nodatabase": "डेटाबेस $1 उपलब्ध छैन या स्थानीय हैन।",
        "userrights-nologin": "प्रयोगकर्ता अधिकार प्रदान गर्न तपाईंले प्रबन्धक खाताबाट [[Special:UserLogin|प्रवेश]] गर्नुपर्छ।",
        "userrights-notallowed": "प्रयोगकर्तालाई अधिकार प्रदान गर्ने वा हटाउने अनुमति तपाईंलाई छैन।",
-       "userrights-changeable-col": "परिवरà¥\8dतन à¤\97रà¥\8dन à¤¸à¤\95िनà¥\87 à¤¸à¤®à¥\82हहरà¥\81",
+       "userrights-changeable-col": "तपाà¤\88à¤\82लà¥\87 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रà¥\8dन à¤¸à¤\95à¥\8dनà¥\87 à¤¸à¤®à¥\82हहरà¥\82",
        "userrights-unchangeable-col": "तपाईंले परिवर्तन गर्न नसक्ने समूहहरू",
        "userrights-irreversible-marker": "$1*",
        "userrights-conflict": "प्रयोगकर्ताको अधिकार परिवर्तनमा मतभेद भयो ! कृपया तपाईंको परिवर्तन पुनरावलोकन तथा पुष्टि गर्नुहोस् ।",
        "right-purge": "साइटको क्याश( cache) निश्चित नगरिकनै पर्ज(Purge) गर्ने",
        "right-autoconfirmed": "आइपी दर सीमाले असर नपार्ने",
        "right-bot": "स्वाचालित कार्यको रुपमा व्यवहार गर्ने",
-       "right-nominornewtalk": "वारà¥\8dता à¤ªà¥\83षà¥\8dठहरà¥\82मा à¤¸à¤¾à¤¨à¥\8b à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रà¥\8dदा à¤ªà¥\8dरयà¥\8bà¤\97à¤\95रà¥\8dताहरà¥\82लाà¤\88 \"तपाà¤\88à¤\82à¤\95à¥\8b à¤²à¤¾à¤\97ि à¤¨à¤¯à¤¾à¤\81 à¤¸à¤¨à¥\8dदà¥\87श à¤\9b\" à¤­à¤¨à¥\80 à¤¨à¥\8dदà¥\87à¤\96ाà¤\89नà¥\87",
+       "right-nominornewtalk": "वार्ता पृष्ठहरूमा सानो परिवर्तन गर्दा प्रयोगकर्ताहरूलाई \"तपाईंको लागि नयाँ सन्देश छ\" भनी देखाउने",
        "right-apihighlimits": "API खोजको लागि उच्च सीमा प्रयोग गर्नुहोस्",
        "right-writeapi": "लेखन API प्रयोग गर्ने",
        "right-delete": "पृष्ठहरू मेट्ने",
        "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": "à¤\85à¤\98िलà¥\8dलà¥\8b à¤¹à¥\87राà¤\87पà¤\9bिà¤\95ा à¤¸à¤¬à¥\88 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतनहरà¥\81को निम्ति हेर्नुहोस्: $1",
+       "enotif_body_intro_changed": "{{SITENAME}} à¤ªà¥\83षà¥\8dठ $1 à¤²à¤¾à¤\88 {{gender:$2|$2}} à¤²à¥\87 $PAGEEDITDATE à¤®à¤¾ à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रà¥\87à¤\95à¥\8b à¤¹à¥\8b, à¤µà¤°à¥\8dतमान à¤\85वतरण à¤\95à¥\8b à¤²à¤¾à¤\97ि $3 à¤¹à¥\87रà¥\8dनà¥\81हà¥\8bसà¥\8d à¥¤",
+       "enotif_lastvisited": "तपाà¤\88à¤\82à¤\95à¥\8b à¤\85नà¥\8dतिम à¤¹à¥\87राà¤\87पà¤\9bिà¤\95ा à¤¸à¤¬à¥\88 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतनहरà¥\82को निम्ति हेर्नुहोस्: $1",
        "enotif_lastdiff": "यस परिवर्तनको निम्ति यो $1 हेर्नुहोस्",
        "enotif_anon_editor": "अज्ञात  प्रयोगकर्ता  $1",
        "enotif_body": "प्रिय $WATCHINGUSERNAME,\n\n\n{{SITENAME}}को पृष्ठ $PAGETITLE  $PAGEEDITDATE को दिन $PAGEEDITORद्वारा $CHANGEDORCREATED, \nहालको संशोधनको निम्ति हेर्नुहोस्  $PAGETITLE_URL ।\n\n$NEWPAGE\n\nसम्पादकको सारांश: $PAGESUMMARY $PAGEMINOREDIT\n\nसम्पादकसित सम्पर्क राख्नुहोस्:\nमेल: $PAGEEDITOR_EMAIL\nविकि: $PAGEEDITOR_WIKI\n\nतपाईं यस पृष्ठमा नगएसम्म अब उसो कुनै परिवर्तन भएका खण्डमा कुनै सूचना दिनेछैन।\nतपाईंका सम्पूर्ण निगरानी पृष्ठहरूको लागि तपाईंले सूचना पताकालाई निगरानी सूचीमा पुनर्बहाली गर्न सक्नुहुन्छ। \n\n             तपाईंको मित्र {{SITENAME}} सूचना प्रणाली\n--\nइमेल सूचना व्यवस्था परिवर्तन गर्न, जानुहोस्\n{{canonicalurl:{{#special:Preferences}}}}\n\nनिगरानी सूची व्यवस्थित गर्न, जानुहोस्\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nनिगरानी सूची मेट्न, जानुहोस्\n$UNWATCHURL\n\nप्रतिक्रिया र अन्य सहयोगको निम्ति:\n$HELPPAGE",
        "sp-contributions-blocklog": "रोकावट लग",
        "sp-contributions-suppresslog": "प्रयोगकर्ताको योगदानहरू दबाइएको छ ।",
        "sp-contributions-deleted": "प्रयोगकर्ताका योगदानहरू मेटाइयो",
-       "sp-contributions-uploads": "à¤\89रà¥\8dधà¥\8dवभरणहरà¥\81",
+       "sp-contributions-uploads": "à¤\89रà¥\8dधà¥\8dवभरणहरà¥\82",
        "sp-contributions-logs": "लगहरू",
        "sp-contributions-talk": "वार्ता",
        "sp-contributions-userrights": "प्रयोगकर्ता अधिकार व्यवस्थापन",
        "yesterday-at": "हिजो $1मा",
        "bad_image_list": "(* बाट शुरु हुने पंक्ति)को  विषय सूची मात्र मान्य छ।  पंक्तिको पहिलो लिङ्क नराम्रो फाइलसित लिङ्क हुनैपर्छ । एउटै पंक्तिमा कुनै पछिबाट हुने लिंकलाई अपवाद मानिनेछ अर्थात् जुन पृष्ठमा फाइल इन-लाइन हुनसक्छ।",
        "metadata": "मेटाडेटा",
-       "metadata-help": "यस à¤«à¤¾à¤\87लमा à¤\85तिरिà¤\95à¥\8dत à¤\9cानà¤\95ारà¥\80हरà¥\82 à¤\9bनà¥\8d, à¤¯à¤¸à¤²à¤¾à¤\88 à¤¬à¤¨à¤¾à¤\89न à¤¸à¤®à¥\8dभवतà¤\83 à¤¡à¤¿à¤\9cिà¤\9fल à¤\95à¥\8dयामà¥\87रा à¤\85थवा à¤¸à¥\8dà¤\95à¥\8dयानर à¤ªà¥\8dरयà¥\8bà¤\97 à¤\97रिà¤\8fà¤\95à¥\8b à¤¹à¥\81नà¥\81परà¥\8dà¤\9b à¥¤ à¤¯à¤¦à¤¿ à¤¯à¤¸ à¤«à¤¾à¤\87ललाà¤\88 à¤®à¥\82ल à¤\85वसà¥\8dथाबाà¤\9f à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रिà¤\8fà¤\95à¥\8b à¤¹à¥\8b à¤­à¤¨à¥\87  à¤¯à¤¸ à¤«à¤¾à¤\87ललà¥\87  à¤¸à¤®à¥\8dपà¥\82रà¥\8dण à¤µà¤¿à¤µà¤°à¤£ à¤ªà¥\8dरतिबिमà¥\8dबित à¤\97रà¥\8dन à¤¸à¤\95à¥\8dनà¥\87à¤\9bà¥\88न à¥¤",
+       "metadata-help": "यस फाइलमा अतिरिक्त जानकारीहरू छन्, यसलाई बनाउन सम्भवतः डिजिटल क्यामरा अथवा स्क्यानर प्रयोग गरिएको हुनुपर्छ । यदि यस फाइललाई मूल अवस्थाबाट परिवर्तन गरिएको हो भने  यस फाइलले  सम्पूर्ण विवरण प्रतिबिम्बित गर्न सक्नेछैन ।",
        "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",
        "table_pager_limit": "प्रतिपृष्ठ $1 वस्तुहरु देखाउने",
        "table_pager_limit_label": "प्रति पृष्ठ सामग्री:",
        "table_pager_limit_submit": "जाउ",
-       "table_pager_empty": "नतिà¤\9cाहरà¥\81 छैन ।",
+       "table_pager_empty": "नतिà¤\9cाहरà¥\82 छैन ।",
        "autosumm-blank": "पृष्ठ खाली गरीयो",
        "autosumm-replace": "पृष्ठलाई '$1' संग हटाइदै",
        "autoredircomment": "पृष्ठ[[$1]]मा पठाइएको",
        "specialpages-group-other": "अरू विशेष पृष्ठहरू",
        "specialpages-group-login": "प्रवेश गर्ने / नयाँ खाता बनाउने",
        "specialpages-group-changes": "भर्खरैका परिवर्तन र लगहरू",
-       "specialpages-group-media": "मà¥\87डिया à¤ªà¥\8dरतिवà¥\87दन à¤° à¤\89रà¥\8dधà¥\8dवभरणहरà¥\81",
+       "specialpages-group-media": "मिडिया à¤ªà¥\8dरतिवà¥\87दन à¤° à¤\89रà¥\8dधà¥\8dवभरणहरà¥\82",
        "specialpages-group-users": "प्रयोगकर्ता र अधिकारहरु",
        "specialpages-group-highuse": "उच्च प्रयोग भएका पृष्ठहरू",
        "specialpages-group-pages": "पृष्ठहरूको सूची:",
        "tags-title": "ट्यागहरु",
        "tags-intro": "यो पृष्ठले पुच्छरहरु सुचीकृत गर्छ जससँग यो सफ्टवेयरले चिनो लगाउन र सम्पादन गर्न सक्छ र तिनका अर्थहरु ।",
        "tags-tag": "आन्तरिक ट्याग नाम",
-       "tags-display-header": "परिवरà¥\8dतन à¤¸à¥\82à¤\9aà¥\80हरà¥\81माथि झलक",
+       "tags-display-header": "परिवरà¥\8dतन à¤¸à¥\82à¤\9aà¥\80हरà¥\82माथि झलक",
        "tags-description-header": "पूर्ण अर्थको वर्णन",
        "tags-source-header": "स्रोत",
        "tags-active-header": "सक्रिय?",
        "tags-apply-no-permission": "परिवर्तन ट्यागहरूलाई आफ्नो ट्यागसँग जोड्न तपाईंलाई अनुमति छैन।",
        "tags-apply-not-allowed-one": "ट्याग \"$1\" मानवीय रूपले जोड्न सक्ने अनुमति छैन।",
        "tags-apply-not-allowed-multi": "निम्नलिखित {{PLURAL:$2|ट्यागलाई अनुमति छैन|ट्यागहरूलाई अनुमति छैन}} कि त्यसलाई मानवीय रूपले प्रयोगमा ल्याउन सकियोस: $1",
-       "tags-update-no-permission": "तपाà¤\88à¤\82लाà¤\88 à¤µà¥\8dयà¤\95à¥\8dतिà¤\97त à¤¸à¤\82शà¥\8bधनहरà¥\82 à¤µà¤¾ à¤²à¤\97 à¤ªà¥\8dरविषà¥\8dà¤\9fिहरà¥\82सà¤\81à¤\97 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\9cà¥\8bड़ने वा हटाउने अनुमति छैन।",
+       "tags-update-no-permission": "तपाà¤\88à¤\82लाà¤\88 à¤µà¥\8dयà¤\95à¥\8dतिà¤\97त à¤¸à¤\82शà¥\8bधनहरà¥\82 à¤µà¤¾ à¤²à¤\97 à¤ªà¥\8dरविषà¥\8dà¤\9fिहरà¥\82सà¤\81à¤\97 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\9cà¥\8bडà¥\8dने वा हटाउने अनुमति छैन।",
        "tags-update-add-not-allowed-one": "ट्याग \"\"$1\" लाई मानवीय रूपले जोड्न सकिंदैन।",
        "tags-update-add-not-allowed-multi": "निम्नलिखित {{PLURAL:$2|ट्याग|वा ट्यागहरू}} मानवीय रूपले जोड्न सकिंदैन: $1",
        "tags-update-remove-not-allowed-one": "ट्याग \"$1\" मेटाउने अनुमति छैन ।",
        "htmlform-cloner-create": "अरू जोड्ने",
        "htmlform-cloner-delete": "हटाउने",
        "htmlform-cloner-required": "कम्तिमा एउटामा आवश्यक छ ।",
-       "sqlite-has-fts": "$1 पूरा पाठ खोज समर्थन सहित",
-       "sqlite-no-fts": "$1 पूरा पाठ खोज समर्थन बिना",
        "logentry-delete-delete": "$1 द्वारा पृष्ठ $3 {{GENDER:$2|मेटाइयो}}",
        "logentry-delete-restore": "$3 पृष्ठ $1ले {{GENDER:$2|पुनर्स्थापित}} गरेको हो",
        "logentry-delete-event": "$1 ले $3 पृष्ठको लग {{PLURAL:$5|प्रविष्टि|प्रविष्टिहरू}}को दृश्यता {{GENDER:$2|परिवर्तन गर्यो}}: $4",
        "feedback-external-bug-report-button": "प्राविधिक कार्य पेश गर्नुहोस्",
        "feedback-dialog-title": "प्रतिक्रिया दिनुहोस्",
        "feedback-dialog-intro": "तपाईं तल दिइएको सरल फारमको प्रयोग गरेर आफ्नो प्रतिपुष्टि पठाउन सक्नुहुन्छ। तपाईंको टिप्पणी पृष्ठ \"$1\" स तपाईंको प्रयोगकर्तानामको अगाडी जोडिनेछ ।",
-       "feedback-error-title": "त्रुटि",
        "feedback-error1": "त्रुटीः एपिआईबाट अज्ञात परिणाम",
        "feedback-error2": "त्रुटि: सम्पादन असफल",
        "feedback-error3": "त्रुटीः एपिआईबाट कुनै प्रतिक्रिया नआएको",
        "mediastatistics-header-text": "पाठ",
        "mediastatistics-header-executable": "कार्यान्वयन गर्न मिल्नेहरू",
        "mediastatistics-header-archive": "संकुचित ढाँचाहरू",
-       "mediastatistics-header-total": "सबà¥\88 à¤«à¤¾à¤\87लहरà¥\81",
+       "mediastatistics-header-total": "सबà¥\88 à¤«à¤¾à¤\87लहरà¥\82",
        "json-warn-trailing-comma": "$1 पछाडी रहेको छ {{PLURAL:$1|कोमा को|कोमाहरूको}} जेएसओएनबाट हटाइयो",
        "json-error-unknown": "जेएसओएन मा समस्या छ । समस्याः $1",
        "json-error-depth": "स्ट्याकको अधिकतम गहिराई बढी सकेको छ",
index 442c685..34419b4 100644 (file)
        "tog-enotifminoredits": "Mij e-mailen bij kleine bewerkingen van pagina’s en bestanden op mijn volglijst",
        "tog-enotifrevealaddr": "Mijn e-mailadres weergeven in e-mailberichten",
        "tog-shownumberswatching": "Het aantal gebruikers weergeven dat deze pagina volgt",
-       "tog-oldsig": "Bestaande ondertekening:",
+       "tog-oldsig": "Uw bestaande ondertekening:",
        "tog-fancysig": "Handtekening als wikitekst behandelen (zonder automatische koppeling)",
        "tog-uselivepreview": "Livevoorvertoning gebruiken",
        "tog-forceeditsummary": "Een melding geven bij een lege bewerkingssamenvatting",
        "newwindow": "(opent in een nieuw venster)",
        "cancel": "Annuleren",
        "moredotdotdot": "Meer…",
-       "morenotlisted": "Deze lijst is niet compleet.",
+       "morenotlisted": "Deze lijst kan onvolledig zijn.",
        "mypage": "Gebruikerspagina",
        "mytalk": "Overleg",
        "anontalk": "Overleg",
        "talk": "Overleg",
        "views": "Weergaven",
        "toolbox": "Hulpmiddelen",
+       "tool-link-userrights": "{{GENDER:$1|Gebruikersgroepen}} wijzigen",
+       "tool-link-emailuser": "Deze {{GENDER:$1|gebruiker}} e-mailen",
        "userpage": "Gebruikerspagina bekijken",
        "projectpage": "Projectpagina bekijken",
        "imagepage": "Bestandspagina bekijken",
        "createacct-yourpasswordagain-ph": "Geef het wachtwoord opnieuw in",
        "userlogin-remembermypassword": "Aangemeld blijven",
        "userlogin-signwithsecure": "Beveiligde verbinding gebruiken",
+       "cannotlogin-title": "Niet mogelijk om aan te melden",
+       "cannotlogin-text": "Aanmelden is niet mogelijk.",
        "cannotloginnow-title": "Niet mogelijk om aan te melden",
        "cannotloginnow-text": "Aanmelden is niet mogelijk bij het gebruik van $1.",
+       "cannotcreateaccount-title": "Kan geen accounts aanmaken",
        "yourdomainname": "Uw domein:",
        "password-change-forbidden": "U kunt uw wachtwoord niet wijzigen in deze wiki.",
        "externaldberror": "Er is een fout opgetreden bij het aanmelden bij de database of u hebt geen toestemming uw externe gebruiker bij te werken.",
        "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?",
        "passwordreset-emailelement": "Gebruikersnaam: \n$1\n\nTijdelijk wachtwoord: \n$2",
        "passwordreset-emailsentemail": "Als dit e-mailadres aan uw account gekoppeld is, dan wordt er een e-mail verzonden om uw wachtwoord opnieuw in te stellen.",
        "passwordreset-emailsentusername": "Als er een e-mailadres geregistreerd is voor die gebruikersnaam, dan wordt er een e-mail verzonden om uw wachtwoord opnieuw in te stellen.",
+       "passwordreset-emailerror-capture2": "Het e-mailen naar de {{GENDER:$2|gebruiker}} is mislukt: $1 {{PLURAL:$3|De gebruikersnaam en het wachtwoord|De lijst met gebruikersnamen en wachtwoorden}} wordt hieronder weergegeven.",
        "passwordreset-invalideamil": "Ongeldig e-mailadres",
+       "passwordreset-nodata": "Er is geen gebruikersnaam of e-mailadres opgegeven",
        "changeemail": "E-mailadres wijzigen of verwijderen",
        "changeemail-header": "Vul dit formulier in om uw e-mailadres te wijzigen. Als u het e-mailadres wilt ontkoppelen van uw account, laat het e-mailadres dan leeg als u het formulier opslaat.",
        "changeemail-no-info": "U moet aangemeld zijn om rechtstreeks toegang te hebben tot deze pagina.",
        "searchprofile-advanced-tooltip": "Zoeken in opgegeven naamruimten",
        "search-result-size": "$1 ({{PLURAL:$2|1 woord|$2 woorden}})",
        "search-result-category-size": "{{PLURAL:$1|1 categorielid|$1 categorieleden}} ({{PLURAL:$2|1 ondercategorie|$2 ondercategorieën}}, {{PLURAL:$3|1 bestand|$3 bestanden}})",
-       "search-redirect": "(doorverwijzing $1)",
+       "search-redirect": "(doorverwijzing vanaf $1)",
        "search-section": "(subkop $1)",
        "search-category": "(categorie $1)",
        "search-file-match": "(komt overeen met de inhoud van het bestand)",
        "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",
        "upload-foreign-cant-upload": "Deze wiki is niet geconfigureerd om bestanden te uploaden naar de bestandsrepository op een andere site.",
        "upload-dialog-title": "Bestand uploaden",
        "upload-dialog-button-cancel": "Annuleren",
+       "upload-dialog-button-back": "Terug",
        "upload-dialog-button-done": "Afgerond",
        "upload-dialog-button-save": "Opslaan",
        "upload-dialog-button-upload": "Uploaden",
        "addedwatchtext": "\"[[:$1]]\" en de bijhorende overlegpagina zijn toegevoegd aan uw [[Special:Watchlist|volglijst]].",
        "addedwatchtext-short": "De pagina \"$1\" is aan uw volglijst toegevoegd.",
        "removewatch": "Verwijderen uit volglijst",
-       "removedwatchtext": "\"[[:$1]]\" en de overlegpagina zijn verwijderd van [[Special:Watchlist|uw volglijst]].",
+       "removedwatchtext": "\"[[:$1]]\" en de bijhorende overlegpagina zijn verwijderd van uw [[Special:Watchlist|volglijst]].",
+       "removedwatchtext-talk": "\"[[:$1]]\" en de bijhorende pagina zijn verwijderd van uw [[Special:Watchlist|volglijst]].",
        "removedwatchtext-short": "De pagina \"$1\" is van uw volglijst verwijderd.",
        "watch": "Volgen",
        "watchthispage": "Pagina volgen",
        "undeletedrevisions": "$1 {{PLURAL:$1|versie|versies}} teruggeplaatst",
        "undeletedrevisions-files": "{{PLURAL:$1|1 versie|$1 versies}} en {{PLURAL:$2|1 bestand|$2 bestanden}} teruggeplaatst",
        "undeletedfiles": "{{PLURAL:$1|1 bestand|$1 bestanden}} teruggeplaatst",
-       "cannotundelete": "Het terugplaatsen is mislukt:\n$1",
+       "cannotundelete": "Het terugplaatsen is (gedeeltelijk) mislukt:\n$1",
        "undeletedpage": "'''$1 is teruggeplaatst'''\n\nIn het [[Special:Log/delete|verwijderingslogboek]] staan recente verwijderingen en herstelhandelingen.",
        "undelete-header": "Zie het [[Special:Log/delete|verwijderingslogboek]] voor recent verwijderde pagina's.",
        "undelete-search-title": "Verwijderde pagina's zoeken",
        "sp-contributions-newbies-title": "Bijdragen van nieuwe gebruikers",
        "sp-contributions-blocklog": "blokkeerlogboek",
        "sp-contributions-suppresslog": "onderdrukte gebruikersbijdragen",
-       "sp-contributions-deleted": "verwijderde bijdragen",
+       "sp-contributions-deleted": "verwijderde {{GENDER:$1|bijdragen}}",
        "sp-contributions-uploads": "uploads",
        "sp-contributions-logs": "logboeken",
        "sp-contributions-talk": "overleg",
        "pageinfo-article-id": "Paginanummer",
        "pageinfo-language": "Taal voor de pagina",
        "pageinfo-content-model": "Paginainhoudmodel",
+       "pageinfo-content-model-change": "wijzigen",
        "pageinfo-robot-policy": "Indexering door robots",
        "pageinfo-robot-index": "Toegestaan",
        "pageinfo-robot-noindex": "Niet toegestaan",
        "watchlistedit-raw-done": "Uw volglijst is bijgewerkt.",
        "watchlistedit-raw-added": "Er {{PLURAL:$1|is 1 pagina|zijn $1 pagina's}} toegevoegd:",
        "watchlistedit-raw-removed": "Er {{PLURAL:$1|is 1 pagina|zijn $1 pagina's}} verwijderd:",
-       "watchlistedit-clear-title": "Volglijst gewist",
+       "watchlistedit-clear-title": "Volglijst wissen",
        "watchlistedit-clear-legend": "Volglijst wissen",
        "watchlistedit-clear-explain": "Alle pagina's worden van uw volglijst verwijderd",
        "watchlistedit-clear-titles": "Pagina's:",
        "htmlform-title-not-exists": "$1 bestaat niet.",
        "htmlform-user-not-exists": "<strong>$1</strong> bestaat niet.",
        "htmlform-user-not-valid": "<strong>$1</strong> is geen geldige gebruikersnaam.",
-       "sqlite-has-fts": "Versie $1 met ondersteuning voor \"full-text\" zoeken",
-       "sqlite-no-fts": "Versie $1 zonder ondersteuning voor \"full-text\" zoeken",
        "logentry-delete-delete": "$1 {{GENDER:$2|heeft}} de pagina $3 verwijderd",
        "logentry-delete-restore": "$1 {{GENDER:$2|heeft}} de pagina $3 teruggeplaatst",
        "logentry-delete-event": "$1 {{GENDER:$2|heeft}} de zichtbaarheid van {{PLURAL:$5|een logboekregel|$5 logboekregels}} van $3 gewijzigd: $4",
        "feedback-external-bug-report-button": "Een technische taak indienen",
        "feedback-dialog-title": "Terugkoppeling verzenden",
        "feedback-dialog-intro": "U kunt het eenvoudige formulier gebruiken om uw terugkoppeling in te sturen. Uw reactie wordt toegevoegd aan de pagina \"$1\" samen met uw gebruikersnaam.",
-       "feedback-error-title": "Fout",
        "feedback-error1": "Fout: onbekend resultaat uit de API",
        "feedback-error2": "Fout: de bewerking is mislukt",
        "feedback-error3": "Fout: geen reactie van de API",
        "searchsuggest-containing": "bevat...",
        "api-error-badaccess-groups": "U mag geen bestanden uploaden in deze wiki.",
        "api-error-badtoken": "Interne fout: het token klopt niet.",
+       "api-error-blocked": "U bent geblokkeerd en kunt niet bewerken.",
        "api-error-copyuploaddisabled": "Uploaden via URL is uitgeschakeld op deze server.",
        "api-error-duplicate": "Er {{PLURAL:$1|staat al een bestand|staan al bestanden}} met dezelfde inhoud in de wiki.",
        "api-error-duplicate-archive": "Er {{PLURAL:$1|was al een ander bestand|waren al $1 andere bestanden}}  op de site met dezelfde inhoud, maar {{PLURAL:$1|dat is|die zijn}} verwijderd.",
        "api-error-nomodule": "Interne fout: er is geen uploadmodule ingesteld.",
        "api-error-ok-but-empty": "Interne fout: de server heeft geen gegevens teruggeleverd.",
        "api-error-overwrite": "Het overschrijven van een bestand bestand is niet toegestaan.",
+       "api-error-ratelimited": "U probeert meer bestanden te uploaden in een korte periode dan deze wiki toelaat.\nProbeer het over een aantal minuten opnieuw.",
        "api-error-stashfailed": "Interne fout: de server kon het tijdelijke bestand niet opslaan.",
        "api-error-publishfailed": "Interne fout: de server kon het tijdelijke bestand niet publiceren.",
        "api-error-stasherror": "Er is een fout opgetreden tijdens het uploaden van het bestand naar de tijdelijke opslagruimte.",
        "authmanager-email-help": "E-mailadres",
        "authmanager-realname-label": "Echte naam",
        "authmanager-realname-help": "Echte naam van de gebruiker",
+       "authmanager-provider-password": "Op wachtwoord gebaseerde authenticatie",
        "authmanager-provider-temporarypassword": "Tijdelijk wachtwoord",
        "authprovider-resetpass-skip-label": "Overslaan",
-       "specialpage-securitylevel-not-allowed-title": "Niet toegestaan"
+       "specialpage-securitylevel-not-allowed-title": "Niet toegestaan",
+       "cannotauth-not-allowed-title": "Geen toegang",
+       "changecredentials": "Authenticatiegegevens wijzigen",
+       "changecredentials-submit": "Authenticatiegegevens wijzigen",
+       "changecredentials-success": "Uw authenticatiegegevens zijn gewijzigd.",
+       "removecredentials": "Authenticatiegegevens verwijderen",
+       "removecredentials-submit": "Authenticatiegegevens verwijderen",
+       "removecredentials-success": "Uw authenticatiegegevens zijn verwijderd.",
+       "credentialsform-provider": "Soort authenticatiegegevens:",
+       "credentialsform-account": "Gebruikersnaam:",
+       "cannotlink-no-provider-title": "Er zijn geen accounts om te koppelen",
+       "cannotlink-no-provider": "Er zijn geen accounts om te koppelen.",
+       "linkaccounts": "Accounts koppelen",
+       "linkaccounts-success-text": "Het account is gekoppeld.",
+       "linkaccounts-submit": "Accounts koppelen",
+       "unlinkaccounts": "Accounts ontkoppelen",
+       "unlinkaccounts-success": "Het account is ontkoppeld."
 }
index 58b6b3d..417110e 100644 (file)
        "days": "{{PLURAL:$1|$1 dag|$1 dagar}}",
        "weeks": "{{PLURAL:$1|$1 veke|$1 veker}}",
        "months": "{{PLURAL:$1|éin månad|$1 månader}}",
-       "years": "{{PLURAL:$1|éitt år|$1 år}}",
+       "years": "{{PLURAL:$1|eitt år|$1 år}}",
        "ago": "$1 sidan",
        "just-now": "akkurat no",
        "hours-ago": "$1 {{PLURAL:$1|time|timar}} sidan",
        "htmlform-no": "Nei",
        "htmlform-yes": "Ja",
        "htmlform-chosen-placeholder": "Vel ein",
-       "sqlite-has-fts": "$1 med støtte for fulltekstsøk",
-       "sqlite-no-fts": "$1 utan støtte for fulltekstsøk",
        "logentry-delete-delete": "$1 {{GENDER:$2|sletta}} sida $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|attoppretta}} sida $3",
        "logentry-delete-event": "$1 {{GENDER:$2|endra}} synlegdomen av {{PLURAL:$5|éi loggoppføring|$5 loggoppføringar}} på $3: $4",
index 750bc46..ef0bd68 100644 (file)
@@ -50,7 +50,7 @@
        "tog-showhiddencats": "Ozuta peitetyt kategouriet",
        "tog-norollbackdiff": "Älä ozuta eroloi, konzu olet ottanuh järilleh aijemban versien järilleh tuondu -toimindol",
        "tog-useeditwarning": "Ollen lähtemäs sivulpäi iäre tallendamattah muutoksii, huomaita minuu",
-       "tog-prefershttps": "Käytä ainos suojattuu yhtevytty ku olet kirjutannuhes",
+       "tog-prefershttps": "Käytä ainos suojattuu yhtevytty, ku olet kirjutannuhes",
        "underline-always": "Ainos",
        "underline-never": "Nikonzu",
        "underline-default": "Käytä livaimen piäazetuksii",
        "minoredit": "Tämä on pieni kohendus",
        "watchthis": "Tarkaile tädä sivuu",
        "savearticle": "Tallenda sivu",
+       "savechanges": "Tallenda muutokset",
        "preview": "Ezikačo",
        "showpreview": "Ezikačo",
        "showdiff": "Luajitut kohendukset",
        "content-model-text": "perustekstu",
        "content-model-javascript": "JavaScript",
        "content-json-empty-object": "Tyhjy objektu",
-       "cantcreateaccounttitle": "Ei voi luadie tunnustu",
        "cantcreateaccount-text": "Tunnuksien luadimine täs IP-adressaspäi ('''$1''') on estetty. Estäjänny on [[User:$3|$3]].\n\nKäyttäjän $3 annettu syy on ''$2''",
        "cantcreateaccount-range-text": "Tunnuksien luadimine IP-adressilois adressualovehel <strong>$1</strong>, kuduah kuuluu sinungi käytetty IP-adressu(<strong>$4</strong>), on estetty. Eston on azetannuh [[User:$3|$3]].\n\nKäyttäjän $3 annettu syy estole on \"$2\".",
        "viewpagelogs": "Ozuta tämän sivun lougat",
index ba5db3b..619a3dc 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": "ଖାଲି ସମ୍ପାଦନା ସାରକଥାକୁ ଯିବା ବେଳେ ମୋତେ ଜଣାଇବେ",
        "unprotectthispage": "ଏହି ପୃଷ୍ଠା ପାଇଁ ସୁରକ୍ଷାର ପ୍ରକାର ବଦଳାଇବେ",
        "newpage": "ନୂଆ ପୃଷ୍ଠା",
        "talkpage": "ପୃଷ୍ଠାକୁ ଆଲୋଚନା କରନ୍ତୁ",
-       "talkpagelinktext": "à¬\95ଥାଭାଷା",
+       "talkpagelinktext": "à¬\86ଲà­\8bà¬\9aନା",
        "specialpage": "ବିଶେଷ ପୃଷ୍ଠା",
        "personaltools": "ନିଜର ଟୁଲ",
        "articlepage": "ସୂଚୀ ପୃଷ୍ଠାଟି ଦେଖାଇବେ",
        "cannotdelete-title": "\"$1\" ପୃଷ୍ଠାଟି ଲିଭଯାଇପାରିବ ନାହିଁ",
        "delete-hook-aborted": "ସମ୍ପାଦନା ଏକ ହୁକ (hook) ଦେଇ ବାରଣ କରାଗଲା ।\nଏହା କିଛି ବି କାରଣ ଦେଇନାହିଁ ।",
        "no-null-revision": "\"$1\" ପୃଷ୍ଠାଟି ପାଇଁ ଫାଙ୍କା ସଂସ୍କରଣଟିଏ ତିଆରି କରିପାରିଲୁ ନାହିଁ",
-       "badtitle": "à¬\96ରାପ à¬¨à¬¾à¬\86à¬\81",
+       "badtitle": "à¬\96ରାପ à¬¨à¬¾à¬®",
        "badtitletext": "ଆପଣ ଅନୁରୋଧ କରିଥିବା ପୃଷ୍ଠାଟି ଭୁଲ, ଖାଲି ଅଛି ବା ବାକି ଭାଷା ସାଙ୍ଗରେ ଭୁଲରେ ଯୋଡ଼ା ଯାଇଛି ବା ଭୁଲ ଇଣ୍ଟର ଉଇକି ନାମ ଦିଆଯାଇଛି ।\nଏଥିରେ ଥିବା ଗୋଟିଏ ବା ଦୁଇଟି ଅକ୍ଷର ଶିରୋନାମା ଭାବରେ ବ୍ୟବହାର କରାଯାଇ ପାରିବ ନାହିଁ ।",
        "title-invalid-empty": "ଅନୁରୋଧ କରାଯାଇଥିବା ପୃଷ୍ଠାର ଶର୍ଷକଟି ଖାଲି ଅଛି କିମ୍ବା ନେମସ୍ପସର ନାମ ଅଛି ।",
        "title-invalid-utf8": "ଅନୁରୋଧ କରାଯାଇଥିବା ପୃଷ୍ଠାର ଶର୍ଷକରେ ଅବୈଧ UTF-8 ଧାରା ଅଛି ।",
        "minoredit": "ଏହା ଏକ ସାମାନ୍ୟ ସମ୍ପାଦନା",
        "watchthis": "ଏହି ପୃଷ୍ଠାଟିକୁ ଦେଖିବେ",
        "savearticle": "ସାଇତିବେ [Save]",
+       "savechanges": "ସାଇତିବେ ['''Save''']",
        "preview": "ସାଇତିବା ଆଗରୁ ଦେଖନ୍ତୁ",
        "showpreview": "ଦେଖଣା [Preview]",
        "showdiff": "ବଦଳଗୁଡ଼ିକ ଦେଖାଇବେ",
        "newarticle": "(ନୁଆ)",
        "newarticletext": "ଆପଣ ଖୋଲିଥିବା ଲିଙ୍କଟିରେ ଏଯାଏଁ କିଛିବି ପୃଷ୍ଠା ନାହିଁ ।\nଏହି ପୃଷ୍ଠାଟିକୁ ତିଆରି କରିବା ପାଇଁ ତଳ ବାକ୍ସରେ ଟାଇପ କରନ୍ତୁ (ଅଧିକ ଜାଣିବା ପାଇଁ [$1 ସାହାଯ୍ୟ ପୃଷ୍ଠା] ଦେଖନ୍ତୁ) ।\nଯଦି ଆପଣ ଏଠାକୁ ଭୁଲରେ ଆସିଯାଇଥାନ୍ତି ତେବେ ଆପଣଙ୍କ ବ୍ରାଉଜରର '''Back''' ପତିଟି ଦବାନ୍ତୁ ।",
        "anontalkpagetext": "----''ଏହା ଏକ ଖାତା ଖୋଲିନଥିବା ବା ଖାତା ବ୍ୟବହାର କରିନଥିବା ଜଣେ ବେନାମି ସଭ୍ୟଙ୍କର ଆଲୋଚନା ପୃଷ୍ଠା ।''\nତେଣୁ ଆମ୍ଭେ ସଂଖ୍ୟା ଦେଇ ସୂଚୀତ IP ଠିକଣା ଦେଇ ତାଙ୍କୁ ଜାଣିବା ।\nଏହି ପ୍ରକାରର ଗୋଟିଏ IP ଠିକଣା ବହୁ ସଭ୍ୟଙ୍କ ଦେଇ ବ୍ୟବହାର କରାଯାଇପାରେ । \nଯଦି ଆପଣ ଜଣେ ଅଜଣା ସଭ୍ୟ ଓ ଭାବୁଛନ୍ତି ଇଆଡୁ ସିଆଡୁ ମତାମତ ସବୁ ଆପଣଙ୍କ ପାଇଁ ଦିଆଯାଇଛି ତେବେ ଦୟାକରି [[Special:CreateAccount|ନୂଆ ଖାତାଟିଏ ଖୋଲନ୍ତୁ]] କିମ୍ବା [[Special:UserLogin|ଆଗରୁ ଥିବା ଖାତାରେ ଲଗ ଇନ କରନ୍ତୁ]] ଯାହା ବେନାମି ସଭ୍ୟଙ୍କୁ ନେଇ ଉପୁଜିଥିବା ଦ୍ଵନ୍ଦର ସମାଧାନ କରିବ ।",
-       "noarticletext": "à¬\8fହି à¬ªà­\83ଷà­\8dଠାà¬\9fିରà­\87 à¬\95ିà¬\9bି à¬¬à¬¿ à¬²à­\87à¬\96ା à¬¨à¬¾à¬¹à¬¿à¬\81 à¥¤\nà¬\86ପଣ [[Special:Search/{{PAGENAME}}|à¬\8fହି à¬²à­\87à¬\96ାà¬\9fିର à¬¨à¬¾à¬\86à¬\81]] à¬¬à¬¾à¬\95ି à¬ªà­\83ଷà­\8dଠାମାନà¬\99à­\8dà¬\95ରà­\87 à¬\96à­\8bà¬\9cି à¬ªà¬¾à¬°à¬¨à­\8dତି,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}ରà­\87 à¬¯à­\8bଡ଼ାଯାà¬\87ଥିବା à¬¬à¬¾à¬\95ି à¬ªà­\83ଷà­\8dଠାସବà­\81à¬\95à­\81 à¬\96à­\8bà¬\9cି à¬ªà¬¾à¬°à¬¨à­\8dତି],\nà¬\95ିମà­\8dବା [{{fullurl:{{FULLPAGENAME}}|action=edit}} à¬\8fହି à¬ªà­\83ଷà­\8dଠାà¬\9fିà¬\95à­\81 à¬¬à¬¦à¬³à¬¾à¬\87 ପାରନ୍ତି]</span> ।",
+       "noarticletext": "à¬\8fହି à¬ªà­\83ଷà­\8dଠାà¬\9fିରà­\87 à¬\95à­\8cଣସି à¬²à­\87à¬\96ା à¬¨à¬¾à¬¹à¬¿à¬\81 à¥¤\nà¬\86ପଣ [[Special:Search/{{PAGENAME}}|à¬\8fହି à¬²à­\87à¬\96ାà¬\9fିର à¬¨à¬¾à¬®]] à¬¬à¬¾à¬\95ି à¬ªà­\83ଷà­\8dଠାମାନà¬\99à­\8dà¬\95ରà­\87 à¬\96à­\8bà¬\9cିପାରନà­\8dତି,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}ରà­\87 à¬¯à­\8bଡ଼ାଯାà¬\87ଥିବା à¬¬à¬¾à¬\95ି à¬ªà­\83ଷà­\8dଠାସବà­\81à¬\95à­\81 à¬\96à­\8bà¬\9cି à¬ªà¬¾à¬°à¬¨à­\8dତି],\nà¬\95ିମà­\8dବା [{{fullurl:{{FULLPAGENAME}}|action=edit}} à¬\8fହି à¬ªà­\83ଷà­\8dଠାà¬\9fି à¬¤à¬¿à¬\86ରି à¬\95ରିପାରନ୍ତି]</span> ।",
        "noarticletext-nopermission": "ଏବେ ଏହି ପୃଷ୍ଠାଟିରେ କିଛି ବି ଲେଖା ନାହିଁ ।\nଆପଣ [[Special:Search/{{PAGENAME}}|ଏହି ଲେଖାଟିର ନାଆଁ]] ବାକି ପୃଷ୍ଠାମାନଙ୍କରେ ଖୋଜି ପାରନ୍ତି, କିମ୍ବା\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}ରେ ଯୋଡ଼ାଯାଇଥିବା ବାକି ପୃଷ୍ଠାସବୁକୁ ଖୋଜି ପାରନ୍ତି]\n</span>, କିନ୍ତୁ ଏହି ପୃଷ୍ଠାଟିକୁ ଆପଣ ତିଆରି କରିପାରିବେ ନାହିଁ ।",
        "missing-revision": "\"{{FULLPAGENAME}}\" ନାମରେ ଥିବା ପୃଷ୍ଠାଟିର #$1 ପୁନରାବୃତ୍ତି ନାହିଁ ।\n\nପୁରୁଣା ହୋଇଯାଇଥିବା ଇତିହାସ ଲିଙ୍କ ଯାହା ଏକ ଲିଭାଯାଇଥିବା ପୃଷ୍ଠାକୁ ଦିଆଯାଇଥିବାରୁ ଏହା ସାଧାରଣତଃ ହୋଇଥାଏ ।\nଅଧିକ ବିବରଣୀ [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log]ରେ ମିଳିପାରିବ ।",
        "userpage-userdoesnotexist": "ଇଉଜର ଖାତା \"$1\" ଟି ତିଆରି କରାଯାଇନାହିଁ ।\nଆପଣ ଏହି ପୃଷ୍ଠାଟିକୁ ତିଆରି କରିବାକୁ ଚାହାନ୍ତି କି ନାହିଁ ଦୟାକରି ପରଖି ନିଅନ୍ତୁ ।",
        "sectioneditnotsupported-text": "ଏହି ପୃଷ୍ଠାରେ ବିଭାଗ ସମ୍ପାଦନା କାମ କରିବ ନାହିଁ ।",
        "permissionserrors": "ଅନୁମତି ମିଳିବାରେ ଅସୁବିଧା",
        "permissionserrorstext": "ତଳଲିଖିତ {{PLURAL:$1|କାରଣ|କାରଣସବୁ}} ପାଇଁ ଆପଣଙ୍କୁ ଏହା କରିବା ନିମନ୍ତେ ଅନୁମତି ନାହିଁ:",
-       "permissionserrorstext-withaction": "ତଳଲିଖିତ {{PLURAL:$1|କାରଣ|କାରଣସବୁ}} ପାଇଁ ଆପଣଙ୍କୁ $2 ଭିତରକୁ ଅନୁମତି ନାହିଁ:",
+       "permissionserrorstext-withaction": "ତଳଲିଖିତ {{PLURAL:$1|କାରଣ|କାରଣସବୁ}} ପାଇଁ ଆପଣଙ୍କୁ $2ରେ ଅନୁମତି ନାହିଁ:",
        "recreate-moveddeleted-warn": "'''ସୂଚନା: ଆଗରୁ ଲିଭାଯାଇଥିବା ପୃଷ୍ଠାଟିଏକୁ ଆପଣ ଆଉଥରେ ତିଆରୁଛନ୍ତି ।'''\n\nଆପଣ ଏହି ପୃଷ୍ଠାଟିକୁ ସମ୍ପାଦନା କରିବା ଉଚିତ କି ନୁହେଁ ବିଚାର କରିବା ଦରକାର ।\nଏହି ପୃଷ୍ଠାର ଲିଭାଇବା ଓ ଘୁଞ୍ଚାଇବା ଇତିହାସ ଏଠାରେ ସୁବିଧା ନିମନ୍ତେ ଦିଆଗଲା ।:",
-       "moveddeleted-notice": "à¬\8fହି à¬ªà­\83ଷà­\8dଠାà¬\9fିà¬\95à­\81 à¬²à¬¿à¬­à¬¾à¬\87 à¬¦à¬¿à¬\86ଯାà¬\87à¬\85à¬\9bି à¥¤\nà¬\8fହାର à¬²à¬¿à¬­à¬¾à¬\87ବା à¬\93 à¬\98à­\81à¬\9eà­\8dà¬\9aାà¬\87ବା à¬\87ତିହାସ à¬\86ଧାର à¬¨à¬¿à¬®à¬¨à­\8dତà­\87 à¬¤à¬³à­\87 à¬¦à¬¿à¬\86à¬\97ଲା à¥¤",
+       "moveddeleted-notice": "ଏହି ପୃଷ୍ଠାଟିକୁ ଲିଭାଇ ଦିଆଯାଇଛି ।\nଏହାର ଲିଭାଇବା ଓ ଘୁଞ୍ଚାଇବା ଇତିହାସ ଆଧାର ନିମନ୍ତେ ତଳେ ଦିଆଗଲା ।",
        "log-fulllog": "ପୁରା ଲଗ ଇତିହାସ ଦେଖନ୍ତୁ",
        "edit-hook-aborted": "ସମ୍ପାଦନା ଏକ ହୁକ (hook) ଦେଇ ବାରଣ କରାଗଲା ।\nଏହା କିଛି ବି କାରଣ ଦେଇନାହିଁ ।",
        "edit-gone-missing": "ଏହି ପୃଷ୍ଠାଟିକୁ ସତେଜ କରାଯାଇପାରିବ ନାହିଁ ।\nଏହାକୁ ଲିଭାଇ ଦିଆଗଲା ପରି ମନେ ହେଉଛି ।",
        "mergelog": "ମିଶ୍ରଣ ଲଗ୍",
        "revertmerge": "ମିଶାଇବା ନାହିଁ",
        "mergelogpagetext": "ତଳେ ସବୁଠାରୁ ନଗଦ ଯୋଡ଼ାଯାଇଥିବା ପୃଷ୍ଠାର ଇତିହାସ ଆଉ ଗୋଟିଏ ସହ ଦିଆଯାଇଅଛି ।",
-       "history-title": "\"$1\" à¬° à¬ªà­\81ନରାବà­\83ତି ଇତିହାସ",
+       "history-title": "\"$1\" à¬° à¬ªà­\81ନରà­\8dସà¬\82ସà­\8dà¬\95ରଣ ଇତିହାସ",
        "difference-title": "\"$1\" ପୃଷ୍ଠାର ସଂସ୍କରଣ‌ଗୁଡ଼ିକ ମଧ୍ୟରେ ତଫାତ",
        "difference-title-multipage": "ପୃଷ୍ଠା \"$1\" ଏବଂ \"$2\" ମଧ୍ୟରେ ଥିବା ପାର୍ଥକ୍ୟ",
        "difference-multipage": "(ପୃଷ୍ଠା ଭିତରେ ଥିବା ତଫାତ)‌",
        "searchprofile-advanced-tooltip": "ନିଜେ ତିଆରିକରିହେବା ଭଳି ନେମସ୍ପେସରେ ଖୋଜିବେ",
        "search-result-size": "$1 ({{PLURAL:$2|ଗୋଟେ ଶବ୍ଦ|$2 ଟି ଶବ୍ଦ}})",
        "search-result-category-size": "{{PLURAL:$1|ଜଣେ ସଭ୍ୟ|$1 ଜଣ ସଭ୍ୟ}} ({{PLURAL:$2|ଗୋଟିଏ ଶ୍ରେଣୀy|$2ଟି ଶ୍ରେଣୀ ସମୂହ}}, {{PLURAL:$3|ଗୋଟିଏ ଫାଇଲ|$3ଟି ଫାଇଲ}})",
-       "search-redirect": "($1 à¬\95à­\81 à¬\86à¬\97à¬\95à­\81 à¬¬à¬¢à­\87à¬\87ନିà¬\85 )",
+       "search-redirect": "($1 à¬°à­\81 à¬²à­\87à¬\89à¬\9fାଣି)",
        "search-section": "(ଭାଗ $1)",
        "search-category": "(ଶ୍ରେଣୀ $1)",
        "search-file-match": "(ଫାଇଲରେ ଥିବା ବିଷୟବସ୍ତୁ ସାଙ୍ଗେ ମେଳ)",
        "number_of_watching_users_pageview": "[$1 {{PLURAL:$1|ସଭ୍ୟ|ସଭ୍ୟଗଣା}}ଙ୍କୁ ଦେଖୁଅଛି]",
        "rc_categories": "ଶ୍ରେଣୀସମୂହ ପାଇଁ ସୀମା ( \"|\" ଦେଇ ଅଲଗା କରିବେ)",
        "rc_categories_any": "ଯେ କୌଣସି",
-       "rc-change-size-new": "ବଦଳ ପରେ $1 {{PLURAL:$1|ବାଇଟ|ବାଇଟ}}",
+       "rc-change-size-new": "ବଦଳ ପରେ $1 {{PLURAL:$1|ବାଇଟ|ବାଇଟସବୁ}}",
        "newsectionsummary": "/* $1 */ ନୂଆ ଭାଗ",
        "rc-enhanced-expand": "ସବିଶେଷ ଦେଖାନ୍ତୁ",
        "rc-enhanced-hide": "ବେଶି କଥାସବୁ ଲୁଚାଇଦିଅ",
        "deleting-backlinks-warning": "''' ଚେତାବନୀ:''' [[Special:WhatLinksHere/{{FULLPAGENAME}}|ବାକି ପୃଷ୍ଠା]] ଆପଣ ଲିଭାଇବାକୁ ଯାଉଥିବା ପୃଷ୍ଠାଟି ସହିତ ଲିଙ୍କ କରନ୍ତୁ କିମ୍ବା ତାହାକୁ କାଢ଼ନ୍ତୁ ।",
        "rollback": "ପୁରାପୁରି ପଛକୁ ଫେରିବା ବଦଳ",
        "rollbacklink": "ପୂରାପୂରି ପଛକୁ ଫେରିଯିବେ",
-       "rollbacklinkcount": "{{PLURAL:$1|edit|edits}} $1 ପଛକୁ ଫେରାଇବେ",
+       "rollbacklinkcount": "$1ଟି {{PLURAL:$1|ସମ୍ପାଦନା|ସମ୍ପାଦନା ସବୁ}} ପଛକୁ ଫେରାଇବେ",
        "rollbacklinkcount-morethan": "{{PLURAL:$1|edit|edits}} $1ରୁ ଅଧିକ ପଛକୁ ଫେରାଇବେ",
        "rollbackfailed": "ପୁରାପୁରି ପଛକୁ ଫେରିବା ବିଫଳ ହେଲା",
        "cantrollback": "ବଦଳକୁ ପଛକୁ ଫେରାଇ ପାରିବେ ନାହିଁ;\nଶେଷ ଦାତାଜଣଙ୍କ ଏହି ପୃଷ୍ଠାର ଜଣେମାତ୍ର ଦାତା ।",
        "whatlinkshere-prev": "{{PLURAL:$1|ଆଗ|ଆଗ $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|ପର|ପର $1}}",
        "whatlinkshere-links": "← ଲିଙ୍କ",
-       "whatlinkshere-hideredirs": "$1 କୁ ଲେଉଟାଣି",
-       "whatlinkshere-hidetrans": "$1 ଆଧାର ସହ ଭିତରେ ରଖିବା",
-       "whatlinkshere-hidelinks": "$1 ଟି ଲିଙ୍କ",
+       "whatlinkshere-hideredirs": "$1ଟି ଲେଉଟାଣି",
+       "whatlinkshere-hidetrans": "$1ଟି ବାଦ ଦିଆଯାଇଛି",
+       "whatlinkshere-hidelinks": "$1ଟି ଲିଙ୍କ",
        "whatlinkshere-hideimages": "$1 ଫାଇଲର ଲିଙ୍କସବୁ",
        "whatlinkshere-filters": "ଛଣା",
        "autoblockid": "#$1ଙ୍କୁ ଆପେଆପେ ଅଟକାଇଦେବେ",
        "tooltip-watchlistedit-raw-submit": "ଦେଖଣା ତାଲିକାକୁ ଅପଡ଼େଟ କରିବେ",
        "tooltip-recreate": "ଏହି ପୃଷ୍ଠାଟି ଲିଭାଇଦିଆଯାଇଥିଲେ ବି ଆଉଥରେ ତିଆରି କରନ୍ତୁ",
        "tooltip-upload": "ଅପଲୋଡ଼ କରନ୍ତୁ",
-       "tooltip-rollback": "\"ଫà­\87ରିବା\" à¬\8fହି à¬«à¬°à¬¦à¬°à­\87 à¬¶à­\87ଷ à¬¦à¬¾à¬¤à¬¾à¬\99à­\8dà¬\95 à¬¦à­\87à¬\87 à¬\95ରାଯାà¬\87ଥିବା à¬¸à¬¬à­\81ଯାà¬\95 à¬¬à¬¦à¬³à¬\95à­\81  à¬\8fà¬\95ାଥରà¬\95ରà­\87 à¬ªà¬\9bà¬\95à­\81 à¬«à­\87ରାà¬\87ଦà­\87ବ",
+       "tooltip-rollback": "\"ଫà­\87ରିବା\" à¬\8fହି à¬ªà­\83ଷà­\8dଠାରà­\87 à¬¶à­\87ଷ à¬\85ବଦାନà¬\95ାରà­\80à¬\99à­\8dà¬\95 à¬¦à­\87à¬\87 à¬\95ରାଯାà¬\87ଥିବା à¬¸à¬¬à­\81ଯାà¬\95 à¬¬à¬¦à¬³à¬\95à­\81  à¬\8fà¬\95ାଥରà¬\95ରà­\87 à¬ªà¬\9bà¬\95à­\81 à¬«à­\87ରାà¬\87ଦିà¬\8f",
        "tooltip-undo": "\"କରନାହିଁ\" ଦବାଇଲେ ଆଗରୁ ହୋଇଥିବା ସମ୍ପାଦନାଟିଏ ପଛକୁ ଲେଉଟିଯାଏ ଓ ତାହା ସମ୍ପାଦନା ପୃଷ୍ଠାଟିକୁ '''ଦେଖଣା''' ଭାବେ ଖୋଲେ । ଆପଣଙ୍କୁ ଏହା ସାରକଥାରେ କାରଣଟିଏ ଲେଖିବାର ସୁଯୋଗ ଦିଏ ।",
        "tooltip-preferences-save": "ଆପଣା ପସନ୍ଦ ସାଇତିବେ",
        "tooltip-summary": "ଛୋଟ ସାରକଥାଟିଏ ଦିଅନ୍ତୁ",
        "pageinfo-magic-words": "ଚମତ୍କାର {{PLURAL:$1|word|words}} ($1)",
        "pageinfo-hidden-categories": "{{PLURAL:$1|category|categories}} ($1) ଲୁଚାଗଲା",
        "pageinfo-templates": "{{PLURAL:$1|template|templates}} ($1) ଯୋଡିହେଇଥିବା",
-       "pageinfo-transclusions": "{{PLURAL:$1|Page|Pages}} ($1)ରେ ଯୋଡାଗଲା",
+       "pageinfo-transclusions": "{{PLURAL:$1|ପୃଷ୍ଠା|ପୃଷ୍ଠାସବୁ}} ($1)ରେ ଯୋଡାଗଲା",
        "pageinfo-toolboxlink": "ପୃଷ୍ଠା ସୂଚନା",
        "pageinfo-redirectsto": "କୁ ଲେଉଟାଣି",
        "pageinfo-redirectsto-info": "ସୂଚନା",
        "exif-xresolution": "ଭୂସମାନ୍ତର ରେଜଲୁସନ",
        "exif-yresolution": "ଭୁଲମ୍ବ ରେଜଲୁସନ",
        "exif-stripoffsets": "ଛବି ଡାଟା ଅବସ୍ଥାନ",
-       "exif-rowsperstrip": "ପà¬\9fି à¬ªà¬¿à¬\9bା à¬¸à­\8dତମà­\8dଭ à¬¸à¬\99à­\8dଖ୍ୟା",
+       "exif-rowsperstrip": "ପà¬\9fି à¬ªà¬¿à¬\9bା à¬¸à­\8dତମà­\8dଭ à¬¸à¬\82ଖ୍ୟା",
        "exif-stripbytecounts": "ସଙ୍କୁଚିତ ପଟି ପିଛା ବାଇଟ",
        "exif-jpeginterchangeformat": "Offset ରୁ JPEG SOI",
        "exif-jpeginterchangeformatlength": "JPEG ଡାଟାର ବାଇଟ",
        "exif-subsectimedigitized": "DateTimeDigitized ସାନ ସେକେଣ୍ଡ",
        "exif-exposuretime": "ଏକ୍ସପୋଜର କାଳ",
        "exif-exposuretime-format": "$1 ସେକେଣ୍ଡ ($2)",
-       "exif-fnumber": "F à¬¸à¬\99à­\8dà¬\96à­\8dà­\9fା",
+       "exif-fnumber": "F à¬¨à¬®à­\8dବର",
        "exif-exposureprogram": "ଏକ୍ସପୋଜର ପ୍ରୋଗ୍ରାମ",
        "exif-spectralsensitivity": "ବର୍ଣ୍ଣାଳି ସମ୍ବେଦନଶୀଳତା",
        "exif-isospeedratings": "ISO ବେଗ ସୂଚାଙ୍କ",
        "tags": "ବୈଧ ସମ୍ପାଦନା ଚିହ୍ନ",
        "tag-filter": "[[Special:Tags|ଟାଗ]] ଛଣା:",
        "tag-filter-submit": "ଛାଣିବା",
-       "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|ଟ୍ୟାଗ|ଟ୍ୟାଗ}}]]: $2)",
+       "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|ଟ୍ୟାଗ|ଟ୍ୟାଗସବୁ}}]]: $2)",
        "tags-title": "ସୂଚକ",
        "tags-intro": "ଏହି ପୃଷ୍ଠା ସଫ୍ଟୱାର ଏକ ବଦଳ ଭାବେ ଚିହ୍ନିତ କରୁଥିବା ଚିହ୍ନସବୁର ମାନେ ସହ ତାଲିକା ତିଆରି କରିଥାଏ ।",
        "tags-tag": "ଚିହ୍ନ ନାମ",
        "htmlform-cloner-create": "ଅଧିକ ଯୋଡ଼ନ୍ତୁ",
        "htmlform-cloner-delete": "ବାହାର କରନ୍ତୁ",
        "htmlform-cloner-required": "ଅତି କମରେ ଗୋଟିଏ ମୂଲ୍ୟ ଲୋଡ଼ା",
-       "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|ଲଗ ଘଟଣାଟିଏ|$5 ଗୋଟି ଲଗ ଘଟଣା}}ର ଦେଖଣା $3 ପୃଷ୍ଠାରେ {{GENDER:$2|ବଦଳାଇଲେ}}: $4",
        "feedback-bugornote": "ଦୟାକରି ଆପଣ ଏକ କାରିଗରି ଅସୁବିଧାଟିଏ ଜଣାଇବା ପାଇଁ ଚାହୁଁଥିଲେ ଦୟାକରି [$1 ଏଠାରେ ଅସୁବିଧାଟି ଜଣାନ୍ତୁ] । \nଅଥବା, ଆପଣ ତଳେ ଠିଆ ସହଜ ଆବେଦନ ପତ୍ରଟି ପୁରଣ କରିପାରିବେ ।  ଆପଣଙ୍କ ବ୍ୟବହାରକାରୀ ନାମ ଓ ଆପଣ ବ୍ୟବହାର କରୁଥିବା ବ୍ରାଉଜର ଅନୁସାରେ ଆପଣଙ୍କ ମତାମତ \"[$3 $2]\"ରେ ଯୋଡ଼ାଯିବ ।",
        "feedback-cancel": "ନାକଚ",
        "feedback-close": "ହୋଇଗଲା",
-       "feedback-error-title": "ଅସୁବିଧା",
        "feedback-error1": "ଭୁଲ: API ରୁ ଅଚିହ୍ନା ଫଳାଫଳ",
        "feedback-error2": "ଅସୁବିଧା: ସମ୍ପାଦନା ବିଫଳ ହେଲା",
        "feedback-error3": "ଅସୁବିଧା: API ରୁ କିଛି ଉତ୍ତର ମିଳିଲା ନାହିଁ",
index c66b261..f3c7034 100644 (file)
        "newwindow": "(otwiera się w nowym oknie)",
        "cancel": "Anuluj",
        "moredotdotdot": "Więcej...",
-       "morenotlisted": "Nie jest to kompletna lista.",
+       "morenotlisted": "Ta lista może być niekompletna.",
        "mypage": "Strona",
        "mytalk": "Dyskusja",
        "anontalk": "Dyskusja",
        "talk": "Dyskusja",
        "views": "Widok",
        "toolbox": "Narzędzia",
+       "tool-link-userrights": "Zmiana grup {{GENDER:$1|użytkownika|użytkowniczki}}",
+       "tool-link-emailuser": "Wyślij e-mail do {{GENDER:$1|tego użytkownika|tej użytkowniczki}}",
        "userpage": "Pokaż stronę użytkownika",
        "projectpage": "Pokaż stronę projektu",
        "imagepage": "Pokaż stronę pliku",
        "eauthentsent": "Potwierdzenie zostało wysłane na adres e‐mail.\nZanim jakiekolwiek inne wiadomości zostaną wysłane na ten adres, należy wykonać zawarte w mailu instrukcje. Potwierdzisz w ten sposób, że ten adres e‐mail należy do Ciebie.",
        "throttled-mailpassword": "Przypomnienie hasła zostało już wysłane w ciągu {{PLURAL:$1|ostatniej godziny|ostatnich $1 godzin}}.\nAby zapobiec nadużyciom nadużyć możliwość wysyłania przypomnień została ograniczona do jednego na {{PLURAL:$1|godzinę|$1 godziny|$1 godzin}}.",
        "mailerror": "W trakcie wysyłania wiadomości e‐mail wystąpił błąd: $1",
-       "acct_creation_throttle_hit": "Z adresu IP, z którego korzystasz {{PLURAL:$1|ktoś już utworzył dziś konto|utworzono dziś $1 konta|utworzono dziś $1 kont}}, co jest maksymalną dopuszczalną liczbą w tym czasie.\nW związku z tym, osoby korzystające z tego adresu IP w chwili obecnej nie mogą założyć kolejnego.",
+       "acct_creation_throttle_hit": "Z adresu IP, z którego korzystasz {{PLURAL:$1|ktoś już utworzył dziś konto|utworzono dziś $1 konta|utworzono dziś $1 kont}} w ciągu ostatnich $2, co jest maksymalną dopuszczalną liczbą w tym czasie.\nW związku z tym, osoby korzystające z tego adresu IP w chwili obecnej nie mogą założyć kolejnego.",
        "emailauthenticated": "Twój adres e‐mail został potwierdzony $2 o $3.",
        "emailnotauthenticated": "Twój adres '''e‐mail nie został potwierdzony'''.\nPoniższe funkcje poczty nie działają.",
        "noemailprefs": "Podaj adres e‐mail w preferencjach, by skorzystać z tych funkcji.",
        "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?",
        "botpasswords-updated-body": "Hasło bota \"$1\" użytkownika \"$2\" zostało zaktualizowane.",
        "botpasswords-deleted-title": "Hasło bota usunięte",
        "botpasswords-deleted-body": "Hasło bota \"$1\" użytkownika \"$2\" zostało usunięte.",
-       "botpasswords-newpassword": "Nowe hasło do zalogowania przez <strong>$1</strong> to <strong>$2</strong>. <em>Proszę je zapisać w celu wykorzystania w przyszłości.</em>",
+       "botpasswords-newpassword": "Nowe hasło do zalogowania się przez <strong>$1</strong> to <strong>$2</strong>. <em>Proszę je zapisać w celu wykorzystania w przyszłości.</em> <br> (Dla starszych botów, które wymagają loginu takiego samego jak ewentualna nazwa użytkownika można użyć <strong>$3</strong> jako nazwę użytkownika i <strong>$4</strong> jako hasło.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider nie jest dostępne.",
        "botpasswords-restriction-failed": "Logowanie nie powiodło się z powodu ograniczeń na hasło bota.",
        "botpasswords-invalid-name": "Określona nazwa użytkownika nie zawiera separatora hasła bota (\"$1\").",
        "passwordreset-emailelement": "Nazwa użytkownika: \n$1\n\nTymczasowe hasło: \n$2",
        "passwordreset-emailsentemail": "Jeśli ten adres e‐mail jest przypisany do Twojego konta, zostanie na niego wysłany e-mail do odzyskiwania hasła.",
        "passwordreset-emailsentusername": "Jeśli z tym kontem powiązany jest adres e‐mail, zostanie na niego wysłany e-mail do odzyskiwania hasła.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Został wysłany e-mail|Zostały wysłane e-mail}} z informacjami o resetowaniu hasła. {{PLURAL:$1|Użytkownik i hasło jest pokazany|Lista użytkowników i haseł jest pokazana}} poniżej.",
-       "passwordreset-emailerror-capture2": "Wysyłanie e-maila do {{GENDER:$2|użytkownika|użytkowniczki}} nie powiodło się: $1 {{PLURAL:$3|Użytkownik i hasło jest pokazany|Lista użytkowników i haseł jest pokazana}} poniżej.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|Został wysłany e-mail|Zostały wysłane e-maile}} z informacjami o resetowaniu hasła. {{PLURAL:$1|Użytkownik i hasło jest pokazany|Lista użytkowników i haseł jest pokazana}} tutaj.",
+       "passwordreset-emailerror-capture2": "Wysyłanie e-maila do {{GENDER:$2|użytkownika|użytkowniczki}} nie powiodło się: $1 {{PLURAL:$3|Użytkownik i hasło jest pokazany|Lista użytkowników i haseł jest pokazana}} tutaj.",
        "passwordreset-nocaller": "Musi być podany wywołujący",
        "passwordreset-nosuchcaller": "Wywołujący nie istnieje: $1",
        "passwordreset-invalideamil": "Nieprawidłowy adres e-mail",
        "userpage-userdoesnotexist": "Użytkownik „<nowiki>$1</nowiki>” nie jest zarejestrowany.\nUpewnij się, czy na pewno zamierza{{GENDER:|łeś|łaś|sz}} utworzyć lub zmodyfikować właśnie tę stronę.",
        "userpage-userdoesnotexist-view": "Konto użytkownika „$1” nie jest zarejestrowane.",
        "blocked-notice-logextract": "{{GENDER:$1|Ten użytkownik|Ta użytkowniczka}} jest obecnie {{GENDER:$1|zablokowany|zablokowana}}.\nOstatni wpis rejestru blokad jest pokazany poniżej.",
-       "clearyourcache": "<strong>Uwaga:</strong> aby zobaczyć zmiany po zapisaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.\n* <strong>Firefox / Safari:</strong> Przytrzymaj <em>Shift</em> podczas klikania <em>Odśwież bieżącą stronę</em>, lub naciśnij klawisze <em>Ctrl+F5</em> lub <em>Ctrl+R</em> (<em>⌘-R</em> na komputerze Mac)\n* <strong>Google Chrome:</strong> Naciśnij <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> na komputerze Mac)\n* <strong>Internet Explorer:</strong> Przytrzymaj <em>Ctrl</em>, jednocześnie klikając <em>Odśwież</em>, lub naciśnij klawisze <em>Ctrl+F5</em>\n* <strong>Opera:</strong> Wyczyść pamięć podręczną w <em>Narzędzia → Preferencje</em>",
+       "clearyourcache": "<strong>Uwaga:</strong> aby zobaczyć zmiany po zapisaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.\n* <strong>Firefox / Safari:</strong> Przytrzymaj <em>Shift</em> podczas klikania <em>Odśwież bieżącą stronę</em>, lub naciśnij klawisze <em>Ctrl+F5</em> lub <em>Ctrl+R</em> (<em>⌘-R</em> na komputerze Mac)\n* <strong>Google Chrome:</strong> Naciśnij <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> na komputerze Mac)\n* <strong>Internet Explorer:</strong> Przytrzymaj <em>Ctrl</em>, jednocześnie klikając <em>Odśwież</em>, lub naciśnij klawisze <em>Ctrl+F5</em>\n* <strong>Opera:</strong> Przejdź do <em>Menu → Ustawienia</em> (<em>Opera → Preferencje</em> w Mac), a następnie <em>Prywatność i bezpieczeństwo → Wyczyść dane przeglądania → Opróżnij pamięć podręczną</em>.",
        "usercssyoucanpreview": "'''Podpowiedź:''' Użyj przycisku „Podgląd”, aby przetestować nowy arkusz stylów CSS przed jego zapisaniem.",
        "userjsyoucanpreview": "'''Podpowiedź:''' Użyj przycisku „Podgląd”, aby przetestować nowy kod JavaScript przed jego zapisaniem.",
        "usercsspreview": "'''Pamiętaj, że to tylko podgląd arkusza stylów CSS – nic jeszcze nie zostało zapisane!'''",
        "invalid-content-data": "Zawartość strony zawiera nieprawidłowe dane",
        "content-not-allowed-here": "Zawartość tego typu ($1) nie jest dozwolona na stronie [[$2]]",
        "editwarning-warning": "Opuszczenie tej strony może spowodować utratę wprowadzonych przez Ciebie zmian.\nJeśli jesteś zalogowany, możesz wyłączyć wyświetlanie tego ostrzeżenia w zakładce „{{int:prefs-editing}}” w swoich preferencjach.",
+       "editpage-invalidcontentmodel-title": "Model zawartości nie jest obsługiwany",
+       "editpage-invalidcontentmodel-text": "Model zawartości „$1” nie jest obsługiwany.",
        "editpage-notsupportedcontentformat-title": "Nieobsługiwany format zawartości",
        "editpage-notsupportedcontentformat-text": "Format zawartości $1 nie jest obsługiwany modelem treści $2.",
        "content-model-wikitext": "wikitekst",
        "searchprofile-advanced-tooltip": "Szukanie w wybranych przestrzeniach nazw",
        "search-result-size": "$1 ({{PLURAL:$2|1 słowo|$2 słowa|$2 słów}})",
        "search-result-category-size": "{{PLURAL:$1|1 element|$1 elementy|$1 elementów}} ({{PLURAL:$2|1 kategoria|$2 kategorie|$2 kategorii}}, {{PLURAL:$3|1 plik|$3 pliki|$3 plików}})",
-       "search-redirect": "(przekierowanie $1)",
+       "search-redirect": "(przekierowanie $1)",
        "search-section": "(sekcja $1)",
        "search-category": "(kategoria $1)",
        "search-file-match": "(odpowiada zawartości pliku)",
        "grant-group-high-volume": "Czynności na dużą skalę",
        "grant-group-customization": "Dostosowywanie i preferencje",
        "grant-group-administration": "Czynności administracyjne",
+       "grant-group-private-information": "Dostęp do prywatnych danych o tobie",
        "grant-group-other": "Różne czynności",
        "grant-blockusers": "Blokowanie i odblokowywanie użytkowników",
        "grant-createaccount": "Tworzenie kont",
        "grant-highvolume": "Masowe edytowanie",
        "grant-oversight": "Ukrywanie użytkowników i wersji stron",
        "grant-patrol": "Patrolować zmiany w stronach",
+       "grant-privateinfo": "Dostęp do prywatnych danych",
        "grant-protect": "Zabezpieczanie i odbezpieczanie stron",
        "grant-rollback": "Wycofywanie zmian na stronach",
        "grant-sendemail": "Wysyłanie e‐maili do innych użytkowników",
        "upload-dialog-disabled": "Przesyłanie plików przy pomocy tego okna jest wyłączone na tej wiki.",
        "upload-dialog-title": "Prześlij plik",
        "upload-dialog-button-cancel": "Anuluj",
+       "upload-dialog-button-back": "Wstecz",
        "upload-dialog-button-done": "Gotowe",
        "upload-dialog-button-save": "Zapisz",
        "upload-dialog-button-upload": "Prześlij",
        "apisandbox-results-fixtoken-fail": "Nie udało się pobrać tokena „$1”.",
        "apisandbox-alert-page": "Pola na tej stronie są nieprawidłowe.",
        "apisandbox-alert-field": "Wartość tego pola jest nieprawidłowa.",
+       "apisandbox-continue": "Kontynuuj",
+       "apisandbox-continue-clear": "Wyczyść",
        "booksources": "Książki",
        "booksources-search-legend": "Szukaj informacji o książkach",
        "booksources-search": "Szukaj",
        "blankpage": "Pusta strona",
        "intentionallyblankpage": "Tę stronę umyślnie pozostawiono pustą.",
        "external_image_whitelist": " #Pozostaw tę linię dokładnie tak, jak jest.<pre>\n#Wstaw poniżej fragmenty wyrażeń regularnych (tylko to, co znajduje się między //).\n#Wyrażenia te zostaną dopasowane do adresów URL zewnętrznych (bezpośrednio linkowanych) grafik.\n#Dopasowane adresy URL zostaną wyświetlone jako grafiki, w przeciwnym wypadku będzie pokazany jedynie link do grafiki.\n#Linie zaczynające się od # są traktowane jako komentarze.\n#We wpisach ma znaczenie wielkość znaków.\n\n#Wstaw wszystkie deklaracje wyrażeniami regularnymi poniżej tej linii. Pozostaw tę linię dokładnie tak, jak jest.</pre>",
-       "tags": "Sprawdź zmiany w oparciu o wzorce tekstu",
+       "tags": "Znaczniki zmian",
        "tag-filter": "Filtr [[Special:Tags|znaczników]]:",
        "tag-filter-submit": "Filtr",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Znacznik|Znaczniki}}]]: $2)",
+       "tag-mw-contentmodelchange": "zmiana modelu zawartości",
        "tags-title": "Znaczniki",
        "tags-intro": "Na tej stronie znajduje się lista znaczników, którymi oprogramowanie może oznaczyć edycje, oraz ich opisy.",
        "tags-tag": "Nazwa znacznika",
        "tags-actions-header": "Działania",
        "tags-active-yes": "Tak",
        "tags-active-no": "Nie",
-       "tags-source-extension": "Określony przez rozszerzenie",
+       "tags-source-extension": "Określony przez oprogramowanie",
        "tags-source-manual": "Ręcznie wprowadzany przez użytkowników i boty",
        "tags-source-none": "Nieużywany",
        "tags-edit": "edytuj",
        "htmlform-cloner-create": "Dodaj więcej",
        "htmlform-cloner-delete": "Usuń",
        "htmlform-cloner-required": "Wymagana jest co najmniej jedna wartość.",
+       "htmlform-date-placeholder": "RRRR-MM-DD",
+       "htmlform-time-placeholder": "GG:MM:SS",
+       "htmlform-datetime-placeholder": "RRRR-MM-DD GG:MM:SS",
        "htmlform-title-badnamespace": "[[:$1]] nie znajduje się w przestrzeni nazw „{{ns:$2}}”.",
        "htmlform-title-not-creatable": "Nie można użyć „$1” do utworzenia tytułu strony",
        "htmlform-title-not-exists": "$1 nie istnieje.",
        "htmlform-user-not-exists": "<strong>$1</strong> nie istnieje.",
        "htmlform-user-not-valid": "<strong>$1</strong> nie jest prawidłową nazwą użytkownika.",
-       "sqlite-has-fts": "$1 z obsługą pełnotekstowego wyszukiwania",
-       "sqlite-no-fts": "$1 bez obsługi pełnotekstowego wyszukiwania",
        "logentry-delete-delete": "$1 {{GENDER:$2|usunął|usunęła}} stronę $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|odtworzył|odtworzyła}} stronę $3",
        "logentry-delete-event": "$1 {{GENDER:$2|zmienił|zmieniła}} widoczność {{PLURAL:$5|zdarzenia|$5 zdarzeń}} w rejestrze $3, wykonano następujące operacje: $4",
        "feedback-external-bug-report-button": "Zgłoś problem techniczny",
        "feedback-dialog-title": "Prześlij opinię",
        "feedback-dialog-intro": "Możesz użyć tego prostego formularza w celu zgłoszenia swojej opinii. Twój komentarz, wraz z Twoją nazwą użytkownika (albo numerem IP) pojawi się na stronie $1.",
-       "feedback-error-title": "Błąd",
        "feedback-error1": "Błąd – nierozpoznana odpowiedź API",
        "feedback-error2": "Błąd – edycja nieudana",
        "feedback-error3": "Błąd – brak odpowiedzi API",
        "authform-notoken": "Brakujący token",
        "authform-wrongtoken": "Nieprawidłowy token",
        "specialpage-securitylevel-not-allowed": "Niestety, nie możesz korzystać z tej strony, ponieważ twoja tożsamość nie może zostać zweryfikowana.",
+       "authpage-cannot-login": "Nie można uruchomić logowania.",
        "authpage-cannot-login-continue": "Nie można kontynuować logowania. Sesja najprawdopodobniej wygasła.",
        "authpage-cannot-create": "Nie można rozpocząć tworzenie konta.",
        "authpage-cannot-create-continue": "Nie można kontynuować tworzenia konta. Twoja sesja najprawdopodobniej wygasła.",
        "linkaccounts-success-text": "Konto zostało połączone.",
        "linkaccounts-submit": "Połącz konta",
        "unlinkaccounts": "Odłącz konta",
-       "unlinkaccounts-success": "Konta zostały odłączone."
+       "unlinkaccounts-success": "Konta zostały odłączone.",
+       "restrictionsfield-badip": "Nieprawidłowy adres IP lub zakres adresów: $1",
+       "restrictionsfield-label": "Dozwolone zakresy adresów IP:"
 }
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 5dac084..668515e 100644 (file)
@@ -99,7 +99,9 @@
                        "Anderson Costa",
                        "LucyDiniz",
                        "Tusca",
-                       "Cristofer Alves"
+                       "Cristofer Alves",
+                       "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?",
        "rightslogtext": "Este é um registro de mudanças nos privilégios de usuários.",
        "action-read": "ler esta página",
        "action-edit": "editar esta página",
-       "action-createpage": "criar esta páginas",
-       "action-createtalk": "criar esta páginas de discussão",
+       "action-createpage": "criar esta página",
+       "action-createtalk": "criar esta página de discussão",
        "action-createaccount": "criar esta conta de usuário",
        "action-autocreateaccount": "Criar uma conta de usuário externa automaticamente",
        "action-history": "Ver o histórico desta página",
        "htmlform-title-not-exists": "$1 não existe.",
        "htmlform-user-not-exists": "<strong>$1</strong> não existe.",
        "htmlform-user-not-valid": "<strong>$1</strong> não é um nome de usuário válido.",
-       "sqlite-has-fts": "$1 com suporte de pesquisa de texto completo",
-       "sqlite-no-fts": "$1 sem suporte de pesquisa de texto completo",
        "logentry-delete-delete": "$1 apagou a página $3",
        "logentry-delete-restore": "$1 restaurou a página $3",
        "logentry-delete-event": "$1 alterou a visibilidade {{PLURAL:$5|de uma entrada|de $5 entradas}} do registro $3: $4",
index c68b6f8..384ec28 100644 (file)
@@ -90,7 +90,7 @@
        "tog-watchdefault": "Adicionar as páginas e ficheiros que eu editar às minhas páginas vigiadas",
        "tog-watchmoves": "Adicionar as páginas e ficheiros que eu mover às minhas páginas vigiadas",
        "tog-watchdeletion": "Adicionar as páginas e ficheiros que eu eliminar às minhas páginas vigiadas",
-       "tog-watchuploads": "Adicionar novos ficheiros carregados por mim à minha lista de artigos vigiados",
+       "tog-watchuploads": "Adicionar novos ficheiros carregados por mim à minha lista de páginas vigiadas",
        "tog-watchrollback": "Adicionar páginas onde fiz uma reversão às minhas páginas vigiadas",
        "tog-minordefault": "Por omissão, marcar todas as edições como menores",
        "tog-previewontop": "Mostrar a antevisão antes da caixa de edição",
        "category_header": "Páginas na categoria \"$1\"",
        "subcategories": "Subcategorias",
        "category-media-header": "Multimédia na categoria \"$1\"",
-       "category-empty": "''Esta categoria não contém atualmente nenhuma página ou ficheiro multimédia.''",
+       "category-empty": "<em>Esta categoria não contém atualmente nenhuma página ou ficheiro multimédia.</em>",
        "hidden-categories": "{{PLURAL:$1|Categoria oculta|Categorias ocultas}}",
        "hidden-category-category": "Categorias ocultas",
        "category-subcat-count": "{{PLURAL:$2|Esta categoria só contém a seguinte subcategoria.|Esta categoria contém {{PLURAL:$1|a seguinte subcategoria|as seguintes $1 subcategorias}} (de um total de $2).}}",
        "talk": "Discussão",
        "views": "Vistas",
        "toolbox": "Ferramentas",
+       "tool-link-userrights": "Alterar grupos {{GENDER:$1|do utilizador|da utilizadora}}",
+       "tool-link-emailuser": "Enviar correio eletrónico a {{GENDER:$1|este utilizador|esta utilizadora|este(a) utilizador(a)}}",
        "userpage": "Ver página de utilizador",
        "projectpage": "Ver página de projeto",
        "imagepage": "Ver página de ficheiro",
        "missingarticle-rev": "(revisão#: $1)",
        "missingarticle-diff": "(Dif.: $1, $2)",
        "readonly_lag": "A base de dados foi automaticamente bloqueada enquanto os servidores secundários se sincronizam com o primário",
+       "nonwrite-api-promise-error": "O cabeçalho HTTP 'Promise-Non-Write-API-Action' foi enviado, mas o pedido está a ser feito a um módulo de escrita da API.",
        "internalerror": "Erro interno",
        "internalerror_info": "Erro interno: $1",
        "internalerror-fatal-exception": "Exceção fatal do tipo \"$1\"",
        "cannotloginnow-title": "Não é possível iniciar sessão agora",
        "cannotloginnow-text": "Não pode iniciar a sessão quando utilizar $1.",
        "cannotcreateaccount-title": "Não é possível criar contas",
+       "cannotcreateaccount-text": "A criação direta de contas não está ativada nesta wiki.",
        "yourdomainname": "O seu domínio:",
        "password-change-forbidden": "Não pode alterar palavras-passe nesta wiki.",
        "externaldberror": "Ocorreu um erro externo à base de dados durante a autenticação ou não lhe é permitido atualizar a sua conta externa.",
        "eauthentsent": "Foi enviada uma mensagem de confirmação para o endereço de correio eletrónico que especificou.\nAntes que seja enviada qualquer outra mensagem para a conta, terá de seguir as instruções na mensagem enviada, de modo a confirmar que a conta lhe pertence.",
        "throttled-mailpassword": "Já foi enviada um email de recuperação de palavra-passe {{PLURAL:$1|na última hora|nas últimas $1 horas}}.\nPara prevenir abusos, só um email de recuperação de palavra-passe pode ser enviado a cada {{PLURAL:$1|hora|$1 horas}}.",
        "mailerror": "Erro ao enviar correio electrónico: $1",
-       "acct_creation_throttle_hit": "Visitantes desta wiki com o seu endereço IP criaram $1 {{PLURAL:$1|conta|contas}} no último dia, o que é o máximo permitido neste período de tempo.\nEm resultado, visitantes com este endereço IP não podem criar mais nenhuma conta neste momento.",
+       "acct_creation_throttle_hit": "Visitantes desta wiki com endereço IP igual ao seu criaram {{PLURAL:$1|uma conta|$1 contas}} nos últimos (ou últimas) $2, o que é o máximo permitido neste período de tempo.\nEm resultado, visitantes com este endereço IP não podem criar mais nenhuma conta de momento.",
        "emailauthenticated": "O seu endereço de correio eletrónico foi confirmado a $2, às $3.",
        "emailnotauthenticated": "O seu endereço de correio eletrónico ainda não foi confirmado.\nNão lhe serão enviadas mensagens por nenhuma das seguintes funcionalidades.",
        "noemailprefs": "Especifique um endereço de correio eletrónico nas suas preferências para ativar estas funcionalidades.",
        "changepassword-success": "A sua palavra-passe foi alterada!",
        "changepassword-throttled": "Realizou demasiadas tentativas de início de sessão com esta conta.\nAguarde $1 antes de tentar novamente, por favor.",
        "botpasswords": "Palavras-passe de robô",
-       "botpasswords-summary": "As <em>palavras-passe de robô</em> permitem o acesso a uma conta de utilizador através da API sem utilizar as principais credenciais de login da conta. Os direitos de um utilizador, ao iniciar sessão com uma palavra-passe de robô, podem estar limitados.\n\nSe não sabe o que o leva a fazer isso, provavelmente não deveria fazê-lo. Ninguém deve solicitar que gere uma destas palavras-passe e a entregue.",
+       "botpasswords-summary": "As <em>palavras-passe de robô</em> permitem o acesso a uma conta de utilizador através da API, sem utilizar as credenciais principais de autenticação dessa conta. Os direitos de um utilizador, ao iniciar uma sessão com a palavra-passe de robô, podem estar limitados.\n\nSe não sabe para que necessita desta palavra-passe provavelmente não deveria criá-la. Nunca lhe deve ser solicitado que gere e entregue uma destas palavras-passe.",
        "botpasswords-disabled": "As palavras-passe de robô estão desactivadas.",
        "botpasswords-no-central-id": "Para utilizar palavras-passe de robô, deve iniciar sessão com uma conta centralizada.",
        "botpasswords-existing": "Palavras-passe de robô existentes",
        "botpasswords-label-cancel": "Cancelar",
        "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": "Atribuições aplicáveis:",
+       "botpasswords-help-grants": "Cada atribuição dá acesso às permissões listadas que uma conta de utilizador já possua. Consulte a [[Special:ListGrants|tabela de atribuições]] para mais informação.",
        "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?",
        "botpasswords-updated-body": "O robô palavra-passe para o nome do robô \"$1\" do utilizador \"$2\" foi atualizado.",
        "botpasswords-deleted-title": "Palavra-passe de robô eliminada",
        "botpasswords-deleted-body": "O robô palavra-passe para o nome do robô \"$1\"do utilizador \"$2\" foi eliminado.",
-       "botpasswords-newpassword": "A nova palavra-passe para iniciar sessão com <strong>$1</strong> é <strong>$2</strong>. Por favor, recorde-se dela para futura referência.</em>",
+       "botpasswords-newpassword": "A nova palavra-passe para iniciar sessão com <strong>$1</strong> é <strong>$2</strong>. <em>Anote-a para referência futura, por favor.</em> <br> (Para robôs antigos cujo nome de acesso tenha de ser igual ao eventual nome de utilizador, também pode usar o nome de utilizador <strong>$3</strong> e a palavra-passe <strong>$4</strong>.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider não está disponível.",
        "botpasswords-restriction-failed": "Restrições de palavra-passe de robô evitam esta autenticação.",
        "botpasswords-invalid-name": "O nome de utilizador especificado não contém o separador de palavra-passe de robô (\"$1\").",
        "passwordreset-emailelement": "{{GENDER:$1|Utilizador|Utilizadora}}: \n$1\n\nPalavra-passe temporária: \n$2",
        "passwordreset-emailsentemail": "Se este é o endereço de correio eletrónico associado a esta conta, ser-lhe-á enviada uma palavra-passe de reposição.",
        "passwordreset-emailsentusername": "Se houver um endereço de correio eletrónico associado a esta conta, ser-lhe-á enviada uma mensagem para redefinir a sua palavra-passe.",
-       "passwordreset-emailsent-capture2": "A redefinição da palavra-passe {{PLURAL:$1|do e-mail|dos e-mails}} foi enviada. {{PLURAL:$1|O nome de utilizador e palavra-passe|A lista de nomes de utilizador e palavras-passe}} encontram-se a seguir.",
-       "passwordreset-emailerror-capture2": "O envio do correio {{GENDER:$2|ao utilizador|à utilizadora|a(o) utilizador(a)}} falhou: $1 {{PLURAL:$3|O nome de utilizador e palavra-passe são mostradas abaixo|A lista de nomes de utilizadores e palavras-passe é mostrada abaixo}}.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|A mensagem|As mensagens}} de redefinição da palavra-passe {{PLURAL:$1|foi enviada|foram enviadas}} para o seu correio eletrónico. {{PLURAL:$1|O nome de utilizador e palavra-passe encontram-se|A lista de nomes de utilizador e palavras-passe encontra-se}} a seguir.",
+       "passwordreset-emailerror-capture2": "O envio do correio {{GENDER:$2|ao utilizador|à utilizadora|a(o) utilizador(a)}} falhou: $1 {{PLURAL:$3|O nome de utilizador e palavra-passe são mostrados aqui|A lista de nomes de utilizador e palavras-passe é mostrada aqui}}.",
        "passwordreset-nocaller": "Um interlocutor deve ser fornecido",
        "passwordreset-nosuchcaller": "A pessoa que chama não existe: $1",
-       "passwordreset-ignored": "A reposição de palavra-passe não foi realizada. Talvez não tenha sido configurado o provedor?",
+       "passwordreset-ignored": "A reposição de palavra-passe não foi realizada. Talvez o fornecedor não tenha sido configurado?",
        "passwordreset-invalideamil": "Correio eletrónico inválido",
        "passwordreset-nodata": "Não foram fornecidos nome de utilizador(a) nem endereço de correio eletrónico",
        "changeemail": "Alterar ou remover o endereço de correio eletrónico",
        "invalid-content-data": "Dados de conteúdo inválidos",
        "content-not-allowed-here": "Conteúdo do tipo \"$1\" não é permitido na página [[$2]]",
        "editwarning-warning": "Sair desta página fará com que perca quaisquer alterações feitas por si.\nSe iniciou sessão, pode desativar este aviso na secção \"{{int:prefs-editing}}\" das suas preferências.",
+       "editpage-invalidcontentmodel-title": "Modelo de conteúdo não suportado",
+       "editpage-invalidcontentmodel-text": "O modelo de conteúdo \"$1\" não é suportado.",
        "editpage-notsupportedcontentformat-title": "Formato de conteúdo não suportado",
        "editpage-notsupportedcontentformat-text": "O formato de conteúdo $1 não é suportado pelo modelo de conteúdo $2.",
        "content-model-wikitext": "wikitexto",
        "content-json-empty-object": "Objeto vazio",
        "content-json-empty-array": "Matriz vazia",
        "deprecated-self-close-category": "Páginas com etiquetas HTML de autofechamento não válidas",
+       "deprecated-self-close-category-desc": "Esta página contém marcações HTML auto-fechadas, que são inválidas, tais como <code>&lt;b/></code> ou <code>&lt;span/></code>.  O comportamento destas tags será alterado em breve, para ser consistente com a especificação HTML5, pelo que o seu uso na notação wiki foi descontinuado.",
        "duplicate-args-warning": "<strong>Aviso:</strong> [[:$1]] chama [[:$2]] com mais de um valor para o parâmetro \"$3\". Somente o último valor fornecido será utilizado.",
        "duplicate-args-category": "Páginas com argumentos de predefinições duplicados",
        "duplicate-args-category-desc": "A página contém campos de predefinições que utilizam duplicatas de argumentos, tais como <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> ou <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "mergehistory-fail-bad-timestamp": "Registo data/hora inválido",
        "mergehistory-fail-invalid-source": "Página de origem inválida.",
        "mergehistory-fail-invalid-dest": "Página de destino inválida.",
+       "mergehistory-fail-no-change": "A fusão de histórico não fundiu nenhuma revisão. Verifique os parâmetros de página e tempo, por favor.",
        "mergehistory-fail-permission": "Privilégios insuficientes para fundir os históricos.",
        "mergehistory-fail-self-merge": "As páginas de origem e de destino não podem ser a mesma.",
+       "mergehistory-fail-timestamps-overlap": "As revisões de origem sobrepõem ou são posteriores às revisões de destino.",
        "mergehistory-fail-toobig": "Não é possível fundir o histórico, já que um número de revisão(ões) acima do limite ($1 {{PLURAL:$1|revisão|revisões}}) seriam movidos.",
        "mergehistory-no-source": "A página de origem $1 não existe.",
        "mergehistory-no-destination": "A página de destino $1 não existe.",
        "grant-highvolume": "Alta quantidade de edições",
        "grant-oversight": "Ocultar utilizadores e edições suprimidas",
        "grant-patrol": "Patrulhar alterações a páginas",
+       "grant-privateinfo": "Aceder a informação privada",
        "grant-protect": "Proteger e desproteger páginas",
        "grant-rollback": "Reverter alterações a páginas",
        "grant-sendemail": "Enviar correio electrónico a outros utilizadores",
        "action-managechangetags": "criar e (des)ativar etiquetas",
        "action-applychangetags": "aplicar etiquetas juntamente com as suas alterações",
        "action-changetags": "adicionar e remover etiquetas arbitrárias em revisões e entradas de registo individuais",
+       "action-deletechangetags": "eliminar etiquetas da base de dados",
        "action-purge": "recarregar esta página",
        "nchanges": "$1 {{PLURAL:$1|alteração|alterações}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|desde a última visita}}",
        "file-thumbnail-no": "O nome do ficheiro começa por <strong>$1</strong>.\nParece ser uma imagem de tamanho reduzido (uma ''miniatura'' ou ''thumbnail)''.\nSe tiver a imagem original de maior dimensão, envie-a em vez desta. Se não, altere o nome do ficheiro, por favor.",
        "fileexists-forbidden": "Já existe um ficheiro com este nome, e não pode ser reescrito.\nSe ainda pretende carregar o seu ficheiro volte atrás e use outro nome, por favor. [[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Já existe um ficheiro com este nome no repositório de ficheiros partilhados.\nCaso deseje, mesmo assim, carregar o seu ficheiro, volte atrás e envie-o com um novo nome. [[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "O ficheiro carregado é um duplicado exato da versão atual de <strong>[[:$1]]</strong>.",
+       "fileexists-duplicate-version": "O ficheiro carregado é um duplicado exato {{PLURAL:$2|de uma versão anterior|de uma das versões anteriores}} de <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Este ficheiro é um duplicado {{PLURAL:$1|do seguinte|dos seguintes}}:",
        "file-deleted-duplicate": "Um ficheiro idêntico a este ([[:$1]]) foi eliminado anteriormente.\nVerifique o motivo da eliminação do ficheiro antes de prosseguir com o re-envio.",
        "file-deleted-duplicate-notitle": "Um ficheiro idêntico já foi eliminado e o seu título suprimido. Devia pedir a alguém capaz de ver os dados dos ficheiros eliminados para verificar a situação antes de carregá-lo novamente.",
        "uploaddisabledtext": "O carregamento de ficheiros está desativado.",
        "php-uploaddisabledtext": "O carregamento de ficheiros está desativado no PHP.\nVerifique a configuração file_uploads, por favor.",
        "uploadscripted": "Este ficheiro contém HTML ou código que pode ser erradamente interpretado por um navegador.",
-       "upload-scripted-pi-callback": "Não se podem carregar arquivos que contenham instruções de processamento de páginas de estilo XML",
+       "upload-scripted-pi-callback": "Não é possível carregar ficheiros que contenham instruções de processamento de páginas de estilo XML.",
        "uploaded-script-svg": "Encontrou um elemento scriptable no ficheiro \"$1\" SVG carregado.",
-       "uploaded-hostile-svg": "Encontrou-se um código CSS não seguro no elemento de estilo do arquivo SVG carregado.",
-       "uploaded-event-handler-on-svg": "Não está permitido configurar atributos controladores de eventos <code>$1=\"$2\"</code> nos arquivos SVG.",
+       "uploaded-hostile-svg": "Encontrou-se um código CSS não seguro no elemento de estilo do ficheiro SVG carregado.",
+       "uploaded-event-handler-on-svg": "Não á permitido configurar atributos controladores de eventos <code>$1=\"$2\"</code> nos ficheiros SVG.",
+       "uploaded-href-attribute-svg": "Os atributos <code>href</code> em ficheiros SVG só estão autorizados a ligar a endereços http:// ou https://, mas foi encontrado <code>&lt;$1 $2=\"$3\"&gt;</code>.",
+       "uploaded-href-unsafe-target-svg": "Detetado <code>href</code> para dados inseguros: alvo URI <code>&lt;$1 $2=\"$3\"&gt;</code> no ficheiro SVG carregado.",
+       "uploaded-animate-svg": "Foi detetado um elemento \"animate\" que pode estar a alterar <code>href</code>, usando o atributo \"from\" <code>&lt;$1 $2=\"$3\"&gt;</code> no ficheiro SVG carregado.",
+       "uploaded-setting-event-handler-svg": "A definição de atributos controladores de eventos está bloqueada. Foi detetado <code>&lt;$1 $2=\"$3\"&gt;</code> no ficheiro SVG carregado.",
+       "uploaded-setting-href-svg": "O uso da tag \"set\" para adicionar o atributo \"href\" ao elemento mãe está bloqueado.",
+       "uploaded-wrong-setting-svg": "O uso da tag \"set\" para adicionar um destino remoto/de dados/<i>script</i> a qualquer atributo está bloqueado. No ficheiro SVG enviado foi encontrado <code>&lt;set to=\"$1\"&gt;</code>.",
+       "uploaded-setting-handler-svg": "A configuração do atributo \"handler\" com destino remoto/de dados/<i>script</i> em ficheiros SVG está bloqueada. Foi detetado <code>$1=\"$2\"</code> no ficheiro SVG carregado.",
+       "uploaded-remote-url-svg": "A configuração de qualquer atributo de estilo com uma URL remota em ficheiros SVG está bloqueada. Foi detetado <code>$1=\"$2\"</code> no ficheiro SVG carregado.",
        "uploaded-image-filter-svg": "Foi encontrado um filtro de imagem com a URL: <code>&lt;$1 $2=\"$3\"&gt;</code> no ficheiro SVG carregado.",
        "uploadscriptednamespace": "Este ficheiro SVG contém um domínio que não é permitido \"$1\".",
        "uploadinvalidxml": "Erro detectado na análise do XML do ficheiro carregado.",
        "upload-http-error": "Ocorreu um erro HTTP: $1",
        "upload-copy-upload-invalid-domain": "Não é possível realizar carregamentos remotos neste domínio.",
        "upload-foreign-cant-upload": "Esta wiki não está configurada para carregar ficheiros para o repositório externo solicitado.",
+       "upload-foreign-cant-load-config": "Não foi possível inserir a configuração de carregamento de ficheiros no repositório externo.",
+       "upload-dialog-disabled": "O carregamento de ficheiros através deste diálogo está desativado na wiki.",
        "upload-dialog-title": "Carregar ficheiro",
        "upload-dialog-button-cancel": "Cancelar",
+       "upload-dialog-button-back": "Voltar",
        "upload-dialog-button-done": "Feito",
        "upload-dialog-button-save": "Gravar",
        "upload-dialog-button-upload": "Carregar",
        "upload-form-label-infoform-title": "Detalhes",
        "upload-form-label-infoform-name": "Nome",
+       "upload-form-label-infoform-name-tooltip": "Um título descritivo e único para ser usado como nome do ficheiro. Pode usar linguagem normal e espaços. Não inclua a extensão do ficheiro.",
        "upload-form-label-infoform-description": "Descrição",
+       "upload-form-label-infoform-description-tooltip": "Descreva de forma breve todos os elementos notórios sobre o trabalho.\nPara uma fotografia, mencione os principais destaques, a ocasião ou o lugar.",
        "upload-form-label-usage-title": "Uso",
        "upload-form-label-usage-filename": "Nome do ficheiro",
        "upload-form-label-own-work": "Este é minha obra própria",
        "uploadstash-errclear": "Não foi possível apagar os ficheiros.",
        "uploadstash-refresh": "Atualizar a lista de ficheiros",
        "uploadstash-thumbnail": "ver miniatura",
+       "uploadstash-exception": "Não foi possível gravar o carregamento na área de ficheiros escondidos ($1): \"$2\".",
        "invalid-chunk-offset": "Deslocamento de fragmento inválido",
        "img-auth-accessdenied": "Acesso negado",
        "img-auth-nopathinfo": "PATH_INFO em falta.\nO seu servidor não está configurado para passar esta informação.\nPode ser baseado em CGI e não consegue suportar img_auth.\nConsulte a documentação em https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "filerevert-submit": "Reverter",
        "filerevert-success": "'''[[Media:$1|$1]]''' foi revertida para a [$4 versão das $3 de $2].",
        "filerevert-badversion": "Não há uma versão local anterior deste ficheiro no período de tempo especificado.",
+       "filerevert-identical": "A versão atual do ficheiro já é idêntica à selecionada.",
        "filedelete": "Eliminar $1",
        "filedelete-legend": "Eliminar ficheiro",
        "filedelete-intro": "Está prestes a eliminar o ficheiro '''[[Media:$1|$1]]''' e todo o seu histórico.",
        "filedelete-success": "'''$1''' foi eliminado.",
        "filedelete-success-old": "A versão de '''[[Media:$1|$1]]''' tal como $3, $2 foi eliminada.",
        "filedelete-nofile": "'''$1''' não existe.",
-       "filedelete-nofile-old": "Não há nenhuma versão de '''$1''' em arquivo com os parâmetros especificados.",
+       "filedelete-nofile-old": "Não há nenhuma versão de <strong>$1</strong> em arquivo com os atributos especificados.",
        "filedelete-otherreason": "Outro/motivo adicional:",
        "filedelete-reason-otherlist": "Outro motivo",
        "filedelete-reason-dropdown": "*Motivos comuns para eliminação\n** Violação de direitos de autor\n** Ficheiro duplicado",
        "apihelp": "Ajuda API",
        "apihelp-no-such-module": "Módulo \"$1\" não encontrado.",
        "apisandbox": "Testes da API",
+       "apisandbox-jsonly": "Para usar a área de testes da API é necessário o JavaScript.",
        "apisandbox-api-disabled": "A API está desativada neste site.",
        "apisandbox-intro": "Use esta página para fazer experiências com a <strong>API de serviços da web do MediaWiki</strong>.\nConsulte a [[mw:API:Main page|documentação da API]] para informações sobre o uso da API. Exemplo: [https://www.mediawiki.org/wiki/API#A_simple_example obter o conteúdo da Página Principal]. Selecione uma operação para ver mais exemplos.\n\nNote que, embora esta seja uma área de testes, as operações que executar nesta página podem modificar a wiki.",
        "apisandbox-fullscreen": "Expandir painel",
        "apisandbox-fullscreen-tooltip": "Expandir o painel da página de testes para preencher a janela do navegador.",
        "apisandbox-unfullscreen": "Mostrar página",
+       "apisandbox-unfullscreen-tooltip": "Reduza o painel da área de testes, para que as ligações de navegação estejam disponíveis.",
        "apisandbox-submit": "Fazer o pedido",
        "apisandbox-reset": "Limpar",
        "apisandbox-retry": "Tentar novamente",
        "apisandbox-results": "Resultados",
        "apisandbox-sending-request": "A enviar solicitação de API...",
        "apisandbox-loading-results": "A receber resultados da API...",
+       "apisandbox-results-error": "Ocorreu um erro ao carregar a resposta à consulta por API: $1",
        "apisandbox-request-url-label": "URL do pedido:",
        "apisandbox-request-time": "Tempo de processamento: {{PLURAL:$1|$1 ms}}",
        "apisandbox-results-fixtoken": "Corrija o identificador e volte a submete-lo",
        "apisandbox-results-fixtoken-fail": "Não foi possível obter o identificador \"$1\".",
        "apisandbox-alert-page": "Os campos nesta página não são válidos.",
        "apisandbox-alert-field": "O valor deste campo não é válido.",
+       "apisandbox-continue": "Continuar",
+       "apisandbox-continue-clear": "Limpar",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} [https://www.mediawiki.org/wiki/API:Query#Continuing_queries continuará] o último pedido; {{int:apisandbox-continue-clear}} limpará os parâmetros relativos à continuação.",
        "booksources": "Fontes bibliográficas",
        "booksources-search-legend": "Pesquisar referências bibliográficas",
        "booksources-search": "Pesquisar",
        "listgrouprights-namespaceprotection-namespace": "Domínio",
        "listgrouprights-namespaceprotection-restrictedto": "Direito(s) do utilizador para editar",
        "listgrants": "Atribuições",
-       "listgrants-summary": "Esta é uma lista de atribuições com os respetivos acessos às permissões de utilizador. Os utilizadores podem autorizar aplicações a utilizar suas contas, mas com permissões limitadas baseadas nas atribuições dadas pelos utilizadores a cada aplicação. No entanto, uma aplicação agindo em nome de um utilizador não pode utilizar permissões que o utilizador não possui.\nPode haver [[{{MediaWiki:Listgrouprights-helppage}}|informação adicional]] sobre permissões individuais.",
+       "listgrants-summary": "Esta é uma lista de atribuições com os respetivos acessos às permissões de utilizador. Os utilizadores podem autorizar aplicações a utilizar as suas contas, mas com permissões limitadas baseadas nas atribuições dadas pelos utilizadores a cada aplicação. No entanto, uma aplicação que age em nome de um utilizador não pode utilizar permissões que o utilizador não possui.\nPode haver [[{{MediaWiki:Listgrouprights-helppage}}|informação adicional]] sobre as permissões individuais.",
        "listgrants-grant": "Atribuição",
        "listgrants-rights": "Direitos",
        "trackingcategories": "Categorias de monitorização",
-       "trackingcategories-summary": "Esta página lista as categorias monitoradas que foram geradas automaticamente pelo software MediaWiki. Os seus nomes podem ser alterados ao editar sua mensagem correspondente no domínio {{ns:8}}.",
+       "trackingcategories-summary": "Esta página lista as categorias de monitorização geradas automaticamente pelo software MediaWiki. Os nomes das categorias podem ser alterados modificando as mensagens de sistema relevantes no domínio {{ns:8}}.",
        "trackingcategories-msg": "Categoria monitorada",
        "trackingcategories-name": "Nome da mensagem",
        "trackingcategories-desc": "Critérios de inclusão",
        "restricted-displaytitle-ignored": "Páginas com títulos de exibição ignorados",
        "restricted-displaytitle-ignored-desc": "Esta página tem um <code><nowiki>{{DISPLAYTITLE}}</nowiki></code> ignorado porque não é equivalente ao título verdadeiro da página.",
-       "noindex-category-desc": "A página não é indexada por robôs porque contém a palavra mágica <code><nowiki>__NOINDEX__</nowiki></code> e está num domínio onde o estatuto é permitido.",
+       "noindex-category-desc": "A página não é indexada por robôs porque contém a palavra mágica <code><nowiki>__NOINDEX__</nowiki></code> e está num domínio onde esta palavra mágica é permitida.",
        "index-category-desc": "A página contém a palavra mágica <code><nowiki>__INDEX__</nowiki></code> (e está num domínio em que essa marca é permitida) e, portanto, será indexada pelos robôs mesmo quando normalmente não o seria.",
        "post-expand-template-inclusion-category-desc": "O tamanho da página é superior a <code>$wgMaxArticleSize</code>, após a expansão de todas as predefinições, pelo que algumas predefinições não foram expandidas.",
        "post-expand-template-argument-category-desc": "O tamanho da página é superior a <code>$wgMaxArticleSize</code>, após a expansão de um argumento de predefinição (algo em chavetas triplas, como <code>{{{Foo}}}</code>).",
        "undeletehistorynoadmin": "Esta página foi eliminada. O motivo de eliminação é apresentado no sumário abaixo, junto dos detalhes do utilizador que editou esta página antes de eliminar. O texto atual destas edições eliminadas encontra-se agora apenas disponível para administradores.",
        "undelete-revision": "Edição eliminada da página $1 (das $5 de $4), por $3:",
        "undeleterevision-missing": "Edição inválida ou não encontrada.\nPode ter usado uma ligação incorreta ou talvez a revisão tenha sido restaurada ou removida do arquivo.",
+       "undeleterevision-duplicate-revid": "Não foi possível restaurar {{PLURAL:$1|uma revisão|$1 revisões}}, porque {{PLURAL:$1|a sua <code>rev_id</code> já estava a ser usada|as respetivas <code>rev_id</code> já estavam a ser usadas}}.",
        "undelete-nodiff": "Não foram encontradas edições anteriores.",
        "undeletebtn": "Restaurar",
        "undeletelink": "ver/restaurar",
        "whatlinkshere-hideredirs": "$1 redirecionamentos",
        "whatlinkshere-hidetrans": "$1 transclusões",
        "whatlinkshere-hidelinks": "$1 ligações",
-       "whatlinkshere-hideimages": "$1 links para arquivos",
+       "whatlinkshere-hideimages": "$1 ligações para ficheiros",
        "whatlinkshere-filters": "Filtros",
        "whatlinkshere-submit": "Ir",
        "autoblockid": "Bloqueio automático nº$1",
        "tag-filter": "Filtro de [[Special:Tags|etiquetas]]:",
        "tag-filter-submit": "Filtrar",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Etiqueta|Etiquetas}}]]: $2)",
+       "tag-mw-contentmodelchange": "alteração do modelo de conteúdo",
+       "tag-mw-contentmodelchange-description": "Edições que [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel alteram o modelo de conteúdo] de uma página",
        "tags-title": "Etiquetas",
        "tags-intro": "Esta página lista as etiquetas com que o software poderá marcar uma edição, e o seu significado.",
        "tags-tag": "Nome da etiqueta",
        "tags-delete-not-found": "A etiqueta \"$1\" não existe.",
        "tags-delete-too-many-uses": "A etiqueta \"$1\" está aplicada em mais que $2 {{PLURAL:$2|edição|edições}}, o que significa que não pode ser eliminada.",
        "tags-delete-warnings-after-delete": "A etiqueta \"$1\" foi eliminada, mas {{PLURAL:$2|o seguinte aviso foi encontrado|os seguintes avisos foram encontrados}}:",
+       "tags-delete-no-permission": "Não tem permissão para eliminar etiquetas de modificação.",
        "tags-activate-title": "Ativar etiqueta",
        "tags-activate-question": "Está prestes a ativar a etiqueta \"$1\".",
        "tags-activate-reason": "Motivo:",
        "tags-deactivate-not-allowed": "Não é possível desativar a etiqueta \"$1\".",
        "tags-deactivate-submit": "Desativar",
        "tags-apply-no-permission": "Não possui privilégios para aplicar alterações a etiquetas em conjunto com as suas modificações.",
+       "tags-apply-blocked": "Não pode aplicar etiquetas de modificação nas suas alterações enquanto estiver bloqueado(a).",
        "tags-apply-not-allowed-one": "A etiqueta \"$1\" não pode ser aplicada manualmente.",
        "tags-apply-not-allowed-multi": "{{PLURAL:$2|A seguinte etiqueta não pode ser aplicada|As seguintes etiquetas não podem ser aplicadas}} manualmente: $1",
        "tags-update-no-permission": "Não possui privilégios para adicionar ou remover etiquetas de revisões individuais ou entradas de registo.",
+       "tags-update-blocked": "Não pode adicionar ou remover etiquetas de modificação enquanto estiver bloqueado(a).",
        "tags-update-add-not-allowed-one": "A etiqueta \"$1\" não pode ser adicionada manualmente.",
        "tags-update-add-not-allowed-multi": "{{PLURAL:$2|A seguinte etiqueta não pode ser adicionada|As seguintes etiquetas não podem ser adicionadas}} manualmente: $1",
        "tags-update-remove-not-allowed-one": "A remoção da etiqueta \"$1\" não é permitida.",
        "htmlform-cloner-create": "Adicionar mais",
        "htmlform-cloner-delete": "Remover",
        "htmlform-cloner-required": "Pelo menos um valor é necessário.",
+       "htmlform-date-placeholder": "AAAA-MM-DD",
+       "htmlform-time-placeholder": "HH:MM:SS",
+       "htmlform-datetime-placeholder": "AAAA-MM-DD HH:MM:SS",
+       "htmlform-date-invalid": "O valor especificado não é reconhecido como data. Tente usar o formato AAAA-MM-DD.",
+       "htmlform-time-invalid": "O valor especificado não é reconhecido como hora. Tente usar o formato HH:MM:SS.",
+       "htmlform-datetime-invalid": "O valor especificado não é reconhecido como data e hora. Tente usar o formato AAAA-MM-DD HH:MM:SS.",
+       "htmlform-date-toolow": "O valor especificado é anterior à data mais antiga que é permitida, $1.",
+       "htmlform-date-toohigh": "O valor especificado é posterior à data mais recente que é permitida, $1.",
+       "htmlform-time-toolow": "O valor especificado é anterior à hora mínima permitida, $1.",
+       "htmlform-time-toohigh": "O valor especificado é anterior à hora máxima permitida, $1.",
+       "htmlform-datetime-toolow": "O valor especificado é anterior à data e hora mínima permitida, $1.",
+       "htmlform-datetime-toohigh": "O valor especificado é posterior à data e hora máxima permitida, $1.",
        "htmlform-title-badnamespace": "[[:$1]] não se encontra no domínio \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" não é um título que possa ser atribuído a uma página",
        "htmlform-title-not-exists": "$1 não existe.",
        "htmlform-user-not-exists": "<strong>$1</strong> não existe.",
        "htmlform-user-not-valid": "<strong>$1</strong> não é um nome de utilizador válido.",
-       "sqlite-has-fts": "$1 com suporte de pesquisa de texto completo",
-       "sqlite-no-fts": "$1 sem suporte de pesquisa de texto completo",
        "logentry-delete-delete": "$1 apagou a página $3",
        "logentry-delete-restore": "$1 restaurou a página $3",
        "logentry-delete-event": "$1 alterou a visibilidade de {{PLURAL:$5|uma entrada|$5 entradas}} em $3: $4",
        "logentry-suppress-block": "$1 {{GENDER:$2|bloqueou}} {{GENDER:$4|$3}} com expiração a $5 $6",
        "logentry-suppress-reblock": "$1 {{GENDER:$2|modificou}} parâmetros de bloqueio de {{GENDER:$4|$3}} com expiração a $5 $6",
        "logentry-import-upload": "$1 {{GENDER:$2|importou}} $3 através de carregamento de ficheiro",
+       "logentry-import-upload-details": "$1 {{GENDER:$2|importou}} $3 por carregamento de ficheiro($4 {{PLURAL:$4|revisão|revisões}})",
        "logentry-import-interwiki": "$1 {{GENDER:$2|importou}} $3 de outra wiki",
+       "logentry-import-interwiki-details": "$1 {{GENDER:$2|importou}} $3 de $5 ($4 {{PLURAL:$4|revisão|revisões}})",
        "logentry-merge-merge": "$1 {{GENDER:$2|fundiu}} $3 com $4 (edições até $5)",
        "logentry-move-move": "$1 moveu a página $3 para $4",
        "logentry-move-move-noredirect": "$1 moveu a página $3 para $4 sem deixar um redirecionamento",
        "feedback-external-bug-report-button": "Assinalar erro técnico",
        "feedback-dialog-title": "Enviar opinião",
        "feedback-dialog-intro": "Pode usar o fácil formulário abaixo para enviar os seus comentários. A sua opinião será adicionada à página \"$1\", juntamente com o seu nome de utilizador(a).",
-       "feedback-error-title": "Erro",
        "feedback-error1": "Erro: O resultado da API não foi reconhecido",
        "feedback-error2": "Erro: A edição falhou",
        "feedback-error3": "Erro: A API não responde",
        "api-error-nomodule": "Erro interno: Não está definido nenhum módulo para o carregamento de ficheiros.",
        "api-error-ok-but-empty": "Erro interno: o servidor não respondeu.",
        "api-error-overwrite": "Não é permitido sobrescrever um ficheiro existente.",
+       "api-error-ratelimited": "Está a tentar carregar mais ficheiros do que esta wiki permite num espaço de tempo curto. Tente de novo dentro de alguns minutos, por favor.",
        "api-error-stashfailed": "Erro interno: O servidor não conseguiu armazenar o ficheiro temporário.",
        "api-error-publishfailed": "Erro interno: Servidor não conseguiu publicar ficheiro temporário.",
        "api-error-stasherror": "Ocorreu um erro no carregamento do ficheiro escondido.",
-       "api-error-stashedfilenotfound": "O ficheiro do stash não foi encontrado ao tentar carregá-lo.",
+       "api-error-stashedfilenotfound": "O ficheiro escondido não foi encontrado ao tentar carregá-lo.",
        "api-error-stashpathinvalid": "O caminho no qual o ficheiro escondido deveria ter sido encontrado era inválido.",
        "api-error-stashfilestorage": "Ocorreu um erro no carregamento do ficheiro escondido.",
        "api-error-stashzerolength": "O servidor não pôde esconder o ficheiro, porque ele tinha de comprimento zero.",
-       "api-error-stashnotloggedin": "Você deve estar com sessão iniciaca para gravar ficheiros no carregamento do stash.",
-       "api-error-stashwrongowner": "O ficheiro que estava a tentar aceder o stash não pertence a você.",
-       "api-error-stashnosuchfilekey": "O ficheiro de chave que está a tentar aceder no stash não existe.",
+       "api-error-stashnotloggedin": "Tem de ter uma sessão iniciada para gravar ficheiros na área de ficheiros escondidos.",
+       "api-error-stashwrongowner": "O ficheiro a que estava a tentar aceder na área de ficheiros escondidos não lhe pertence.",
+       "api-error-stashnosuchfilekey": "O chave do ficheiro a que estava a tentar aceder na área de ficheiros escondidos não existe.",
        "api-error-timeout": "O servidor não respondeu no prazo esperado.",
        "api-error-unclassified": "Ocorreu um erro desconhecido",
        "api-error-unknown-code": "Erro desconhecido: \"$1\"",
        "log-name-pagelang": "Registo de alteração de idioma",
        "log-description-pagelang": "Este é um registo de alterações aos idiomas das páginas.",
        "logentry-pagelang-pagelang": "$1 {{GENDER:$2|alterou}} o idioma da página $3 de $4 para $5.",
+       "default-skin-not-found": "O tema padrão da sua wiki definido em <code dir=\"ltr\">$wgDefaultSkin</code>, <code>$1</code>, não está disponível.\n\nA instalação parece incluir {{PLURAL:$4|o seguinte tema|os seguintes temas}}. Consulte [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Configuração de Temas] para saber como {{PLURAL:$4|ativá-lo|ativá-los e escolher o tema padrão}}.\n\n$2\n\n; Se acabou de instalar o MediaWiki:\n: Provavelmente instalou-o a partir do git, ou diretamente do código fonte usando outro método. O comportamento é o esperado. Tente instalar temas a partir do [https://www.mediawiki.org/wiki/Category:All_skins diretório de temas da mediawiki.org], assim:\n:* Descarregue o  [https://www.mediawiki.org/wiki/Download tarball de instalação], que contém vários temas e extensões. Pode copiar o diretório <code>skins/</code> nele incluído.\n:* Descarregue tarballs de temas individuais, da [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Use o Git para descarregar temas].\n: Se é programador(a) do MediaWiki, isto não deverá interferir com o seu repositório git.\n\n; Se fez uma atualização do MediaWiki:\n: O MediaWiki 1.24 e versões mais recentes não ativam automaticamente os temas instalados (consulte [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manual: Autodescoberta do Tema]). Pode copiar {{PLURAL:$5|a linha seguinte|as linhas seguintes}} para o ficheiro <code>LocalSettings.php</code> para ativar {{PLURAL:$5|o tema instalado|os temas instalados}}:\n\n<pre dir=\"ltr\">$3</pre>\n\n; Se acabou de modificar o <code>LocalSettings.php</code>:\n: Verifique cuidadosamente se o nome de cada tema está bem soletrado.",
+       "default-skin-not-found-no-skins": "O tema padrão da sua wiki definido em <code dir=\"ltr\">$wgDefaultSkin</code>, <code>$1</code>, não está disponível.\n\nNão tem nenhum tema instalado.\n\n; Se acabou de instalar ou atualizar o MediaWiki:\n: Provavelmente instalou-o a partir do git, ou diretamente do código fonte usando outro método. O comportamento é o esperado. O MediaWiki 1.24 e versões mais recentes não incluem qualquer tema no repositório principal. Tente instalar temas a partir do [https://www.mediawiki.org/wiki/Category:All_skins diretório de temas da mediawiki.org], assim:\n:* Descarregue o  [https://www.mediawiki.org/wiki/Download tarball de instalação], que contém vários temas e extensões. Pode copiar o diretório <code>skins/</code> nele incluído.\n:* Descarregue tarballs de temas individuais, da [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Use o Git para descarregar temas].\n: Se é programador(a) do MediaWiki, isto não deverá interferir com o seu repositório git. Consulte [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Configuração de Temas] para saber como ativar temas e escolher o tema padrão.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (ativado)",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>desativado</strong>)",
        "mediastatistics": "Estatísticas multimédia",
        "log-action-filter-suppress-revision": "Supressão de revisões",
        "log-action-filter-suppress-delete": "Supressão de página",
        "log-action-filter-suppress-block": "Supressão de utilizadores por bloqueio",
+       "log-action-filter-suppress-reblock": "Supressão de utilizador por rebloqueio",
        "log-action-filter-upload-upload": "Novo carregamento",
        "log-action-filter-upload-overwrite": "Recarregar",
+       "authmanager-authn-not-in-progress": "A autenticação não está em curso ou os dados da sessão foram perdidos. Comece novamente desde o princípio, por favor.",
        "authmanager-authn-no-primary": "As informações de identificação fornecidas não podem ser autenticadas.",
+       "authmanager-authn-no-local-user": "As credenciais fornecidas não estão associadas a nenhum utilizador nesta wiki.",
+       "authmanager-authn-no-local-user-link": "As credenciais fornecidas são válidas mas não estão associadas a nenhum utilizador nesta wiki. Inicie a sessão de outra forma, ou crie um novo utilizador, e terá a opção de ligar as credenciais anteriores a essa conta.",
        "authmanager-authn-autocreate-failed": "A criação automática de uma conta local falhou: $1",
+       "authmanager-change-not-supported": "As credenciais fornecidas não podem ser alteradas porque ninguém as utiliza.",
        "authmanager-create-disabled": "A criação de contas está desativada.",
        "authmanager-create-from-login": "Para criar a sua conta, por favor, preencha os campos abaixo.",
+       "authmanager-create-not-in-progress": "A criação da conta não está em curso ou os dados da sessão foram perdidos. Comece novamente desde o princípio, por favor.",
+       "authmanager-create-no-primary": "Não foi possível criar uma conta com as credenciais fornecidas.",
+       "authmanager-link-no-primary": "Não foi possível ligar a conta usando as credenciais fornecidas.",
+       "authmanager-link-not-in-progress": "A ligação da conta não está em curso ou os dados da sessão foram perdidos. Comece novamente desde o princípio, por favor.",
        "authmanager-authplugin-setpass-failed-title": "A alteração de palavra-passe falhou",
        "authmanager-authplugin-setpass-failed-message": "O plugin de autenticação negou a alteração de palavra-passe.",
        "authmanager-authplugin-create-fail": "O plugin de autenticação negou a criação de conta.",
        "authmanager-autocreate-noperm": "A criação automática de contas não é permitida.",
        "authmanager-autocreate-exception": "A criação automática de contas foi temporariamente desativada devido a erros prévios.",
        "authmanager-userdoesnotexist": "A conta de utilizador(a) \"$1\" não está registada.",
+       "authmanager-userlogin-remembermypassword-help": "Se a palavra-passe deve ser memorizada por um período superior à duração da sessão.",
        "authmanager-username-help": "Nome de utilizador(a) para autenticação.",
        "authmanager-password-help": "Palavra-passe para autenticação.",
        "authmanager-domain-help": "Domínio para a autenticação externa.",
        "authmanager-provider-password-domain": "Autenticação baseada em palavra-passe e domínio",
        "authmanager-provider-temporarypassword": "Palavra-passe temporária",
        "authprovider-confirmlink-message": "Com base nas tuas últimas tentativas para iniciar sessão, as seguintes contas podem ser ligadas à tua conta wiki. Vinculá-las permite que inicie sessão através das mesmas. Selecione quais pretende vincular.",
-       "authprovider-confirmlink-success-line": "$1: Ligado com êxito.",
+       "authprovider-confirmlink-request-label": "Contas que devem ser ligadas",
+       "authprovider-confirmlink-success-line": "$1: Ligação realizada.",
+       "authprovider-confirmlink-failed": "A ligação das contas não foi totalmente realizada: $1",
+       "authprovider-confirmlink-ok-help": "Continuar depois de mostrar as mensagens de erro na ligação.",
        "authprovider-resetpass-skip-label": "Ignorar",
        "authprovider-resetpass-skip-help": "Ignorar redefinição de palavra-passe",
+       "authform-nosession-login": "A autenticação ocorreu, mas o seu navegador não se \"recorda\" de ter iniciado uma sessão.\n\n$1",
+       "authform-nosession-signup": "A conta foi criada, mas o seu navegador não se \"recorda\" de ter iniciado uma sessão.\n\n$1",
        "authform-newtoken": "Chave em falta. $1",
        "authform-notoken": "Chave em falta",
        "authform-wrongtoken": "Chave errada",
        "linkaccounts-success-text": "A conta foi associada.",
        "linkaccounts-submit": "Associar contas",
        "unlinkaccounts": "Desassociar contas",
-       "unlinkaccounts-success": "A conta foi desassociada."
+       "unlinkaccounts-success": "A conta foi desassociada.",
+       "authenticationdatachange-ignored": "A alteração dos dados de autenticação não foi realizada. Talvez o fornecedor não tenha sido configurado?",
+       "userjsispublic": "Nota: As subpáginas de Javascript não devem conter dados confidenciais porque podem ser vistas por outros utilizadores.",
+       "usercssispublic": "Nota: As subpáginas de CSS não devem conter dados confidenciais porque podem ser vistas por outros utilizadores.",
+       "restrictionsfield-badip": "Endereço IP (ou gama de endereços IP) inválido: $1",
+       "restrictionsfield-label": "Gamas de endereços IP permitidas:",
+       "restrictionsfield-help": "Um endereço IP ou uma gama CIDR por linha. Para activar todos,\nuse<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index fbf95cc..96f8c38 100644 (file)
        "navigation": "This is shown as a section header in the sidebar of most skins.\n\n{{Identical|Navigation}}",
        "and": "The translation for \"and\" appears in the [[Special:Version]] page, between the last two items of a list. If a comma is needed, add it at the beginning without a gap between it and the \"&\". &amp;#32; is a blank space, one character long. Please leave it as it is.\n\nThis can also appear in the credits page if the credits feature is enabled,for example [{{canonicalurl:Support|action=credits}} the credits of the support page]. (To view any credits page type <nowiki>&action=credits</nowiki> at the end of any URL in the address bar.)\n{{Identical|And}}",
        "qbfind": "Alternative for \"search\" as used in Cologne Blue skin.\n{{Identical|Find}}",
-       "qbbrowse": "Heading in sidebar menu in CologneBlue skin as seen in http://i.imgur.com/I08Y3jW.png\n{{Identical|Browse}}",
+       "qbbrowse": "Heading in sidebar menu in CologneBlue skin as seen in [[File:CologneBlue sidebar qqx.png]]\n{{Identical|Browse}}",
        "qbedit": "Heading in sidebar menu in CologneBlue skin as seen in http://i.imgur.com/I08Y3jW.png\n{{Identical|Edit}}",
        "qbpageoptions": "Heading in sidebar menu in CologneBlue skin as seen in http://i.imgur.com/I08Y3jW.png\n{{Identical|This page}}",
        "qbmyoptions": "Heading in the Cologne Blue skin user menu containing links to user (talk) page, preferences, watchlist, etc.\n{{Identical|My pages}}",
        "talk": "Used as display name for the tab to all {{msg-mw|Talk}} pages. These pages accompany all content pages and can be used for discussing the content page. Example: [[Talk:Example]].\n\nSee also:\n* {{msg-mw|Talk}}\n* {{msg-mw|Accesskey-ca-talk}}\n* {{msg-mw|Tooltip-ca-talk}}\n{{Identical|Discussion}}",
        "views": "Subtitle for the list of available views, for the current page. In \"monobook\" skin the list of views are shown as tabs, so this sub-title is not shown. For an example, see [{{canonicalurl:Main_Page|useskin=simple}} Main Page using simple skin].\n\n'''Note:''' This is \"views\" as in \"appearances\"/\"representations\", '''not''' as in \"visits\"/\"accesses\".\n{{Identical|View}}",
        "toolbox": "The title of the toolbox below the search menu.\n{{Identical|Tool}}",
+       "tool-link-userrights": "Link to [[Special:UserRights]] (user rights management) in the sidebar toolbox.\n\nParameters:\n* $1 - Name of user for the user group management (usable for GENDER)",
+       "tool-link-emailuser": "Link to [[Special:EmailUser]] (email user tool) in the sidebar toolbox.\n\nParameters:\n* $1 - Name of user who would receive the email\n\nSee also:\n* {{msg-mw|Emailuser-title-target}}",
        "userpage": "Used in user talk pages as the text of the link to the user page, with the Cologne Blue skin.",
        "projectpage": "Used as link text in Talk page of project page with the Cologne Blue skin.",
        "imagepage": "Used as link text in Talk page of file page.",
        "signupend": "{{notranslate}}",
        "signupend-https": "{{notranslate}}",
        "mailerror": "Used as error message in sending confirmation mail to user. Parameters:\n* $1 - new mail address",
-       "acct_creation_throttle_hit": "Error message at [[Special:CreateAccount]].\n\n\"in the last day\" precisely means: during the lasts 86400 seconds (24 hours) ending right now.\n\nParameters:\n* $1 - number of accounts",
+       "acct_creation_throttle_hit": "Error message at [[Special:CreateAccount]].\n\nParameters:\n* $1 - number of accounts\n* $2 - period",
        "emailauthenticated": "In user preferences ([[Special:Preferences]] > {{int:prefs-personal}} > {{int:email}}) and on [[Special:ConfirmEmail]].\n\nParameters:\n* $1 - (Unused) obsolete, date and time\n* $2 - date\n* $3 - time",
        "emailnotauthenticated": "Message in [[Special:Preferences]] > {{int:prefs-personal}} > {{int:email}}.\n\nIt appears after saving your email address but before you confirm it.",
        "noemailprefs": "Message appearing in the \"Email options\" section of the \"User profile\" page in [[Special:Preferences|Preferences]], when no user email address has been entered.",
        "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",
        "group-bot": "{{doc-group|bot}}\n{{Identical|Bot}}",
        "group-sysop": "{{doc-group|sysop}}\n{{Identical|Administrator}}",
        "group-bureaucrat": "{{doc-group|bureaucrat}}",
-       "group-suppress": "{{doc-group|suppress}}\nThis is an optional (disabled by default) user group, meant for the [[mw:RevisionDelete|RevisionDelete]] feature, to change the visibility of revisions through [[Special:RevisionDelete]].\n\n{{Identical|Suppress}}",
+       "group-suppress": "{{doc-group|suppress}}\nThis is an optional (disabled by default) user group, meant for the suppression feature in [[mw:Flow|Flow]]. It is not to be confused with the Oversighters group, which also has access to the [[mw:RevisionDelete|RevisionDelete]] feature, to change the visibility of revisions through [[Special:RevisionDelete]].\n\n{{Identical|Suppress}}",
        "group-all": "The name of the user group that contains all users, including anonymous users\n\n{{Identical|All}}",
        "group-user-member": "{{doc-group|user|member}}\n{{Identical|User}}",
        "group-autoconfirmed-member": "{{doc-group|autoconfirmed|member}}",
        "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}}",
        "apisandbox-results-fixtoken-fail": "Displayed as an error message from JavaScript when a CSRF token could not be fetched.\n\nParameters:\n* $1 - Token type",
        "apisandbox-alert-page": "Tooltip for the alert icon on a module's page tab when the page contains fields with issues.",
        "apisandbox-alert-field": "Tooltip for the alert icon on a field when the field has issues.",
+       "apisandbox-continue": "Button text for sending another request using query continuation.\n{{Identical|Continue}}",
+       "apisandbox-continue-clear": "Button text for clearing query continuation parameters.\n{{Identical|Clear}}",
+       "apisandbox-continue-help": "Help text for the continue and clear buttons.",
        "booksources": "{{doc-special|BookSources}}\n\n'''This message shouldn't be changed unless it has serious mistakes.'''\n\nIt's used as the page name of the configuration page of [[Special:BookSources]]. Changing it breaks existing sites using the default version of this message.\n\nSee also:\n* {{msg-mw|Booksources|title}}\n* {{msg-mw|Booksources-text|text}}",
        "booksources-summary": "{{doc-specialpagesummary|booksources}}",
        "booksources-search-legend": "Box heading on [[Special:BookSources|book sources]] special page. The box is for searching for places where a particular book can be bought or viewed.",
        "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.",
        "htmlform-user-not-exists": "Error message shown if a user with the name provided by the user does not exist. $1 is the username.",
        "htmlform-user-not-valid": "Error message shown if the name provided by the user isn't a valid username. $1 is the username.",
        "rawmessage": "{{notranslate}} Used to pass arbitrary text as a message specifier array",
-       "sqlite-has-fts": "Shown on [[Special:Version]].\nParameters:\n* $1 - version",
-       "sqlite-no-fts": "Shown on [[Special:Version]].\nParameters:\n* $1 - version",
        "logentry-delete-delete": "{{Logentry|[[Special:Log/delete]]}}",
        "logentry-delete-restore": "{{Logentry|[[Special:Log/delete]]}}",
        "logentry-delete-event": "{{Logentry|[[Special:Log/delete]]}}\n{{Logentryparam}}\n* $5 - count of affected log events",
        "feedback-external-bug-report-button": "A button for submitting an external technical bug report.",
        "feedback-dialog-title": "Title of the feedback dialog",
        "feedback-dialog-intro": "An introduction at the top of the feedback dialog. $1 - Feedback page link",
-       "feedback-error-title": "{{Identical|Error}}",
        "feedback-error1": "Error message, appears when an unknown error occurs submitting feedback",
        "feedback-error2": "Error message, appears when we could not add feedback",
        "feedback-error3": "Error message, appears when we lose our connection to the wiki",
        "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 2a2e31c..52111d3 100644 (file)
@@ -12,7 +12,8 @@
                        "아라",
                        "Macofe",
                        "Matma Rex",
-                       "Translaziuns"
+                       "Translaziuns",
+                       "Terfili"
                ]
        },
        "tog-underline": "Suttastritgar colliaziuns:",
        "otherlanguages": "En autras linguas",
        "redirectedfrom": "(renvià da $1)",
        "redirectpagesub": "questa pagina renviescha tar in'auter artitgel",
+       "redirectto": "Renviescha a:",
        "lastmodifiedat": "Questa pagina è vegnida modifitgada l'ultima giada ils $1 a las $2.",
        "viewcount": "Questa pagina è vegnida contemplada {{PLURAL:$1|ina giada|$1 giadas}}.",
        "protectedpage": "Pagina protegida",
        "nstab-template": "Model",
        "nstab-help": "Agid",
        "nstab-category": "Categoria",
+       "mainpage-nstab": "Pagina principala",
        "nosuchaction": "Talas acziuns n'existan betg",
        "nosuchactiontext": "L'acziun specifitgada per questa URL è faussa.\nTi has endatà fauss la URL, u es suandà in link incorrect.\nI po dentant er esser ina errur en la software da {{SITENAME}}.",
        "nosuchspecialpage": "I n'exista betg ina tala pagina speziala",
        "welcomeuser": "Bainvegni, $1!",
        "welcomecreation-msg": "Tes conto è vegnì creà. \nN'emblida betg da midar tias [[Special:Preferences|{{SITENAME}} preferenzas]].",
        "yourname": "Num d'utilisader",
+       "userlogin-yourname": "Num d'utilisader",
        "userlogin-yourname-ph": "Endatescha tes num d'utilisader",
        "createacct-another-username-ph": "Endatescha in num d'utilisader",
        "yourpassword": "pled-clav",
        "yourpasswordagain": "repeter pled-clav",
        "createacct-yourpasswordagain": "Confermar il pled-clav",
        "createacct-yourpasswordagain-ph": "Endatescha il pled-clav anc ina giada",
-       "remembermypassword": "S'annunziar permanantamain sin quest computer (per maximalmain $1 {{PLURAL:$1|di|dis}})",
        "userlogin-remembermypassword": "Restar annunzià",
        "userlogin-signwithsecure": "Duvrar ina connexiun segira",
        "yourdomainname": "Vossa domain",
        "pt-login": "T'annunziar",
        "pt-login-button": "T'annunziar",
        "pt-createaccount": "Crear in conto d'utilisader",
+       "pt-userlogout": "Sortir",
        "php-mail-error-unknown": "Errur nunenconuschenta en la funcziun mail() da PHP",
        "user-mail-no-addy": "Empruvà da trametter in e-mail senza ina adressa dad e-mail.",
        "changepassword": "Midar pled-clav",
        "passwordreset-emailtext-user": "L'utilisader $1 sin {{SITENAME}} ha dumandà da redefinir il pled-clav per {{SITENAME}} ($4). \n{{PLURAL:$3|Il suandant conto d'utilisader è collià|Ils suandants contos d'utilisader èn colliads}} cun questa adressa dad e-mail:\n\n$2\n\n{{PLURAL:$3|Quest pled-clav temporar|Quests pled-clav temporars}} èn valids {{PLURAL:$5|in di|$5 dis}}.\nTi duessas t'annunziar ussa e tscherner in nov pled-clav. Sche ti na levas betg quests novs pleds-clav u sche ti ta regordas puspè da tes pled-clav original e na vuls betg pli midar il pled-clav pos ti ignorar quest messadi e cuntinuar dad utilisar tes pled-clav original.",
        "passwordreset-emailelement": "Num d'utilisader: \n$1\n\nPled-clav temporar: \n$2",
        "passwordreset-emailsentemail": "In e-mail per redefinir il pled-clav è vegnì tramess.",
-       "passwordreset-emailsent-capture": "In e-mail (sco mussà sutvart) per redefinir il pled-clav è vegnì tramess.",
-       "passwordreset-emailerror-capture": "In e-mail (sco mussà sutvart) per redefinir il pled-clav è vegnì generà ma n'ha betg pudì envià a l'{{GENDER:$2|utilisader|utilisadra}}: $1",
        "changeemail": "Midar l'adressa dad e-mail",
        "changeemail-header": "Midar l'adressa dad e-mail dal conto",
        "changeemail-no-info": "Ti stos t'annunziar per acceder directamain questa pagina.",
        "newarticle": "(Nov)",
        "newarticletext": "Ti has cliccà ina colliaziun ad ina pagina che n'exista anc betg. Per crear ina pagina, entschaiva a tippar en la stgaffa sutvart (guarda [$1 la pagina d'agid] per t'infurmar).",
        "anontalkpagetext": "----''Quai è la pagina da discussiun per in utilisader anomim che n'ha anc betg creà in conto d'utilisader u che n'al utilisescha betg.\nPerquai avain nus d'utilisar l'adressa dad IP per l'identifitgar.\nIna tala adressa dad IP po vegnir utilisada da differents utilisaders.\nSche ti es in utilisaders anonim e pensas che commentaris che na pertutgan betg tai vegnan adressads a tai, lura [[Special:CreateAccount|creescha in conto]] u [[Special:UserLogin|t'annunzia]] per evitar en futur che ti vegns sbaglià cun auters utilisaders.''",
-       "noarticletext": "Quest artitgel na cuntegna actualmain nagin text.\nTi pos [[Special:Search/{{PAGENAME}}|tschertgar il term]] sin in'autra pagina,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tschertgar en ils protocols],\nu [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear questa pagina]</span>.",
+       "noarticletext": "Questa pagina na cuntegna actualmain nagin text.\nTi pos [[Special:Search/{{PAGENAME}}|tschertgar il term]] sin in'autra pagina,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tschertgar en ils protocols],\nu [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear questa pagina]</span>.",
        "noarticletext-nopermission": "Questa pagina na cuntegna actualmain nagin text.\nTi pos [[Special:Search/{{PAGENAME}}|tschertgar quest titel]] en autras paginas u <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} tschertgar en ils protocols correspundents]</span>, ma ti n'has betg ils dretgs da crear questa pagina.",
        "missing-revision": "La versiun #$1 da la pagina cun il num \"{{FULLPAGENAME}}\" n'exista betg.\n\nQuai capita savnes sche ti cliccas sin ina colliaziun antiquada en la cronologia per ina pagina ch'è vegnida stizzada.\nDetagls pon vegnri chattads en il [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} protocol da stizzar].",
        "userpage-userdoesnotexist": "Il conto d'utilisader \"<nowiki>$1</nowiki>\" n'èxista betg.\nControllescha sch ti vuls propi crear/modiftgar questa pagina.",
        "undo-failure": "La modificaziun na pudeva betg vegnir revocada causa modificaziuns pli novas che stattan en conflict cun questa acziun.",
        "undo-norev": "La modificaziun na pudeva betg vegnir revocada perquai ch'ella n'exista betg u è vegnida stizzada.",
        "undo-summary": "Revocar la versiun $1 da [[Special:Contributions/$2|$2]] ([[User talk:$2|discussiun]])",
-       "cantcreateaccounttitle": "Betg pussaivel da crear il conto",
        "cantcreateaccount-text": "La creaziun da contos du'utilisader è vegnida bloccada da l'utilisader [[User:$3|$3]] per questa adressa IP ('''$1''').\n\nIl motiv inditgà da $3 è ''$2''",
        "viewpagelogs": "Guardar ils protocols da questa pagina",
        "nohistory": "Per questa pagina n'exista nagina cronologia.",
        "action-siteadmin": "bloccar u debloccar la banca da datas",
        "action-sendemail": "trametter e-mails",
        "nchanges": "$1 {{PLURAL:$1|midada|midadas}}",
+       "enhancedrc-history": "Cronologia",
        "recentchanges": "Ultimas midadas",
        "recentchanges-legend": "Opziuns per las ultimas midadas",
        "recentchanges-summary": "Sin questa pagina pos ti suandar las ultimas midadas sin '''{{SITENAME}}'''.",
        "querypage-disabled": "Questa pagina speciala è deactivada ord motivs da prestaziun.",
        "booksources": "Tschertga da ISBN",
        "booksources-search-legend": "Tschertgar pussaivladad da cumpra per cudeschs",
+       "booksources-search": "Tschertgar",
        "booksources-text": "Sutvart è ina glista da las colliaziuns ad autras paginas che vendan cudeschs novs ed utilisads e che pudessan avair dapli infurmaziuns davart ils cudeschs che ti tschertgas:",
        "booksources-invalid-isbn": "Il numer ISBN na para betg dad esser valid; controllescha che ti n'has betg fatg errurs cun la scriver.",
        "specialloguserlabel": "Acziun exequida da:",
        "contributions": "Contribuziuns {{GENDER:$1|da l'utilisader|da l'utilisadra}}",
        "contributions-title": "Contribuziuns d'utilisader da $1",
        "mycontris": "Contribuziuns",
+       "anoncontribs": "Contribuziuns",
        "contribsub2": "Per {{GENDER:$3|$1}} ($2)",
        "nocontribs": "Chattà naginas modificaziuns che correspundan a quests criteris.",
        "uctop": "(actual)",
        "import-logentry-interwiki-detail": "{{PLURAL:$1|Ina versiun|$1 versiuns}} da $2",
        "javascripttest": "Test da JavaScript",
        "javascripttest-qunit-intro": "Legia la [$1 documentaziun da tests] sin mediawiki.org.",
-       "tooltip-pt-userpage": "Mussar tia pagina d'utilisader",
+       "tooltip-pt-userpage": "Mussar {{GENDER:|tia pagina d'utilisader}}",
        "tooltip-pt-anonuserpage": "La pagina d'utilisader per l'adressa IP cun la quala che ti fas modificaziuns",
-       "tooltip-pt-mytalk": "Mussar tia pagina da discussiun",
+       "tooltip-pt-mytalk": "Mussar {{GENDER:|tia}} pagina da discussiun",
        "tooltip-pt-anontalk": "Discussiun davart modificaziuns che derivan da questa adressa dad IP",
        "tooltip-pt-preferences": "mias preferenzas",
        "tooltip-pt-watchlist": "La glista da las paginas da las qualas jau observ las midadas",
        "tooltip-pt-login": "I fiss bun sche ti s'annunziassas, ti na stos dentant betg.",
        "tooltip-pt-logout": "Sortir",
        "tooltip-ca-talk": "Discussiuns davart il cuntegn da l'artitgel",
-       "tooltip-ca-edit": "Ti pos modifitgar questa pagina.\nUtilisescha per plaschair il buttun 'mussar prevista' avant che memorisar.",
+       "tooltip-ca-edit": "Modifitgar questa pagina",
        "tooltip-ca-addsection": "Cumenzar nov paragraf",
        "tooltip-ca-viewsource": "Questa pagina è protegida.\nTi pos vesair il code-fundamental.",
        "tooltip-ca-history": "Versiuns pli veglias da questa pagina",
        "tooltip-t-recentchangeslinked": "Ultimas midadas sin paginas colliadas cun questa pagina",
        "tooltip-feed-rss": "RSS feed per questa pagina",
        "tooltip-feed-atom": "Atom feed per questa pagina",
-       "tooltip-t-contributions": "Mussar las contribuziuns da quest utilisader",
+       "tooltip-t-contributions": "Mussar las contribuziuns da {{GENDER:$1|quest utilisader}}",
        "tooltip-t-emailuser": "Trametter in e-mail a quest utilisader",
        "tooltip-t-upload": "Chargiar si datotecas",
        "tooltip-t-specialpages": "Glista da tut las paginas spezialas",
        "htmlform-submit": "Trametter",
        "htmlform-reset": "Revocar las midadas",
        "htmlform-selectorother-other": "Auters",
-       "sqlite-has-fts": "$1 cun sustegn per la retschertga da text integrala",
-       "sqlite-no-fts": "$1 senza sustegn per la retschertga da text integrala",
        "logentry-delete-delete": "$1 {{GENDER:$2|ha stizzà}} la pagina $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|ha restaurà}} la pagina $3",
        "logentry-delete-event": "$1 ha midà la visibilitad da{{PLURAL:$5|d ina occurrenza en il protocol| $5 occurrenzas en il protocol}} da '''$3''': $4",
        "revdelete-uname-unhid": "dà liber il num d'utilisader",
        "revdelete-restricted": "applitgà restricziuns per administraturs",
        "revdelete-unrestricted": "allontanà restricziuns per administraturs",
-       "logentry-move-move": "$1 ha spustà la pagina $3 a $4",
+       "logentry-move-move": "$1 {{GENDER:$2|ha spustà}} la pagina $3 a $4",
        "logentry-move-move-noredirect": "$1 ha spustà la pagina $3 a $4 senza crear in renviament",
        "logentry-move-move_redir": "$1 ha spustà la pagina $3 a $4 e surscrit quatras in renviament",
        "logentry-move-move_redir-noredirect": "$1 ha spustà la pagina $3 a $4 e surscrit quatras in renviament senza crear in renviament",
        "logentry-patrol-patrol": "$1 ha marcà la versiun $4 da la pagina $3 sco controllada",
        "logentry-patrol-patrol-auto": "$1 ha marcà automaticamain la versiun $4 da la pagina $3 sco controllada",
        "logentry-newusers-newusers": "Il conto $1 è vegnì creà",
-       "logentry-newusers-create": "Il conto $1 è vegnì creà",
+       "logentry-newusers-create": "Il conto $1 è vegnì {{GENDER:$2|creà}}",
        "logentry-newusers-create2": "Il conto $3 è vegnì creà da $1",
        "logentry-newusers-autocreate": "Il conto $1 è vegnì creà automaticamain",
        "logentry-rights-rights": "$1 ha midà la commembranza da gruppas per $3 da $4 a $5",
index cc77f46..98f7e64 100644 (file)
@@ -58,7 +58,7 @@
        "tog-enotifminoredits": "Trimite-mi, de asemenea, un e-mail în caz de modificări minore asupra paginilor și fișierelor",
        "tog-enotifrevealaddr": "Afișează-mi adresa de e-mail în mesajele de notificare",
        "tog-shownumberswatching": "Arată numărul utilizatorilor care urmăresc",
-       "tog-oldsig": "Semnătură actuală:",
+       "tog-oldsig": "Semnătura actuală:",
        "tog-fancysig": "Tratează semnătura ca wikitext (fără o legătură automată)",
        "tog-uselivepreview": "Folosește previzualizarea în timp real",
        "tog-forceeditsummary": "Avertizează-mă când uit să descriu modificările",
        "newwindow": "(se deschide într-o fereastră nouă)",
        "cancel": "Revocare",
        "moredotdotdot": "Mai mult…",
-       "morenotlisted": "Această listă nu este completă.",
+       "morenotlisted": "Această listă ar putea fi incompletă.",
        "mypage": "Pagină",
        "mytalk": "Discuții",
        "anontalk": "Discuții",
        "talk": "Discuție",
        "views": "Vizualizări",
        "toolbox": "Unelte",
+       "tool-link-userrights": "Schimbă grupurile {{GENDER:$1|utilizatorului|utilizatoarei}}",
+       "tool-link-emailuser": "Trimiteți un mesaj {{GENDER:$1|acestui utilizator|acestei utilizatoare}}",
        "userpage": "Vizualizați pagina utilizatorului",
        "projectpage": "Vizualizați pagina proiectului",
        "imagepage": "Vizualizați pagina fișierului",
        "perfcachedts": "Informațiile de mai jos provin din cache, ultima actualizare efectuându-se la $1. Un maxim de {{PLURAL:$4|un rezultat este disponibil|$4 rezultate sunt disponibile}} în cache.",
        "querypage-no-updates": "Actualizările acestei pagini sunt momentan dezactivate. Informațiile de aici nu sunt împrospătate.",
        "viewsource": "Sursă pagină",
-       "viewsource-title": "Vizualizare sursă pentru $1",
+       "viewsource-title": "Vizualizare sursă pentru „$1”",
        "actionthrottled": "Acțiune limitată",
        "actionthrottledtext": "Ca o măsură anti-spam, aveți permisiuni limitate în a efectua această acțiune de prea multe ori într-o perioadă scurtă de timp, iar dumneavoastră tocmai ați depășit această limită.\nVă rugăm să încercați din nou în câteva minute.",
        "protectedpagetext": "Această pagină este protejată împotriva modificărilor sau a altor acțiuni.",
        "botpasswords-disabled": "Parolele de roboți sunt dezactivate.",
        "botpasswords-no-central-id": "Pentru a folosi parole pentru roboți, trebuie să fiți logat într-un cont centralizat.",
        "botpasswords-existing": "Parole de robot existente",
+       "botpasswords-createnew": "Creați o nouă parolă de bot",
+       "botpasswords-editexisting": "Editați o parolă de bot",
        "botpasswords-label-appid": "Numele robotului:",
        "botpasswords-label-create": "Creare",
        "botpasswords-label-update": "Actualizează",
        "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.",
        "expensive-parserfunction-warning": "Atenție: Această pagină conține prea multe apelări costisitoare ale funcțiilor parser.\n\nAr trebui să existe mai puțin de $2 {{PLURAL:$2|apelare|apelări}}, acolo există {{PLURAL:$1|$1 apelare|$1 apelări}}.",
        "expensive-parserfunction-category": "Pagini cu prea multe apelări costisitoare de funcții parser",
        "post-expand-template-inclusion-warning": "Atenție: Formatele incluse sunt prea mari.\nUnele formate nu vor fi incluse.",
-       "post-expand-template-inclusion-category": "Paginile în care este inclus formatul are o dimensiune prea mare",
+       "post-expand-template-inclusion-category": "Pagini în care formatele incluse au o dimensiune prea mare",
        "post-expand-template-argument-warning": "Atenție: Această pagină conține cel puțin un argument al unui format care are o mărime prea mare atunci când este expandat.\nAcsete argumente au fost omise.",
        "post-expand-template-argument-category": "Pagini care conțin formate cu argumente omise",
        "parser-template-loop-warning": "Buclă de formate detectată: [[$1]]",
        "grant-generic": "set de permisiuni „$1”",
        "grant-group-page-interaction": "Interacționează cu paginile",
        "grant-group-file-interaction": "Interacționează cu conținut media",
+       "grant-highvolume": "Volum mare de editare",
+       "grant-oversight": "Ascunde utilizatori și suprimă versiuni",
+       "grant-patrol": "Patrulează schimbările paginilor",
        "grant-basic": "Drepturi de bază",
        "newuserlogpage": "Jurnal utilizatori noi",
        "newuserlogpagetext": "Acesta este jurnalul creărilor conturilor de utilizator.",
        "uploadstash-badtoken": "Execuția acestei acțiuni nu a reușit, probabil deoarece informațiile dumneavoastră de identificare au expirat. Încercați din nou.",
        "uploadstash-errclear": "Golirea fișierelor nu a reușit.",
        "uploadstash-refresh": "Reîmprospătează lista de fișiere",
+       "uploadstash-thumbnail": "arată miniatura",
+       "uploadstash-exception": "Nu pot stoca încărcare în spațiul temporar ($1): \"$2\"",
        "invalid-chunk-offset": "Decalaj de segment nevalid",
        "img-auth-accessdenied": "Acces interzis",
        "img-auth-nopathinfo": "PATH_INFO lipsește.\nServerul dumneavoastră nu a fost setat pentru a trece aceste informații.\nS-ar putea să fie bazat pe CGI și să nu suporte img_auth.\nVedeți https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "delete-toobig": "Această pagină are un istoric al modificărilor important, cu mai mult de $1 {{PLURAL:$1|versiune|versiuni|de versiuni}}.\nȘtergerea unei astfel de pagini a fost restricționată pentru a preveni apariția unor erori în {{SITENAME}}.",
        "delete-warning-toobig": "Această pagină are un istoric al modificărilor mult prea mare, cu mai mult de $1 {{PLURAL:$1|versiune|versiuni|de versiuni}}.\nȘtergerea sa poate afecta baza de date a sitului {{SITENAME}};\nacționați cu precauție.",
        "deleteprotected": "Nu puteți șterge această pagină, deoarece este protejată.",
-       "deleting-backlinks-warning": "'''Atenție:''' [[Special:WhatLinksHere/{{FULLPAGENAME}}|Alte pagini]] se leagă sau transclud pagina pe care doriți să o ștergeți.",
+       "deleting-backlinks-warning": "<strong>Atenție:</strong> [[Special:WhatLinksHere/{{FULLPAGENAME}}|Alte pagini]] se leagă sau transclud pagina pe care doriți să o ștergeți.",
        "rollback": "Editări de revenire",
        "rollbacklink": "revenire",
        "rollbacklinkcount": "revenire asupra {{PLURAL:$1|unei modificări|a $1 modificări|a $1 de modificări}}",
        "htmlform-title-not-exists": "$1 nu există.",
        "htmlform-user-not-exists": "<strong>$1</strong> nu există.",
        "htmlform-user-not-valid": "<strong>$1</strong> nu este un nume de utilizator valid.",
-       "sqlite-has-fts": "$1 cu suport de căutare în tot textul",
-       "sqlite-no-fts": "$1 fără suport de căutare în tot textul",
        "logentry-delete-delete": "$1 {{GENDER:$2|a șters}} pagina $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|a restaurat}} pagina $3",
        "logentry-delete-event": "$1 {{GENDER:$2|a schimbat}} vizibilitatea {{PLURAL:$5|unui eveniment din jurnal|a $5 evenimente din jurnal|a $5 de evenimente din jurnal}} pentru $3: $4",
        "feedback-external-bug-report-button": "Semnalare problemă tehnică",
        "feedback-dialog-title": "Trimitere păreri",
        "feedback-dialog-intro": "Puteți folosi formularul simplificat de mai jos pentru a vă trimite părerile. Comentariul dumneavoastră va fi adăugat în pagina „$1”, alături de numele dumneavoastră de utilizator.",
-       "feedback-error-title": "Eroare",
        "feedback-error1": "Eroare: Rezultat necunoscut de la API",
        "feedback-error2": "Eroare: editarea nu a reușit",
        "feedback-error3": "Eroare: Niciun răspuns de la API",
        "log-action-filter-rights-rights": "Modificare manuală",
        "log-action-filter-rights-autopromote": "Schimbare automată",
        "log-action-filter-upload-upload": "Încărcare nouă",
-       "log-action-filter-upload-overwrite": "Reîncărcare"
+       "log-action-filter-upload-overwrite": "Reîncărcare",
+       "linkaccounts-submit": "Leagă conturile",
+       "unlinkaccounts": "Dezleagă conturile",
+       "unlinkaccounts-success": "Contul a fost dezlegat"
 }
index 7e6289d..23c7739 100644 (file)
        "tog-enotifminoredits": "Уведомлять даже при незначительных изменениях страниц и файлов",
        "tog-enotifrevealaddr": "Показывать мой почтовый адрес в сообщениях оповещения",
        "tog-shownumberswatching": "Показывать число участников, включивших страницу в свой список наблюдения",
-       "tog-oldsig": "Текущая подпись:",
+       "tog-oldsig": "Ð\92аÑ\88а Ñ\82екущая подпись:",
        "tog-fancysig": "Собственная вики-разметка подписи (без автоматической ссылки)",
        "tog-uselivepreview": "Использовать быстрый предварительный просмотр",
        "tog-forceeditsummary": "Предупреждать, когда не заполнено поле описания правки",
        "newwindow": "&nbsp;(в новом окне)",
        "cancel": "Отменить",
        "moredotdotdot": "Далее…",
-       "morenotlisted": "ЭÑ\82оÑ\82 Ñ\81пиÑ\81ок Ð½ÐµÐ¿Ð¾Ð»Ð¾Ð½.",
+       "morenotlisted": "ЭÑ\82оÑ\82 Ñ\81пиÑ\81ок Ð¼Ð¾Ð¶ÐµÑ\82 Ð±Ñ\8bÑ\82Ñ\8c Ð½ÐµÐ¿Ð¾Ð»Ð½Ñ\8bм.",
        "mypage": "Страница",
        "mytalk": "Обсуждение",
        "anontalk": "Обсуждение",
        "talk": "Обсуждение",
        "views": "Просмотры",
        "toolbox": "Инструменты",
+       "tool-link-userrights": "Изменить группы {{GENDER:$1|участника|участницы}}",
+       "tool-link-emailuser": "Написать письмо {{GENDER:$1|участнику|участнице}}",
        "userpage": "Просмотреть страницу участника",
        "projectpage": "Просмотреть страницу проекта",
        "imagepage": "Просмотреть страницу файла",
        "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": "Сброс пароля не был обработан. Может быть, не был настроен ни один провайдер?",
        "upload-dialog-disabled": "На этом вики-сайте отключена возможность загрузки файлов с помощью этого диалогового окна.",
        "upload-dialog-title": "Загрузить файл",
        "upload-dialog-button-cancel": "Отменить",
+       "upload-dialog-button-back": "Назад",
        "upload-dialog-button-done": "Готово",
        "upload-dialog-button-save": "Сохранить",
        "upload-dialog-button-upload": "Загрузить",
        "apisandbox-results-fixtoken-fail": "Не удалось вызвать токен «$1».",
        "apisandbox-alert-page": "Поля на этой странице некорректны.",
        "apisandbox-alert-field": "Значение этого поля является недопустимым.",
+       "apisandbox-continue": "Продолжить",
+       "apisandbox-continue-clear": "Очистить",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} [https://www.mediawiki.org/wiki/API:Query#Continuing_queries продолжит] последний запрос; {{int:apisandbox-continue-clear}} очистит связанные с продолжением параметры.",
        "booksources": "Источники книг",
        "booksources-search-legend": "Поиск информации о книге",
        "booksources-isbn": "ISBN:",
        "htmlform-cloner-create": "Добавить ещё",
        "htmlform-cloner-delete": "Удалить",
        "htmlform-cloner-required": "Требуется по крайней мере одно значение.",
+       "htmlform-date-placeholder": "ГГГГ-ММ-ДД",
+       "htmlform-time-placeholder": "ЧЧ:ММ:СС",
+       "htmlform-datetime-placeholder": "ГГГГ-ММ-ДД ЧЧ:ММ:СС",
+       "htmlform-date-invalid": "Указанное вами значение не похоже на дату. Попробуйте использовать формат ГГГГ-ММ-ДД.",
+       "htmlform-time-invalid": "Указанное вами значение не похоже на время. Попробуйте использовать формат ЧЧ-ММ-СС.",
+       "htmlform-datetime-invalid": "Указанное вами значение не похоже на дату и время. Попробуйте использовать формат ГГГГ-ММ-ДД ЧЧ-ММ-СС.",
+       "htmlform-date-toolow": "Указанное вами значение меньше самой ранней разрешённой даты — $1.",
+       "htmlform-date-toohigh": "Указанное вами значение больше самой поздней разрешённой даты — $1.",
+       "htmlform-time-toolow": "Указанное вами значение меньше самого раннего разрешённого времени — $1.",
+       "htmlform-time-toohigh": "Указанное вами значение больше самого позднего разрешённого времени — $1.",
+       "htmlform-datetime-toolow": "Указанное вами значение меньше самым ранних разрешённых даты и времени — $1.",
+       "htmlform-datetime-toohigh": "Указанное вами значение больше самых поздних разрешённых даты и времени — $1.",
        "htmlform-title-badnamespace": "[[:$1]] находится не в пространстве имён «{{ns:$2}}».",
        "htmlform-title-not-creatable": "«$1» — заголовок страницы, которая не может быть создана",
        "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 {{GENDER:$2|удалил|удалила}} страницу $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|восстановил|восстановила}} страницу $3",
        "logentry-delete-event": "$1 {{GENDER:$2|изменил|изменила}} видимость {{PLURAL:$5|$5 записи|$5 записей|1=записи}} журнала для $3: $4",
        "feedback-external-bug-report-button": "Отправить техническое задание",
        "feedback-dialog-title": "Отправить отзыв",
        "feedback-dialog-intro": "Вы можете воспользоваться простой формой ниже, чтобы оставить свой отзыв. Комментарий с вашим именем участника будет добавлен на страницу «$1».",
-       "feedback-error-title": "Ошибка",
        "feedback-error1": "Ошибка. Неизвестный результат из API",
        "feedback-error2": "Ошибка. Сбой редактирования",
        "feedback-error3": "Ошибка. Нет ответа от API",
        "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 8f664fc..9591937 100644 (file)
        "minoredit": "इदं लघु सम्पादनम्",
        "watchthis": "इदं पृष्ठं निरीक्षताम्",
        "savearticle": "पृष्ठं रक्ष्यताम्",
+       "savechanges": "परिवर्तनानि रक्ष्यन्ताम्",
        "publishpage": "पृष्ठं प्रकाश्यताम्",
        "publishchanges": "परिवर्तनानि प्रकाश्यन्ताम्",
        "preview": "प्राग्दृश्यम्",
        "htmlform-cloner-create": "अधिकं योज्यताम्",
        "htmlform-cloner-delete": "निष्कास्यताम्",
        "htmlform-cloner-required": "न्यूनातिन्यूनम् एकं मूल्यम् अपेक्ष्यते ।",
-       "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": "$3: $4 इत्यत्र {{PLURAL:$5|संरक्षिताऽऽवलेः घटनायाः|$5 संरक्षिताऽऽवलीनां घटनानां}} दर्शनीयता $1 द्वारा {{GENDER:$2|परिवर्तिता}}",
index 4dca72d..598e10d 100644 (file)
@@ -15,7 +15,8 @@
                        "Macofe",
                        "Matma Rex",
                        "Мария Олесова",
-                       "Ай-Куо"
+                       "Ай-Куо",
+                       "Туллук"
                ]
        },
        "tog-underline": "Сигэлэри аннынан тардыы:",
@@ -42,7 +43,7 @@
        "tog-enotifminoredits": "Кыра да уларытыы киирдэҕинэ эл. почтанан биллэрээр",
        "tog-enotifrevealaddr": "Мин почтам аадырыһын биллэриилэргэ көрдөр",
        "tog-shownumberswatching": "Сирэйи кэтээн көрөр дьон ахсаанын көрдөр",
-       "tog-oldsig": "Билиҥҥи илии баттааһын:",
+       "tog-oldsig": "Билигин туттар илии баттааһынын",
        "tog-fancysig": "Бэйэ илии баттааһына (сигэтэ суох)",
        "tog-uselivepreview": "Хайдах буолуохтааҕын тутатына эрдэ көрүүнү туттуу",
        "tog-forceeditsummary": "Тугу уларыппытым туһунан суруйбатахпына сэрэт",
        "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» диэн ааттаах оруобаты эбэр табыллыбата. Баҕар хайыы-үйэ эбиллибитэ буолаарай?",
        "tag-filter": "[[Special:Tags|Бэлиэлэр]] фильтрдара:",
        "tag-filter-submit": "Фильтр",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Тиэк|Тиэктэр}}]]: $2)",
+       "tag-mw-contentmodelchange": "Иһинээҕи киэбин уларытыы сурунаала",
        "tags-title": "Бэлиэлэр (тиэктэр)",
        "tags-intro": "Бу сирэйгэ бырагыраамма уларытыылары бэлиэтиир анал бэлиэлэрин (тиэктэрин) тиһиктэрэ уонна ол бэлиэлэр суолталара көстөр.",
        "tags-tag": "Бэлиэ (тиэк) аата",
        "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": "$3 сирэйи $1 соппут",
        "logentry-delete-restore": "$3 сирэйи $1 сөргүппүт",
        "logentry-delete-event": "Сурунаал $5 суругун көстүүтүн манна $3 $1 уларыппыт: $4",
index 15d45cf..bf9746d 100644 (file)
@@ -31,7 +31,7 @@
        "tog-enotifminoredits": "صفحن ۾ معمولي ترميمن جي صورت ۾ بہ مون کي برق ٽپال ڪريو",
        "tog-enotifrevealaddr": "پڌراين ۾ منهنجو برق ٽپال پتو ظاهر ڪريو.",
        "tog-shownumberswatching": "ڏسندڙ يوزرس جو انگ ڏيکاريو",
-       "tog-oldsig": "موجوده دستخط",
+       "tog-oldsig": "توھان جو موجوده دستخط:",
        "tog-uselivepreview": "سڌي سنئين پيش نگاھہ استعمال ڪريو",
        "tog-watchlisthideown": "زير نظر فهرست مان منهنجون ڪيل ترميمون لڪايو",
        "tog-watchlisthidebots": "ٽيٽ فهرست تان بوٽ جون ترميمون لڪايو",
@@ -43,7 +43,7 @@
        "tog-diffonly": "تفاوت هيٺان صفحي جو مواد نہ ڏيکاريو",
        "tog-showhiddencats": "لڪل زمرا ڏيکاريو",
        "tog-useeditwarning": "مونکي خبردار ڪريو جڏهن مان هڪ ترميم وارو صفحو بغير تبديلين سانڍڻ جي ڇڏيان",
-       "tog-prefershttps": "هميشه محفوظ ڪنيڪشن استعمال ڪريو جڏهن لاگ اِن ٿيل هجو",
+       "tog-prefershttps": "هميشہ محفوظ ڪنيڪشن استعمال ڪريو جڏهن داخل ٿيل هجو",
        "underline-always": "هميشہ",
        "underline-never": "ڪڏهن بہ نہ",
        "editfont-style": "ايراضي جو فونٽ اسٽائيل سنواريو:",
        "category-file-count-limited": "هيٺيون يا هيٺيان {{PLURAL:$1|فائيل آهي|$1 فائيل آهن}} هن تازي زمري ۾.",
        "listingcontinuesabbrev": "جاري..",
        "index-category": "ڏسڻيل صفحا",
-       "noindex-category": "غيرڏسڻيل صفحا",
+       "noindex-category": "غير-ڏسڻيل صفحا",
        "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": "محفوظ ڳانڍاپو استعمال ڪريو",
        "cannotloginnow-title": "ھاڻي لاگ ان نٿو ڪري سگھجي",
        "accountcreatedtext": "يوزر کاتو [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|talk]]) جي لاءِ تخليق ٿي چڪو آهي.",
        "createaccount-title": "{{SITENAME}} تي کاتو کولڻ",
        "login-throttled": "توهان تازو ئي لاگ اِن ٿيڻ جون هيڪانديون گھڻيون ڪوششون ڪيون آهن. مهرباني ڪري $1 لاءِ ترسي پوءِ وري ڪوشش ڪريو.",
-       "login-abort-generic": "توهان جو لاگ اِن ناڪام ويو.",
+       "login-abort-generic": "توهان جو داخل ٿيڻ ناڪام ويو - بند ڪيل",
        "login-migrated-generic": "توهان جو کاتو لڏي چڪو آهي، ۽ هن وڪيءَ تي توهان جو يُوزنانءُ هاڻي وجود نہ ٿو رکي.",
        "loginlanguagelabel": "ٻولي: $1",
        "createacct-another-realname-tip": "اصل نالو ڄاڻائڻ اختياري آهي. جيڪڏهن توهان اصل نالو ڄاڻايو ٿا، تہ اهو توهان کي توهان جي ڪم جي مڃتا ڏيڻ لاءِ ڪم آندو ويندو.",
        "passwordreset-emailtitle": "{{SITENAME}} واري کاتي جا تفصيل",
        "passwordreset-emailelement": "يُوزر نانءُ: \n$1\n\nعارضي ڳجھو لفظ:\n$2",
        "changeemail": "برق ٽپال پتو مِٽايو يا بدلايو",
-       "changeemail-passwordrequired": "توهانکي هن تبديلي جي تصديق ڪرڻ جي لاءِ پنهنجو ڳجھو لفظ داخل ڪرڻ جي ضرورت پوندي.",
        "changeemail-oldemail": "هاڻوڪو برق ٽپال پتو:",
        "changeemail-newemail": "نئون برق ٽپال پتو:",
        "changeemail-none": "(ڪو بہ نہ)",
        "content-json-empty-array": "خالي اري",
        "duplicate-args-warning": "چتاءُ: [[:$2]] کي [[:$1]] ڪال ڪري رهيو آهي، جنهن منجھہ ’$3‘ نيم‌پيما لاءِ هڪ کان وڌيڪ قدر ڄاڻايل آهن. فقط آخري ڄاڻايل قدر استعمال ڪيو ويندو.",
        "parser-template-loop-warning": "سانچو چڪر لڌو ويو: [[$1]]",
-       "cantcreateaccounttitle": "کاتو کولي نہ ٿو سگھجي",
        "cantcreateaccount-text": "هن آءِ پي پتي تان کاتي جي تخليق تي يُوز (<strong>$1</strong>)  [[User:$3|$3]] روڪ لڳائي آهي.\n\n$3 جو ڄاڻايل سبب آهي <em>$2</em> آهي.",
        "cantcreateaccount-range-text": "آءِپي پتن جي حد <strong>$1</strong> ۾ [[User:$3|$3]] کاتو کولڻ تي روڪ لڳائي وئي آهي،$4 جنهن ۾ توهان جو آءِپي پتو بہ (<strong>$4</strong>)،  پڻ شامل آهي. \n\n$3 ان روڪَ جو سبب \"$2\" ڄاڻايو آهي.",
        "viewpagelogs": "هن صفحي جا لاگس ڏسو",
        "sp-contributions-newbies-sub": "نون کاتن لاءِ",
        "sp-contributions-newbies-title": "نون کاتن جي لاءِ يوزر جون ڀاڱيداريون",
        "sp-contributions-blocklog": "بنسش لاگ",
-       "sp-contributions-deleted": "يُوزر جون ڊاٺل ڀاڱيداريون",
+       "sp-contributions-deleted": "ڊاٿل {{GENDER:$1|يوزر}} ڀاڱيداريون",
        "sp-contributions-uploads": "چاڙھَ",
        "sp-contributions-logs": "لاگس",
        "sp-contributions-talk": "ڳالھہ",
        "feedback-back": "پوئتي",
        "feedback-cancel": "رد",
        "feedback-close": "ٿي ويو",
-       "feedback-error-title": "چُڪَ",
        "feedback-message": "نياپو:",
        "feedback-subject": "موضوع:",
        "feedback-submit": "جمع ڪرايو",
index 9d36abb..2cb5da0 100644 (file)
        "category-file-count-limited": "ဢၼ်ပဵၼ် {{PLURAL:$1|ၾၢႆႇၼႆႉ|$1 ၾၢႆႇၸိူဝ်းၼႆႉ}} မီးဝႆႉတီႈၼႂ်း တွၼ်ႈၵၼ်ၼႆ့။",
        "listingcontinuesabbrev": "သိုပ်ႇ",
        "index-category": "ၼႃႈလိၵ်ႈ ၸိူဝ်းၸီ့ဝႆ့",
-       "noindex-category": "á\81¼á\82\83á\82\88á\80\9cá\80­á\81µá\80ºá\82\88 á\81¸á\80­á\80°á\80\9dá\80ºá\80¸á\81¸á\80®á\80·á\80\9dá\82\86á\80·",
+       "noindex-category": "á\81¼á\82\83á\82\88á\80\9cá\80­á\81µá\80ºá\82\88 á\81¸á\80­á\80°á\80\9dá\80ºá\80¸á\81¸á\80®á\82\89á\80\9dá\82\86á\82\89",
        "broken-file-category": "ၼႃႈလိၵ်ႈၸိူဝ်းမီးဝႆႉ ႁဵင်းၵွင်ႉၾၢႆႇဢၼ်လူ့လႅဝ်",
        "about": "လွင်ႈတၢင်း",
        "article": "ၼမ်းၼႂ်း",
        "newwindow": "(ပိုတ်ႇၼင်ႇဝိၼ်းတူဝ်း ဢၼ်မႂ်ႇ)",
        "cancel": "ဢမ်ႇႁဵတ်း",
        "moredotdotdot": "ထႅင်ႈ...",
-       "morenotlisted": "သဵၼ်ႈမၢႆဢၼ်ၼႆႉ ဢမ်ႇတဵမ်ထူၼ်ႈ။",
+       "morenotlisted": "á\80\9eá\80µá\81¼á\80ºá\82\88á\80\99á\81¢á\82\86á\80¢á\81¼á\80ºá\81¼á\82\86á\82\89 á\80¢á\80\99á\80ºá\82\87á\80¢á\81¢á\80\95á\80ºá\82\88á\80\90á\80µá\80\99á\80ºá\80\91á\80°á\81¼á\80ºá\82\88á\81\8b",
        "mypage": "ၼႃႈလိၵ်ႈ",
        "mytalk": "တွၼ်ႈဢုပ်ႇ",
        "anontalk": "တွၼ်ႈဢုပ်ႇ",
        "talk": "တႃႇဢုပ်ႇ",
        "views": "လူတူၺ်း",
        "toolbox": "ၶိူင်ႈၵမ်ႉၵႅမ်",
+       "tool-link-userrights": "လႅၵ်ႈလၢႆႈ {{GENDER:$1|ၽူႈၸႂ်ႉတိုဝ်း}} ၸုမ်း",
+       "tool-link-emailuser": "သူင်ႇဢီးမေးလ်ဢၼ်ၼႆႉ {{GENDER:$1|ၽူႈၸႂ်ႉတိုဝ်း}}",
        "userpage": "တူၺ်းၼႃႈလိၵ်ႈၽူႈၸႂ်ႉတိုဝ်း",
        "projectpage": "တူၺ်းၼႃႈလိၵ်ႈ ပရေႃးၵျႅၵ်ႉ",
        "imagepage": "တူၺ်းၼႃႈလိၵ်ႈၾၢႆႇ",
        "yourpasswordagain": "ၶိုၼ်းပေႃႉပၼ် ၶေႃႈလပ်ႉ :",
        "createacct-yourpasswordagain": "ၼႄႉၼွၼ်းပၼ် ၶေႃႈလပ်ႉ",
        "createacct-yourpasswordagain-ph": "ပေႃႉသႂ်ႇၶေႃႈလပ်ႉထႅင်ႈၵမ်းၼိုင်ႈ",
-       "remembermypassword": "တွင်းဝႆႉပၼ် လွၵ်ႉဢိၼ်ႇၵဝ်ၶႃႈ တီႈၼႂ်း ၶိူင်ႈပိုတ်ႇဝႆႉၼႆႉ  (တီႈႁိုင်ႁိုင်မၼ်း $1 {{PLURAL:$1|ၼိုင်ႈဝၼ်း|ဝၼ်း}})",
        "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": "လွၵ်ႉဢိၼ်ႇ / သၢင်ႈဢၶွင်ႉ",
        "createacct-email-ph": "ပေႃႉသႂ်ႇပၼ် ႁဵင်းလိၵ်ႈ ဢီးမေးၸဝ်ႈၵဝ်ႇ",
        "createacct-another-email-ph": "ပေႃႉသႂ်ႇပၼ် ႁဵင်းလိၵ်ႈ ဢီးမေးလ်",
        "createaccountmail": "ၸႂ်ႉပၼ် ၶေႃႈလပ်ႉၸူဝ်ႈၵႅပ်ႉ သူင်ႇၼၼ်ႉၵႂႃႇၸူး ႁဵင်းလိၵ်ႈဢီးမေးလ် ဢၼ်မၵ်းမၼ်ႈဝႆႉ ပၼ်ၼၼ်ႉ။",
+       "createaccountmail-help": "ပေႃးဢမ်ႇမီး လွင်ႈလဵပ်ႈႁဵၼ်း ၶေႃႈလပ်ႉၼႆ တေဢမ်ႇၸၢင်ႈဢဝ်ၸႂ်ႉ တႃႇတေၵေႃႇတင်ႈ ဢၶွင်ႉတွၼ်ႈတႃႇ ၵူၼ်းတၢင်ႇၵေႃႉ။",
        "createacct-realname": "ၸိုဝ်ႈတႄႉတႄႉ (ဢဝ်ၸႂ်ဝႃႈ)",
        "createaccountreason": "လွင်ႈတၢင်း :",
        "createacct-reason": "လွင်ႈတၢင်း :",
        "loginerror": "လွၵ်ႉဢိၼ်ႇ ၽိတ်းပိူင်ႈ",
        "createacct-error": "ၵၢၼ်ၵေႃႇသၢင်ႈ ဢၶွင်ႉ ၽိတ်းပိူင်ႈ",
        "createaccounterror": "ဢမ်ႇၸၢင်ႈၵေႃႇသၢင်ႈ ဢၶွင်ႉ : $1",
+       "nocookiesnew": "ဢၶွင်ႉ ၽူႈၸႂ်ႉတိုဝ်းၼႆႉ ထုၵ်ႇၵေႃႇသၢင်ႈယဝ်ႉယဝ်ႈ၊ ၵူၺ်းၵႃႈ ၸဝ်ႈၵဝ်ႇဢမ်ႇပႆႇလႆႈၶဝ်ႈ လွၵ်ႉဢိၼ်ႇဝႆႉ။ {{SITENAME}} ၸႂ်ႉ ၶုၵ်ႉၵီး တႃႇတေၶဝ်ႈလွၵ်ႉဢိၼ်ႇ ၽူႈၸႂ်ႉတိုဝ်း။\nၸဝ်ႈၵဝ်ႇလႆႈဢိုတ်းဝႆႉ ၶုၵ်ႉၵီး။\nၶႅၼ်ႈတေႃႈ ပိုတ်ႇပၼ်ၸိူဝ်းၼၼ်ႉသေ ၶဝ်ႈလွၵ်ႉဢိၼ်ႇပၼ်တင်း ၸိုဝ်ႈၽူႈၸႂ်ႉတိုဝ်း ၸဝ်ႈၵဝ်ႇ ဢၼ်မႂ်ႇလႄႈတင်း ၶေႃႈလပ်ႉၼၼ်ႉလႄႈ။",
+       "nocookieslogin": "{{SITENAME}} ၸႂ်ႉ ၶုၵ်ႉၵီး တႃႇတေၶဝ်ႈလွၵ်ႉဢိၼ်ႇ ၽူႈၸႂ်ႉတိုဝ်း။\nၸဝ်ႈၵဝ်ႇလႆႈဢိုတ်းဝႆႉ ၶုၵ်ႉၵီး။\nၶႅၼ်ႈတေႃႈ ပိုတ်ႇပၼ်ၸိူဝ်းၼၼ်ႉသေ ၶတ်းၸႂ်တူၺ်းထႅမ်ႈလႄႈ။",
+       "nocookiesfornew": "ဢၶွင်ႉၽူႈၸႂ်ႉတိုဝ်းၼႆႉ ဢမ်ႇထုၵ်ႇၵေႃႇသၢင်ႈ ယွၼ်ႉပိူဝ်ႈႁဝ်းၶႃႈ ဢမ်ႇၸၢင်ႈၼႄႉၼွၼ်း ငဝ်ႈငႃႇမၼ်း။\nၶႅၼ်းတေႃႈ ပိုတ်ႇပၼ် ၶုၵ်ႉၵီးၸဝ်ႈၵဝ်ႇယဝ်ႉသေ ၶိုၼ်းတူင်ႉပိုတ်ႇၼႃႈလိၵ်ႈၼႆႉသေ ၶတ်းၸႂ်တူၺ်းထႅင်ႈၶႃႈလႄႈ။",
+       "createacct-loginerror": "ဢၶွင်ႉ ၵေႃႇတင်ႈၶႅမ်ႉလႅပ်ႈၵႂႃႇသေတႃႉ ၸဝ်ႈၵဝ်ႇ တေဢမ်ႇပႆႇၸၢင်ႈၶဝ်ႈ လွၵ်ႉဢိၼ်ႇ ႁင်းၶေႃႈ။ ၶႅၼ်းတေႃႈ ႁဵတ်းတႃႇ  [[Special:UserLogin|manual login]].",
        "noname": "ၸဝ်ႈၵဝ်ႇ ဢမ်ႇလႆႈ မၵ်းမၼ်ႈဝႆႉပၼ် ၸိုဝ်ႈၽူႈၸႂ်ႉတိုဝ်း ဢၼ်ၸႂ်ႉလႆႈ။",
        "loginsuccesstitle": "လွၵ်ႉဢိၼ်ႇဝႆႉယဝ်ႉ",
        "loginsuccess": "<strong>ၸဝ်ႈၵဝ်ႇ လွၵ်ႉဢိၼ်ႇၶဝ်ႈၸူး  {{SITENAME}} ၼင်ႇ \"$1\".</strong> ယဝ်ႉဢေႃႈ ယၢမ်းလဵဝ်",
+       "nosuchuser": "ၸိုဝ်ႈ တွၼ်ႈတႃႇတႃႇၽူႈၸႂ်ႉတိုဝ်း \"$1\" ဢၼ်ၼႆႉ မၼ်းဢမ်ႇမီးဝႆႉ။\nၸိုဝ်ႈၽူႈၸႂ်ႉတိုဝ်းၼႆႉ မၼ်းမီးလိူင်ႈတူဝ်လဵၵ်ႉတူဝ်ယႂ်ႇ။\nၵူတ်ႇထတ်းတူၺ်း တူဝ်လေႃးမၼ်း ဢမ်ႇၼၼ် [[Special:CreateAccount|create a new account]].",
        "nosuchusershort": "ဢၼ်ပဵၼ်ၸိုဝ်ႈ ၽူႈၸႂ်ႉတိုဝ်း \"$1\" ဢၼ်ၼႆႉ မၼ်းဢမ်ႇမီး။\nမႄးၵူတ်ႇတူၺ်း တူဝ်လိၵ်ႈမၼ်းလီလီလႄႈ။",
        "nouserspecified": "ၸဝ်ႈၵဝ်ႇ ထုၵ်ႇလီမၵ်းမၼ်ႈ ၸိုဝ်ႈၽူႈၸႂ်ႉတိုဝ်း",
        "login-userblocked": "ၽူႈၸႂ်ႉတိုဝ်းၵေႃႉၼႆႉ ထုၵ်ႇႁႄႉတတ်းဝႆႉ။ ဢမ်ႇမီးသုၼ်ႇတႃႇ လွၵ်ႉဢိၼ်ႇ",
        "passwordreset-emailelement": "ၸိုဝ်ႈၽူႈၸႂ်ႉတိုဝ်း:\n$1\n\nၶေႃႈလပ်ႉ ၸူဝ်ႈၵႅပ်ႉ:\n$2",
        "passwordreset-emailsentemail": "ႁဵင်းလိၵ်ႈ ဢီးမေးလ်ဢၼ်ၼႆႉၼႆႉ မၼ်းၵပ်းၵၢႆႇၵၼ်တင်း ဢၶွင်ႉၸဝ်ႈၵဝ်ႇ၊ ဢၼ်ပဵၼ် ဢီးမေးလ် တႃႇတင်ႈၶိုၼ်းမၢႆလပ်ႉၼၼ်ႉ တေထုၵ်ႇသူင်ႇၸူးယူႇ.",
        "passwordreset-emailsentusername": "ႁဵင်းလိၵ်ႈ ဢီးမေးလ်ဢၼ်ၼႆႉၼႆႉ မၼ်းၵပ်းၵၢႆႇၵၼ်တင်း ၸိုဝ်ႈၽူႈၸႂ်ႉတိုဝ်း ဢၼ်ၼႆႉ၊ ဢၼ်ပဵၼ် ဢီးမေးလ် တႃႇတင်ႈၶိုၼ်းမၢႆလပ်ႉၼၼ်ႉ တေထုၵ်ႇသူင်ႇၸူးယူႇ.",
-       "passwordreset-emailsent-capture": "ဢီးမေးလ် ၵၢၼ်တင်ႈၶိုၼ်း ​မၢႆလပ်ႉၼၼ်ႉ ထုၵ်ႇသူင်ႇၵႂႃႇၸူး ဢၼ်ၼႄဝႆႉၼင်ႇ ၽၢႆႇတႂ်ႈၼႆႉ။",
        "passwordreset-invalideamil": "ႁဵင်းလိၵ်ႈ ဢီႈမေးလ် ၽိတ်းဝႆႉ။",
        "passwordreset-nodata": "ၸိုဝ်ႈၽူႈၸႂ်ႉတိုဝ်းလႄႈ ႁဵင်းလိၵ်ႈဢီးမေးလ် ဢမ်ႇလႆႈၵမ်ႉထႅမ်ဝႆႉ သေဢၼ်။",
        "changeemail": "လႅၵ်ႈလၢႆႈ ဢမ်ႇၼၼ် ထွၼ်ပႅတ်ႈ ႁဵင်းလိၵ်ႈ ဢီးမေးလ်",
        "content-json-empty-object": "ၵၢၼ်ပဝ်ႇ",
        "content-json-empty-array": "ထႅဝ်ပဝ်ႇ",
        "post-expand-template-inclusion-warning": "<strong>ၶေႃႈၽၢင်ႉ</strong> - ပိူင်ဢဝ်မႃးႁူမ်ႈၼၼ်ႉယႂ်ႇပူၼ်ႉၼႃႇ။\nပိူင်မၢင်ၼႃႈတေဢမ်ႇႁူမ်ႈပႃးၸွမ်း။",
-       "cantcreateaccounttitle": "ဢမ်ႇၸၢင်ႈၵေႃႇသၢင်ႈ ဢၶွင်ႉ",
        "viewpagelogs": "တူၺ်းသၢႆမၢႆ တွၼ်ႈတႃႇၼႃႈလိၵ်ႈၼႆႉ",
        "nohistory": "တီႈၼႆႈ ဢမ်ႇမီး ပိုၼ်းထတ်းသၢင်ႈ တွၼ်ႈတႃႇၼႃႈလိၵ်ႈၼႆႉ",
        "currentrev": "ၵၢၼ်ၶူၼ်ႉလူ ၵမ်းလိုၼ်းသုတ်း",
        "upload-form-label-own-work": "ဢၼ်ၼႆႉပဵၼ် ၼႃႈၵၢၼ်တူဝ်ၶႃႈ။",
        "upload-form-label-infoform-categories": "လိူင်ႈ",
        "upload-form-label-infoform-date": "ဝၼ်းထီႉ",
+       "backend-fail-stream": "ဢမ်ႇၸၢင်ႈပိုတ်ႇလႆၾၢႆႇ \"$1\" ဢၼ်ၼႆႉ။",
+       "backend-fail-backup": "ဢမ်ႇၸၢင်ႈႁဵတ်း ၵႅမ်လင်ၾၢႆႇ \"$1\" ၼႆႉ။",
+       "backend-fail-notexists": "ၾၢႆႇ $1 ၼႆႉ မၼ်းဢမ်ႇလႆႈမီးဝႆႉ။",
+       "backend-fail-hashes": "ဢမ်ႇၸၢင်ႈဢဝ် ၾၢႆႇဢၼ်ယွႆႈ တွၼ်ႈတႃႇတေမႃး ၶဵင်ႇထတ်းၵၼ်။",
+       "backend-fail-notsame": "ၾၢႆႇဢၼ်ဢမ်ႇပႆႇလႆႈ ထတ်းသၢင်ႈၼၼ်ႉ မီးဝႆႉလူင်ႈၼႃႈ ၵႃႈတီႈ \"$1\" ။",
+       "backend-fail-invalidpath": "\"$1\" ၼႆႉ သၢႆတၢင်းတႃႇတေသိမ်းမၼ်း ၽိတ်းဝႆႉ။",
+       "backend-fail-delete": "ဢမ်ႇၸၢင်ႈမွတ်ႇပႅတ်ႈ ၾၢႆႇ \"$1\".",
+       "backend-fail-describe": "ဢမ်ႇၸၢင်ႈလႅၵ်ႈလၢႆႈ ၶေႃႈမုၼ်းၽၢႆႇလင် တွၼ်ႈတႃႇၾၢႆႇ \"$1\" ။",
+       "backend-fail-alreadyexists": "ၾၢႆႇ \"$1\" မီးဝႆႉထႃႈယဝ်ႉ။",
+       "backend-fail-store": "ဢမ်ႇၸၢင်ႈသိမ်းၾၢႆႇ \"$1\" ၵႃႈတီႈ \"$2\" ။",
+       "backend-fail-copy": "ဢမ်ႇၸၢင်ႈထုတ်ႇဢဝ် ၾၢႆႇ \"$1\" ၸူး \"$2\".",
+       "backend-fail-move": "ဢမ်ႇၸၢင်ႈၶၢႆႉၾၢႆႇ \"$1\" ၸူး \"$2\" ။",
+       "backend-fail-opentemp": "ဢမ်ႇၸၢင်ႈပိုတ်ႇၾၢႆႇတိုဝ်းၸူဝ်ႈၵႅပ်ႉ။",
+       "backend-fail-writetemp": "ဢမ်ႇၸၢင်ႈတႅမ်ႈသႂ်ႇၸူး ၾၢႆႇတိုဝ်းၸူဝ်ႈၵႅပ်ႉ။",
+       "backend-fail-closetemp": "ဢမ်ႇၸၢင်ႈဢိုတ်း ၾၢႆႇတိုဝ်းၸူဝ်ႈၵႅပ်ႉ။",
+       "backend-fail-read": "ဢမ်ႇၸၢင်ႈလူၾၢႆႇ \"$1\" ။",
+       "backend-fail-create": "ဢမ်ႇၸၢင်ႈတႅမ်ႈၾၢႆႇ \"$1\" ။",
+       "backend-fail-maxsize": "ဢမ်ႇၸၢင်ႈတႅမ်ႈၾၢႆႇ \"$1\" ယွၼ်ႉပိူဝ်ႈဝႃႈ မၼ်းၼႆႉ ယႂ်ႇလိူဝ်သေ {{PLURAL:$2|ၼိုင်ႈပၢႆႉ|$2 ပၢႆႉ}}",
+       "backend-fail-readonly": "တီႈသိမ်းသုတ်းလင် \"$1\" ၼႆႉ ယၢမ်းလဵဝ် မၼ်းတေၸၢင်ႈလူလႆႈၵူၺ်း။ လွင်ႈတၢင်း ဢၼ်လႆႈပၼ်ဝႆႉတႄႉ ပဵၼ် <em>$2</em>",
+       "backend-fail-synced": "ၾၢႆႇ \"$1\" ၼႆႉ မၼ်းပဵၼ်သၢႆႇငၢႆ ဢၼ်ဢမ်ႇမႃးငမ်ႇမႅၼ်ႈၵၼ် တီႈၼႂ်း ဢွင်ႈသိမ်းသုတ်းလင် ၽၢႆႇၼႂ်း။",
+       "backend-fail-connect": "ဢမ်ႇၸၢင်ႈၵွင်ႉသိုပ်ႇၸူး ဢွင်ႈသိမ်းသုတ်းလင် \"$1\" ။",
+       "backend-fail-internal": "လွင်ႈၽိတ်းပိူင်ႈ ဢၼ်ဢမ်ႇႁူႉလွင်ႈမၼ်း လႆႈပဵၼ်ဝႆႉ ၵႃႈတီႈၼႂ်း ဢွင်ႈသိမ်းသုတ်းလင် \"$1\" ။",
+       "backend-fail-contenttype": "ဢမ်ႇၸၢင်ႈတႅပ်းတတ်းလိူင်ႈၾၢႆႇ ဢၼ်တႃႇတေသိမ်းၵႃႈတီႈ \"$1\" ။",
+       "backend-fail-batchsize": "ဢွင်ႈတီႈသိမ်းသုတ်းလင် ထုၵ်ႇပၼ်ဝႆႉ ၸုမ်းၾၢႆႇ $1 ၾၢႆႇ\n{{PLURAL:$1|ၵၢၼ်ႁဵတ်း|ၵၢၼ်ႁဵတ်း}}; တီႈမၵ်းၶၢၼ်းမၼ်းပဵၼ် $2\n{{PLURAL:$2|ၵၢၼ်ႁဵတ်း|ၵၢၼ်ႁဵတ်း}}.",
+       "backend-fail-usable": "ဢမ်ႇၸၢင်ႈလူ ဢမ်ႇၼၼ် ဢမ်ႇၸၢင်ႈတႅမ်ႈ ၾၢႆႇ \"$1\" ယွၼ်ႉပိူဝ်ႈဝႃႈ ၵၢၼ်လူတ်းပွႆႇ ဢမ်ႇတဵမ်ထူၼ်ႈ ဢမ်ႇၼၼ် ႁၢမ်းဝႆႉ ဢွင်ႈတီႈသိမ်း။",
+       "filejournal-fail-dbconnect": "ဢမ်ႇၸၢင်ႈၵွင်ႉသိုပ်ႇၸူး ယေးၶေႃႈမုၼ်း ၵျႃႇၼႄႇ တွၼ်ႈတႃႇ ဢွင်ႈတီႈသိမ်းသုတ်းလင် \"$1\" ။",
+       "filejournal-fail-dbquery": "ဢမ်ႇၸၢင်ႈဢၢပ်ႉတိတ်ႉ ယေးၶေႃႈမုၼ်း ၵျႃႇၼႄႇ တွၼ်ႈတႃႇ ဢွင်ႈတီႈသိမ်းသုတ်းလင် \"$1\" ။",
        "lockmanager-notlocked": "ဢမ်ႇၸၢင်ႈ ပိုတ်ႇသေႃး \"$1\"; ၼၼ်ႉမၼ်းဢမ်ႇလႆႈ ၶတ်းဝႆႉ။",
        "lockmanager-fail-closelock": "ဢမ်ႇၸၢင်ႈဢိုတ်း ၾၢႆႇဢၼ်ၶတ်းဝႆႉ တွၼ်ႈတႃႇ \"$1\" ။",
        "lockmanager-fail-deletelock": "ဢမ်ႇၸၢင်ႈမွတ်ႇပႅတ်ႈ ၾၢႆႇဢၼ်ၶတ်းဝႆႉ တွၼ်ႈတႃႈ \"$1\".",
        "lockmanager-fail-acquirelock": "ဢမ်ႇၸၢင်ႈဢဝ် ၶတ်းတွၼ်ႈတႃႇ \"$1\" ။",
        "lockmanager-fail-openlock": "ဢမ်ႇၸၢင်ႈဢိုတ်း ၾၢႆႇဢၼ်ၶတ်းဝႆႉ တွၼ်ႈတႃႇ \"$1\" ။",
        "lockmanager-fail-releaselock": "ဢမ်ႇၸၢင်ႈပွႆႇပၼ် ဢၼ်ၶတ်း တွၼ်ႈတႃႈ \"$1\" ။",
+       "lockmanager-fail-db-bucket": "ဢမ်ႇၸၢင်ႈၵပ်းသိုပ်ႇလႆႈ ယေးၶေႃႈမုၼ်းဢၼ်ၶတ်းဝႆႉ ၵႃႈတီႈၼႂ်း ပုင်း $1 ၼၼ်ႉ။",
        "lockmanager-fail-db-release": "ဢမ်ႇၸၢင်ႈပွႆႇပၼ်ၵၢၼ်ၶတ်း ၵႃႈတီႈ ယွင်ၶေႃႈမုၼ်း $1 ။",
        "lockmanager-fail-svr-acquire": "ဢမ်ႇၸၢင်ႈဢဝ်လႆႈ ၵၢၼ်ၶတ်း ၵႃႈတီႈၼိူဝ် သႃႇပိူဝ်ႇ $1 ။",
        "lockmanager-fail-svr-release": "ဢမ်ႇၸၢင်ႈပွႆႇပၼ် ၵၢၼ်ၶတ်း ၵႃႈတီႈၼိူဝ် သႃႇပိူဝ်ႇ $1 ။",
+       "zip-file-open-error": "ၽွင်းမိူဝ်ႈပိုတ်ႇၽၢႆႇ တွၼ်ႈတႃႇ ၵူတ်ႇထတ်း ZIP ၼၼ်ႉ လႆႈထူပ်းၺႃး လွင်ႈၽိတ်းပိူင်ႈ။",
        "zip-wrong-format": "ၾၢႆႇဢၼ်မၵ်းမၼ်ႈဝႆႉပၼ်ၼၼ်ႉ မၼ်းဢမ်ႇၸႂ်ႈ ၾၢႆႇ ZIP ။",
        "zip-bad": "ၾၢႆႇၼႆႉ မၼ်းၵွႆဝႆႉ ဢမ်ႇၼၼ် မၼ်းပဵၼ် ၾၢႆႇ ZIP ဢၼ်ဢမ်ႇလူႇလႆႈ။\nမၼ်းဢမ်ႇၸၢင်ႈ ၵူတ်ႇတူၺ်း တွၼ်ႈတႃႇ ပၢႆးႁူမ်ႇလူမ်ႈ လႆႈလီလီ။",
        "zip-unsupported": "ၾၢႆႇၼႆႉပဵၼ် ZIP ၾၢႆႇ ဢၼ်ၸႂ်ႉဝႆႉ ၽၢင်ႁၢင်ႈၵၢၼ် ZIP ဢၼ် သိူဝ်ႇၶၢဝ်ႇဝီႇၶီႇ ဢမ်ႇၵမ်ႉထႅမ်ဝႆႉ။  မၼ်းဢမ်ႇၸၢင်ႈ ၵူတ်ႇတူၺ်း တွၼ်ႈတႃႇ ပၢႆးႁူမ်ႇလူမ်ႈ လႆႈလီလီ။",
        "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-notindir": " သၢႆတၢင်းဢၼ်တုၵ်းယွၼ်းမႃးၼၼ်ႉ မၼ်းဢမ်ႇမီးၵႃႈတီႈၼႂ်း သၢႆတၢင်းလူတ်ႇၶိုၼ်ႈ ဢၼ်မႄးၵုမ်းဝႆႉ။",
        "img-auth-nologinnWL": "ၸဝ်ႈၵဝ်ႇ ဢမ်ႇလႆႈၶဝ်ႈ လွၵ်ႉဢိၼ်ဝႆႉသေ \"$1\" ၼႆႉ မၼ်းဢမ်ႇမီးဝႆႉ တီႈၼႂ်း သဵၼ်ႈမၢႆၶၢဝ်။",
        "img-auth-nofile": "ၾၢႆႇ \"$1\" ၼႆႉ မၼ်းဢမ်ႇမီးဝႆႉ။",
        "img-auth-isdir": "ၸဝ်ႈၵဝ်ႇ တိုၵ်ႉၶတ်းၸႂ်ႉ ၶဝ်ႈၸႂ်ႉ ၾူဝ်ႇတိူဝ်ႇ \"$1\" ယူႇ။\nၶႂၢင်းဝႆႉပၼ် ၵၢၼ်ၸႂ်ႉတိုဝ်းၾၢႆႇၵူၺ်း။",
        "unusedtemplates": "လွၵ်းပိူင် ဢၼ်ဢမ်ႇၸႂ်ႉဝႆႉ",
        "unusedtemplateswlh": "ႁဵင်းၵွင်ႉ တၢင်ႇၸိူဝ်း",
        "randompage": "ဢဝ်ၼႃႈလိၵ်ႈသၢင်ႇထုၵ်ႇဝႃႈ",
-       "randompage-nopages": "á\81¸á\80½á\80\99á\80ºá\80¸á\81¼á\80\84á\80ºá\82\87á\80\95á\82\83á\82\88á\80\90á\82\82á\80ºá\82\88á\81¼á\82\86á\82\89 á\80\99á\81¼á\80ºá\80¸á\80¢á\80\99á\80ºá\82\87á\80\99á\80®á\80¸ ဝႆႉ ၼႃႈလိၵ်ႈသင်\n{{PLURAL:$2|လွၵ်းၸိုဝ်ႈ|လွၵ်းၸိုဝ်ႈ}}: $1.",
+       "randompage-nopages": "á\80\90á\81¢á\80\84á\80ºá\80¸á\81½á\81¢á\82\86á\82\87á\80\9cá\80\84á\80ºá\81¼á\81¼á\80ºá\82\89 á\80¢á\80\99á\80ºá\82\87á\80\99á\80®á\80¸ဝႆႉ ၼႃႈလိၵ်ႈသင်\n{{PLURAL:$2|လွၵ်းၸိုဝ်ႈ|လွၵ်းၸိုဝ်ႈ}}: $1.",
        "randomincategory": "ၼႃႈလိၵ်ႈၵမ်ႉသၢင်ႇတေႃႇ ၵႃႈတီႈၼႂ်း လိူင်ႈ",
        "randomincategory-invalidcategory": "\"$1\" ၼႆႉ ပဵၼ်ၸိုဝ်ႈလိူင်ႈ ဢၼ်ဢမ်ႇပဵၼ်လႆႈ။",
        "randomincategory-nopages": "မၼ်းဢမ်ႇမီးဝႆ ၼႃႈလိၵ်ႈသင် ၵႃႈတီႈၼႂ်း [[:Category:$1|$1]] လိူင်ႈ။",
        "nbytes": "$1 {{PLURAL:$1|ၿႆႉ|ၿႆႉ}}",
        "ncategories": "{{PLURAL:$1|လိူင်ႈ|လိူင်ႈတင်းလၢႆ}}",
        "ninterwikis": "$1 {{PLURAL:$1|ဝီႇၶီႇၽၢႆႇၼႂ်း|ဝီႇၶီႇၸိူဝ်းၽၢႆႇၼႂ်း}}",
-       "nlinks": "$1 {{PLURAL:$1|ႁဵင်းၵွင်ႉ|ႁဵင်းၵွင်ႉ}}",
+       "nlinks": "$1 {{PLURAL:$1|ႁဵင်းၵွင်ႉ|ႁဵင်းၵွင်ႉၼမ်}}",
        "nmembers": "$1 {{PLURAL:$1|member|ၽူႈၶဝ်ႈၸုမ်း}}",
-       "nmemberschanged": "$1 → $2 {{PLURAL:$2|ၽူႈၶဝ်ႈၸုမ်း|ၽူႈၶဝ်ႈၸုမ်း}}",
+       "nmemberschanged": "$1 {{PLURAL:$1|member|ၽူႈၶဝ်ႈၸုမ်း}}",
        "nrevisions": "$1 {{PLURAL:$1|​ၶေႃႈၶူၼ်ႉလူ|ၶေႃႈၶူၼ်ႉလူ}}",
-       "nimagelinks": "á\81¸á\82\82á\80ºá\82\89á\80\9dá\82\86á\82\89á\81µá\82\83á\82\88á\80\90á\80®á\82\88á\81¼á\80­á\80°á\80\9dá\80º $1 {{PLURAL:$1|á\81¼á\82\83á\82\88á\80\9cá\80­á\81µá\80ºá\82\88|ၼႃႈလိၵ်ႈ}}",
-       "ntransclusions": "ၸႂ်ႉဝႆႉၵႃႈတီႈၼိူဝ် $1 {{PLURAL:$1|ၼႃႈလိၵ်ႈ|ၼႃႈလိၵ်ႈ}}",
+       "nimagelinks": "á\81¸á\82\82á\80ºá\82\88á\80\9dá\82\86á\82\89 á\80\90á\80®á\82\88 $1 {{PLURAL:$1|page|ၼႃႈလိၵ်ႈ}}",
+       "ntransclusions": "ၸႂ်ႉဝႆႉၵႃႈတီႈၼိူဝ် $1 {{PLURAL:$|page|ၼႃႈလိၵ်ႈ}}",
        "specialpage-empty": "တွၼ်ႈတႃႇ ၶေႃႈပွင်ႇၼႄ ဢၼ်ၼႆႉၼႆႉ မၼ်းဢမ်ႇမီး ၽွၼ်းလႆႈ။",
        "lonelypages": "ၼႃႈလိၵ်ႈ ႁၢမ်းႁိူၼ်း",
        "lonelypagestext": "ၼႃႈလိၵ်ႈၸိူဝ်းပႃႈတႂ်ႈၼႆႉ မၼ်းဢမ်ႇလႆႈ ၵွင်ႉဝႆႉ ဢမ်ႇၼၼ် ဢမ်ႇလႆႈၶဝ်ႈပႃႈဝႆႉ တႂ်ႈၼႂ်း ၼႃႈလိၵ်ႈတႃႇၸိူဝ်း ၼင်ႇ  {{SITENAME}} ၼႆႉ။",
        "listusers-editsonly": "ၼႄပၼ် ၽူႈၸႂ်ႉတိုဝ်း ၸိူဝ်းမႄးထတ်းၼၼ်ႉၵူၺ်း",
        "listusers-creationsort": "ၶပ်ႉၸႅၼ်ၸွမ်း ဝၼ်းထီႉ ၵေႃႇသၢင်ႈ",
        "listusers-desc": "ၶပ်ႉၸႅၼ်ႇၸွမ်း မၢႆၶပ်ႉတူဝ်လဵၵ်ႉ",
-       "usereditcount": "$1 {{PLURAL:$1|မႄးထတ်း|မႄးထတ်း}}",
+       "usereditcount": "$1 {{PLURAL:$1|edit|မႄးထတ်း}}",
        "usercreated": "{{GENDER:$3|ၵေႃႇသၢင်ႈယဝ်ႉ}} မိူဝ်ႈ $1 မိူဝ်ႈ$2",
        "newpages": "ၼႃႈလိၵ်ႈမႂ်ႇ",
        "newpages-submit": "ၼႄ",
        "booksources-search-legend": "ၶူၼ်ႉႁႃတႃႇ ငဝ်ႇငႃႇပပ်ႉ",
        "booksources-search": "ၶူၼ်ႉႁႃ",
        "log": "သၢႆမၢႆ",
+       "checkbox-all": "တင်းမူတ်း",
+       "checkbox-none": "ဢမ်ႇမီးသင်",
+       "checkbox-invert": "ပိၼ်ႈၽိုၼ်",
        "allpages": "ၼႃႈ​လိၵ်ႈ​တင်း​သဵင်ႈ",
+       "nextpage": "ၼႃးလိၵ်ႈ တေမႃး ($1)",
+       "prevpage": "ၼႃးလိၵ်ႈ ပူၼ်ႉမႃး ($1)",
+       "allpagesfrom": "ၼႃႈလိၵ်ႈဢၼ်ၼႄ တႄႇတီႈ :",
+       "allpagesto": "ၼႃႈလိၵ်ႈဢၼ်ၼႄ သုတ်းတီႈ :",
        "allarticles": "ၼႃႈ​လိၵ်ႈ​တင်း​သဵင်ႈ",
+       "allinnamespace": "ၼႃႈလိၵ်ႈတင်းမူတ်း ($1 ဢွင်ႈၸိုဝ်ႈ)",
        "allpagessubmit": "ၶူၼ်ႉႁႃ",
+       "allpagesprefix": "ၼႃးလိၵ်ႈဢၼ်ၼႄ ဢိၵ်ႇတင်း ၶေႃႈလူင်ႈၼႃႈ",
        "categories": "လိူင်ႈ",
        "mywatchlist": "သဵၼ်ႈမၢႆပႂ်ႉတူၺ်း",
        "watch": "ပႂ်ႉတူၺ်း",
index d17cdac..863a822 100644 (file)
        "talk": "Diskusia",
        "views": "Zobrazenia",
        "toolbox": "Nástroje",
+       "tool-link-userrights": "Zmeniť používateľské skupiny {{GENDER:$1|tohoto použivateľa|tejto používateľky}}",
+       "tool-link-emailuser": "Poslať e-mail {{GENDER:$1|tomuto používateľovi|tejto používateľke}}",
        "userpage": "Zobraziť stránku používateľa",
        "projectpage": "Zobraziť projektovú stránku",
        "imagepage": "Zobraziť stránku súboru",
        "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ý?",
        "content-model-css": "CSS",
        "content-json-empty-object": "Prázdny objekt",
        "content-json-empty-array": "Prázdne pole",
+       "deprecated-self-close-category": "Stránky s neplatnými samouzavrenými HTML značkami",
+       "deprecated-self-close-category-desc": "Stránka obsahuje neplatné samouzatvárajúce HTML značky, napr. <code>&lt;b/></code> alebo <code>&lt;span/></code>. Ich správanie sa v záujme konzistencie so špecifikáciou HTML5 čoskoro zmení a použitie je preto vo wikitexte zastarané.",
        "duplicate-args-warning": "<strong>Upozornenie:</strong> Stránka [[:$1]] volá [[:$2]] s viacerými hodnotami parametra „$3“. Použitá bude len posledná odovzdaná hodnota.",
        "duplicate-args-category": "Stránky s duplicitnými parametrami pri volaniach šablón",
        "duplicate-args-category-desc": "Stránka obsahuje volania šablóny používajúce duplicitné parametere, ako napríklad <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> alebo <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "search-redirect": "(presmerovanie $1)",
        "search-section": "(sekcia $1)",
        "search-category": "($1 kategória)",
+       "search-file-match": "(výskyt v obsahu súboru)",
        "search-suggest": "Mali ste na mysli „$1“?",
        "search-rewritten": "Zobrazujú sa výsledky pre $1. Vyhľadať namiesto toho $2.",
        "search-interwiki-caption": "Sesterské projekty",
        "prefs-files": "Súbory",
        "prefs-custom-css": "Vlastný CSS",
        "prefs-custom-js": "Vlastný JS",
-       "prefs-common-css-js": "Spoločné CSS/JS pre všetky témy vzhľadu:",
+       "prefs-common-css-js": "Spoločné CSS/JS pre všetky témy:",
        "prefs-reset-intro": "Túto stránku môžete použiť na vrátenie predvolených hodnôt vašich nastavení.\nTúto operáciu nemožno vrátiť.",
        "prefs-emailconfirm-label": "Overenie e-emailu:",
        "youremail": "Váš e-mail²",
        "right-edit": "Upravovať stránky (ktoré nie sú diskusné stránky)",
        "right-createpage": "Vytvárať stránky (ktoré nie sú diskusné stránky)",
        "right-createtalk": "Vytvárať diskusné stránky",
-       "right-createaccount": "Vytvárať nové používateľské účty",
+       "right-createaccount": "Vytvárať nové používateľské kontá",
+       "right-autocreateaccount": "Automatické prihlásenie s externým používateľským kontom",
        "right-minoredit": "Označovať úpravy ako drobné",
        "right-move": "Presúvať stránky",
        "right-move-subpages": "Presunúť stránky aj s podstránkami",
        "rcshowhidebots": "$1 botov",
        "rcshowhidebots-show": "Zobraziť",
        "rcshowhidebots-hide": "Skryť",
-       "rcshowhideliu": "$1 registrovaných používateľov",
+       "rcshowhideliu": "$1 registrovaných",
        "rcshowhideliu-show": "Zobraziť",
        "rcshowhideliu-hide": "Skryť",
-       "rcshowhideanons": "$1 anonymných používateľov",
+       "rcshowhideanons": "$1 anonymov",
        "rcshowhideanons-show": "Zobraziť",
        "rcshowhideanons-hide": "Skryť",
        "rcshowhidepatr": "$1 úpravy strážených stránok",
        "rcshowhidemine": "$1 moje úpravy",
        "rcshowhidemine-show": "Zobraziť",
        "rcshowhidemine-hide": "Skryť",
-       "rcshowhidecategorization": "$1 kategorizáciu stránok",
+       "rcshowhidecategorization": "$1 kategorizáciu",
        "rcshowhidecategorization-show": "Zobraziť",
        "rcshowhidecategorization-hide": "Skryť",
        "rclinks": "Zobraziť posledných $1 úprav za posledných $2 dní<br />$3",
        "upload-copy-upload-invalid-domain": "Kopírovanie nahraných súborov nie je dostupné z tejto domény.",
        "upload-dialog-title": "Nahrať súbor",
        "upload-dialog-button-cancel": "Zrušiť",
+       "upload-dialog-button-back": "Späť",
        "upload-dialog-button-done": "Hotovo",
        "upload-dialog-button-save": "Uložiť",
        "upload-dialog-button-upload": "Nahrať",
        "watchnologin": "Nie ste prihlásený/á",
        "addwatch": "Pridať do zoznamu sledovaných stránok",
        "addedwatchtext": "Stránka „[[:$1]]“ a jej diskusná stránka boli pridané do vášho zoznamu [[Special:Watchlist|sledovaných stránok]].",
+       "addedwatchtext-talk": "„[[:$1]]“ a súvisiaca stránka boli pridané do vášho zoznamu [[Special:Watchlist|sledovaných stránok]].",
        "addedwatchtext-short": "Stránka „$1“ bola pridaná do vášho zoznamu sledovaných.",
        "removewatch": "Odstrániť zo zoznamu sledovaných",
        "removedwatchtext": "Stránka „[[:$1]]“ a jej diskusná stránka boli odstránené z vášho [[Special:Watchlist|zoznamu sledovaných stránok]].",
+       "removedwatchtext-talk": "„[[:$1]]“ a súvisiaca stránka boli odstránené z vášho [[Special:Watchlist|zoznamu sledovaných stránok]].",
        "removedwatchtext-short": "Stránka „$1“ bola odstránená z vášho zoznamu sledovaných.",
        "watch": "Sledovať",
        "watchthispage": "Sledovať túto stránku",
        "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",
        "revertpage": "Posledné úpravy používateľa [[Special:Contributions/$2|$2]] ([[User talk:$2|diskusia]]) vrátené; bola obnovená posledná úprava $1",
        "revertpage-nouser": "Vrátené úpravy od skrytého používateľa na poslednú revíziu od {{GENDER:$1|[[User:$1|$1]]}}",
        "rollback-success": "Úpravy $1 vrátené; obnovená posledná verzia od $2.",
+       "rollback-success-notify": "Úpravy používateľa $1 boli vrátené;\nobnovená posledná revízia od používateľa $2. [$3 Zobraziť zmeny]",
        "sessionfailure-title": "Chyba relácie",
        "sessionfailure": "Zdá sa, že je problém s vašou prihlasovacou reláciou;\ntáto akcia bola zrušená ako prevencia proti zneužitiu relácie (session).\nProsím, stlačte \"naspäť\", obnovte stránku, z ktorej ste sa sem dostali, a skúste to znova.",
        "changecontentmodel": "Zmeniť model obsahu stránky",
        "tags-active-yes": "Áno",
        "tags-active-no": "Nie",
        "tags-source-extension": "Definované softvérom",
+       "tags-source-manual": "Pridané manuálne používateľmi a botmi",
        "tags-source-none": "Už sa nepoužíva",
        "tags-edit": "upraviť",
        "tags-delete": "zmazať",
        "htmlform-cloner-create": "Pridať ďalšie",
        "htmlform-cloner-delete": "Odstrániť",
        "htmlform-cloner-required": "Je povinná najmenej jedna hodnota.",
-       "sqlite-has-fts": "$1 s podporou vyhľadávania v plnom texte",
-       "sqlite-no-fts": "$1 bez podpory vyhľadávania v plnom texte",
        "logentry-delete-delete": "$1 zmazal stránku $3",
        "logentry-delete-restore": "$1 obnovil stránku $3",
        "logentry-delete-event": "$1 zmenil viditeľnosť {{PLURAL:$5|záznamu udalostí|$5 záznamov udalostí}} k stránke $3: $4",
        "feedback-bugornote": "Ak ste pripravený podrobne popísať technický problém, prosím pošlite [$1 hlásenie o chybe]. \nV opačnom prípade môžete použiť zjednodušený formulár nižšie. Váš komentár sa pridá na stránku „[$3 $2]“ spolu s vašim používateľským meno a prehliadačom, ktorý používate.",
        "feedback-cancel": "Zrušiť",
        "feedback-close": "Hotovo",
+       "feedback-external-bug-report-button": "Založiť technickú úlohu",
        "feedback-dialog-title": "Odoslať názor",
-       "feedback-error-title": "Chyba",
+       "feedback-dialog-intro": "Pomocou formulára nižšie môžete odoslať svoj názor. Váš komentár sa spolu s vašim použivateľským menom pridá na stránku „$1“.",
        "feedback-error1": "Chyba: Nerozpoznaný výsledok z API",
        "feedback-error2": "Chyba: Úprava sa nepodarila",
        "feedback-error3": "Chyba: Žiadna odpoveď z API",
        "feedback-message": "Správa:",
        "feedback-subject": "Predmet:",
        "feedback-submit": "Odoslať",
+       "feedback-terms": "Beriem na vedomie, že informácie o mojom prehliadači zahŕňajú jeho presnú verziu spolu s verziou operačného systému a budú zverejnené pri mojom komentári.",
+       "feedback-termsofuse": "Súhlasím s tým, že budem poskytovať spätnú väzbu v súlade s Podmienkami použitia.",
        "feedback-thanks": "Ďakujeme. Váš komentár bol odoslaný na stránku „[$2 $1]“.",
        "feedback-thanks-title": "Ďakujeme",
        "feedback-useragent": "Prehliadač:",
        "searchsuggest-search": "Hľadať",
        "searchsuggest-containing": "obsahuje...",
+       "api-error-autoblocked": "Vaše IP adresa bola automaticky zablokovaná, pretože ju používal zablokovaný používateľ.",
        "api-error-badaccess-groups": "Nemáte oprávnenie nahrávať súbory na tejto wiki.",
        "api-error-badtoken": "Vnútorná chyba: Zlý token.",
+       "api-error-blocked": "Možnosť editovať vám bola zablokovaná.",
        "api-error-copyuploaddisabled": "Nahrávanie z URL je na tomto serveri zakázané.",
        "api-error-duplicate": "{{PLURAL:$1|ďalší súbor|ďalšie súbory}} s rovnakým obsahom už na tejto wiki existujú",
        "api-error-duplicate-archive": "{{PLURAL:$1|ďalší súbor|ďalšie súbory}} s rovnakým obsahom už na tejto wiki existoval, ale {{PLURAL:$1|bol zmazaný|boli zmazané}}.",
        "pagelang-language": "Jazyk",
        "pagelang-use-default": "Použiť predvolený jazyk",
        "pagelang-select-lang": "Vybrať jazyk",
+       "pagelang-submit": "Odoslať",
        "right-pagelang": "Zmeniť jazyk stránky",
        "action-pagelang": "meniť jazyk stránky",
        "default-skin-not-found": "Uups! Základná tapeta pre Vašu wiki, popísanú v <code dir=\"ltr\">$wgDefaultSkin</code> ako <code>$1</code>, nie je dostupná. \n\nVaša inštalácia pravdepodobne obsahuje nasledovné tapety. Pozri [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] pre viac informácii o ich aktivácii a zvoľte základnú.\n\n$2\n\n; Ak ste MediaWiki len teraz nainštalovali\n; Zrejme ste to nainštalovali z gitu alebo priamo zo zdrojového kódu inou metódou. Je to očakávané. Skúste nainštalovať nejaké tapety z [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org's skin directory];\n:*Stiahnutím [https://www.mediawiki.org/wiki/Download tarball installer], ktorý ponúka viacero tapiet a rozšírení. Skopírovať a nalepiť možno priamo z <code>skins/</code>.\n:*Klonovanie jednej zo <code>mediawiki/skins/*</code> schránok cez git do <code dir=\"ltr\">skins/</code> priečinku Vašej Media Wiki inštalácie.\n: S existujúcou git schránkou, ak ste vývojár MediaWiki, by nemal byť konflikt.\n\n: Ak ste upgradeovali MediaWiki\n: MediaWiki 1.24 a novšie už tapety automaticky neaktivujú. (see [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manual: Skin autodiscovery]). Nasledovný kód môžete skopírovať do <code>LocalSettings.php</code> pre aktivovanie všetkých dostupných tapiet.\n\n<pre dir=\"ltr\">$3</pre>\n\n; Ak ste upravili <code>LocalSettings.php</code>:\n: Skontrolujte chyby.",
        "mediastatistics-header-text": "Text",
        "mediastatistics-header-executable": "Spustiteľné súbory",
        "mediastatistics-header-archive": "Komprimované formáty",
+       "mediastatistics-header-total": "Všetky súbory",
        "json-warn-trailing-comma": "Z JSONu {{PLURAL:$1|bola odstránená 1 koncová čiarka|boli odstránené $1 koncové čiarky|bolo odstránených $1 koncových čiarok}}",
        "json-error-unknown": "Došlo k problému s JSONom. Chyba: $1",
        "json-error-depth": "Maximálna hĺbka zásobníka bola prekročená",
        "special-characters-group-ipa": "IPA",
        "special-characters-group-symbols": "Symboly",
        "special-characters-group-greek": "Grécke",
+       "special-characters-group-greekextended": "Grécke rozšírené",
        "special-characters-group-cyrillic": "Azbuka",
        "special-characters-group-arabic": "Arabské",
        "special-characters-group-arabicextended": "Arabské rozšírené",
        "mw-widgets-dateinput-placeholder-month": "RRRR-MM",
        "mw-widgets-titleinput-description-new-page": "stránka zatiaľ neexistuje",
        "mw-widgets-titleinput-description-redirect": "presmerovanie na $1",
-       "randomrootpage": "Náhodná koreňová stránka"
+       "randomrootpage": "Náhodná koreňová stránka",
+       "changecredentials": "Zmena prihlasovacích údajov",
+       "removecredentials": "Odstránenie prihlasovacích údajov"
 }
index 221e53a..36470f5 100644 (file)
        "talk": "Pogovor",
        "views": "Pogled",
        "toolbox": "Orodja",
+       "tool-link-userrights": "Spremeni {{GENDER:$1|uporabnikove|uporabničine}} skupine",
+       "tool-link-emailuser": "Pošlji e-pošto {{GENDER:$1|uporabniku|uporabnici}}",
        "userpage": "Prikaži uporabnikovo stran",
        "projectpage": "Prikaži projektno stran",
        "imagepage": "Pokaži stran z datoteko",
        "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?",
        "passwordreset-emailelement": "Uporabniško ime: \n$1\n\nZačasno geslo: \n$2",
        "passwordreset-emailsentemail": "Če je e-poštni naslov povezan z vašim računom, vam bomo poslali e-pošto za postavitev gesla.",
        "passwordreset-emailsentusername": "Če obstaja e-poštni naslov, povezan s tem uporabniškim imenom, vam bomo poslali e-pošto za postavitev gesla.",
-       "passwordreset-emailsent-capture2": "Poslali smo {{PLURAL:$1|e-pošto|e-pošti|e-pošte}} za ponastavitev gesla. {{PLURAL:$1|Uporabniško ime in geslo sta navedena spodaj.|Seznam uporabniških imen in gesel je naveden spodaj.}}",
-       "passwordreset-emailerror-capture2": "Pošiljanje e-pošte {{GENDER:$2|uporabniku|uporabnici}} je spodletelo: $1 {{PLURAL:$3|Uporabniško ime in geslo sta navedena spodaj.|Seznam uporabniških imen in gesel je naveden spodaj.}}",
+       "passwordreset-emailsent-capture2": "Poslali smo {{PLURAL:$1|e-pošto|e-pošti|e-pošte}} za ponastavitev gesla. {{PLURAL:$1|Uporabniško ime in geslo sta navedena tukaj.|Seznam uporabniških imen in gesel je naveden tukaj.}}",
+       "passwordreset-emailerror-capture2": "Pošiljanje e-pošte {{GENDER:$2|uporabniku|uporabnici}} je spodletelo: $1 {{PLURAL:$3|Uporabniško ime in geslo sta navedena tukaj.|Seznam uporabniških imen in gesel je naveden tukaj.}}",
        "passwordreset-nocaller": "Podati morate klicatelja",
        "passwordreset-nosuchcaller": "Klicatelj ne obstaja: $1",
        "passwordreset-ignored": "Ponastavitve gesla nismo izvedli. Morda ni nastavljen noben ponudnik?",
        "searchprofile-advanced-tooltip": "Iskanje v imenskih prostorih po meri",
        "search-result-size": "$1 ({{PLURAL:$2|1 beseda|2 besedi|$2 besede|$2 besed|$2 besed}})",
        "search-result-category-size": "$1 {{PLURAL:$1|član|člana|člani|članov}} ($1 {{PLURAL:$2|podkategorija|podkategoriji|podkategorije|podkategorij}}, $1 {{PLURAL:$3|datoteka|datoteki|datoteke|datotek}})",
-       "search-redirect": "(preusmeritev $1)",
+       "search-redirect": "(preusmeritev s strani $1)",
        "search-section": "(razdelek $1)",
        "search-category": "(kategorija $1)",
        "search-file-match": "(ujema se z vsebino datoteke)",
        "upload-dialog-disabled": "Nalaganj datotek z uporabo tega obrazca je na wikiju onemogočeno.",
        "upload-dialog-title": "Naloži datoteko",
        "upload-dialog-button-cancel": "Prekliči",
+       "upload-dialog-button-back": "Nazaj",
        "upload-dialog-button-done": "Končano",
        "upload-dialog-button-save": "Shrani",
        "upload-dialog-button-upload": "Naloži",
        "apisandbox-results-fixtoken-fail": "Pridobivanje žetona »$1« je spodletelo.",
        "apisandbox-alert-page": "Polja na strani niso veljavna.",
        "apisandbox-alert-field": "Vrednost polja ni veljavna.",
+       "apisandbox-continue": "Nadaljuj",
+       "apisandbox-continue-clear": "Počisti",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} bo [https://www.mediawiki.org/wiki/API:Query#Continuing_queries nadaljevalo] zadnjo zahtevo; {{int:apisandbox-continue-clear}} bo počistilo parametre, povezane z nadaljevanjem.",
        "booksources": "Viri knjig",
        "booksources-search-legend": "Išči knjižne vire",
        "booksources-search": "Išči",
        "htmlform-cloner-create": "Dodaj več",
        "htmlform-cloner-delete": "Odstrani",
        "htmlform-cloner-required": "Zahtevana je vsaj ena vrednost.",
+       "htmlform-date-placeholder": "LLLL-MM-DD",
+       "htmlform-time-placeholder": "UU:MM:SS",
+       "htmlform-datetime-placeholder": "LLLL-MM-DD UU:MM:SS",
+       "htmlform-date-invalid": "Navedena vrednost ni prepoznan datum. Poskusite uporabiti obliko LLLL-MM-DD.",
+       "htmlform-time-invalid": "Navedena vrednost ni prepoznan čas. Poskusite uporabiti obliko UU:MM:SS.",
+       "htmlform-datetime-invalid": "Navedena vrednost ni prepoznan datum in čas. Poskusite uporabiti obliko LLLL-MM-DD UU:MM:SS.",
+       "htmlform-date-toolow": "Navedena vrednost je časovno pred najzgodnejšim dovoljenim datumom $1.",
+       "htmlform-date-toohigh": "Navedena vrednost je časovno po najpoznejšim dovoljenim datumom $1.",
+       "htmlform-time-toolow": "Navedena vrednost je časovno pred najzgodnejšim dovoljenim časom $1.",
+       "htmlform-time-toohigh": "Navedena vrednost je časovno po najpoznejšim dovoljenim časom $1.",
+       "htmlform-datetime-toolow": "Navedena vrednost je časovno pred najzgodnejšim dovoljenim datumom in časom $1.",
+       "htmlform-datetime-toohigh": "Navedena vrednost je časovno po najpoznejšim dovoljenim datumom in časom $1.",
        "htmlform-title-badnamespace": "[[:$1]] ni v imenskem prostoru »{{ns:$2}}«.",
        "htmlform-title-not-creatable": "»$1« je naslov strani, ki ga ni mogoče ustvariti",
        "htmlform-title-not-exists": "$1 ne obstaja.",
        "htmlform-user-not-exists": "<strong>$1</strong> ne obstaja.",
        "htmlform-user-not-valid": "<strong>$1</strong> ni veljavno uporabniško ime.",
-       "sqlite-has-fts": "$1 s podporo iskanju polnih besedil",
-       "sqlite-no-fts": "$1 brez podpore iskanju polnih besedil",
        "logentry-delete-delete": "$1 je {{GENDER:$2|izbrisal|izbrisala|izbrisal(-a)}} stran $3",
        "logentry-delete-restore": "$1 je {{GENDER:$2|obnovil|obnovila|obnovil(-a)}} stran $3",
        "logentry-delete-event": "$1 je {{GENDER:$2|spremenil|spremenila|spremenil(-a)}} vidljivost $5 {{PLURAL:$5|dnevniškega dogodka|dnevniških dogodkov}} na $3: $4",
        "feedback-external-bug-report-button": "Vloži tehnično opravilo",
        "feedback-dialog-title": "Pošljite povratne informacije",
        "feedback-dialog-intro": "Spodnji enostavni obrazec lahko uporabite za pošiljanje povratnih informacij. Vašo pripombo bomo dodali na stran »$1« skupaj z vašim uporabniškim imenom.",
-       "feedback-error-title": "Napaka",
        "feedback-error1": "Napaka: Neznan rezultat iz API",
        "feedback-error2": "Napaka: Urejanje je spodletelo",
        "feedback-error3": "Napaka: Ni odgovora od API",
        "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 fce5916..d197721 100644 (file)
        "nstab-special": "Bogga khaaska ah",
        "nstab-project": "Bogga mashruuca",
        "nstab-image": "Gal",
-       "nstab-mediawiki": "Fariin",
+       "nstab-mediawiki": "Farriin",
        "nstab-template": "Tusmo",
        "nstab-help": "Bogga caawinaada",
        "nstab-category": "Qeybta",
        "yourpasswordagain": "Markale qor ereysirka:",
        "createacct-yourpasswordagain": "Hubi erey sirta",
        "createacct-yourpasswordagain-ph": "Mar kale gali erey sirta",
-       "remembermypassword": "Kumbuyuutarkaan ku xasuusnaaw magaceyga gudagalka (ilaa  $1 {{PLURAL:$1|maalin|maalmood}})",
        "userlogin-remembermypassword": "Ka dhig mid diyaar ah mar waliba",
        "userlogin-signwithsecure": "Adeegso khad la hubo",
        "yourdomainname": "Magacaga shabakada",
        "eauthentsent": "Xaqiijin e-mail ah ayaa loo  diray e-mailkan aad dooratay.\nIntii aadan wax e-mail ah lagu soo dirin koontada, waa in aad daba kacdaa waxa kuugu jiro e-mailka, si aad u xaqiijisid  in aad adiga  leedahay akoon'ka.",
        "mailerror": "Qalad dirida e-mailka: $1",
        "acct_creation_throttle_hit": "Dadka soo booqanaayo wiki:gaan oo isticmaalaayo cinwaankaaga IP:ka waxay sameeyeen {{PLURAL:$1|1 magac gudagale ah|$1 magac  gudagalayaal ah}} maalintii ugu dambeysay, taasina waa inta ugu badan ee la'ogolyahay muddadaas. Sidaas daraadeed, booqoshadayaasha isticmaalaya cinwaankaan-IP:ka hadda ma'sameysankaraan magac gudagale danbe.",
-       "emailauthenticated": "E-mailkaada waxaa lagu xaqiijiyay $2 markey ahayd $3.",
+       "emailauthenticated": "E-mailkaada waxaa la xaqiijiyay $2 saacadduna ahayd $3.",
        "emailnotauthenticated": "Ciwaankaada e-mailka weli lama xaqiijinin.\nWax e-mail ah oo ku saabsan arrimaha soo socdo looma soo diridoono.",
        "emailconfirmlink": "Soo xaqiiji ciwaankaada e-mailka",
        "invalidemailaddress": "e-mailkaan lama ogolaan karo ayada oo ku ku jirto format la aqoonsan..\nFadlan ku qor ciwaan leh format sax ah ama ebar ka dhig  meesha.",
        "stub-threshold-sample-link": "tusaale",
        "stub-threshold-disabled": "Howlgab",
        "recentchangesdays": "Tirada maalmaha lagu tusaayo isbedelada dhow:",
+       "recentchangescount": "Tirada isbedellada guud ahaan muuqda:",
        "savedprefs": "Dooqyadaada waa la keydiyey.",
        "timezonelegend": "Soonaha waqtiga:",
        "localtime": "Waqtigaaga",
        "yournick": "Saxiix cusub:",
        "prefs-help-signature": "Waan in la saxiixaa wadahdalada ku dhaca bogga wadhadalka adigoo adeegsaanaya \"<nowiki>~~~~</nowiki>\", kaasoo u rogi doona saxiixa iyo waqtiga.",
        "badsiglength": "Naaneysta aad bey u dheertahay.\nWaa in aysan ka badanin $1 {{PLURAL:$1|eray|erayo}}.",
-       "yourgender": "Jinsi:",
-       "gender-unknown": "Aana la qeexin",
-       "gender-male": "Lab",
-       "gender-female": "Dhedig",
+       "yourgender": "Qaabkee baad doonayssaa in laguu qeeqo?",
+       "gender-unknown": "Haddii lagu soo hadal qaado, barnaamijka waxa uu adeegsan doonaa ereyo nooc dhexdhexaad ah goor weliba ay suuragal tahay",
+       "gender-male": "Wuxuu wax ka bedelaa bogagga wikiga",
+       "gender-female": "Waxa ay wax ka bedeshaa bogagga wikipedia",
        "prefs-help-gender": "Ahaysiinta xulashaan waa mid dooqasha ah.\nBarnaamijkaan adeegsigiisa waxay qiima u yeelaysaa wadahadalkaada iyo kuwa kale iyadoo bedelaysa habka qofka sida lab ama dheddig.\nMacluumadkaan waa mid la wada arkayo.",
        "email": "E-mail",
-       "prefs-help-realname": "Optional: if you choose to provide it this will be used for giving you attribution for your work.",
+       "prefs-help-realname": "Magaca runta ah waa dooqasho.\nHaddii aad doorato in aan keento, waxaa loo adeegsan karaa mid tilmaama howshaada.",
        "prefs-help-email": "E-mail waa wax aad xor u leedahay. laakiin waa loo baahanyahay hadii aad eraysirka badaleesid, hadii aad ilaawdo eraygaaga sirta ah",
        "prefs-help-email-others": "Waxaa kale oo aad u isticmaali kartaa in ee dadka kale kugula soo xiriiraan e-mail ayaga oo isticmaalaayo linki isticmaalahaada ama bogga wadahadalka.\nE-mailkaada mala sheegaayo markii ee dadka kale kula soo xiriirayaan.",
        "prefs-help-email-required": "Waxaa loo baahanyahay e-mail.",
        "prefs-info": "Macluumaadka asaasiga ah",
        "prefs-i18n": "Caalamiyeen",
        "prefs-signature": "Saxiixa",
+       "prefs-advancedediting": "Xulasho guud",
        "prefs-advancedrc": "Xulasho horumarin ah",
+       "prefs-displayrc": "Dooga bandhigista",
        "prefs-diffs": "Farqiga",
        "prefs-help-prefershttps": "Waxaa lahagaajin doonaan dooqaan marka xiga ee aad soo gasho",
        "saveusergroups": "Kaydi kooxaha isticmaalayaasha",
index b04c775..4ce76d9 100644 (file)
        "invalid-content-data": "Неисправни подаци садржаја",
        "content-not-allowed-here": "Садржај модела „$1“ није дозвољен на страници [[$2]]",
        "editwarning-warning": "Ако напустите ову страницу, изгубићете све измене које сте направили. Ако сте пријављени, можете онемогућити ово упозорење у својим подешавањима, у одељку „{{int:prefs-editing}}“.",
+       "editpage-invalidcontentmodel-title": "Модел садржаја није подржан",
+       "editpage-invalidcontentmodel-text": "Модел садржаја „$1“ није подржан.",
        "editpage-notsupportedcontentformat-title": "Формат садржаја није подржан",
        "editpage-notsupportedcontentformat-text": "Формат садржаја $1 није подржан за модел садржаја $2.",
        "content-model-wikitext": "викитекст",
        "searchprofile-advanced-tooltip": "Претражите прилагођене именске просторе",
        "search-result-size": "$1 ({{PLURAL:$2|1 реч|$2 речи|$2 речи}})",
        "search-result-category-size": "{{PLURAL:$1|1 члан|$1 члана|$1 чланова}}, ({{PLURAL:$2|1 поткатегорија|$2 поткатегорије|$2 поткатегорија}}, {{PLURAL:$3|1 датотека|$3 датотеке|$3 датотека}})",
-       "search-redirect": "(преусмерење $1)",
+       "search-redirect": "(преусмерено са $1)",
        "search-section": "(одељак $1)",
        "search-category": "(категорија $1)",
        "search-file-match": "(подудара се садржај датотеке)",
        "apisandbox-reset": "Очисти",
        "apisandbox-results": "Резултати",
        "apisandbox-request-url-label": "Адреса захтева:",
+       "apisandbox-continue": "Настави",
+       "apisandbox-continue-clear": "Очисти",
        "booksources": "Штампани извори",
        "booksources-search-legend": "Тражи књижевне изворе",
        "booksources-isbn": "ISBN:",
        "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": "Ð\94ео ÐµÐºÑ\81Ñ\82ензиÑ\98е",
+       "tags-source-extension": "Ð\94ео Ð\9cедиÑ\98авикиÑ\98а",
        "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": "$1 је {{GENDER:$2|обрисао|обрисала}} страницу $3",
        "logentry-delete-restore": "$1 је {{GENDER:$2|вратио|вратила}} страницу $3",
        "logentry-delete-event": "$1 је {{GENDER:$2|променио|променила}} видљивост {{PLURAL:$5|1=догађаја|$5 догађаја}} у дневнику $3: $4",
        "feedback-cancel": "Откажи",
        "feedback-close": "Урађено",
        "feedback-external-bug-report-button": "Пријави баг",
-       "feedback-error-title": "Грешка",
        "feedback-error1": "Грешка: непрепознат резултат од АПИ-ја",
        "feedback-error2": "Грешка: уређивање није успело",
        "feedback-error3": "Грешка: нема одговора од АПИ-ја",
index 155e8c6..76545f0 100644 (file)
        "yourpasswordagain": "Potvrda lozinke:",
        "createacct-yourpasswordagain": "Potvrdite lozinku",
        "createacct-yourpasswordagain-ph": "Unesite lozinku još jednom",
-       "remembermypassword": "Zapamti me na ovom pregledaču (najduže $1 {{PLURAL:$1|dan|dana}})",
        "userlogin-remembermypassword": "Ostavi me prijavljenog/u",
        "userlogin-signwithsecure": "Koristite sigurnu konekciju",
        "yourdomainname": "Domen:",
        "searchprofile-advanced-tooltip": "Pretražite prilagođene imenske prostore",
        "search-result-size": "$1 ({{PLURAL:$2|1 reč|$2 reči|$2 reči}})",
        "search-result-category-size": "{{PLURAL:$1|1 član|$1 člana|$1 članova}}, ({{PLURAL:$2|1 potkategorija|$2 potkategorije|$2 potkategorija}}, {{PLURAL:$3|1 datoteka|$3 datoteke|$3 datoteka}})",
-       "search-redirect": "(preusmerenje $1)",
+       "search-redirect": "(preusmereno sa $1)",
        "search-section": "(odeljak $1)",
        "search-category": "(kategorija $1)",
        "search-file-match": "(podudara se sadržaj datoteke)",
        "tags-actions-header": "Radnje",
        "tags-active-yes": "Da",
        "tags-active-no": "Ne",
-       "tags-source-extension": "Deo ekstenzije",
+       "tags-source-extension": "Deo Medijavikija",
        "tags-source-manual": "Ručno je dodaju korisnici i botovi",
        "tags-source-none": "Van upotrebe",
        "tags-edit": "uredi",
        "htmlform-title-not-exists": "$1 ne postoji.",
        "htmlform-user-not-exists": "<strong>$1</strong> ne postoji.",
        "htmlform-user-not-valid": "<strong>$1</strong> nije ispravno korisničko ime.",
-       "sqlite-has-fts": "$1 s podrškom pretrage celog teksta",
-       "sqlite-no-fts": "$1 bez podrške pretrage celog teksta",
        "logentry-delete-delete": "$1 je {{GENDER:$2|obrisao|obrisala}} stranicu $3",
        "logentry-delete-restore": "$1 je {{GENDER:$2|vratio|vratila}} stranicu $3",
        "logentry-delete-event": "$1 je {{GENDER:$2|promenio|promenila}} vidljivost {{PLURAL:$5|1=događaja|$5 događaja}} u dnevniku $3: $4",
        "feedback-cancel": "Otkaži",
        "feedback-close": "Urađeno",
        "feedback-external-bug-report-button": "Prijavi bag",
-       "feedback-error-title": "Greška",
        "feedback-error1": "Greška: neprepoznat rezultat od API-ja",
        "feedback-error2": "Greška: uređivanje nije uspelo",
        "feedback-error3": "Greška: nema odgovora od API-ja",
index 3ea3fc4..b32f6ba 100644 (file)
        "tog-enotifminoredits": "Skicka mig e-post även för mindre ändringar av sidor och filer",
        "tog-enotifrevealaddr": "Visa min e-postadress i e-postmeddelanden om ändringar som skickas till andra",
        "tog-shownumberswatching": "Visa antalet användare som bevakar",
-       "tog-oldsig": "Nuvarande signatur:",
+       "tog-oldsig": "Din nuvarande signatur:",
        "tog-fancysig": "Behandla signatur som wikitext (utan en automatisk länk)",
        "tog-uselivepreview": "Använd direktuppdaterad förhandsgranskning",
        "tog-forceeditsummary": "Påminn mig om jag inte fyller i en redigeringskommentar",
        "tog-showhiddencats": "Visa dolda kategorier",
        "tog-norollbackdiff": "Visa inte diff efter tillbakarullning",
        "tog-useeditwarning": "Varna mig om jag lämnar en redigeringssida där jag gjort ändringar men inte sparat.",
-       "tog-prefershttps": "Använd alltid en säker anslutning när jag är inloggad",
+       "tog-prefershttps": "Använd alltid en säker anslutning medan jag är inloggad",
        "underline-always": "Alltid",
        "underline-never": "Aldrig",
        "underline-default": "Webbläsarens eller utseendets standardinställning",
        "newwindow": "(öppnas i ett nytt fönster)",
        "cancel": "Avbryt",
        "moredotdotdot": "Mer...",
-       "morenotlisted": "Denna lista är inte fullständig.",
+       "morenotlisted": "Denna lista är kanske inte fullständig.",
        "mypage": "Sida",
        "mytalk": "Diskussion",
        "anontalk": "Diskussion",
        "talk": "Diskussion",
        "views": "Visningar",
        "toolbox": "Verktyg",
+       "tool-link-userrights": "Ändra {{GENDER:$1|användargrupper}}",
+       "tool-link-emailuser": "Skicka e-post till denna {{GENDER:$1|användare}}",
        "userpage": "Visa användarsida",
        "projectpage": "Visa projektsida",
        "imagepage": "Visa filsida",
        "eauthentsent": "Ett e-postmeddelande för bekräftelse har skickats till den angivna e-postadressen.\nInnan någon annan e-post kan skickas till kontot, måste du följa instruktionerna i e-postmeddelandet för att bekräfta att kontot verkligen är ditt.",
        "throttled-mailpassword": "En lösenordsåterställning har redan skickats för mindre än {{PLURAL:$1|en timme|$1 timmar}} sedan.\nFör att förhindra missbruk skickas bara en lösenordsåterställning per {{PLURAL:$1|timme|$1-timmarsperiod}}.",
        "mailerror": "Fel vid skickande av e-post: $1",
-       "acct_creation_throttle_hit": "Besökare till den här wikin som har använt din IP-adress har skapat {{PLURAL:$1|ett användarkonto|$1 användarkonton}} under det senaste dygnet, vilket är det maximalt tillåtna inom den tidsperioden.\nSom ett resultat kan besökare som använder den här IP-adressen inte skapa några fler användarkonton just nu.",
+       "acct_creation_throttle_hit": "Besökare till den här wikin som har använt din IP-adress har skapat {{PLURAL:$1|ett användarkonto|$1 användarkonton}} under det senaste $2, vilket är det maximalt tillåtna inom den tidsperioden.\nSom ett resultat kan besökare som använder den här IP-adressen inte skapa några fler användarkonton just nu.",
        "emailauthenticated": "Din e-postadress bekräftades den $2 kl. $3.",
        "emailnotauthenticated": "Din e-postadress är ännu inte bekräftad. Ingen e-post kommer att skickas vad gäller det följande funktionerna.",
        "noemailprefs": "Uppge en e-postadress i dina inställningar för att få dessa funktioner att fungera.",
        "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?",
        "botpasswords-updated-body": "Botlösenordet för botnamnet \"$1\" till användaren \"$2\" uppdaterades.",
        "botpasswords-deleted-title": "Botlösenord raderades",
        "botpasswords-deleted-body": "Botlösenordet för botnamnet \"$1\" till användaren \"$2\" raderades.",
-       "botpasswords-newpassword": "Det nya lösenordet att logga in för <strong>$1</strong> är <strong>$2</strong>. <em>Spara detta som framtida referens.</em>",
+       "botpasswords-newpassword": "Det nya lösenordet att logga in för <strong>$1</strong> är <strong>$2</strong>. <em>Spara detta som framtida referens.</em> <br> (För äldre botar som kräver att inloggningsnamnet är detsamma som det eventuella användarnamnet kan du även använda <strong>$3</strong> som användarnamn och <strong>$4</strong> som lösenord.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider är inte tillgänglig.",
        "botpasswords-restriction-failed": "Begränsningar av botlösenord tillåter inte denna inloggning.",
        "botpasswords-invalid-name": "Det angivna användarnamnet innehåller inte separatorn för botlösenord (\"$1\").",
        "passwordreset-emailelement": "Användarnamn: \n$1\n\nTillfälligt lösenord: \n$2",
        "passwordreset-emailsentemail": "Om denna e-postadress är associerad med ditt konto kommer en lösenordsåterställning skickas via e-post.",
        "passwordreset-emailsentusername": "Om det finns en e-postadress som associeras med detta användarnamn kommer en lösenordsåterställning skickas via e-post.",
-       "passwordreset-emailsent-capture2": "{{PLURAL:$1|E-postmeddelande|E-postmeddelanden}} för återställning av lösenord har skickats. {{PLURAL:$1|Användarnamnet och lösenordet|Listan över användarnamn och lösenord}} visas nedan.",
-       "passwordreset-emailerror-capture2": "Kunde inte skicka e-post till {{GENDER:$2|användaren}}: $1 {{PLURAL:$3|Användarnamnet och lösenordet|Listan över användarnamn och lösenord}} listas nedan.",
+       "passwordreset-emailsent-capture2": "{{PLURAL:$1|E-postmeddelande|E-postmeddelanden}} för återställning av lösenord har skickats. {{PLURAL:$1|Användarnamnet och lösenordet|Listan över användarnamn och lösenord}} visas här.",
+       "passwordreset-emailerror-capture2": "Kunde inte skicka e-post till {{GENDER:$2|användaren}}: $1 {{PLURAL:$3|Användarnamnet och lösenordet|Listan över användarnamn och lösenord}} listas här.",
        "passwordreset-nocaller": "En användare måste anges",
        "passwordreset-nosuchcaller": "Användare finns inte: $1",
        "passwordreset-ignored": "Lösenordsåterställningen hanterades inte. Kanske ingen leverantör har konfigurerats?",
        "invalid-content-data": "Ogiltig innehållsdata",
        "content-not-allowed-here": "innehåll av \"$1\" är inte tillåtet på sidan [[$2]]",
        "editwarning-warning": "Om du lämnar den här sidan kommer du att förlora alla ändringar du har gjort.\nOm du är inloggad kan du slå av den här varningen under \"{{int:prefs-editing}}\" i dina inställningar.",
+       "editpage-invalidcontentmodel-title": "Innehållsmodellen stöds inte",
+       "editpage-invalidcontentmodel-text": "Innehållsmodellen \"$1\" stöds inte.",
        "editpage-notsupportedcontentformat-title": "Innehållsformat stöds inte",
        "editpage-notsupportedcontentformat-text": "Innehållsformatet $1 stöds inte av innehållsmodellen $2.",
        "content-model-wikitext": "wikitext",
        "searchprofile-advanced-tooltip": "Sök i vissa namnrymder",
        "search-result-size": "$1 ({{PLURAL:$2|1 ord|$2 ord}})",
        "search-result-category-size": "{{PLURAL:$1|1 medlem|$1 medlemmar}} ({{PLURAL:$2|1 underkategori|$2 underkategorier}}, {{PLURAL:$3|1 fil|$3 filer}})",
-       "search-redirect": "(omdirigering $1)",
+       "search-redirect": "(omdirigering från $1)",
        "search-section": "(avsnitt $1)",
        "search-category": "(kategorin $1)",
        "search-file-match": "(överensstämmer filens innehåll)",
        "upload-dialog-disabled": "Filuppladdningar med denna dialogruta har inaktiverats på denna wiki.",
        "upload-dialog-title": "Ladda upp fil",
        "upload-dialog-button-cancel": "Avbryt",
+       "upload-dialog-button-back": "Tillbaka",
        "upload-dialog-button-done": "Klar",
        "upload-dialog-button-save": "Spara",
        "upload-dialog-button-upload": "Ladda upp",
        "apisandbox-results-fixtoken-fail": "Misslyckades att hämta nyckeln \"$1\".",
        "apisandbox-alert-page": "Fälten på denna sida är inte giltiga.",
        "apisandbox-alert-field": "Värdet i detta fält är inte giltigt.",
+       "apisandbox-continue": "Fortsätt",
+       "apisandbox-continue-clear": "Rensa",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} kommer att [https://www.mediawiki.org/wiki/API:Query#Continuing_queries fortsätta] den sista begäran; {{int:apisandbox-continue-clear}} kommer att rensa fortsättningsrelaterade parametrar.",
        "booksources": "Bokkällor",
        "booksources-search-legend": "Sök efter bokkällor",
        "booksources-search": "Sök",
        "tag-filter": "Filter för [[Special:Tags|märken]]:",
        "tag-filter-submit": "Filter",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Märke|Märken}}]]: $2)",
+       "tag-mw-contentmodelchange": "ändring av innehållsmodell",
+       "tag-mw-contentmodelchange-description": "Redigeringar som [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel ändrar innehållsmodellen] för en sida",
        "tags-title": "Märken",
        "tags-intro": "Denna sida listar de taggar som mjukvaran kan markera en redigering med, och deras betydelse.",
        "tags-tag": "Märkesnamn",
        "tags-actions-header": "Handlingar",
        "tags-active-yes": "Ja",
        "tags-active-no": "Nej",
-       "tags-source-extension": "Definieras av ett tillägg",
+       "tags-source-extension": "Definieras av programvaran",
        "tags-source-manual": "Används manuellt av användare och robotar",
        "tags-source-none": "Används inte längre",
        "tags-edit": "redigera",
        "htmlform-cloner-create": "Lägg till mer",
        "htmlform-cloner-delete": "Ta bort",
        "htmlform-cloner-required": "Det krävs minst ett värde.",
+       "htmlform-date-placeholder": "ÅÅÅÅ-MM-DD",
+       "htmlform-time-placeholder": "TT:MM:SS",
+       "htmlform-datetime-placeholder": "ÅÅÅÅ-MM-DD TT:MM:SS",
+       "htmlform-date-invalid": "Värdet du angav är inte ett igenkänt datum. Prova att använda formatet ÅÅÅÅ-MM-DD.",
+       "htmlform-time-invalid": "Värdet du angav är inte en igenkänd tid. Prova att använda formatet HH:MM:SS.",
+       "htmlform-datetime-invalid": "Värdet du angav är inte ett igenkänt datum och tid. Prova att använda formatet ÅÅÅÅ-MM-DD HH:MM:SS.",
+       "htmlform-date-toolow": "Värdet du angav är innan det tidigaste tillåtna datumet för $1.",
+       "htmlform-date-toohigh": "Värdet du angav är efter det senaste tillåtna datumet $1.",
+       "htmlform-time-toolow": "Värdet du angav är innan den tidigaste tillåtna tiden för $1.",
+       "htmlform-time-toohigh": "Värdet du angav är efter den senaste tillåtna tiden $1.",
+       "htmlform-datetime-toolow": "Värdet du angav är innan det tidigaste tillåtna datumet och tiden för $1.",
+       "htmlform-datetime-toohigh": "Värdet du angav är efter det senaste tillåtna datumet och tiden $1.",
        "htmlform-title-badnamespace": "[[:$1]] är inte i \"{{ns:$2}}\"-namnrymden.",
        "htmlform-title-not-creatable": "\"$1\" är inte en sidtitel som kan skapas",
        "htmlform-title-not-exists": "$1 finns inte.",
        "htmlform-user-not-exists": "<strong>$1</strong> finns inte.",
        "htmlform-user-not-valid": "<strong>$1</strong> är inte ett giltigt användarnamn.",
-       "sqlite-has-fts": "$1 med stöd för fulltextsökning",
-       "sqlite-no-fts": "$1 utan stöd för fulltextsökning",
        "logentry-delete-delete": "$1 {{GENDER:$2|raderade}} sidan $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|återställde}} sidan $3",
        "logentry-delete-event": "$1 {{GENDER:$2|ändrade}} synligheten för {{PLURAL:$5|en logghändelse|$5 logghändelser}} på $3: $4",
        "feedback-external-bug-report-button": "Registrera en teknisk uppgift",
        "feedback-dialog-title": "Skicka återkoppling",
        "feedback-dialog-intro": "Du kan använda det enkla formuläret nedan för att skicka in din återkoppling. Din kommentar kommer att läggas till på sidan \"$1\" tillsammans med ditt användarnamn.",
-       "feedback-error-title": "Fel",
        "feedback-error1": "Fel: Okänt resultat från API",
        "feedback-error2": "Fel: Redigeringen misslyckades",
        "feedback-error3": "Fel: Inget svar från API",
        "unlinkaccounts-success": "Kontot avlänkades.",
        "authenticationdatachange-ignored": "Ändringen av autentiseringsdata hanterades inte. Kanske ingen tillhandahållare har konfigurerats?",
        "userjsispublic": "Observera: JavaScript-undersidor bör inte innehålla konfidentiella uppgifter eftersom de kan ses av andra användare.",
-       "usercssispublic": "Observera: CSS-undersidor bör inte innehålla konfidentiella uppgifter eftersom de kan ses av andra användare."
+       "usercssispublic": "Observera: CSS-undersidor bör inte innehålla konfidentiella uppgifter eftersom de kan ses av andra användare.",
+       "restrictionsfield-badip": "Ogiltig IP-adress eller intervall: $1",
+       "restrictionsfield-label": "Tillåtna IP-intervall:",
+       "restrictionsfield-help": "En IP-adress eller CIDR-intervall per rad. För att aktivera allting, använd<br><code>0.0.0.0/0</code><br><code>::/0</code>"
 }
index 7c33f6a..c065fce 100644 (file)
        "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": "$3 பக்கத்தை $1 {{GENDER:$2|நீக்கினார்}}",
        "logentry-delete-restore": "$3 பக்கத்தை $1 {{GENDER:$2|மீட்டமைத்தார்}}",
        "logentry-delete-event": "$3 :$4 இல் {{PLURAL:$5| ஒரு நிகழ்வு குறிப்பேட்டின்| $5  நிகழ்வுகள் குறிப்பேடுகளின்}} காட்சித்தன்மை $1 மாற்றினார்",
index 1f7fa45..315a732 100644 (file)
        "recentchangeslinked-summary": "ಒಂಜಿ ನಿರ್ದಿಸ್ಟೊ ಪುಟೊರ್ದು (ಅತ್ತ್’ನ್ಡ ನಿರ್ದಿಸ್ಟೊ ವರ್ಗೊಗು ಸೇರ್ದಿನ ಪುಟೊಲೆರ್ದ್) ಸಂಪರ್ಕೊ ಉಪ್ಪುನ ಪುಟೊಲೆಡ್ ಇಂಚಿಪ ಮಲ್ತಿನಂಚಿನ ಬದಲಾವಣೆಲೆನ್ ತಿರ್ತ್ ಪಟ್ಟಿ ಮಲ್ಪೆರಾತ್ಂಡ್.\n[[Special:Watchlist|ಇರೆನ ವೀಕ್ಷಣೆ ಪಟ್ಟಿಡ್]] ಉಪ್ಪುನ ಪುಟೊಲು '''ದಪ್ಪ ಅಕ್ಷರೊಡು''' ಉಂಡು.",
        "recentchangeslinked-page": "ಪುಟೊತ ಪುದರ್:",
        "recentchangeslinked-to": "ಇಂದೆತ ಬದಲ್‍ಗ್ ಕೊರ್ತ್‍ನ ಪುಟೊಗು ಕೊಂಡಿ ಉಪ್ಪುನಂಚಿನ ಪುಟೊಲೆದ ಬದಲಾವಣೆಲೆನ್ ತೋಜಾವು",
-       "upload": "ಫೈಲ್ ಅಪ್ಲೋಡ್",
+       "upload": "ಫೈಲ್’ನ್ ಅಪ್ಲೋಡ್ ಮಲ್ಪುಲೆ",
        "uploadbtn": "ಫೈಲ್’ನ್ ಅಪ್ಲೋಡ್ ಮಲ್ಪುಲೆ",
        "uploadnologin": "ಲಾಗಿನ್ ಆತ್‘ಜ್ಜರ್",
        "uploadlogpage": "ಅಪ್ಲೋಡ್ ದಾಖಲೆ",
index b49742e..6ec9371 100644 (file)
        "talk": "చర్చ",
        "views": "చూపులు",
        "toolbox": "పనిముట్లు",
+       "tool-link-userrights": "{{GENDER:$1|వాడుకరి}} గుంపులను మార్చు",
+       "tool-link-emailuser": "ఈ {{GENDER:$1|వాడుకరికి}} ఈమెయిలు పంపు",
        "userpage": "వాడుకరి పేజీని చూడండి",
        "projectpage": "ప్రాజెక్టు పేజీని చూడు",
        "imagepage": "ఫైలు పేజీని చూడండి",
        "botpasswords-label-delete": "తొలగించు",
        "botpasswords-label-resetpassword": "సంకేతపదాన్ని మార్చు",
        "botpasswords-label-grants": "వర్తించే గ్రాంట్లు:",
-       "botpasswords-label-restrictions": "వాడుక పరిమితులు:",
        "botpasswords-label-grants-column": "గ్రాంటు చేసాం",
        "botpasswords-bad-appid": "బాట్ పేరు \"$1\" సరైనది కాదు.",
        "botpasswords-insert-failed": "బాట్ పేరు \"$1\"ను చేర్చలేకపోయాం. దీన్ని ఇంతకు ముందే చేర్చారా ఏంటి?",
        "htmlform-cloner-create": "ఇంకా చేర్చు",
        "htmlform-cloner-delete": "తొలగించు",
        "htmlform-cloner-required": "కనీసం ఒక విలువు అయినా ఇవ్వాలి.",
-       "sqlite-has-fts": "$1 పూర్తి-పాఠ్య అన్వేషణ తోడ్పాటుతో",
-       "sqlite-no-fts": "$1 పూర్తి-పాఠ్య అన్వేషణ తోడ్పాటు లేకుండా",
        "logentry-delete-delete": "$1 $3 పేజీని {{GENDER:$2|తొలగించారు}}",
        "logentry-delete-restore": "పేజీ $3 ని $1 {{GENDER:$2|పునస్థాపించారు}}",
        "logentry-delete-event": "$3 లో {{PLURAL:$5|ఒక లాగ్ ఘటన|$5 లాగ్ ఘటనల}} యొక్క కన్పట్టటాన్ని (విజిబిలిటీ) $1 {{GENDER:$2|మార్చారు}}: $4",
        "feedback-close": "పూర్తయ్యింది",
        "feedback-dialog-title": "ప్రతిస్పందనను తెలియజేయండి",
        "feedback-dialog-intro": "మీ ప్రతిస్పందనను తెలియజేయడానికి ఈ తేలిక ఫారాన్ని వాడుకోవచ్చు. మీ వాడుకరి పేరుతో పాటు మీ వ్యాఖ్య \"$1\" పేజీకి చేర్చబడుతుంది.",
-       "feedback-error-title": "లోపం",
        "feedback-error1": "లోపం: API నుండి గుర్తుపట్టలేని ఫలితం",
        "feedback-error2": "దోషము: సవరణ విఫలమైంది",
        "feedback-error3": "లోపం: API నుండి ప్రతిస్పందన లేదు",
index 115d81d..c339149 100644 (file)
@@ -17,7 +17,8 @@
                        "Leeheonjin",
                        "Macofe",
                        "Matma Rex",
-                       "Stranger195"
+                       "Stranger195",
+                       "Emem.calist"
                ]
        },
        "tog-underline": "Pagsasalungguhit ng link:",
        "yourpasswordagain": "Password mo uli:",
        "createacct-yourpasswordagain": "Tiyakin ang password",
        "createacct-yourpasswordagain-ph": "Muling ilagay ang password",
-       "remembermypassword": "Tandaan ang paglagda ko sa kompyuter na ito (pinakamarami na ang $1 {{PLURAL:$1|araw|mga araw}})",
        "userlogin-remembermypassword": "Panatilihin akong naka-login",
        "userlogin-signwithsecure": "Gumamit ng ligtas na koneksyon",
        "yourdomainname": "Dominyo mo:",
        "passwordreset-emailtext-user": "Ang tagagamit na si $1 sa {{SITENAME}} ay humiling ng isang paalala ng iyong mga akawnt ng detalye para sa {{SITENAME}}\n($4). Ang sumusunod na pangtagagamit na {{PLURAL:$3|akawnt ay|mga akawnt ay}} may kaugnayan sa tirahang ito ng e-liham:\n\n$2\n\n{{PLURAL:$3|Ang pansamantalang hudyat na ito|Ang pansamantalang mga hudyat na ito}} mawawalan ng bias sa loob ng {{PLURAL:$5|isang araw|$5 mga araw}}.\nDapat kang lumagda at pumili ng isang hudyat ngayon. Kung ibang tao ang gumawa ng kahilingang ito, o kung naalala mo na ang iyong orihinal na hudyat, at hindi mo na nais palitan pa ito, maaari mong huwag nang pansinin ang mensaheng ito at magpatuloy sa paggamit ng iyong lumang hudyat.",
        "passwordreset-emailelement": "Pangalan ng tagagamit: \n$1\n\nPansamantalang password: \n$2",
        "passwordreset-emailsentemail": "Naipadala na ang isang e-liham na pampaalala.",
-       "passwordreset-emailsent-capture": "Naipadala na ang isang e-liham na paalala, na ipinapakita sa ibaba.",
-       "passwordreset-emailerror-capture": "Nalikha na ang isang e-liham na paalala, na ipinapakita sa ibaba, subalit nabigo ang pagpapadala sa tagagamit: $1",
        "changeemail": "Baguhin ang direksiyong e-liham",
        "changeemail-header": "Baguhin ang email address ng account",
        "changeemail-no-info": "Kailangan mong lumagda upang tuwirang mapuntahan ang pahinang ito.",
        "undo-failure": "Hindi matanggal ang pagbabago dahil sa magkakasalungat na panggitnang mga pagbabago.",
        "undo-norev": "Hindi matanggal ang pagbabago dahil hindi ito umiiral o nabura na.",
        "undo-summary": "Tanggalin ang pagbabagong $1 ni [[Special:Contributions/$2|$2]] ([[User talk:$2|Usapan]])",
-       "cantcreateaccounttitle": "Hindi malikha ang account",
        "cantcreateaccount-text": "Hinarang ni [[User:$3|$3]] ang paglikha ng acciybt mula sa IP address ('''$1''') na ito.\n\nAng dahilang ibinigay ni $3 ay ''$2''",
        "viewpagelogs": "Tingnan ang mga pagtatala para sa pahinang ito",
        "nohistory": "Walang kasaysayan ng pagbabago para sa pahinang ito.",
        "apisandbox-results": "Kinalabasan",
        "apisandbox-request-url-label": "Hilingin ang URL:",
        "apisandbox-request-time": "Oras ng paghiling: $1",
+       "apisandbox-continue": "Ipagpatuloy",
+       "apisandbox-continue-clear": "Burado",
        "booksources": "Mga mapagkukunang aklat",
        "booksources-search-legend": "Maghanap ng mapagkukunang aklat",
        "booksources-isbn": "ISBN:",
        "htmlform-reset": "Bawiin ang mga pagbabago",
        "htmlform-selectorother-other": "Iba pa",
        "htmlform-title-not-exists": "Hindi nairal ang $1.",
-       "sqlite-has-fts": "$1 na may suportang paghahanap ng buong teksto",
-       "sqlite-no-fts": "$1 na walang suporta ng paghahanap ng buong teksto",
        "logentry-delete-delete": "Binura ni $1 ang pahinang $3",
        "logentry-delete-restore": "Ibinalik ni $1 ang pahinang $3",
        "logentry-delete-event": "Binago ni $1 ang antas ng pagkanatatanaw ng {{PLURAL:$5|isang pangyayari sa talaan|$5 mga pangyayari sa talaan}} sa $3: $4",
        "feedback-external-bug-report-button": "Mag-habla ng teknikal na gawain",
        "feedback-dialog-title": "I-sumite ang katugunan",
        "feedback-dialog-intro": "Maaari mo nang gamitin ang madaling pormularyo na nasa ibaba upang i-sumite ang iyong katugunan. Madadagdag ang iyong komento sa pahinang $1, kasama ang iyong ngalan-tagagamit.",
-       "feedback-error-title": "Kamalian",
        "feedback-error1": "Kamalian: Hindi nakikilalang kinalabasan mula sa API",
        "feedback-error2": "Kamalian: Nabigo ang pagpatnugot",
        "feedback-error3": "Kamalian: Walang tugon mula sa API",
        "special-characters-group-khmer": "Khmer",
        "mw-widgets-dateinput-placeholder-day": "TTTT-BB-AA",
        "mw-widgets-dateinput-placeholder-month": "TTTT-BB",
-       "api-error-blacklisted": "Paki pumili ng isang naiibang mapaglarawang pamagat.",
        "randomrootpage": "Alin mang pinag-ugatang/pinagmulang pahina"
 }
index 7e167ac..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ı?",
        "htmlform-title-not-exists": "$1 mevcut değil.",
        "htmlform-user-not-exists": "<strong>$1</strong> mevcut değil.",
        "htmlform-user-not-valid": "<strong>$1</strong> geçerli bir kullanıcı ismi değildir.",
-       "sqlite-has-fts": "$1 tam-metin arama desteği ile",
-       "sqlite-no-fts": "$1 tam-metin arama desteği olmaksızın",
        "logentry-delete-delete": "$1 $3 sayfasını {{GENDER:$2|sildi}}",
        "logentry-delete-restore": "$1 $3 sayfasını {{GENDER:$2|geri getirdi}}",
        "logentry-delete-event": "$1, $3 sayfasında {{PLURAL:$5|bir günlük girdisinin |$5 günlük girdisinin}} görünürlüğünü {{GENDER:$2|değiştirdi}}: $4",
index 0af6224..b7cadee 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": "Сезнең доменыгыз:",
        "password-change-forbidden": "Сез бу викидә серсүзне үзгәртә алмыйсыз.",
        "externaldberror": "Тышкы мәгълүмат базасы ярдәмендә аутентификация үткәндә хата чыкты, яисә тышкы хисап язмагызга үзгәрешләр кертү хокукыгыз юк.",
        "login": "Керү",
+       "login-security": "Шәхесегезне раслагыз",
        "nav-login-createaccount": "Керү / теркәлү",
        "userlogin": "Керү / теркәлү",
        "userloginnocreate": "Керү",
        "botpasswords-label-delete": "Бетерү",
        "botpasswords-label-resetpassword": "Серсүзне ташлау",
        "botpasswords-label-grants": "Кулланылган рөхсәтләр:",
-       "botpasswords-label-restrictions": "Куллану чикләүләре:",
        "botpasswords-label-grants-column": "Рөхсәт",
        "botpasswords-bad-appid": "Атамасы «$1» булган бот исеме ярамый.",
        "botpasswords-created-title": "Бот серсүзе булдырылды",
        "minoredit": "Бу кече үзгәртү",
        "watchthis": "Бу битне күзәтү",
        "savearticle": "Битне саклау",
+       "savechanges": "Үзгәртүләрне саклау",
+       "publishpage": "Бит ясау",
+       "publishchanges": "Битне бастыру",
        "preview": "Алдан карау",
        "showpreview": "Алдан карау",
        "showdiff": "Кертелгән үзгәртүләр",
        "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-copyrighted": "Автор хокуклары халәте:",
        "exif-copyrightowner": "Автор хокуклары иясе",
        "exif-usageterms": "Куллану шартлары",
-       "exif-orientation-1": "Ð\9dоÑ\80малÑ\8c",
+       "exif-orientation-1": "Ð\93адÓ\99Ñ\82и",
        "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-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 62dc255..66f355d 100644 (file)
@@ -65,7 +65,8 @@
                        "Dars",
                        "Mix Gerder",
                        "E.belykh",
-                       "Visem"
+                       "Visem",
+                       "MMH"
                ]
        },
        "tog-underline": "Підкреслювання посилань:",
        "talk": "Обговорення",
        "views": "Перегляди",
        "toolbox": "Інструменти",
+       "tool-link-userrights": "Змінити групи {{GENDER:$1|користувачів}}",
+       "tool-link-emailuser": "Надіслати електронного листа {{GENDER:$1|цьому користувачеві|цій користувачці}}",
        "userpage": "Переглянути сторінку користувача",
        "projectpage": "Переглянути сторінку проекту",
        "imagepage": "Переглянути сторінку файлу",
        "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": "Скидання пароля не відбулося. Можливо, не було налашатовано надавача?",
        "italic_tip": "Курсив",
        "link_sample": "Назва посилання",
        "link_tip": "Внутрішнє посилання",
-       "extlink_sample": "назва посилання http://www.example.com",
+       "extlink_sample": "http://www.example.com назва посилання",
        "extlink_tip": "Зовнішнє посилання (не забудьте про префікс http://)",
        "headline_sample": "Текст заголовка",
        "headline_tip": "Заголовок 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 {{GENDER:$2|вилучив|вилучила}} сторінку $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|відновив|відновила}} сторінку $3",
        "logentry-delete-event": "$1 {{GENDER:$2|змінив|змінила}} видимість {{PLURAL:$5 запису журналу|$5 записів журналу}} на $3: $4",
        "feedback-external-bug-report-button": "Повідомити про технічну проблему",
        "feedback-dialog-title": "Надіслати відгук",
        "feedback-dialog-intro": "Для надсилання відгуку Ви можете скористатись простою формою внизу. Ваш коментар буде залишений на сторінці «$1», разом із Вашим іменем користувача (або IP-адресою).",
-       "feedback-error-title": "Помилка",
        "feedback-error1": "Помилка: Невідомий результаті API",
        "feedback-error2": "Помилка: Збій редагувань",
        "feedback-error3": "Помилка: Немає відповіді від API",
        "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 95d8031..64accfa 100644 (file)
                        "Macofe",
                        "Hindustanilanguage",
                        "امین اکبر",
-                       "Jdforrester"
+                       "Jdforrester",
+                       "قیصرانی"
                ]
        },
        "tog-underline": "ربط کی خط کشیدگی:",
        "tog-hideminor": "حالیہ تبدیلیوں میں معمولی ترامیم چھپائیں",
-       "tog-hidepatrolled": "حالیہ تبدیلیوں میں گشتی ترامیم چھپائیں",
+       "tog-hidepatrolled": "حالیہ تبدیلیوں میں مراجعت شدہ ترامیم چھپائیں",
        "tog-newpageshidepatrolled": "جدید صفحات کی فہرست میں مراجعت شدہ صفحات چھپائیں",
        "tog-hidecategorization": "صفحات کی زمرہ بندی چھپائیں",
        "tog-extendwatchlist": "حالیہ ترین تبدیلیوں کی بجائے تمام تبدیلیاں دیکھنے کے لیے زیر نظر فہرست کو وسیع کریں",
        "tog-usenewrc": "حالیہ تبدیلیاں اور زیر نظر فہرست میں تبدیلیوں کو بلحاظ صفحہ گروہ بند کریں",
        "tog-numberheadings": "سرخیوں کو خودکار نمبر دیں",
        "tog-showtoolbar": "آلات ترمیم دکھائیں",
-       "tog-editondblclick": "دو کلک پر صفحات کی ترمیم کریں",
+       "tog-editondblclick": "دہرے کلک پر صفحات کی ترمیم کریں",
        "tog-editsectiononrightclick": "قطعہ کے عنوانات پر رائیٹ کلک کے ذریعے قطعہ کی ترمیم کاری فعال کریں",
        "tog-watchcreations": "میرے تخلیق کردہ صفحات اور اپلوڈ کردہ فائلوں کو میری زیر نظر فہرست میں شامل کریں",
        "tog-watchdefault": "میرے ترمیم شدہ صفحات اور فائلوں کو میری زیر نظر فہرست میں شامل کریں",
        "hidden-categories": "{{PLURAL:$1|پوشیدہ زمرہ|پوشیدہ زمرہ جات}}",
        "hidden-category-category": "پوشیدہ زمرہ جات",
        "category-subcat-count": "{{PLURAL:$2|اِس زمرہ میں محض درج ذیل ذیلی زمرہ موجود ہے.|اِس زمرہ میں کل $2 میں سے درج ذیل {{PLURAL:$1|ذیلی زمرہ|$1 ذیلی زمرہ جات}} موجود ہیں۔}}",
-       "category-subcat-count-limited": "اِس زمرہ میں درج ذیل {{PLURAL:$1|ذیلی زمرہ ہے|$1 ذیلی زمرہ جات ہیں}}.",
+       "category-subcat-count-limited": "اِس زمرہ میں درج ذیل {{PLURAL:$1|ذیلی زمرہ ہے|$1 ذیلی زمرہ جات ہیں}}۔",
        "category-article-count": "{{PLURAL:$2|اس زمرہ میں محض درج ذیل صفحہ موجود ہے۔|اس زمرہ کے کل $2 صفحات میں سے $1 {{PLURAL:$1|صفحہ|صفحات}} درج ذیل {{PLURAL:$1|ہے|ہیں}}}}۔",
        "category-article-count-limited": "درج ذیل {{PLURAL:$1|صفحہ|$1 صفحات}} اس زمرہ میں شامل {{PLURAL:$1|ہے|ہیں}}۔",
-       "category-file-count": "{{PLURAL:$2|اس Ø²Ù\85رÛ\81 Ù\85Û\8cÚº ØµØ±Ù\81 Ø¯Ø±Ø¬ Ø°Û\8cÙ\84 Ù\81ائÙ\84 Ù\85Ù\88جÙ\88د Û\81Û\92Û\94|اس Ø²Ù\85رÛ\81 Ú©Û\8c Ú©Ù\84 $2 Ù\81ائÙ\84Ù\88Úº Ù\85Û\8cÚº Ø³Û\92 $1 {{PLURAL:$1|Ù\81ائÙ\84\81ائÙ\84Û\8cÚº}} Ø¯Ø±Ø¬ ذیل {{PLURAL:$1|ہے|ہیں}}}}۔",
+       "category-file-count": "{{PLURAL:$2|اس Ø²Ù\85رÛ\81 Ù\85Û\8cÚº ØµØ±Ù\81 Ø¯Ø±Ø¬ Ø°Û\8cÙ\84 Ù\81ائÙ\84 Ù\85Ù\88جÙ\88د Û\81Û\92Û\94|اس Ø²Ù\85رÛ\81 Ú©Û\8c Ú©Ù\84 $2 Ù\81ائÙ\84Ù\88Úº Ù\85Û\8cÚº Ø³Û\92 $1 {{PLURAL:$1|Ù\81ائÙ\84\81ائÙ\84Û\8cÚº}} Ø­Ø³Ø¨ ذیل {{PLURAL:$1|ہے|ہیں}}}}۔",
        "category-file-count-limited": "درج ذیل {{PLURAL:$1|فائل|$1 فائلیں}} اس زمرہ میں شامل {{PLURAL:$1|ہے|ہیں}}۔",
        "listingcontinuesabbrev": "جاری۔",
        "index-category": "فہرست شدہ صفحات",
        "noindex-category": "غیر فہرست شدہ صفحات",
-       "broken-file-category": "صفحات مع شکستہ فائل روابط",
+       "broken-file-category": "فائل کے شکستہ روابط کے حامل صفحات",
        "categoryviewer-pagedlinks": "($1) ($2)",
        "about": "تعارف",
        "article": "صفحہ مواد",
        "qbpageoptions": "یہ صفحہ",
        "qbmyoptions": "میرے صفحات",
        "faq": "عام طور پر پوچھے جانے والے سوالات",
-       "faqpage": "Project:معلوماتِ عامہ",
-       "actions": "ایکشنز",
-       "namespaces": "جائے نام",
+       "faqpage": "Project:عمومی سوالات",
+       "actions": "اقدامات",
+       "namespaces": "نام فضا",
        "variants": "متغیرات",
-       "navigation-heading": "Ù\82ائÙ\85Û\81 رہنمائی",
-       "errorpagetitle": "خطاء",
-       "returnto": "واپس $1۔",
+       "navigation-heading": "Ù\81Û\81رست رہنمائی",
+       "errorpagetitle": "نقص",
+       "returnto": "واپس $1 پر جائیں",
        "tagline": "{{SITENAME}} سے",
        "help": "معاونت",
        "search": "تلاش",
        "search-ignored-headings": " #<!-- اس سطر کو ہو بہو اپنی حالت پر چھوڑ دیں --> <pre>\n# سرخیاں جو تلاش کے دوران میں نظر انداز کر دی جائیں گی۔\n# سرخی پر مشتمل صفحہ کی فہرست سازی مکمل ہوتے ہی تبدیلیاں نافذ ہو جائیں گی۔\n# ایک خالی ترمیم کر کے آپ صفحہ کی دوبارہ فہرست سازی کر سکتے ہیں۔\n# صیغہ حسب ذیل ہے:\n# * ہر چیز جو \"#\" علامت کے بعد آخری سطر تک ہو اسے تبصرہ سمجھا جائے گا۔\n#* ہر وہ سطر جو خالی نہ ہو عنوان ہوگا اور اسے نظر انداز کر دیا جائے گا (نیز جس طرح درج ہے اسی طرح استعمال کیا جائے گا)۔\nحوالہ جات\nبیرونی روابط\nمزید دیکھیے\n #<!-- اس سطر کو ہو بہو اپنی حالت پر چھوڑ دیں --> <pre>",
        "searchbutton": "تلاش",
-       "go": "چلو",
-       "searcharticle": "چلو",
-       "history": "تارÛ\8cØ®Ú\86Û\81 Ø¡ صفحہ",
+       "go": "چلیں",
+       "searcharticle": "چلیں",
+       "history": "تارÛ\8cØ®Ú\86Û\82 صفحہ",
        "history_short": "تاریخچہ",
-       "updatedmarker": "میری آخری آمد تک جدید",
+       "updatedmarker": "میری آخری آمد کے بعد تجدید شدہ",
        "printableversion": "قابل طبع نسخہ",
        "permalink": "مستقل ربط",
        "print": "طباعت",
-       "view": "منظر",
+       "view": "مطالعہ",
        "view-foreign": "$1 پر دیکھیں",
        "edit": "ترمیم",
-       "edit-local": "ترمیم مقامی وضاحت",
+       "edit-local": "مقامی وضاحت کی ترمیم",
        "create": "تخلیق",
-       "create-local": "ادخال مقامی وضاحت",
+       "create-local": "مقامی وضاحت کا اندراج",
        "editthispage": "اس صفحہ میں ترمیم کریں",
-       "create-this-page": "صÙ\81Ø­Û\81 Û\81ٰذا ØªØ®Ù\84Û\8cÙ\82 Ú©Û\8cجئÛ\92",
+       "create-this-page": "اس ØµÙ\81Ø­Û\81 Ú©Ù\88 ØªØ®Ù\84Û\8cÙ\82 Ú©Ø±Û\8cÚº",
        "delete": "حذف",
        "deletethispage": "یہ صفحہ حذف کریں",
        "undeletethispage": "یہ صفحہ بحال کریں",
        "undelete_short": "بحال {{PLURAL:$1|ایک ترمیم|$1 ترامیم}}",
-       "viewdeleted_short": "{{PLURAL:$1|ایک ترمیم حذف ہوچکی|$1 ترامیم حذف ہوچکیں}}",
+       "viewdeleted_short": "{{PLURAL:$1|ایک ترمیم حذف ہو چکی|$1 ترامیم حذف ہو چکیں}} دیکھیں",
        "protect": "محفوظ",
-       "protect_change": "تبدیل کرو",
-       "protectthispage": "اس صفحےکومحفوظ کریں",
-       "unprotect": "تحÙ\81ظ میں تبدیلی",
-       "unprotectthispage": "اÙ\90سÛ\92 ØµÙ\81Ø­Û\92 Ú©Û\8c ØªØ­Ù\81ظ ØªØ¨Ø¯Û\8cÙ\84 Ú©Ø±یں",
+       "protect_change": "تبدیل کریں",
+       "protectthispage": "اس صفحے کو محفوظ کریں",
+       "unprotect": "Ø­Ù\81اظت میں تبدیلی",
+       "unprotectthispage": "اÙ\90سÛ\92 ØµÙ\81Ø­Û\92 Ú©Û\8c Ø­Ù\81اظت Ø¨Ø¯Ù\84یں",
        "newpage": "نیا صفحہ",
        "talkpage": "اس صفحہ پر تبادلۂ خیال کریں",
        "talkpagelinktext": "تبادلۂ خیال",
        "specialpage": "خصوصی صفحہ",
-       "personaltools": "ذاتÛ\8c Ø§Ù\88زار",
-       "articlepage": "Ù\85Ù\86درجاتÛ\8c ØµÙ\81Ø­Û\81 Ø¯Û\8cÚ©Ú¾Û\8cÛ\93",
+       "personaltools": "ذاتÛ\8c Ø¢Ù\84ات",
+       "articlepage": "Ù\85Ù\86درجاتÛ\8c ØµÙ\81Ø­Û\81 Ø¯Û\8cÚ©Ú¾Û\8cÛ\92",
        "talk": "تبادلہٴ خیال",
-       "views": "خیالات",
+       "views": "مشاہدات",
        "toolbox": "آلات",
-       "userpage": "صفحۂ صارف دیکھئے",
-       "projectpage": "صفحۂ منصوبہ دیکھئے",
-       "imagepage": "صفحۂ مسل دیکھئے",
-       "mediawikipage": "صفحۂ پیغام دیکھئے",
-       "templatepage": "صفحۂ سانچہ دیکھئے",
-       "viewhelppage": "صفحۂ معاونت دیکھیے",
-       "categorypage": "زمرہ‌جاتی صفحہ دیکھئے",
-       "viewtalkpage": "تبادلۂ خیال دیکھئے",
+       "tool-link-userrights": "{{GENDER:$1|صارف}} کے گروہوں میں تبدیلی کریں",
+       "tool-link-emailuser": "اس {{GENDER:$1|صارف}} کو برقی خط لکھیں",
+       "userpage": "صارف کا صفحہ دیکھیے",
+       "projectpage": "منصوبہ کا صفحہ دیکھیے",
+       "imagepage": "فائل کا صفحہ دیکھیے",
+       "mediawikipage": "پیغام کا صفحہ دیکھیے",
+       "templatepage": "سانچہ کا صفحہ دیکھیے",
+       "viewhelppage": "معاونت کا صفحہ دیکھیے",
+       "categorypage": "زمرہ‌ جاتی صفحہ دیکھیے",
+       "viewtalkpage": "تبادلۂ خیال دیکھیں",
        "otherlanguages": "دیگر زبانوں میں",
-       "redirectedfrom": "($1 سے پلٹایا گیا)",
-       "redirectpagesub": "لوٹایا گیا صفحہ",
-       "redirectto": "لوٹایا گیا صفحہ:",
-       "lastmodifiedat": "آخرÛ\8c Ø¨Ø§Ø± ØªØ¯Ù\88Û\8cÙ\86 $2, $1 Ú©Ù\88 کی گئی۔",
+       "redirectedfrom": "($1 سے رجوع مکرر)",
+       "redirectpagesub": "رجوع مکرر",
+       "redirectto": "رجوعِ مکرر از:",
+       "lastmodifiedat": "اس ØµÙ\81Ø­Û\81 Ù\85Û\8cÚº Ø¢Ø®Ø±Û\8c Ø¨Ø§Ø± Ù\85Ù\88رخÛ\81 $1Ø¡ Ú©Ù\88 $2 Ø¨Ø¬Û\92 ØªØ±Ù\85Û\8cÙ\85 کی گئی۔",
        "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": "مقفل کرنے کے لیے انتظار کی مہلت ختم",
        "copyrightpage": "{{ns:project}}:حقوق تصانیف",
        "currentevents": "حالیہ واقعات",
        "currentevents-url": "Project:حالیہ واقعات",
-       "disclaimers": "اعÙ\84اÙ\86ات",
-       "disclaimerpage": "Project:عام اعلان",
+       "disclaimers": "اظÛ\81ار Ù\84ا ØªØ¹Ù\84Ù\82Û\8c",
+       "disclaimerpage": "Project:عمومی اظہار لا تعلقی",
        "edithelp": "معاونت براۓ ترمیم",
        "helppage-top-gethelp": "مدد",
        "mainpage": "صفحۂ اول",
        "portal-url": "Project:دیوان عام",
        "privacy": "اخفائے راز کے اصول",
        "privacypage": "Project:اصولِ اخفائے راز",
-       "badaccess": "خطائے اجازت",
+       "badaccess": "نقص اجازت",
        "badaccess-group0": "آپ متمنی عمل کا اجراء کرنے کے مُجاز نہیں۔",
        "badaccess-groups": "آپ کا درخواست‌کردہ عمل {{PLURAL:$2|گروہ|گروہوں میں سے ایک}}: $1 کے صارفین تک محدود ہے.",
        "versionrequired": "میڈیا ویکی کا $1 نسخہ لازمی چاہئیے.",
        "versionrequiredtext": "اِس صفحہ کو استعمال کرنے کیلئے میڈیاویکی کا $1 نسخہ چاہئیے.\n\n\nدیکھئے [[خاص:نسخہ|صفحۂ نسخہ]]",
        "ok": "ٹھیک ہے",
        "pagetitle-view-mainpage": "{{SITENAME}}",
-       "retrievedfrom": "‘‘$1’’ مستعادہ منجانب",
+       "backlinksubtitle": "→ $1",
+       "retrievedfrom": "اخذ کردہ از «$1»",
        "youhavenewmessages": "آپکے لیۓ ایک $1 ہے۔ ($2)",
        "youhavenewmessagesfromusers": "{{PLURAL:$4|آپ کے لیے}} {{PLURAL:$3|کسی دوسرے صارف|$3 صارفین}} کی جانب سے $1 ($2)۔",
        "youhavenewmessagesmanyusers": "آپ کے لیے متعدد صارفین کی جانب سے $1 ($2)۔",
-       "newmessageslinkplural": "{{PLURAL:$1|نیا پیغام|999=نئے پیغاماتs}}",
+       "newmessageslinkplural": "{{PLURAL:$1|نیا پیغام|999=نئے پیغامات}}",
        "newmessagesdifflinkplural": "آخری {{PLURAL:$1|تبدیلی|تبدیلیاں}}",
-       "youhavenewmessagesmulti": "ء$1 پر آپ کیلئے نئے پیغامات ہیں",
+       "youhavenewmessagesmulti": "$1 پر آپ کے لیے نئے پیغامات ہیں",
        "editsection": "ترمیم",
        "editold": "ترمیم",
        "viewsourceold": "مآخذ دیکھئے",
-       "editlink": "تدÙ\88Û\8cÙ\86 Ú©Ø±Û\8cÚº",
-       "viewsourcelink": "Ù\85آخذ Ø¯Û\8cکھئÛ\92",
-       "editsectionhint": "تدÙ\88Û\8cÙ\86Ù\90 Ø­ØµÙ\91ہ: $1",
+       "editlink": "ترÙ\85Û\8cÙ\85",
+       "viewsourcelink": "Ù\85اخذ Ø¯Û\8cÚ©Ú¾Û\8cÚº",
+       "editsectionhint": "ترÙ\85Û\8cÙ\85 Ù\82طعہ: $1",
        "toc": "فہرست",
        "showtoc": "دکھائیں",
        "hidetoc": "چھپائیں",
        "sort-ascending": "ترتیب صعودی",
        "nstab-main": "صفحہ",
        "nstab-user": "صفحۂ صارف",
-       "nstab-media": "صÙ\81Ø­Û\82 Ù\88سÛ\8cØ·",
-       "nstab-special": "خاص صفحہ",
+       "nstab-media": "صÙ\81Ø­Û\82 Ù\85Û\8cÚ\88Û\8cا",
+       "nstab-special": "خصÙ\88صÛ\8c صفحہ",
        "nstab-project": "صفحۂ منصوبہ",
-       "nstab-image": "Ù\85سل",
+       "nstab-image": "Ù\81ائل",
        "nstab-mediawiki": "پیغام",
        "nstab-template": "سانچہ",
        "nstab-help": "معاونت",
        "nstab-category": "زمرہ",
        "mainpage-nstab": "صفحۂ اول",
-       "nosuchaction": "کوئی سا عمل نہیں",
+       "nosuchaction": "مطلوبہ اقدام موجود نہیں",
        "nosuchactiontext": "URL کی جانب سے مختص کیا گیا عمل درست نہیں.\nآپ نے شاید URL غلط لکھا، یا کسی غیر صحیح ربط کی پیروی کی ہے.\n{{اِس سے SITENAME کے زیرِ استعمال مصنع لطیف میں کھٹمل کی نشاندہی کا بھی اندیشہ ہے}}.",
        "nosuchspecialpage": "کوئی ایسا خاص صفحہ نہیں",
        "nospecialpagetext": "<strong>آپ نے ایک غیر موجود خصوصی صفحہ کی درخواست کی ہے۔</strong>\n\nدرست خاص صفحات کی ایک فہرست [[Special:SpecialPages|{{int:specialpages}}]] پر دیکھی جاسکتی ہے۔",
-       "error": "خطاء",
-       "databaseerror": "خطائے ڈیٹابیس",
+       "error": "نقص",
+       "databaseerror": "ڈیٹابیس کا نقص",
        "databaseerror-text": "ڈیٹا بیس کیوری میں خامی پیدا ہوگئی ہے.\nیہ سافٹ ویئر میں ایک مسئلے (بگ) کی نشاندہی کر سکتے ہیں.",
        "databaseerror-textcl": "ڈیٹا بیس کیوری میں خامی پیدا ہوگئی ہے.",
        "databaseerror-query": "کیوری: $1",
        "databaseerror-function": "فنکشن: $ 1",
-       "databaseerror-error": "خرابی: $ 1",
+       "databaseerror-error": "نقص: $1",
        "transaction-duration-limit-exceeded": "زیادہ تاخیر سے بچنے کے لیے اس اقدام کو منسوخ کر دیا گیا ہے کیونکہ مدت تحریر ($1) اپنی حد $2 سیکنڈ سے تجاوز کر چکی ہے۔\nاگر آپ ایک ہی وقت میں کئی چیزیں تبدیل کر رہے ہیں تو بہتر ہوگا کہ اس تبدیلی کو متعدد قسطوں میں انجام دیں۔",
-       "laggedslavemode": "انتباہ: ممکن ہے کہ صفحہ میں حالیہ بتاریخہ جات شامل نہ ہوں.\n\nWarning: Page may not contain recent updates.",
+       "laggedslavemode": "<strong>انتباہ:</strong> شاید اس صفحہ میں تازہ ترین معلومات موجود نہیں۔",
        "readonly": "ڈیٹابیس مقفل ہے",
        "enterlockreason": "قفل کیلئے کوئی وجہ درج کیجئے، بشمولِ تخمینہ کہ قفل کب کھولا جائے گا.",
        "readonlytext": "ڈیٹابیس  شاید معمول کی اصلاح کے لیے نئے اندراجات اور دوسری ترمیمات کیلئے مقفل ہے، جس کے بعد یہ عام حالت پر آجائے گا۔\nمنتظم، جس نے قفل لگایا، یہ تفصیل فراہم کی ہے: $1",
        "missing-article": "ڈیٹابیس نے کسی صفحے کا متن بنام \"$1\" $2  نہیں پایا جو اِسے پانا چاہئے تھا.\n\nیہ عموماً کسی صفحے کے تاریخی یا پرانے حذف شدہ ربط کی وجہ سے ہوسکتا ہے.\n\nاگر یہ وجہ نہیں، تو آپ نے مصنع‌لطیف میں کھٹمل پایا ہے.\nبرائے مہربانی، URL کی نشاندہی کرتے ہوئے کسی [[Special:ListUsers/sysop|منتظم]] کو اِس کا سندیس کیجئے.",
-       "missingarticle-rev": "(Ù\86ظرثاÙ\86Û\8c#: $1)",
+       "missingarticle-rev": "(Ù\86سخÛ\81#: $1)",
        "missingarticle-diff": "(فرق: $1، $2)",
        "readonly_lag": "ڈیٹابیس خودکار طور پر مقفل ہوچکا ہے تاکہ ماتحت ڈیٹابیسی معیلات کا درجہ آقا کا ہوجائے.",
        "nonwrite-api-promise-error": "ایچ ٹی ٹی پی سرنامہ 'Promise-Non-Write-API-Action' بھیجا گیا لیکن یہ ایک اے پی آئی نویس ماڈیول کو بھیجی گئی درخواست تھی۔",
        "unexpected": "غیرمتوقع قدر: \"$1\"=\"$2\"",
        "formerror": "خطا: ورقہ بھیجا نہ جاسکا.",
        "badarticleerror": "اس صفحہ پر یہ عمل انجام نہیں دیا جاسکتا۔",
-       "cannotdelete": "صÙ\81Ø­Û\81 Û\8cا Ù\85Ù\84Ù\81 $1 Ú©Ù\88 Ø­Ø°Ù\81 Ù\86Û\81Û\8cÚº Ú©Û\8cا Ø¬Ø§Ø³Ú©ØªØ§.\nÛ\81Ù\88سکتا Û\81Û\92 Ú©Û\81 Ø§Ø³Û\92 Ù¾Û\81Ù\84Û\92 Û\81Û\8c Ú©Ø³Û\8c Ù\86Û\92 Ø­Ø°Ù\81 Ú©Ø±Ø¯Û\8cا Û\81Ù\88.",
-       "cannotdelete-title": "صفحہ ھذف نہیں کیا جا سکتا \"$1\"",
+       "cannotdelete": "صÙ\81Ø­Û\81 Û\8cا Ù\81ائÙ\84 Â«$1» Ú©Ù\88 Ø­Ø°Ù\81 Ù\86Û\81Û\8cÚº Ú©Û\8cا Ø¬Ø§ Ø³Ú©Ø§Û\94\nÙ\85Ù\85Ú©Ù\86 Û\81Û\92 Ú©Ø³Û\8c Ù\86Û\92 Ø§Ø³Û\92 Ù¾Û\81Ù\84Û\92 Û\81Û\8c Ø­Ø°Ù\81 Ú©Ø± Ø¯Û\8cا Û\81Ù\88Û\94",
+       "cannotdelete-title": "صفحہ «$1» حذف نہیں کیا جا سکا",
        "delete-hook-aborted": "حذف شدگی روک دی گئی\nوضاحت نہیں کی گئی",
        "no-null-revision": "صفحہ \"$1\" کے لیے نیا خالی نسخہ نہیں بنایا جا سکتا",
        "badtitle": "خراب عنوان",
-       "badtitletext": "درخواست شدہ صفحہ کا عنوان ناقص، خالی، یا کوئی غلط ربط شدہ بین لسانی یا بین ویکی عنوان ہے.\nشاید اِس میں ایک یا زیادہ ایسے حروف موجود ہوں جو عنوانات میں استعمال نہیں ہوسکتے.",
+       "badtitletext": "درخواست شدہ صفحہ کا عنوان ناقص، خالی، یا کوئی غلط ربط شدہ بین اللسانی یا بین الویکی عنوان ہے۔\nشاید اِس میں ایک یا زیادہ ایسے حروف موجود ہوں جو عنوانات میں استعمال نہیں ہو سکتے۔",
        "title-invalid-empty": "درخواست شدہ عنوان خالی ہے یا اس میں محض نام فضا کا نام ہے۔",
        "title-invalid-utf8": "درخواست شدہ عنوان میں نادرست یونیکوڈ حروف موجود ہیں۔",
        "title-invalid-interwiki": "درخواست شدہ عنوان میں ایسا بین الویکی ربط موجود ہے جسے عنوانات میں استعمال نہیں کیا جا سکتا۔",
        "title-invalid-too-long": "درخواست شدہ عنوان بے حد طویل ہے۔ عنوان کی طوالت یونیکوڈ کے $1 {{PLURAL:$1|بائٹ}} سے کم ہونی چاہیے۔",
        "title-invalid-leading-colon": "درخواست شدہ عنوان کے شروع میں ایک نادرست رابطہ موجود ہے۔",
        "perfcached": "ذیلی ڈیٹا ابطن شدہ (cached) ہے اور اِس کے پُرانے ہونے کا امکان ہے. A maximum of {{PLURAL:$1|one result is|$1 results are}} available in the cache.",
-       "perfcachedts": "ذیلی ڈیٹا ابطن شدہ ہے (cached) اور آخری بار اِس کی بتاریخیت $1 کو ہوئی. A maximum of {{PLURAL:$4|one result is|$4 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براہِ کرم، کچھ منٹس بعد دوبارہ کوشش کریں۔",
        "viewsourcetext": "آپ صرف مسودہ دیکھ سکتے ہیں اور اسکی نقل اتار سکتے ہیں۔",
        "viewyourtext": "آپ اس مواد کو دیکھ سکتے ہیں اور اٹھا (کاپی) سکتے ہیں <strong>آپ کی ترامیم</strong> اس صفحہ پر۔",
        "protectedinterface": "یہ صفحہ سافٹ ویئر کا انٹرفیس متن فراہم کرتا ہے اور غلط استعمال سے بچنے کے لیے اسے محفوظ رکھا گیا ہے۔\nتمام ویکیوں میں ترجمہ شامل کرنے یا اس میں تبدیلی کرنے کے لیے میڈیاویکی دار الترجمہ [https://translatewiki.net/ translatewiki.net]کو استعمال کریں۔",
-       "editinginterface": "<strong>انتباہ: </strong> آپ ایک ایسے صفحے میں ترمیم کر رہے ہیں جو سوفٹ ویئر کے لیے انٹرفیس متن فراہم کرتا ہے۔ اس صفحہ میں کی جانے والی تبدیلی سے اس ویکی پر دیگر صارفین کے لیے انٹرفیس متاثر ہوگی۔",
-       "translateinterface": "تÙ\85اÙ\85 Ù\88Û\8cÚ©Û\8cÙ\88Úº Ù\85Û\8cÚº ØªØ¨Ø¯Û\8cÙ\84Û\8c Û\8cا Ø´Ø§Ù\85Ù\84 Ú©Ø±Ù\86Û\92 Ú©Û\92 Ù\84Û\8cÛ\92Ø\8c [https://translatewiki.net/ translatewiki.net]Ú©Ù\88 Ø§Ø³ØªØ¹Ù\85اÙ\84 Ú©Ø±Û\8cÚº Ø\8c Ù\85Û\8cÚ\88Û\8cا Ù\88Û\8cÚ©Û\8c Ø¯Ø§Ø±Ø§Ù\84ترجÙ\85Û\81.",
+       "editinginterface": "<strong>انتباہ:</strong> آپ ایک ایسے صفحے میں ترمیم کر رہے ہیں جو سافٹ ویئر کا انٹرفیس متن فراہم کرتا ہے۔ اس صفحہ میں کی جانے والی ترمیم، دیگر صارفین کے انٹرفیس کو تبدیل کردے گی۔",
+       "translateinterface": "تÙ\85اÙ\85 Ù\88Û\8cÚ©Û\8cÙ\88Úº Ù\85Û\8cÚº ØªØ±Ø§Ø¬Ù\85 Ú©Ù\88 ØªØ¨Ø¯Û\8cÙ\84 Û\8cا Ø´Ø§Ù\85Ù\84 Ú©Ø±Ù\86Û\92 Ú©Û\92 Ù\84Û\8cÛ\92  Ù\85Û\8cÚ\88Û\8cاÙ\88Û\8cÚ©Û\8c Ú©Û\92 Ø¯Ø§Ø± Ø§Ù\84ترجÙ\85Û\81 [https://translatewiki.net/ translatewiki.net] Ú©Ù\88 Ø§Ø³ØªØ¹Ù\85اÙ\84 Ú©Ø±Û\8cÚºÛ\94",
        "cascadeprotected": "درج ذیل محفوظ کردہ {{PLURAL:$1|صفحہ|صفحات}} کی «آبشاری» حفاظت میں شامل ہونے کی وجہ سے یہ صفحہ بھی محفوظ ہے:\n$2",
        "namespaceprotected": "آپ کو '''$1''' فضائے نام میں صفحات تدوین کرنے کی اِجازت نہیں ہے.",
        "customcssprotected": "آپ کو اس سی ایس ایس میں ترمیم کرنے کی اجازت نہیں کیونکہ اس میں کسی دوسرے صارف کی ذاتی ترتیبات موجود ہیں۔",
        "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": "پاس ورڈ کی تصدیق کریں",
+       "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": "اِن خصائص کو کام میں لانے کیلئے اپنے ترجیحات میں برقی ڈاک کا پتہ متعین کیجئے.",
        "cannotchangeemail": "کھاتے کا برقی پتہ اس ویکی سے پر رہتے ہوئے نہیں تبدیل کیا جا سکتا۔",
        "emaildisabled": "اس سائٹ سے برقی خط نہیں بھیجے جاسکتے",
        "accountcreated": "تخلیقِ کھاتہ",
-       "accountcreatedtext": "[[{{ns:صارف}}:$1|$1]] ([[{{ns:تبادلۂ خیال صارف}}:$1|تبادلۂ خیال]]) کا صارف کھاتہ بن چکا ہے۔",
+       "accountcreatedtext": "[[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|تبادلۂ خیال]]) کا صارف کھاتہ بن چکا ہے۔",
        "createaccount-title": "کھاتہ سازی برائے {{SITENAME}}",
        "createaccount-text": "کسی نے {{SITENAME}} ($4) پر \"$2\" کے نام سے اور \"$3\" پارلفظ کے ساتھ آپ کا برقی پتہ استعمال کرتے ہوئے کھاتہ بنایا ہے.\nآپ کو چاہئے کہ ابھی داخلِ نوشتہ ہوکر اپنا پارلفظ تبدیل کردیں.\n\nاگر یہ کھاتہ غلطی سے بنا تھا تو آپ یہ پیغام نظرانداز کرسکتے ہیں.",
        "login-throttled": "آپ نے حال ہی میں متعدد مرتبہ لاگ ان ہونے کی کوشش کی ہے۔\nدوبارہ کوشش کرنے سے پہلے $1 انتظار فرمائیے۔",
        "loginlanguagelabel": "زبان: $1",
        "suspicious-userlogout": "کھاتے سے خارج ہونے کی درخواست رد کر دی گئی ہے کیونکہ ایسا معلوم ہوتا ہے یہ درخواست کسی شکستہ براؤزر یا کیشے کی حامل پراکسی سے بھیجی گئی تھی۔",
        "createacct-another-realname-tip": "حقیقی نام اختیاری ہے۔\nاگر آپ اسے فراہم کریں تو آپ کے کاموں کو اس نام سے منسوب کرنے کے لیے استعمال کیا جائے گا۔",
-       "pt-login": "داخل ہوجائیے",
+       "pt-login": "داخل ہوں",
        "pt-login-button": "داخل ہو",
        "pt-login-continue-button": "داخل ہوں",
        "pt-createaccount": "کھاتا بنائیں",
        "changepassword-success": "آپ کا پاس ورڈ تبدیل کر دیا گیا!",
        "changepassword-throttled": "آپ نے حال ہی میں متعدد مرتبہ داخل ہونے کی کوشش کی ہے۔\nدوبارہ کوشش کرنے سے پہلے $1 انتظار کریں۔",
        "botpasswords": "روبہ پاس ورڈ",
+       "botpasswords-summary": "<em>روبہ کے پاس ورڈ</em> کے ذریعہ اصل کھاتے کی لاگ ان معلومات کے بغیر اے پی آئی کی مدد سے صارف کھاتے میں رسائی حاصل ہوتی ہے۔\n\nاگر آپ اس سے واقف نہیں ہیں تو بہتر ہوگا کہ آپ اسے نہ چھیڑیں۔ کوئی دوسرا صارف کبھی اس پاس ورڈ کے بنانے اور اسے سپرد کرنے کا آپ سے مطالبہ نہیں کرے گا۔",
        "botpasswords-disabled": "روبہ کے پاس ورڈ غیر فعال ہیں۔",
        "botpasswords-no-central-id": "روبہ کے پاس ورڈ کو استعمال کرنے کے لیے آپ کا مرکزی کھاتے میں داخل رہنا ضروری ہے۔",
        "botpasswords-existing": "روبہ کے موجودہ پاس ورڈ",
        "botpasswords-label-delete": "حذف کریں",
        "botpasswords-label-resetpassword": "پاس ورڈ تبدیل کریں",
        "botpasswords-label-grants": "قابل تطبیق عطیے:",
-       "botpasswords-label-restrictions": "استعمال کی پابندیاں:",
        "botpasswords-label-grants-column": "دے دیا گیا",
        "botpasswords-bad-appid": "روبہ نام \"$1\" درست نہیں۔",
        "botpasswords-insert-failed": "روبہ نام \"$1\" کو شامل کرنے میں ناکامی۔ کیا اسے پہلے شامل کیا جا چکا ہے؟",
        "botpasswords-updated-body": "صارف \"$2\" کے روبہ نام \"$1\" کا پاس ورڈ تازہ کر دیا گیا۔",
        "botpasswords-deleted-title": "روبہ کا پاس ورڈ حذف ہو چکا ہے",
        "botpasswords-deleted-body": "صارف \"$2\" کے روبہ نام \"$1\" کا پاس ورڈ حذف کیا جا چکا ہے۔",
+       "botpasswords-newpassword": "<strong>$1</strong> کے کھاتے میں داخل ہونے کے لیے نیا پاس ورڈ <strong>$2</strong> ہے۔ <em>براہ کرم اسے آئندہ کے لیے محفوظ کر لیں۔</em> <br> (وہ قدیم روبہ جات جنہیں یکساں لاگ ان نام اور آخری نام درکار ہوتا ہے، ان کے لیے آپ <strong>$3</strong> کو صارف نام اور <strong>$4</strong> کو پاس ورڈ کے طور پر استعمال کر سکتے ہیں۔)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider دستیاب نہیں۔",
        "botpasswords-restriction-failed": "روبہ کے پاس ورڈ کی پابندیاں اس لاگ ان سے مانع ہیں۔",
        "botpasswords-invalid-name": "درج کردہ صارف نام میں روبہ کے پاس ورڈ کا فاصل لفظ موجود نہیں ہے (\"$1\")۔",
        "resetpass-expired": "آپ کے پاس ورد کی مدت ختم ہو چکی ہے۔ داخل ہونے کے لیے براہ کرم نیا پاس ورڈ بنائیں۔",
        "resetpass-expired-soft": "آپ کے پاس ورڈ کی مدت ختم ہو چکی ہے، لہذا اسے دوبارہ بنانے کی ضرورت ہے۔\nبراہ کرم نیا پاس ورڈ بنائیں، تاہم اگر مستقبل میں اس کی ترتیب نو مقصود ہو تو «{{int:authprovider-resetpass-skip-label}}» پر کلک کریں۔",
        "resetpass-validity-soft": "آپ کا پاس ورڈ درست نہیں: $1\n\nبراہ کرم نیا پاس ورڈ بنائیں، تاہم اگر مستقبل میں اس کی ترتیب نو مقصود ہو تو «{{int:authprovider-resetpass-skip-label}}» پر کلک کریں۔",
-       "passwordreset": "پارÙ\84Ù\81ظ Ú©Û\8c Ø¨Ø§Ø²ØªØ¹Û\8cÙ\86Û\8c",
+       "passwordreset": "پاس Ù\88رÚ\88 Ú©Û\8c ØªØ±ØªÛ\8cب Ù\86Ù\88",
        "passwordreset-text-one": "برقی خط کے ذریعہ عارضی پاس ورڈ حاصل کرنے کے لیے اس فارم کو پُر کریں۔",
        "passwordreset-text-many": "{{PLURAL:$1|برقی خط کے ذریعہ عارضی پاس ورڈ حاصل کرنے کے لیے کسی ایک خانے کو پُر کریں۔}}",
        "passwordreset-disabled": "اس ویکی پر پاس ورڈ کی ترتیب نو کی سہولت فعال نہیں ہے۔",
        "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-invalideamil": "نادرست برقی ڈاک پتا",
        "passwordreset-nodata": "کوئی صارف نام اور نہ کوئی برقی ڈاک پتا فراہم کیا گیا",
-       "changeemail": "برقی ڈاک پتا تبدیل یا حذف کریں",
+       "changeemail": "برقی ڈاک پتے میں تبدیلی یا حذف شدگی",
        "changeemail-header": "اپنے برقی ڈاک پتے کو تبدیل کرنے کے لیے اس فارم کو پُر کریں۔ اگر آپ اپنے کھاتے سے منسلک کسی برقی ڈاک پتے کو ختم کرنا چاہتے ہیں تو فارم پُر کرنے کے دوران میں نئے برقی ڈاک پتے کا خانہ خالی چھوڑ دیں۔",
        "changeemail-no-info": "اِس صفحہ تک براہِ راست رسائی کیلئے آپ کو داخل ہونا پڑے گا۔",
        "changeemail-oldemail": "حالیہ برقی ڈاک پتہ:",
        "changeemail-submit": "برقی ڈاک تبدیل کریں",
        "changeemail-throttled": "آپ نے متعدد مرتبہ داخل ہونے کی کوشش کی ہے۔\nدوبارہ کوشش کرنے سے پہلے $1 انتظار فرمائیں۔",
        "changeemail-nochange": "براہ کرم کوئی دوسرا برقی ڈاک پتہ درج کریں۔",
-       "resettokens": "ٹوکنوں کو دوبارہ ترتیب دیں",
+       "resettokens": "ٹوکنوں کی ترتیب نو",
        "resettokens-no-tokens": "ترتیب نو کے لیے کوئی ٹوکن موجود نہیں۔",
        "resettokens-tokens": "ٹوکن:",
        "resettokens-token-label": "$1 (موجودہ قدر: $2)",
        "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",
-       "media_tip": "ربطِ ملف",
+       "nowiki_tip": "ویکی فارمیٹ کو نظرانداز کریں",
+       "image_tip": "Ù¾Û\8cÙ\88ستÛ\81 Ù\81ائÙ\84",
+       "media_tip": "فائل کا ربط",
        "sig_tip": "آپکا دستخط بمع مہرِوقت",
        "hr_tip": "اُفقی لکیر (زیادہ استعمال نہ کریں)",
        "summary": "خلاصہ:",
        "subject": "عنوان:",
        "minoredit": "معمولی ترمیم",
-       "watchthis": "یہ صفحہ زیر نظر کیجیۓ",
+       "watchthis": "اس صفحہ کو زیر نظر کریں",
        "savearticle": "محفوظ",
        "savechanges": "تبدیلیاں محفوظ کریں",
        "publishpage": "شائع کریں",
        "nosuchsectiontitle": "قطعہ نہیں ملا",
        "nosuchsectiontext": "آپ نے ایسے قطعہ میں ترمیم کی کوشش کی ہے جو کہ موجود نہیں.\nہوسکتا ہے کہ جب آپ صفحہ ملاحظہ فرمارہے تھے اُسی اثناء مذکورہ قطعہ کو منتقل یا حذف کردیا گیا ہو.",
        "loginreqtitle": "داخلہ / اندراج لازم",
-       "loginreqlink": "داخلہ",
+       "loginreqlink": "لاگ ان",
        "loginreqpagetext": "دوسرے صفحات ملاحظہ کرنے کیلئے آپکا $1 ضروری ہے.",
        "accmailtitle": "کلمہ شناخت بھیج دیا گیا۔",
        "accmailtext": "[[User talk:$1|$1]] کے لیے خودکار طریقے سے تخلیق کیا گیا پاسورڈ $2 کو بھیج دیا گیا ہے.\n\nلاگ ان ہونے کے بعد <em>[[Special:ChangePassword|اسے تبدیل]]</em> کیا جا سکتا ہے۔",
        "newarticle": "(نیا)",
        "newarticletext": "آپ نے ایک ایسے صفحے کے ربط کی پیروی کی ہے جو کہ ابھی موجود نہیں ہے.\nیہ صفحہ تخلیق کرنے کیلئے درج ذیل خانہ میں متن درج کیجئے (مزید معلومات کیلئے [$1 صفحۂ معاونت] ملاحظہ فرمائیے).\nاگر آپ یہاں غلطی سے پہنچے ہیں تو پچھلے صفحے پر واپس جانے کیلئے اپنے متصفح پر '''back''' کا بٹن ٹک کیجئے.",
-       "anontalkpagetext": "----''یہ صفحہ ایک ایسے صارف کا ہے جنہوں نے یا تو اب تک اپنا کھاتا نہیں بنایا یا پھر وہ اسے استعمال نہیں کر رہے/ رہی ہیں۔ لہٰذا ہمیں انکی شناخت کے لئے ایک عددی آئی پی پتہ استعمال کرنا پڑرہا ہے۔ اس قسم کا آئی پی پتہ ایک سے زائد صارفین کے لئے مشترک بھی ہوسکتا ہے۔ اگر آپکی موجودہ حیثیت ایک گمنام صارف کی ہے اور آپ محسوس کریں کہ اس صفحہ پر آپکی جانب منسوب یہ بیان غیرضروری ہے تو براہ کرم [[Special:CreateAccount|کھاتہ بنائیں]] یا [[Special:UserLogin|داخلِ نوشتہ]] ہوجائیے تاکہ مستقبل میں آپکو گمنام صارفین میں شمار کرنے سے پرہیز کیا جاسکے۔\"",
+       "anontalkpagetext": "----\n<em>یہ تبادلۂ خیال صفحہ ایک ایسے صارف کا ہے جس نے اب تک اپنا کھاتہ نہیں بنایا یا یہ صفحہ اس کے زیر استعمال نہیں۔</em> \nلہٰذا ہمیں اس کی شناخت کے لئے ایک آئی پی پتہ استعمال کرنا پڑ رہا ہے۔ \nاس قسم کا آئی پی پتہ ایک سے زائد صارفین کے درمیان میں مشترک بھی ہوسکتا ہے۔ \nاگر آپ کی موجودہ حیثیت ایک گمنام صارف کی ہے اور آپ محسوس کریں کہ اس صفحہ پر آپ کے متعلق یہ تبصرے غیر متعلق ہیں تو براہ کرم [[Special:CreateAccount|ایک کھاتہ بنا لیں]] یا [[Special:UserLogin|داخل ہو جائیں]] تاکہ مستقبل میں آپ کو گمنام صارفین میں شمار کرنے سے گریز کیا جائے۔",
        "noarticletext": "اِس صفحہ میں فی الحال کوئی متن موجود نہیں ہے۔\nآپ دیگر صفحات میں [[Special:Search/{{PAGENAME}}|اِس صفحہ کے عنوان کو تلاش کر سکتے ہیں]]، <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} متعلقہ نوشتہ جات میں تلاش کر سکتے ہیں]،\nیا [{{fullurl:{{FULLPAGENAME}}|action=edit}} اِس صفحہ کو تخلیق کر سکتے ہیں]</span>۔",
        "noarticletext-nopermission": "اس صفحہ میں فی الحال کوئی متن موجود نہیں ہے۔\nآپ دیگر صفحات میں [[Special:Search/{{PAGENAME}}|اِس صفحہ کے عنوان کے لیے]] یا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} متعلقہ نوشتہ جات تلاش کرسکتے ہیں]</span>",
        "userpage-userdoesnotexist": "«$1» کے نام سے صارف کھاتہ موجود نہیں ہے۔\nاگر آپ اس صفحہ کو تخلیق یا اس میں ترمیم کرنا چاہتے ہیں تو براہ کرم پہلے جانچ لیں۔",
        "blocked-notice-logextract": "یہ صارف معطل ہے۔\nحوالہ کے لیے نوشتہ پابندی کا تازہ ترین اندراج ذیل میں دستیاب ہے:",
        "clearyourcache": "<strong>یاددہانی:</strong> محفوظ کرنے کے بعد ان تبدیلیوں کو دیکھنے کے لیے آپ کو اپنے براؤزر کا کیشے صاف کرنا ہوگا۔\n* '''فائرفاکس/ سفاری:''' جب ''Reload'' پر کلک کریں تو ''Shift'' دباکر رکھیں، یا ''Ctrl-F5'' یا ''Ctrl-R'' دبائیں (Mac پر ''R-⌘'')\n* '''گوگل کروم:''' ''Ctrl-Shift-R'' دبائیں (Mac پر ''Shift-R-⌘'')\n* '''انٹرنیٹ ایکسپلورر:''' جب ''Refresh'' پر کلک کریں تو ''Ctrl'' یا ''Ctrl-F5'' دبائیں\n* '''اوپیرا:'''  ''Tools → Preferences'' میں جائیں اور کیشے (cache) صاف کریں",
        "usercssyoucanpreview": "<strong>نکتہ:</strong> اپنی نئی سی ایس ایس کو جانچنے کے لیے اسے محفوظ کرنے سے قبل «{{int:showpreview}}» کی بٹن استعمال کریں۔",
-       "userjsyoucanpreview": "<strong>نکتہ:</strong> اپنی نئی جاوا اسکرپٹ کو جانچنے کے لیے اسے محفوظ کرنے سے قبل «{{int:showpreview}}» کی بٹن استعمال کریں۔",
+       "userjsyoucanpreview": "<strong>نکتہ:</strong>اپنی نئی جاوا اسکرپٹ کو  محفوظ کرنے سے قبل «{{int:showpreview}}» کی بٹن پر کلک کرکے جانچ لیں۔",
        "usercsspreview": "<strong>یاد رہے کہ اس وقت آپ اپنی سی ایس کی محض نمائش دیکھ رہے ہیں، یہ اب تک محفوظ نہیں ہوئی ہے!</strong>",
        "userjspreview": "<strong>یاد رہے کہ اس وقت آپ اپنی جاوا اسکرپٹ کی محض نمائش دیکھ/جانچ رہے ہیں، یہ اب تک محفوظ نہیں ہوئی ہے!</strong>",
        "sitecsspreview": "<strong>یاد رہے کہ اس وقت آپ اس سی ایس کی محض نمائش دیکھ رہے ہیں، یہ اب تک محفوظ نہیں ہوئی ہے!</strong>",
        "previewnote": "'''یاد رکھیں، یہ صرف نمائش ہے ۔آپ کی ترامیم ابھی محفوظ نہیں کی گئیں۔'''",
        "continue-editing": "خانہ ترمیم میں جائیں",
        "previewconflict": "اس نمائش میں خانہ ترمیم کے اوپر موجود متن جس انداز میں ظاہر ہو رہا ہے، محفوظ کرنے کے بعد اسی طرح نظر آئے گا۔",
-       "session_fail_preview": "معاف کیجئے! نشست کے مواد میں خامی کی وجہ سے آپکی  ترمیم پر عمل نہیں کیا جاسکا.\nبرائے مہربانی دوبارہ کوشش کیجئے.\nاگر آپکو پھر بھی مشکل پیش آرہی ہے تو [[Special:UserLogout|خارجِ نوشتہ]] ہوکر واپس داخلِ نوشتہ ہوجایئے.",
+       "session_fail_preview": "معذرت! نشست کے مواد میں خامی کی وجہ سے آپ کی  ترمیم مکمل نہیں ہو سکی۔\n\nشاید آپ اپنے کھاتے سے خارج ہو گئے ہیں۔ <strong>براہ کرم اس بات کی تصدیق کر لیں کہ آپ داخل ہیں اور دوبارہ کوشش کریں۔</strong> اگر آپ کو پھر بھی مشکل پیش آرہی ہو تو ایک بار [[Special:UserLogout|خارج ہو کر]] واپس داخل ہو جائیں اور اپنے براؤزر کو جانچ لیں کہ آیا وہ اس سائٹ کی کوکیز اخذ کر رہا ہے یا نہیں۔",
+       "edit_form_incomplete": "<strong>خانہ ترمیم سے کچھ حصے سرور تک نہیں پہنچ سکے ہیں؛ براہ کرم اپنی ترامیم کو دوبارہ جانچ لیں کہ آیا وہ برقرار ہیں یا نہیں اور دوبارہ کوشش کریں۔</strong>",
        "editing": "آپ \"$1\" میں ترمیم کر رہے ہیں۔",
        "creating": "زیر تخلیق $1",
-       "editingsection": "$1 کے قطعہ کی تدوین",
+       "editingsection": "«$1» کے قطعہ کی ترمیم",
        "editingcomment": "زیرترمیم $1 (نیا قطعہ)",
        "editconflict": "تنازعہ ترمیم:$1",
        "explainconflict": "آپکی تدوین شروع ہونے کے بعد شاید کسی نے یہ صفحہ تبدیل کردیا ہے.\nبالائی خانۂ متن میں صفحہ کا موجودہ مواد ہے.\nآپ کی تبدیلیاں نچلے متن خانہ میں دکھائی گئی ہیں.\nآپ کو اپنی تبدیلیاں موجودہ متن میں ضم کرنا ہوں گی.\n\"محفوظ\" کا بٹن ٹک کرنے سے '''صرف''' بالائی متن محفوظ ہوگا.",
        "editingold": "'''انتباہ: آپ اس صفحے کا ایک پرانا مسودہ مرتب کررہے ہیں۔ اگر آپ اسے محفوظ کرتے ہیں تو اس صفحے کے اس پرانے مسودے سے اب تک کی جانے والی تمام تدوین ضائع ہو جاۓ گی۔'''",
        "yourdiff": "تضادات",
        "copyrightwarning": "یہ یادآوری کرلیجیۓ کہ {{SITENAME}} میں تمام تحریری شراکت جی این یو آزاد مسوداتی اجازہ ($2)کے تحت تصور کی جاتی ہے (مزید تفصیل کیلیۓ $1 دیکھیۓ)۔ اگر آپ اس بات سے متفق نہیں کہ آپکی تحریر میں ترمیمات کری جائیں اور اسے آزادانہ (جیسے ضرورت ہو) استعمال کیا جاۓ تو براۓ کرم اپنی تصانیف یہاں داخل نہ کیجیۓ۔ اگر آپ یہاں اپنی تحریر جمع کراتے ہیں تو آپ اس بات کا بھی اقرار کر رہے ہیں کہ، اسے آپ نے خود تصنیف کیا ہے یا دائرہ ءعام (پبلک ڈومین) سے حاصل کیا ہے یا اس جیسے کسی اور آذاد وسیلہ سے۔'''بلااجازت ایسا کام داخل نہ کیجیۓ جسکا حق ِطبع و نشر محفوظ ہو!'''",
+       "copyrightwarning2": "براہ کرم اس بات کا خیال رکھیں کہ {{SITENAME}} میں آپ کی جانب سے کی جانے والی تمام ترمیموں میں دیگر صارفین بھی حذف و اضافہ کر سکتے ہیں۔\nاگر آپ اپنی تحریر کے ساتھ اس قسم کے سلوک کے روادار نہیں تو براہ کرم اسے یہاں شائع نہ کریں۔<br />\nنیز اس تحریر کو شائع کرتے وقت آپ ہم سے یہ وعدہ بھی کر رہے ہیں کہ اسے آپ نے خود لکھا ہے یا اسے دائرہ عام یا کسی آزاد ماخذ سے یہاں نقل کر رہے ہیں (تفصیلات کے لیے $1 ملاحظہ فرمائیں)۔\n<strong>براہ کرم اجازت کے بغیر کسی کاپی رائٹ شدہ مواد کو یہاں شائع نہ کریں۔</strong>",
+       "editpage-cannot-use-custom-model": "اس صفحہ کے مواد کے ماڈل کو تبدیل نہیں کیا جا سکتا۔",
+       "readonlywarning": "<strong>انتباہ: انتظامی نگہداشت کی خاطر ڈیٹابیس کو مقفل کر دیا گیا ہے، لہذا اس وقت آپ اپنی ترامیم کو محفوظ نہیں کر سکتے۔</strong>\nآپ اپنی تحریر کو کسی ٹیکسٹ فائل میں محفوظ کر سکتے ہیں تاکہ وہ ضائع نہ ہو اور آئندہ اسے استعمال کیا جا سکے۔\n\nانتظامیہ کی جانب سے مقفل کرنے کی حسب ذیل وجہ بیان کی گئی ہے:\n\n$1",
        "protectedpagewarning": "<strong>انتباہ: اس صفحہ میں ترمیم کاری کو مقفل کر دیا گیا ہے اور محض انتظامی اختیارات کے حامل صارفین ہی اس میں ترمیم کر سکتے ہیں۔</strong>\nحوالہ کے لیے ذیل میں نوشتہ جاتی اندراج فراہم کیا گیا ہے:",
-       "semiprotectedpagewarning": "<strong>اطلاع:</strong> اس صفحہ کو یوں مقفل کیا جاچکا ہے کہ اس میں صرف اندراج شدہ صارفین ہی ترمیم کرسکتے ہیں۔\nحوالہ کے لیے ذیل میں تازہ ترین نوشتہ جاتی اندراج دیا گیا ہے:",
+       "semiprotectedpagewarning": "<strong>اطلاع:</strong> اس صفحہ کو محفوظ کر دیا گیا ہے، لہذا اب اس میں محض اندراج شدہ صارفین ہی ترمیم کر سکتے ہیں۔\nحوالہ کے لیے ذیل میں نوشتہ کا تازہ ترین اندراج درج ہے:",
        "cascadeprotectedwarning": "<strong>انتباہ:</strong> اس صفحہ میں ترمیم کاری کو مقفل کر دیا گیا ہے اور محض انتظامی اختیارات کے حامل صارفین ہی اس میں ترمیم کر سکتے ہیں۔ اسے مقفل کرنے کی وجہ یہ ہے کہ پیش نظر صفحہ درج ذیل محفوظ {{PLURAL:$1|صفحہ|صفحات}} کی آبشاری حفاظت میں شامل ہے:",
+       "titleprotectedwarning": "<strong>انتباہ: اس صفحہ کو محفوظ کر دیا گیا ہے، چنانچہ اسے تخلیق کرنے کے لیے [[Special:ListGroupRights|خصوصی اختیارات]] درکار ہونگے۔</strong>\nحوالہ کے لیے ذیل میں نوشتہ کا تازہ ترین اندراج موجود ہے:",
        "templatesused": "اِس صفحہ پر مستعمل {{PLURAL:$1|سانچہ|سانچے}}:",
        "templatesusedpreview": "اِس پیش منظر میں مستعمل {{PLURAL:$1|سانچہ|سانچے}}:",
        "templatesusedsection": "اِس قطعہ میں مستعمل {{PLURAL:$1|سانچہ|سانچے}}:",
        "template-protected": "(محفوظ شدہ)",
        "template-semiprotected": "(نیم محفوظ)",
        "hiddencategories": "یہ صفحہ {{PLURAL:$1|1 چُھپے زمرے|$1 چُھپے زمرہ جات}} میں شامل ہے:",
+       "nocreatetext": "{{SITENAME}} نے نئے صفحات تخلیق کرنے پر پابندی لگا رکھی ہے۔\nتاہم آپ پہلے سے موجود صفحات میں ترمیم کر سکتے ہیں یا [[Special:UserLogin|اپنے کھاتے ميں داخل ہوں یا کھاتہ بنائیں]]۔",
        "nocreate-loggedin": "آپ کو نئے صفحات تخلیق کرنے کی اجازت نہیں ہے.",
        "sectioneditnotsupported-title": "قطعہ کی تدوین حمایت شدہ نہیں ہے",
        "sectioneditnotsupported-text": "اِس صفحہ میں قطعہ کی تدوین حمایت شدہ نہیں ہے.",
        "permissionserrors": "خطائے اجازت",
        "permissionserrorstext": "درج ذیل {{PLURAL:$1|وجہ|وجوہات}} کی بناء پر آپ کو ایسا کرنے کی اجازت نہیں ہے:",
-       "permissionserrorstext-withaction": "درج ذیل {{PLURAL:$1|وجہ|وجوہات}} کی بناء پر آپ کو $2 کرنے کی اجازت نہیں ہے:",
+       "permissionserrorstext-withaction": "درج ذیل {{PLURAL:$1|وجہ|وجوہات}} کی بناء پر آپ کو $2  کی اجازت نہیں ہے:",
+       "contentmodelediterror": "آپ اس نسخے میں ترمیم نہیں کر سکتے کیونکہ اس کے مواد کا ماڈل ‌‌<code>$1</code> ہے جو اس صفحہ کے مواد کے موجودہ ماڈل <code>$2</code> سے مختلف ہے۔",
        "recreate-moveddeleted-warn": "''' انتباہ: آپ ایک گزشتہ حذف شدہ صفحہ دوبارہ تخلیق کررہے ہیں. '''\n\nآپ کو اِس بات پر غور کرنا چاہئے کہ آیا اِس صفحہ کی تدوین جاری رکھنا موزوں ہے یا نہیں.\nصفحہ کا نوشتۂ حذف شدگی و منتقلی یہاں سہولت کی خاطر مہیّا کیا جارہا ہے:",
-       "moveddeleted-notice": "یہ ایک حذف شدہ صفحہ ہے.\nصفحہ کا نوشتۂ حذف شدگی و منتقلی ذیل میں بطورِ حوالہ دیا جارہا ہے.",
+       "moveddeleted-notice": "اس صفحہ کو حذف کر دیا گیا ہے۔\nحوالہ کے لیے ذیل میں اس صفحہ کا نوشتہ حذف شدگی اور نوشتہ منتقلی درج ہے۔",
+       "moveddeleted-notice-recent": "معذرت، اس صفحہ کو حال ہی میں حذف کیا گیا ہے (گزشتہ چوبیس گھنٹوں میں)۔\nحوالہ کے لیے ذیل میں اس صفحہ کا نوشتہ حذف اور نوشتہ منتقلی موجود ہے۔",
        "log-fulllog": "پورا نوشتہ دیکھئے",
+       "edit-hook-aborted": "کسی رکاوٹ کی وجہ سے ترمیم کاری منسوخ کر دی گئی ہے۔\nاور کوئی وضاحت نہیں دی گئی۔",
        "edit-gone-missing": "صفحہ تجدید نہیں کیا جاسکتا.\nلگتا ہے یہ حذف ہوچکا ہے.",
        "edit-conflict": "تنازعۂ تدوین.",
        "edit-no-change": "آپ کی تدوین کو نظرانداز کردیا گیا، کیونکہ متن میں کوئی تبدیلی نہیں ہوئی تھی.",
        "postedit-confirmation-saved": "آپ کی ترمیم محفوظ ہوگئی۔",
        "edit-already-exists": "نیا صفحہ تخلیق نہیں کیا جاسکتا.\nیہ پہلے سے موجود ہے.",
        "defaultmessagetext": "طے شدہ پیغام کا متن",
+       "content-failed-to-parse": "ماڈل $1 کے $2 مواد کے تجزیہ میں ناکامی: $3",
        "invalid-content-data": "نادرست ڈیٹا مندرجات",
+       "content-not-allowed-here": "صفحہ [[$2]] پر \"$1\" مواد کی اجازت نہیں",
+       "editwarning-warning": "اس صفحہ کو چھوڑنے پر ممکن ہے جو تبدیلیاں آپ نے کی ہیں وہ سب ضائع ہو جائیں۔\nاگر آپ داخل ہیں تو اپنی ترجیحات کے خانہ «{{int:prefs-editing}}» سے اس انتباہ کو غیر فعال کر سکتے ہیں۔",
+       "editpage-invalidcontentmodel-title": "مواد کا ماڈل معاونت یافتہ نہیں",
+       "editpage-invalidcontentmodel-text": "مواد کا ماڈل \"$1\" معاونت یافتہ نہیں ہے۔",
+       "editpage-notsupportedcontentformat-title": "مواد کا فارمیٹ معاونت یافتہ نہیں",
+       "editpage-notsupportedcontentformat-text": "مواد کے ماڈل $2 کی جانب سے مواد کا فارمیٹ $1 معاونت یافتہ نہیں۔",
        "content-model-wikitext": "ویکی متن",
        "content-model-text": "سادہ متن",
        "content-model-javascript": "جاوا اسکرپٹ",
+       "content-model-css": "سی ایس ایس",
        "content-json-empty-object": "خالی آبجیکٹ",
        "content-json-empty-array": "خالی ایرے",
+       "deprecated-self-close-category": "صفحات مع نادرست ایچ ٹی ایم ایل ٹیگ",
+       "deprecated-self-close-category-desc": "اس صفحہ میں ایچ ٹی ایم ایل کے نادرست ٹیگ مثلاً <code>&lt;b/></code> or <code>&lt;span/></code> استعمال کیے گئے ہیں۔ چونکہ ایچ ٹی ایم ایل 5 میں ان ٹیگوں کا رویہ تبدیل ہو جائے گا، لہذا ویکی متن میں ان کا استعمال متروک ہو چکا ہے۔",
+       "duplicate-args-category": "سانچے میں دوہرے آرگومنٹ کے حامل صفحات",
+       "duplicate-args-category-desc": "وہ صفحات جن میں مکرر یا دوہرے آرگومنٹ مستعمل ہیں، مثلاً <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> یا <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>۔",
+       "expensive-parserfunction-category": "سنگین پارسر فنکشنوں کے بے پناہ استعمال والے صفحات",
+       "post-expand-template-inclusion-warning": "<strong>انتباہ:</strong> سانچہ کا حجم بہت زیادہ ہے۔ کچھ سانچے شامل نہیں ہو سکیں گے۔",
+       "post-expand-template-inclusion-category": "حجم سے متجاوز سانچوں والے صفحات",
+       "post-expand-template-argument-warning": "<strong>انتباہ:</strong> اس صفحہ میں موجود سانچہ کے کم از کم کسی ایک پیرامیٹر کا حجم بہت زیادہ ہے۔\nان پیرامیٹروں کو ترک کر دیا گیا ہے۔",
+       "post-expand-template-argument-category": "سانچہ کے ترک کردہ پیرامیٹروں کے حامل صفحات",
+       "parser-template-loop-warning": "سانچہ میں تکرار پایا گیا: [[$1]]",
+       "parser-template-recursion-depth-warning": "سانچہ میں تکرار کی گہرائی اپنی حد سے تجاوز کر گئی ($1)",
+       "language-converter-depth-warning": "لسانی مبدل کی گہرائی اپنی حد سے تجاوز کر گئی ($1)",
+       "node-count-exceeded-category": "گرہوں کی تعداد سے تجاوز کرنے والے صفحات",
+       "node-count-exceeded-category-desc": "اس صفحہ میں گرہیں اپنی مقررہ تعداد سے تجاوز کر گئیں۔",
+       "node-count-exceeded-warning": "صفحہ کی گرہ اپنی تعداد سے تجاوز کر گئی",
+       "expansion-depth-exceeded-category": "توسیع کی گہرائی سے تجاوز کرنے والے صفحات",
+       "expansion-depth-exceeded-category-desc": "اس صفحہ میں توسیع کی گہرائی اپنی حد سے تجاوز کر گئی۔",
+       "expansion-depth-exceeded-warning": "صفحہ میں توسیع کی گہرائی اپنی حد سے تجاوز کر گئی",
+       "parser-unstrip-loop-warning": "unstrip فنکشن میں تکرار پایا گیا",
+       "parser-unstrip-recursion-limit": "unstrip فنکشن میں تکرار اپنی حد سے تجاوز کر گیا ($1)",
+       "converter-manual-rule-error": "زبان کی دستی تبدیلی کے ضوابط میں نقص دریافت ہوا",
+       "undo-success": "اس ترمیم کو واپس پھیرا جا سکتا ہے۔\nبراہ کرم ذیل میں موجود موازنہ ملاحظہ فرمائیں اور یقین کر لیں کہ اس موازنے میں موجود فرق ہی آپ کا مقصود ہے۔ اس کے بعد تبدیلیوں کو محفوظ کر دیں، ترمیم واپس پھیر دی جائے گی۔",
+       "undo-failure": "درمیان میں متنازع ترامیم کی موجودگی کی بنا پر اس ترمیم کو واپس نہیں پھیرا جا سکا۔",
+       "undo-norev": "اس ترمیم کو واپس نہیں پھیرا جا سکا کیونکہ یہ موجود ہی نہیں یا حذف کر دی گئی ہے۔",
+       "undo-nochange": "معلوم ہوتا ہے کہ اس ترمیم کو پہلے ہی واپس پھیر دیا گیا ہے۔",
        "undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|تبادلہ خیال]]) کی جانب سے کی گئی ترمیم $1 رد کردی گئی ہے۔",
+       "undo-summary-username-hidden": "پوشیدہ صارف کے نسخہ $1 کو واپس پھیریں",
+       "cantcreateaccount-text": "[[User:$3|$3]] نے اس آئی پی پتہ (<strong>$1</strong>) کی کھاتہ سازی پر پابندی لگا رکھی ہے۔\n\n$3 نے «<em>$2</em>» وجہ بیان کی ہے",
+       "cantcreateaccount-range-text": "[[User:$3|$3]] نے <strong>$1</strong> رینج کے آئی پی پتوں پر جس میں آپ کا آئی پی پتہ (<strong>$4</strong>) بھی موجود ہے پر پابندی لگا دی ہے۔\n\n$3 نے «<em>$2</em>» وجہ بیان کی ہے",
        "viewpagelogs": "اس صفحہ کیلیے نوشتہ جات دیکھیے",
        "nohistory": "اِس صفحہ کیلئے کوئی تدوینی تاریخچہ موجود نہیں ہے.",
        "currentrev": "حـالیـہ تـجدید",
        "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-deleted-comment": "(تبصرہ حذف کی گيا ہے)",
        "rev-deleted-user": "(صارف نام حذف کیا گيا ہے)",
        "rev-deleted-event": "(نوشتہ کی تفصیلات ہٹا دی گئیں)",
-       "rev-delundel": "دکھاؤ/چھپاؤ",
+       "rev-deleted-user-contribs": "[صارف نام یا آئی پی پتہ ہٹا دیا گیا - شراکتوں سے ترمیم پوشیدہ ہو گئی]",
+       "rev-deleted-text-permission": "پیش نظر صفحہ کی یہ ترمیم <strong>حذف کر دی گئی ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-suppressed-text-permission": "پیش نظر صفحہ کی یہ ترمیم <strong>پوشیدہ کر دی گئی ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} نوشتہ پوشیدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-deleted-text-unhide": "پیش نظر صفحہ کی یہ ترمیم <strong>حذف کر دی گئی ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔\nتاہم اگر آپ چاہیں تو [$1 اس نسخے کو ابھی بھی دیکھ سکتے ہیں]۔",
+       "rev-suppressed-text-unhide": "پیش نظر صفحہ کی یہ ترمیم <strong>پوشیدہ کر دی گئی ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔\nتاہم اگر آپ چاہیں تو [$1 اس نسخے کو ابھی بھی دیکھ سکتے ہیں]۔",
+       "rev-deleted-text-view": "پیش نظر صفحہ کی یہ ترمیم <strong>حذف کر دی گئی ہے</strong>۔\nتاہم اسے آپ دیکھ سکتے ہیں، مزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-suppressed-text-view": "پیش نظر صفحہ کی یہ ترمیم <strong>پوشیدہ کر دی گئی ہے</strong>۔\nتاہم اسے آپ دیکھ سکتے ہیں، مزید تفصیلات [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-deleted-no-diff": "آپ اس فرق کو نہیں دیکھ سکتے کیونکہ دونوں میں سے کسی ایک ترمیم کو <strong>حذف کر دیا گیا ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-suppressed-no-diff": "آپ اس فرق کو نہیں دیکھ سکتے کیونکہ دونوں میں سے کسی ایک ترمیم کو <strong>حذف کر دیا گیا ہے</strong>۔",
+       "rev-deleted-unhide-diff": "اس فرق کی کسی ایک ترمیم کو <strong>حذف کر دیا گیا ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔\nتاہم اگر آپ چاہیں تو [$1 اس فرق کو ابھی بھی دیکھ سکتے ہیں]۔",
+       "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-showdeleted": "دکھاؤ",
        "revisiondelete": "نظرثانی حذف کریں/واپس لائیں",
        "revdelete-nooldid-title": "ناقص مقصود نظرثانی",
+       "revdelete-nooldid-text": "اس فنکشن کو جس نسخے پر انجام دینا ہے اسے آپ نے منتخب نہیں کیا، یا منتخب کردہ نسخہ موجود نہیں، یا آپ موجودہ نسخہ کو پوشیدہ کرنے کی کوشش کر رہے ہیں۔",
        "revdelete-no-file": "درج کردہ فائل موجود نہیں ہے۔",
+       "revdelete-show-file-confirm": "کیا آپ واقعی فائل «<nowiki>$1</nowiki>» کے مورخہ $2 بوقت $3 بجے حذف ہونے والے نسخے کو دیکھنا چاہتے ہیں؟",
        "revdelete-show-file-submit": "ہاں",
+       "revdelete-selected-text": "[[:$2]] {{PLURAL:$1|کا منتخب نسخہ|کے منتخب نسخے}}:",
+       "revdelete-selected-file": "[[:$2]] {{PLURAL:$1|کا منتخب فائل نسخہ|کے منتخب فائل نسخے}}:",
        "logdelete-selected": "{{PLURAL:$1|منتخب واقعۂ نوشتہ|منتخب واقعاتِ نوشتہ}}:",
+       "revdelete-text-text": "صفحہ کے تاریخچے میں حذف شدہ نسخے نظر آئی گے لیکن ان کا مواد عام صارفین کے لیے ناقابل رسائی ہوگا۔",
+       "revdelete-text-file": "فائل کے تاریخچے میں حذف شدہ نسخے نظر آئی گے لیکن ان کا مواد عام صارفین کے لیے ناقابل رسائی ہوگا۔",
+       "logdelete-text": "نوشتہ کے حذف شدہ اندراجات نوشتوں میں ظاہر ہوتے رہیں گے لیکن ان کا مواد عام صارفین کے لیے ناقابل رسائی ہوگا۔",
+       "revdelete-text-others": "جب تک اضافی پابندیاں نہیں لگائی جاتیں دیگر منتظمین کو اس پوشیدہ مواد تک رسائی اور اسے بحال کرنے کا اختیار حاصل ہوگا۔",
        "revdelete-confirm": "برائے مہربانی! یقین دِہانی کرلیجئے کہ آپ واقعی ایسا کرنا چاہتے ہیں، آپ اِس کے نتائج سے باخبر ہیں، اور آپ یہ [[{{MediaWiki:Policy-url}}|پالیسی]] کے مطابق کررہے ہیں.",
        "revdelete-legend": "رویتی پابندیاں لگائیں",
        "revdelete-hide-text": "نظرثانی متن چھپاؤ",
-       "revdelete-hide-image": "Ù\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": "ترمیم کار کا اسمِ صارف / آئی.پی پتہ چُھپاؤ",
+       "revdelete-hide-restricted": "منتظمین اور دیگر صارفین سے معلومات کو پوشیدہ کریں",
        "revdelete-radio-same": "(تبدیل مت کرو)",
        "revdelete-radio-set": "پوشیدہ",
        "revdelete-radio-unset": "ظاہر",
+       "revdelete-suppress": "منتظمین اور دیگر صارفین سے معلومات کو پوشیدہ کریں",
        "revdelete-unsuppress": "بحال شدہ نظرثانیوں پر پابندیاں ہٹاؤ",
        "revdelete-log": "وجہ",
-       "revdelete-success": "'''رؤیتِ نظرثانی کی تجدید کامیابی سے ہوئی.'''",
-       "logdelete-success": "'''نوشتۂ رویت کامیابی سے مرتب.'''",
+       "revdelete-submit": "منتخب {{PLURAL:$1|نسخے|نسخوں}} پر منطبق کریں",
+       "revdelete-success": "نسخہ کی مرئیت کی تجدید مکمل۔",
+       "revdelete-failure": "نسخہ کی مرئیت کی تجدید نہیں ہو سکی:\n$1",
+       "logdelete-success": "نوشتہ مرئیت میں تبدیلی مکمل۔",
        "logdelete-failure": "'''نوشتۂ رویت مرتب نہیں کیا جاسکتا:'''\n\n$1",
        "revdel-restore": "ظاہریت تبدیل کرو",
        "pagehist": "تاریخچۂ صفحہ",
        "deletedhist": "حذف شدہ تاریخچہ",
+       "revdelete-hide-current": "مورخہ $2، بوقت $1 بجے والے آئٹم کو پوشیدہ کرنے کے دوران میں نقص: یہ موجودہ نسخہ ہے۔ اسے پوشیدہ نہیں کیا جا سکتا۔",
+       "revdelete-show-no-access": "مورخہ $2، بوقت $1 بجے والا آئٹم دکھانے کے دوران میں نقص: اس نسخہ کو  بطور «محدود» نشان زد کر دیا گیا ہے۔ چنانچہ اب یہ آپ کی دسترس سے باہر ہے۔",
+       "revdelete-modify-no-access": "مورخہ $2، بوقت $1 بجے والے آئٹم میں تبدیلی کے دوران میں نقص: اس نسخہ کو  بطور «محدود» نشان زد کر دیا گیا ہے۔ چنانچہ اب یہ آپ کی دسترس سے باہر ہے۔",
+       "revdelete-modify-missing": "آئٹم آئی ڈی $1 میں تبدیلی کے دوران میں نقص: یہ نسخہ ڈیٹابیس میں موجود نہیں ہے!",
+       "revdelete-no-change": "<strong>انتباہ:</strong> مورخہ $2، بوقت $1 بجے والے آئٹم میں پہلے ہی سے مرئیت کی مطلوبہ ترتیبات موجود ہیں۔",
+       "revdelete-concurrent-change": "مورخہ $2، بوقت $1 بجے والے آئٹم میں تبدیلی کے دوران میں نقص: ایسا معلوم ہوتا ہے کہ آپ کی جانب سے تبدیلی کی کوشش کے دوران میں کسی اور نے اس میں تبدیلی کر دی ہے۔\nبراہ کرم نوشتے دیکھ لیں۔",
+       "revdelete-only-restricted": "مورخہ $2، بوقت $1 بجے والے آئٹم کو پوشیدہ کرنے کے دوران میں نقص: مرئیت کے دیگر اختیارات میں سے مزید کسی ایک اختیار کو منتخب کیے بغیر آپ ان آئٹموں کو منتظمین کی نگاہوں سے مخفی نہیں کر سکتے۔",
+       "revdelete-reason-dropdown": "* عمومی وجوہات حذف شدگی\n** کاپی رائٹ کی خلاف ورزی\n** نامناسب تبصرہ یا ذاتی معلومات\n** نامناسب صارف نام\n** ممکنہ طور پر افترا آمیر معلومات",
        "revdelete-otherreason": "دوسری/اضافی وجہ:",
        "revdelete-reasonotherlist": "کوئی اَور وجہ",
        "revdelete-edit-reasonlist": "تحذیفی وجوہات کی تدوین",
        "revdelete-offender": "نظرثانی مصنف:",
+       "suppressionlog": "نوشتہ پوشیدگی",
+       "suppressionlogtext": "ذیل میں ان حذف شدگیوں اور پابندیوں کی فہرست ہے جن میں منتظمین سے پوشیدہ رکھا گیا مواد موجود ہے۔\nموجودہ جاری پابندیوں اور معطل صارفین کی فہرست دیکھنے کے لیے [[Special:BlockList|فہرست پابندی]] ملاحظہ فرمائیں۔",
        "mergehistory": "تواریخِ صفحہ کا انضمام",
+       "mergehistory-header": "اس صفحہ کے ذریعہ آپ ماخذ صفحہ کے تاریخچہ کے نسخوں کو نئے صفحہ میں ضم کر سکتے ہیں۔\nالبتہ اس بات کا یقین کر لیں کہ اس تبدیلی کے بعد بھی تاریخچہ کا تسلسل حسب سابق برقرار رہے گا۔",
        "mergehistory-box": "دو صفحات کی نظرثانیوں کا انضمام:",
        "mergehistory-from": "مآخذ صفحہ:",
        "mergehistory-into": "صفحۂ مقصود:",
+       "mergehistory-list": "قابل ضم تاریخچہ",
        "mergehistory-go": "ضم پذیر ترامیم دِکھاؤ",
        "mergehistory-submit": "نظرثانیاں ضم کرو",
        "mergehistory-empty": "نظرثانیاں ضم نہیں کی جاسکتیں.",
+       "mergehistory-done": "$1 کے $3 {{PLURAL:$3|نسخے|نسخوں}} کو [[:$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-toobig": "تاریخچے کو ضم نہیں کیا جا سکتا، کیونکہ {{PLURAL:$1|نسخے|نسخوں}} کی مقررہ حد  $1 سے تجاوز کرنا ہوگا۔",
        "mergehistory-no-source": "مآخذ صفحہ $1 موجود نہیں.",
        "mergehistory-no-destination": "مقصود صفحہ $1 موجود نہیں.",
        "mergehistory-invalid-source": "مآخذ صفحہ کا عنوان صحیح ہونا چاہئے.",
        "mergehistory-reason": "وجہ:",
        "mergelog": "نوشتہ کا انضمام",
        "revertmerge": "غیر ضم",
+       "mergelogpagetext": "ذیل میں ان صفحات کی فہرست ہے جن کے تاریخچے حال ہی میں دوسرے صفحوں میں ضم کیے گئے ہیں۔",
        "history-title": "\"$1\" کا نظرثانی تاریخچہ",
        "difference-title": "\"$1\" کے نسخوں کے درمیان فرق",
+       "difference-title-multipage": "«$1» اور «$2» صفحوں کے درمیان فرق",
        "difference-multipage": "(فرق مابین صفحات)",
-       "lineno": "لکیر $1:",
+       "lineno": "سطر $1:",
        "compareselectedversions": "منتخب متـن کا موازنہ",
+       "showhideselectedversions": "منتخب نسخوں کی مرئیت تبدیل کریں",
        "editundo": "رد ترمیم",
        "diff-empty": "(کوئی فرق نہیں)",
-       "diff-multi-sameuser": "({{PLURAL: $1 | ایک متوسط نظرثانی | $1 کئی متوسط نظرثانیاں}}ایک ہی صارف کی جانب سے نہیں دکھائی گئی)",
-       "searchresults": "تلاش کا نتیجہ",
-       "searchresults-title": "نتائجِ تلاش برائے \"$1\"",
+       "diff-multi-sameuser": "(ایک ہی صارف کا {{PLURAL: $1 |ایک درمیانی نسخہ نہیں دکھایا گیا| $1 درمیانی نسخے نہیں دکھائے گئے}})",
+       "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» کے نتائج تلاش",
+       "titlematches": "عنوان صفحہ سے ملتا ہے",
+       "textmatches": "متن صفحہ سے ملتا ہے",
        "notextmatches": "کوئی بھی مماثل متن موجود نہیں",
        "prevn": "پچھلے {{PLURAL:$1|$1}}",
        "nextn": "اگلے {{PLURAL:$1|$1}}",
        "prev-page": "پچھلا صفحہ",
        "next-page": "اگلا صفحہ",
        "prevn-title": "پچھلے $1 {{PLURAL:$1|نتیجہ|نتائج}}",
-       "nextn-title": "آگے $1 {{PLURAL:$1|نتیجہ|نتائج}}",
-       "shown-title": "فی صفحہ $1 {{PLURAL:$1|نتیجہ|نتائج}} دِکھاؤ",
-       "viewprevnext": "دیکھیں($1 {{int:pipe-separator}} $2) ($3)۔",
-       "searchmenu-exists": "'''اِس ویکی پر \"[[:$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",
+       "search-rewritten": "$1 کے نتائج کی نمائش، اس کی بجائے آپ $2 کو تلاش کر سکتے ہیں۔",
        "search-interwiki-caption": "ساتھی منصوبے",
        "search-interwiki-default": "$1 نتائج:",
        "search-interwiki-more": "(مزید)",
        "search-relatedarticle": "متعلقہ",
        "searchrelated": "متعلقہ",
        "searchall": "تمام",
-       "search-nonefound": "استفسار کے مطابق نتائج نہیں ملے.",
+       "showingresults": "ذیل میں #<strong>$2</strong> سے {{PLURAL:$1|<strong>1</strong> تک نتیجہ دکھایا گیا ہے|<strong>$1</strong> تک نتائج دکھائے گئے ہیں}}۔",
+       "showingresultsinrange": "ذیل میں #<strong>$2</strong> سے #<strong>$3</strong> تک {{PLURAL:$1|<strong>1</strong> نتیجہ|<strong>$1</strong> نتائج}} ایک قطار میں درج {{PLURAL:$1|ہے|ہیں}}۔",
+       "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": "جانچ",
        "powersearch-toggleall": "تمام",
        "powersearch-togglenone": "کوئی نہیں",
+       "powersearch-remember": "اس انتخاب کو مستقبل کی تلاشوں کے لیے یاد رکھیں",
        "search-external": "بیرونی تلاش",
        "searchdisabled": "{{SITENAME}} تلاش غیرفعال.\nآپ فی الحال گوگل کے ذریعے تلاش کرسکتے ہیں.\nیاد رکھئے کہ اُن کے {{SITENAME}} اشاریے ممکناً پرانے ہوسکتے ہیں.",
+       "search-error": "تلاش کے دوران میں کوئی نقص واقع ہوا: $1",
        "preferences": "ترجیحات",
        "mypreferences": "ترجیحات",
        "prefs-edits": "تعداد ترامیم:",
+       "prefsnologintext2": "اپنی ترجیحات میں تبدیلی کے لیے براہ کرم لاگ ان کریں",
        "prefs-skin": "جِلد",
        "skin-preview": "پیش منظر",
        "datedefault": "کوئی ترجیح نہیں",
+       "prefs-labs": "تجرباتی خصوصیتیں",
        "prefs-user-pages": "صارف صفحات",
        "prefs-personal": "پروفائل",
        "prefs-rc": "حالیہ تبدیلیاں",
        "prefs-editwatchlist-raw": "زیر نظر خام فہرست میں ترمیم کریں",
        "prefs-editwatchlist-clear": "اپنی زیر نظر فہرست صاف کریں",
        "prefs-watchlist-days": "زیر نظر فہرست میں نظر آنے والے ایام:",
-       "prefs-watchlist-days-max": "زیادہ سے زیادہ $1 دن",
+       "prefs-watchlist-days-max": "زیادہ سے زیادہ $1 {{PLURAL:$1|دن}}",
        "prefs-watchlist-edits": "توسیع شدہ زیر نظر فہرست میں نظر آنے والی تبدیلیوں کی زیادہ سے زیادہ تعداد:",
        "prefs-watchlist-edits-max": "زیادہ سے زیادہ تعداد: 1000",
        "prefs-watchlist-token": "زیر نظر فہرست کی کلید:",
        "prefs-misc": "دیگر",
        "prefs-resetpass": "پاس ورڈ تبدیل کریں",
-       "prefs-changeemail": "برقی ڈاک پتہ (e-mail address) تبدیل کریں",
+       "prefs-changeemail": "برقی ڈاک پتا تبدیل یا حذف کریں",
        "prefs-setemail": "برقی پتہ دیں",
        "prefs-email": "برقی خط کے اختیارات",
        "prefs-rendering": "ظاہریت",
        "stub-threshold-sample-link": "نمونہ",
        "stub-threshold-disabled": "غیر فعال",
        "recentchangesdays": "حالیہ تبدیلیوں میں دکھائے جانے والے ایّام:",
-       "recentchangesdays-max": "زیادہ سے زیادہ $1 دن",
+       "recentchangesdays-max": "زیادہ سے زیادہ $1 {{PLURAL:$1|دن}}",
        "recentchangescount": "دکھائی جانے والی ترامیم کی تعداد:",
        "prefs-help-recentchangescount": "اِس میں حالیہ تبدیلیاں، تاریخچے اور نوشتہ جات شامل ہیں۔",
        "prefs-help-watchlist-token2": "یہ آپ کی زیر نظر فہرست کے ویب فیڈ کی خفیہ کلید ہے۔\nاسے خفیہ رکھیں، تاکہ کوئی دوسرا شخص آپ کی زیر نظر فہرست نہ دیکھ سکے۔\nاگر آپ کو کلید تبدیل کرنی ہو تو [[Special:ResetTokens|یہاں کلک کریں]]۔",
        "savedprefs": "آپ کی ترجیحات محفوظ ہوگئیں۔",
+       "savedrights": "{{GENDER:$1|$1}} کے اختیارات محفوظ ہو گئے۔",
        "timezonelegend": "منطقۂ وقت:",
        "localtime": "مقامی وقت:",
        "timezoneuseserverdefault": "ویکی کا طے شدہ استعمال کریں ($1)",
        "prefs-custom-css": "شخصی سی ایس ایس",
        "prefs-custom-js": "شخصی جاوا اسکرپٹ",
        "prefs-common-css-js": "جملہ پوشاکوں کے لیے مشترکہ سی ایس ایس/جاوا اسکرپٹ:",
+       "prefs-reset-intro": "آپ اس صفحہ کے ذریعہ اپنی موجودہ ترجیحات کو سائٹ کی ابتدائی ترتیبات کے مطابق ڈھال سکتے ہیں۔\nلیکن اسے واپس نہیں پھیرا جا سکتا۔",
        "prefs-emailconfirm-label": "برقی خط کی تصدیق:",
        "youremail": "برقی خط:",
        "username": "صارف:",
        "yourrealname": "* اصلی نام",
        "yourlanguage": "زبان:",
        "yourvariant": "متغیّر:",
+       "prefs-help-variant": "اس ویکی کے صفحات دکھانے کے لیے آپ کا پسندیدہ لہجہ یا املا۔",
        "yournick": "شخصی دستخط:",
        "prefs-help-signature": "تبادلۂ خیال صفحات پر تبصرہ تحریر کرنے کے بعد یہ \"<nowiki>~~~~</nowiki>\" علامتیں درج کرنی چاہئیں، یہ علامتیں از خود آپ کے دستخط اور وقت میں تبدیل ہو جائیں گی۔",
        "badsig": "ناقص خام دستخط.\nHTML tags جانچئے.",
        "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": "دستخط",
        "prefs-displaywatchlist": "نمائش کے اختیارات",
        "prefs-tokenwatchlist": "ٹوکن",
        "prefs-diffs": "فرق",
+       "prefs-help-prefershttps": "یہ ترجیح آپ کے اگلے لاگ ان پر اثر انداز ہوگی۔",
+       "prefswarning-warning": "ترجیحات میں آپ کی جانب سے کی جانے والی تبدیلیاں ابھی محفوظ نہیں ہوئی ہیں۔\nاگر آپ «$1» پر کلک کیے بغیر اس صفحہ کو چھوڑ دیں تو آپ کی تبدیلیاں محفوظ نہیں ہوگی۔",
+       "prefs-tabs-navigation-hint": "نکتہ: مختلف خانوں میں جانے کے لیے آپ دائیں اور بائیں کی جہت نما کلیدیں استعمال کر سکتے ہیں۔",
        "userrights": "حقوقِ صارف کی نظامت",
        "userrights-lookup-user": "گروہائے صارف کا انتظام",
        "userrights-user-editname": "کوئی اسم‌صارف داخل کیجئے:",
-       "editusergroup": "ترمیم گروہائے صارف",
-       "editinguser": "تبدیلی اختیارات صارف برائے {{GENDER:$1|صارف}} <strong>[[صارف:$1|$1]]</strong> $2",
+       "editusergroup": "{{GENDER:$1|صارف}} کے گروہوں میں ترمیم کریں",
+       "editinguser": "{{GENDER:$1|صارف}} <strong>[[User:$1|$1]]</strong> $2 کے اختیارات میں تبدیلی",
        "userrights-editusergroup": "ترمیم گروہائے صارف",
-       "saveusergroups": "گروہائے صارف محفوظ",
+       "saveusergroups": "{{GENDER:$1|صارف}} کے گروہوں کو محفوظ کریں",
        "userrights-groupsmember": "رکنِ:",
        "userrights-groupsmember-auto": "اعتباری صارف در",
        "userrights-groups-help": "آپ ان گروہان میں تبدیلی کرسکتے ہیں جن سے صارف متعلق ہے: \n* نشان زد خانہ کا مطلب یہ ہے کہ صارف کا تعلق اس گروہ سے ہے۔ \n* غیر نشان زد خانہ کا مطلب یہ ہے کہ صارف کا تعلق اس گروہ سے نہیں ہے۔ \n* یہ * علامت اس بات کا اشارہ ہے کہ آپ اس گروہ کو نہیں ہٹا سکتے جسے ایک مرتبہ آپ نے شامل کردیا ہو، یا اس کے بر عکس۔",
        "userrights-reason": "وجہ:",
        "userrights-no-interwiki": "دوسرے ویکیوں پر حقوقِ صارف میں ترمیم کی آپ کو اجازت نہیں ہے.",
+       "userrights-nodatabase": "ڈیٹابیس $1 موجود نہیں یا مقامی نہیں۔",
+       "userrights-nologin": "اختیارات تفویض کرنے کے لیے آپ کا کسی منتظم کھاتے سے [[Special:UserLogin|داخل ہونا]] ضروری ہے۔",
+       "userrights-notallowed": "آپ کو  اختیارات تفویض کرنے یا انہیں واپس لینے کی اجازت نہیں ہے۔",
        "userrights-changeable-col": "مجموعات جو آپ تبدیل کرسکتے ہیں",
        "userrights-unchangeable-col": "مجموعات جو آپ تبدیل نہیں کرسکتے",
+       "userrights-conflict": "اختیارات کی تبدیلی میں تنازعہ! براہ کرم نظر ثانی کریں اور اپنی تبدیلیوں کی تصدیق کریں۔",
+       "userrights-removed-self": "آپ نے اپنے اختیارات ختم کر لیے ہیں، چنانچہ اب یہ صفحہ آپ کی دسترس سے باہر ہو گیا ہے۔",
        "group": "گروہ:",
        "group-user": "صارفین",
        "group-autoconfirmed": "خود توثیق شدہ صارفین",
        "grouppage-bot": "{{ns:project}}:روبہ جات",
        "grouppage-sysop": "{{ns:project}}:منتظمین",
        "grouppage-bureaucrat": "{{ns:project}}:مامورین اداری",
-       "right-upload": "ملفات زبراثقال (اپ لوڈ) کریں",
-       "right-writeapi": "اے پی آئی لکھائی کا استعمال",
+       "grouppage-suppress": "{{ns:project}}:پوشیدگی",
+       "right-read": "مطالعہ صفحات",
+       "right-edit": "ترمیم صفحات",
+       "right-createpage": "تخلیق صفحات (تبادلہ خیال صفحات نہیں)",
+       "right-createtalk": "تخلیق تبادلہ خیال صفحات",
+       "right-createaccount": "کھاتہ سازی",
+       "right-autocreateaccount": "بیرونی صارف کھاتے کے ذریعہ خودکار لاگ ان",
+       "right-minoredit": "ترامیم کی بطور معمولی ترمیم نشان زدگی",
+       "right-move": "منتقلی صفحات",
+       "right-move-subpages": "منتقلی صفحات مع ذیلی صفحات",
+       "right-move-rootuserpages": "منتقلی صارف صفحات",
+       "right-move-categorypages": "منتقلی زمرہ صفحات",
+       "right-movefile": "منتقلی فائل",
+       "right-suppressredirect": "پرانے عنوان سے رجوع مکرر کے بغیر منتقلی صفحہ",
+       "right-upload": "فائلوں کو اپلوڈ کرنا",
+       "right-reupload": "موجود فائلوں کا دوبارہ اپلوڈ",
+       "right-reupload-own": "ذاتی اپلوڈ کردہ فائلوں کا دوبارہ اپلوڈ",
+       "right-reupload-shared": "مقامی طور پر مشترکہ میڈیا کے ذخیرے میں فائلوں کی منسوخی",
+       "right-upload_by_url": "بذریعہ یوآرایل فائل اپلوڈ",
+       "right-purge": "بدون تصدیق صفحہ کے کیشے کی صفائی",
+       "right-autoconfirmed": "آئی پی پر مبنی پابندیوں سے غیر متاثر",
+       "right-bot": "خودکار عمل کے طور پر تعامل",
+       "right-nominornewtalk": "تبادلۂ خیال صفحات میں معمولی ترامیم کرنے پر نئے پیغام کے اعلان کی عدم نمائش",
+       "right-apihighlimits": "API کا بڑے پیمانے پر استعمال",
+       "right-writeapi": "اے پی آئی تحریر کا استعمال",
        "right-delete": "صفحات حذف کریں",
+       "right-bigdelete": "بڑے تاریخچوں پر مشتمل صفحات کی حذف شدگی",
+       "right-deletelogentry": "نوشتہ کے مخصوص اندراجات کی حذف شدگی و بحالی",
+       "right-deleterevision": "صفحات کے مخصوص نسخوں کی حذف شدگی و بحالی",
+       "right-deletedhistory": "ملحقہ متن کے بغیر تاریخچہ کے حذف شدہ اندراجات کا معائنہ",
+       "right-deletedtext": "حذف شدہ متن اور حذف شدہ نسخوں کے درمیان میں تبدیلیوں کا معائنہ",
+       "right-browsearchive": "حذف شدہ صفحات میں تلاش",
+       "right-undelete": "بحالی صفحہ",
+       "right-suppressrevision": "صفحات کے مخصوص نسخوں کا معائنہ و پوشیدگی",
+       "right-viewsuppressed": "پوشیدہ نسخوں کا معائنہ",
+       "right-suppressionlog": "نجی نوشتوں کا معائنہ",
+       "right-block": "صارفین کی ترمیم کاری پر پابندی کا نفاذ",
+       "right-blockemail": "برقی خط بھیجنے پر پابندی کا نفاذ",
+       "right-hideuser": "عمومی نگاہ سے مخفی رکھتے ہوئے صارف نام پر پابندی کا نفاذ",
+       "right-ipblock-exempt": "آئی پی، خودکار اور رینج پر پابندیوں سے خلاصی",
+       "right-unblockself": "رفع پابندی",
+       "right-protect": "آبشاری حفاظت کے حامل صفحات میں ترمیم اور درجات حفاظت میں تبدیلی",
+       "right-editprotected": "\"{{int:protect-level-sysop}}\" کے طور پر محفوظ صفحات میں ترمیم",
+       "right-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" کے طور پر محفوظ صفحات میں ترمیم",
+       "right-editcontentmodel": "صفحہ کے مواد کے ماڈل میں ترمیم",
+       "right-editinterface": "صارف انٹرفیس میں ترمیم",
+       "right-editusercssjs": "دیگر صارفین کی سی ایس ایس اور جاوا اسکرپٹ فائلوں میں ترمیم",
+       "right-editusercss": "دیگر صارفین کی سی ایس ایس فائلوں میں ترمیم",
+       "right-edituserjs": "دیگر صارفین کی جاوا اسکرپٹ فائلوں میں ترمیم",
+       "right-editmyusercss": "اپنی ذاتی سی ایس ایس فائلوں میں ترمیم",
+       "right-editmyuserjs": "اپنی ذاتی جاوا اسکرپٹ فائلوں میں ترمیم",
+       "right-viewmywatchlist": "اپنی ذاتی زیرنظر فہرست کا معائنہ",
+       "right-editmywatchlist": "اپنی ذاتی زیرنظر فہرست میں ترمیم۔ خیال رکھیں کہ اس اختیار کے بغیر بھی بعض اقدامات کے ذریعہ صفحات شامل کیے جا سکتے ہیں۔",
+       "right-viewmyprivateinfo": "اپنی ذاتی نجی معلومات کا معائنہ (مثلاً برقی ڈاک پتہ، حقیقی نام وغیرہ)",
+       "right-editmyprivateinfo": "اپنی ذاتی نجی معلومات میں ترمیم (مثلاً برقی ڈاک پتہ، حقیقی نام وغیرہ)",
+       "right-editmyoptions": "اپنی ذاتی ترجیحات میں ترمیم",
+       "right-rollback": "کسی مخصوص صفحہ پر ترمیم کرنے والے آخری صارف کی ترامیم کا فوری استرجع",
+       "right-markbotedits": "استرجع شدہ ترامیم کی روبہ ترامیم کے طور پر نشان زدگی",
+       "right-noratelimit": "وقت کی پابندیوں سے آزادی",
+       "right-import": "دوسری ویکیوں سے صفحات کی درآمد",
+       "right-importupload": "بذریعہ اپلوڈ صفحات کی درآمد",
+       "right-patrol": "دیگر صارفین کی ترامیم کی مراجعت",
+       "right-autopatrol": "ذاتی ترامیم کی خودکار مراجعت",
+       "right-patrolmarks": "حالیہ تبدیلیوں میں علامات مراجعت کا معائنہ",
+       "right-unwatchedpages": "نادیدہ صفحات کی فہرست کا معائنہ",
+       "right-mergehistory": "صفحات کے تاریخچے کا انضمام",
+       "right-userrights": "تمام اختیارات میں ترمیم",
+       "right-userrights-interwiki": "دوسری ویکیوں پر صارف کے اختیارات میں ترمیم",
+       "right-siteadmin": "ڈیٹابیس کو مقفل یا غیر مقفل کرنا",
+       "right-override-export-depth": "پانچویں سطح کی گہرائی تک مربوط صفحات پر مشتمل صفحات کی برآمد",
        "right-sendemail": "دیگر صارفین کو برقی ڈاک بھیجیں",
+       "right-passwordreset": "پاس ورڈ کی ترتیب نو کے حامل برقی خطوط کا معائنہ",
+       "right-managechangetags": "[[Special:Tags|ٹیگوں]] کی تخلیق اور (غیر)فعالی",
+       "right-applychangetags": "کسی کی تبدیلیوں کے ساتھ [[Special:Tags|ٹیگوں]] کا اطلاق",
+       "right-changetags": "انفرادی نسخوں اور نوشتہ کے اندراج پر [[Special:Tags|ٹیگوں]] کا حذف و اضافہ",
+       "right-deletechangetags": "ڈیٹابیس سے [[Special:Tags|ٹیگوں]] کی حذف شدگی",
+       "grant-generic": "\"$1\" مجموعہ اختیارات",
+       "grant-group-page-interaction": "صفحات سے تعامل",
+       "grant-group-file-interaction": "میڈیا سے تعامل",
+       "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-createaccount": "اس کھاتے کو بنانے",
+       "action-autocreateaccount": "اس بیرونی کھاتے کو خودکار طور پر بنانے",
+       "action-history": "اس صفحہ کا تاریخچہ دیکھنے",
+       "action-minoredit": "اس ترمیم کو معمولی نشان زد کرنے",
+       "action-move": "اس صفحہ کو منتقل کرنے",
+       "action-move-subpages": "اس صفحہ اور اس کے ذیلی صفحات کو منتقل کرنے",
+       "action-move-rootuserpages": "اصل صارف صفحات کو منتقل کرنے",
+       "action-move-categorypages": "زمرے کے صفحات کو منتقل کرنے",
+       "action-movefile": "اس فائل کو منتقل کرنے",
+       "action-upload": "اس فائل کو اپلوڈ کرنے",
+       "action-reupload": "اس موجودہ فائل کو دوبارہ اپلوڈ کرنے",
+       "action-reupload-shared": "مشترکہ ذخیرے میں فائل کو منسوخ کرنے",
+       "action-upload_by_url": "بذریعہ یوآرایل اس فائل کو اپلوڈ کرنے",
+       "action-writeapi": "اے پی آئی تحریر کے استعمال کرنے",
+       "action-delete": "یہ صفحہ حذف کرنے",
+       "action-deleterevision": "یہ نسخہ حذف کرنے",
+       "action-deletedhistory": "اس صفحہ کا حذف شدہ تاریخچہ دیکھنے",
+       "action-browsearchive": "حذف شدہ صفحات میں تلاش کرنے",
+       "action-undelete": "اس صفحہ کو بحال کرنے",
+       "action-suppressrevision": "اس پوشیدہ ترمیم کی نظرثانی اور بحال کرنے",
+       "action-suppressionlog": "نجی نوشتہ کے دیکھنے",
+       "action-block": "اس صارف پر پابندی لگانے",
+       "action-protect": "اس صفحہ کے درجات حفاظت میں تبدیلی کرنے",
+       "action-rollback": "آخری صارف جس نے ایک متعین صفحہ میں ترمیم کی ہے، اس کی ترامیم کا فوری استرجع کرنے",
+       "action-import": "دوسری ویکی سے صفحات درآمد کرنے",
+       "action-importupload": "بذریعہ اپلوڈ صفحات درآمد کرنے",
+       "action-patrol": "دیگر صارفین کی ترامیم کو بطور مراجعت شدہ نشان زد کرنے",
+       "action-autopatrol": "اپنی ترمیم کو بطور مراجعت شدہ نشان زد کرنے",
+       "action-unwatchedpages": "نادیدہ صفحات کی فہرست دیکھنے",
+       "action-mergehistory": "اس صفحہ کے تاریخچہ کو ضم کرنے",
+       "action-userrights": "تمام اختیارات میں تبدیلی کرنے",
+       "action-userrights-interwiki": "دوسری ویکیوں پر صارف کے اختیارات میں ترمیم کرنے",
+       "action-siteadmin": "ڈیٹابیس کو مقفل کرنے یا کھولنے",
+       "action-sendemail": "برقی خطوط روانہ کرنے",
+       "action-editmywatchlist": "اپنی زیرنظر فہرست میں ترمیم کرنے",
+       "action-viewmywatchlist": "اپنی زیر نظر فہرست دیکھنے",
+       "action-viewmyprivateinfo": "اپنی نجی معلومات دیکھنے",
+       "action-editmyprivateinfo": "اپنی نجی معلومات میں ترمیم کرنے",
+       "action-editcontentmodel": "صفحہ کے مواد کے ماڈل میں ترمیم کرنے",
+       "action-managechangetags": "ٹیگوں کو بنانے اور انہیں غیر فعال کرنے",
+       "action-applychangetags": "اپنی تبدیلیوں پر ٹیگ گاری کرنے",
+       "action-changetags": "انفرادی نسخوں اور نوشتہ کے اندراج پر ٹیگوں کو لگانے اور ہٹانے",
+       "action-deletechangetags": "ڈیٹابیس سے ٹیگوں کو حذف کرنے",
+       "action-purge": "اس صفحہ کا کیشے خالی کرنے",
        "nchanges": "$1 {{PLURAL:$1|تبدیلی|تبدیلیاں}}",
+       "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|آخری آمد کے بعد سے}}",
        "enhancedrc-history": "تاریخچہ",
        "recentchanges": "حالیہ تبدیلیاں",
        "recentchanges-legend": "اِختیاراتِ حالیہ تبدیلیاں",
-       "recentchanges-summary": "اس صفحے پر ویکی میں ہونے والی تازہ تریں تبدیلیوں کا مشاہدہ کیجیۓ۔",
-       "recentchanges-feed-description": "اس خورد میں ویکی پر ہونے والی تازہ تریں تبدیلیوں کا مشاہدہ کیجیۓ۔",
+       "recentchanges-summary": "اس صفحے پر ویکی میں ہونے والی تازہ تریں تبدیلیوں کا مشاہدہ کریں۔",
+       "recentchanges-noresult": "مقررہ مدت کے دوران میں اس معیار سے مشابہت رکھنے والی کوئی تبدیلی نہیں ہوئی۔",
+       "recentchanges-feed-description": "اس فیڈ میں ویکی پر ہونے والی تازہ تریں تبدیلیوں کا مشاہدہ کریں۔",
        "recentchanges-label-newpage": "یہ ترمیم ایک نئے صفحے کی تخلیق ہے",
        "recentchanges-label-minor": "یہ ایک معمولی ترمیم ہے",
        "recentchanges-label-bot": "اس ترمیم کو ایک روبہ نے انجام دیا ہے",
        "recentchanges-label-unpatrolled": "اس ترمیم کی اب تک مراجعت نہیں کی گئی",
-       "recentchanges-label-plusminus": "صÙ\81Ø­Û\81 Ú©Ø§ Ø­Ø¬Ù\85 ØªØ¨Ø¯Û\8cÙ\84 Ø´Ø¯Û\81 Ø¨Ù\84حاظ Ø¨Ø§Ø¦Ù¹ Ù\85Ù\82دار",
-       "recentchanges-legend-heading": "<strong>اختیارات</strong>",
+       "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": "دکھائیں",
        "rcnotefrom": "ذیل میں <strong>$2</strong> سے کی گئی {{PLURAL:$5|تبدیلی|تبدیلیاں}} <strong>$1</strong> تک دکھائی جا رہی ہیں۔",
        "rcshowhideminor": "معمولی ترامیم $1",
        "rcshowhideminor-show": "دکھائیں",
        "rcshowhideminor-hide": "چھپائیں",
-       "rcshowhidebots": "خودکار صارف $1",
+       "rcshowhidebots": "خودکار صارفین $1",
        "rcshowhidebots-show": "دکھائیں",
        "rcshowhidebots-hide": "چھپائیں",
-       "rcshowhideliu": "داخل شدہ صارف $1",
-       "rcshowhideliu-show": "دکھاؤ",
+       "rcshowhideliu": "مندرج صارفین $1",
+       "rcshowhideliu-show": "دکھائÛ\8cÚº",
        "rcshowhideliu-hide": "چھپائیں",
        "rcshowhideanons": "گمنام صارف $1",
-       "rcshowhideanons-show": "دکھاؤ",
+       "rcshowhideanons-show": "دکھائÛ\8cÚº",
        "rcshowhideanons-hide": "چھپائیں",
        "rcshowhidepatr": "$1 مراجعت شدہ ترامیم",
        "rcshowhidepatr-show": "دکھاؤ",
        "rcshowhidepatr-hide": "چھپائيں",
        "rcshowhidemine": "ذاتی ترامیم $1",
-       "rcshowhidemine-show": "دکھاؤ",
+       "rcshowhidemine-show": "دکھائÛ\8cÚº",
        "rcshowhidemine-hide": "چھپائیں",
        "rcshowhidecategorization": "صفحاتی زمرہ بندی $1",
        "rcshowhidecategorization-show": "دکھائیں",
        "diff": "فرق",
        "hist": "تاریخچہ",
        "hide": "چھـپائیں",
-       "show": "دکھاؤ",
+       "show": "دکھائÛ\8cÚº",
        "minoreditletter": "م",
        "newpageletter": "نیا ..",
        "boteditletter": " خودکار",
+       "number_of_watching_users_pageview": "[$1 مشاہد {{PLURAL:$1|صارف|صارفین}}]",
+       "rc_categories": "ان زمروں تک محدود رکھیں («|» سے علاحدہ کریں):",
        "rc_categories_any": "کوئی بھی منتخب",
-       "rc-change-size-new": "$1 {{PLURAL:$1|بائٹ|بائٹ}} تبدیلی کے بعد",
+       "rc-change-size-new": "تبدیلی کے بعد $1 {{PLURAL:$1|بائٹ}}",
+       "newsectionsummary": "/* $1 */ نیا قطعہ",
        "rc-enhanced-expand": "تفصیلات دکھائیں",
        "rc-enhanced-hide": "تفصیلات چھپائیے",
+       "rc-old-title": "اصلاً «$1» کے عنوان سے تخلیق شدہ",
        "recentchangeslinked": "متعلقہ تبدیلیاں",
        "recentchangeslinked-feed": "متعلقہ تبدیلیاں",
        "recentchangeslinked-toolbox": "متعلقہ تبدیلیاں",
        "recentchangeslinked-title": "\"$1\" سے متعلقہ تبدیلیاں",
-       "recentchangeslinked-summary": "یہ ان تبدیلیوں کی فہرست ہے جو حال ہی میں کسی مخصوص صفحہ سے مربوط صفحات (یا مخصوص زمرہ کے اراکین) میں کی گئی ہیں\n\n[[Special:Watchlist|آپ کی زیر نظر فہرست]] میں یہ صفحات متجل (bold) نظر آئیں گےـ",
-       "recentchangeslinked-page": "صفحۂ منصوبہ دیکھئے",
+       "recentchangeslinked-summary": "یہ ان تبدیلیوں کی فہرست ہے جو حال ہی میں کسی مخصوص صفحہ سے مربوط صفحات (یا مخصوص زمرہ کے اراکین) میں کی گئی ہیں۔\n\n[[Special:Watchlist|آپ کی زیر نظر فہرست]] میں یہ صفحات <strong>جلی</strong نظر آئیں گےـ",
+       "recentchangeslinked-page": "صفحہ کا نام:",
+       "recentchangeslinked-to": "اس کی بجائے درج کردہ صفحہ سے مربوط صفحات کی تبدیلیاں دکھائیں",
        "recentchanges-page-added-to-category": "[[:$1]] کو زمرہ میں شامل کیا گیا",
-       "recentchanges-page-added-to-category-bundled": "[[:$1]] اور {{PLURAL:$2|ایک صفحہ|$2 صفحات}} زمرہ میں شامل {{PLURAL:$2|کیا گیا|$2 کیے گئے}}",
+       "recentchanges-page-added-to-category-bundled": "[[:$1]] کو زمرہ میں شامل کر دیا گیا، [[Special:WhatLinksHere/$1|یہ صفحہ دیگر صفحات میں بھی موجود ہے]]",
        "recentchanges-page-removed-from-category": "[[:$1]] کو زمرہ سے ہٹایا",
+       "recentchanges-page-removed-from-category-bundled": "[[:$1]] زمرے سے ہٹا دیا گیا ہے، [[Special:WhatLinksHere/$1|یہ صفحہ دیگر صفحات میں بھی موجود ہے]]",
        "autochange-username": "میڈیاویکی خودکار تبدیلیاں",
-       "upload": "اپلوڈ",
-       "uploadbtn": "زبراثقال ملف (اپ لوڈ فائل)",
-       "reuploaddesc": "زبراثقال ورقہ (فارم) کیجانب واپس۔",
+       "upload": "فائل اپلوڈ کریں",
+       "uploadbtn": "فائل اپلوڈ کریں",
+       "reuploaddesc": "اپلوڈ منسوخ کرکے اپلوڈ فارم کی جانب واپس جائیں",
+       "upload-tryagain": "فائل کی تبدیل شدہ وضاحت روانہ کریں",
        "uploadnologin": "آپ داخل شدہ حالت میں نہیں",
        "uploadnologintext": "فائلیں اپلوڈ کرنے کے لیے براہ کرم $1 ہوں",
-       "uploadtext": "\n'''اطلاع''': اگر آپ اپنی فائل اپلوڈ کرتے وقت خلاصہ کے خانے میں درج ذیل دو باتوں کی وضاحت نہیں کریں گے تو اس فائل کو حذف کیا جاسکتا ہے:\n# فائل کا '''مـاخـذ''' ، یعنی:\n#*اگر یہ آپ نے خود تخلیق کی ہے تو اسے بیان کریں۔\n#*اگر یہ آن لائن دستیاب ہے تو اس سائٹ کا  '''ربط''' درج کریں۔\n#*اگر آپ نے اسے کسی دوسری زبان کے {{SITENAME}} سے لیا ہے تو اسکا نام تحریر کریں۔\n#صاحب حق طبع و نشر اور فائل کے اجازت نامہ کے بارے میں:\n#* فائل کے اجازت نامہ کے متعلق یہ درج کریں کہ اس کی موجودہ حیثیت کیا ہے۔\n#*اگر آپ خود اسکا حق طبع و نشر رکھتے ہیں تو آپ پر لازم ہے کہ آپ اسے [[دائرۂ عام]] (پبلک ڈومین) میں بھی شائع کریں۔\n\nجب کوئی صارف مستقل ایسی فائل اپلوڈ کرتا رہے جس کے اجازت نامہ کے بارے میں غلط بیانی کی گئی ہو یا وہ مستقل ایسی تصاویر اپلوڈ کرے جن کے بارے میں کوئی وضاحت موجود نہ ہو تو ایسی صورت میں اس صارف پر پابندی لگائے جانے کا قوی امکان موجود ہے۔\n\nفائل اپلوڈ کرنے کے لیے ذیل میں موجود فارم استعمال کریں، اگر آپ جملہ اپلوڈ کردہ تصاویر کو دیکھنا یا تلاش کرنا چاہتے ہیں تو [[Special:FileList|اس فہرست]] کو ملاحظہ فرمائیں۔ <br /> تمام اپلوڈ کردہ و حذف شدہ تصاویر کو [[Special:Log/upload|نوشتۂ منتقلی]] میں درج کر لیا جاتا ہے۔\n\nتصویر کی منتقلی کے بعد، اسکو کسی صفحہ پر رکھنے کیلیے مندرجہ ذیل طریقہ سے استعمال کریں۔\n\n'''<nowiki>[[تصویر:فائل کا نام|متبادل متن]]</nowiki>'''\n\n* مندرجہ بالا رموز آپ انگریزی میں بھی درج کرسکتے ہیں، یعنی\n<nowiki>[[Image:File name|Alt.text]]</nowiki>\n* فائل کا ربط درج کرنے کے لیے۔ '''<code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code>'''\n* ملف کا نام؛ حرف ابجد کے لیے حساس ہے لہذا اگر اپلوڈ کرتے وقت فائل کا نام -- name:JPG  ہے اور آپ name:jpg یــا Name:jpg کا ربط درج کرتے ہیں تو ربط کام نہیں کرے گا۔",
-       "uploadlogpage": "نوشتۂ زبراثقال (اپ لوڈ لاگ)",
-       "uploadlogpagetext": "درج ذیل میں حالیہ زبراثقال (اپ لوڈ) کی گئی املاف (فائلوں) کی فہرست دی گئی ہے۔",
+       "upload_directory_missing": "اپلوڈ فولڈر ($1) موجود نہیں اور ویب سرور کے ذریعہ اسے تخلیق نہیں کیا جا سکا۔",
+       "upload_directory_read_only": "اپلوڈ فولڈر ($1) میں ویب سرور لکھ نہیں پا رہا ہے۔",
+       "uploaderror": "اپلوڈ کے دوران میں نقص",
+       "upload-recreate-warning": "<strong>انتباہ: اس نام کی فائل حذف یا منتقل کر دی گئی ہے۔</strong>\n\nآسانی کے لیے ذیل میں اس صفحہ کا نوشتہ منتقلی و حذف شدگی درج ہے:",
+       "uploadtext": "فائلیں اپلوڈ کرنے کے لیے درج ذیل فارم پُر کریں۔\n\n'''اطلاع''': اگر آپ اپنی فائل اپلوڈ کرتے وقت خلاصہ کے خانے میں درج ذیل دو باتوں کی وضاحت نہیں کریں گے تو اس فائل کو حذف کیا جاسکتا ہے:\n# فائل کا '''مـاخـذ''' ، یعنی:\n#*اگر یہ آپ نے خود تخلیق کی ہے تو اسے بیان کریں۔\n#*اگر یہ آن لائن دستیاب ہے تو اس سائٹ کا  '''ربط''' درج کریں۔\n#*اگر آپ نے اسے کسی دوسری زبان کے {{SITENAME}} سے لیا ہے تو اس کا نام تحریر کریں۔\n#صاحب حق طبع و نشر اور فائل کے اجازت نامہ کے بارے میں:\n#* فائل کے اجازت نامہ کے متعلق یہ درج کریں کہ اس کی موجودہ حیثیت کیا ہے۔\n#*اگر آپ خود اسکا حق طبع و نشر رکھتے ہیں تو آپ پر لازم ہے کہ آپ اسے [[دائرۂ عام]] (پبلک ڈومین) میں بھی شائع کریں۔\n\nجب کوئی صارف مستقل ایسی فائل اپلوڈ کرتا رہے جس کے اجازت نامہ کے بارے میں غلط بیانی کی گئی ہو یا وہ مستقل ایسی تصاویر اپلوڈ کرے جن کے بارے میں کوئی وضاحت موجود نہ ہو تو ایسی صورت میں اس صارف پر پابندی لگائے جانے کا قوی امکان موجود ہے۔\n\nفائل اپلوڈ کرنے کے لیے ذیل میں موجود فارم استعمال کریں، اگر آپ جملہ اپلوڈ کردہ تصاویر کو دیکھنا یا تلاش کرنا چاہتے ہیں تو [[Special:FileList|اس فہرست]] کو ملاحظہ فرمائیں۔ <br /> تمام اپلوڈ کردہ و حذف شدہ تصاویر کو [[Special:Log/upload|نوشتۂ منتقلی]] اور [[Special:Log/delete|نوشتہ حذف شدگی]] میں درج کر لیا جاتا ہے۔\n\nتصویر کی منتقلی کے بعد، اس کو کسی صفحہ پر رکھنے کیلیے مندرجہ ذیل طریقہ سے استعمال کریں۔\n\n'''<nowiki>[[تصویر:فائل کا نام|متبادل متن]]</nowiki>'''\n\n* فائل کا ربط درج کرنے کے لیے۔ '''<code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code>'''\n* فائل کا نام چھوٹے بڑے حروف کے معاملہ میں حساس ہے لہذا اگر اپلوڈ کرتے وقت فائل کا نام -- name:JPG  ہے اور آپ name:jpg یــا Name:jpg کا ربط درج کرتے ہیں تو ربط کام نہیں کرے گا۔",
+       "upload-permitted": "فائلوں کی اجازت یافتہ {{PLURAL:$2|قسم|قسمیں}}: $1",
+       "upload-preferred": "ترجیحی فائلوں کی {{PLURAL:$2|قسم|قسمیں}}: $1",
+       "upload-prohibited": "ممنوع فائلوں کی {{PLURAL:$2|قسم|قسمیں}}: $1",
+       "uploadlogpage": "نوشتہ اپلوڈ",
+       "uploadlogpagetext": "ذیل میں حالیہ اپلوڈ کردہ فائلوں کی فہرست موجود ہے۔\nمزید بصری جائزے کے لیے [[Special:NewFiles|نئی فائلوں کا نگارخانہ]] ملاحظہ فرمائیں۔",
+       "filename": "فائل کا نام",
        "filedesc": "خلاصہ",
        "fileuploadsummary": "خلاصہ :",
+       "filereuploadsummary": "فائل کی تبدیلیاں:",
+       "filestatus": "کاپی رائٹ کی صورت حال:",
        "filesource": "ذرائع",
-       "ignorewarning": "انتباہ نظرانداز کرتے ہوۓ بہرصورت ملف (فائل) کو محفوظ کرلیا جاۓ۔",
-       "ignorewarnings": "ہر انتباہ نظرانداز کردیا جاۓ۔",
-       "badfilename": "ملف (فائل) کا نام \"$1\" ، تبدیل کردیا گیا۔",
+       "ignorewarning": "انتباہ نظر انداز کرتے ہوئے فائل کو بہرصورت محفوظ کر لیا جائے",
+       "ignorewarnings": "تمام انتباہات کو نظر انداز کریں",
+       "minlength1": "فائل کے ناموں میں کم از کم ایک حرف ہونا ضروری ہے۔",
+       "illegalfilename": "اس فائل کے نام \"$1\" میں ایسے حروف موجود ہیں جو صفحہ کے عنوانات میں ممنوع ہیں۔\nبراہ کرم فائل کا نام تبدیل کرکے دوبارہ اپلوڈ کرنے کی کوشش کریں۔",
+       "filename-toolong": "فائل کے نام 240 بائٹ سے زیادہ طویل نہ ہوں۔",
+       "badfilename": "فائل کا نام «$1» کر دیا گیا ہے۔",
+       "filetype-mime-mismatch": "فائل کی توسیع «$1.‎» فائل کی MIME قسم ($2) کے مطابق نہیں۔",
+       "filetype-badmime": "MIME قسم \"$1\" کی فائلوں کو اپلوڈ کرنے کی اجازت نہیں ہے۔",
+       "filetype-bad-ie-mime": "اس فائل کو اپلوڈ نہیں کیا جا سکتا کیونکہ انٹرنیٹ ایکسپلورر اسے «$1» سمجھے گا جس کی اجازت نہیں اور اس نوع کی فائل کے خطرناک ہونے کا احتمال ہے۔",
+       "filetype-unwanted-type": "<strong>\".$1\"</strong> ناپسندیدہ نوعیت کی فائل ہے۔\nراجح نوعیت کی {{PLURAL:$3|فائل|فائلیں}} $2 {{PLURAL:$3|ہے|ہیں}}۔",
+       "filetype-banned-type": "<strong>\".$1\"</strong> نوعیت کی {{PLURAL:$4|فائل|فائلوں}} کی اجازت نہیں۔\nاجازت یافتہ نوعیت کی {{PLURAL:$3|فائل|فائلیں}} $2 {{PLURAL:$3|ہے|ہیں}}۔",
+       "filetype-missing": "اس فائل کی کوئی توسیع نہیں ہے (مثلاً  \".jpg\")۔",
+       "empty-file": "آپ کی ارسال کردہ فائل خالی تھی۔",
+       "file-too-large": "آپ کی ارسال کردہ فائل بہت بڑی تھی",
+       "filename-tooshort": "فائل کا نام انتہائی مختصر ہے۔",
+       "filetype-banned": "فائل کی اس قسم پر پابندی عائد ہے۔",
+       "verification-error": "یہ فائل، فائل کی تصدیق میں کامیاب نہیں ہو سکی۔",
+       "hookaborted": "آپ نے جو تبدیلی کرنے کی کوشش کی اسے کسی توسیع نے منسوخ کر دیا۔",
+       "illegal-filename": "اس نام کی فائل ممنوع ہے۔",
+       "overwrite": "موجودہ فائل کو دوبارہ اپلوڈ کرنے کی اجازت نہیں۔",
+       "unknown-error": "نامعلوم نقص واقع ہوا۔",
+       "tmp-create-error": "عارضی فائل نہیں بن سکی۔",
+       "tmp-write-error": "عارضی فائل کی تحریر کے دوران میں نقص۔",
+       "large-file": "اس بات کی سفارش کی جاتی ہے کہ فائلوں کا حجم $1 سے زیادہ نہ ہو؛\nاس فائل کا حجم $2 ہے۔",
+       "largefileserver": "یہ فائل سرور پر تعین کردہ تشکیل سے بڑی ہے۔",
+       "emptyfile": "لگتا ہے آپ کی اپلوڈ کردہ فائل خالی ہے۔\nایسا ٹائپنگ میں کوئی غلطی کی وجہ سے ہو سکتا ہے۔\nبرائے مہربانی جانچ کر لیں کہ آیا آپ واقعی اس فائل کو اپلوڈ کرنا چاہتے ہیں۔",
+       "windows-nonascii-filename": "یہ ویکی خاص حروف کے ساتھ فائل کا نام تسلیم نہیں کرتا۔",
        "fileexists": "اس نام سے ایک فائل پہلے سے موجود ہے، اگر آپ کو یقین نہ ہو کہ اسے حذف کردیا جانا چاہیے تو براہ کرم  <strong>[[:$1]]</strong> کو ایک نظر دیکھ لیجیے۔ [[$1|thumb]]",
-       "uploadwarning": "انتباہ بہ سلسلۂ زبراثقال",
+       "filepageexists": "اس فائل کا صفحۂ وضاحت پہلے ہی <strong>[[:$1]]</strong> پر بنا دیا گیا ہے، تاہم فی الحال اس نام سے کوئی فائل موجود نہیں۔\nجو خلاصہ آپ درج کر رہے ہیں یہ صفحۂ وضاحت میں نظر نہیں آئے گا۔\nاگر آپ اپنے خلاصے کو صفحہ وضاحت میں دیکھنا چاہیں تو وہاں دستی طور پر شامل کریں۔\n[[$1|thumb]]",
+       "fileexists-extension": "اسی نام سے ایک فائل موجود ہے: [[$2|thumb]]\n* اپلوڈ کی جانے والی فائل کا نام: <strong>[[:$1]]</strong>\n* پہلے سے موجود فائل کا نام: <strong>[[:$2]]</strong>\nبراہ کرم فائل کا منفرد نام رکھیں۔",
+       "fileexists-thumbnail-yes": "ایسا معلوم ہوتا ہے کہ یہ فائل کم حجم <em>(تھمب نیل)</em> کی تصویر ہے۔\n[[$1|thumb]]\nبراہ کرم فائل <strong>[[:$1]]</strong> کو جانچ لیں۔\nاگر وہ اپنے اصل حجم کے ساتھ یہی فائل ہے تو کسی اضافی تھمب نیل کے اپلوڈ کرنے کی ضرورت نہیں۔",
+       "file-thumbnail-no": "اس فائل کا نام <strong>$1</strong> سے شروع ہوتا ہے۔\nایسا معلوم ہوتا ہے کہ یہ فائل کم حجم <em>(تھمب نیل)</em> کی تصویر ہے۔\nاگر آپ کے پاس یہ تصویر مکمل ریزولیوشن میں موجود ہو تو اسے اپلوڈ کریں، ورنہ اس فائل کا نام تبدیل کر دیں۔",
+       "fileexists-forbidden": "اس نام سے پہلے ہی ایک فائل موجود ہے اور اب اسے برتحریر نہیں کیا جا سکتا۔\nاگر آپ بہرصورت اپنی فائل کو اپلوڈ کرنا چاہتے ہیں تو براہ کرم واپس جائیں اور فائل کا نام تبدیل کریں۔\n[[File:$1|thumb|center|$1]]",
+       "fileexists-shared-forbidden": "فائلوں کے مشترکہ ذخیرے میں اس نام سے پہلے ہی ایک فائل موجود ہے۔\nاگر آپ بہرصورت اپنی فائل کو اپلوڈ کرنا چاہتے ہیں تو براہ کرم واپس جائیں اور فائل کا نام تبدیل کریں۔\n[[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "اپلوڈ کردہ فائل <strong>[[:$1]]</strong> کے موجودہ نسخے کی واضح نقل ہے۔",
+       "fileexists-duplicate-version": "اپلوڈ کردہ فائل <strong>[[:$1]]</strong> کے {{PLURAL:$2|پرانے نسخے|پرانے نسخوں}} کی واضح نقل ہے۔",
+       "file-exists-duplicate": "پیش نظر فائل درج ذیل {{PLURAL:$1|فائل|فائلوں}} کی نقل ہے:",
+       "file-deleted-duplicate": "اس فائل ([[:$1]]) سے ملتی جلتی دوسری فائل کو پہلے حذف کیا جا چکا ہے۔\nچنانچہ اسے دوبارہ اپلوڈ کرنے سے قبل اُس پرانی فائل کے حذف کا تاریخچہ جانچ لیں۔",
+       "file-deleted-duplicate-notitle": "اس فائل سے ملتی جلتی دوسری فائل کو پہلے حذف کیا اور اس عنوان کو ممنوع قرار دیا جا چکا ہے۔\nاسے دوبارہ اپلوڈ کرنے سے قبل کسی ایسے شخص سے اس صورت حال کا جائزہ لینے کی درخواست کریں جسے ممنوع فائلوں کی معلومات تک رسائی حاصل ہو۔",
+       "uploadwarning": "اپلوڈ انتباہ",
+       "uploadwarning-text": "ذیل میں موجود فائل کی وضاحت میں تبدیلی کریں اور دوبارہ کوشش کریں۔",
        "savefile": "فائل محفوظ کریں",
-       "sourcefilename": "اسم ملف (فائل) کا منبع:",
-       "destfilename": "تعین شدہ اسم ملف:",
-       "watchthisupload": "یہ صفحہ زیر نظر کریں",
+       "uploaddisabled": "اپلوڈ غیر فعال ہے۔",
+       "copyuploaddisabled": "بذریعہ یوآرایل اپلوڈ غیر فعال ہے۔",
+       "uploaddisabledtext": "فائل اپلوڈ غیر فعال ہے۔",
+       "php-uploaddisabledtext": "پی ایچ پی کی فائلیں اپلوڈ نہیں کی جا سکتیں۔\nبراہ کرم  file_uploads کی ترتیبات جانچ لیں۔",
+       "uploadscripted": "اس فائل میں ایچ ٹی ایم ایل یا اسکرپٹ کوڈ کا استعمال کیا گیا ہے لہذا عین ممکن ہے کہ کوئی ویب براؤزر اس کی غلط تشریح کرے۔",
+       "upload-scripted-pi-callback": "ایسی کسی فائل کو اپلوڈ نہیں کیا جا سکتا جس میں ایکس ایم ایل اسٹائل شیٹ پر عمل کرنے کی ہدایت ہو۔",
+       "uploaded-hostile-svg": "اپلوڈ کردہ ایس وی جی فائل کے اسٹائل عنصر میں غیر محفوظ سی ایس ایس دریافت ہوئی ہے۔",
+       "uploadscriptednamespace": "اس ایس وی جی فائل میں غیر قانونی نام فضا \"$1\" موجود ہے۔",
+       "uploadinvalidxml": "اپلوڈ کردہ فائل میں موجود ایکس ایم ایل کا تجزیہ نہیں کیا جا سکا۔",
+       "uploadvirus": "اس فائل میں وائرس موجود ہے!\nتفصیلات: $1",
+       "upload-source": "اصل فائل",
+       "sourcefilename": "اصل فائل کا نام:",
+       "sourceurl": "اصل یوآرایل",
+       "destfilename": "ہدف فائل کا نام:",
+       "upload-maxfilesize": "فائل کا زیادہ سے زیادہ حجم: $1",
+       "upload-description": "فائل کی وضاحت",
+       "upload-options": "اپلوڈ کے اختیارات",
+       "watchthisupload": "اس فائل کو زیر نظر کریں",
+       "upload-proto-error": "غلط پروٹوکول",
+       "upload-file-error": "داخلی نقص",
+       "upload-misc-error": "اپلوڈ کے دوران میں نامعلوم نقص",
+       "upload-too-many-redirects": "اس یوآرایل میں بہت سارے رجوع مکررات ہیں",
+       "upload-http-error": "ایچ ٹی ٹی پی نقص واقع ہوا: $1",
+       "upload-copy-upload-invalid-domain": "اس ڈومین سے کاپی اپلوڈ دستیاب نہیں ہیں۔",
        "upload-dialog-disabled": "اس ویکی پر اس ڈائیلاگ سے فائل اپ لوڈز غیر فعال ہیںَ",
+       "upload-dialog-title": "فائل اپلوڈ کریں",
        "upload-dialog-button-cancel": "منسوخ",
+       "upload-dialog-button-back": "پیچھے جائیں",
        "upload-dialog-button-done": "مکمل",
        "upload-dialog-button-save": "محفوظ",
        "upload-dialog-button-upload": "اپلوڈ",
        "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": "تاریخ",
-       "license": "اجازہ:",
+       "upload-form-label-own-work-message-generic-local": "میں اس بات کی تصدیق کرتا ہوں کہ {{SITENAME}} میں موجود اجازت ناموں کی حکمت عملیوں اور استعمال کے جملہ شرائط کی پیروی کرتے ہوئے اس فائل کو اپلوڈ کر رہا ہوں۔",
+       "upload-form-label-not-own-work-message-generic-local": "اگر آپ {{SITENAME}} کی حکمت عملیوں کے تحت اس فائل کو اپلوڈ نہیں کر سکتے تو براہ کرم اسے بند کرکے دوسرا طریقہ استعمال کرنے کی کوشش کریں۔",
+       "upload-form-label-not-own-work-local-generic-local": "نیز آپ [[Special:Upload|ڈیفالٹ اپلوڈ صفحہ]] بھی استعمال کر سکتے ہیں۔",
+       "upload-form-label-own-work-message-generic-foreign": "میں یہ سمجھتا ہوں کہ اس فائل کو ایک مشترکہ ذخیرے میں اپلوڈ کیا جا رہا ہے اور اس امر کی تصدیق کرتا ہوں کہ اس کام کی انجام دہی کے دوران میں یہاں موجود استعمال کے جملہ شرائط اور اجازت ناموں کی تمام حکمت عملیوں کی پیروی کر رہا ہوں۔",
+       "upload-form-label-not-own-work-message-generic-foreign": "اگر آپ مشترکہ ذخیرے کی حکمت عملیوں کے تحت اس فائل کو اپلوڈ نہیں کر سکتے تو براہ کرم اسے بند کرکے دوسرا طریقہ استعمال کرنے کی کوشش کریں۔",
+       "upload-form-label-not-own-work-local-generic-foreign": "اگر اس فائل کو {{SITENAME}} کی مقررہ پالیسیوں کے تحت اپلوڈ کرنا ممکن ہو تو آپ [[Special:Upload|{{SITENAME}} کا اپلوڈ صفحہ]] استعمال کر سکتے ہیں۔",
+       "backend-fail-stream": "فائل $1 کی نمائش ممکن نہیں۔",
+       "backend-fail-backup": "فائل $1 کا احتیاطی نسخہ بنانا ممکن نہیں۔",
+       "backend-fail-notexists": "فائل $1 موجود نہیں ہے۔",
+       "backend-fail-hashes": "موازنہ کے لیے فائل کے ہیش کو حاصل نہیں کیا جا سکا۔",
+       "backend-fail-notsame": "$1 میں ایک غیر یکساں فائل پہلے سے موجود ہے۔",
+       "backend-fail-invalidpath": "$1 ذخیرہ اندوزی کا درست راستہ نہیں ہے۔",
+       "backend-fail-delete": "فائل $1 کو حذف نہیں کیا جا سکا۔",
+       "backend-fail-describe": "فائل $1 کا میٹاڈیٹا تبدیل نہیں کیا جا سکا۔",
+       "backend-fail-alreadyexists": "فائل \"$1\" پہلے سے موجود ہے۔",
+       "backend-fail-store": "فائل $1 کو $2 میں محفوظ نہیں کیا جا سکا۔",
+       "backend-fail-copy": "فائل $1 کو $2 میں نقل نہیں کیا جا سکا۔",
+       "backend-fail-move": "فائل $1 کو $2 میں منتقل نہیں کیا جا سکا۔",
+       "backend-fail-opentemp": "عارضی فائل کھل نہیں سکی۔",
+       "backend-fail-writetemp": "عارضی فائل میں لکھا نہیں جا سکا۔",
+       "backend-fail-closetemp": "عارضی فائل بند نہیں ہو سکی۔",
+       "backend-fail-read": "فائل \"$1\" کو پڑھا نہ جا سکا۔",
+       "backend-fail-create": "فائل \"$1\" کو لکھا نہ جا سکا۔",
+       "backend-fail-maxsize": "فائل $1 کی معلومات نہیں لکھی جا سکی کیونکہ اس کا حجم {{PLURAL:$2|ایک بائٹ|$2 بائٹ}} سے زیادہ ہے۔",
+       "backend-fail-readonly": "فی الحال ذخیرہ کا پس منظر $1 فقط خواندگی حالت میں ہے۔ اس کی وجہ حسب ذیل ہے:\n\n\n<em>«$2»</em>",
+       "backend-fail-synced": "اس وقت فائل $1 داخلی ذخیرہ کے پس منظر کے اندر ناپائیدار حالت میں ہے۔",
+       "lockmanager-notlocked": "«$1» کو کھولا نہیں جا سکا؛ کیونکہ یہ مقفل نہیں ہے۔",
+       "lockmanager-fail-closelock": "«$1» کا فائل لاک بند نہیں کیا جا سکا۔",
+       "lockmanager-fail-deletelock": "«$1» کا فائل لاک حذف نہیں کیا جا سکا۔",
+       "lockmanager-fail-acquirelock": "«$1» کا قفل حاصل نہیں کیا جا سکا۔",
+       "lockmanager-fail-openlock": "«$1» کا فائل لاک کھولا نہیں جا سکا۔",
+       "lockmanager-fail-releaselock": "«$1» کا قفل کھولا نہ جا سکا۔",
+       "lockmanager-fail-db-release": "$1 ڈیٹابیس سے قفل نہیں ہٹائے جا سکے۔",
+       "lockmanager-fail-svr-acquire": "$1 سرور کے قفل حاصل نہیں کیے جا سکے۔",
+       "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 درخواست کی مہلت ختم ہو گئی۔",
+       "http-curl-error": "یوآرایل $1 کو اخذ کرنے کے دوران میں نقص",
+       "http-bad-status": "HTTP درخواست کے دوران میں ایک مشکل پیش آگئی: $1 $2",
+       "upload-curl-error6": "یوآرایل تک پہنچنا ممکن نہیں",
+       "upload-curl-error6-text": "فراہم کردہ یوآرایل قابل رسائی نہیں ہے۔\nبراہ کرم اس یوآرایل کو دوبارہ جانچ لیں کہ آیا وہ درست ہے اور متعلقہ سائٹ فعال ہے یا نہیں۔",
+       "upload-curl-error28": "اپلوڈ کی مہلت ختم",
+       "upload-curl-error28-text": "یہ سائٹ جواب دینے میں بہت زیادہ وقت لے رہی ہے۔\nبراہ کرم اس سائٹ کو جانچ لیں کہ آیا وہ فعال ہے یا نہیں، اور کچھ دیر انتظار کرنے کے بعد دوبارہ کوشش کریں۔\nشاید آپ اسے کم مصروف وقت میں آزمانا چاہیں۔",
+       "license": "اجازت نامہ:",
        "license-header": "اجازہ کاری",
+       "nolicense": "غیر منتخب",
+       "licenses-edit": "اجازت نامہ کے اختیارات میں ترمیم کریں",
+       "license-nopreview": "(نمائش دستیاب نہیں)",
+       "upload_source_url": "(آپ نے ایک درست اور عوامی طور پر قابل رسائی یوآرایل سے اس فائل کا انتخاب کیا ہے)",
+       "upload_source_file": "(آپ نے اپنے کمپیوٹر سے اس فائل کو منتخب کیا ہے)",
        "listfiles-delete": "حذف",
-       "imgfile": "ملف",
-       "listfiles": "فہرست فائل",
+       "listfiles-summary": "اس خصوصی صفحہ میں تمام اپلوڈ کردہ فائلیں نظر آئیں گی۔",
+       "listfiles_search_for": "میڈیا کے نام کو تلاش کریں:",
+       "listfiles-userdoesnotexist": "«$1» کے نام سے کھاتہ موجود نہیں۔",
+       "imgfile": "فائل",
+       "listfiles": "فائلوں کی فہرست",
+       "listfiles_thumb": "تھمب نیل",
        "listfiles_date": "تاریخ",
        "listfiles_name": "نام",
        "listfiles_user": "صارف",
        "listfiles_size": "حجم",
-       "listfiles_description": "تفصیل",
-       "listfiles_count": "ورژن",
+       "listfiles_description": "وضاحت",
+       "listfiles_count": "نسخے",
+       "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": "اظفورہ",
-       "filehist-thumbtext": "$1 کا تھمب نیل (thumbnail) ورژن",
+       "filehist-thumb": "تھمب نیل",
+       "filehist-thumbtext": "مورخہ $1 کا تھمب نیل",
+       "filehist-nothumb": "تھمب نیل نہیں ہے",
        "filehist-user": "صارف",
        "filehist-dimensions": "ابعاد",
        "filehist-filesize": "تصویر کا حجم",
        "filehist-comment": "تبصرہ",
-       "imagelinks": "ملف کا استعمال",
-       "linkstoimage": "اِس ملف کے ساتھ درج ذیل {{PLURAL:$1|صفحہ مربوط ہے|$1 صفحات مربوط ہیں}}",
-       "nolinkstoimage": "ایسے کوئی صفحات نہیں جو اس ملف (فائل) سے رابطہ رکھتے ہوں۔",
-       "sharedupload-desc-here": "یہ ملف $1 سے ہے اور دوسرے منصوبوں میں استعمال ہوسکتا ہے۔\nاِس کے [$2 ملفاتی صفحۂ وضاحت] سے تفصیل درج ذیل ہے۔",
-       "upload-disallowed-here": "آپ اوپر چھڑا کر اس ملف کو نہیں لکھ سکتے۔",
+       "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": "یہ فائل $1 کی ہے نیز ممکن ہے دوسرے منصوبوں میں بھی زیر استعمال ہو۔\nاِس کے [$2 صفحۂ وضاحت] میں درج وضاحت ذیل میں موجود ہے۔",
+       "sharedupload-desc-edit": "یہ فائل $1 میں موجود ہے، نیز ممکن ہے دیگر منصوبوں میں بھی مستعمل ہو۔\nاگر آپ [$2 فائل کے صفحۂ وضاحت] میں موجود معلومات میں ترمیم کرنا چاہیں تو وہاں کر سکتے ہیں۔",
+       "sharedupload-desc-create": "یہ فائل $1 میں موجود ہے، نیز ممکن ہے دیگر منصوبوں میں بھی مستعمل ہو۔\nاگر آپ [$2 فائل کے صفحۂ وضاحت] میں موجود معلومات میں ترمیم کرنا چاہیں تو وہاں کر سکتے ہیں۔",
+       "filepage-nofile": "اس نام سے کوئی فائل موجود نہیں ہے۔",
+       "filepage-nofile-link": "اس نام سے کوئی فائل موجود نہیں ہے، لیکن آپ [$1 اسے اپلوڈ کر سکتے ہیں]۔",
+       "uploadnewversion-linktext": "اس فائل کا نیا نسخہ اپلوڈ کریں",
+       "shared-repo-from": "از $1",
+       "shared-repo": "مشترکہ ذخیرہ",
+       "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": "فہرست متبادل ربط",
-       "unusedtemplates": "غیر استعمال شدہ سانچے",
+       "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» زمرے کا درست نام نہیں ہے۔",
+       "randomincategory-nopages": "[[:Category:$1|$1]] زمرہ میں کوئی صفحہ نہیں ہے۔",
        "randomincategory-category": "زمرہ:",
+       "randomincategory-legend": "زمرہ میں بے ترتیب صفحہ",
        "randomincategory-submit": "جانا",
+       "randomredirect": "بے ترتيب رجوع مکرر",
+       "randomredirect-nopages": "«$1» نام فضا میں کوئی رجوع مکرر نہیں ہے۔",
        "statistics": "اعداد و شمار",
        "statistics-header-pages": "صفحات کے اعداد و شمار",
        "statistics-header-edits": "ترمیمی اعداد و شمار",
        "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": "ترمیم کریں",
        "brokenredirects-delete": "حذف",
+       "withoutinterwiki": "صفحات بدون بین الویکی روابط",
+       "withoutinterwiki-summary": "درج ذیل صفحات دوسری زبان کے صفحات سے مربوط نہیں ہیں۔",
        "withoutinterwiki-legend": "سابقہ",
        "withoutinterwiki-submit": "دکھائیں",
-       "fewestrevisions": "کم نظرِ ثانی شدہ مضامین",
-       "nbytes": "$1 {{PLURAL:$1|لکمہ|لکمہ جات}}",
+       "fewestrevisions": "کم ترامیم کے حامل صفحات",
+       "nbytes": "$1 {{PLURAL:$1|بائٹ}}",
        "ncategories": "{{PLURAL:$1|زمرہ|زمرہ جات}} $1",
-       "ninterwikis": "$1 {{PLURAL:$1|بین الویکی|بین الویکی}}",
-       "nlinks": "$1 {{PLURAL:$1|بÛ\8cÙ\86 Ø§Ù\84Ù\88Û\8cÚ©Û\8c|بÛ\8cÙ\86 Ø§Ù\84Ù\88Û\8cÚ©Û\8c}}",
+       "ninterwikis": "$1 {{PLURAL:$1|بین الویکی ربط|بین الویکی روابط}}",
+       "nlinks": "$1 {{PLURAL:$1|ربط|رÙ\88ابط}}",
        "nmembers": "{{PLURAL:$1|رکن|اراکین}}",
+       "nmemberschanged": "$1 ← $2 {{PLURAL:$2|رکن|اراکین}}",
        "nrevisions": "$1 {{PLURAL:$1|نظر ثانی|نظر ثانیاں}}",
        "nimagelinks": "$1 پر مستعمل {{PLURAL:$1|صفحہ|صفحات}}",
        "ntransclusions": "$1 پر مستعمل {{PLURAL:$1|صفحہ|صفحات}}",
        "uncategorizedcategories": "بے زمرہ زمرہ جات",
        "uncategorizedimages": "بے زمرہ تصاویر",
        "uncategorizedtemplates": "غیر زمرہ بند سانچہ جات",
-       "unusedcategories": "غیر استعمال شدہ زمرہ جات",
-       "unusedimages": "غیر استعمال شدہ فائلیں",
-       "wantedcategories": "طلب شدہ زمرہ جات",
-       "wantedpages": "درخواست شدہ مضامین",
+       "unusedcategories": "غیر مستعمل زمرہ جات",
+       "unusedimages": "غیر مستعمل فائلیں",
+       "wantedcategories": "مطلوبہ زمرہ جات",
+       "wantedpages": "مطلوبہ مضامین",
+       "wantedpages-summary": "ذیل میں ان غیر موجود صفحات کی فہرست ہے جن سے بہت سارے روابط مربوط ہیں، البتہ ان میں وہ صفحات شامل نہیں جن میں محض ان سے مربوط رجوع مکررات موجود ہیں۔ ان صفحوں کو دیکھنے کے لیے [[{{#special:BrokenRedirects}}|شکستہ روابط کی فہرست]] ملاحظہ فرمائیں۔",
+       "wantedpages-badtitle": "نتائج میں نادرست عنوان: $1",
        "wantedfiles": "مطلوب تصاویر",
+       "wantedfiletext-cat": "ذیل میں موجود فائلیں مستعمل ہیں لیکن موجود نہیں۔ البتہ اس بات کا امکان ہے کہ بیرونی ذخیروں کی موجود فائلیں یہاں اس فہرست میں درج ہو گئی ہوں۔ ایسے غلط امکانات کو <del>مٹا دیا جائے گا</del>۔ علاوہ ازیں، غیر موجود فائلوں پر مشتمل صفحات کی فہرست [[:$1]] میں ملاحظہ فرمائیں۔",
+       "wantedfiletext-cat-noforeign": "ذیل میں موجود فائلیں زیر استعمال ہیں لیکن موجود نہیں۔ علاوہ ازیں، جن صفحات میں یہ غیر موجود فائلیں زیر استعمال ہیں ان کی فہرست [[:$1]] میں ملاحظہ فرمائیں۔",
+       "wantedfiletext-nocat": "ذیل میں موجود فائلیں مستعمل ہیں لیکن موجود نہیں۔ البتہ اس بات کا امکان ہے کہ بیرونی ذخیروں کی موجود فائلیں یہاں اس فہرست میں درج ہو گئی ہوں۔ ایسے غلط امکانات کو <del>مٹا دیا جائے گا</del>۔",
+       "wantedfiletext-nocat-noforeign": "ذیل میں موجود فائلیں زیر استعمال ہیں لیکن موجود نہیں۔",
        "wantedtemplates": "مطلوب سانچے",
        "mostlinked": "سب سے زیادہ ربط والے مضامین",
        "mostlinkedcategories": "سب سے زیادہ ربط والے زمرہ جات",
+       "mostlinkedtemplates": "کثیر مستعمل صفحات",
        "mostcategories": "سب سے زیادہ زمرہ جات والے مضامین",
        "mostimages": "سب سے زیادہ استعمال کردہ تصاویر",
        "mostinterwikis": "کثیر اندرونی ربط والے صفحات",
        "shortpages": "چھوٹے صفحات",
        "longpages": "طویل ترین صفحات",
        "deadendpages": "مردہ صفحات",
+       "deadendpagestext": "درج ذیل صفحات {{SITENAME}} کے دیگر صفحوں سے مربوط نہیں ہیں۔",
        "protectedpages": "محفوظ صفحات",
+       "protectedpages-indef": "فقط غیر متعین محفوظ شدگیاں",
        "protectedpages-summary": "ذیل میں ان صفحات کی فہرست موجود ہے جو ابھی محفوظ ہیں۔ محفوظ شدہ عنوانات جنہیں تخلیق نہیں کیا جا سکتا، ان کی فہرست کے لیے [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]] ملاحظہ فرمائیں۔",
+       "protectedpages-cascade": "فقط آبشاری محفوظ شدگیاں",
        "protectedpages-noredirect": "رجوع مکررات چھپائیں",
+       "protectedpagesempty": "ان پیرامیٹروں کے ساتھ فی الحال کوئی صفحہ محفوظ نہیں ہے۔",
        "protectedpages-timestamp": "وقت کی مہر",
        "protectedpages-page": "صفحہ",
        "protectedpages-expiry": "مدت محفوظ شدگی",
        "protectedpages-unknown-performer": "نامعلوم صارف",
        "protectedtitles": "محفوظ عنوانات",
        "protectedtitles-summary": "ذیل میں ان عنوانات کی فہرست ہے جنہیں تخلیق نہیں کیا جا سکتا، یہ عنوانات محفوظ شدہ ہیں۔ ان صفحات کی فہرست کے لیے جو ابھی محفوظ ہیں [[{{#special:ProtectedPages}}|{{int:protectedpages}}]] ملاحظہ فرمائیں۔",
+       "protectedtitlesempty": "ان پیرامیٹروں کے ساتھ فی الحال کوئی عنوان محفوظ نہیں ہے۔",
        "protectedtitles-submit": "دکھائیں",
        "listusers": "فہرست ارکان",
+       "listusers-editsonly": "محض ترمیم کرنے والے صارفین دکھائیں",
+       "listusers-creationsort": "تاریخ تخلیق کے مطابق مرتب کریں",
+       "listusers-desc": "نزولی ترتیب",
        "usereditcount": "$1 {{PLURAL:$1|ترمیم|ترامیم}}",
        "usercreated": "{{GENDER:$3|تخلیق شدہ}}  بتاریخ $1 بوقت $2",
        "newpages": "جدید صفحات",
        "ancientpages": "قدیم ترین صفحات",
        "move": "منتقـل",
        "movethispage": "یہ صفحہ منتقل کیجئے",
-       "pager-newer-n": "{{PLURAL:$1|جدید 1|جدید $1}}",
-       "pager-older-n": "{{PLURAL:$1|پُرانا 1|پُرانے $1}}",
+       "unusedimagestext": "درج ذیل فائلیں موجود ہیں لیکن کسی صفحہ میں زیر استعمال نہیں۔\nممکن ہے کہ دیگر ویب سائٹیں براہ راست ربط کے ذریعہ کسی فائل سے مربوط ہوں، اور اس کے باوجود وہ فائل یہاں درج ہو گئی ہوں۔",
+       "unusedcategoriestext": "درج ذیل زمرہ جات موجود ہیں لیکن کسی مضمون یا دوسرے کسی زمرے میں مستعمل نہیں۔",
+       "notargettitle": "کوئی ہدف نہیں",
+       "notargettext": "اس اقدام کی تکمیل کے لیے آپ نے کسی صفحہ یا صارف کا تعین نہیں کیا ہے۔",
+       "nopagetitle": "ایسا کوئی صفحہ موجود نہیں",
+       "nopagetext": "آپ کا درج کردہ ہدف صفحہ موجود نہیں ہے۔",
+       "pager-newer-n": "{{PLURAL:$1|جدید $1}}",
+       "pager-older-n": "{{PLURAL:$1|قدیم}} $1",
+       "suppress": "دبائیں",
+       "querypage-disabled": "اس خصوصی صفحہ کو بوجوہ غیر فعال کر دیا گیا ہے۔",
        "apihelp": "معاونت اے پی آئی",
        "apihelp-no-such-module": "ماڈیول \"$1\" نہیں ملا",
+       "apisandbox": "اے پی آئی کا تختۂ مشق",
+       "apisandbox-jsonly": "اے پی آئی کے تختۂ مشق کو استعمال کرنے کے لیے جاوا اسکرپٹ درکار ہے۔",
+       "apisandbox-api-disabled": "اس سائٹ پر اے پی آئی غیر فعال ہے۔",
+       "apisandbox-fullscreen": "پینل کو وسیع کریں",
+       "apisandbox-fullscreen-tooltip": "براؤزر کے دریچے کا مکمل احاطہ کرنے کے لیے تختۂ مشق کے پینل کو وسیع کریں۔",
+       "apisandbox-unfullscreen": "صفحہ دکھائیں",
+       "apisandbox-unfullscreen-tooltip": "تختہ مشق کا پینل چھوٹا کریں تاکہ میڈیاویکی کے روابطِ رہنمائی دسترس میں ہوں۔",
        "apisandbox-submit": "بنانے کی درخواست",
        "apisandbox-reset": "واضح",
-       "apisandbox-examples": "مثال کے طور پر",
-       "apisandbox-results": "نتیجہ",
+       "apisandbox-retry": "دوبارہ کوشش کریں",
+       "apisandbox-loading": "اے پی آئی ماڈیول \"$1\" کی معلومات لوڈ ہو رہی ہے۔۔۔",
+       "apisandbox-load-error": "اے پی آئی ماڈیول \"$1\" کی معلومات لوڈ ہونے کے دوران میں نقص واقع ہوا: $2",
+       "apisandbox-no-parameters": "اس اے پی آئی ماڈیول میں کوئی پیرامیٹر نہیں ہے۔",
+       "apisandbox-helpurls": "روابط رہنمائی",
+       "apisandbox-examples": "مثالیں",
+       "apisandbox-dynamic-parameters": "اضافی پیرامیٹر",
+       "apisandbox-dynamic-parameters-add-label": "پیرامیٹر شامل کریں:",
+       "apisandbox-dynamic-parameters-add-placeholder": "پیرامیٹر کا نام",
+       "apisandbox-dynamic-error-exists": "\"$1\" کے نام سے ایک پیرامیٹر پہلے سے موجود ہے۔",
+       "apisandbox-deprecated-parameters": "متروک پیرامیٹر",
+       "apisandbox-fetch-token": "ٹوکن کو خودکار طور پر پُر کریں",
+       "apisandbox-submit-invalid-fields-title": "بعض خانے نادرست ہیں",
+       "apisandbox-submit-invalid-fields-message": "براہ کرم نشان زد خانوں کو درست کرکے دوبارہ کوشش کریں۔",
+       "apisandbox-results": "نتائج",
+       "apisandbox-sending-request": "اے پی آئی درخواست بھیجی جا رہی ہے۔۔۔",
+       "apisandbox-loading-results": "اے پی آئی کے نتائج موصول ہو رہے ہیں۔۔۔",
+       "apisandbox-results-error": "اے پی آئی کوئری کا جواب لوڈ ہونے کے دوران میں نقص واقع ہوا: $1",
+       "apisandbox-request-url-label": "درخواست کا ربط:",
+       "apisandbox-request-time": "درخواست کا وقت: {{PLURAL:$1|$1 ملی سیکنڈ}}",
+       "apisandbox-results-fixtoken": "ٹوکن کو درست کرکے دوبارہ بھیجیں",
+       "apisandbox-results-fixtoken-fail": "\"$1\" ٹوکن اخذ کرنے میں ناکامی۔",
+       "apisandbox-alert-page": "اس صفحہ میں موجود خانے نادرست ہیں۔",
+       "apisandbox-alert-field": "اس خانے کی قدر نادرست ہے۔",
        "booksources": "کتابی وسائل",
        "booksources-search-legend": "تلاش برائے مآخذاتِ کتاب",
        "booksources-search": "تلاش",
+       "booksources-invalid-isbn": "درج کردہ آئی ایس بی این درست نہیں معلوم ہوتا؛ اصل ماخذ سے نقل کے دوران میں ہوئی غلطیوں کو جانچ لیں۔",
        "specialloguserlabel": "صارف:",
        "speciallogtitlelabel": "ہدف (عنوان یا {{ns:user}}:صارف نام برائے صارف):",
        "log": "نوشتہ جات",
        "logeventslist-submit": "دکھائیں",
+       "all-logs-page": "تمام عوامی نوشتہ جات",
+       "logempty": "نوشتہ میں اس سے مشابہ کوئی اندراج موجود نہیں ہے۔",
+       "log-title-wildcard": "اس عبارت سے شروع ہونے والے عناوین میں تلاش کریں",
+       "showhideselectedlogentries": "نوشتہ کے منتخب اندراج کی مرئیت تبدیل کریں",
+       "log-edit-tags": "نوشتہ کے منتخب اندراج کے ٹیگوں میں ترمیم کریں",
        "checkbox-select": "$1 کو منتخب کریں",
        "checkbox-all": "سب",
        "checkbox-none": "کچھ نہیں",
        "allpages": "تمام صفحات",
        "nextpage": "اگلا صفحہ ($1)",
        "prevpage": "پچھلا صفحہ ($1)",
-       "allpagesfrom": "مطلوبہ حرف شروع ہونے والے صفحات کی نمائش:",
+       "allpagesfrom": "اس حرف سے شروع ہونے والے صفحات دکھائیں:",
+       "allpagesto": "اس حرف پر ختم ہونے والے صفحات دکھائیں:",
        "allarticles": "تمام مقالات",
-       "allpagessubmit": "چلو",
+       "allinnamespace": "تمام صفحات ($1 نام فضا)",
+       "allpagessubmit": "چلیں",
        "allpagesprefix": "مطلوبہ سابقہ سے شروع ہونے والے صفحات کی نمائش:",
+       "allpages-bad-ns": "{{SITENAME}} میں «$1» نام فضا موجود نہیں۔",
+       "allpages-hide-redirects": "رجوع مکررات چھپائیں",
+       "cachedspecial-viewing-cached-ttl": "آپ اس وقت اس صفحہ کا کیشے شدہ نسخہ دیکھ رہے ہیں جو ممکن ہے $1 پرانا ہو۔",
+       "cachedspecial-viewing-cached-ts": "آپ اس وقت اس صفحہ کا کیشے شدہ نسخہ دیکھ رہے ہیں جو شاید مکمل طور پر اصلی نہ ہو۔",
+       "cachedspecial-refresh-now": "تازہ ترین دیکھیں۔",
        "categories": "زمرہ",
        "categories-submit": "دکھائیں",
        "categoriespagetext": "ذیل میں موجود {{PLURAL:$1|زمرہ|زمرہ جات}} میں صفحات یا میڈیا موجود ہے۔\n[[Special:UnusedCategories|غیر مستعمل زمرہ جات]] یہاں نہیں دکھائے گئے ہیں۔\nنیز [[Special:WantedCategories|مطلوبہ زمرہ جات کی فہرست]] بھی ملاحظہ فرمائیں۔",
+       "categoriesfrom": "اس حرف سے شروع ہونے والے زمرے دکھائیں:",
+       "deletedcontributions": "حذف شدہ صارف کی شراکتیں",
+       "deletedcontributions-title": "صارف کی حذف شدہ شراکتیں",
        "sp-deletedcontributions-contribs": "شراکتیں",
        "linksearch": "بیرونی روابط کی تلاش",
        "linksearch-pat": "تلاش کا انداز",
        "linksearch-ns": "فضائے نام:",
        "linksearch-ok": "تلاش",
        "linksearch-line": "$1 مربوط ہے $2 سے",
+       "listusersfrom": "اس حرف سے شروع ہونے والے صارفین کے نام دکھائیں:",
        "listusers-submit": "دکھاؤ",
        "listusers-noresult": "یہ صارف نہیں ملا",
        "listusers-blocked": "(مسدود)",
        "activeusers": "متحرک صارفین کی فہرست",
-       "activeusers-hidebots": "پوشیدہ خود کار صارف",
-       "activeusers-hidesysops": "پوشیدہ منتظمین",
+       "activeusers-intro": "ذیل میں ان صارفین کی فہرست ہے جو گزشتہ $1 {{PLURAL:$1|دن|دنوں}} میں کسی بھی قسم کی سرگرمی میں شریک رہے ہوں۔",
+       "activeusers-count": "گزشتہ {{PLURAL:$3|دن|$3 دنوں}} میں $1 {{PLURAL:$1|اقدام|اقدامات}}",
+       "activeusers-from": "اس حرف سے شروع ہونے والے صارفین کے نام دکھائیں:",
+       "activeusers-hidebots": "خودکار صارفین کو چھپائیں",
+       "activeusers-hidesysops": "منتظمین کو چھپائیں",
        "activeusers-noresult": "یہ صارف نہیں مل سکا",
+       "activeusers-submit": "فعال صارفین دکھائیں",
+       "listgrouprights": "صارف گروہوں کے اختیارات",
+       "listgrouprights-summary": "ذیل میں اس ویکی پر موجود صارف گروہوں کی فہرست درج ہے۔ اس میں دائیں جانب گروہ کا نام اور بائیں جانب متعلقہ گروہ کو حاصل شدہ اختیارات کی تفصیل بیان کی گئی ہے۔\nانفرادی اختیارات کے متعلق [[{{MediaWiki:Listgrouprights-helppage}}|اضافی معلومات یہاں]] دیکھی جا سکتی ہیں۔",
+       "listgrouprights-key": "عنوان:\n* <span class=\"listgrouprights-granted\">تفویض کردہ اختیارات</span>\n* <span class=\"listgrouprights-revoked\">منسوخ کردہ اختیارات</span>",
        "listgrouprights-group": "گروہ",
        "listgrouprights-rights": "اختیارات",
+       "listgrouprights-helppage": "Help:اختیاراتِ گروہ",
        "listgrouprights-members": "(اراکین کی فہرست)",
+       "listgrouprights-addgroup": "{{PLURAL:$2|اس گروہ|ان گروہوں}} میں شامل کرنے کا اختیار ہے: \n\n$1",
+       "listgrouprights-removegroup": "{{PLURAL:$2|اس گروہ|ان گروہوں}} سے ہٹانے کا اختیار ہے: \n\n$1",
+       "listgrouprights-addgroup-all": "تمام گروہوں کا ا ضافہ کریں",
+       "listgrouprights-removegroup-all": "تمام گروہوں کو ہٹانے کا اختیار ہے",
+       "listgrouprights-addgroup-self": "{{PLURAL:$2|اس گروہ|ان گروہوں}} میں از خود شامل ہونے کا اختیار ہے: \n\n$1",
+       "listgrouprights-removegroup-self": "{{PLURAL:$2|اس گروہ|ان گروہوں}} سے از خود نکلنے کا اختیار ہے: \n\n$1",
+       "listgrouprights-addgroup-self-all": "تمام گروہوں میں از خود شامل ہونے کا اختیار ہے",
+       "listgrouprights-removegroup-self-all": "تمام گروہوں سے از خود نکلنے کا اختیار ہے",
+       "listgrouprights-namespaceprotection-header": "نام فضا پابندیاں",
        "listgrouprights-namespaceprotection-namespace": "فضائے نام",
+       "listgrouprights-namespaceprotection-restrictedto": "ترمیم کی اجازت دینے والے اختیار(ات)",
+       "listgrants": "عطا",
+       "listgrants-grant": "عطیہ",
+       "listgrants-rights": "حقوق",
+       "trackingcategories": "متلاشی زمرہ جات",
+       "trackingcategories-summary": "اس صفحہ میں ان متلاشی زمروں کی فہرست موجود جنہیں خودکار طور پر میڈیاویکی سافٹ ویئر تخلیق کرتا ہے۔ نیز {{ns:8}} نام فضا میں موجود متعلقہ نظامی پیغامات کے ذریعہ ان کے ناموں میں تبدیلی کی جا سکتی ہے۔",
        "trackingcategories-msg": "کھوجی زمرہ",
        "trackingcategories-name": "پیغام کا عنوان",
        "trackingcategories-desc": "زمرہ کی شمولیت کا معیار",
        "restricted-displaytitle-ignored": "صفحات مع نظرانداز کردہ عناوین",
+       "broken-file-category-desc": "اس صفحہ میں کسی فائل کا شکستہ ربط موجود ہے (یعنی ایسی فائل کا ربط دیا گیا ہے جو موجود نہیں)۔",
+       "hidden-category-category-desc": "اس زمرہ میں ابتدائی طور پر <code><nowiki>__HIDDENCAT__</nowiki></code> کا کوڈ شامل ہے جو اس زمرہ کو صفحات میں ظاہر ہونے سے روکتا ہے۔",
+       "trackingcategories-nodesc": "کوئی وضاحت دستیاب نہیں۔",
        "trackingcategories-disabled": "زمرہ غیر فعال ہے",
+       "mailnologin": "بھیجنے کے لیے کوئی پتہ نہیں",
        "mailnologintext": "دیگر ارکان کو برقی خط ارسال کرنے کیلیۓ لازم ہے کہ آپ [[Special:UserLogin|داخل شدہ]] حالت میں ہوں اور آپ کی [[Special:Preferences|ترجیحات]] ایک درست برقی خط کا پتا درج ہو۔",
        "emailuser": "صارف کو برقی خط لکھیں",
+       "emailuser-title-target": "اس {{GENDER:$1|صارف}} کو برقی خط لکھیں",
        "emailuser-title-notarget": "ای میل صارف",
+       "emailpagetext": "درج ذیل فارم کے ذریعہ آپ اس {{GENDER:$1|صارف}} کو برقی پیغام بھیج سکتے ہیں۔ جو برقی ڈاک پتا آپ نے [[Special:Preferences|اپنی ترجیحات]] میں دیا ہے وہ یہاں \"از\" کے طور پر نظر آئے گا، تاکہ وصول کنندہ براہ راست آپ کو جواب دے سکے۔",
        "defemailsubject": "{{SITENAME}} سے برقی خط",
        "usermaildisabled": "صارف برقی پتہ غیر فعال ہے",
        "usermaildisabledtext": "آپ اس ویکی پر رہتے ہوئے دوسرے صارف کو برقی خط ارسال نہيں کر سکتے",
        "noemailtitle": "کوئی برقی پتہ نہیں ہے",
-       "noemailtext": "اس صارف نے برقی خط کے لیے پتہ فراہم نہیں کیا، یا یہ چاہتا ہے کا اس سے کوئی صارف رابطہ نہ کرے۔",
+       "noemailtext": "اس صارف نے کوئی درست برقی ڈاک پتا نہیں دیا ہے۔",
+       "nowikiemailtext": "اس صارف نے دیگر صارفین سے برقی خط وصول نہ کرنے کا فیصلہ کیا ہے۔",
+       "emailnotarget": "وصول کنندہ موجود نہیں یا صارف نام نادرست ہے۔",
+       "emailtarget": "وصول کنندہ کا صارف نام داخل کریں",
        "emailusername": "صارف نام:",
+       "emailusernamesubmit": "روانہ کریں",
+       "email-legend": "{{SITENAME}} کے دوسرے صارف کو برقی خط بھیجیں",
        "emailfrom": "از:",
        "emailto": "بہ:",
        "emailsubject": "موضوع:",
        "emailmessage": "پیغام:",
        "emailsend": "بھیجیں",
        "emailccme": "میرے پیغام کی ایک نقل مجھے بھی میل کی جائے۔",
+       "emailccsubject": "$1 کو بھیجے جانے والے پیغام کا نسخہ: $2",
+       "emailsent": "ای میل بھیج دی گئی",
        "emailsenttext": "آپ کا پیغام بھیج دیا گیا۔",
+       "emailuserfooter": "اس برقی خط کو $1 نے {{SITENAME}} پر موجود «{{int:emailuser}}» کی سہولت کو استعمال کرتے ہوئے {{GENDER:$2|$2}} کو {{GENDER:$1|بھیجا}} ہے۔",
+       "usermessage-summary": "نظامی پیغام کی ترسیل۔",
+       "usermessage-editor": "نظامی پیغام رساں",
        "watchlist": "میری زیرنظرفہرست",
-       "mywatchlist": "زیرنظرفہرست",
+       "mywatchlist": "زیرنظر فہرست",
        "watchlistfor2": "براۓ $1 ($2)",
-       "addedwatchtext": "یہ صفحہ \"<nowiki>$1</nowiki>\" آپکی [[Special:Watchlist|زیرنظر]] فہرست میں شامل کردیا گیا ہے۔ اب مستقل میں اس صفحے اور اس سے ملحقہ تبادلہ خیال کا صفحے میں کی جانے والی تبدیلوں کا اندراج کیا جاتا رہے گا، اور ان صفحات کی شناخت کو سہل بنانے کے لیۓ [[Special:حالیہ تبدیلیاں|حالیہ تبدیلیوں کی فہرست]] میں انکو '''مُتَجَل''' (bold) تحریر کیا جاۓ گا۔ <p> اگر آپ کسی وقت اس صفحہ کو زیرنظرفہرست سے خارج کرنا چاہیں تو اوپر دیۓ گۓ \"زیرنظرمنسوخ\" پر ٹک کیجیۓ۔",
-       "removedwatchtext": "صفحہ \"[[:$1]]\" آپ کی زیر نظر فہرست سے خارج کر دیا گیا۔",
-       "watch": "زیرنظر",
-       "watchthispage": "یہ صفحہ زیر نظر کیجیۓ",
+       "nowatchlist": "آپ کی زیرنظر فہرست میں کوئی مواد موجود نہیں ہے۔",
+       "watchlistanontext": "اپنی زیرنظر فہرست میں موجود مواد کو دیکھنے اور ان میں ترمیم کرنے کے لیے براہ کرم لاگ ان کریں۔",
+       "watchnologin": "داخل نوشتہ نہیں",
+       "addwatch": "زیر نظر فہرست میں شامل کریں",
+       "addedwatchtext": "صفحہ «[[:$1]]» اور اس کا تبادلۂ خیال صفحہ آپ کی [[Special:Watchlist|زیرنظر فہرست]] میں شامل کردیا گیا ہے۔",
+       "addedwatchtext-talk": "صفحہ «[[:$1]]» اور اس سے ملحقہ صفحہ آپ کی [[Special:Watchlist|زیرنظر فہرست]] میں شامل کردیا گیا ہے۔",
+       "addedwatchtext-short": "صفحہ «$1» کو آپ کی زیرنظر فہرست میں شامل کر دیا گیا ہے۔",
+       "removewatch": "زیرنظر فہرست سے ہٹائیں",
+       "removedwatchtext": "صفحہ «[[:$1]]» اور اس کا تبادلۂ خیال صفحہ آپ کی [[Special:Watchlist|زیرنظر فہرست]] سے خارج کر دیا گیا ہے۔",
+       "removedwatchtext-talk": "صفحہ «[[:$1]]» اور اس سے ملحقہ صفحہ آپ کی [[Special:Watchlist|زیرنظر فہرست]] سے خارج کر دیا گیا ہے۔",
+       "removedwatchtext-short": "صفحہ «$1» کو آپ کی زیرنظر فہرست سے خارج کر دیا گیا ہے۔",
+       "watch": "زیر نظر کریں",
+       "watchthispage": "اس صفحہ کو زیر نظر کریں",
        "unwatch": "زیرنظرمنسوخ",
-       "watchlist-details": "آپ کی زیرِنظرفہرست پر {{PLURAL:$1|$1 صفحہ ہے|$1 صفحات ہیں}}، اِس میں تبادلۂ خیال صفحات کی تعداد شامل نہیں.",
-       "wlnote": "نیچےآخری $1 تبدیلیاں ہیں جو کے پیچھلے <b>$2</b> گھنٹوں میں کی گئیں۔",
+       "unwatchthispage": "زیرنظر فہرست سے خارج کریں",
+       "notanarticle": "ویکی کے موضوع سے متعلق صفحہ نہیں ہے",
+       "notvisiblerev": "دوسرے صارف کی آخری ترمیم حذف کر دی گئی",
+       "watchlist-details": "آپ کی زیرنظر فہرست میں {{PLURAL:$1|$1 صفحہ ہے|$1 صفحات ہیں}}، اس میں تبادلۂ خیال صفحات کی تعداد شامل نہیں ہے۔",
+       "wlheader-enotif": "ای میل کی اطلاع فعال ہے ۔",
+       "wlheader-showupdated": "آپ کی آخری آمد کے بعد جن صفحات میں تبدیلی ہوئی ہے وہ <strong>جلی حروف</strong> میں نظر آئیں گے۔",
+       "wlnote": "ذیل میں گزشتہ {{PLURAL:$2|گھنٹے|<strong>$2</strong> گھنٹوں}} میں ہونے والی {{PLURAL:$1|تبدیلی|<strong>$1</strong> تبدیلیوں}} کی فہرست درج ہے، تاریخ تجدید $3، $4",
        "wlshowlast": "دکھائیں آخری $1 گھنٹے $2 دن",
        "watchlist-hide": "چھپائیں",
        "watchlist-submit": "دکھائیں",
        "wlshowhidemine": "میری ترامیم",
        "wlshowhidecategorization": "صفحاتی زمرہ بندی",
        "watchlist-options": "اختیارات برائے زیرِنظرفہرست",
+       "watching": "زیرنظر فہرست میں شامل کیا جا رہا ہے۔۔۔",
+       "unwatching": "زیرنظر فہرست سے خارج کیا جا رہا ہے۔۔۔",
+       "watcherrortext": "«$1» کے لیے آپ کی زیرنظر فہرست کی ترتیبات میں تبدیلی کے دوران میں کوئی نقص ہوا۔",
        "enotif_reset": "جملہ صفحات کو بطور زیارت شدہ نشان زد کریں",
+       "enotif_impersonal_salutation": "{{SITENAME}} کا صارف",
        "enotif_subject_deleted": "{{SITENAME}} میں صفحہ $1 صارف $2 نے {{GENDER:$2|حذف کیا}}",
        "enotif_subject_created": "{{SITENAME}} میں صفحہ $1 کو $2 نے {{GENDER:$2|تخلیق کیا}}",
        "enotif_subject_moved": "{{SITENAME}} میں صفحہ $1 کو $2 نے {{GENDER:$2|منتقل کیا}}",
        "enotif_body_intro_changed": "{{SITENAME}} میں صفحہ $1 میں بتاریخ $PAGEEDITDATEء صارف $2 نے {{GENDER:$2|تبدیلی کی}}، موجودہ نسخہ دیکھنے کے لیے $3 ملاحظہ فرمائیں۔",
        "enotif_lastvisited": "آپ کی آخری آمد کے بعد سے ہونے والی تمام تبدیلیوں کو دیکھنے کے لیے $1 کو ملاحظہ فرمائیں۔",
        "enotif_lastdiff": "اس تبدیلی کو دیکھنے کے لیے $1 کو ملاحظہ فرمائیں۔",
+       "enotif_anon_editor": "گمنام صارف $1",
        "enotif_body": "جناب $WATCHINGUSERNAME!\n\n$PAGEINTRO $NEWPAGE\n\nخلاصہ ترمیم: $PAGESUMMARY $PAGEMINOREDIT\n\nصارف سے رابطہ کریں:\nبذریعہ برقی خط: $PAGEEDITOR_EMAIL\nبذریعہ ویکی: $PAGEEDITOR_WIKI\n\nاس صفحہ میں آئندہ ہونے والی تبدیلیوں کی اطلاعات آپ کو موصول نہیں ہوگی جب تک آپ لاگ ان ہو کر اس صفحہ کو ملاحظہ نہ کر لیں۔ نیز آپ اپنی زیر نظر فہرست میں موجود تمام صفحات سے اطلاعی علامتیں بھی ختم کر سکتے ہیں۔\n\nفقط\nآپ کا خادم، {{SITENAME}} نظام اطلاعات\n\n--\nاطلاعات بذریعہ برقی خط کی ترتیبات تبدیل کرنے کے لیے\n{{canonicalurl:{{#special:Preferences}}}} ملاحظہ فرمائیں\n\nاپنی زیر نظر فہرست کی ترتیبات میں تبدیلی کے لیے\n{{canonicalurl:{{#special:EditWatchlist}}}} ملاحظہ فرمائیں\n\nاس صفحہ کو اپنی زیر نظر فہرست سے حذف کرنے کے لیے\n$UNWATCHURL ملاحظہ فرمائیں\n\nتجاویز اور مزید معاونت کے لیے ملاحظہ فرمائیں:\n$HELPPAGE",
        "created": "بنا دیا گیا",
        "changed": "تبدیل کردیاگیا",
-       "deletepage": "صÙ\81Ø­Û\81 Ø¶Ø§Ø¦Ø¹ کریں",
+       "deletepage": "حذÙ\81 کریں",
        "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": "<strong>اÙ\86تباÛ\81</strong>: Ø¢Ù¾ Ø§Ø³ ØµÙ\81Ø­Û\81 Ú©Ù\88 $1 {{PLURAL:$1|Ù\86ظر Ø«Ø§Ù\86Û\8c\86ظر Ø«Ø§Ù\86Û\8cوں}} کے تاریخچہ کے ساتھ حذف کر رہے ہیں:",
+       "historywarning": "<strong>اÙ\86تباÛ\81</strong>: Ø¢Ù¾ Ø§Ø³ ØµÙ\81Ø­Û\81 Ú©Ù\88 $1 {{PLURAL:$1|Ù\86سخÛ\81\86سخوں}} کے تاریخچہ کے ساتھ حذف کر رہے ہیں:",
        "historyaction-submit": "دکھائیں",
-       "confirmdeletetext": "آپ نے اس صفحے کو اس سے ملحقہ تاریخچہ سمیت حذف کرنے کا ارادہ کیا ہے۔ براۓ مہربانی تصدیق کرلیجیۓ کہ آپ اس عمل کے نتائج سے بخوبی آگاہ ہیں، اور یہ بھی یقین کرلیجیۓ کہ آپ ایسا [[{{MediaWiki:Policy-url}}|ویکیپیڈیا کی حکمت عملی]] کے دائرے میں رہ کر کر رہے ہیں۔",
+       "confirmdeletetext": "آپ اس صفحے کو اس سے ملحقہ تاریخچہ سمیت حذف کر رہے ہیں۔ براہ مہربانی اس بات کی تصدیق کر لیں کہ آپ اس عمل کے نتائج سے بخوبی آگاہ ہیں، اور یہ بھی جانچ لیں کہ آیا آپ کا یہ اقدام [[{{MediaWiki:Policy-url}}|حکمت عملی]] کے دائرے میں ہے یا نہیں۔",
        "actioncomplete": "اقدام تکمیل کو پہنچا",
        "actionfailed": "عمل ناکام",
-       "deletedtext": "\"$1\" کو حذف کر دیا گیا ہے ۔\nحالیہ حذف شدگی کے تاریخ نامہ کیلیۓ  $2  دیکھیۓ",
+       "deletedtext": "\"$1\" کو حذف کر دیا گیا ہے۔\nحالیہ حذف شدگیوں کی فہرست دیکھنے کے لیے $2 ملاحظہ فرمائیں۔",
        "dellogpage": "نوشتۂ حذف شدگی",
        "dellogpagetext": "حالیہ حذف شدگی کی فہرست درج ذیل ہے۔",
        "deletionlog": "نوشتۂ حذف شدگی",
+       "reverted": "ابتدائی نسخہ کی جانب واپس پھیر دیا گیا",
        "deletecomment": "وجہ:",
        "deleteotherreason": "دوسری/اِضافی وجہ:",
        "deletereasonotherlist": "دوسری وجہ",
+       "deletereason-dropdown": "* عمومی وجوہات حذف\n** فاضل کاری\n** تخریب کاری\n** کاپی رائٹ کی خلاف ورزی\n** مصنف کی درخواست\n** شکستہ روابط",
+       "delete-edit-reasonlist": "وجوہات حذف میں ترمیم کریں",
+       "delete-toobig": "$1 {{PLURAL:$1|نسخے|نسخوں}} پر مشتمل اس صفحہ کا تاریخچہ بہت طویل ہے۔\n{{SITENAME}} پر کسی حادثاتی انتشار سے بچنے کے لیے اس طرح کے صفحات کو حذف کرنے کی اجازت نہیں ہے۔",
+       "delete-warning-toobig": "$1 {{PLURAL:$1|نسخے|نسخوں}} پر مشتمل اس صفحہ کا تاریخچہ بہت طویل ہے۔\nعین ممکن ہے کہ اسے حذف کرنے سے {{SITENAME}} کے ڈیٹابیس کی کارروائیاں انتشار کا شکار ہو جائیں؛ لہذا احتیاط سے آگے بڑھیں۔",
+       "deleteprotected": "آپ اس صفحہ کو حذف نہیں کر سکتے کیونکہ اسے محفوظ کر دیا گیا ہے۔",
+       "deleting-backlinks-warning": "<strong>انتباہ:</strong> جس صفحہ کو آپ حذف کر رہے ہیں اس سے مربوط یا اس میں شامل [[Special:WhatLinksHere/{{FULLPAGENAME}}|دیگر صفحات]]۔",
        "rollback": "ترمیمات سابقہ حالت پرواپس",
        "rollbacklink": "استرجع کریں",
        "rollbacklinkcount": "استرجع $1 {{PLURAL:$1|ترمیم|ترامیم}}",
        "rollbacklinkcount-morethan": "$1 {{PLURAL:$1|ترمیم|ترامیم}} سے زیادہ کا استرجع",
        "rollbackfailed": "سابقہ حالت پر واپسی ناکام",
+       "rollback-missingparam": "درخواست میں ضروری پیرامیٹر موجود نہیں۔",
+       "rollback-missingrevision": "نسخہ کی معلومات لوڈ نہیں ہو سکتی۔",
        "cantrollback": "تدوین ثانی کا اعادہ نہیں کیا جاسکتا؛ کیونکہ اس میں آخری بار حصہ لینے والا ہی اس صفحہ کا واحد کاتب ہے۔",
+       "editcomment": "خلاصہ ترمیم یہ تھا: <em>«$1»</em>.",
+       "revertpage": "[[Special:Contributions/$2|$2]] ([[User talk:$2|تبادلۂ خیال]]) کی ترامیم [[User:$1|$1]] کی گذشتہ ترمیم کی جانب واپس پھیر دی گئیں۔",
+       "revertpage-nouser": "(حذف شدہ صارف نام) کی ترامیم {{GENDER:$1|[[User:$1|$1]]}} کی گذشتہ ترمیم کی جانب واپس پھیر دی گئیں",
+       "rollback-success": "$1 کی ترامیم واپس پھیر دی گئیں؛\nصفحہ واپس $2 کی آخری ترمیم کی جانب منتقل کر دیا گیا۔",
+       "rollback-success-notify": "$1 کی ترامیم واپس پھیر دی گئیں؛\nصفحہ واپس $2 کی آخری ترمیم کی جانب منتقل کر دیا گیا۔ [$3 تبدیلیاں دکھائیں]",
+       "sessionfailure-title": "نشست میں خامی",
+       "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 نے مواد کے غیر ڈیفالٹ ماڈل «$5» کے ذریعہ  صفحہ $3 کو {{GENDER:$2|تخلیق کیا}}",
        "logentry-contentmodel-change": "$1 نے صفحہ $3 کے مواد کی ساخت کو \"$4\" سے \"$5\" میں {{GENDER:$2|تبدیل کیا}}",
+       "logentry-contentmodel-change-revertlink": "استرجع",
+       "logentry-contentmodel-change-revert": "استرجع",
        "protectlogpage": "نوشتۂ محفوظ شدگی",
+       "protectlogtext": "ذیل میں صفحات کے درجہ حفاظت کی تبدیلیوں کی فہرست درج ہے۔\nموجودہ محفوظ صفحات کی فہرست دیکھنے کے لیے [[Special:ProtectedPages|محفوظ صفحات کی فہرست]] ملاحظہ فرمائیں۔",
        "protectedarticle": "\"[[$1]]\" کومحفوظ کردیا",
-       "unprotectedarticle": "\"[[$1]]\" کوغیر محفوظ کیا",
+       "modifiedarticleprotection": "«[[$1]]» کا درجہ حفاظت تبدیل کیا",
+       "unprotectedarticle": "«[[$1]]» کو غیر محفوظ کیا",
        "movedarticleprotection": "نے \"[[$2]]\" کا درجہ حفاظت \"[[$1]]\" کی جانب منتقل کیا",
+       "protect-title": "«$1» کا درجہ حفاظت تبدیل کریں",
+       "protect-title-notallowed": "«$1» کا درجہ حفاظت دیکھیں",
        "prot_1movedto2": "[[$1]] بجانب [[$2]] منتقل",
+       "protect-badnamespace-title": "ناقابل حفاظت نام فضا",
+       "protect-badnamespace-text": "اس نام فضا میں موجود صفحات کو محفوظ نہیں کیا جا سکتا۔",
+       "protect-norestrictiontypes-text": "اس صفحہ کو محفوظ نہیں کیا جا سکتا کیونکہ حفاظت کے مطلوبہ پیرامیٹر دستیاب نہیں ہیں۔",
+       "protect-norestrictiontypes-title": "ناقابل حفاظت صفحہ",
+       "protect-legend": "تحفظ کی تصدیق کریں",
        "protectcomment": "وجہ:",
-       "protect-default": "تمام صارفین کو اہل بناؤ",
+       "protectexpiry": "زاید میعاد:",
+       "protect_expiry_invalid": "وقت اختتام نادرست ہے۔",
+       "protect_expiry_old": "وقت اختتام گزر چکا ہے۔",
+       "protect-unchain-permissions": "مزید اختیارات حفاظت کو غیر مقفل کریں",
+       "protect-text": "یہاں آپ <strong>$1</strong> کا درجۂ حفاظت دیکھ اور تبدیل کر سکتے ہیں۔",
+       "protect-locked-blocked": "پابندی کے دوران میں آپ درجہ حفاظت میں تبدیلی نہیں کر سکتے۔\nصفحہ <strong>$1</strong> کی حالیہ ترتیبات یہاں دیکھی جا سکتی ہیں:",
+       "protect-locked-dblock": "ڈیٹابیس مقفل ہونے کی وجہ سے درجہ حفاظت کو تبدیل نہیں کیا جا سکتا۔\nصفحہ <strong>$1</strong> کی حالیہ ترتیبات یہاں دیکھی جا سکتی ہیں:",
+       "protect-locked-access": "آپ کو درجہ حفاظت تبدیل کرنے کا اختیار حاصل نہیں ہے۔\nصفحہ <strong>$1</strong> کی حالیہ ترتیبات یہاں دیکھی جا سکتی ہیں:",
+       "protect-cascadeon": "یہ صفحہ محفوظ ہے کیونکہ یہ درج ذیل {{PLURAL:$1|صفحہ|صفحات}} میں شامل ہے جہاں آبشاری حفاظت فعال ہے۔\nآپ اس صفحہ کے درجۂ حفاظت میں تبدیلی کرسکتے ہیں، لیکن یہ آبشاری حفاظت پر اثر انداز نہیں ہوگی۔",
+       "protect-default": "تمام صارفین کو اجازت ہے",
+       "protect-fallback": "محض «$1» کا اختیار رکھنے والے صارفین کو اجازت ہے",
+       "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": "آپ اس صفحہ کے درجہ حفاظت میں تبدیلی نہیں کر سکتے کیونکہ آپ کو اس میں ترمیم کرنے کی اجازت نہیں ہے۔",
        "protect-othertime": "دیگر وقت:",
        "protect-othertime-op": "دیگر وقت",
+       "protect-existing-expiry": "موجودہ وقت اختتام: $3، $2",
+       "protect-existing-expiry-infinity": "موجودہ وقت اختتام: لامحدود",
+       "protect-otherreason": "دوسری/اضافی وجہ:",
        "protect-otherreason-op": "دیگر وجہ",
+       "protect-dropdown": "* عمومی وجوہات حفاظت\n** مسلسل تخریب کاری\n** مسلسل فاضل کاری\n** غیر مفید ترمیمی جنگ\n** زیادہ آمد و رفت صفحہ",
+       "protect-edit-reasonlist": "وجوہات حفاظت میں ترمیم کریں",
        "protect-expiry-options": "1 hour:1 hour,1 day:1 day,1 week:1 week,2 weeks:2 weeks,1 month:1 month,3 months:3 months,6 months:6 months,1 year:1 year,infinite:infinite",
        "restriction-type": "اجازت:",
+       "restriction-level": "درجہ پابندی:",
+       "minimum-size": "کم از کم سائز",
+       "maximum-size": "زیادہ سے زیادہ سائز:",
        "pagesize": "(بائیٹ)",
-       "restriction-edit": "تحرÛ\8cر Ù\88 ØªØ±Ù\85Û\8cÙ\85",
+       "restriction-edit": "ترمیم",
        "restriction-move": "منتقل",
        "restriction-create": "تخلیق",
        "restriction-upload": "اپلوڈ",
        "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": "ترامیم بحال کریں",
-       "undeletehistory": "اگر آپ اس صفحہ کو بحال کرتے ہیں، تو اس صفحہ کے تاریخچہ میں تمام ترامیم بھی بحال ہوجائیگی۔\nاگر حذف شدگی کے بعد کوئی نیا صفحہ اسی نام سے تخلیق کیا گیا ہو، تو تمام بحال شدہ ترامیم گذشتہ تاریخچہ میں ظاہر ہوگی۔",
-       "undeleterevdel": "بحالیٔ صفحہ کا اقدام مکمل نہیں ہوگا اگر اس کا تنیجہ صفحہ کے اوپر کے حصہ کی ترمیم یا ملف کا اعادہ جزوی طور پر حذف کیا جارہا ہو۔\nایسی صورت میں لازمی طور آپ حالیہ حذف شدہ اعادے کو ظاہر کریں۔",
+       "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": "دیکھو/بحال کرو",
        "undeleteviewlink": "دکھاؤ",
        "undeleteinvert": "انتخاب بالعکس",
        "undeletecomment": "وجہ:",
        "undeletedrevisions": "{{PLURAL:$1|1 نظر ثانی|$1 نظر ثانیاں}} بحال",
-       "undeletedrevisions-files": "{{PLURAL:$1|1 نظر ثانی|$1 نظر ثانیاں}} اور {{PLURAL:$2|1 ملف|$2 املاف}} بحال",
-       "undeletedfiles": "{{PLURAL:$1|1 ملف|$1 املاف}} بحال",
+       "undeletedrevisions-files": "{{PLURAL:$1|1 نسخہ|$1 نسخے}} اور {{PLURAL:$2|1 فائل|$2 فائلیں}} بحال",
+       "undeletedfiles": "{{PLURAL:$1|1 فائل|$1 فائل}} بحال کی {{PLURAL:$1|گئی|گئیں}}",
+       "cannotundelete": "کلی یا جزوی طور پر بحالی کا اقدام ناکام رہا:\n$1",
+       "undeletedpage": "<strong>$1 کو بحال کر دیا گیا</strong>\n\nحالیہ حذف شدگیوں اور بحالیوں کا نوشتہ دیکھنے کے لیے [[Special:Log/delete|نوشتہ حذف شدگی]] ملاحظہ فرمائیں۔",
        "undelete-header": "حالیہ حذف شدہ صفحات کے لیے [[Special:Log/delete|نوشتۂ حذف شدگی]] دیکھیں۔",
        "undelete-search-title": "حذف شدہ صفحات میں تلاش کریں",
        "undelete-search-box": "حذف شدہ صفحات میں تلاش کریں",
        "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",
+       "undelete-show-file-confirm": "کیا آپ واقعی فائل <nowiki>$1</nowiki> کا مورخہ $2 بوقت $3 بجے کا حذف شدہ نسخہ دیکھنا چاہتے ہیں؟",
        "undelete-show-file-submit": "ہاں",
        "namespace": "نام فضا:",
-       "invert": "انتخاب بالعکس",
-       "tooltip-invert": "منتخب شدہ فضائے نام (اور مُلحقہ فضائے نام) میں شامل صفحات کی تبدیلیوں کو چُھپانے کیلئے اِس خانہ کو ٹِک کریں۔",
-       "namespace_association": "متعلقہ نام فضا",
-       "blanknamespace": "(مرکز)",
-       "contributions": "{{GENDER:$1|صارف}} شراکتیں",
-       "contributions-title": "مساہماتِ صارف برائے $1",
-       "mycontris": "شراکت",
+       "invert": "انتخاب معکوس",
+       "tooltip-invert": "منتخب نام فضا (اور مُلحقہ نام فضا) میں شامل صفحات کی تبدیلیوں کو چھپانے کے لیے اس خانہ کو نشان زد کریں۔",
+       "tooltip-whatlinkshere-invert": "منتخب نام فضا میں موجود صفحات کے روابط چھپانے کے لیے اس خانہ کو نشان زد کریں۔",
+       "namespace_association": "ملحقہ نام فضا",
+       "tooltip-namespace_association": "منتخب نام فضا سے منسلک تبادلۂ خیال یا ذیلی نام فضا کو شامل کرنے کے لیے اس خانہ کو نشان زد کریں",
+       "blanknamespace": "(مرکزی)",
+       "contributions": "{{GENDER:$1|صارف}} کی شراکتیں",
+       "contributions-title": "صارف $1 کی شراکتیں",
+       "mycontris": "شراکتیں",
        "anoncontribs": "شراکتیں",
-       "contribsub2": "برائے {{GENDER:$3|$1}} ($2)",
-       "uctop": "(موجودہ)",
+       "contribsub2": "{{GENDER:$3|$1}} ($2)",
+       "contributions-userdoesnotexist": "«$1» کے نام سے صارف کھاتہ مندرج نہیں ہے۔",
+       "nocontribs": "اس معیار کے مطابق کوئی ترمیم دستیاب نہیں ہوئی۔",
+       "uctop": "(موجودہ نسخہ)",
        "month": "مہینہ (اور اُس سے قبل):",
        "year": "سال (اور اُس سے قبل):",
-       "sp-contributions-newbies": "صرف نئے کھاتوں کے مساہمات دکھاؤ",
+       "sp-contributions-newbies": "محض جدید صارفین کی شراکتیں دکھائیں",
+       "sp-contributions-newbies-sub": "جدید صارفین کے",
+       "sp-contributions-newbies-title": "جدید صارفین کی شراکتیں",
        "sp-contributions-blocklog": "نوشتۂ پابندی",
-       "sp-contributions-uploads": "اثقالات",
+       "sp-contributions-suppresslog": "{{GENDER:$1|صارف}} کی پوشیدہ شراکتیں",
+       "sp-contributions-deleted": "{{GENDER:$1|صارف}} کی حذف شدہ شراکتیں",
+       "sp-contributions-uploads": "اپلوڈ کردہ",
        "sp-contributions-logs": "نوشتہ جات",
-       "sp-contributions-talk": "گفتگو",
+       "sp-contributions-talk": "تبادلۂ خیال",
        "sp-contributions-userrights": "انتظام اختیارات صارف",
-       "sp-contributions-search": "تلاش برائے مساہمات",
-       "sp-contributions-username": "آئی.پی پتہ یا اسمِ صارف:",
-       "sp-contributions-toponly": "صرف حالیہ ترین نظرثانی ترمیمات دِکھاؤ",
+       "sp-contributions-blocked-notice": "اس صارف پر پابندی لگائی گئی ہے۔\nحوالہ کے لیے نوشتہ پابندی کا تازہ ترین اندراج ذیل میں دستیاب ہے:",
+       "sp-contributions-blocked-notice-anon": "اس آئی پی پتے پر پابندی لگا دی گئی ہے۔\nحوالہ کے لیے نوشتہ پابندی کا تازہ ترین اندراج ذیل میں دستیاب ہے:",
+       "sp-contributions-search": "شراکتوں میں تلاش کریں",
+       "sp-contributions-username": "آئی پی پتا یا صارف نام:",
+       "sp-contributions-toponly": "محض نئے نسخوں پر مشتمل ترامیم دکھائیں",
+       "sp-contributions-newonly": "محض نئے صفحات دکھائیں",
        "sp-contributions-hideminor": "معمولی ترامیم چھپائیں",
        "sp-contributions-submit": "تلاش",
-       "whatlinkshere": "ادھر کونسا ربط ہے",
-       "whatlinkshere-title": "\"$1\" سے مربوط صفحات",
+       "whatlinkshere": "مربوط صفحات",
+       "whatlinkshere-title": "«$1» سے مربوط صفحات",
        "whatlinkshere-page": "صفحہ:",
-       "linkshere": "'''[[:$1]]''' سے درج ذیل صفحات مربوط ہیں:",
-       "nolinkshere": "'''[[:$1]]''' سے کوئی روابط نہیں۔",
-       "isredirect": "لوٹایا گیا صفحہ",
+       "linkshere": "<strong>[[:$1]]</strong> سے درج ذیل صفحات مربوط ہیں:",
+       "nolinkshere": "<strong>[[:$1]]</strong> سے کوئی صفحہ مربوط نہیں ہے۔",
+       "nolinkshere-ns": "منتخب نام فضا میں <strong>[[:$1]]</strong> سے مربوط کوئی صفحہ نہیں ہے۔",
+       "isredirect": "رجوع مکرر صفحہ",
        "istemplate": "شامل شدہ",
-       "isimage": "ربطِ ملف",
+       "isimage": "فائل کا ربط",
        "whatlinkshere-prev": "{{PLURAL:$1|پچھلا|پچھلے $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|اگلا|اگلے $1}}",
-       "whatlinkshere-links": "روابط ←",
+       "whatlinkshere-links": "→ روابط",
        "whatlinkshere-hideredirs": "رجوع مکررات $1",
-       "whatlinkshere-hidetrans": "$1 استعمالات",
+       "whatlinkshere-hidetrans": "استعمالات $1",
        "whatlinkshere-hidelinks": "روابط $1",
-       "whatlinkshere-hideimages": "رÙ\88ابطÙ\90 ØªØµØ§Ù\88Û\8cر $1",
-       "whatlinkshere-filters": "Ù\81Ù\84ٹرذ",
+       "whatlinkshere-hideimages": "تصÙ\88Û\8cر Ú©Û\92 Ø±Ù\88ابط $1",
+       "whatlinkshere-filters": "Ù\85Ù\82طارات",
        "whatlinkshere-submit": "ٹھیک",
+       "autoblockid": "خودکار پابندی #$1",
+       "block": "صارف مسدود کریں",
+       "unblock": "صارف سے پابندی ہٹائیں",
        "blockip": "داخلہ ممنوع برائے صارف",
        "blockip-legend": "ممنوع کردہ صارفین",
+       "ipaddressorusername": "آئی پی پتہ یا صارف نام:",
+       "ipbexpiry": "وقت اختتام:",
        "ipbreason": "وجہ:",
+       "ipbreason-dropdown": "* عمومی وجوہات پابندی\n** غلط معلومات کا اندراج\n** صفحات سے متن کا مٹانا\n** بیرونی روابط میں بے کار روابط کی فاضل کاری\n** صفحات میں لغو چیزوں کا اندراج\n** بدتمیزی/بداخلاقی\n** متعدد کھاتوں کا استعمال\n** ناقابلِ قبول اسمِ صارف",
+       "ipb-hardblock": "اس آئی پی پتے سے داخل شدہ صارفین کو ترمیم کاری سے باز رکھیں",
+       "ipbcreateaccount": "کھاتہ سازی سے باز رکھیں",
+       "ipbemailban": "برقی خط بھیجنے سے باز رکھیں",
        "ipbsubmit": "اس صارف کا داخلہ ممنوع کریں",
+       "ipbother": "دیگر وقت:",
        "ipboptions": "2 گھنٹے:2 hours,1 یوم:1 day,3 ایام:3 days,1 ہفتہ:1 week,2 ہفتے:2 weeks,1 مہینہ:1 month,3 مہینے:3 months,6 مہینے:6 months,1 سال:1 year,لامحدود:infinite",
+       "ipbhidename": "ترامیم اور فہرستوں سے صارف نام کو چھپائیں",
+       "ipbwatchuser": "اس صارف کے صارف اور تبادلۂ خیال صفحات کو زیر نظر کریں",
+       "ipb-disableusertalk": "بحالت پابندی اس صارف کو اپنے ذاتی تبادلۂ خیال صفحہ میں ترمیم کرنے سے باز رکھیں",
+       "ipb-change-block": "ان ترتیبات کے ساتھ اس صارف پر دوبارہ پابندی لگائیں",
+       "ipb-confirm": "پابندی کی تصدیق کریں",
+       "badipaddress": "نادرست آئی پی پتا",
+       "blockipsuccesssub": "پابندی لگا دی گئی",
+       "blockipsuccesstext": "[[Special:Contributions/$1|$1]] پر پابندی لگادی گئی۔<br />\nپابندیوں پر نظر ثانی کے لیے [[Special:BlockList|فہرست پابندی]] دیکھیں۔",
+       "ipb-blockingself": "آپ اپنے آپ پر پابندی لگانے جا رہے ہیں! کیا آپ واقعی ایسا کرنا چاہتے ہیں؟",
+       "ipb-edit-dropdown": "وجوہات پابندی میں ترمیم کریں",
+       "ipb-unblock-addr": "$1 سے پابندی ہٹائیں",
+       "ipb-unblock": "صارف نام یا آئی پی پتے سے پابندی ہٹائیں",
+       "ipb-blocklist": "موجودہ پابندیاں دیکھیں",
+       "ipb-blocklist-contribs": "{{GENDER:$1|$1}} کی شراکتیں",
+       "ipb-blocklist-duration-left": "$1 باقی ہے",
+       "unblockip": "صارف سے پابندی ہٹائیں",
+       "unblockiptext": "گزشتہ ممنوع صارف یا آئی پی پتے کی تحریری دسترس بحال کرنے کے لیے درج ذیل فارم استعمال کریں۔",
+       "ipusubmit": "اس پابندی کو ہٹائیں",
+       "unblocked": "[[User:$1|$1]] سے پابندی ہٹا دی گئی۔",
+       "unblocked-range": "$1 سے پابندی ہٹا دی گئی۔",
+       "unblocked-id": "پابندی نمبر $1 سے پابندی ہٹا دی گئی۔",
+       "unblocked-ip": "[[Special:Contributions/$1|$1]] سے پابندی ہٹا دی گئی۔",
+       "blocklist": "ممنوع صارفین",
        "ipblocklist": "ممنوع صارفین",
+       "ipblocklist-legend": "ممنوع صارف کو تلاش کریں",
+       "blocklist-userblocks": "کھاتے کی پابندیاں چھپائیں",
+       "blocklist-tempblocks": "عارضی پابندیاں چھپائیں",
+       "blocklist-addressblocks": "تنہا آئی پی پابندیوں کو چھپائیں",
+       "blocklist-rangeblocks": "رینج پابندیاں چھپائیں",
+       "blocklist-timestamp": "وقت کی مہر",
+       "blocklist-target": "ہدف",
+       "blocklist-expiry": "وقت اختتام",
+       "blocklist-by": "منتظم",
+       "blocklist-params": "پابندی کے پیرامیٹر",
        "blocklist-reason": "وجہ",
        "ipblocklist-submit": "تلاش",
-       "infiniteblock": "مستقل",
+       "ipblocklist-localblock": "مقامی پابندی",
+       "ipblocklist-otherblocks": "دیگر {{PLURAL:$1|پابندی|پابندیاں}}",
+       "infiniteblock": "لا محدود",
+       "expiringblock": "مورخہ $1 بوقت $2 بجے اختتام پزیر ہوگی",
+       "anononlyblock": "محض گمنام صارف",
+       "noautoblockblock": "خودکار پابندی غیر فعال",
+       "createaccountblock": "کھاتہ سازی غیر فعال",
+       "emailblock": "برقی خط غیر فعال",
+       "blocklist-nousertalk": "اپنے ذاتی تبادلۂ خیال میں ترمیم نہیں کر سکتا",
+       "ipblocklist-empty": "پابندیوں کی فہرست خالی ہے۔",
+       "ipblocklist-no-results": "درخواست شدہ آئی پی پتے یا صارف نام پر پابندی عائد نہیں ہے",
        "blocklink": "پابندی لگائیں",
        "unblocklink": "پابندی ختم",
        "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": "کھاتے کی تخلیق غیرفعال",
+       "block-log-flags-noautoblock": "خودکار پابندی غیر فعال",
+       "block-log-flags-noemail": "برقی خط غیر فعال",
+       "block-log-flags-nousertalk": "اپنے ذاتی تبادلۂ خیال میں ترمیم نہیں کر سکتا",
+       "block-log-flags-angry-autoblock": "پیشرفتہ خودکار پابندی فعال",
+       "block-log-flags-hiddenname": "صارف نام پوشیدہ ہے",
+       "range_block_disabled": "منتظمین سے رینج پر پابندی لگانے کا اختیار واپس لے لیا گیا ہے۔",
+       "ipb_expiry_invalid": "وقت اختتام نادرست ہے۔",
+       "ipb_expiry_old": "وقت اختتام گزر چکا ہے۔",
+       "ipb_expiry_temp": "پوشیدہ صارف نام پر عائد کی جانے والی پابندیاں مستقل ہونا لازمی ہے۔",
+       "ipb_hide_invalid": "اس کھاتے کو دبایا نہیں جا سکا؛ اس کھاتے سے {{PLURAL:$1|ایک ترمیم کی گئی ہے|$1 ترامیم کی گئی ہیں}}۔",
+       "ipb_already_blocked": "«$1» پر پہلے ہی پابندی لگا دی گئی ہے۔",
+       "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": "پراکسی مسدود کنندہ",
+       "proxyblockreason": "آپ کے آئی پی پتے پر پابندی لگا دی گئی ہے کیونکہ یہ اوپن پراکسی ہے۔\nبراہ کرم انٹرنیٹ خدمات فراہم کرنے والے یا اپنی تنظیم کے تکنیکی معاون سے رابطہ کریں اور انہیں اس سنجیدہ مسئلہ سے آگاہ کریں۔",
+       "sorbsreason": "{{SITENAME}} کے زیر استعمال DNSBL میں آپ کا آئی پی پتا اوپن پراکسی کے طور پر درج فہرست ہے۔",
+       "sorbs_create_account_reason": "{{SITENAME}} کے زیر استعمال DNSBL میں آپ کا آئی پی پتا اوپن پراکسی کے طور پر درج فہرست ہے۔\nآپ کھاتہ نہیں بنا سکتے۔",
+       "ipbblocked": "آپ دیگر صارفین پر پابندی لگا یا ہٹا نہیں سکتے کیونکہ خود آپ پر پابندی عائد کی گئی ہے۔",
+       "ipbnounblockself": "آپ کو اپنی ذات سے پابندی ہٹانے کی اجازت نہیں ہے۔",
+       "lockdb": "ڈیٹابیس مقفل کریں",
+       "unlockdb": "ڈیٹابیس غیر مقفل کریں",
+       "lockconfirm": "ہاں، میں واقعی ڈیٹابیس کو مقفل کرنا چاہتا ہوں۔",
+       "unlockconfirm": "ہاں، میں واقعی ڈیٹابیس کو غیر مقفل کرنا چاہتا ہوں۔",
+       "lockbtn": "ڈیٹابیس مقفل کریں",
+       "unlockbtn": "ڈیٹابیس غیر مقفل کریں",
+       "locknoconfirm": "آپ نے تصدیقی خانے پر نشان زد نہیں کیا ہے۔",
+       "lockdbsuccesssub": "ڈیٹابیس کو مقفل کر دیا گیا",
+       "unlockdbsuccesssub": "ڈیٹابیس کو غیر مقفل کر دیا گیا",
+       "lockdbsuccesstext": "ڈیٹابیس مقفل کر دیا گیا۔<br />\nنگہداشت مکمل ہو جانے کے بعد ڈیٹابیس کو [[Special:UnlockDB|غیر مقفل کرنا]] نہ بھولیں۔",
+       "unlockdbsuccesstext": "ڈیٹابیس کو غیر مقفل کر دیا گیا۔",
+       "databaselocked": "ڈیٹابیس پہلے سے مقفل ہے۔",
+       "databasenotlocked": "ڈیٹابیس مقفل نہیں ہے۔",
+       "lockedbyandtime": "(بذریعہ {{GENDER:$1|$1}} مورخہ $2 بوقت $3 بجے)",
        "move-page": "منتقلی $1",
        "move-page-legend": "منتقلئ صفحہ",
-       "movepagetext": "درج ذیل فارم کے ذریعہ صفحہ کو نیا نام دیا جاسکتا ہے، اس کے ساتھ صفحہ کا تاریخچہ بھی منتقل ہو جائے گا اور\nنئے عنوان کے جانب قدیم عنوان کو رجوع مکرر کردیا جائے گا۔\n\nاس بات کا یقین کر لیں کہ [[Special:DoubleRedirects|دوہرے]] یا [[Special:BrokenRedirects|شکستہ رجوع مکررات]] موجود نہ ہوں۔\n\nنیز آپ اس بات کو بھی یقینی بنانے کے ذمہ دار ہیں کہ روابط انہیں جگہوں سے مربوط رہیں جہاں ہونا چاہیے۔\n\nخیال رہے کہ یہ صفحہ منتقل '''نہیں''' ہوگا اگر نئے عنوان کے ساتھ صفحہ پہلے سے موجود ہو، ہاں اگر صفحہ خالی ہو اور اس کا گذشتہ ترمیمی تاریخچہ موجود نہ ہو تو منتقل کیا جا سکتا ہے۔\nاس کا مطلب ہے آپ سے اگر غلطی ہوجائے تو آپ صفحہ کو اسی جگہ لوٹا سکتے ہیں، تاہم موجود صفحہ پر برتحریر (overwrite) نہیں کرسکتے۔\n\n'''انتباہ!'''\nکسی اہم اور مقبول صفحہ کی منتقلی، غیرمتوقع اور پریشان کن بھی ہی ہوسکتی ہے اس لیے \nمنتقلی سے قبل براہ کرم یقین کرلیں کہ آپ اس کے منطقی نتائج سے باخبر ہیں۔",
-       "movepagetext-noredirectfixer": "درج ذیل ورقہ کے ذریعہ صفحہ کو نیا نام دیا جاسکتا ہے، اس کے ساتھ صفحہ کا تاریخچہ بھی منتقل ہوجائیگا۔\nنئے عنوان کے جانب قدیم عنوان کو رجوع مکرر کردیا جائیگا۔\n\nیقین کرلیں کہ [[Special:DoubleRedirects|مکرر]] یا [[Special:BrokenRedirects|شکستہ رجوع مکررات]] موجود نہیں ہیں۔\nآپ اس بات کو یقینی بنانے کے ذمہ دار ہیں کہ روابط انہیں جگہوں سے مربوط ہیں جن کو فرض کیا گیا ہے۔\n\nخیال رہے کہ یہ صفحہ منتقل '''نہیں''' ہوگا اگر نئے عنوان کے ساتھ صفحہ پہلے سے موجود ہو، سوائے اس کے کہ صفحہ خالی ہو اور اس کا گذشتہ ترمیمی تاریخچہ موجود نہ ہو۔\nاس کا مطلب ہے آپ سے اگر غلطی ہوجائے تو آپ صفحہ کو اسی جگہ لوٹا سکتے ہیں، تاہم موجود صفحہ پر برتحریر (overwrite) نہیں کرسکتے۔\n\n'''انتباہ!'''\nکسی اہم اور مقبول صفحہ کی منتقلی، غیرمتوقع اور پریشان کن بھی ہی ہوسکتی ہے اس لیۓ؛ \nمنتقلی سے قبل براہ کرم یقین کرلیجۓ کہ آپ اسکے منطقی نتائج سے باخبر ہیں۔",
+       "movepagetext": "درج ذیل فارم کے ذریعہ صفحہ کو نیا نام دیا جاسکتا ہے، اس کے ساتھ صفحہ کا تاریخچہ بھی منتقل ہو جائے گا اور نئے عنوان کے جانب قدیم عنوان کو رجوع مکرر کردیا جائے گا۔\n\nاس بات کا یقین کر لیں کہ [[Special:DoubleRedirects|دوہرے]] یا [[Special:BrokenRedirects|شکستہ رجوع مکررات]] موجود نہ ہوں۔\n\nنیز آپ اس بات کے بھی ذمہ دار ہیں کہ روابط انہیں جگہوں سے مربوط رہیں جہاں سے ابھی ہیں۔\n\nخیال رہے کہ اگر نئے عنوان سے کوئی صفحہ پہلے سے موجود ہو تو یہ صفحہ منتقل '''نہیں''' ہوگا، ہاں اگر صفحہ خالی ہو اور اس کا گذشتہ ترمیمی تاریخچہ موجود نہ ہو تو منتقل کیا جا سکتا ہے۔\nاس کا مطلب یہ ہے کہ آپ سے اگر غلطی ہوجائے تو آپ صفحہ کو اسی جگہ لوٹا سکتے ہیں، تاہم موجود صفحہ پر برتحریر (overwrite) نہیں کرسکتے۔\n\n<strong>اطلاع:</strong>\nکسی اہم اور مقبول صفحہ کی منتقلی، غیرمتوقع اور پریشان کن بھی ہی ہوسکتی ہے اس لیے منتقلی سے قبل یقین کرلیں کہ آپ اس کے منطقی نتائج سے باخبر ہیں۔",
+       "movepagetext-noredirectfixer": "درج ذیل فارم کے ذریعہ صفحہ کو نیا نام دیا جاسکتا ہے، اس کے ساتھ صفحہ کا تاریخچہ بھی منتقل ہو جائے گا اور نئے عنوان کے جانب قدیم عنوان کو رجوع مکرر کردیا جائے گا۔\n\nاس بات کا یقین کر لیں کہ [[Special:DoubleRedirects|دوہرے]] یا [[Special:BrokenRedirects|شکستہ رجوع مکررات]] موجود نہ ہوں۔\n\nنیز آپ اس بات کے بھی ذمہ دار ہیں کہ روابط انہیں جگہوں سے مربوط رہیں جہاں سے ابھی ہیں۔\n\nخیال رہے کہ اگر نئے عنوان سے کوئی صفحہ پہلے سے موجود ہو تو یہ صفحہ منتقل '''نہیں''' ہوگا، ہاں اگر صفحہ خالی ہو اور اس کا گذشتہ ترمیمی تاریخچہ موجود نہ ہو تو منتقل کیا جا سکتا ہے۔\nاس کا مطلب یہ ہے کہ آپ سے اگر غلطی ہوجائے تو آپ صفحہ کو اسی جگہ لوٹا سکتے ہیں، تاہم موجود صفحہ پر برتحریر (overwrite) نہیں کرسکتے۔\n\n<strong>اطلاع:</strong>\nکسی اہم اور مقبول صفحہ کی منتقلی، غیرمتوقع اور پریشان کن بھی ہی ہوسکتی ہے اس لیے منتقلی سے قبل یقین کرلیں کہ آپ اس کے منطقی نتائج سے باخبر ہیں۔",
+       "movepagetalktext": "اگر آپ اس خانے کو نشان زد کریں تو ملحقہ تبادلہ خیال صفحہ بھی نئے عنوان کی جانب خودکار طور پر منتقل ہو جائے گا اگر اس عنوان کے تحت پہلے سے کوئی تبادلۂ خیال صفحہ موجود نہ ہو۔\n\nاس صورت میں آپ کو دستی طور پر اس صفحہ کو منتقل ضم کرنا ہوگا۔",
+       "moveuserpage-warning": "<strong>انتباہ:</strong> آپ صارف صفحہ کو منتقل کر رہے ہیں۔ واضح رہے کہ اس منتقلی کے بعد صارف کا محض صفحہ منتقل ہوگا، اس کا صارف نام تبدیل <em>نہیں</em> ہوگا۔",
+       "movecategorypage-warning": "<strong>انتباہ:</strong> آپ زمرہ منتقل کر رہے ہیں۔ واضح رہے کہ منتقلی کے بعد اس زمرے میں موجود صفحات نئے زمرے میں منتقل <em>نہیں</em> ہونگے۔",
+       "movenologintext": "صفحہ کو منتقل کرنے کے لیے آپ کو اپنے کھاتے میں [[Special:UserLogin|داخل ہونا]] ضروری ہے۔",
+       "movenotallowed": "آپ کو صفحات منتقل کرنے کی اجازت نہیں ہے۔",
+       "movenotallowedfile": "آپ کو فائلیں منتقل کرنے کی اجازت نہیں ہے۔",
+       "cant-move-user-page": "آپ کو صارف صفحات منتقل کرنے کی اجازت نہیں ہے (ذیلی صفحات اس سے مستثنی ہیں)۔",
+       "cant-move-to-user-page": "کسی صفحہ کو کسی صارف صفحہ میں منتقل کرنے کی اجازت نہیں ہے (صارف کا ذیلی صفحہ اس سے مستثنی ہے)۔",
+       "cant-move-category-page": "آپ کو زمرہ جات منتقل کرنے کی اجازت نہیں ہے۔",
+       "cant-move-to-category-page": "کسی صفحہ کو زمرے میں منتقل کرنے کی اجازت نہیں ہے۔",
        "newtitle": "نـیــا عـنــوان:",
-       "move-watch": "صÙ\81Ø­Û\81 Ø²Û\8cر Ù\86ظر",
+       "move-watch": "اصÙ\84 Ø§Ù\88ر Û\81دÙ\81 ØµÙ\81Ø­Û\81 Ú©Ù\88 Ø²Û\8cر Ù\86ظر Ú©Ø±Û\8cÚº",
        "movepagebtn": "مـنـتـقـل",
        "pagemovedsub": "انتقال کامیاب",
        "movepage-moved": "<strong>\"$1\" کو \"$2\" کی جانب منتقل کر دیا گیا</strong>",
        "movepage-moved-redirect": "رجوع مکرر تخلیق کر دیا گیا۔",
        "movepage-moved-noredirect": "رجوع مکرر کو بننے سے روک دیا گیا ہے۔",
        "articleexists": "اس عنوان سے کوئی صفحہ پہلے ہی موجود ہے، یا آپکا منتخب کردہ نام مستعمل نہیں۔ براۓ مہربانی دوسرا نام منتخب کیجیۓ۔",
+       "cantmove-titleprotected": "آپ اس جگہ کسی صفحہ کو منتقل نہیں کر سکتے کیونکہ اس نئے عنوان کی تخلیق کو محفوظ کر دیا گیا ہے۔",
+       "movetalk": "ملحقہ تبادلۂ خیال صفحہ بھی منتقل کریں",
+       "move-subpages": "ذیلی صفحات منتقل کریں ($1 سے زیادہ)",
+       "move-talk-subpages": "تبادلۂ خیال صفحہ کے ذیلی صفحات منتقل کریں ($1 سے زیادہ)",
+       "movepage-page-exists": "صفحہ $1 پہلے سے موجود ہے اور خودکار طور پر برتحریر نہیں کیا جا سکتا۔",
+       "movepage-page-moved": "صفحہ $1 کو $2 کی جانب منتقل کر دیا گیا۔",
+       "movepage-page-unmoved": "صفحہ $1 کو $2 کی جانب منتقل نہیں کیا جا سکا۔",
+       "movepage-max-pages": "$1 کی زیادہ سے زیادہ تعداد تک {{PLURAL:$1|صفحہ منتقل کر دیا گیا ہے|صفحات منتقل کر دیے گئے ہیں}}، اب خودکار طور پر مزید صفحے منتقل نہیں کیے جا سکتے۔",
        "movelogpage": "نوشتۂ منتقلی",
+       "movelogpagetext": "ذیل میں ان تمام صفحات کی فہرست درج ہے جو منتقل کیے گئے ہیں۔",
+       "movesubpage": "{{PLURAL:$1|ذیلی صفحہ|ذیلی صفحات}}",
+       "movesubpagetext": "ذیل میں اس صفحہ {{PLURAL:$1|کا|کے}} $1 {{PLURAL:$1|ذیلی صفحہ موجود ہے|ذیلی صفحات موجود ہیں}}۔",
+       "movenosubpage": "اس صفحہ کے ذیلی صفحات موجود نہیں ہیں۔",
        "movereason": "وجہ:",
        "revertmove": "رجوع",
-       "delete_and_move_text": "==حذف شدگی لازم==\n\nمنتقلی کے سلسلے میں انتخاب کردہ مضمون \"[[:$1]]\" پہلے ہی موجود ہے۔ کیا آپ اسے حذف کرکے منتقلی کیلیۓ راستہ بنانا چاہتے ہیں؟",
+       "delete_and_move_text": "منتقلی کے سلسلے میں منتخب شدہ مضمون «[[:$1]]» پہلے سے موجود ہے۔ کیا آپ اسے حذف کرکے منتقلی کے لیے راستہ بنانا چاہتے ہیں؟",
        "delete_and_move_confirm": "ہاں، صفحہ حذف کر دیا جائے",
        "delete_and_move_reason": "[[$1]] سے منتقلی کے سلسلے میں حذف",
+       "selfmove": "اصل اور ہدف صفحے کے عناوین یکساں ہیں؛\nصفحہ کو اسی جگہ پر منتقل نہیں کیا جا سکتا۔",
+       "immobile-source-namespace": "«$1» نام فضا میں صفحات منتقل نہیں کیے جا سکتے۔",
+       "immobile-target-namespace": "«$1» نام فضا میں صفحات منتقل نہیں کیے جا سکتے۔",
+       "immobile-target-namespace-iw": "صفحہ منتقل کرنے کے لیے بین الویکی ربط درست ہدف نہیں ہے۔",
+       "immobile-source-page": "اس صفحہ کو منتقل نہیں کیا جا سکتا۔",
+       "immobile-target-page": "اس ہدف عنوان کی جانب منتقل نہیں کیا جا سکتا۔",
+       "bad-target-model": "مطلوبہ منزل میں مواد کا مختلف ماڈل زیر استعمال ہے۔ لہذا $1 سے $2 میں تبدیلی نہیں ہو سکی۔",
+       "imagenocrossnamespace": "فائل کو غیر فائل نام فضا میں منتقل نہیں کیا جا سکتا۔",
+       "nonfile-cannot-move-to-file": "غیر فائل کو فائل نام فضا میں منتقل نہیں کیا جا سکتا۔",
+       "imagetypemismatch": "نئی فائل کی توسیع اس کی نوعیت کے مطابق نہیں ہے۔",
+       "imageinvalidfilename": "ہدف فائل کا نام نادرست ہے۔",
+       "fix-double-redirects": "اصل عنوان کی جانب موجود تمام رجوع مکررات کو بھی تازہ کریں",
+       "move-leave-redirect": "پیچھے رجوع مکرر بنائیں",
        "protectedpagemovewarning": "<strong>انتباہ:</strong> اس صفحہ کو محفوظ کر دیا گیا ہے اور اب محض منتظمین ہی اسے منتقل کر سکتے ہیں۔\nحوالہ کے لیے نوشتہ کا جدید اندراج ذیل میں درج ہے:",
+       "semiprotectedpagemovewarning": "<strong>اطلاع:</strong> یہ صفحہ نیم محفوظ ہے اور اسے محض مندرج صارفین ہی منتقل کر سکتے ہیں۔\nذیل میں حوالہ کے لیے نوشتہ کا تازہ ترین اندراج موجود ہے:",
+       "move-over-sharedrepo": "[[:$1]] ایک مشترکہ ذخیرے میں موجود ہے۔ چنانچہ اس عنوان کی جانب کسی فائل کو منتقل کرنے پر مشترکہ فائل منسوخ ہو جائے گی۔",
+       "file-exists-sharedrepo": "فائل کا منتخب کردہ نام ایک مشترکہ ذخیرے میں پہلے ہی سے زیر استعمال ہے۔\nبراہ کرم کوئی دوسرا نام درج کریں۔",
        "export": "برآمد صفحات",
+       "exportall": "تمام صفحات برآمد کریں",
+       "exportcuronly": "مکمل تاریخچہ کی بجائے محض موجودہ نسخہ کو شامل کریں",
+       "exportnohistory": "----\n<strong>اطلاع:</strong> اس فارم کے ذریعہ صفحات کے مکمل تاریخچہ کی برآمد کو بوجوہ غیر فعال کر دیا گیا ہے۔",
+       "exportlistauthors": "ہر صفحہ کے مشارکت کنندگان کی مکمل فہرست شامل کریں",
+       "export-submit": "برآمد کریں",
+       "export-addcattext": "اس زمرہ سے صفحات شامل کریں:",
+       "export-addcat": "شامل کریں",
+       "export-addnstext": "اس نام فضا سے صفحات شامل کریں:",
+       "export-addns": "شامل کریں",
+       "export-download": "فائل کے طور پر محفوظ کریں",
+       "export-templates": "سانچے شامل کریں",
+       "export-pagelinks": "مربوط صفحات کو اس گہرائی تک شامل کریں:",
+       "export-manual": "صفحات کو دستی طور پر شامل کریں:",
        "allmessages": "نظامی پیغامات",
        "allmessagesname": "نام",
        "allmessagesdefault": "طے شدہ متن",
        "allmessagescurrent": "موجودہ متن",
-       "allmessagestext": "یہ میڈیاویکی: جاۓ نام میں دستیاب نظامی پیغامات کی فہرست ہے۔",
+       "allmessagestext": "ذیل میں میڈیاویکی نام فضا میں دستیاب نظامی پیغامات کی فہرست موجود ہے۔\nاگر آپ میڈیاویکی کا ترجمہ کرنا چاہتے ہیں تو [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation میڈیاویکی مقامیت کاری] اور [https://translatewiki.net translatewiki.net] ملاحظہ فرمائیں۔",
+       "allmessagesnotsupportedDB": "اس صفحہ کو استعمال نہیں کیا جا سکتا کیونکہ <strong>$wgUseDatabaseMessages</strong> کو غیر فعال کر دیا گیا ہے۔",
+       "allmessages-filter-legend": "مقطار",
        "allmessages-filter": "تلاش بلحاظ:",
+       "allmessages-filter-unmodified": "غیر تبدیل شدہ",
        "allmessages-filter-all": "تمام",
        "allmessages-filter-modified": "تبدیل شدہ",
        "allmessages-prefix": "تلاش بلحاظ سابقہ:",
        "allmessages-filter-submit": "ٹھیک",
        "allmessages-filter-translate": "ترجمہ",
        "thumbnail-more": "چوڑا کریں",
+       "filemissing": "فائل غیر موجود ہے",
+       "thumbnail_error": "تھمب نیل بنانے کے دوران میں نقص: $1",
+       "thumbnail_error_remote": "$1 کی جانب سے پیغام نقص:\n$2",
+       "djvu_page_error": "DjVu صفحہ رینج سے باہر ہے",
+       "djvu_no_xml": "DjVu فائل کے لیے XML حاصل نہیں کیا جا سکتا",
+       "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-interwiki-sourcewiki": "اصل ویکی:",
+       "import-interwiki-sourcepage": "اصل صفحہ:",
+       "import-interwiki-history": "اس صفحہ کے تاریخچے کے تمام نسخوں کو نقل کریں",
+       "import-interwiki-templates": "تمام سانچے شامل کریں",
+       "import-interwiki-submit": "درآمد کریں",
+       "import-mapping-default": "طے شدہ جگہوں پر درآمد کریں",
+       "import-mapping-namespace": "کسی نام فضا میں درآمد کریں:",
+       "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-anontalk": "اس آئی پی پتے کی ترامیم سے متعلق گفتگو",
        "tooltip-pt-preferences": "آپ کی ترجیحات",
        "tooltip-pt-watchlist": "اُن صفحات کی فہرست جن کی تبدیلیاں آپ کی زیرِنظر ہیں",
        "tooltip-pt-mycontris": "آپ کی شراکتوں کی فہرست",
-       "tooltip-pt-login": "آپ کیلئے داخلِ نوشتہ ہونا اچھا ہے؛ تاہم، یہ ضروری نہیں",
-       "tooltip-pt-logout": "خارجِ نوشتہ ہوجائیں",
-       "tooltip-pt-createaccount": "آپ کو مدعو کیا جاتا ہے کہ کھاتہ بنائیں۔تاہم کھاتہ بنانا لازم نہیں۔",
-       "tooltip-ca-talk": "مضمون بارے تبادلۂ خیال",
-       "tooltip-ca-edit": "اس صفحے پر ترمیم کریں",
-       "tooltip-ca-addsection": "نیا قطعہ شروع کیجئے",
+       "tooltip-pt-anoncontribs": "اس آئی پی پتے سے انجام دی جانے والی تمام ترامیم کی فہرست",
+       "tooltip-pt-login": "کھاتے میں داخل ہونے کی سفارش کی جاتی ہے؛ تاہم یہ ضروری نہیں",
+       "tooltip-pt-logout": "خارج ہوجائیں",
+       "tooltip-pt-createaccount": "کھاتہ بنانے یا اس میں داخل ہونے کی سفارش کی جاتی ہے؛ تاہم یہ ضروری نہیں",
+       "tooltip-ca-talk": "مضمون کے متعلق گفتگو کریں",
+       "tooltip-ca-edit": "اس صفحہ میں ترمیم کریں",
+       "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-t-contributions": "نئی تدوین →",
-       "tooltip-t-emailuser": "اِس صارف کو برقی خط ارسال کریں",
-       "tooltip-t-upload": "زبراثقالِ ملفات",
-       "tooltip-t-specialpages": "تمام خاص صفحات کی فہرست",
-       "tooltip-t-print": "اِس صفحہ کا قابلِ طبعہ نسخہ",
-       "tooltip-t-permalink": "صفحہ کے موجودہ نظرثانی کا مستقل ربط",
-       "tooltip-ca-nstab-main": "صفحۂ مضمون دیکھئے",
-       "tooltip-ca-nstab-user": "اِس صارف کے مساہمات کی فہرست دیکھئے",
-       "tooltip-ca-nstab-special": "ہم معذرت خواہ ہیں! آپ اس [[ویکیپیڈیا:نام فضا|نام فضا]] میں ترمیم کا اختیار نہیں رکھتے۔",
+       "tooltip-feed-atom": "اِس صفحہ کا اٹوم فیڈ",
+       "tooltip-t-contributions": "{{GENDER:$1|اس صارف}} کی شراکتوں کی فہرست",
+       "tooltip-t-emailuser": "{{GENDER:$1|اس صارف}} کو برقی خط بھیجیں",
+       "tooltip-t-info": "اس صفحہ کے بارے میں مزید معلومات",
+       "tooltip-t-upload": "فائلیں اپلوڈ کریں",
+       "tooltip-t-specialpages": "جملہ خصوصی صفحات کی فہرست",
+       "tooltip-t-print": "اِس صفحہ کا قابلِ طبع نسخہ",
+       "tooltip-t-permalink": "صفحہ کے اس نسخہ کا مستقل ربط",
+       "tooltip-ca-nstab-main": "مواد پر مشتمل صفحہ دیکھیں",
+       "tooltip-ca-nstab-user": "صارف صفحہ دیکھیں",
+       "tooltip-ca-nstab-media": "میڈیا کا صفحہ دیکھیں",
+       "tooltip-ca-nstab-special": "یہ ایک خصوصی صفحہ ہے، اس میں ترمیم نہیں کی جا سکتی",
        "tooltip-ca-nstab-project": "صفحۂ صارف دیکھئے",
-       "tooltip-ca-nstab-image": "صفحۂ ملف دیکھئے",
-       "tooltip-ca-nstab-template": "سانچہ دیکھئے",
-       "tooltip-ca-nstab-category": "زمرہ‌جاتی صفحہ دیکھئے",
+       "tooltip-ca-nstab-image": "فائل کا صفحہ دیکھیں",
+       "tooltip-ca-nstab-mediawiki": "نظامی پیغام دیکھیں",
+       "tooltip-ca-nstab-template": "سانچہ دیکھیں",
+       "tooltip-ca-nstab-help": "صفحۂ معاونت دیکھیں",
+       "tooltip-ca-nstab-category": "زمرہ‌ دیکھیں",
        "tooltip-minoredit": "اِس تدوین کو بطورِ معمولی ترمیم نشانزد کیجئے",
-       "tooltip-save": "تبدیلیاں محفوظ کیجئے",
-       "tooltip-preview": "برائے مہربانی! محفوظ کرنے سے پہلے تبدیلیوں کا پیش منظر دیکھيے",
+       "tooltip-save": "تبدیلیاں محفوظ کریں",
+       "tooltip-publish": "اپنی تبدیلیاں شائع کریں",
+       "tooltip-preview": "براہ مہربانی! محفوظ کرنے سے پہلے تبدیلیوں کی نمائش دیکھ لیں",
        "tooltip-diff": "دیکھئے کہ اپنے متن میں کیا تبدیلیاں کیں",
        "tooltip-compareselectedversions": "اِس صفحہ کی دو منتخب نظرثانیوں میں فرق دیکھئے",
        "tooltip-watch": "اِس صفحہ کو اپنی زیرِنظرفہرست میں شامل کریں",
+       "tooltip-watchlistedit-normal-submit": "عناوین حذف کریں",
+       "tooltip-watchlistedit-raw-submit": "زیرنظر فہرست کی تجدید کریں",
+       "tooltip-recreate": "حذف شدہ صفحہ ہونے کے باوجود اسے دوبارہ تخلیق کریں",
+       "tooltip-upload": "اپلوڈ کریں",
        "tooltip-rollback": "پچھلے صارف کی کی گئی اِس صفحے پر استرجع شدہ ترامیم کو ایک کلِک میں واپس کریں",
        "tooltip-undo": "''استرجع'' اس ترمیم کو پچھلی ترمیم کے جانب واپس کردیگا اور نمائشی انداز میں خانہ ترمیم کھول دے گا۔ آپ مختصراً سبب بیان کرنے کے بھی مجاز ہونگے۔",
+       "tooltip-preferences-save": "ترجیحات محفوظ کریں",
        "tooltip-summary": "مختصر خلاصہ درج کریں",
        "common.css": "body,\ntextarea {\n    font-family: Amiri;\n}",
-       "anonymous": "{{SITENAME}} گمنام صارف",
+       "anonymous": "{{SITENAME}} {{PLURAL:$1|کا|کے}} گمنام {{PLURAL:$1|صارف|صارفین}}",
+       "siteuser": "{{SITENAME}} $1 صارف",
+       "anonuser": "{{SITENAME}} کا گمنام صارف $1",
+       "lastmodifiedatby": "مورخہ $1 کو $2 بجے $3 نے اس صفحہ میں آخری بار تبدیلی کی۔",
+       "othercontribs": "$1 کے کام کے مطابق۔",
        "others": "دیگر",
-       "pageinfo-visiting-watchers": "تعداد ناظرین جنہوں نے حالیہ ترامیم کا مشاہدہ کیا",
+       "siteusers": "{{SITENAME}} {{PLURAL:$2|کا|کے}} {{PLURAL:$2|{{GENDER:$1|صارف}}|صارفین}} $1",
+       "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-header-edits": "تاریخچۂ ترمیم",
+       "pageinfo-header-restrictions": "صفحہ کی حفاظت",
+       "pageinfo-header-properties": "صفحہ کی خاصیتیں",
+       "pageinfo-display-title": "عنوان",
+       "pageinfo-default-sort": "کلید برائے ابتدائی ترتیب",
+       "pageinfo-length": "صفحہ کا حجم (بائٹ میں)",
+       "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-firstuser": "صفحہ ساز",
+       "pageinfo-firsttime": "صفحہ سازی کی تاریخ",
+       "pageinfo-lastuser": "آخری ترمیم کنندہ",
+       "pageinfo-lasttime": "آخری ترمیم کی تاریخ",
+       "pageinfo-edits": "ترامیم کی مجموعی تعداد",
+       "pageinfo-authors": "مختلف مصنفین کی مجموعی تعداد",
+       "pageinfo-recent-edits": "حالیہ ترامیم کی تعداد (گزشتہ $1 میں)",
+       "pageinfo-recent-authors": "مختلف مصنفین کی حالیہ تعداد",
+       "pageinfo-magic-words": "جادوئی {{PLURAL:$1|لفظ|الفاظ}} ($1)",
        "pageinfo-hidden-categories": "پوشیدہ {{PLURAL:$1|زمرہ|زمرہ جات}} ($1)",
+       "pageinfo-templates": "زیر استعمال {{PLURAL:$1|سانچہ|سانچے}} ($1)",
+       "pageinfo-transclusions": "($1) میں زیر استعمال {{PLURAL:$1|صفحہ|صفحات}}",
        "pageinfo-toolboxlink": "معلومات صفحہ",
+       "pageinfo-redirectsto": "رجوع مکررات برائے",
+       "pageinfo-redirectsto-info": "معلومات",
+       "pageinfo-contentpage": "شمار بطور صفحہ",
+       "pageinfo-contentpage-yes": "ہاں",
+       "pageinfo-protect-cascading": "آبشاری حفاظت کا ماخذ",
+       "pageinfo-protect-cascading-yes": "ہاں",
+       "pageinfo-protect-cascading-from": "آبشاری حفاظت از",
        "pageinfo-category-info": "زمرے کی معلومات",
+       "pageinfo-category-total": "اراکین کی مجموعی تعداد",
        "pageinfo-category-pages": "تعداد صفحات",
        "pageinfo-category-subcats": "تعداد ذیلی زمرہ جات",
-       "pageinfo-category-files": "تعداد املاف",
+       "pageinfo-category-files": "فائلوں کی تعداد",
+       "markaspatrolleddiff": "بطور مراجعت شدہ نشان زد کریں",
        "markaspatrolledtext": "اس صفحہ کو بطور مراجعت شدہ نشان زد کریں",
+       "markaspatrolledtext-file": "فائل کے اس نسخے کو مراجعت شدہ نشان زد کریں",
+       "markedaspatrolled": "مراجعت شدہ نشان زد کر دیا گیا",
+       "markedaspatrolledtext": "[[:$1]] کی منتخب ترمیم کو مراجعت شدہ نشان زد کر دیا گیا۔",
+       "rcpatroldisabled": "حالیہ تبدیلیوں کی مراجعت غیر فعال ہے",
+       "rcpatroldisabledtext": "فی الحال حالیہ تبدیلیوں کی مراجعت کی سہولت غیر فعال ہے۔",
+       "markedaspatrollederror": "مراجعت شدہ نشان زد نہیں کیا جا سکا",
+       "markedaspatrollederrortext": "مراجعت شدہ نشان زد کرنے کے لیے کسی ترمیم کو منتخب کرنا ضروری ہے۔",
+       "markedaspatrollederror-noautopatrol": "آپ کو اپنی ذاتی تبدیلیاں مراجعت شدہ نشان زد کرنے کی اجازت نہیں ہے۔",
+       "markedaspatrollednotify": "$1 کی اس تبدیلی کو نشان زد کر دیا گیا۔",
        "markedaspatrollederrornotify": "بطور مراجعت نشان زد نہیں کیا جا سکا۔",
+       "patrol-log-page": "نوشتہ مراجعت",
+       "patrol-log-header": "ذیل میں مراجعت شدہ ترامیم کا نوشتہ ہے۔",
+       "log-show-hide-patrol": "$1 نوشتہ مراجعت",
+       "log-show-hide-tag": "$1 نوشتہ ٹیگ",
        "deletedrevision": "حذف شدہ پرانی ترمیم $1۔",
-       "previousdiff": "← پُرانی تدوین",
+       "filedeleteerror-short": "فائل حذف کاری میں نقص: $1",
+       "filedeleteerror-long": "فائل حذف کرنے کے دوران میں نقص واقع ہوا:\n\n$1",
+       "filedelete-missing": "فائل «$1» کو حذف نہیں کیا جا سکتا کیونکہ یہ موجود نہیں ہے۔",
+       "filedelete-old-unregistered": "فائل «$1» کا منتخب نسخہ ڈیٹابیس میں موجود نہیں ہے۔",
+       "filedelete-current-unregistered": "«$1» کے عنوان سے کوئی فائل ڈیٹابیس میں موجود نہیں ہے۔",
+       "filedelete-archive-read-only": "$1 کی وثق ڈائرکٹری میں ویب سرور نہیں لکھ پا رہا ہے۔",
+       "previousdiff": "→ پرانی ترمیم",
        "nextdiff": "صفحہ کا نام:",
+       "mediawarning": "<strong>انتباہ:</strong> شاید اس نوع کی فائل میں نقصان دہ کوڈ موجود ہے۔\nممکن ہے اسے چلانے پر آپ کا سسٹم مشکوک ہاتھوں میں چلا جائے۔",
        "imagemaxsize": "تصویر کی جسامت کی حد:<br /><em>(فائل کے توضیحی صفحات کے لیے)</em>",
        "thumbsize": "تھمب نیل کی جسامت:",
-       "file-info-size": "\n$1 × $2 عکصر (پکسلز)، حجم ملف: $3، MIME قسم: $4",
-       "file-nohires": "اس سے بڑی تصمیم دستیاب نہیں۔",
-       "show-big-image": "اصل ملف",
+       "widthheightpage": "$1×$2، $3 {{PLURAL:$3|صفحہ|صفحات}}",
+       "file-info": "فائل کا حجم: $1، MIME قسم: $2",
+       "file-info-size": "\n$1 × $2 پکسل، فائل کا حجم: $3، MIME قسم: $4",
+       "file-info-size-pages": "$1 × $2 پکسل، فائل کا حجم: $3، MIME قسم: $4، $5 {{PLURAL:$5|صفحہ|صفحات}}",
+       "file-nohires": "اس سے زیادہ ریزولیوشن دستیاب نہیں۔",
+       "svg-long-desc": "ایس وی جی فائل، ابعاد $1 × $2 پکسل، فائل کا حجم: $3",
+       "svg-long-desc-animated": "متحرک ایس وی جی فائل، ابعاد $1 × $2 پکسل، فائل کا حجم: $3",
+       "svg-long-error": "نادرست ایس وی جی فائل: $1",
+       "show-big-image": "اصل فائل",
        "show-big-image-preview": "اس نمائش کا حجم:$1",
-       "show-big-image-other": "دیگر {{PLURAL:$2|تجویز|تجویزیں}}: $1۔",
-       "show-big-image-size": "$1 × $2 pixels",
+       "show-big-image-preview-differ": "اس $2 فائل کی $3 نمائش کا حجم: $1",
+       "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": "چکردار",
+       "file-info-png-repeat": "$1 {{PLURAL:$1|مرتبہ}} دکھائی گئی",
+       "file-info-png-frames": "$1 {{PLURAL:$1|چوکھٹا|چوکھٹے}}",
+       "file-no-thumb-animation": "<strong>اطلاع: تکنیکی پابندیوں کی وجہ سے اس فائل کے تھمب نیل غیر متحرک ہونگے۔</strong>",
+       "file-no-thumb-animation-gif": "<strong>اطلاع: تکنیکی پابندیوں کی وجہ سے اس طرح کی زیادہ ریزولیوشن والی جی آئی ایف تصویروں کے تھمب نیل غیر متحرک ہونگے۔</strong>",
        "newimages": "نئی فائلوں کی گیلری",
+       "imagelisttext": "ذیل میں $2 <strong>$1</strong> {{PLURAL:$1|فائل|فائلوں}} کی فہرست موجود ہے۔",
+       "newimages-summary": "اس خصوصی صفحہ میں تازہ ترین اپلوڈ شدہ فائلوں کی فہرست موجود ہے۔",
+       "newimages-legend": "مقطار",
+       "newimages-label": "فائل کا نام (یا اس کا جزو):",
+       "newimages-showbots": "روبہ جات کے ذریعہ اپلوڈ کردہ فائلیں دکھائیں",
+       "newimages-hidepatrolled": "مراجعت شدہ اپلوڈ چھپائیں",
+       "noimages": "دیکھنے کیلئے کچھ نہیں ہے۔",
        "ilsubmit": "تلاش",
-       "bydate": "بالحاظ تاریخ",
+       "bydate": "بلحاظ تاریخ",
+       "sp-newimages-showfrom": "$2، $1 کے بعد اپلوڈ کی جانے والی فائلیں دکھائیں",
+       "seconds": "{{PLURAL:$1|$1 سیکنڈ}}",
+       "minutes": "{{PLURAL:$1|$1 منٹ}}",
+       "hours": "{{PLURAL:$1|گھنٹہ|گھنٹے}}",
+       "days": "{{PLURAL:$1|دن}}",
        "weeks": "{{PLURAL:$1|$1ہفتہ| $1  ہفتے}}",
+       "months": "{{PLURAL:$1|مہینہ|مہینے}}",
+       "years": "{{PLURAL:$1|$1 سال|$1 برس}}",
        "ago": "$1 قبل",
-       "minutes-ago": "$1 {{PLURAL:$1|منٹ|منٹ}} قبل",
-       "seconds-ago": "$1 {{PLURAL:$1|سیکنڈ|سیکنڈ}} قبل",
-       "bad_image_list": "شکلبند درج ذیل ہے:\n\nصرف فہرستی عناصر (* سے شروع ہونے والی لکیری) شامل کی جاتی ہیں۔\nکسی لکیر میں پہلا ربط کوئی خراب ملف کا ہونا چاہئے۔\nاُسی لکیر میں باقی آنے والے ربط کو مستثنیٰ قرار دیا جاتا ہے، مثلاً صفحات جہاں ملف لکیر کے وسط میں آسکتا ہے۔",
+       "just-now": "بس ابھی",
+       "hours-ago": "$1 {{PLURAL:$1|گھنٹہ|گھنٹے}} قبل",
+       "minutes-ago": "$1 {{PLURAL:$1|منٹ}} قبل",
+       "seconds-ago": "$1 {{PLURAL:$1|سیکنڈ}} قبل",
+       "monday-at": "پیر بوقت $1",
+       "tuesday-at": "منگل بوقت $1",
+       "wednesday-at": "بدھ بوقت $1",
+       "thursday-at": "جمعرات بوقت $1",
+       "friday-at": "جمعہ بوقت $1",
+       "saturday-at": "سنیچر بوقت $1",
+       "sunday-at": "اتوار بوقت $1",
+       "yesterday-at": "گزشتہ کل بوقت $1",
+       "bad_image_list": "فارمیٹ درج ذیل ہے:\n\nمحض فہرست میں موجود مندرجات (* سے شروع ہونے والی سطریں) شامل سمجھے جائیں گے۔\nسطر میں پہلا ربط کسی خراب فائل کا ہونا لازمی ہے۔\nاُسی سطر کے بقیہ روابط کو مستثنیٰ سمجھا جائے گا، مثلاً وہ صفحات جن میں فائل سطر میں موجود ہوں۔",
        "metadata": "میٹا ڈیٹا",
-       "metadata-help": "اِس ملف میں اِضافی معلومات شامل ہیں، جو کہ شاید اُس رقمی کیمرے یا سکینر سے آئے ہیں جس کے ذریعے یہ ملف بنائی گئی تھی۔\nاگر ملف اپنی اصل حالت میں نہیں رہی ہے تو کچھ تفاصیل ترمیم شدہ ملف کی مکمل طور پر عکاسی نہیں کرپائیں گے۔",
-       "metadata-collapse": "طویل تفاصیل چھپاؤ",
+       "metadata-help": "اِس فائل میں اِضافی معلومات شامل ہیں، جو شاید اُس ڈیجیٹل کیمرے یا سکینر سے آئی ہیں جس کے ذریعے یہ فائل بنائی گئی تھی۔\nاگر فائل اپنی اصل حالت میں نہ ہو تو کچھ معلومات ترمیم شدہ فائل کی مکمل طور پر عکاسی نہیں کر پائیں گی۔",
+       "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-orientation": "پیشکش",
-       "exif-xresolution": "چھوڑاوی دکھاوت",
-       "exif-yresolution": "لمباوی دکھاوت",
-       "exif-datetime": "ملف کے تبدیلی کا تاریخ او وقت",
-       "exif-make": "کیمرے کا صانع",
+       "exif-imagewidth": "چوڑائی",
+       "exif-imagelength": "لمبائی",
+       "exif-bitspersample": "بٹ فی جزو",
+       "exif-compression": "نظام کمپریشن",
+       "exif-photometricinterpretation": "پکسل کی ترکیب",
+       "exif-orientation": "جہت",
+       "exif-samplesperpixel": "اجزا کی تعداد",
+       "exif-planarconfiguration": "ڈیٹا کی ترتیب",
+       "exif-ycbcrsubsampling": "Y کا C سے ذیلی نمونہ ساز تناسب",
+       "exif-ycbcrpositioning": "Y اور C کی جگہ",
+       "exif-xresolution": "افقی ریزولوشن",
+       "exif-yresolution": "عمودی ریزولیشن",
+       "exif-stripoffsets": "تصویر کے دیٹا کا محل وقوع",
+       "exif-rowsperstrip": "فی پٹی تعداد قطار",
+       "exif-stripbytecounts": "بائٹ فی کمپریس شدہ پٹی",
+       "exif-jpeginterchangeformat": "JPEG SOI کا آفسیٹ",
+       "exif-jpeginterchangeformatlength": "JPEG ڈیٹا کے بائٹ",
+       "exif-whitepoint": "سفید نقطہ کے رنگ",
+       "exif-primarychromaticities": "اساسیات کے رنگ",
+       "exif-ycbcrcoefficients": "فضائے رنگ کی میٹرکس تبدیلی کی مقداریں",
+       "exif-referenceblackwhite": "سیاہ و سفید جوالے کی قدروں کی جوڑی",
+       "exif-datetime": "فائل کی تبدیلی کی تاریخ اور وقت",
+       "exif-imagedescription": "تصویر کا عنوان",
+       "exif-make": "کیمرہ ساز کمپنی",
        "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-exposureprogram": "نمائش کا پروگرام",
+       "exif-spectralsensitivity": "طیفی حساسیت",
+       "exif-isospeedratings": "آئیسو کی رفتار کی درجہ بندی",
+       "exif-shutterspeedvalue": "APEX شٹر کی رفتار",
+       "exif-aperturevalue": "APEX اپرچر",
+       "exif-brightnessvalue": "APEX کی چمک",
+       "exif-exposurebiasvalue": "APEX نمائش کا نقص",
+       "exif-maxaperturevalue": "زیادہ سے زیادہ لینڈ اپرچر",
+       "exif-subjectdistance": "شئی کا فاصلہ",
+       "exif-meteringmode": "پیمائش کاری کی حالت",
+       "exif-lightsource": "روشنی کا ماخذ",
+       "exif-flash": "فلیش",
+       "exif-focallength": "عدسہ کی ماسکی لمبائی",
+       "exif-subjectarea": "شئی کی مساحت",
+       "exif-flashenergy": "فلیش توانائی",
+       "exif-focalplanexresolution": "X کی تحلیل کا درجہ ماسکہ",
+       "exif-focalplaneyresolution": "Y کی تحلیل کا درجہ ماسکہ",
+       "exif-focalplaneresolutionunit": "درجہ ماسکہ کی تحلیل کی اکائی",
+       "exif-subjectlocation": "شئی کا محل وقوع",
+       "exif-exposureindex": "نمائش کا اشاریہ",
+       "exif-sensingmethod": "سینسنگ کا طریقہ",
+       "exif-filesource": "فائل کا ماخذ",
+       "exif-scenetype": "منظر کی نوعیت",
+       "exif-customrendered": "تصویر کی شخصی پروسیسینگ",
+       "exif-exposuremode": "نمائش کی حالت",
+       "exif-whitebalance": "وائٹ بیلنس",
+       "exif-digitalzoomratio": "ڈیجیٹل زوم کا تناسب",
+       "exif-focallengthin35mmfilm": "35 ایم ایم کی فلم میں ماسکی لمبائی",
+       "exif-scenecapturetype": "منظر کے گرفت کی نوعیت",
+       "exif-gaincontrol": "منظر کنٹرول",
+       "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-gpsaltitude": "ارتفاع",
+       "exif-gpstimestamp": "جی پی ایس وقت (جوہری گھڑی)",
+       "exif-gpssatellites": "پیمائش کے لیے مستعمل مصنوعی سیارے",
+       "exif-gpsstatus": "ریسیور کی صورت حال",
+       "exif-gpsmeasuremode": "حالت پیمائش",
+       "exif-gpsdop": "پیمائش کی درستی",
+       "exif-gpsspeedref": "رفتار کی اکائی",
+       "exif-gpsspeed": "جی پی ایس ریسیور کی رفتار",
+       "exif-gpstrackref": "حرکت کی سمت کا حوالہ",
+       "exif-gpstrack": "حرکت کی سمت",
+       "exif-gpsimgdirectionref": "تصویر کی سمت کا حوالہ",
+       "exif-gpsimgdirection": "تصویر کی سمت",
+       "exif-gpsmapdatum": "زیر استعمال جیو ڈیٹک سروے",
+       "exif-gpsdestlatituderef": "منزل کے عرض البلد کا حوالہ",
+       "exif-gpsdestlatitude": "منزل کا عرض البلد",
+       "exif-gpsdestlongituderef": "منزل کے طول البلد کا حوالہ",
+       "exif-gpsdestlongitude": "منزل کا طول البلد",
+       "exif-gpsdestbearingref": "منزل کی سمت کا حوالہ",
+       "exif-gpsdestbearing": "منزل کی سمت",
+       "exif-gpsdestdistanceref": "منزل کی مسافت کا حوالہ",
+       "exif-gpsdestdistance": "منزل کی مسافت",
+       "exif-gpsprocessingmethod": "جی پی ایس پراسیسنگ کے طریقہ کا نام",
+       "exif-gpsareainformation": "جی پی ایس کے علاقے کا نام",
+       "exif-gpsdatestamp": "جی پی ایس کی تاریخ",
+       "exif-gpsdifferential": "جی پی ایس کی تفریقی درستی",
+       "exif-jpegfilecomment": "JPEG فائل کا تبصرہ",
+       "exif-keywords": "کلیدی الفاظ",
+       "exif-worldregioncreated": "دنیا کا وہ خطہ جہاں یہ تصویر کھینچی گئی",
+       "exif-countrycreated": "وہ ملک جہاں یہ تصویر کھینچی گئی",
+       "exif-countrycodecreated": "اس ملک کا کوڈ جہاں یہ تصویر کھینچی گئی",
+       "exif-provinceorstatecreated": "وہ ریاست یا صوبہ جہاں یہ تصویر کھینچی گئی",
+       "exif-citycreated": "وہ شہر جہاں یہ تصویر کھینچی گئی",
+       "exif-sublocationcreated": "شہر کا وہ مقام جہاں یہ تصویر کھینچی گئی",
+       "exif-worldregiondest": "دنیا کا دکھایا گیا خطہ",
+       "exif-countrydest": "دکھایا گیا ملک",
+       "exif-countrycodedest": "دکھائے گئے ملک کا کوڈ",
+       "exif-provinceorstatedest": "دکھایا گیا صوبہ یا ریاست",
+       "exif-citydest": "دکھایا گیا شہر",
+       "exif-sublocationdest": "شہر کا دکھایا گیا مقام",
+       "exif-objectname": "مختصر عنوان",
+       "exif-specialinstructions": "خصوصی ہدایات",
+       "exif-headline": "سرخی",
+       "exif-credit": "کریڈٹ/مہیا کار",
+       "exif-source": "ماخذ",
+       "exif-editstatus": "تصویر کی ادارتی کیفیت",
+       "exif-urgency": "فوری طور پر",
+       "exif-fixtureidentifier": "مستقل شئی کا نام",
+       "exif-locationdest": "دکھایا گیا مقام",
+       "exif-locationdestcode": "دکھائے گئے مقام کا کوڈ",
+       "exif-objectcycle": "اس میڈیا کا مقصود دن کا وقت",
+       "exif-contact": "رابطہ کی معلومات",
        "exif-writer": "مصنف",
        "exif-languagecode": "زبان",
+       "exif-iimversion": "IIM نسخہ",
        "exif-iimcategory": "زمرہ",
+       "exif-iimsupplementalcategory": "تکمیلی زمرہ جات",
+       "exif-datetimeexpires": "اس تاریخ کے بعد استعمال نہ کریں",
+       "exif-datetimereleased": "جاری کردہ بتاریخ",
+       "exif-originaltransmissionref": "منتقلی کے اصل محل وقوع کا کوڈ",
+       "exif-identifier": "شناخت کنندہ",
+       "exif-lens": "زیر استعمال عدسے",
+       "exif-serialnumber": "کیمرے کا نمبر شمار",
+       "exif-cameraownername": "کیمرے کا مالک",
+       "exif-label": "لیبل",
+       "exif-datetimemetadata": "میٹاڈیٹا میں تبدیلی کی آخری تاریخ",
+       "exif-nickname": "تصویر کا غیر رسمی نام",
+       "exif-rating": "درجہ بندی (5 میں سے)",
+       "exif-rightscertificate": "حقوق کے انتظام کا تصدیق نامہ",
+       "exif-copyrighted": "کاپی رائٹ کی صورت حال",
+       "exif-copyrightowner": "کاپی رائٹ کا حامل",
+       "exif-usageterms": "استعمال کے شرائط",
+       "exif-webstatement": "آن لائن موجود کاپی رائٹ کا اعلامیہ",
+       "exif-originaldocumentid": "اصل دستاویز کی منفرد شناخت",
+       "exif-licenseurl": "کاپی رائٹ کے اجازت نامے کا یوآرایل",
+       "exif-morepermissionsurl": "متبادل اجازت ناموں کی معلومات",
+       "exif-attributionurl": "اس کام کو دوربارہ استعمال کرنے کے وقت اس کا ربط دیں",
+       "exif-preferredattributionname": "اس کام کو دوربارہ استعمال کرنے کے وقت اس سے منسوب کریں",
+       "exif-pngfilecomment": "پی این جی فائل کا تبصرہ",
+       "exif-disclaimer": "اظہار لا تعلقی",
+       "exif-contentwarning": "مواد سے متعلق انتباہ",
+       "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-meteringmode-2": "مرکز کی حجم شدہ اوسط",
+       "exif-meteringmode-3": "داغ",
+       "exif-meteringmode-4": "کثیر داغ",
+       "exif-meteringmode-5": "طرز",
+       "exif-meteringmode-6": "جزوی",
+       "exif-meteringmode-255": "دیگر",
+       "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-subjectdistancerange-0": "نامعلوم",
+       "exif-subjectdistancerange-1": "میکرو",
+       "exif-subjectdistancerange-2": "قریبی منظر",
+       "exif-subjectdistancerange-3": "دور سے دیکھیں",
+       "exif-gpslatitude-n": "شمالی عرض البلد",
+       "exif-gpslatitude-s": "جنوبی عرض البلد",
+       "exif-gpslongitude-e": "مشرقی طول البلد",
+       "exif-gpslongitude-w": "مغربی طول البلد",
+       "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-gpsdop-excellent": "بہترین ($1)",
+       "exif-gpsdop-good": "بہترین ($1)",
+       "exif-gpsdop-moderate": "معتدل ($1)",
+       "exif-gpsdop-fair": "کمتر ($1)",
+       "exif-gpsdop-poor": "کمزور ($1)",
+       "exif-objectcycle-a": "صرف صبح",
+       "exif-objectcycle-p": "صرف شام",
+       "exif-objectcycle-b": "صبح و شام",
+       "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-dc-rights": "حقوق",
+       "exif-dc-source": "ماخذ میڈیا",
+       "exif-dc-type": "میڈیا کی قسم",
+       "exif-rating-rejected": "مسترد شدہ",
+       "exif-isospeedratings-overflow": "65535 سے بڑا",
+       "exif-iimcategory-ace": "فنون لطیفہ، ثقافت اور تفریح",
+       "exif-iimcategory-clj": "جرم اور قانون",
+       "exif-iimcategory-dis": "آفات اور حادثات",
+       "exif-iimcategory-fin": "معیشت اور کاروبار",
+       "exif-iimcategory-edu": "تعلیم",
+       "exif-iimcategory-evn": "ماحول",
+       "exif-iimcategory-hth": "صحت",
+       "exif-iimcategory-hum": "انسانی دلچسپی",
+       "exif-iimcategory-lab": "مزدوری",
+       "exif-iimcategory-lif": "طرز زندگی اور تفریح",
+       "exif-iimcategory-pol": "سیاست",
+       "exif-iimcategory-rel": "مذہب اور عقیدہ",
+       "exif-iimcategory-sci": "سائنس اور ٹیکنالوجی",
+       "exif-iimcategory-soi": "سماجی مسائل",
+       "exif-iimcategory-spo": "کھیل",
+       "exif-iimcategory-war": "جنگ، تصادم اور بدامنی",
+       "exif-iimcategory-wea": "موسم",
+       "exif-urgency-normal": "عام ($1)",
+       "exif-urgency-low": "کم ($1)",
+       "exif-urgency-high": "اعلیٰ ($1)",
+       "exif-urgency-other": "صارف کی وضاحت کردہ ترجیح ($1)",
        "namespacesall": "تمام",
        "monthsall": "تمام",
-       "deletedwhileediting": "انتباہ: آپ کے ترمیم شروع کرنے کے بعد یہ صفحہ حذف کیا جا چکا ہے!",
+       "confirmemail": "اپنے برقی پتہ کی تصدیق کریں",
+       "confirmemail_noemail": "آپ نے [[Special:Preferences|اپنی ترجیحات]] میں درست برقی ڈاک پتا نہیں دیا ہے۔",
+       "confirmemail_text": "{{SITENAME}} میں موجود برقی خط کی سہولتوں کو استعمال کرنے کے لیے آپ کے برقی ڈاک پتے کی تصدیق ضروری ہے۔\nاپنے پتے پر تصدیقی ڈاک روانہ کرنے کے لیے ذیل میں موجود بٹن پر کلک کریں۔\nموصولہ برقی خط میں آپ کو کوڈ پر مشتمل ایک ربط نظر آئے گا۔\nچنانچہ اپنے بڑقی ڈاک پتے کی تصدیق کے لیے اس ربط کو اپنے براؤزر میں کھولیں۔",
+       "confirmemail_pending": "آپ کو تصدیقی کوڈ پہلے ہی روانہ کیا جا چکا ہے۔\nاگر آپ نے ابھی اپنا کھاتہ بنایا ہے تو نئے کوڈ کی درخواست دینے سے قبل اس کے موصول ہونے کا کچھ دیر انتظار کر لیں۔",
+       "confirmemail_send": "تصدیقی کوڈ بھیجیں",
+       "confirmemail_sent": "تصدیقی برقی خط بھیجا گیا ہے۔",
+       "confirmemail_oncreate": "آپ کے برقی ڈاک پتے پر تصدیقی کوڈ بھیجا گیا ہے۔\nیہ کوڈ داخل ہونے کے لیے ضروری نہیں، تاہم اس ویکی میں برقی ڈاک پر مبنی کسی سہولت کو فعال کرنے سے قبل آپ کو اس کوڈ کی ضرورت پڑے گی۔",
+       "confirmemail_sendfailed": "{{SITENAME}} آپ کو تصدیقی کوڈ نہیں بھیج سکا۔\nبراہ کرم اپنا برقی ڈاک پتا جانچ لیں کہ کہیں اس میں نادرست حروف تو موجود نہیں۔\n\nڈاک رساں کا جواب: $1",
+       "confirmemail_invalid": "نادرست تصدیقی کوڈ۔\nممکن ہے اس کوڈ کی مدت ختم ہو چکی ہو۔",
+       "confirmemail_needlogin": "اپنے برقی ڈاک پتے کی تصدیق کے لیے براہ کرم $1 ہوں۔",
+       "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": "ٹھیک ہے",
+       "confirm-unwatch-top": "اس صفحہ کو آپ کی زیرِنظرفہرست سے حذف کر دیا جائے؟",
        "confirm-rollback-button": "ٹھیک ہے",
+       "confirm-rollback-top": "اس صفحے پر ترمیم کو استرجع کیا جائے؟",
        "semicolon-separator": "؛&#32;",
        "comma-separator": "،&#32;",
+       "quotation-marks": "\"$1\"",
        "imgmultipageprev": "← پچھلا",
        "imgmultipagenext": "اگلا →",
        "imgmultigo": "جائیں!",
        "imgmultigoto": "$1 صفحہ پر جائیں",
+       "img-lang-default": "(طے شدہ زبان)",
+       "img-lang-info": "تصویر کا اس زبان میں ترجمہ کریں $1۔ $2",
+       "img-lang-go": "چلیں",
+       "ascending_abbrev": "صعودی",
+       "descending_abbrev": "نزولی",
        "table_pager_next": "اگلا صفحہ",
        "table_pager_prev": "پچھلا صفحہ",
        "table_pager_first": "پہلا صفحہ",
        "table_pager_last": "آخری صفحہ",
+       "table_pager_limit": "فی صفحہ $1 آئٹم دکھائیں",
+       "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-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": "زیرِنظرفہرست دیکھیں اور تدوین کریں",
        "watchlisttools-raw": "خام زیرِنظرفہرست تدوین کریں",
        "hijri-calendar-m10": "شوال",
        "hijri-calendar-m11": "ذوالقعدہ",
        "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\" کو نظر انداز کر دیا گیا ہے کیونکہ یہ متعلقہ صفحہ کے عنوان کا حقیقی متبادل نہیں ہے۔",
-       "version": "ورژن",
+       "invalid-indicator-name": "<strong>نقص:</strong> صفحہ کی صورت حال کے اشارہ نما کی <code>name</code> خاصیت خالی نہیں ہونی چاہیے۔",
+       "version": "نسخہ",
+       "version-extensions": "نصب شدہ توسیعات",
+       "version-skins": "نصب شدہ پوشاکیں",
        "version-specialpages": "خاص صفحات",
+       "version-parserhooks": "تجزیہ کار ہک",
+       "version-variables": "متغیرات",
+       "version-antispam": "فاضل کاری کی روک تھام",
        "version-other": "دیگر",
+       "version-mediahandlers": "میڈیا ناظمین",
+       "version-hooks": "ہک",
+       "version-parser-extensiontags": "پارسر توسیع کے ٹیگ",
+       "version-parser-function-hooks": "پارسر فنکشن کے ہک",
+       "version-hook-name": "ہک کا نام",
+       "version-hook-subscribedby": "مستعمل بذریعہ",
+       "version-no-ext-name": "[کوئی نام نہیں]",
+       "version-license": "میڈیاویکی کا اجازت نامہ",
+       "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-license-title": "$1 کا اجازت نامہ",
+       "version-license-not-found": "اس توسیع کے اجازت نامے سے متعلق تفصیلی معلومات دستیاب نہین ہوئی۔",
+       "version-credits-title": "$1 کے انتسابات",
+       "version-credits-not-found": "اس توسیع کے انتسابات سے متعلق تفصیلی معلومات دستیاب نہین ہوئی۔",
+       "version-poweredby-credits": "پیش نظر ویکی <strong>[https://www.mediawiki.org/ میڈیاویکی]</strong> کی تقویت یافتہ ہے، جملہ حقوق محفوظ © 2001-$1 $2 بنام",
        "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-software": "نصب شدہ سافٹ ویئر",
+       "version-software-product": "مصنوعات",
+       "version-software-version": "نسخہ",
+       "version-entrypoints": "یوآرایل کا نقطہ آمد",
+       "version-entrypoints-header-entrypoint": "نقطہ آمد",
+       "version-entrypoints-header-url": "یوآرایل",
+       "version-libraries": "نصب شدہ کتب خانے",
+       "version-libraries-library": "کتب خانہ",
+       "version-libraries-version": "نسخہ",
+       "version-libraries-license": "اجازت نامہ",
+       "version-libraries-description": "وضاحت",
        "version-libraries-authors": "مصنف",
+       "redirect": "فائل، صارف، صفحہ، نسخہ، یا نوشتہ کی شناخت سے رجوع مکرر",
+       "redirect-summary": "اس خصوصی صفحہ کے ذریعہ فائل (درج کردہ فائل نام)، صفحہ (صفحہ یا نسخہ کا درج کردہ شناختی نمبر)، صفحہ صارف (صارف کا درج کردہ شناختی نمبر) یا اندراج نوشتہ (نوشتہ کا درج کردہ شناختی نمبر) کا رجوع مکرر حاصل کیا جا سکتا ہے۔ طریقہ استعمال: [[{{#Special:Redirect}}/file/Example.jpg]]، \n[[{{#Special:Redirect}}/page/64308]]، [[{{#Special:Redirect}}/revision/328429]] یا [[{{#Special:Redirect}}/user/101]] یا \n[[{{#Special:Redirect}}/logid/186]]۔",
+       "redirect-submit": "چلیں",
+       "redirect-lookup": "تلاش:",
+       "redirect-value": "قدر:",
+       "redirect-user": "صارف کی شناخت",
+       "redirect-page": "صفحہ کی شناخت",
+       "redirect-revision": "صفحہ کا نسخہ",
+       "redirect-file": "فائل کا نام",
+       "redirect-logid": "نوشتہ کی شناخت",
+       "redirect-not-exists": "یہ قدر نہیں ملی",
+       "fileduplicatesearch": "مکرر فائلوں کی تلاش",
+       "fileduplicatesearch-summary": "ہیش قدروں کے مطابق مکرر فائلوں کو تلاش کریں۔",
+       "fileduplicatesearch-filename": "فائل کا نام:",
        "fileduplicatesearch-submit": "تلاش",
+       "fileduplicatesearch-info": "$1 × $2 پکسل <br />فائل کا حجم: $3<br />MIME قسم: $4",
+       "fileduplicatesearch-result-1": "فائل «$1» کی کوئی نقل نہیں ہے۔",
+       "fileduplicatesearch-result-n": "فائل «$1» کی {{PLURAL:$2|1 نقل ہے|$2 نقلیں ہیں}}۔",
+       "fileduplicatesearch-noresults": "«$1» کے نام سے کوئی فائل نہیں مل سکی۔",
        "specialpages": "خصوصی صفحات",
+       "specialpages-note-top": "وضاحت",
+       "specialpages-note": "* عام خصوصی صفحات۔\n* <span class=\"mw-specialpagerestricted\">ممنوع خصوصی صفحات</span>",
+       "specialpages-group-maintenance": "نگہداشت کی رپورٹیں",
+       "specialpages-group-other": "دیگر خصوصی صفحات",
+       "specialpages-group-login": "کھاتہ کھولیں یا اندراج کریں",
+       "specialpages-group-changes": "حالیہ تبدیلیاں اور نوشتہ جات",
+       "specialpages-group-media": "میڈیا رپورٹیں اور اپلوڈ کردہ",
+       "specialpages-group-users": "صارفین اور اختیارات",
+       "specialpages-group-highuse": "کثیر مستعمل صفحات",
        "specialpages-group-pages": "فہارست صفحات",
-       "tag-filter": "[[Special:Tags|لوحہ]] فلٹر:",
-       "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|ٹیگ|ٹیگ}}]]: $2)",
+       "specialpages-group-pagetools": "آلات صفحہ",
+       "specialpages-group-wiki": "ڈیٹا اور آلات",
+       "specialpages-group-redirects": "رجوع مکرر کے حامل خصوصی صفحات",
+       "specialpages-group-spam": "آلات فاضل کاری",
+       "specialpages-group-developer": "آلات ترقی دہندہ",
+       "blankpage": "خالی صفحہ",
+       "intentionallyblankpage": "اس صفحہ کو دانستہ خالی چھوڑا گیا ہے۔",
+       "external_image_whitelist": "#اس سطر کو ہو بہو ایسا ہی رہنے دیں<pre>\n#ذیل میں ریجیکس کی عبارتیں درج کریں (محض // کے درمیان)\n#ان عبارتوں کی بیرونی تصویروں کے روابط سے مطابقت کی جائے گی\n#جو مطابق ہو جائیں وہ تصویر کے طور پر نظر آئیں گے ورنہ محض تصویر کا ربط ظاہر ہوگا\n# علامت # سے شرع ہونے والی سطروں کو تبصرہ سمجھا جائے گا\n#چھوٹے بڑے حروف کو نظر انداز کیا جائے گا\n\nریجیکس کی تمام عبارتوں کو اس سطر کے اوپر رکھیں۔ اس سطر کو ہو بہو ایسا ہی رہنے دیں</pre>",
+       "tags": "درست تبدیلی کے ٹیگ",
+       "tag-filter": "مقطار [[Special:Tags|ٹیگ]]:",
+       "tag-filter-submit": "مقطار",
+       "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|ٹیگ}}]]: $2)",
+       "tag-mw-contentmodelchange": "مواد کے ماڈل میں تبدیلی",
+       "tag-mw-contentmodelchange-description": "ترامیم جو صفحہ کے [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel مواد کے ماڈل کو تبدیل کرتی ہیں]",
+       "tags-title": "ٹیگ",
+       "tags-intro": "اس صفحہ میں ان تمام ٹیگوں کی فہرست درج ہے، جنہیں سافٹ ویئر کسی ترمیم پر مفہوم کے ساتھ نشان زد کرتا ہے۔",
+       "tags-tag": "ٹیگ کا نام",
+       "tags-display-header": "تبدیلی کی فہرستوں میں نمائش",
+       "tags-description-header": "مکمل وضاحت",
        "tags-source-header": "ماخذ",
        "tags-active-header": "فعال؟",
+       "tags-hitcount-header": "ٹیگ شدہ تبدیلیاں",
+       "tags-actions-header": "اقدامات",
        "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-create-submit": "تخلیق",
+       "tags-manage-no-permission": "آپ کو تبدیلی کے ٹیگوں کے انتظام کی اجازت نہیں ہے۔",
+       "tags-manage-blocked": "آپ بحالت پابندی تبدیلی کے ٹیگوں کا انتظام نہیں کر سکتے۔",
+       "tags-create-heading": "نیا ٹیگ بنائیں",
+       "tags-create-explanation": "ابتدائی طور پر نو تخلیق شدہ ٹیگ صارفین اور روبہ جات کے استعمال کے لیے دستیاب ہونگے۔",
+       "tags-create-tag-name": "ٹیگ کا نام:",
+       "tags-create-reason": "وجہ:",
+       "tags-create-submit": "بنائیں",
+       "tags-create-no-name": "آپ نے ایک ٹیگ کا نام دینا ہو گا۔",
+       "tags-create-invalid-chars": "ٹیگوں کے نام میں فاصلے (<code>,</code>) یا فارورڈ سلیش (<code>/</code>) نہیں ہونے چاہئیں۔",
+       "tags-create-invalid-title-chars": "ٹیگوں کے نام میں ایسے حروف کے استعمال کی اجازت نہیں جنہیں صفحات کے عناوین میں استعمال نہیں کیا جا سکتا۔",
+       "tags-create-already-exists": "ٹیگ \"$1\" پہلے سے موجود ہے۔",
+       "tags-create-warnings-above": " \"$1\" نامی ٹیگ بنانے کے دوران میں درج ذیل {{PLURAL:$2|انتباہ دیا گیا|انتباہات دیے گئے}}:",
+       "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-activate-reason": "وجہ:",
+       "tags-activate-not-allowed": "«$1» ٹیگ کو فعال کرنا ممکن نہیں۔",
+       "tags-activate-not-found": "«$1» ٹیگ موجود نہیں ہے۔",
        "tags-activate-submit": "فعال",
        "tags-deactivate-title": "ٹیگ غیر فعال",
+       "tags-deactivate-question": "آپ «$1» ٹیگ کو غیر فعال کرنے جا رہے ہیں۔",
        "tags-deactivate-reason": "وجہ:",
+       "tags-deactivate-not-allowed": "«$1» ٹیگ کو غیر فعال کرنا ممکن نہیں۔",
        "tags-deactivate-submit": "غیر فعال",
+       "tags-apply-no-permission": "آپ کو اپنی تبدیلیوں پر تبدیلی کے ٹیگوں کو نافذ کرنے کی اجازت نہیں ہے۔",
+       "tags-apply-blocked": "بحالت پابندی آپ اپنی تبدیلیوں پر تبدیلی کے ٹیگ نافذ نہیں کر سکتے۔",
+       "tags-apply-not-allowed-one": "«$1» ٹیگ کو دستی طور پر نافذ کرنے کی اجازت نہیں ہے۔",
+       "tags-apply-not-allowed-multi": "درج ذیل {{PLURAL:$2|ٹیگ|ٹیگوں}} کو دستی طور پر نافذ کرنے کی اجازت نہیں ہے: $1",
+       "tags-update-no-permission": "آپ کو انفرادی نسخوں یا نوشتہ کے اندراجات سے تبدیلی کے ٹیگوں کو ہٹانے یا ان میں لگانے کی اجازت نہیں ہے۔",
+       "tags-update-blocked": "بحالت پابندی آپ تبدیلی کے ٹیگوں کو لگا یا ہٹا نہیں سکتے۔",
+       "tags-update-add-not-allowed-one": "«$1» ٹیگ کو دستی طور پر لگانے کی اجازت نہیں ہے۔",
+       "tags-update-add-not-allowed-multi": "درج ذیل {{PLURAL:$2|ٹیگ|ٹیگوں}} کو دستی طور پر لگانے کی اجازت نہیں ہے: $1",
+       "tags-update-remove-not-allowed-one": "«$1» کو ہٹانے کی اجازت نہیں ہے۔",
+       "tags-update-remove-not-allowed-multi": "درج ذیل {{PLURAL:$2|ٹیگ|ٹیگوں}} کو دستی طور پر ہٹانے کی اجازت نہیں ہے: $1",
        "tags-edit-title": "ٹیگ میں ترمیم",
+       "tags-edit-manage-link": "ٹیگ کا انتظام",
+       "tags-edit-revision-selected": "[[:$2]] {{PLURAL:$1|کا منتخب نسخہ|کے منتخب نسخے}}:",
+       "tags-edit-logentry-selected": "{{PLURAL:$1|منتخب واقعۂ نوشتہ|منتخب واقعاتِ نوشتہ}}:",
+       "tags-edit-revision-legend": "{{PLURAL:$1|اس نسخے|ان تمام $1 نسخوں}} میں ٹیگ لگائیں یا ان سے ہٹائیں",
+       "tags-edit-logentry-legend": "{{PLURAL:$1|اس اندراج نوشتہ|ان تمام $1 اندراجات نوشتہ}} میں ٹیگ لگائیں یا ان سے ہٹائیں",
+       "tags-edit-existing-tags": "موجودہ ٹیگ:",
+       "tags-edit-existing-tags-none": "<em>کوئی نہیں</em>",
+       "tags-edit-new-tags": "نئے ٹیگ:",
+       "tags-edit-add": "ان ٹیگ کا اضافہ کریں:",
+       "tags-edit-remove": "یہ ٹیگ نکالیں:",
+       "tags-edit-remove-all-tags": "(تمام ٹیگ نکال دیں)",
+       "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": "تبدیلیاں نافذ نہیں کی جا سکیں:\n$1",
+       "tags-edit-nooldid-title": "نادرست ہدف نسخہ",
+       "tags-edit-nooldid-text": "اس کارروائی کو انجام دینے کے لیے یا تو آپ نے کسی ہدف نسخے کا تعین نہیں کیا ہے، یا متعینہ نسخہ موجود نہیں ہے۔",
+       "tags-edit-none-selected": "اضافہ کرنے یا ہٹانے کے لیے کم از کم ایک ٹیگ منتخب کریں۔",
+       "comparepages": "صفحات کا موازنہ کریں",
+       "compare-page1": "صفحہ 1",
+       "compare-page2": "صفحہ 2",
+       "compare-rev1": "نظرثانی 1",
+       "compare-rev2": "نظرثانی 2",
+       "compare-submit": "موازنہ",
+       "compare-invalid-title": "آپ کا اختصاصی عنوان غلط ہے۔",
+       "compare-title-not-exists": "آپ کا اختصاصی عنوان موجود نہیں۔",
+       "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": "آپ کی درج کردہ قدر عدد صحیح نہیں ہے۔",
+       "htmlform-float-invalid": "آپ کی درج کردہ قدر عدد نہیں ہے۔",
+       "htmlform-int-toolow": "آپ کی درج کردہ قدر کمترین حد یعنی $1 سے کم ہے۔",
+       "htmlform-int-toohigh": "آپ کی درج کردہ قدر $1 سے زیادہ ہے۔",
+       "htmlform-required": "یہ قدر درکار ہے۔",
+       "htmlform-submit": "ٹھیک ہے",
+       "htmlform-reset": "رد ترامیم",
        "htmlform-selectorother-other": "دیگر",
        "htmlform-no": "نہیں",
        "htmlform-yes": "ہاں",
+       "htmlform-chosen-placeholder": "ایک اختیار منتخب کریں",
+       "htmlform-cloner-create": "مزید اضافہ کریں",
+       "htmlform-cloner-delete": "حذف",
+       "htmlform-cloner-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": "آپ کی درج کردہ قدر تسلیم شدہ تاریخ نہیں ہے۔ براہ کرم YYYY-MM-DD فارمیٹ استعمال کرنے کی کوشش کریں۔",
+       "htmlform-time-invalid": "آپ کی درج کردہ قدر تسلیم شدہ وقت نہیں ہے۔ براہ کرم HH:MM:SS فارمیٹ استعمال کرنے کی کوشش کریں۔",
+       "htmlform-datetime-invalid": "آپ کی درج کردہ قدر تسلیم شدہ تاریخ اور وقت نہیں ہے۔ براہ کرم YYYY-MM-DD HH:MM:SS فارمیٹ استعمال کرنے کی کوشش کریں۔",
+       "htmlform-title-badnamespace": "[[:$1]] صفحہ \"{{ns:$2}}\" نام فضا میں موجود نہیں۔",
+       "htmlform-title-not-creatable": "«$1» عنوان قابل تخلیق نہیں",
+       "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 نے صفحہ $3 کو {{GENDER:$2|بحال کیا}}",
+       "logentry-delete-event": "$1 نے $3 میں موجود {{PLURAL:$5|ایک واقعۂ نوشتہ|$5 واقعات نوشتہ}} کی مرئیت کو {{GENDER:$2|تبدیل کیا}}: $4",
+       "logentry-delete-revision": "$1 نے $3 میں موجود {{PLURAL:$5|ایک نسخے|$5 نسخوں}} کی مرئیت کو {{GENDER:$2|تبدیل کیا}}: $4",
+       "logentry-delete-event-legacy": "$1 نے $3 میں موجود واقعات نوشتہ کی مرئیت کو {{GENDER:$2|تبدیل کیا}}",
+       "logentry-delete-revision-legacy": "$1 نے $3 میں موجود نسخوں کی مرئیت کو {{GENDER:$2|تبدیل کیا}}",
+       "logentry-suppress-delete": "$1 نے صفحہ $3 کو {{GENDER:$2|پوشیدہ کیا}}",
+       "logentry-suppress-event": "$1 نے $3 میں موجود {{PLURAL:$5|ایک واقعۂ نوشتہ|$5 واقعات نوشتہ}} کی مرئیت کو خفیہ طور پر {{GENDER:$2|تبدیل کیا}}: $4",
+       "logentry-suppress-revision": "$1 نے $3 میں موجود {{PLURAL:$5|ایک نسخے|$5 نسخوں}} کی مرئیت کو خفیہ طور پر {{GENDER:$2|تبدیل کیا}}: $4",
+       "logentry-suppress-event-legacy": "$1 نے $3 میں موجود واقعات نوشتہ کی مرئیت کو خفیہ طور پر {{GENDER:$2|تبدیل کیا}}",
+       "logentry-suppress-revision-legacy": "$1 نے $3 میں موجود نسخوں کی مرئیت کو خفیہ طور پر {{GENDER:$2|تبدیل کیا}}",
+       "revdelete-content-hid": "مواد کو پوشیدہ کرد یا گیا",
+       "revdelete-summary-hid": "خلاصہ ترمیم کو پوشیدہ کر دیا گیا",
+       "revdelete-uname-hid": "صارف نام کو پوشیدہ کر دیا گیا",
+       "revdelete-content-unhid": "مواد کی پوشیدگی ختم کی گئی",
+       "revdelete-summary-unhid": "خلاصہ ترمیم کی پوشیدگی ختم کی گئی",
+       "revdelete-uname-unhid": "صارف نام کی پوشیدگی ختم کی گئی",
+       "revdelete-restricted": "منتظمین کو محدود کر دیا گیا",
+       "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-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": "تلاش",
-       "expandtemplates": "سانچے کو وسیع کریں",
+       "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-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|ہزاریے|ہزاریہ}}",
+       "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": "زبان",
+       "pagelang-use-default": "طے شدہ زبان استعمال کرتا ہے",
+       "pagelang-select-lang": "زبان کا انتخاب کریں",
+       "pagelang-submit": "ٹھیک ہے",
+       "right-pagelang": "صفحے کی زبان تبدیل کریں",
+       "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-table-totalbytes": "مشترکہ حجم",
+       "mediastatistics-header-unknown": "نامعلوم",
+       "mediastatistics-header-bitmap": "بٹ میپ تصویریں",
+       "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-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": "عربی توسیع شدہ",
        "special-characters-group-persian": "فارسی",
        "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": "کھمیری"
+       "special-characters-group-khmer": "کھمیری",
+       "special-characters-title-endash": "علامت خط",
+       "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-import": "درآمد کی نوعیت:",
+       "log-action-filter-managetags": "انتظام ٹیگ کے اقدام کی نوعیت:",
+       "log-action-filter-move": "منتقلی کی نوعیت:",
+       "log-action-filter-newusers": "کھاتہ سازی کی نوعیت:",
+       "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": "تبدیلیٔ پابندی",
+       "log-action-filter-block-unblock": "پابندی ختم",
+       "log-action-filter-contentmodel-change": "مواد کے ماڈل کی تبدیلی",
+       "log-action-filter-contentmodel-new": "غیر معیاری contentmodel پر مشتمل صفحہ سازی",
+       "log-action-filter-delete-delete": "صفحہ کی حذف شدگی",
+       "log-action-filter-delete-restore": "صفحہ کی بحالی",
+       "log-action-filter-delete-event": "نوشتہ کی حذف شدگی",
+       "log-action-filter-delete-revision": "ترمیم کی حذف شدگی",
+       "log-action-filter-import-interwiki": "ماورائے ویکی درآمد",
+       "log-action-filter-import-upload": "ایکس ایم ایل اپلوڈ کی مدد سے درآمد",
+       "log-action-filter-managetags-create": "ٹیگ سازی",
+       "log-action-filter-managetags-delete": "ٹیگ کی حذف شدگی",
+       "log-action-filter-managetags-activate": "ٹیگ کی فعالی",
+       "log-action-filter-managetags-deactivate": "ٹیگ کی غیر فعالی",
+       "log-action-filter-move-move": "رجوع مکررات کو برتحریر کیے بغیر منتقلی",
+       "log-action-filter-move-move_redir": "رجوع مکررات کی برتحریری کے ساتھ منتقلی",
+       "log-action-filter-newusers-create": "گمنام صارف کی تخلیق",
+       "log-action-filter-newusers-create2": "مندرج صارف کی تخلیق",
+       "log-action-filter-newusers-autocreate": "خودکار تخلیق",
+       "log-action-filter-newusers-byemail": "برقی خط کے ذریعہ بھیجے گئے پاس ورڈ کی مدد سے تخلیق",
+       "log-action-filter-patrol-patrol": "دستی مراجعت",
+       "log-action-filter-patrol-autopatrol": "خودکار مراجعت",
+       "log-action-filter-protect-protect": "حفاظت",
+       "log-action-filter-protect-modify": "تبدیلیٔ حفاظت",
+       "log-action-filter-protect-unprotect": "اختتام حفاظت",
+       "log-action-filter-protect-move_prot": "منتقلیٔ حفاظت",
+       "log-action-filter-rights-rights": "دستی تبدیلی",
+       "log-action-filter-rights-autopromote": "خود کار تبدیلی",
+       "log-action-filter-suppress-event": "نوشتہ کی پوشیدگی",
+       "log-action-filter-suppress-revision": "نسخے کی پوشیدگی",
+       "log-action-filter-suppress-delete": "صفحہ کی پوشیدگی",
+       "log-action-filter-suppress-block": "بذریعہ پابندی صارف کی پوشیدگی",
+       "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-retype-help": "تصدیق کے لیے دوبارہ پاس ورڈ درج کریں",
+       "authmanager-email-label": "برقی خط",
+       "authmanager-email-help": "برقی ڈاک پتا",
+       "authmanager-realname-label": "حقیقی نام",
+       "authmanager-realname-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": "کھاتے کا نام:",
+       "cannotlink-no-provider-title": "قابل ربط کھاتے موجود نہیں ہیں",
+       "cannotlink-no-provider": "قابل ربط کھاتے موجود نہیں ہیں۔",
+       "linkaccounts": "کھاتوں کو مربوط کریں",
+       "linkaccounts-success-text": "کھاتے کو مربوط کر دیا گیا۔",
+       "linkaccounts-submit": "کھاتوں کو مربوط کریں",
+       "unlinkaccounts": "مربوط کھاتوں کو علاحدہ کریں",
+       "unlinkaccounts-success": "مربوط کھاتہ علاحدہ کر دیا گیا۔",
+       "authenticationdatachange-ignored": "تصدیقی معلومات کی تبدیلی نہیں ہو سکی۔ شاید کوئی پرووائڈر فراہم نہیں کیا گیا؟",
+       "userjsispublic": "براہ کرم اس بات کا خیال رکھیں کہ جاوا اسکرپٹ کے ذیلی صفحات میں خفیہ معلومات نہیں رکھی جانی چاہئیں کیونکہ ان صفحات کو دیگر صارفین بھی دیکھ سکتے ہیں۔",
+       "usercssispublic": "براہ کرم اس بات کا خیال رکھیں کہ سی ایس ایس کے ذیلی صفحات میں خفیہ معلومات نہیں رکھی جانی چاہئیں کیونکہ ان صفحات کو دیگر صارفین بھی دیکھ سکتے ہیں۔",
+       "restrictionsfield-badip": "آئی پی پتا یا رینج نادرست ہے: $1",
+       "restrictionsfield-label": "آئی پی کی اجازت یافتہ رینج:",
+       "restrictionsfield-help": "فی سطر ایک آئی پی پتا یا سی آئی ڈی آر رینج۔ تمام کو فعال کرنے کے لیے <br><code>0.0.0.0/0</code><br><code>::/0</code> استعمال کریں"
 }
index 71b389f..25b3d3d 100644 (file)
                        "Arystanbek",
                        "6ahodir",
                        "Таржимон",
-                       "Ximik1991"
+                       "Ximik1991",
+                       "Bmansurov"
                ]
        },
-       "tog-underline": "Havolalarning tagiga chizish:",
+       "tog-underline": "Havolaning tagiga chizish:",
        "tog-hideminor": "Yangi oʻzgarishlar roʻyxatida kichik tahrirlarni yashirish",
        "tog-hidepatrolled": "Yangi oʻzgarishlar roʻyxatida tekshirilgan tahrirlarni yashirish",
        "tog-newpageshidepatrolled": "Yangi sahifalar roʻyxatidan tekshirilgan sahifalarni yashirish",
        "tog-diffonly": "Versiyalar taqqoslanayotganda, pastda sahifa matni koʻrsatilmasin",
        "tog-showhiddencats": "Yashirin turkumlarni koʻrsatish",
        "tog-norollbackdiff": "Tahrir qaytarilganda, versiyalar taqqosi koʻrsatilmasin",
-       "tog-useeditwarning": "Oʻzgarishlarni saqlamay sahifadan chiqib ketayotganim haqida ogohlantirish",
+       "tog-useeditwarning": "Oʻzgarishlarni saqlamay sahifadan chiqib ketayotganim haqida ogohlantir",
        "tog-prefershttps": "Doim himoyalangan holda kirish",
        "underline-always": "Har doim",
        "underline-never": "Hech qachon",
-       "underline-default": "Brauzer moslamari boʻyicha",
+       "underline-default": "Bezak mavzusi yoki brauzer andozasi boʻyicha",
        "editfont-style": "Tahrirlash maydonidagi shrift turi:",
-       "editfont-default": "Brauzer moslamari boʻyicha",
+       "editfont-default": "Brauzer andozasi boʻyicha",
        "editfont-monospace": "Teng enli shrift (Monospaced)",
        "editfont-sansserif": "Kertiksiz shrift (Sans-serif)",
        "editfont-serif": "Kertikli shrift (Serif)",
        "oct": "Okt",
        "nov": "Noy",
        "dec": "Dek",
-       "january-date": "Yanvar $1",
-       "february-date": "Fevral $1",
-       "march-date": "Mart $1",
-       "april-date": "Aprel $1",
-       "may-date": "$1-may",
-       "june-date": "Iyun $1",
-       "july-date": "Iyul $1",
+       "january-date": "$1 yanvar",
+       "february-date": "$1 fevral",
+       "march-date": "$1 mart",
+       "april-date": "$1 aprel",
+       "may-date": "$1 may",
+       "june-date": "$1 iyun",
+       "july-date": "$1 iyul",
        "august-date": "Avgust $1",
        "september-date": "Sentabr $1",
        "october-date": "Oktabr $1",
        "yourpasswordagain": "Maxfiy so‘zni qayta kiriting:",
        "createacct-yourpasswordagain": "Maxfiy soʻzni tasdiqlang",
        "createacct-yourpasswordagain-ph": "Maxfiy soʻzni yana bir bor kiriting",
-       "remembermypassword": "Hisob ma’lumotlarim ushbu brauzerda eslab qolinsin (ko‘pi bilan $1 {{PLURAL:$1|kunga|kunga}})",
        "userlogin-remembermypassword": "Kirgan deb esda saqla",
        "userlogin-signwithsecure": "Himoyalangan holda kirish",
        "yourdomainname": "Sizning domeningiz:",
index 42c3a28..429f979 100644 (file)
@@ -88,7 +88,7 @@
        "march": "marso",
        "april": "avril",
        "may_long": "majo",
-       "june": "giugno",
+       "june": "zugno",
        "july": "lujo",
        "august": "agosto",
        "september": "setenbre",
        "march-gen": "marso",
        "april-gen": "avril",
        "may-gen": "majo",
-       "june-gen": "giugno",
+       "june-gen": "zugno",
        "july-gen": "lujo",
        "august-gen": "agosto",
        "september-gen": "setenbre",
        "yourpasswordagain": "De novo la password:",
        "createacct-yourpasswordagain": "Conferma la password",
        "createacct-yourpasswordagain-ph": "Inserissi da novo la password",
-       "remembermypassword": "Tiente in mente la password su sto conputer (par un massimo de $1 {{PLURAL:$1|zorno|zorni}})",
        "userlogin-remembermypassword": "Tienme colegà",
        "userlogin-signwithsecure": "Entra con na conesion segura",
        "yourdomainname": "Spesifegare el dominio",
        "htmlform-no": "No",
        "htmlform-yes": "Sì",
        "htmlform-chosen-placeholder": "Selessiona na opzione",
-       "sqlite-has-fts": "$1 con la possibilità de riserca completa nel testo",
-       "sqlite-no-fts": "$1 sensa la possibilità de riserca completa nel testo",
        "logentry-delete-delete": "$1 {{GENDER:$2|el|la}} ga scansełà ła pajina $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|el|la}} ga ripristinà \"$3\"",
        "logentry-delete-event": "$1 {{GENDER:$2|el|la}} ga canbià ła vixibiłità de {{PLURAL:$5|n'asion del registro|$5 asion del registro}} de \"$3\": $4",
        "feedback-bugornote": "Se se xe in grado de descrivare el problema tenico riscontrà in maniera precixa, [$1 segnałare el bug]. In alternadiva, se pol doparar el moduło senplifegà cuà soto. El comento inserio el sarà xontà a ła pàjina \"[$3 $2]\", insieme al propio nome utente.",
        "feedback-cancel": "Anuła",
        "feedback-close": "Fato",
-       "feedback-error-title": "Eròr",
        "feedback-error1": "Eror: Da ła API xe rivà un rexultà nó riconosùo",
        "feedback-error2": "Eror: Nó xe sta posibiłe exeguir ła modifega",
        "feedback-error3": "Errore: Nisuna risposta da ła API",
index 31b213f..1a15bf2 100644 (file)
@@ -62,7 +62,7 @@
        "tog-enotifminoredits": "Gửi thư cho tôi cả những thay đổi nhỏ trong trang và tập tin",
        "tog-enotifrevealaddr": "Hiện địa chỉ thư điện tử của tôi trong thư thông báo",
        "tog-shownumberswatching": "Hiển thị số người đang xem",
-       "tog-oldsig": "Chữ ký hiện tại:",
+       "tog-oldsig": "Chữ ký hiện tại của bạn:",
        "tog-fancysig": "Xem chữ ký là mã wiki (không có liên kết tự động)",
        "tog-uselivepreview": "Xem trước trực tiếp",
        "tog-forceeditsummary": "Nhắc tôi khi tôi quên tóm lược sửa đổi",
@@ -79,7 +79,7 @@
        "tog-showhiddencats": "Hiển thị thể loại ẩn",
        "tog-norollbackdiff": "Bỏ qua bản so sánh sau khi lùi sửa",
        "tog-useeditwarning": "Cảnh báo khi tôi thoát trang sửa đổi mà chưa lưu trang",
-       "tog-prefershttps": "Luôn kết nối an toàn khi đăng nhập",
+       "tog-prefershttps": "Luôn kết nối an toàn khi đã đăng nhập",
        "underline-always": "Luôn luôn",
        "underline-never": "Không bao giờ",
        "underline-default": "Mặc định của giao diện hoặc trình duyệt",
        "newwindow": "(mở cửa sổ mới)",
        "cancel": "Hủy bỏ",
        "moredotdotdot": "Thêm nữa…",
-       "morenotlisted": "Danh sách này không có đầy đủ.",
+       "morenotlisted": "Danh sách này có thể không đầy đủ.",
        "mypage": "Trang cá nhân",
        "mytalk": "Tin nhắn",
        "anontalk": "Thảo luận",
        "eauthentsent": "Thư xác nhận đã được gửi cho địa chỉ thư điện tử được chỉ định. Trước khi bạn có thể nhận thư, bạn cần thực hiện hướng dẫn trong thư để xác nhận tài khoản thuộc về bạn.",
        "throttled-mailpassword": "Mật khẩu đã được gửi đến cho bạn trong vòng {{PLURAL:$1|$1 giờ|$1 giờ}} đồng hồ trở lại. Để tránh lạm dụng, chỉ có thể gửi mật khẩu $1 giờ đồng hồ một lần.",
        "mailerror": "Lỗi gửi thư : $1",
-       "acct_creation_throttle_hit": "Ai đó cùng [[địa chỉ IP]] với bạn đã mở {{PLURAL:$1|một tài khoản|$1 tài khoản}} ở đây trong vòng 24 giờ. Vì quy định hạn chế số tài khoản mở trên một địa chỉ IP nên bạn hiện không thể mở thêm được nữa dùng địa chỉ IP này.",
+       "acct_creation_throttle_hit": "Ai đó cùng địa chỉ IP với bạn đã mở {{PLURAL:$1|một tài khoản|$1 tài khoản}} ở đây vào $2 qua. Vì quy định hạn chế số tài khoản mở trên một địa chỉ IP nên bạn hiện không thể mở thêm được nữa dùng địa chỉ IP này.",
        "emailauthenticated": "Địa chỉ thư điện tử của bạn được xác nhận vào lúc $3 $2.",
        "emailnotauthenticated": "Địa chỉ thư điện tử của bạn chưa được xác nhận. Các chức năng sau sẽ không gửi thư điện tử.",
        "noemailprefs": "Hãy ghi một địa chỉ thư điện tử trong tùy chọn cá nhân để có thể sử dụng tính năng này.",
        "botpasswords-label-resetpassword": "Đặt lại mật khẩu",
        "botpasswords-label-grants": "Các quyền có liên quan:",
        "botpasswords-help-grants": "Các lượt cấp phép cho phép truy cập các quyền người dùng mà một tài khoản đã có sẵn. Xem thêm thông tin trong [[Special:ListGrants|bảng cấp phép]].",
-       "botpasswords-label-restrictions": "Hạn chế sử dụng:",
        "botpasswords-label-grants-column": "Cấp quyền",
        "botpasswords-bad-appid": "Bot có tên \"$1\" không hợp lệ.",
        "botpasswords-insert-failed": "Không thể thêm tên bot \"$1\". Nó đã được thêm vào chưa?",
        "botpasswords-updated-body": "Đã cập nhật mật khẩu cho bot “$1” của người dùng “$2”.",
        "botpasswords-deleted-title": "Bot mật khẩu đã bị xóa",
        "botpasswords-deleted-body": "Đã xóa mật khẩu cho bot “$1” của người dùng “$2”.",
-       "botpasswords-newpassword": "Mật khẩu mới để đăng nhập như <strong>$1</strong> là <strong>$2</strong>. <em>Xin hãy ghi lại mật khẩu này để mai mốt tham khảo.</em>",
+       "botpasswords-newpassword": "Mật khẩu mới để đăng nhập như <strong>$1</strong> là <strong>$2</strong>. <em>Xin hãy ghi lại mật khẩu này để mai mốt tham khảo.</em> <br> (Các bot cũ cần tên đăng nhập khớp với tên người dùng cuối cùng có thể sử  dụng tên người dùng <strong>$3</strong> và mật khẩu <strong>$4</strong>.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider không có sẵn.",
        "botpasswords-restriction-failed": "Mật khẩu bot giới hạn ngăn chặn đăng nhập này.",
        "botpasswords-invalid-name": "Tên người dùng đã chỉ định không chứa dấu tách mật khẩu bot (\"$1\").",
        "group-bot": "Bot",
        "group-sysop": "Bảo quản viên",
        "group-bureaucrat": "Hành chính viên",
-       "group-suppress": "Giám sát viên",
+       "group-suppress": "Người xóa hẳn Flow",
        "group-all": "(tất cả)",
        "group-user-member": "{{GENDER:$1}}thành viên",
        "group-autoconfirmed-member": "{{GENDER:$1}}thành viên tự động xác nhận",
        "group-bot-member": "{{GENDER:$1}}bot",
        "group-sysop-member": "{{GENDER:$1}}bảo quản viên",
        "group-bureaucrat-member": "{{GENDER:$1}}hành chính viên",
-       "group-suppress-member": "{{GENDER:$1}}giám sát viên",
+       "group-suppress-member": "{{GENDER:$1}}người xóa hẳn Flow",
        "grouppage-user": "{{ns:project}}:Thành viên",
        "grouppage-autoconfirmed": "{{ns:project}}:Thành viên tự xác nhận",
        "grouppage-bot": "{{ns:project}}:Bot",
        "grouppage-sysop": "{{ns:project}}:Bảo quản viên",
        "grouppage-bureaucrat": "{{ns:project}}:Hành chính viên",
-       "grouppage-suppress": "{{ns:project}}:Đàn áp viên",
+       "grouppage-suppress": "{{ns:project}}:Người xóa hẳn Flow",
        "right-read": "Đọc trang",
        "right-edit": "Sửa trang",
        "right-createpage": "Tạo trang (không phải trang thảo luận)",
        "tags-actions-header": "Tác vụ",
        "tags-active-yes": "Kích hoạt",
        "tags-active-no": "Vô hiệu",
-       "tags-source-extension": "Xác định bởi một mở rộng",
+       "tags-source-extension": "Xác định bởi phần mềm",
        "tags-source-manual": "Áp dụng thủ công bởi người dùng và bot",
        "tags-source-none": "Không còn sử dụng",
        "tags-edit": "sửa",
        "htmlform-title-not-exists": "$1 không tồn tại.",
        "htmlform-user-not-exists": "<strong>$1</strong> không tồn tại.",
        "htmlform-user-not-valid": "<strong>$1</strong> không phải là tên người dùng.",
-       "sqlite-has-fts": "$1 với sự hỗ trợ tìm kiếm toàn văn",
-       "sqlite-no-fts": "$1 không có hỗ trợ tìm kiếm toàn văn",
        "logentry-delete-delete": "$1 {{GENDER:$2}}đã xóa trang “$3”",
        "logentry-delete-restore": "$1 {{GENDER:$2}}đã phục hồi trang “$3”",
        "logentry-delete-event": "$1 {{GENDER:$2}}đã thay đổi mức hiển thị của {{PLURAL:$5|một mục nhật trình|$5 mục nhật trình}} về $3: $4",
        "feedback-external-bug-report-button": "Tạo một công việc kỹ thuật",
        "feedback-dialog-title": "Gửi phản hồi",
        "feedback-dialog-intro": "Bạn có thể gửi phản hồi dễ dàng qua biểu mẫu bên dưới. Thông tin phản hồi của bạn sẽ được bổ sung vào trang “$1” cùng với tên người dùng của bạn.",
-       "feedback-error-title": "Lỗi",
        "feedback-error1": "Hủy bỏ",
        "feedback-error2": "Lỗi: Sửa đổi thất bại",
        "feedback-error3": "Lỗi: API không có phản ứng",
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 45f0721..e3b9494 100644 (file)
        "talk": "讨论",
        "views": "视图",
        "toolbox": "工具",
+       "tool-link-userrights": "更改{{GENDER:$1|用户}}组",
+       "tool-link-emailuser": "电邮联系该{{GENDER:$1|用户}}",
        "userpage": "查看用户页面",
        "projectpage": "查看项目页面",
        "imagepage": "查看文件页面",
        "cannotdelete": "无法删除页面或文件“$1”。\n它可能已被其他人删除了。",
        "cannotdelete-title": "无法删除页面“$1”",
        "delete-hook-aborted": "删除被扩展钩子取消。钩子并没有给出解释。",
-       "no-null-revision": "无法创建对\"$1\"页面新的空白版本",
+       "no-null-revision": "无法创建对“$1”页面新的空白版本",
        "badtitle": "错误标题",
        "badtitletext": "您请求了个无效、不存在或者跨语言或跨wiki链接标题错误的页面。它可能包含一个或多个不能用于标题的字符。",
        "title-invalid-empty": "请求的页面标题为空,或只包含名字空间名称。",
        "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}}的身份登录。使用下面的表格以其他用户的身份登录。",
        "userlogin-createanother": "创建另一个账户",
        "createacct-emailrequired": "电子邮件地址",
        "createacct-emailoptional": "电子邮件地址(可选)",
-       "createacct-email-ph": "请输入的电子邮件地址",
+       "createacct-email-ph": "请输入的电子邮件地址",
        "createacct-another-email-ph": "输入电子邮件地址",
        "createaccountmail": "使用一个临时的随机密码并将其发送到指定的电子邮件地址中",
        "createaccountmail-help": "可被用于为另一个人创建账户而不需要得知密码。",
        "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”。它是否已添加?",
        "resetpass-temp-emailed": "您通过一个暂时电子邮件发送的代码登录。要完成登录,您必须在此设置一个新密码:",
        "resetpass-temp-password": "临时密码:",
        "resetpass-abort-generic": "密码更改已经被扩展程序中止。",
-       "resetpass-expired": "的密码已经到期。请设置新登录密码。",
+       "resetpass-expired": "的密码已经到期。请设置新登录密码。",
        "resetpass-expired-soft": "您的密码已经到期,需要重置。请现在更换新密码,或单击“{{int:authprovider-resetpass-skip-label}}”以稍后重置。",
        "resetpass-validity-soft": "您的密码无效:$1\n\n请选择一个新密码,或单击“{{int:authprovider-resetpass-skip-label}}”以稍后重置。",
        "passwordreset": "重置密码",
-       "passwordreset-text-one": "请输入你要重置的用户名。",
+       "passwordreset-text-one": "请完成此表单来通过电子邮件接收临时密码。",
        "passwordreset-text-many": "{{PLURAL:$1|在此键入您希望接收临时密码的邮件地址。}}",
        "passwordreset-disabled": "此Wiki已经禁用密码重置。",
        "passwordreset-emaildisabled": "此Wiki上无法使用邮件功能。",
        "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": "密码重置没有处理。也许没有配置提供者?",
        "shown-title": "每页显示$1项结果",
        "viewprevnext": "查看($1{{int:pipe-separator}}$2)($3)",
        "searchmenu-exists": "<strong>本wiki上有名为“[[:$1]]”的页面。</strong>{{PLURAL:$2|0=|另请查看找到的其他搜索结果。}}",
-       "searchmenu-new": "<strong>在本Wiki上新建名为“[[:$1]]”的页面!</strong>{{PLURAL:$2|0=|另请查看您的搜索找的结果。|另请查看搜索结果。}}",
+       "searchmenu-new": "<strong>在本Wiki上新建名为“[[:$1]]”的页面!</strong>{{PLURAL:$2|0=|另请查看您的搜索找的结果。|另请查看搜索结果。}}",
        "searchprofile-articles": "内容页面",
        "searchprofile-images": "多媒体",
        "searchprofile-everything": "全部",
        "searchprofile-advanced-tooltip": "在自定义名字空间中搜索",
        "search-result-size": "$1($2个字)",
        "search-result-category-size": "$1个成员($2个子分类,$3个文件)",
-       "search-redirect": "(重定向自“$1”)",
+       "search-redirect": "(重定向自$1)",
        "search-section": "(“$1”章节)",
        "search-category": "(分类$1)",
        "search-file-match": "(匹配文件内容)",
        "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": "连同您的更改应用标签",
        "upload-dialog-disabled": "使用此对话框的文件上传在此wiki已禁用。",
        "upload-dialog-title": "上传文件",
        "upload-dialog-button-cancel": "取消",
+       "upload-dialog-button-back": "返回",
        "upload-dialog-button-done": "完成",
        "upload-dialog-button-save": "保存",
        "upload-dialog-button-upload": "上传",
        "uploadstash-summary": "这个页面提供已经上传(或者上传中)但未发布到wiki之文件存取。这些文件除了上传的用户之外不会被其他人可见。",
        "uploadstash-clear": "清除贮藏文件",
        "uploadstash-nofiles": "您没有被隐藏的文件。",
-       "uploadstash-badtoken": "执行对应操作失败。可能是因为您的编辑凭证已过期。请重试。",
+       "uploadstash-badtoken": "执行对应操作失败,可能是因为您的编辑凭据已过期。请重试。",
        "uploadstash-errclear": "清除文件失败。",
        "uploadstash-refresh": "更新文件列表",
        "uploadstash-thumbnail": "显示缩略图",
        "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请求已过时。",
        "apisandbox-results-fixtoken-fail": "检索“$1”令牌失败。",
        "apisandbox-alert-page": "此页面上的字段无效。",
        "apisandbox-alert-field": "此字段的值无效。",
+       "apisandbox-continue": "继续",
+       "apisandbox-continue-clear": "清除",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}}将[https://www.mediawiki.org/wiki/API:Query#Continuing_queries 继续]上次请求;{{int:apisandbox-continue-clear}}将清除继续相关的参数。",
        "booksources": "网络书源",
        "booksources-search-legend": "搜索图书来源",
        "booksources-isbn": "ISBN:",
        "exif-imagewidth": "宽度",
        "exif-imagelength": "高度",
        "exif-bitspersample": "每像素字节数",
-       "exif-compression": "压缩方",
+       "exif-compression": "压缩方",
        "exif-photometricinterpretation": "像素构成",
        "exif-orientation": "方位",
        "exif-samplesperpixel": "像素数",
        "htmlform-cloner-create": "添加更多",
        "htmlform-cloner-delete": "移除",
        "htmlform-cloner-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": "您指定的值不是可识别的日期。尝试使用YYYY-MM-DD格式。",
+       "htmlform-time-invalid": "您指定的值不是可识别的时间。尝试使用HH:MM:SS格式。",
+       "htmlform-datetime-invalid": "您指定的值不是可识别的日期和时间。尝试使用YYYY-MM-DD HH:MM:SS格式。",
+       "htmlform-date-toolow": "您指定的值在$1的最早允许日期之前。",
+       "htmlform-date-toohigh": "您指定的值在$1的最晚允许日期之后。",
+       "htmlform-time-toolow": "您指定的值在$1的最早允许时间之前。",
+       "htmlform-time-toohigh": "您指定的值在$1的最晚允许时间之后。",
+       "htmlform-datetime-toolow": "您指定的值在$1的最早允许日期和时间之前。",
+       "htmlform-datetime-toohigh": "您指定的值在$1的最晚允许日期和时间之后。",
        "htmlform-title-badnamespace": "[[:$1]]不在“{{ns:$2}}”名字空间中。",
        "htmlform-title-not-creatable": "“$1”不是一个可创建的页面标题",
        "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{{GENDER:$2|删除}}页面$3",
        "logentry-delete-restore": "$1{{GENDER:$2|还原}}页面$3",
        "logentry-delete-event": "$1{{GENDER:$2|更改}}$3的{{PLURAL:$5|$5个日志事件}}的可见性:$4",
        "feedback-external-bug-report-button": "提交技术报告",
        "feedback-dialog-title": "发送反馈",
        "feedback-dialog-intro": "您可以使用下面的简便表格提交您的反馈。您的评论将连同您的用户名一起加入至页面“$1”。",
-       "feedback-error-title": "错误",
        "feedback-error1": "错误:从API返回无法识别的结果",
        "feedback-error2": "错误:编辑失败",
        "feedback-error3": "错误:API没有响应",
        "authmanager-authn-not-in-progress": "身份验证尚未进行,或会话数据丢失。请从头重新开始。",
        "authmanager-authn-no-primary": "提供的凭据不能被认证。",
        "authmanager-authn-no-local-user": "提供的证书没有与该wiki上的任何用户相关联。",
-       "authmanager-authn-no-local-user-link": "提供的证书有效,但没有与该wiki上的任何用户相关联。请通过不同方式登录,或创建一个新用户,然后您将拥有一个把您之前的证书链接到对应账户的选项。",
+       "authmanager-authn-no-local-user-link": "提供的凭据有效,但没有与该wiki上的任何用户相关联。请通过不同方式登录,或创建一个新用户,然后您将拥有一个把您之前的凭据链接到对应账户的选项。",
        "authmanager-authn-autocreate-failed": "所有账户的自动创建失败:$1",
-       "authmanager-change-not-supported": "提供的证书不能被更改,因为没有东西会使用它们。",
+       "authmanager-change-not-supported": "提供的凭据不能被更改,因为没有东西会使用它们。",
        "authmanager-create-disabled": "账户创建已停用。",
        "authmanager-create-from-login": "要创建您的账户,请填写下方的字段。",
        "authmanager-create-not-in-progress": "账户创建尚未进行,或会话数据丢失。请从头重新开始。",
-       "authmanager-create-no-primary": "提供的证书不能用于账户创建。",
+       "authmanager-create-no-primary": "提供的凭据不能用于账户创建。",
        "authmanager-link-no-primary": "提供的证书不能用于账户链接。",
        "authmanager-link-not-in-progress": "账户链接尚未进行,或会话数据丢失。请从头重新开始。",
        "authmanager-authplugin-setpass-failed-title": "密码更改失败",
        "authpage-cannot-link-continue": "无法继续账户链接。您的会话大概已超时。",
        "cannotauth-not-allowed-title": "权限被拒绝",
        "cannotauth-not-allowed": "您不被允许使用此页面",
-       "changecredentials": "更改证书",
-       "changecredentials-submit": "更改证书",
-       "changecredentials-invalidsubpage": "$1不是有效的证书类型。",
+       "changecredentials": "更改凭据",
+       "changecredentials-submit": "更改凭据",
+       "changecredentials-invalidsubpage": "$1不是有效的凭据类型。",
        "changecredentials-success": "您的证书已被更改。",
-       "removecredentials": "移除证书",
+       "removecredentials": "移除凭据",
        "removecredentials-submit": "移除证书",
-       "removecredentials-invalidsubpage": "$1不是有效的证书类型。",
+       "removecredentials-invalidsubpage": "$1不是有效的凭据类型。",
        "removecredentials-success": "您的证书已被移除。",
-       "credentialsform-provider": "证书类型:",
+       "credentialsform-provider": "凭据类型:",
        "credentialsform-account": "帐户名称:",
        "cannotlink-no-provider-title": "没有可链接账户",
        "cannotlink-no-provider": "没有可链接账户。",
        "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 37d164b..f791979 100644 (file)
@@ -75,7 +75,8 @@
                        "Kly",
                        "Cosine02",
                        "一個正常人",
-                       "Wehwei"
+                       "Wehwei",
+                       "1233thehongkonger"
                ]
        },
        "tog-underline": "底線標示連結:",
        "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\" 失敗,是否已新增過?",
        "apisandbox-results-fixtoken-fail": "取得 \"$1\" 密鑰失敗。",
        "apisandbox-alert-page": "此頁面上的欄位無效。",
        "apisandbox-alert-field": "此欄位的值無效。",
+       "apisandbox-continue-clear": "清除",
        "booksources": "圖書資源",
        "booksources-search-legend": "尋找圖書資源",
        "booksources-isbn": "國際標準書號:",
        "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",
        "logentry-delete-restore": "$1 還原頁面 $3",
        "logentry-delete-event": "$1 {{GENDER:$2|已更改}} $3 中 {{PLURAL:$5|1 筆日誌|$5 筆日誌}}的可見性:$4",
        "feedback-external-bug-report-button": "回報技術問題",
        "feedback-dialog-title": "送出意見回饋",
        "feedback-dialog-intro": "您可以使用以下簡易表單傳送您的意見回饋。您的意見將會使用您的使用者名稱新增至頁面 \"$1\"。",
-       "feedback-error-title": "錯誤",
        "feedback-error1": "錯誤:無法識別 API 回傳的結果",
        "feedback-error2": "錯誤:編輯失敗",
        "feedback-error3": "錯誤:API 沒有回應",
index 589144c..67369e2 100644 (file)
@@ -104,9 +104,9 @@ $namespaceAliases = [];
  * Mapping NS_xxx to array of GENDERKEY to alias.
  * Example:
  * @code
- * $namespaceGenderAliases = array(
- *     NS_USER => array( 'male' => 'Male_user', 'female' => 'Female_user' ),
- * );
+ * $namespaceGenderAliases = [
+ *     NS_USER => [ 'male' => 'Male_user', 'female' => 'Female_user' ],
+ * ];
  * @endcode
  */
 $namespaceGenderAliases = [];
index ba3d364..14f5b6f 100644 (file)
 
 $fallback = 'ru';
 
+$namespaceNames = [
+       NS_MEDIA            => 'Medii',
+       NS_SPECIAL          => 'Erikoine',
+       NS_TALK             => 'Pagin',
+       NS_USER             => 'Käyttäi',
+       NS_USER_TALK        => 'Käyttäi_pagin',
+       NS_PROJECT_TALK     => '$1_pagin',
+       NS_FILE             => 'Failu',
+       NS_FILE_TALK        => 'Failu_pagin',
+       NS_MEDIAWIKI        => 'MediiWiki',
+       NS_MEDIAWIKI_TALK   => 'MediiWiki_pagin',
+       NS_TEMPLATE         => 'Šablonu',
+       NS_TEMPLATE_TALK    => 'Šablonu_pagin',
+       NS_HELP             => 'Abu',
+       NS_HELP_TALK        => 'Abu_pagin',
+       NS_CATEGORY         => 'Kategourii',
+       NS_CATEGORY_TALK    => 'Kategourii_pagin',
+];
+
 $linkTrail = '/^([a-zčČšŠžŽäÄöÖ]+)(.*)$/sDu';
 
index c832237..974771f 100644 (file)
--- a/load.php
+++ b/load.php
@@ -23,6 +23,7 @@
  */
 
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 
 // This endpoint is supposed to be independent of request cookies and other
 // details of the session. Enforce this constraint with respect to session use.
@@ -35,6 +36,12 @@ if ( !$wgRequest->checkUrlExtension() ) {
        return;
 }
 
+// Don't initialise ChronologyProtector from object cache, and
+// don't wait for unrelated MediaWiki writes when querying ResourceLoader.
+MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->setRequestInfo( [
+       'ChronologyProtection' => 'false',
+] );
+
 // Set up ResourceLoader
 $resourceLoader = new ResourceLoader(
        ConfigFactory::getDefaultInstance()->makeConfig( 'main' ),
index 7e0fb45..1cb5eef 100644 (file)
@@ -1241,7 +1241,7 @@ abstract class Maintenance {
         * @param integer $db DB index (DB_REPLICA/DB_MASTER)
         * @param array $groups; default: empty array
         * @param string|bool $wiki; default: current wiki
-        * @return IDatabase
+        * @return Database
         */
        protected function getDB( $db, $groups = [], $wiki = false ) {
                if ( is_null( $this->mDb ) ) {
@@ -1316,7 +1316,7 @@ abstract class Maintenance {
 
        /**
         * Lock the search index
-        * @param DatabaseBase &$db
+        * @param Database &$db
         */
        private function lockSearchindex( $db ) {
                $write = [ 'searchindex' ];
@@ -1334,7 +1334,7 @@ abstract class Maintenance {
 
        /**
         * Unlock the tables
-        * @param DatabaseBase &$db
+        * @param Database &$db
         */
        private function unlockSearchindex( $db ) {
                $db->unlockTables( __CLASS__ . '::' . __METHOD__ );
@@ -1343,7 +1343,7 @@ abstract class Maintenance {
        /**
         * Unlock and lock again
         * Since the lock is low-priority, queued reads will be able to complete
-        * @param DatabaseBase &$db
+        * @param Database &$db
         */
        private function relockSearchindex( $db ) {
                $this->unlockSearchindex( $db );
@@ -1354,7 +1354,7 @@ abstract class Maintenance {
         * Perform a search index update with locking
         * @param int $maxLockTime The maximum time to keep the search index locked.
         * @param string $callback The function that will update the function.
-        * @param DatabaseBase $dbw
+        * @param Database $dbw
         * @param array $results
         */
        public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
@@ -1390,7 +1390,7 @@ abstract class Maintenance {
 
        /**
         * Update the searchindex table for a given pageid
-        * @param DatabaseBase $dbw A database write handle
+        * @param Database $dbw A database write handle
         * @param int $pageId The page ID to update.
         * @return null|string
         */
diff --git a/maintenance/addRFCandPMIDInterwiki.php b/maintenance/addRFCandPMIDInterwiki.php
new file mode 100644 (file)
index 0000000..9740ef2
--- /dev/null
@@ -0,0 +1,86 @@
+<?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
+ */
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Run automatically with update.php
+ *
+ * - Changes "rfc" URL to use tools.ietf.org domain
+ * - Adds "pmid" interwiki
+ *
+ * @since 1.28
+ */
+class AddRFCAndPMIDInterwiki extends LoggedUpdateMaintenance {
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription( 'Add RFC and PMID to the interwiki database table' );
+       }
+
+       protected function getUpdateKey() {
+               return __CLASS__;
+       }
+
+       protected function updateSkippedMessage() {
+               return 'RFC and PMID already added to interwiki database table';
+       }
+
+       protected function doDBUpdates() {
+               $interwikiCache = $this->getConfig()->get( 'InterwikiCache' );
+               // Using something other than the database,
+               if ( $interwikiCache !== false ) {
+                       return true;
+               }
+               $dbw = $this->getDB( DB_MASTER );
+               $rfc = $dbw->selectField(
+                       'interwiki',
+                       'iw_url',
+                       [ 'iw_prefix' => 'rfc' ],
+                       __METHOD__
+               );
+
+               // Old pre-1.28 default value, or not set at all
+               if ( $rfc === false || $rfc === 'http://www.rfc-editor.org/rfc/rfc$1.txt' ) {
+                       $dbw->replace(
+                               'interwiki',
+                               [ 'iw_prefix' ],
+                               [
+                                       'iw_prefix' => 'rfc',
+                                       'iw_url' => 'https://tools.ietf.org/html/rfc$1'
+                               ],
+                               __METHOD__
+                       );
+               }
+
+               $dbw->insert(
+                       'interwiki',
+                       [
+                               'iw_prefix' => 'pmid',
+                               'iw_url' => 'https://www.ncbi.nlm.nih.gov/pubmed/$1?dopt=Abstract',
+                       ],
+                       __METHOD__,
+                       // If there's already a pmid interwiki link, don't
+                       // overwrite it
+                       [ 'IGNORE' ]
+               );
+
+               return true;
+       }
+}
diff --git a/maintenance/archives/patch-change_tag-ct_id.sql b/maintenance/archives/patch-change_tag-ct_id.sql
new file mode 100644 (file)
index 0000000..7b986d6
--- /dev/null
@@ -0,0 +1,5 @@
+-- Primary key in change_tag table
+
+ALTER TABLE /*$wgDBprefix*/change_tag
+       ADD COLUMN ct_id INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
+       ADD PRIMARY KEY (ct_id);
diff --git a/maintenance/archives/patch-tag_summary-ts_id.sql b/maintenance/archives/patch-tag_summary-ts_id.sql
new file mode 100644 (file)
index 0000000..66fa72e
--- /dev/null
@@ -0,0 +1,5 @@
+-- Primary key in tag_summary table
+
+ALTER TABLE /*$wgDBprefix*/tag_summary
+       ADD COLUMN ts_id INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
+       ADD PRIMARY KEY (ts_id);
index cf5a19c..0beff7c 100644 (file)
@@ -32,7 +32,7 @@ require __DIR__ . '/../commandLine.inc';
 class UpdateLogging {
 
        /**
-        * @var DatabaseBase
+        * @var Database
         */
        public $dbw;
        public $batchSize = 1000;
index e9cdb58..2a8d79a 100644 (file)
@@ -69,7 +69,7 @@ class BenchmarkDeleteTruncate extends Benchmarker {
        }
 
        /**
-        * @param DatabaseBase $dbw
+        * @param Database $dbw
         * @return void
         */
        private function insertData( $dbw ) {
@@ -82,7 +82,7 @@ class BenchmarkDeleteTruncate extends Benchmarker {
        }
 
        /**
-        * @param DatabaseBase $dbw
+        * @param Database $dbw
         * @return void
         */
        private function delete( $dbw ) {
@@ -90,7 +90,7 @@ class BenchmarkDeleteTruncate extends Benchmarker {
        }
 
        /**
-        * @param DatabaseBase $dbw
+        * @param Database $dbw
         * @return void
         */
        private function truncate( $dbw ) {
index b8a246e..8672223 100644 (file)
@@ -147,7 +147,6 @@ TEXT
                        } else {
                                $where = [];
                        }
-                       $i = 0;
 
                        $this->output( "Removing empty categories without description pages...\n" );
                        while ( true ) {
index 14557f4..b8001a4 100644 (file)
@@ -74,7 +74,7 @@ class ConvertUserOptions extends Maintenance {
 
        /**
         * @param ResultWrapper $res
-        * @param DatabaseBase $dbw
+        * @param Database $dbw
         * @return null|int
         */
        function convertOptionBatch( $res, $dbw ) {
index 507a494..df496d4 100644 (file)
@@ -83,7 +83,7 @@ class DeleteOrphanedRevisions extends Maintenance {
         * Do this inside a transaction
         *
         * @param array $id Array of revision id values
-        * @param DatabaseBase $dbw DatabaseBase class (needs to be a master)
+        * @param Database $dbw Database class (needs to be a master)
         */
        private function deleteRevs( $id, &$dbw ) {
                if ( !is_array( $id ) ) {
index 31272bc..9f983c1 100644 (file)
@@ -117,7 +117,7 @@ abstract class DumpIterator extends Maintenance {
        /**
         * Callback function for each revision, child classes should override
         * processRevision instead.
-        * @param DatabaseBase $rev
+        * @param Database $rev
         */
        public function handleRevision( $rev ) {
                $title = $rev->getTitle();
index d0bda4e..d8661c1 100644 (file)
@@ -86,7 +86,7 @@ class TextPassDumper extends BackupDumper {
        protected $checkpointFiles = [];
 
        /**
-        * @var DatabaseBase
+        * @var Database
         */
        protected $db;
 
index 2ed1efa..9dee6e5 100644 (file)
@@ -71,7 +71,7 @@ class FetchText extends Maintenance {
 
        /**
         * May throw a database error if, say, the server dies during query.
-        * @param DatabaseBase $db
+        * @param Database $db
         * @param int $id The old_id
         * @return string
         */
index ac700ef..5a4ab39 100644 (file)
@@ -245,7 +245,8 @@ if ( $count > 0 ) {
                if ( isset( $options['dry'] ) ) {
                        echo " publishing {$file} by '" . $wgUser->getName() . "', comment '$commentText'... ";
                } else {
-                       $props = FSFile::getPropsFromPath( $file );
+                       $mwProps = new MWFileProps( MimeMagic::singleton() );
+                       $props = $mwProps->getPropsFromPath( $file, true );
                        $flags = 0;
                        $publishOptions = [];
                        $handler = MediaHandler::getHandler( $props['mime'] );
index 5531ffc..2894653 100644 (file)
@@ -103,16 +103,16 @@ class ImportTextFiles extends Maintenance {
                        $timestamp = $useTimestamp ? wfTimestamp( TS_UNIX, filemtime( $file ) ) : wfTimestampNow();
 
                        $title = Title::newFromText( $pageName );
-                       $exists = $title->exists();
-                       $oldRevID = $title->getLatestRevID();
-                       $oldRev = $oldRevID ? Revision::newFromId( $oldRevID ) : null;
-
-                       if ( !$title ) {
+                       // Have to check for # manually, since it gets interpreted as a fragment
+                       if ( !$title || $title->hasFragment() ) {
                                $this->error( "Invalid title $pageName. Skipping.\n" );
                                $skipCount++;
                                continue;
                        }
 
+                       $exists = $title->exists();
+                       $oldRevID = $title->getLatestRevID();
+                       $oldRev = $oldRevID ? Revision::newFromId( $oldRevID ) : null;
                        $actualTitle = $title->getPrefixedText();
 
                        if ( $exists ) {
index 6b06da7..96aea03 100644 (file)
@@ -42,8 +42,6 @@ in the load balancer, usually indicating a replication environment.' );
                $user = $dbw->tableName( 'user' );
                $revision = $dbw->tableName( 'revision' );
 
-               $dbver = $dbw->getServerVersion();
-
                // Autodetect mode...
                if ( $this->hasOption( 'background' ) ) {
                        $backgroundMode = true;
index b1e0fa4..fa8bf3b 100644 (file)
@@ -7,7 +7,6 @@ c2find|http://c2.com/cgi/wiki?FindPage&value=$1|0|
 cache|http://www.google.com/search?q=cache:$1|0|
 commons|https://commons.wikimedia.org/wiki/$1|0|https://commons.wikimedia.org/w/api.php
 dictionary|http://www.dict.org/bin/Dict?Database=*&Form=Dict1&Strategy=*&Query=$1|0|
-docbook|http://wiki.docbook.org/$1|0|
 doi|http://dx.doi.org/$1|0|
 drumcorpswiki|http://www.drumcorpswiki.com/$1|0|http://drumcorpswiki.com/api.php
 dwjwiki|http://www.suberic.net/cgi-bin/dwj/wiki.cgi?$1|0|
@@ -16,22 +15,18 @@ emacswiki|http://www.emacswiki.org/cgi-bin/wiki.pl?$1|0|
 foldoc|http://foldoc.org/?$1|0|
 foxwiki|http://fox.wikis.com/wc.dll?Wiki~$1|0|
 freebsdman|http://www.FreeBSD.org/cgi/man.cgi?apropos=1&query=$1|0|
-gej|http://www.esperanto.de/dej.malnova/aktivikio.pl?$1|0|
 gentoo-wiki|http://gentoo-wiki.com/$1|0|
 google|http://www.google.com/search?q=$1|0|
 googlegroups|http://groups.google.com/groups?q=$1|0|
 hammondwiki|http://www.dairiki.org/HammondWiki/$1|0|
 hrwiki|http://www.hrwiki.org/wiki/$1|0|http://www.hrwiki.org/w/api.php
 imdb|http://www.imdb.com/find?q=$1&tt=on|0|
-jargonfile|http://sunir.org/apps/meta.pl?wiki=JargonFile&redirect=$1|0|
 kmwiki|http://kmwiki.wikispaces.com/$1|0|
 linuxwiki|http://linuxwiki.de/$1|0|
 lojban|http://mw.lojban.org/papri/$1|0|
 lqwiki|http://wiki.linuxquestions.org/wiki/$1|0|
-lugkr|http://www.lug-kr.de/wiki/$1|0|
 meatball|http://www.usemod.com/cgi-bin/mb.pl?$1|0|
 mediawikiwiki|https://www.mediawiki.org/wiki/$1|0|https://www.mediawiki.org/w/api.php
-mediazilla|https://bugzilla.wikimedia.org/$1|0|
 memoryalpha|http://en.memory-alpha.org/wiki/$1|0|http://en.memory-alpha.org/api.php
 metawiki|http://sunir.org/apps/meta.pl?$1|0|
 metawikimedia|https://meta.wikimedia.org/wiki/$1|0|https://meta.wikimedia.org/w/api.php
@@ -39,25 +34,21 @@ mozillawiki|http://wiki.mozilla.org/$1|0|https://wiki.mozilla.org/api.php
 mw|https://www.mediawiki.org/wiki/$1|0|https://www.mediawiki.org/w/api.php
 oeis|http://oeis.org/$1|0|
 openwiki|http://openwiki.com/ow.asp?$1|0|
-ppr|http://c2.com/cgi/wiki?$1|0|
+pmid|https://www.ncbi.nlm.nih.gov/pubmed/$1?dopt=Abstract|0|
 pythoninfo|http://wiki.python.org/moin/$1|0|
-rfc|http://www.rfc-editor.org/rfc/rfc$1.txt|0|
+rfc|https://tools.ietf.org/html/rfc$1|0|
 s23wiki|http://s23.org/wiki/$1|0|http://s23.org/w/api.php
 seattlewireless|http://seattlewireless.net/$1|0|
 senseislibrary|http://senseis.xmp.net/?$1|0|
 shoutwiki|http://www.shoutwiki.com/wiki/$1|0|http://www.shoutwiki.com/w/api.php
-sourcewatch|http://www.sourcewatch.org/index.php?title=$1|0|http://www.sourcewatch.org/api.php
 squeak|http://wiki.squeak.org/squeak/$1|0|
-tejo|http://www.tejo.org/vikio/$1|0|
 tmbw|http://www.tmbw.net/wiki/$1|0|http://tmbw.net/wiki/api.php
 tmnet|http://www.technomanifestos.net/?$1|0|
 theopedia|http://www.theopedia.com/$1|0|
 twiki|http://twiki.org/cgi-bin/view/$1|0|
-uea|http://uea.org/vikio/index.php/$1|0|http://uea.org/vikio/api.php
 uncyclopedia|http://en.uncyclopedia.co/wiki/$1|0|http://en.uncyclopedia.co/w/api.php
 unreal|http://wiki.beyondunreal.com/$1|0|http://wiki.beyondunreal.com/w/api.php
 usemod|http://www.usemod.com/cgi-bin/wiki.pl?$1|0|
-webseitzwiki|http://webseitz.fluxent.com/wiki/$1|0|
 wiki|http://c2.com/cgi/wiki?$1|0|
 wikia|http://www.wikia.com/wiki/$1|0|
 wikibooks|https://en.wikibooks.org/wiki/$1|0|https://en.wikibooks.org/w/api.php
index b7d1a84..adb6cd1 100644 (file)
@@ -9,7 +9,6 @@ REPLACE INTO /*$wgDBprefix*/interwiki (iw_prefix,iw_url,iw_local,iw_api) VALUES
 ('cache','http://www.google.com/search?q=cache:$1',0,''),
 ('commons','https://commons.wikimedia.org/wiki/$1',0,'https://commons.wikimedia.org/w/api.php'),
 ('dictionary','http://www.dict.org/bin/Dict?Database=*&Form=Dict1&Strategy=*&Query=$1',0,''),
-('docbook','http://wiki.docbook.org/$1',0,''),
 ('doi','http://dx.doi.org/$1',0,''),
 ('drumcorpswiki','http://www.drumcorpswiki.com/$1',0,'http://drumcorpswiki.com/api.php'),
 ('dwjwiki','http://www.suberic.net/cgi-bin/dwj/wiki.cgi?$1',0,''),
@@ -18,22 +17,18 @@ REPLACE INTO /*$wgDBprefix*/interwiki (iw_prefix,iw_url,iw_local,iw_api) VALUES
 ('foldoc','http://foldoc.org/?$1',0,''),
 ('foxwiki','http://fox.wikis.com/wc.dll?Wiki~$1',0,''),
 ('freebsdman','http://www.FreeBSD.org/cgi/man.cgi?apropos=1&query=$1',0,''),
-('gej','http://www.esperanto.de/dej.malnova/aktivikio.pl?$1',0,''),
 ('gentoo-wiki','http://gentoo-wiki.com/$1',0,''),
 ('google','http://www.google.com/search?q=$1',0,''),
 ('googlegroups','http://groups.google.com/groups?q=$1',0,''),
 ('hammondwiki','http://www.dairiki.org/HammondWiki/$1',0,''),
 ('hrwiki','http://www.hrwiki.org/wiki/$1',0,'http://www.hrwiki.org/w/api.php'),
 ('imdb','http://www.imdb.com/find?q=$1&tt=on',0,''),
-('jargonfile','http://sunir.org/apps/meta.pl?wiki=JargonFile&redirect=$1',0,''),
 ('kmwiki','http://kmwiki.wikispaces.com/$1',0,''),
 ('linuxwiki','http://linuxwiki.de/$1',0,''),
 ('lojban','http://www.lojban.org/tiki/tiki-index.php?page=$1',0,''),
 ('lqwiki','http://wiki.linuxquestions.org/wiki/$1',0,''),
-('lugkr','http://www.lug-kr.de/wiki/$1',0,''),
 ('meatball','http://www.usemod.com/cgi-bin/mb.pl?$1',0,''),
 ('mediawikiwiki','https://www.mediawiki.org/wiki/$1',0,'https://www.mediawiki.org/w/api.php'),
-('mediazilla','https://bugzilla.wikimedia.org/$1',0,''),
 ('memoryalpha','http://en.memory-alpha.org/wiki/$1',0,'http://en.memory-alpha.org/api.php'),
 ('metawiki','http://sunir.org/apps/meta.pl?$1',0,''),
 ('metawikimedia','https://meta.wikimedia.org/wiki/$1',0,'https://meta.wikimedia.org/w/api.php'),
@@ -41,25 +36,21 @@ REPLACE INTO /*$wgDBprefix*/interwiki (iw_prefix,iw_url,iw_local,iw_api) VALUES
 ('mw','https://www.mediawiki.org/wiki/$1',0,'https://www.mediawiki.org/w/api.php'),
 ('oeis','http://oeis.org/$1',0,''),
 ('openwiki','http://openwiki.com/ow.asp?$1',0,''),
-('ppr','http://c2.com/cgi/wiki?$1',0,''),
+('pmid', 'https://www.ncbi.nlm.nih.gov/pubmed/$1?dopt=Abstract',0,''),
 ('pythoninfo','http://wiki.python.org/moin/$1',0,''),
-('rfc','http://www.rfc-editor.org/rfc/rfc$1.txt',0,''),
+('rfc','https://tools.ietf.org/html/rfc$1',0,''),
 ('s23wiki','http://s23.org/wiki/$1',0,'http://s23.org/w/api.php'),
 ('seattlewireless','http://seattlewireless.net/$1',0,''),
 ('senseislibrary','http://senseis.xmp.net/?$1',0,''),
 ('shoutwiki','http://www.shoutwiki.com/wiki/$1',0,'http://www.shoutwiki.com/w/api.php'),
-('sourcewatch','http://www.sourcewatch.org/index.php?title=$1',0,'http://www.sourcewatch.org/api.php'),
 ('squeak','http://wiki.squeak.org/squeak/$1',0,''),
-('tejo','http://www.tejo.org/vikio/$1',0,''),
 ('tmbw','http://www.tmbw.net/wiki/$1',0,'http://tmbw.net/wiki/api.php'),
 ('tmnet','http://www.technomanifestos.net/?$1',0,''),
 ('theopedia','http://www.theopedia.com/$1',0,''),
 ('twiki','http://twiki.org/cgi-bin/view/$1',0,''),
-('uea','http://uea.org/vikio/index.php/$1',0,'http://uea.org/vikio/api.php'),
 ('uncyclopedia','http://en.uncyclopedia.co/wiki/$1',0,'http://en.uncyclopedia.co/w/api.php'),
 ('unreal','http://wiki.beyondunreal.com/$1',0,'http://wiki.beyondunreal.com/w/api.php'),
 ('usemod','http://www.usemod.com/cgi-bin/wiki.pl?$1',0,''),
-('webseitzwiki','http://webseitz.fluxent.com/wiki/$1',0,''),
 ('wiki','http://c2.com/cgi/wiki?$1',0,''),
 ('wikia','http://www.wikia.com/wiki/$1',0,''),
 ('wikibooks','https://en.wikibooks.org/wiki/$1',0,'https://en.wikibooks.org/w/api.php'),
index 33008d1..21cb658 100644 (file)
@@ -30,23 +30,6 @@ class CommonTag < JsDuck::Tag::Tag
   end
 end
 
-class SourceTag < CommonTag
-  def initialize
-    @tagname = :source
-    @pattern = 'source'
-    super
-  end
-
-  def to_html(context)
-    context[@tagname].map do |source|
-      <<-EOHTML
-        <h3 class='pa'>Source</h3>
-        #{source[:doc]}
-      EOHTML
-    end.join
-  end
-end
-
 class SeeTag < CommonTag
   def initialize
     @tagname = :see
index c901240..1613111 100644 (file)
@@ -1,43 +1,43 @@
 /**
+ * Source: <https://api.jquery.com/>
  * @class jQuery
- * @source <http://api.jquery.com/>
  */
 
 /**
+ * Source: <https://api.jquery.com/jQuery.ajax/>
  * @method ajax
  * @static
- * @source <http://api.jquery.com/jQuery.ajax/>
  * @return {jqXHR}
  */
 
 /**
+ * Source: <https://api.jquery.com/Types/#Event>
  * @class jQuery.Event
- * @source <http://api.jquery.com/Types/#Event>
  */
 
 /**
+ * Source: <https://api.jquery.com/jQuery.Callbacks/>
  * @class jQuery.Callbacks
- * @source <http://api.jquery.com/jQuery.Callbacks/>
  */
 
 /**
+ * Source: <https://api.jquery.com/Types/#Promise>
  * @class jQuery.Promise
- * @source <http://api.jquery.com/Types/#Promise>
  */
 
 /**
+ * Source: <https://api.jquery.com/jQuery.Deferred/>
  * @class jQuery.Deferred
  * @mixins jQuery.Promise
- * @source <http://api.jquery.com/jQuery.Deferred/>
  */
 
 /**
+ * Source: <https://api.jquery.com/Types/#jqXHR>
  * @class jQuery.jqXHR
- * @source <http://api.jquery.com/Types/#jqXHR>
  * @alternateClassName jqXHR
  */
 
 /**
+ * Source: <http://api.qunitjs.com/>
  * @class QUnit
- * @source <http://api.qunitjs.com/>
  */
index 9d92794..fa2bd54 100644 (file)
@@ -48,7 +48,6 @@ class DatabaseLag extends Maintenance {
                        echo "\n";
 
                        while ( 1 ) {
-                               $lb->clearLagTimeCache();
                                $lags = $lb->getLagTimes();
                                unset( $lags[0] );
                                echo gmdate( 'H:i:s' ) . ' ';
index baa9c71..baf917c 100644 (file)
@@ -96,7 +96,7 @@ if ( $run ) {
 
                if ( !strcmp( $runMode, 'php' ) ) {
                        print "<?php\n";
-                       print '$dupeMessages = array(' . "\n";
+                       print '$dupeMessages = [' . "\n";
                }
                foreach ( $wgMessages[$langCodeC] as $key => $value ) {
                        foreach ( $wgMessages[$langCode] as $ckey => $cvalue ) {
@@ -118,7 +118,7 @@ if ( $run ) {
                        }
                }
                if ( !strcmp( $runMode, 'php' ) ) {
-                       print ");\n";
+                       print "];\n";
                }
                if ( !strcmp( $runMode, 'text' ) ) {
                        if ( $count == 1 ) {
index 46616db..5bdc8a7 100644 (file)
@@ -55,12 +55,12 @@ class Digit2Html extends Maintenance {
                                continue;
                        }
 
-                       $this->output( "OK\n\$digitTransformTable = array(\n" );
+                       $this->output( "OK\n\$digitTransformTable = [\n" );
                        foreach ( $digitTransformTable as $latin => $translation ) {
                                $htmlent = utf8ToHexSequence( $translation );
                                $this->output( "'$latin' => '$translation', # &#x$htmlent;\n" );
                        }
-                       $this->output( ");\n" );
+                       $this->output( "];\n" );
                }
        }
 }
index bd73f8b..f771fff 100644 (file)
@@ -104,7 +104,7 @@ class MigrateFileRepoLayout extends Maintenance {
                                        $status = $be->prepare( [
                                                'dir' => dirname( $dpath ), 'bypassReadOnly' => 1 ] );
                                        if ( !$status->isOK() ) {
-                                               $this->error( print_r( $status->getErrorsArray(), true ) );
+                                               $this->error( print_r( $status->getErrors(), true ) );
                                        }
 
                                        $batch[] = [ 'op' => 'copy', 'overwrite' => true,
@@ -137,7 +137,7 @@ class MigrateFileRepoLayout extends Maintenance {
                                        $status = $be->prepare( [
                                                'dir' => dirname( $dpath ), 'bypassReadOnly' => 1 ] );
                                        if ( !$status->isOK() ) {
-                                               $this->error( print_r( $status->getErrorsArray(), true ) );
+                                               $this->error( print_r( $status->getErrors(), true ) );
                                        }
                                        $batch[] = [ 'op' => 'copy', 'overwrite' => true,
                                                'src' => $spath, 'dst' => $dpath, 'img' => $ofile->getArchiveName() ];
@@ -195,7 +195,7 @@ class MigrateFileRepoLayout extends Maintenance {
                                $status = $be->prepare( [
                                        'dir' => dirname( $dpath ), 'bypassReadOnly' => 1 ] );
                                if ( !$status->isOK() ) {
-                                       $this->error( print_r( $status->getErrorsArray(), true ) );
+                                       $this->error( print_r( $status->getErrors(), true ) );
                                }
 
                                $batch[] = [ 'op' => 'copy', 'src' => $spath, 'dst' => $dpath,
@@ -227,7 +227,7 @@ class MigrateFileRepoLayout extends Maintenance {
 
                $status = $be->doOperations( $ops, [ 'bypassReadOnly' => 1 ] );
                if ( !$status->isOK() ) {
-                       $this->output( print_r( $status->getErrorsArray(), true ) );
+                       $this->output( print_r( $status->getErrors(), true ) );
                }
 
                $this->output( "Batch done\n\n" );
diff --git a/maintenance/mssql/archives/patch-change_tag-ct_id.sql b/maintenance/mssql/archives/patch-change_tag-ct_id.sql
new file mode 100644 (file)
index 0000000..94cb9d1
--- /dev/null
@@ -0,0 +1,4 @@
+-- Primary key in change_tag table
+
+ALTER TABLE /*_*/change_tag ADD ct_id INT IDENTITY;
+ALTER TABLE /*_*/change_tag ADD CONSTRAINT pk_change_tag PRIMARY KEY(ct_id)
diff --git a/maintenance/mssql/archives/patch-tag_summary-ts_id.sql b/maintenance/mssql/archives/patch-tag_summary-ts_id.sql
new file mode 100644 (file)
index 0000000..d62bd35
--- /dev/null
@@ -0,0 +1,4 @@
+-- Primary key in tag_summary table
+
+ALTER TABLE /*_*/tag_summary ADD ts_id INT IDENTITY;
+ALTER TABLE /*_*/tag_summary ADD CONSTRAINT pk_tag_summary PRIMARY KEY(ts_id)
index ea087a6..beb9727 100644 (file)
@@ -1193,6 +1193,7 @@ CREATE TABLE /*_*/updatelog (
 
 -- A table to track tags for revisions, logs and recent changes.
 CREATE TABLE /*_*/change_tag (
+  ct_id int NOT NULL PRIMARY KEY IDENTITY,
   -- RCID for the change
   ct_rc_id int NULL REFERENCES /*_*/recentchanges(rc_id),
   -- LOGID for the change
@@ -1215,6 +1216,7 @@ CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_i
 -- Rollup table to pull a LIST of tags simply without ugly GROUP_CONCAT
 -- that only works on MySQL 4.1+
 CREATE TABLE /*_*/tag_summary (
+  ts_id int NOT NULL PRIMARY KEY IDENTITY,
   -- RCID for the change
   ts_rc_id int NULL REFERENCES /*_*/recentchanges(rc_id),
   -- LOGID for the change
index 506bc9c..b705500 100644 (file)
@@ -37,7 +37,7 @@ require_once __DIR__ . '/Maintenance.php';
 class NamespaceConflictChecker extends Maintenance {
 
        /**
-        * @var DatabaseBase
+        * @var Database
         */
        protected $db;
 
diff --git a/maintenance/oracle/archives/patch-change_tag-ct_id.sql b/maintenance/oracle/archives/patch-change_tag-ct_id.sql
new file mode 100644 (file)
index 0000000..6672872
--- /dev/null
@@ -0,0 +1,6 @@
+define mw_prefix='{$wgDBprefix}';
+
+ALTER TABLE &mw_prefix.change_tag ADD (
+ct_id NUMBER NOT NULL,
+);
+ALTER TABLE &mw_prefix.change_tag ADD CONSTRAINT &mw_prefix.change_tag_pk PRIMARY KEY (ct_id);
diff --git a/maintenance/oracle/archives/patch-tag_summary-ts_id.sql b/maintenance/oracle/archives/patch-tag_summary-ts_id.sql
new file mode 100644 (file)
index 0000000..91c3338
--- /dev/null
@@ -0,0 +1,6 @@
+define mw_prefix='{$wgDBprefix}';
+
+ALTER TABLE &mw_prefix.tag_summary ADD (
+ts_id NUMBER NOT NULL,
+);
+ALTER TABLE &mw_prefix.tag_summary ADD CONSTRAINT &mw_prefix.tag_summary_pk PRIMARY KEY (ts_id);
index d9369c9..616b401 100644 (file)
@@ -616,23 +616,27 @@ CREATE TABLE &mw_prefix.updatelog (
 ALTER TABLE &mw_prefix.updatelog ADD CONSTRAINT &mw_prefix.updatelog_pk PRIMARY KEY (ul_key);
 
 CREATE TABLE &mw_prefix.change_tag (
+  ct_id NUMBER NOT NULL,
   ct_rc_id NUMBER NULL,
   ct_log_id NUMBER NULL,
   ct_rev_id NUMBER NULL,
   ct_tag VARCHAR2(255) NOT NULL,
   ct_params BLOB NULL
 );
+ALTER TABLE &mw_prefix.change_tag ADD CONSTRAINT &mw_prefix.change_tag_pk PRIMARY KEY (ct_id);
 CREATE UNIQUE INDEX &mw_prefix.change_tag_u01 ON &mw_prefix.change_tag (ct_rc_id,ct_tag);
 CREATE UNIQUE INDEX &mw_prefix.change_tag_u02 ON &mw_prefix.change_tag (ct_log_id,ct_tag);
 CREATE UNIQUE INDEX &mw_prefix.change_tag_u03 ON &mw_prefix.change_tag (ct_rev_id,ct_tag);
 CREATE INDEX &mw_prefix.change_tag_i01 ON &mw_prefix.change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id);
 
 CREATE TABLE &mw_prefix.tag_summary (
+  ts_id NUMBER NOT NULL,
   ts_rc_id NUMBER NULL,
   ts_log_id NUMBER NULL,
   ts_rev_id NUMBER NULL,
   ts_tags BLOB NOT NULL
 );
+ALTER TABLE &mw_prefix.tag_summary ADD CONSTRAINT &mw_prefix.tag_summary_pk PRIMARY KEY (ts_id);
 CREATE UNIQUE INDEX &mw_prefix.tag_summary_u01 ON &mw_prefix.tag_summary (ts_rc_id);
 CREATE UNIQUE INDEX &mw_prefix.tag_summary_u02 ON &mw_prefix.tag_summary (ts_log_id);
 CREATE UNIQUE INDEX &mw_prefix.tag_summary_u03 ON &mw_prefix.tag_summary (ts_rev_id);
index 7b8f2cd..e4f3e91 100644 (file)
@@ -56,7 +56,7 @@ class Orphans extends Maintenance {
 
        /**
         * Lock the appropriate tables for the script
-        * @param DatabaseBase $db
+        * @param Database $db
         * @param string $extraTable The name of any extra tables to lock (eg: text)
         */
        private function lockTables( $db, $extraTable = [] ) {
index 43fbd38..bc21140 100644 (file)
@@ -45,11 +45,13 @@ class PatchSql extends Maintenance {
 
        public function execute() {
                $dbw = $this->getDB( DB_MASTER );
+               $updater = DatabaseUpdater::newForDB( $dbw, true, $this );
+
                foreach ( $this->mArgs as $arg ) {
                        $files = [
                                $arg,
-                               $dbw->patchPath( $arg ),
-                               $dbw->patchPath( "patch-$arg.sql" ),
+                               $updater->patchPath( $dbw, $arg ),
+                               $updater->patchPath( $dbw, "patch-$arg.sql" ),
                        ];
                        foreach ( $files as $file ) {
                                if ( file_exists( $file ) ) {
index 401ef12..c6bd794 100644 (file)
@@ -57,7 +57,7 @@ class PopulateContentModel extends Maintenance {
                }
        }
 
-       private function updatePageRows( DatabaseBase $dbw, $pageIds, $model ) {
+       private function updatePageRows( Database $dbw, $pageIds, $model ) {
                $count = count( $pageIds );
                $this->output( "Setting $count rows to $model..." );
                $dbw->update(
@@ -70,7 +70,7 @@ class PopulateContentModel extends Maintenance {
                $this->output( "done.\n" );
        }
 
-       protected function populatePage( DatabaseBase $dbw, $ns ) {
+       protected function populatePage( Database $dbw, $ns ) {
                $toSave = [];
                $lastId = 0;
                $nsCondition = $ns === 'all' ? [] : [ 'page_namespace' => $ns ];
@@ -102,7 +102,7 @@ class PopulateContentModel extends Maintenance {
                }
        }
 
-       private function updateRevisionOrArchiveRows( DatabaseBase $dbw, $ids, $model, $table ) {
+       private function updateRevisionOrArchiveRows( Database $dbw, $ids, $model, $table ) {
                $prefix = $table === 'archive' ? 'ar' : 'rev';
                $model_column = "{$prefix}_content_model";
                $format_column = "{$prefix}_content_format";
@@ -120,7 +120,7 @@ class PopulateContentModel extends Maintenance {
                $this->output( "done.\n" );
        }
 
-       protected function populateRevisionOrArchive( DatabaseBase $dbw, $table, $ns ) {
+       protected function populateRevisionOrArchive( Database $dbw, $table, $ns ) {
                $prefix = $table === 'archive' ? 'ar' : 'rev';
                $model_column = "{$prefix}_content_model";
                $format_column = "{$prefix}_content_format";
index 05098ac..ac87cf3 100644 (file)
@@ -83,7 +83,7 @@ class PopulateRecentChangesSource extends LoggedUpdateMaintenance {
                return __CLASS__;
        }
 
-       protected function buildUpdateCondition( DatabaseBase $dbw ) {
+       protected function buildUpdateCondition( Database $dbw ) {
                $rcNew = $dbw->addQuotes( RC_NEW );
                $rcSrcNew = $dbw->addQuotes( RecentChange::SRC_NEW );
                $rcEdit = $dbw->addQuotes( RC_EDIT );
index 95c87c0..2273761 100644 (file)
@@ -25,6 +25,8 @@ DROP SEQUENCE IF EXISTS category_cat_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS archive_ar_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS externallinks_el_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS sites_site_id_seq CASCADE;
+DROP SEQUENCE IF EXISTS change_tag_ct_id_seq CASCADE;
+DROP SEQUENCE IF EXISTS tag_summary_ts_id_seq CASCADE;
 DROP FUNCTION IF EXISTS page_deleted() CASCADE;
 DROP FUNCTION IF EXISTS ts2_page_title() CASCADE;
 DROP FUNCTION IF EXISTS ts2_page_text() CASCADE;
@@ -653,7 +655,9 @@ CREATE TABLE category (
 CREATE UNIQUE INDEX category_title ON category(cat_title);
 CREATE INDEX category_pages ON category(cat_pages);
 
+CREATE SEQUENCE change_tag_ct_id_seq;
 CREATE TABLE change_tag (
+  ct_id      INTEGER  NOT NULL  PRIMARY KEY DEFAULT nextval('change_tag_ct_id_seq'),
   ct_rc_id   INTEGER      NULL,
   ct_log_id  INTEGER      NULL,
   ct_rev_id  INTEGER      NULL,
@@ -665,11 +669,13 @@ CREATE UNIQUE INDEX change_tag_log_tag ON change_tag(ct_log_id,ct_tag);
 CREATE UNIQUE INDEX change_tag_rev_tag ON change_tag(ct_rev_id,ct_tag);
 CREATE INDEX change_tag_tag_id ON change_tag(ct_tag,ct_rc_id,ct_rev_id,ct_log_id);
 
+CREATE SEQUENCE tag_summary_ts_id_seq;
 CREATE TABLE tag_summary (
-  ts_rc_id   INTEGER     NULL,
-  ts_log_id  INTEGER     NULL,
-  ts_rev_id  INTEGER     NULL,
-  ts_tags    TEXT    NOT NULL
+  ts_id      INTEGER  NOT NULL  PRIMARY KEY DEFAULT nextval('tag_summary_ts_id_seq'),
+  ts_rc_id   INTEGER      NULL,
+  ts_log_id  INTEGER      NULL,
+  ts_rev_id  INTEGER      NULL,
+  ts_tags    TEXT     NOT NULL
 );
 CREATE UNIQUE INDEX tag_summary_rc_id ON tag_summary(ts_rc_id);
 CREATE UNIQUE INDEX tag_summary_log_id ON tag_summary(ts_log_id);
index d102e08..2503ed2 100644 (file)
@@ -43,7 +43,7 @@ class PPFuzzTester {
        public $minLength = 0;
        public $maxLength = 20;
        public $maxTemplates = 5;
-       // public $outputTypes = array( 'OT_HTML', 'OT_WIKI', 'OT_PREPROCESS' );
+       // public $outputTypes = [ 'OT_HTML', 'OT_WIKI', 'OT_PREPROCESS' ];
        public $entryPoints = [ 'testSrvus', 'testPst', 'testPreprocess' ];
        public $verbose = false;
 
index b278e98..da8a6bc 100644 (file)
@@ -29,6 +29,8 @@ require_once __DIR__ . '/Maintenance.php';
  * @ingroup Maintenance
  */
 class RebuildFileCache extends Maintenance {
+       private $enabled = true;
+
        public function __construct() {
                parent::__construct();
                $this->addDescription( 'Build file cache for content pages' );
@@ -39,23 +41,27 @@ class RebuildFileCache extends Maintenance {
        }
 
        public function finalSetup() {
-               global $wgDebugToolbar;
+               global $wgDebugToolbar, $wgUseFileCache, $wgReadOnly;
 
+               $this->enabled = $wgUseFileCache;
+               // Script will handle capturing output and saving it itself
+               $wgUseFileCache = false;
                // Debug toolbar makes content uncacheable so we disable it.
                // Has to be done before Setup.php initialize MWDebug
                $wgDebugToolbar = false;
+               //  Avoid DB writes (like enotif/counters)
+               $wgReadOnly = 'Building cache'; // avoid DB writes (like enotif/counters)
+
                parent::finalSetup();
        }
 
        public function execute() {
-               global $wgUseFileCache, $wgReadOnly, $wgRequestTime;
-               global $wgOut;
-               if ( !$wgUseFileCache ) {
+               global $wgRequestTime;
+
+               if ( !$this->enabled ) {
                        $this->error( "Nothing to do -- \$wgUseFileCache is disabled.", true );
                }
 
-               $wgReadOnly = 'Building cache'; // avoid DB writes (like enotif/counters)
-
                $start = $this->getOption( 'start', "0" );
                if ( !ctype_digit( $start ) ) {
                        $this->error( "Invalid value for start parameter.", true );
@@ -104,7 +110,6 @@ class RebuildFileCache extends Maintenance {
                        $this->beginTransaction( $dbw, __METHOD__ ); // for any changes
                        foreach ( $res as $row ) {
                                $rebuilt = false;
-                               $wgRequestTime = microtime( true ); # bug 22852
 
                                $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
                                if ( null == $title ) {
@@ -112,39 +117,52 @@ class RebuildFileCache extends Maintenance {
                                        continue; // broken title?
                                }
 
-                               $context = new RequestContext;
+                               $context = new RequestContext();
                                $context->setTitle( $title );
                                $article = Article::newFromTitle( $title, $context );
                                $context->setWikiPage( $article->getPage() );
 
-                               $wgOut = $context->getOutput(); // set display title
-
                                // If the article is cacheable, then load it
-                               if ( $article->isFileCacheable() ) {
-                                       $cache = new HTMLFileCache( $title, 'view' );
-                                       if ( $cache->isCacheGood() ) {
+                               if ( $article->isFileCacheable( HTMLFileCache::MODE_REBUILD ) ) {
+                                       $viewCache = new HTMLFileCache( $title, 'view' );
+                                       $historyCache = new HTMLFileCache( $title, 'history' );
+                                       if ( $viewCache->isCacheGood() && $historyCache->isCacheGood() ) {
                                                if ( $overwrite ) {
                                                        $rebuilt = true;
                                                } else {
-                                                       $this->output( "Page {$row->page_id} already cached\n" );
+                                                       $this->output( "Page '$title' (id {$row->page_id}) already cached\n" );
                                                        continue; // done already!
                                                }
                                        }
-                                       ob_start( [ &$cache, 'saveToFileCache' ] ); // save on ob_end_clean()
-                                       $wgUseFileCache = false; // hack, we don't want $article fiddling with filecache
-                                       $article->view();
+
                                        MediaWiki\suppressWarnings(); // header notices
-                                       $wgOut->output();
+                                       // Cache ?action=view
+                                       $wgRequestTime = microtime( true ); # bug 22852
+                                       ob_start();
+                                       $article->view();
+                                       $context->getOutput()->output();
+                                       $context->getOutput()->clearHTML();
+                                       $viewHtml = ob_get_clean();
+                                       $viewCache->saveToFileCache( $viewHtml );
+                                       // Cache ?action=history
+                                       $wgRequestTime = microtime( true ); # bug 22852
+                                       ob_start();
+                                       Action::factory( 'history', $article, $context )->show();
+                                       $context->getOutput()->output();
+                                       $context->getOutput()->clearHTML();
+                                       $historyHtml = ob_get_clean();
+                                       $historyCache->saveToFileCache( $historyHtml );
                                        MediaWiki\restoreWarnings();
-                                       $wgUseFileCache = true;
-                                       ob_end_clean(); // clear buffer
+
                                        if ( $rebuilt ) {
-                                               $this->output( "Re-cached page {$row->page_id}\n" );
+                                               $this->output( "Re-cached page '$title' (id {$row->page_id})..." );
                                        } else {
-                                               $this->output( "Cached page {$row->page_id}\n" );
+                                               $this->output( "Cached page '$title' (id {$row->page_id})..." );
                                        }
+                                       $this->output( "[view: " . strlen( $viewHtml ) . " bytes; " .
+                                               "history: " . strlen( $historyHtml ) . " bytes]\n" );
                                } else {
-                                       $this->output( "Page {$row->page_id} not cacheable\n" );
+                                       $this->output( "Page '$title' (id {$row->page_id}) not cacheable\n" );
                                }
                        }
                        $this->commitTransaction( $dbw, __METHOD__ ); // commit any changes (just for sanity)
index 3157186..6aa1f37 100644 (file)
@@ -40,7 +40,7 @@ require_once __DIR__ . '/Maintenance.php';
 class ImageBuilder extends Maintenance {
 
        /**
-        * @var DatabaseBase
+        * @var Database
         */
        protected $dbw;
 
index 6465bb3..458dacf 100644 (file)
@@ -304,6 +304,8 @@ class RebuildRecentchanges extends Maintenance {
                        ]
                );
 
+               $field = $dbw->fieldInfo( 'recentchanges', 'rc_cur_id' );
+
                $inserted = 0;
                foreach ( $res as $row ) {
                        $dbw->insert(
@@ -323,7 +325,7 @@ class RebuildRecentchanges extends Maintenance {
                                        'rc_last_oldid' => 0,
                                        'rc_type' => RC_LOG,
                                        'rc_source' => $dbw->addQuotes( RecentChange::SRC_LOG ),
-                                       'rc_cur_id' => $dbw->cascadingDeletes()
+                                       'rc_cur_id' => $field->isNullable()
                                                ? $row->page_id
                                                : (int)$row->page_id, // NULL => 0,
                                        'rc_log_type' => $row->log_type,
index ec99d84..37636c8 100644 (file)
@@ -36,7 +36,7 @@ class RebuildTextIndex extends Maintenance {
        const RTI_CHUNK_SIZE = 500;
 
        /**
-        * @var DatabaseBase
+        * @var Database
         */
        private $db;
 
index aea966f..df4ce56 100644 (file)
@@ -37,7 +37,7 @@ require_once __DIR__ . '/Maintenance.php';
 class RefreshImageMetadata extends Maintenance {
 
        /**
-        * @var DatabaseBase
+        * @var Database
         */
        protected $dbw;
 
@@ -197,7 +197,7 @@ class RefreshImageMetadata extends Maintenance {
        }
 
        /**
-        * @param DatabaseBase $dbw
+        * @param Database $dbw
         * @return array
         */
        function getConditions( $dbw ) {
index 106be1f..e7a4d06 100644 (file)
@@ -90,7 +90,7 @@ class RefreshLinks extends Maintenance {
                $end = null, $redirectsOnly = false, $oldRedirectsOnly = false
        ) {
                $reportingInterval = 100;
-               $dbr = $this->getDB( DB_REPLICA );
+               $dbr = $this->getDB( DB_REPLICA, [ 'vslow' ] );
 
                if ( $start === null ) {
                        $start = 1;
@@ -282,7 +282,7 @@ class RefreshLinks extends Maintenance {
        ) {
                wfWaitForSlaves();
                $this->output( "Deleting illegal entries from the links tables...\n" );
-               $dbr = $this->getDB( DB_REPLICA );
+               $dbr = $this->getDB( DB_REPLICA, [ 'vslow' ] );
                do {
                        // Find the start of the next chunk. This is based only
                        // on existent page_ids.
@@ -324,7 +324,7 @@ class RefreshLinks extends Maintenance {
         */
        private function dfnCheckInterval( $start = null, $end = null, $batchSize = 100 ) {
                $dbw = $this->getDB( DB_MASTER );
-               $dbr = $this->getDB( DB_REPLICA );
+               $dbr = $this->getDB( DB_REPLICA, [ 'vslow' ] );
 
                $linksTables = [ // table name => page_id field
                        'pagelinks' => 'pl_from',
index e6a30a3..cc976ed 100644 (file)
@@ -44,6 +44,8 @@ class MwSql extends Maintenance {
        }
 
        public function execute() {
+               global $IP;
+
                // We wan't to allow "" for the wikidb, meaning don't call select_db()
                $wiki = $this->hasOption( 'wikidb' ) ? $this->getOption( 'wikidb' ) : false;
                // Get the appropriate load balancer (for this wiki)
@@ -66,24 +68,30 @@ class MwSql extends Maintenance {
                                }
                        }
                        if ( $index === null ) {
-                               $this->error( "No replica DB server configured with the name '$server'.", 1 );
+                               $this->error( "No replica DB server configured with the name '$replicaDB'.", 1 );
                        }
                } else {
                        $index = DB_MASTER;
                }
-               // Get a DB handle (with this wiki's DB selected) from the appropriate load balancer
+
+               /** @var Database $db DB handle for the appropriate cluster/wiki */
                $db = $lb->getConnection( $index, [], $wiki );
                if ( $replicaDB != '' && $db->getLBInfo( 'master' ) !== null ) {
                        $this->error( "The server selected ({$db->getServer()}) is not a replica DB.", 1 );
                }
 
+               if ( $index === DB_MASTER ) {
+                       $updater = DatabaseUpdater::newForDB( $db, true, $this );
+                       $db->setSchemaVars( $updater->getSchemaVars() );
+               }
+
                if ( $this->hasArg( 0 ) ) {
                        $file = fopen( $this->getArg( 0 ), 'r' );
                        if ( !$file ) {
                                $this->error( "Unable to open input file", true );
                        }
 
-                       $error = $db->sourceStream( $file, false, [ $this, 'sqlPrintResult' ] );
+                       $error = $db->sourceStream( $file, null, [ $this, 'sqlPrintResult' ] );
                        if ( $error !== true ) {
                                $this->error( $error, true );
                        } else {
@@ -98,14 +106,15 @@ class MwSql extends Maintenance {
                        return;
                }
 
-               $useReadline = function_exists( 'readline_add_history' )
-                       && Maintenance::posix_isatty( 0 /*STDIN*/ );
-
-               if ( $useReadline ) {
-                       global $IP;
+               if (
+                       function_exists( 'readline_add_history' ) &&
+                       Maintenance::posix_isatty( 0 /*STDIN*/ )
+               ) {
                        $historyFile = isset( $_ENV['HOME'] ) ?
                                "{$_ENV['HOME']}/.mwsql_history" : "$IP/maintenance/.mwsql_history";
                        readline_read_history( $historyFile );
+               } else {
+                       $historyFile = null;
                }
 
                $wholeLine = '';
@@ -126,10 +135,10 @@ class MwSql extends Maintenance {
                                $prompt = '    -> ';
                                continue;
                        }
-                       if ( $useReadline ) {
+                       if ( $historyFile ) {
                                # Delimiter is eated by streamStatementEnd, we add it
                                # up in the history (bug 37020)
-                               readline_add_history( $wholeLine . $db->getDelimiter() );
+                               readline_add_history( $wholeLine . ';' );
                                readline_write_history( $historyFile );
                        }
                        $this->sqlDoQuery( $db, $wholeLine, $doDie );
@@ -139,7 +148,7 @@ class MwSql extends Maintenance {
                wfWaitForSlaves();
        }
 
-       protected function sqlDoQuery( $db, $line, $dieOnError ) {
+       protected function sqlDoQuery( IDatabase $db, $line, $dieOnError ) {
                try {
                        $res = $db->query( $line );
                        $this->sqlPrintResult( $res, $db );
@@ -151,7 +160,7 @@ class MwSql extends Maintenance {
        /**
         * Print the results, callback for $db->sourceStream()
         * @param ResultWrapper $res The results object
-        * @param DatabaseBase $db
+        * @param IDatabase $db
         */
        public function sqlPrintResult( $res, $db ) {
                if ( !$res ) {
diff --git a/maintenance/sqlite/archives/patch-change_tag-ct_id.sql b/maintenance/sqlite/archives/patch-change_tag-ct_id.sql
new file mode 100644 (file)
index 0000000..1c01094
--- /dev/null
@@ -0,0 +1,25 @@
+DROP TABLE IF EXISTS /*_*/change_tag_tmp;
+
+CREATE TABLE /*$wgDBprefix*/change_tag_tmp (
+  ct_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  ct_rc_id int NULL,
+  ct_log_id int NULL,
+  ct_rev_id int NULL,
+  ct_tag varchar(255) NOT NULL,
+  ct_params blob NULL
+);
+
+INSERT OR IGNORE INTO /*_*/change_tag_tmp (
+    ct_rc_id, ct_log_id, ct_rev_id, ct_tag, ct_params )
+    SELECT
+    ct_rc_id, ct_log_id, ct_rev_id, ct_tag, ct_params
+    FROM /*_*/change_tag;
+
+DROP TABLE /*_*/change_tag;
+
+ALTER TABLE /*_*/change_tag_tmp RENAME TO /*_*/change_tag;
+
+CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag);
+CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag);
+CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag);
+CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id);
diff --git a/maintenance/sqlite/archives/patch-tag_summary-ts_id.sql b/maintenance/sqlite/archives/patch-tag_summary-ts_id.sql
new file mode 100644 (file)
index 0000000..b6a1202
--- /dev/null
@@ -0,0 +1,23 @@
+DROP TABLE IF EXISTS /*_*/tag_summary_tmp;
+
+CREATE TABLE /*$wgDBprefix*/tag_summary_tmp (
+  ts_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  ts_rc_id int NULL,
+  ts_log_id int NULL,
+  ts_rev_id int NULL,
+  ts_tags blob NOT NULL
+);
+
+INSERT OR IGNORE INTO /*_*/tag_summary_tmp (
+    ts_rc_id, ts_log_id, ts_rev_id, ts_tags )
+    SELECT
+    ts_rc_id, ts_log_id, ts_rev_id, ts_tags
+    FROM /*_*/tag_summary;
+
+DROP TABLE /*_*/tag_summary;
+
+ALTER TABLE /*_*/tag_summary_tmp RENAME TO /*_*/tag_summary;
+
+CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id);
+CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id);
+CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id);
index 289d22d..28be6a3 100644 (file)
@@ -384,7 +384,7 @@ class CompressOld extends Maintenance {
 
                                        if ( $text === false ) {
                                                $this->error( "\nError, unable to get text in old_id $oldid" );
-                                               # $dbw->delete( 'old', array( 'old_id' => $oldid ) );
+                                               # $dbw->delete( 'old', [ 'old_id' => $oldid ] );
                                        }
 
                                        if ( $extdb == "" && $j == 0 ) {
index a0efcb8..c5dd53b 100644 (file)
@@ -640,7 +640,7 @@ class RecompressTracked {
        /**
         * Gets a DB master connection for the given external cluster name
         * @param string $cluster
-        * @return DatabaseBase
+        * @return Database
         */
        function getExtDB( $cluster ) {
                $lb = wfGetLBFactory()->getExternalLB( $cluster );
index b5c14e3..03ce508 100644 (file)
@@ -1472,6 +1472,7 @@ CREATE TABLE /*_*/updatelog (
 
 -- A table to track tags for revisions, logs and recent changes.
 CREATE TABLE /*_*/change_tag (
+  ct_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
   -- RCID for the change
   ct_rc_id int NULL,
   -- LOGID for the change
@@ -1494,6 +1495,7 @@ CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_i
 -- Rollup table to pull a LIST of tags simply without ugly GROUP_CONCAT
 -- that only works on MySQL 4.1+
 CREATE TABLE /*_*/tag_summary (
+  ts_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
   -- RCID for the change
   ts_rc_id int NULL,
   -- LOGID for the change
index e754e3c..e70a176 100644 (file)
@@ -242,7 +242,7 @@ TEXT
         * Return an SQL expression selecting rows which sort above the given row,
         * assuming an ordering of cl_collation, cl_to, cl_type, cl_from
         * @param stdClass $row
-        * @param DatabaseBase $dbw
+        * @param Database $dbw
         * @return string
         */
        function getBatchCondition( $row, $dbw ) {
index bd34a50..7dd0907 100644 (file)
@@ -2,14 +2,21 @@
 
 require_once __DIR__ . '/Maintenance.php';
 
+use Composer\Spdx\SpdxLicenses;
+use JsonSchema\Validator;
+
 class ValidateRegistrationFile extends Maintenance {
        public function __construct() {
                parent::__construct();
                $this->addArg( 'path', 'Path to extension.json/skin.json file.', true );
        }
        public function execute() {
-               if ( !class_exists( 'JsonSchema\Validato' ) ) {
+               if ( !class_exists( Validator::class ) ) {
                        $this->error( 'The JsonSchema library cannot be found, please install it through composer.', 1 );
+               } elseif ( !class_exists( SpdxLicenses::class ) ) {
+                       $this->error(
+                               'The spdx-licenses library cannot be found, please install it through composer.', 1
+                       );
                }
 
                $path = $this->getArg( 0 );
@@ -38,14 +45,29 @@ class ValidateRegistrationFile extends Maintenance {
                        $this->output( "Warning: $path is using a deprecated schema, and should be updated to "
                                . ExtensionRegistry::MANIFEST_VERSION . "\n" );
                }
-               $validator = new JsonSchema\Validator;
+
+               $licenseError = false;
+               // Check if it's a string, if not, schema validation will display an error
+               if ( isset( $data->{'license-name'} ) && is_string( $data->{'license-name'} ) ) {
+                       $licenses = new SpdxLicenses();
+                       $valid = $licenses->validate( $data->{'license-name'} );
+                       if ( !$valid ) {
+                               $licenseError = '[license-name] Invalid SPDX license identifier, '
+                                       . 'see <https://spdx.org/licenses/>';
+                       }
+               }
+
+               $validator = new Validator;
                $validator->check( $data, (object) [ '$ref' => 'file://' . $schemaPath ] );
-               if ( $validator->isValid() ) {
+               if ( $validator->isValid() && !$licenseError ) {
                        $this->output( "$path validates against the version $version schema!\n" );
                } else {
                        foreach ( $validator->getErrors() as $error ) {
                                $this->output( "[{$error['property']}] {$error['message']}\n" );
                        }
+                       if ( $licenseError ) {
+                               $this->output( "$licenseError\n" );
+                       }
                        $this->error( "$path does not validate.", 1 );
                }
        }
index 89168db..e7c8e49 100644 (file)
@@ -1020,7 +1020,6 @@ return [
                        'feedback-cancel',
                        'feedback-close',
                        'feedback-dialog-title',
-                       'feedback-error-title',
                        'feedback-error1',
                        'feedback-error2',
                        'feedback-error3',
@@ -1083,6 +1082,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 +1269,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',
@@ -1858,6 +1859,9 @@ return [
                        'apisandbox-results-fixtoken-fail',
                        'apisandbox-alert-page',
                        'apisandbox-alert-field',
+                       'apisandbox-continue',
+                       'apisandbox-continue-clear',
+                       'apisandbox-continue-help',
                        'blanknamespace',
                ],
        ],
index 594cea2..d72957d 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:02Z
+ * Date: 2016-10-03T18:59:01Z
  */
 ( function ( OO ) {
 
index a4479f7..2f811da 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:06Z
+ * Date: 2016-10-03T18:59:06Z
  */
 .oo-ui-element-hidden {
        display: none !important;
@@ -401,11 +401,15 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
 }
 .oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label {
        color: inherit;
-       display: table;
+       display: inline-table;
        box-sizing: border-box;
        max-width: 100%;
        padding: 0;
        white-space: normal;
+       float: left;
+}
+.oo-ui-fieldsetLayout-group {
+       clear: both;
 }
 .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help {
        float: right;
@@ -421,7 +425,7 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
 .oo-ui-fieldsetLayout + .oo-ui-formLayout {
        margin-top: 2em;
 }
-.oo-ui-fieldsetLayout > .oo-ui-labelElement-label {
+.oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label {
        font-size: 1.1em;
        margin-bottom: 0.5em;
        padding: 0.25em 0;
index 09e6cfc..9a3d7eb 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:06Z
+ * Date: 2016-10-03T18:59:06Z
  */
 .oo-ui-element-hidden {
        display: none !important;
@@ -524,11 +524,15 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
 }
 .oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label {
        color: inherit;
-       display: table;
+       display: inline-table;
        box-sizing: border-box;
        max-width: 100%;
        padding: 0;
        white-space: normal;
+       float: left;
+}
+.oo-ui-fieldsetLayout-group {
+       clear: both;
 }
 .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-help {
        float: right;
@@ -544,7 +548,7 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
 .oo-ui-fieldsetLayout + .oo-ui-formLayout {
        margin-top: 2em;
 }
-.oo-ui-fieldsetLayout > .oo-ui-labelElement-label {
+.oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label {
        margin-bottom: 0.5em;
        font-size: 1.1em;
        font-weight: bold;
index c982010..109645b 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:02Z
+ * Date: 2016-10-03T18:59:01Z
  */
 ( function ( OO ) {
 
@@ -10121,6 +10121,7 @@ OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
        }
 
        // Initialization
+       this.$group.addClass( 'oo-ui-fieldsetLayout-group' );
        this.$element
                .addClass( 'oo-ui-fieldsetLayout' )
                .prepend( this.$label, this.$help, this.$icon, this.$group );
index 343508c..616f78e 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:02Z
+ * Date: 2016-10-03T18:59:01Z
  */
 ( function ( OO ) {
 
index 9c9954e..d6ed767 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:06Z
+ * Date: 2016-10-03T18:59:06Z
  */
 .oo-ui-popupTool .oo-ui-popupWidget-popup,
 .oo-ui-popupTool .oo-ui-popupWidget-anchor {
index a413005..411c6bb 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:06Z
+ * Date: 2016-10-03T18:59:06Z
  */
 .oo-ui-tool.oo-ui-widget-enabled {
        -webkit-transition: background-color 100ms;
index ba959cf..822b2d9 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:02Z
+ * Date: 2016-10-03T18:59:01Z
  */
 ( function ( OO ) {
 
index bd8034d..2d2b200 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:06Z
+ * Date: 2016-10-03T18:59:06Z
  */
 .oo-ui-draggableElement-handle,
 .oo-ui-draggableElement-handle.oo-ui-widget {
index 126b591..d1b4225 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:06Z
+ * Date: 2016-10-03T18:59:06Z
  */
 .oo-ui-draggableElement-handle,
 .oo-ui-draggableElement-handle.oo-ui-widget {
index 62195df..636e3f5 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:02Z
+ * Date: 2016-10-03T18:59:01Z
  */
 ( function ( OO ) {
 
index 3cff8f7..1cceac5 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:06Z
+ * Date: 2016-10-03T18:59:06Z
  */
 .oo-ui-actionWidget.oo-ui-pendingElement-pending {
        background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
index 2c115f9..38e40b9 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:06Z
+ * Date: 2016-10-03T18:59:06Z
  */
 .oo-ui-window {
        background: transparent;
index 8ef5ea5..0a29b8b 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.9
+ * OOjs UI v0.17.10
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-09-13T18:30:02Z
+ * Date: 2016-10-03T18:59:01Z
  */
 ( function ( OO ) {
 
diff --git a/resources/src/mediawiki.legacy/images/help-question-hover.gif b/resources/src/mediawiki.legacy/images/help-question-hover.gif
deleted file mode 100644 (file)
index 515138d..0000000
Binary files a/resources/src/mediawiki.legacy/images/help-question-hover.gif and /dev/null differ
diff --git a/resources/src/mediawiki.legacy/images/help-question.gif b/resources/src/mediawiki.legacy/images/help-question.gif
deleted file mode 100644 (file)
index b4fc9c5..0000000
Binary files a/resources/src/mediawiki.legacy/images/help-question.gif and /dev/null differ
index de442e9..1522de1 100644 (file)
@@ -105,6 +105,25 @@ span.comment {
        clear: both;
 }
 
+/* Edit font preference */
+/* TODO: for 'default' on non-textareas we could compute the default font of textarea in the client */
+.mw-editfont-default:not( textarea ) {
+       font-family: monospace;
+}
+
+/* Keep this rule separate from the :not rule above so it still works in older browsers */
+.mw-editfont-monospace {
+       font-family: monospace;
+}
+
+.mw-editfont-sans-serif {
+       font-family: sans-serif;
+}
+
+.mw-editfont-serif {
+       font-family: serif;
+}
+
 /**
  * rev_deleted stuff
  */
@@ -162,6 +181,7 @@ input#wpSummary {
 
 .mw-input-with-label {
        white-space: nowrap;
+       display: inline-block;
 }
 
 /**
@@ -656,33 +676,6 @@ ol:lang(or) li {
        direction: ltr;
 }
 
-/* tooltip styles */
-.mw-help-field-hint {
-       display: none;
-       margin-left: 2px;
-       margin-bottom: -8px;
-       padding: 0 0 0 15px;
-       background-image: url( images/help-question.gif );
-       background-position: left center;
-       background-repeat: no-repeat;
-       cursor: pointer;
-       font-size: .8em;
-       text-decoration: underline;
-       color: #0645ad;
-}
-
-.mw-help-field-hint:hover {
-       background-image: url( images/help-question-hover.gif );
-}
-
-.mw-help-field-data {
-       display: block;
-       background-color: #d6f3ff;
-       padding: 5px 8px 4px 8px;
-       border: 1px solid #5dc9f4;
-       margin-left: 20px;
-}
-
 #mw-clearyourcache,
 #mw-sitecsspreview,
 #mw-sitejspreview,
index f29897c..1bfa3a3 100644 (file)
@@ -23,7 +23,7 @@
                height: auto;
                margin: 0 0.1em 0 0;
                padding: 0;
-               border: 1px solid @colorFieldBorder;
+               border: 1px solid @colorGray7;
                cursor: pointer;
        }
 }
 // ----------------------------------------------------------------------------
 
 .button-colors( @bgColor, @highlightColor, @activeColor ) {
-       background: @bgColor;
+       background-color: @bgColor;
+       color: @colorButtonText;
+       border: 1px solid @colorFieldBorder;
+
+       // Make sure that `color` isn't inheriting from user-agent styles
+       &:visited {
+               color: @colorButtonText;
+       }
 
        &:hover {
                background-color: @highlightColor;
+               color: @colorGray4;
+               border-color: @colorGray10;
        }
 
        &:focus {
-               border-color: @colorWhite;
-               box-shadow: inset 0 0 0 1px @bgColor, inset 0 0 0 2px @colorWhite;
-               outline-width: 0;
-
-               // Remove the inner border and padding in Firefox.
-               &::-moz-focus-inner {
-                       border-color: transparent;
-                       padding: 0;
-               }
+               background-color: @highlightColor;
+               // Make sure that `color` isn't inheriting from user-agent styles
+               color: @colorButtonText;
+               border-color: @colorProgressive;
+               box-shadow: inset 0 0 0 1px @colorProgressive, inset 0 0 0 2px #fff;
        }
 
        &:active,
        &.is-on,
        &.mw-ui-checked {
                background-color: @activeColor;
+               color: @colorGray1;
+               border-color: @colorGray7;
                box-shadow: none;
        }
-}
-
-.button-colors( @bgColor, @highlightColor, @activeColor ) when ( lightness( @bgColor ) >= 70% ) {
-       color: @colorButtonText;
-       border: 1px solid @colorGray12;
-
-       &:hover,
-       &:active,
-       &:visited {
-               // make sure that is isn't inheriting from a general rule
-               color: @colorButtonText;
-       }
-
-       &:focus {
-               background-color: @highlightColor;
-       }
 
        &:disabled {
-               color: @colorDisabledText;
+               background-color: @colorGray12;
+               color: #fff;
+               border-color: @colorGray12;
 
-               // make sure disabled buttons don't have hover and active states
+               // Make sure disabled buttons don't have hover and active states
                &:hover,
                &:active {
-                       background: @bgColor;
+                       background-color: @colorGray12;
+                       color: #fff;
                        box-shadow: none;
+                       border-color: @colorGray12;
                }
        }
 }
 
-.button-colors( @bgColor, @highlightColor, @activeColor ) when ( lightness( @bgColor ) < 70% ) {
+.button-colors-primary( @bgColor, @highlightColor, @activeColor ) {
+       background-color: @bgColor;
        color: #fff;
        // border of the same color as background so that light background and
        // dark background buttons are the same height and width
        border: 1px solid @bgColor;
-       text-shadow: 0 1px rgba(0, 0, 0, .1);
+       text-shadow: 0 1px rgba( 0, 0, 0, 0.1 );
+
+       &:hover {
+               background-color: @highlightColor;
+               border-color: @highlightColor;
+       }
+
+       &:focus {
+               box-shadow: inset 0 0 0 1px @bgColor, inset 0 0 0 2px #fff;
+       }
+
+       &:active,
+       &.is-on,
+       &.mw-ui-checked {
+               background-color: @activeColor;
+               border-color: @activeColor;
+               box-shadow: none;
+       }
 
        &:disabled {
-               background-color: @colorGray13;
-               border-color: @colorGray13;
+               background-color: @colorGray12;
+               color: #fff;
+               border-color: @colorGray12;
 
-               // make sure disabled buttons don't have hover and active states
+               // Make sure disabled buttons don't have hover and active states
                &:hover,
                &:active,
                &.mw-ui-checked {
+                       background-color: @colorGray12;
+                       color: #fff;
+                       border-color: @colorGray12;
                        box-shadow: none;
                }
        }
 
 .button-colors-quiet( @textColor, @highlightColor, @activeColor ) {
        // Quiet buttons all start gray, and reveal
-       // constructive/progressive/destructive color on hover and active.
+       // progressive/destructive color on hover and active.
        color: @colorButtonText;
 
-       &:hover,
-       &:focus {
+       &:hover {
                background-color: transparent;
-               color: @textColor;
+               color: @highlightColor;
        }
 
        &:active,
                color: @activeColor;
        }
 
+       &:focus {
+               background-color: transparent;
+               color: @textColor;
+       }
+
        &:disabled {
                color: @colorDisabledText;
        }
index 507109a..28ad10a 100644 (file)
@@ -2,41 +2,37 @@
 
 // Although this defines many shades, be parsimonious in your own use of grays. Prefer
 // colors already in use in MediaWiki. Prefer semantic color names such as "@colorText".
-@colorGray1: #111; // darkest
+@colorGray1: #000; // darkest
 @colorGray2: #222;
 @colorGray3: #333;
 @colorGray4: #444;
-@colorGray5: #555;
+@colorGray5: #54595d;
 @colorGray6: #666;
-@colorGray7: #777;
+@colorGray7: #72777d;
 @colorGray8: #888;
 @colorGray9: #999;
-@colorGray10: #aaa;
+@colorGray10: #a2a9b1;
 @colorGray11: #bbb;
-@colorGray12: #ccc;
+@colorGray12: #c8ccd1;
 @colorGray13: #ddd;
-@colorGray14: #eee;
-@colorGray15: #f9f9f9; // lightest
+@colorGray14: #eaecf0;
+@colorGray15: #f8f9fa; // lightest
 
 // Semantic background colors
 // Blue; for contextual use of a continuing action
-@colorProgressive: #347bff;
-@colorProgressiveHighlight: #2962cc;
-@colorProgressiveActive: #2962cc;
-// Green; for contextual use of a positive finalizing action
-@colorConstructive: #00af89;
-@colorConstructiveHighlight: #008c6d;
-@colorConstructiveActive: #008c6d;
+@colorProgressive: #36c;
+@colorProgressiveHighlight: #447ff5;
+@colorProgressiveActive: #2a4b8d;
 // Orange; for contextual use of returning to a past action
 @colorRegressive: #ff5d00;
 // Red; for contextual use of a negative action of high severity
-@colorDestructive: #d11d13;
-@colorDestructiveHighlight: #a7170f;
-@colorDestructiveActive: #a7170f;
+@colorDestructive: #c33;
+@colorDestructiveHighlight: #e53939;
+@colorDestructiveActive: #873636;
 // Orange; for contextual use of a potentially negative action of medium severity
 @colorMediumSevere: #ff5d00;
 // Yellow; for contextual use of a potentially negative action of low severity
-@colorLowSevere: #ffb50d;
+@colorLowSevere: #fc3;
 
 // Used in mixins to darken contextual colors by the same amount (eg. focus)
 @colorDarkenPercentage: 13.5%;
 // Text colors
 @colorText: @colorGray2;
 @colorTextLight: @colorGray6;
-@colorButtonText: @colorGray5;
-@colorButtonTextHighlight: @colorGray7;
-@colorButtonTextActive: @colorGray7;
+@colorButtonText: @colorGray2;
+@colorButtonTextHighlight: @colorGray4;
+@colorButtonTextActive: @colorGray1;
 @colorDisabledText: @colorGray12;
 @colorErrorText: #c00;
+@colorWarningText: #705000;
 
 // UI colors
-@colorFieldBorder: @colorGray12;
+@colorFieldBorder: #9aa0a7;
 @colorShadow: @colorGray14;
 @colorPlaceholder: @colorGray10;
 @colorNeutral: @colorGray7;
 
-// The following rules are deprecated
-@colorWhite: #fff;
-@colorOffWhite: #fafafa;
-@colorGrayDark: #898989;
-@colorGrayLight: #ccc;
-@colorGrayLighter: #ddd;
-@colorGrayLightest: #eee;
-
 // Global border radius to be used to buttons and inputs
 @borderRadius: 2px;
 
 // Form input sizes
 @checkboxSize: 2em;
 @radioSize: 2em;
+
+// The following rules are deprecated
+@colorWhite: #fff;
+@colorOffWhite: #fafafa;
+@colorGrayDark: #898989;
+@colorGrayLight: #ccc;
+@colorGrayLighter: #ddd;
+@colorGrayLightest: #eee;
+// Green; for contextual use of a positive finalizing action
+@colorConstructive: #00af89;
+@colorConstructiveHighlight: #1c6665;
+@colorConstructiveActive: #134645;
+
index 5c3715d..3959900 100644 (file)
@@ -10,7 +10,8 @@
                suppressErrors = true,
                updatingBooklet = false,
                pages = {},
-               moduleInfoCache = {};
+               moduleInfoCache = {},
+               baseRequestParams;
 
        WidgetMethods = {
                textInputWidget: {
                                        fullscreenButton.$element,
                                        new OO.ui.ButtonWidget( {
                                                label: mw.message( 'apisandbox-submit' ).text(),
-                                               flags: [ 'primary', 'constructive' ]
+                                               flags: [ 'primary', 'progressive' ]
                                        } ).on( 'click', ApiSandbox.sendRequest ).$element,
                                        new OO.ui.ButtonWidget( {
                                                label: mw.message( 'apisandbox-reset' ).text(),
 
                /**
                 * Submit button handler
+                *
+                * @param {Object} [params] Use this set of params instead of those in the form fields.
+                *   The form fields will be updated to match.
                 */
-               sendRequest: function () {
+               sendRequest: function ( params ) {
                        var page, subpages, i, query, $result, $focus,
                                progress, $progressText, progressLoading,
                                deferreds = [],
-                               params = {},
+                               paramsAreForced = !!params,
                                displayParams = {},
                                checkPages = [ pages.main ];
 
 
                        suppressErrors = false;
 
+                       // save widget state in params (or load from it if we are forced)
+                       if ( paramsAreForced ) {
+                               ApiSandbox.updateUI( params );
+                       }
+                       params = {};
                        while ( checkPages.length ) {
                                page = checkPages.shift();
                                deferreds.push( page.apiCheckValid() );
                                }
                        }
 
+                       if ( !paramsAreForced ) {
+                               // forced params means we are continuing a query; the base query should be preserved
+                               baseRequestParams = $.extend( {}, params );
+                       }
+
                        $.when.apply( $, deferreds ).done( function () {
                                if ( $.inArray( false, arguments ) !== -1 ) {
                                        windowManager.openWindow( 'errorAlert', {
                                                        );
                                        } )
                                        .done( function ( data, jqXHR ) {
-                                               var m, loadTime, button,
+                                               var m, loadTime, button, clear,
                                                        ct = jqXHR.getResponseHeader( 'Content-Type' );
 
                                                $result.empty();
                                                                .text( data )
                                                                .appendTo( $result );
                                                }
+                                               if ( paramsAreForced || data[ 'continue' ] ) {
+                                                       $result.append(
+                                                               $( '<div>' ).append(
+                                                                       new OO.ui.ButtonWidget( {
+                                                                               label: mw.message( 'apisandbox-continue' ).text()
+                                                                       } ).on( 'click', function () {
+                                                                               ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) );
+                                                                       } ).setDisabled( !data[ 'continue' ] ).$element,
+                                                                       ( clear = new OO.ui.ButtonWidget( {
+                                                                               label: mw.message( 'apisandbox-continue-clear' ).text()
+                                                                       } ).on( 'click', function () {
+                                                                               ApiSandbox.updateUI( baseRequestParams );
+                                                                               clear.setDisabled( true );
+                                                                               booklet.setPage( '|results|' );
+                                                                       } ).setDisabled( !paramsAreForced ) ).$element,
+                                                                       new OO.ui.PopupButtonWidget( {
+                                                                               framed: false,
+                                                                               icon: 'info',
+                                                                               popup: {
+                                                                                       $content: $( '<div>' ).append( mw.message( 'apisandbox-continue-help' ).parse() ),
+                                                                                       padded: true
+                                                                               }
+                                                                       } ).$element
+                                                               )
+                                                       );
+                                               }
                                                if ( typeof loadTime === 'number' ) {
                                                        $result.append(
                                                                $( '<div>' ).append(
                                                // Don't grey out the label when the field is disabled,
                                                // it makes it too hard to read and our "disabled"
                                                // isn't really disabled.
+                                               widgetField.onFieldDisable( false );
                                                widgetField.onFieldDisable = doNothing;
 
                                                if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
                                                        dynamicParamNameWidget,
                                                        new OO.ui.ButtonWidget( {
                                                                icon: 'add',
-                                                               flags: 'constructive'
+                                                               flags: 'progressive'
                                                        } ).on( 'click', addDynamicParamWidget ),
                                                        {
                                                                label: mw.message( 'apisandbox-dynamic-parameters-add-label' ).text(),
index a523d5b..5191f92 100644 (file)
@@ -31,7 +31,7 @@
 }
 .searchresult {
        font-size: 95%;
-       width: 38em;
+       max-width: 38em;
 }
 .mw-search-results {
        margin-left: 0;
index 753f774..cf77a96 100644 (file)
 
 /* Login Button, following `ButtonWidget (progressive)‎` from OOjs UI */
 #mw-createaccount-join {
-       color: #347bff;
+       background-color: #f8f9fa;
+       color: #36c;
 }
 #mw-createaccount-join:hover {
-       background-color: #ebf2ff; /* rgba( 52, 123, 255, 0.1 ); */
+       background-color: #fff;
        border-color: #859ecc;
        box-shadow: none;
 }
 #mw-createaccount-join:active {
-       background-color: #ebf2ff;
-       color: #1f4999;
-       border-color: #1f4999;
+       background-color: #eff3fa;
+       color: #2a4b8d;
+       border-color: #2a4b8d;
 }
 #mw-createaccount-join:focus {
-       background-color: #fff;
-       color: #1f4999;
-       border-color: #1f4999;
-       box-shadow: inset 0 0 0 1px #1f4999;
-}
-#mw-createaccount-join:active:focus {
-       background-color: #ebf2ff;
+       border-color: #36c;
+       box-shadow: inset 0 0 0 1px #36c;
 }
index 5931efb..a281e67 100644 (file)
@@ -14,7 +14,7 @@
 
 // Neutral button styling
 //
-// These are the main actions on the page/workflow. The page should have only one of progressive, constructive and desctructive buttons, the rest being quiet.
+// These are the main actions on the page/workflow. The page should have only one of progressive and destructive buttons, the rest being quiet.
 //
 // Markup:
 // <div>
        vertical-align: middle;
 
        // Content styling
-       .button-colors( #fff, @colorGray12, @colorGray7 );
+       .button-colors( @colorGray15, #fff, #d9d9d9 );
        text-align: center;
        font-weight: bold;
 
        // Interaction styling
        cursor: pointer;
 
+       &:focus {
+               outline-width: 0;
+
+               // Remove the inner border and padding in Firefox.
+               &::-moz-focus-inner {
+                       border-color: transparent;
+                       padding: 0;
+               }
+       }
+
+       // `:not()` is used exclusively for `transition`s as both are not supported by IE < 9
+       &:not( :disabled ) {
+               .transition( ~'background-color 100ms, color 100ms, border-color 100ms, box-shadow 100ms' );
+       }
+
        &:disabled {
                text-shadow: none;
                cursor: default;
@@ -78,9 +93,6 @@
        //   <button class="mw-ui-button mw-ui-progressive mw-ui-big">.mw-ui-progressive</button>
        // </div>
        // <div>
-       //   <button class="mw-ui-button mw-ui-constructive mw-ui-big">.mw-ui-constructive</button>
-       // </div>
-       // <div>
        //   <button class="mw-ui-button mw-ui-destructive mw-ui-big">.mw-ui-destructive</button>
        // </div>
        //
        //   <button class="mw-ui-button mw-ui-progressive mw-ui-block">.mw-ui-progressive</button>
        // </div>
        // <div>
-       //   <button class="mw-ui-button mw-ui-constructive mw-ui-block">.mw-ui-constructive</button>
-       // </div>
-       // <div>
        //   <button class="mw-ui-button mw-ui-destructive mw-ui-block">.mw-ui-destructive</button>
        // </div>
        //
        // Progressive buttons
        //
        // Use progressive buttons for actions which lead to a next step in the process.
+       // .mw-ui-constructive is deprecated; consolidated with `progressive`, see T110555
        // .mw-ui-primary is deprecated, kept for compatibility.
        //
        // Markup:
        //
        // Styleguide 2.1.1.
        &.mw-ui-progressive,
+       &.mw-ui-constructive,
        &.mw-ui-primary {
-               .button-colors( @colorProgressive, @colorProgressiveHighlight, @colorProgressiveActive );
-
-               &.mw-ui-quiet {
-                       .button-colors-quiet( @colorProgressive, @colorProgressiveHighlight, @colorProgressiveActive );
-               }
-       }
-
-       // Constructive buttons (deprecated, consolidated with `progressive` – see T110555)
-       //
-       // Use constructive buttons for actions which result in a final action in the process that results
-       // in a change of state.
-       // e.g. save changes button
-       //
-       // Markup:
-       // <div>
-       //   <button class="mw-ui-button mw-ui-constructive">.mw-ui-constructive</button>
-       // </div>
-       // <div>
-       //   <button class="mw-ui-button mw-ui-constructive" disabled>.mw-ui-constructive</button>
-       // </div>
-       //
-       // Styleguide 2.1.2.
-       &.mw-ui-constructive {
-               .button-colors( @colorProgressive, @colorProgressiveHighlight, @colorProgressiveActive );
+               .button-colors-primary( @colorProgressive, @colorProgressiveHighlight, @colorProgressiveActive );
 
                &.mw-ui-quiet {
                        .button-colors-quiet( @colorProgressive, @colorProgressiveHighlight, @colorProgressiveActive );
        //   <button class="mw-ui-button mw-ui-destructive" disabled>.mw-ui-destructive</button>
        // </div>
        //
-       // Styleguide 2.1.3.
+       // Styleguide 2.1.2.
        &.mw-ui-destructive {
-               .button-colors( @colorDestructive, @colorDestructiveHighlight, @colorDestructiveActive );
+               .button-colors-primary( @colorDestructive, @colorDestructiveHighlight, @colorDestructiveActive );
 
                &.mw-ui-quiet {
                        .button-colors-quiet( @colorDestructive, @colorDestructiveHighlight, @colorDestructiveActive );
 
        // Quiet buttons
        //
-       // Use quiet buttons when they are less important and alongside other constructive, progressive or destructive buttons. It should be used for an action that exits the user from the current view/workflow.
+       // Use quiet buttons when they are less important and alongside other progressive or destructive buttons. It should be used for an action that exits the user from the current view/workflow.
        // Its use is  not recommended on mobile/tablet due to lack of hover state.
        //
        // Markup:
        //   <button class="mw-ui-button mw-ui-quiet">.mw-ui-button</button>
        // </div>
        // <div>
-       //   <button class="mw-ui-button mw-ui-constructive mw-ui-quiet">.mw-ui-constructive</button>
-       // </div>
-       // <div>
-       //   <button class="mw-ui-button mw-ui-constructive mw-ui-quiet" disabled>.mw-ui-constructive</button>
-       // </div>
-       // <div>
        //   <button class="mw-ui-button mw-ui-destructive mw-ui-quiet">.mw-ui-destructive</button>
        // </div>
        // <div>
        //   <button class="mw-ui-button mw-ui-progressive mw-ui-quiet" disabled>.mw-ui-progressive</button>
        // </div>
        //
-       // Styleguide 2.1.4.
+       // Styleguide 2.1.3.
        &.mw-ui-quiet {
                background: transparent;
                border: 0;
index cc96a5c..2327efc 100644 (file)
@@ -30,7 +30,7 @@
 //     <input class="mw-ui-input" value="input">
 //   </div>
 //   <div class="mw-ui-vform-field">
-//     <button class="mw-ui-button mw-ui-constructive">Button in vform</button>
+//     <button class="mw-ui-button mw-ui-progressive">Button in vform</button>
 //   </div>
 // </form>
 //
        //
        // Styleguide 5.2.
        .error,
+       .warning,
        .errorbox,
        .warningbox,
        .successbox {
                color: @colorErrorText;
                border: 1px solid #fac5c5;
                background-color: #fae3e3;
-               text-shadow: 0 1px #fae3e3;
+       }
+
+       // Colours taken from those for .warningbox in shared.css
+       .warning {
+               color: @colorWarningText;
+               border: 1px solid #fde29b;
+               background-color: #fdf1d1;
        }
 
        // This specifies styling for individual field validation error messages.
index 76fee23..90e769e 100644 (file)
@@ -99,7 +99,7 @@ textarea.mw-ui-input {
 //
 // Markup:
 // <input class="mw-ui-input mw-ui-input-inline">
-// <button class="mw-ui-button mw-ui-constructive">Submit</button>
+// <button class="mw-ui-button mw-ui-progressive">Submit</button>
 //
 // Styleguide 1.2.
 input[type="number"],
index cc27e9e..5551745 100644 (file)
@@ -16,10 +16,10 @@ Text
 Context classes may be used on elements with only plain-text content with the mw-ui-text base. When the context classes
 are used on interactive and block-level elements, the appropriate alternative base type classes should also be used. For
 example, mw-ui-anchor with A, or mw-ui-button with buttons.
+'Constructive' is deprecated and merged with 'Progressive'.
 
 Markup:
 <span class="mw-ui-text mw-ui-progressive">Progressive</span>
-<span class="mw-ui-text mw-ui-constructive">Constructive</span>
 <span class="mw-ui-text mw-ui-destructive">Destructive</span>
 
 Styleguide 6.1.
@@ -28,11 +28,9 @@ Styleguide 6.1.
 .mw-ui-text {
        // The selector order is like this on purpose; IE 6 ignores the second selector,
        // so we don't want to accidentally apply this color on all mw-ui-CONTEXT classes
-       .mw-ui-progressive& {
-               color: @colorProgressive;
-       }
+       .mw-ui-progressive&,
        .mw-ui-constructive& {
-               color: @colorConstructive;
+               color: @colorProgressive;
        }
        .mw-ui-destructive& {
                color: @colorDestructive;
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 );
 
index 9d30eb8..4d90496 100644 (file)
 
 .mw-widget-calendarWidget:focus {
        outline: none;
-       box-shadow: inset 0 0 0 2px #347bff;
+       box-shadow: inset 0 0 0 2px #36c;
 }
 
 .mw-widget-calendarWidget-day {
index 7fdef25..267eebb 100644 (file)
@@ -65,6 +65,7 @@
 
                // Initialize
                this.api = config.api || new mw.Api();
+               this.searchCache = {};
        }
 
        /* Setup */
         * @return {jQuery.Promise} Resolves with an array of categories
         */
        CSP.searchCategories = function ( input, searchType ) {
-               var deferred = $.Deferred();
+               var deferred = $.Deferred(),
+                       cacheKey = input + searchType.toString();
+
+               // Check cache
+               if ( this.searchCache[ cacheKey ] !== undefined ) {
+                       return this.searchCache[ cacheKey ];
+               }
 
                switch ( searchType ) {
                        case CategorySelector.SearchType.OpenSearch:
                                        var categories = [];
 
                                        $.each( res.query.pages, function ( index, page ) {
-                                               if ( !page.missing ) {
-                                                       if ( $.isArray( page.categories ) ) {
-                                                               categories.push.apply( categories, page.categories.map( function ( category ) {
-                                                                       return category.title;
-                                                               } ) );
-                                                       }
+                                               if ( !page.missing && $.isArray( page.categories ) ) {
+                                                       categories.push.apply( categories, page.categories.map( function ( category ) {
+                                                               return category.title;
+                                                       } ) );
                                                }
                                        } );
 
                                throw new Error( 'Unknown searchType' );
                }
 
+               // Cache the result
+               this.searchCache[ cacheKey ] = deferred.promise();
+
                return deferred.promise();
        };
 
index 86018a4..46e6b62 100644 (file)
 
        &.oo-ui-widget-enabled {
                .mw-widget-dateInputWidget-handle:hover {
-                       border-color: #347bff;
+                       border-color: #36c;
                }
        }
 
index 1732407..b25b2d4 100644 (file)
@@ -33,6 +33,7 @@
         * @cfg {boolean} [showRedlink] Show red link to exact match if it doesn't exist
         * @cfg {boolean} [showImages] Show page images
         * @cfg {boolean} [showDescriptions] Show page descriptions
+        * @cfg {boolean} [excludeCurrentPage] Exclude the current page from suggestions
         * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title (if set to true,
         *  the widget will marks itself red for invalid inputs, including an empty query).
         * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
@@ -54,6 +55,7 @@
                this.showRedlink = !!config.showRedlink;
                this.showImages = !!config.showImages;
                this.showDescriptions = !!config.showDescriptions;
+               this.excludeCurrentPage = !!config.excludeCurrentPage;
                this.validateTitle = config.validateTitle !== undefined ? config.validateTitle : true;
                this.cache = config.cache;
 
         */
        mw.widgets.TitleWidget.prototype.getOptionsFromData = function ( data ) {
                var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
+                       currentPageName = new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getPrefixedText(),
                        items = [],
                        titles = [],
                        titleObj = mw.Title.newFromText( this.getQueryValue() ),
 
                for ( index in data.pages ) {
                        suggestionPage = data.pages[ index ];
+                       // When excludeCurrentPage is set, don't list the current page unless the user has type the full title
+                       if ( this.excludeCurrentPage && suggestionPage.title === currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) {
+                               continue;
+                       }
                        pageData[ suggestionPage.title ] = {
                                missing: suggestionPage.missing !== undefined,
                                redirect: suggestionPage.redirect !== undefined,
index c2da10e..8169449 100644 (file)
                        this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
                                // Use FormData (if we got here, we know that it's available)
                                contentType: 'multipart/form-data',
+                               // No timeout (default from mw.Api is 30 seconds)
+                               timeout: 0,
                                // Provide upload progress notifications
                                xhr: function () {
                                        var xhr = $.ajaxSettings.xhr();
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 0f61d97..844d74c 100644 (file)
                } );
        };
 
+       /**
+        * @param {mw.Title} filename
+        * @return {jQuery.Promise} Resolves (on success) or rejects with OO.ui.Error
+        */
+       mw.ForeignStructuredUpload.BookletLayout.prototype.validateFilename = function ( filename ) {
+               return ( new mw.Api() ).get( {
+                       action: 'query',
+                       prop: 'info',
+                       titles: filename.getPrefixedDb(),
+                       formatversion: 2
+               } ).then(
+                       function ( result ) {
+                               // if the file already exists, reject right away, before
+                               // ever firing finishStashUpload()
+                               if ( !result.query.pages[ 0 ].missing ) {
+                                       return $.Deferred().reject( new OO.ui.Error(
+                                               $( '<p>' ).msg( 'fileexists', filename.getPrefixedDb() ),
+                                               { recoverable: false }
+                                       ) );
+                               }
+                       },
+                       function () {
+                               // API call failed - this could be a connection hiccup...
+                               // Let's just ignore this validation step and turn this
+                               // failure into a successful resolve ;)
+                               return $.Deferred().resolve();
+                       }
+               );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       mw.ForeignStructuredUpload.BookletLayout.prototype.saveFile = function () {
+               var title = mw.Title.newFromText(
+                               this.getFilename(),
+                               mw.config.get( 'wgNamespaceIds' ).file
+                       );
+
+               return this.uploadPromise
+                       .then( this.validateFilename.bind( this, title ) )
+                       .then( mw.ForeignStructuredUpload.BookletLayout.parent.prototype.saveFile.bind( this ) );
+       };
+
        /* Getters */
 
        /**
index 68062d0..03df086 100644 (file)
@@ -30,6 +30,6 @@
 }
 
 .mw-upload-bookletLayout-filePreview .oo-ui-progressBarWidget-bar {
-       background-color: #347bff;
+       background-color: #36c;
        height: 0.5em;
 }
\ No newline at end of file
index e8a85f1..a719ffe 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' ],
@@ -78,7 +84,7 @@
                        modes: 'insert'
                },
                {
-                       flags: [ 'primary', 'constructive' ],
+                       flags: [ 'primary', 'progressive' ],
                        label: mw.msg( 'upload-dialog-button-save' ),
                        action: 'save',
                        modes: 'info'
                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 170e124..0b3ea04 100644 (file)
                                        ]
                                };
                                break;
-                       case 'error1':
-                       case 'error2':
-                       case 'error3':
-                       case 'error4':
-                               dialogConfig = {
-                                       title: mw.msg( 'feedback-error-title' ),
-                                       message: mw.msg( 'feedback-' + status ),
-                                       actions: [
-                                               {
-                                                       action: 'accept',
-                                                       label: mw.msg( 'feedback-close' ),
-                                                       flags: 'primary'
-                                               }
-                                       ]
-                               };
-                               break;
                }
 
                // Show the message dialog
                {
                        action: 'submit',
                        label: mw.msg( 'feedback-submit' ),
-                       flags: [ 'primary', 'constructive' ]
+                       flags: [ 'primary', 'progressive' ]
                },
                {
                        action: 'external',
                        label: mw.msg( 'feedback-external-bug-report-button' ),
-                       flags: 'constructive'
+                       flags: 'progressive'
                },
                {
                        action: 'cancel',
                                }, function () {
                                        fb.status = 'error4';
                                        mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
-                               } ).always( function () {
+                               } ).then( function () {
                                        fb.close();
+                               }, function () {
+                                       return fb.getErrorMessage();
                                } );
                        }, this );
                }
                return mw.Feedback.Dialog.parent.prototype.getActionProcess.call( this, action );
        };
 
+       /**
+        * Returns an error message for the current status.
+        *
+        * @private
+        *
+        * @return {OO.ui.Error}
+        */
+       mw.Feedback.Dialog.prototype.getErrorMessage = function () {
+               switch ( this.status ) {
+                       case 'error1':
+                       case 'error2':
+                       case 'error3':
+                       case 'error4':
+                               // Messages: feedback-error1, feedback-error2, feedback-error3, feedback-error4
+                               return new OO.ui.Error( mw.msg( 'feedback-' + this.status ) );
+               }
+       };
+
        /**
         * Posts the message
         *
index 04807f4..c7715e5 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 );
                                        }
                                }
 
                                pendingRequests.push( function () {
                                        if ( moduleName && hasOwn.call( registry, moduleName ) ) {
+                                               // Emulate runScript() part of execute()
                                                window.require = mw.loader.require;
                                                window.module = registry[ moduleName ].module;
                                        }
                                        addScript( src ).always( function () {
-                                               // Clear environment
-                                               delete window.require;
+                                               // 'module.exports' should not persist after the file is executed to
+                                               // avoid leakage to unrelated code. 'require' should be kept, however,
+                                               // as asynchronous access to 'require' is allowed and expected. (T144879)
                                                delete window.module;
                                                r.resolve();
 
                                registry[ module ].state = 'executing';
 
                                runScript = function () {
-                                       var script, markModuleReady, nestedAddScript, legacyWait,
+                                       var script, markModuleReady, nestedAddScript, legacyWait, implicitDependencies,
                                                // Expand to include dependencies since we have to exclude both legacy modules
                                                // and their dependencies from the legacyWait (to prevent a circular dependency).
                                                legacyModules = resolve( mw.config.get( 'wgResourceLoaderLegacyModules', [] ) );
-                                       try {
-                                               script = registry[ module ].script;
-                                               markModuleReady = function () {
-                                                       registry[ module ].state = 'ready';
-                                                       handlePending( module );
-                                               };
-                                               nestedAddScript = function ( arr, callback, i ) {
-                                                       // Recursively call queueModuleScript() in its own callback
-                                                       // for each element of arr.
-                                                       if ( i >= arr.length ) {
-                                                               // We're at the end of the array
-                                                               callback();
-                                                               return;
-                                                       }
 
-                                                       queueModuleScript( arr[ i ], module ).always( function () {
-                                                               nestedAddScript( arr, callback, i + 1 );
-                                                       } );
-                                               };
+                                       script = registry[ module ].script;
+                                       markModuleReady = function () {
+                                               registry[ module ].state = 'ready';
+                                               handlePending( module );
+                                       };
+                                       nestedAddScript = function ( arr, callback, i ) {
+                                               // Recursively call queueModuleScript() in its own callback
+                                               // for each element of arr.
+                                               if ( i >= arr.length ) {
+                                                       // We're at the end of the array
+                                                       callback();
+                                                       return;
+                                               }
 
-                                               legacyWait = ( $.inArray( module, legacyModules ) !== -1 )
-                                                       ? $.Deferred().resolve()
-                                                       : mw.loader.using( legacyModules );
+                                               queueModuleScript( arr[ i ], module ).always( function () {
+                                                       nestedAddScript( arr, callback, i + 1 );
+                                               } );
+                                       };
 
-                                               legacyWait.always( function () {
+                                       implicitDependencies = ( $.inArray( module, legacyModules ) !== -1 )
+                                               ? []
+                                               : legacyModules;
+
+                                       if ( module === 'user' ) {
+                                               // Implicit dependency on the site module. Not real dependency because
+                                               // it should run after 'site' regardless of whether it succeeds or fails.
+                                               implicitDependencies.push( 'site' );
+                                       }
+
+                                       legacyWait = implicitDependencies.length
+                                               ? mw.loader.using( implicitDependencies )
+                                               : $.Deferred().resolve();
+
+                                       legacyWait.always( function () {
+                                               try {
                                                        if ( $.isArray( script ) ) {
                                                                nestedAddScript( script, markModuleReady, 0 );
                                                        } else if ( typeof script === 'function' ) {
                                                                // Site and user modules are legacy scripts that run in the global scope.
                                                                // This is transported as a string instead of a function to avoid needing
                                                                // to use string manipulation to undo the function wrapper.
-                                                               if ( module === 'user' ) {
-                                                                       // Implicit dependency on the site module. Not real dependency because
-                                                                       // it should run after 'site' regardless of whether it succeeds or fails.
-                                                                       mw.loader.using( 'site' ).always( function () {
-                                                                               $.globalEval( script );
-                                                                               markModuleReady();
-                                                                       } );
-                                                               } else {
-                                                                       $.globalEval( script );
-                                                                       markModuleReady();
-                                                               }
+                                                               $.globalEval( script );
+                                                               markModuleReady();
+
                                                        } else {
                                                                // Module without script
                                                                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
-                                               registry[ module ].state = 'error';
-                                               mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
-                                               handlePending( module );
-                                       }
+                                               } catch ( e ) {
+                                                       // 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 );
+                                               }
+                                       } );
                                };
 
                                // Add localizations to message system
                                }
                        }
 
-                       /**
-                        * 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 dfc98ad..b58cb69 100644 (file)
         * @member mw
         * @param {Function} callback
         */
+       mw.requestIdleCallback = mw.requestIdleCallbackInternal;
+       /*
+       // XXX: Polyfill disabled due to https://bugs.chromium.org/p/chromium/issues/detail?id=647870
        mw.requestIdleCallback = window.requestIdleCallback
                // Bind because it throws TypeError if context is not window
                ? window.requestIdleCallback.bind( window )
                : mw.requestIdleCallbackInternal;
+       */
 }( mediaWiki ) );
index 294b5de..866f213 100644 (file)
                        // - atext   : defined in RFC 5322 section 3.2.3
                        // - ldh-str : defined in RFC 1034 section 3.5
                        //
-                       // (see STD 68 / RFC 5234 http://tools.ietf.org/html/std68)
+                       // (see STD 68 / RFC 5234 https://tools.ietf.org/html/std68)
                        // First, define the RFC 5322 'atext' which is pretty easy:
                        // atext = ALPHA / DIGIT / ; Printable US-ASCII
                        //     "!" / "#" /    ; characters not including
index 2a985fe..a19fea1 100644 (file)
@@ -147,6 +147,7 @@ $wgAutoloadClasses += [
        # tests/phpunit/mocks
        'MockFSFile' => "$testDir/phpunit/mocks/filebackend/MockFSFile.php",
        'MockFileBackend' => "$testDir/phpunit/mocks/filebackend/MockFileBackend.php",
+       'MockLocalRepo' => "$testDir/phpunit/mocks/filerepo/MockLocalRepo.php",
        'MockBitmapHandler' => "$testDir/phpunit/mocks/media/MockBitmapHandler.php",
        'MockImageHandler' => "$testDir/phpunit/mocks/media/MockImageHandler.php",
        'MockSvgHandler' => "$testDir/phpunit/mocks/media/MockSvgHandler.php",
index 4ef778d..a0d6b22 100644 (file)
@@ -49,7 +49,7 @@ class ParserTestRunner {
 
        /**
         * Our connection to the database
-        * @var DatabaseBase
+        * @var Database
         */
        private $db;
 
@@ -59,11 +59,6 @@ class ParserTestRunner {
         */
        private $dbClone;
 
-       /**
-        * @var DjVuSupport
-        */
-       private $djVuSupport;
-
        /**
         * @var TidySupport
         */
@@ -138,7 +133,6 @@ class ParserTestRunner {
                $this->runDisabled = !empty( $options['run-disabled'] );
                $this->runParsoid = !empty( $options['run-parsoid'] );
 
-               $this->djVuSupport = new DjVuSupport();
                $this->tidySupport = new TidySupport( !empty( $options['use-tidy-config'] ) );
                if ( !$this->tidySupport->isEnabled() ) {
                        $this->recorder->warning(
@@ -348,7 +342,8 @@ class ParserTestRunner {
                        $backend = new FSFileBackend( [
                                'name' => 'local-backend',
                                'wikiId' => wfWikiID(),
-                               'basePath' => $this->uploadDir
+                               'basePath' => $this->uploadDir,
+                               'tmpDirectory' => wfTempDir()
                        ] );
                } elseif ( $this->fileBackendName ) {
                        global $wgFileBackends;
@@ -379,7 +374,7 @@ class ParserTestRunner {
 
                return new RepoGroup(
                        [
-                               'class' => 'LocalRepo',
+                               'class' => 'MockLocalRepo',
                                'name' => 'local',
                                'url' => 'http://example.com/images',
                                'hashLevels' => 2,
@@ -455,7 +450,6 @@ class ParserTestRunner {
                }
                $this->setupDone[$funcName] = true;
                return function () use ( $funcName ) {
-                       wfDebug( "markSetupDone unmarked $funcName" );
                        $this->setupDone[$funcName] = false;
                };
        }
@@ -751,14 +745,6 @@ class ParserTestRunner {
                $user = $context->getUser();
                $options = ParserOptions::newFromContext( $context );
 
-               if ( isset( $opts['djvu'] ) ) {
-                       if ( !$this->djVuSupport->isEnabled() ) {
-                               $this->recorder->skipped( $test,
-                                       'djvu binaries do not exist or are not executable' );
-                               return false;
-                       }
-               }
-
                if ( isset( $opts['tidy'] ) ) {
                        if ( !$this->tidySupport->isEnabled() ) {
                                $this->recorder->skipped( $test, 'tidy extension is not installed' );
@@ -1027,7 +1013,6 @@ class ParserTestRunner {
                };
 
                // Set content language. This invalidates the magic word cache and title services
-               wfDebug( "Setting up language $langCode" );
                $lang = Language::factory( $langCode );
                $setup['wgContLang'] = $lang;
                $reset = function () {
@@ -1523,7 +1508,7 @@ class ParserTestRunner {
                }
 
                // The RepoGroup cache is invalidated by the creation of file redirects
-               if ( $title->getNamespace() === NS_IMAGE ) {
+               if ( $title->inNamespace( NS_FILE ) ) {
                        RepoGroup::singleton()->clearCache( $title );
                }
        }
index a1a8d19..6279d68 100644 (file)
@@ -25,6 +25,7 @@ class TestFileReader {
        private $section = null;
        /** String|null: current test section being analyzed */
        private $sectionData = [];
+       private $sectionLineNum = [];
        private $lineNum = 0;
        private $runDisabled;
        private $runParsoid;
@@ -77,44 +78,83 @@ class TestFileReader {
                // "input" and "result" are old section names allowed
                // for backwards-compatibility.
                $input = $this->checkSection( [ 'wikitext', 'input' ], false );
-               $result = $this->checkSection( [ 'html/php', 'html/*', 'html', 'result' ], false );
+               $nonTidySection = $this->checkSection(
+                       [ 'html/php', 'html/*', 'html', 'result' ], false );
                // Some tests have "with tidy" and "without tidy" variants
-               $tidy = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false );
+               $tidySection = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false );
 
-               if ( !isset( $this->sectionData['options'] ) ) {
-                       $this->sectionData['options'] = '';
+               // Remove trailing newline
+               $data = array_map( 'ParserTestRunner::chomp', $this->sectionData );
+
+               // Apply defaults
+               $data += [
+                       'options' => '',
+                       'config' => ''
+               ];
+
+               if ( $input === false ) {
+                       throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
+                               "lacks input section" );
+               }
+
+               if ( preg_match( '/\\bdisabled\\b/i', $data['options'] ) &&     !$this->runDisabled ) {
+                       // Disabled
+                       return;
                }
 
-               if ( !isset( $this->sectionData['config'] ) ) {
-                       $this->sectionData['config'] = '';
+               if ( $tidySection === false && $nonTidySection === false ) {
+                       if ( isset( $data['html/parsoid'] ) || isset( $data['wikitext/edited'] ) ) {
+                               // Parsoid only
+                               return;
+                       } else {
+                               throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
+                                       "lacks result section" );
+                       }
+               }
+
+               if ( preg_match( '/\\bparsoid\\b/i', $data['options'] ) && $nonTidySection === 'html'
+                       && !$this->runParsoid
+               ) {
+                       // A test which normally runs on Parsoid but can optionally be run with MW
+                       return;
                }
 
-               $isDisabled = preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) &&
-                       !$this->runDisabled;
-               $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) &&
-                       $result == 'html' &&
-                       !$this->runParsoid;
-               $isFiltered = !preg_match( $this->regex, $this->sectionData['test'] );
-               if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) {
-                       // Disabled test
+               if ( !preg_match( $this->regex, $data['test'] ) ) {
+                       // Filtered test
                        return;
                }
 
-               $test = [
-                       'test' => ParserTestRunner::chomp( $this->sectionData['test'] ),
-                       'input' => ParserTestRunner::chomp( $this->sectionData[$input] ),
-                       'result' => ParserTestRunner::chomp( $this->sectionData[$result] ),
-                       'options' => ParserTestRunner::chomp( $this->sectionData['options'] ),
-                       'config' => ParserTestRunner::chomp( $this->sectionData['config'] ),
+               $commonInfo = [
+                       'test' => $data['test'],
+                       'desc' => $data['test'],
+                       'input' => $data[$input],
+                       'options' => $data['options'],
+                       'config' => $data['config'],
                ];
-               $test['desc'] = $test['test'];
-               $this->tests[] = $test;
-
-               if ( $tidy !== false ) {
-                       $test['options'] .= " tidy";
-                       $test['desc'] .= ' (with tidy)';
-                       $test['result'] = ParserTestRunner::chomp( $this->sectionData[$tidy] );
-                       $this->tests[] = $test;
+
+               if ( $nonTidySection !== false ) {
+                       // Add non-tidy test
+                       $this->tests[] = [
+                               'result' => $data[$nonTidySection],
+                       ] + $commonInfo;
+
+                       if ( $tidySection !== false ) {
+                               // Add tidy subtest
+                               $this->tests[] = [
+                                       'desc' => $data['test'] . ' (with tidy)',
+                                       'result' => $data[$tidySection],
+                                       'options' => $data['options'] . ' tidy',
+                               ] + $commonInfo;
+                       }
+               } elseif ( $tidySection !== false ) {
+                       // No need to override desc when there is no subtest
+                       $this->tests[] = [
+                               'result' => $data[$tidySection],
+                               'options' => $data['options'] . ' tidy'
+                       ] + $commonInfo;
+               } else {
+                       throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
+                               "lacks result section" );
                }
        }
 
@@ -199,6 +239,7 @@ class TestFileReader {
                                                . "at line {$this->lineNum} of $this->file\n" );
                                }
 
+                               $this->sectionLineNum[$this->section] = $this->lineNum;
                                $this->sectionData[$this->section] = '';
 
                                continue;
@@ -214,6 +255,7 @@ class TestFileReader {
         * Clear section name and its data
         */
        private function clearSection() {
+               $this->sectionLineNum = [];
                $this->sectionData = [];
                $this->section = null;
 
index 2c8b163..ba7b0d4 100644 (file)
@@ -3681,6 +3681,10 @@ Nested definition lists using html syntax
 <dl><dt>x</dt>
 <dd>a</dd>
 <dd>b</dd></dl>
+!! html
+<dl><dt>x</dt>
+<dd>a</dd>
+<dd>b</dd></dl>
 
 !! end
 
@@ -14726,8 +14730,10 @@ Simple category
 cat
 !! wikitext
 [[Category:MediaWiki User's Guide]]
-!! html
+!! html/php
 cat=MediaWiki_User's_Guide sort=
+!! html/parsoid
+<link rel="mw:PageProp/Category" href="./Category:MediaWiki_User's_Guide" data-parsoid='{"stx":"simple","a":{"href":"./Category:MediaWiki_User&#39;s_Guide"},"sa":{"href":"Category:MediaWiki User&#39;s Guide"}}'/>
 !! end
 
 !! test
@@ -14745,8 +14751,10 @@ Category with different sort key
 cat
 !! wikitext
 [[Category:MediaWiki User's Guide|Foo]]
-!! html
+!! html/php
 cat=MediaWiki_User's_Guide sort=Foo
+!! html/parsoid
+<link rel="mw:PageProp/Category" href="./Category:MediaWiki_User's_Guide#Foo" data-parsoid='{"stx":"piped","a":{"href":"./Category:MediaWiki_User&#39;s_Guide"},"sa":{"href":"Category:MediaWiki User&#39;s Guide"}}'/>
 !! end
 
 !! test
@@ -14755,8 +14763,10 @@ Category with identical sort key
 cat
 !! wikitext
 [[Category:MediaWiki User's Guide|MediaWiki User's Guide]]
-!! html
+!! html/php
 cat=MediaWiki_User's_Guide sort=MediaWiki User's Guide
+!! html/parsoid
+<link rel="mw:PageProp/Category" href="./Category:MediaWiki_User's_Guide#MediaWiki%20User's%20Guide" data-parsoid='{"stx":"piped","a":{"href":"./Category:MediaWiki_User&#39;s_Guide"},"sa":{"href":"Category:MediaWiki User&#39;s Guide"}}'/>
 !! end
 
 !! test
@@ -14781,22 +14791,15 @@ pst
 [[Category:Foo (bar)|Foo]]
 !! end
 
-## We used to, but no longer wt2wt this test since the default serializer
-## will normalize all categories to serialize on their own line.
-## This wikitext usage is going to be fairly uncommon in production and
-## selser will take care of preserving formatting in those scenarios.
 !! test
 Category with link tail
 !! options
 cat
 pst
-parsoid=wt2html
 !! wikitext
 123[[Category:Foo]]456
 !! html/php
 123[[Category:Foo]]456
-!! html/parsoid
-<p>123<link rel="mw:PageProp/Category" href="Category:Foo"/>456</p>
 !! end
 
 !! test
@@ -16707,11 +16710,11 @@ Expansion of multi-line templates in attribute values (bug 6255 sanity check 2)
 !! end
 
 !! test
-evil <math>-wiki-tags without Extension:Math enabled
+Tags which are hidden from Tidy cannot pass through the Sanitizer
 !! wikitext
-<math><img src="some evil external link"><script>some_evil_javascript();</script></math>
+<mw:toc><script>alert();</script></mw:toc>
 !! html+tidy
-<p>&lt;math&gt;&lt;img src="some evil external link"&gt;&lt;script&gt;some_evil_javascript();&lt;/script&gt;&lt;/math&gt;</p>
+<p>&lt;mw:toc&gt;&lt;script&gt;alert();&lt;/script&gt;&lt;/mw:toc&gt;</p>
 !! end
 
 ###
@@ -19860,18 +19863,18 @@ language=sr
 </p>
 !! end
 
-
 !! test
 Simple category in language variants
 !! options
 language=sr cat
 !! wikitext
 [[Category:МедиаWики Усер'с Гуиде]]
-!! html
+!! html/php
 cat=МедиаWики_Усер'с_Гуиде sort=
+!! html/parsoid
+<link rel="mw:PageProp/Category" href="./Категорија:МедиаWики_Усер'с_Гуиде" data-parsoid='{"stx":"simple","a":{"href":"./Категорија:МедиаWики_Усер&#39;с_Гуиде"},"sa":{"href":"Category:МедиаWики Усер&#39;с Гуиде"}}'/>
 !! end
 
-
 !! article
 Category:分类
 !! text
@@ -20905,6 +20908,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
@@ -21631,6 +21654,22 @@ __TOC__
 
 !! end
 
+!! test
+T35715: s/strike element in ToC
+!! wikitext
+__TOC__
+== <s>test</s> test <strike>test</strike> ==
+!! html
+<div id="toc" class="toc"><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#test_test_test"><span class="tocnumber">1</span> <span class="toctext"><s>test</s> test <strike>test</strike></span></a></li>
+</ul>
+</div>
+
+<h2><span class="mw-headline" id="test_test_test"><s>test</s> test <strike>test</strike></span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Parser_test&amp;action=edit&amp;section=1" title="Edit section: test test test">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+
+!! end
+
 # Note that the html output does not have the <p></p>, but the
 # html+tidy output *does*.  This is because the empty <p></p> is
 # removed by the sanitizer, but only when tidy is *not* enabled (!).
@@ -24784,9 +24823,16 @@ Improperly nested inline or quotes tags with whitespace in between
 Encapsulate protected attributes from wt
 !! wikitext
 <div typeof="mw:placeholder stuff" data-mw="whoo" data-parsoid="weird" data-parsoid-other="no" about="time" rel="mw:true">foo</div>
+
+{| typeof="mw:placeholder stuff" data-mw="whoo" data-parsoid="weird" data-parsoid-other="no" about="time" rel="mw:true"
+| ok
+|}
 !! html/parsoid
-<body><div data-x-typeof="mw:placeholder stuff" data-x-data-mw="whoo" data-x-data-parsoid="weird" data-x-data-parsoid-other="no" data-x-about="time" data-x-rel="mw:true">foo</div>
-</body>
+<div data-x-typeof="mw:placeholder stuff" data-x-data-mw="whoo" data-x-data-parsoid="weird" data-x-data-parsoid-other="no" data-x-about="time" data-x-rel="mw:true">foo</div>
+
+<table data-x-typeof="mw:placeholder stuff" data-x-data-mw="whoo" data-x-data-parsoid="weird" data-x-data-parsoid-other="no" data-x-about="time" data-x-rel="mw:true">
+<tbody><tr><td data-parsoid='{"autoInsertedEnd":true}'> ok</td></tr>
+</tbody></table>
 !!end
 
 ## Currently the p-wrapper is fragile in how it adds / removes transformations.
index 920dbb3..e53a958 100644 (file)
@@ -42,7 +42,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        /**
         * Primary database
         *
-        * @var DatabaseBase
+        * @var Database
         * @since 1.18
         */
        protected $db;
@@ -56,7 +56,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        private static $useTemporaryTables = true;
        private static $reuseDB = false;
        private static $dbSetup = false;
-       private static $oldTablePrefix = false;
+       private static $oldTablePrefix = '';
 
        /**
         * Original value of PHP's error_reporting setting.
@@ -336,6 +336,10 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
 
                JobQueueGroup::destroySingletons();
                ObjectCache::clear();
+               $services = MediaWikiServices::getInstance();
+               $services->resetServiceForTesting( 'MainObjectStash' );
+               $services->resetServiceForTesting( 'LocalServerObjectCache' );
+               $services->getMainWANObjectCache()->clearProcessCache();
                FileBackendGroup::destroySingleton();
 
                // TODO: move global state into MediaWikiServices
@@ -920,13 +924,22 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         *
         * Should be called from addDBData().
         *
-        * @since 1.25
-        * @param string $pageName Page name
+        * @since 1.25 ($namespace in 1.28)
+        * @param string|title $pageName Page name or title
         * @param string $text Page's content
+        * @param int $namespace Namespace id (name cannot already contain namespace)
         * @return array Title object and page id
         */
-       protected function insertPage( $pageName, $text = 'Sample page for unit test.' ) {
-               $title = Title::newFromText( $pageName, 0 );
+       protected function insertPage(
+               $pageName,
+               $text = 'Sample page for unit test.',
+               $namespace = null
+       ) {
+               if ( is_string( $pageName ) ) {
+                       $title = Title::newFromText( $pageName, $namespace );
+               } else {
+                       $title = $pageName;
+               }
 
                $user = static::getTestSysop()->getUser();
                $comment = __METHOD__ . ': Sample page for unit test.';
@@ -1061,11 +1074,11 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * Clones all tables in the given database (whatever database that connection has
         * open), to versions with the test prefix.
         *
-        * @param DatabaseBase $db Database to use
+        * @param Database $db Database to use
         * @param string $prefix Prefix to use for test tables
         * @return bool True if tables were cloned, false if only the prefix was changed
         */
-       protected static function setupDatabaseWithTestPrefix( DatabaseBase $db, $prefix ) {
+       protected static function setupDatabaseWithTestPrefix( Database $db, $prefix ) {
                $tablesCloned = self::listTables( $db );
                $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
                $dbClone->useTemporaryTables( self::$useTemporaryTables );
@@ -1114,12 +1127,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * @note this method only works when first called. Subsequent calls have no effect,
         * even if using different parameters.
         *
-        * @param DatabaseBase $db The database connection
+        * @param Database $db The database connection
         * @param string $prefix The prefix to use for the new table set (aka schema).
         *
         * @throws MWException If the database table prefix is already $prefix
         */
-       public static function setupTestDB( DatabaseBase $db, $prefix ) {
+       public static function setupTestDB( Database $db, $prefix ) {
                if ( self::$dbSetup ) {
                        return;
                }
@@ -1130,7 +1143,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                }
 
                // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
-               // and DatabaseBase no longer use global state.
+               // and Database no longer use global state.
 
                self::$dbSetup = true;
 
@@ -1169,19 +1182,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 Database[] Array of Database 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 Database instead of plain DBConnRef/IDatabase
+                               $lb = $externalStoreDB->getLoadBalancer( $cluster );
+                               $dbw = $lb->getConnection( DB_MASTER );
                                $dbws[] = $dbw;
                        }
                }
@@ -1213,7 +1230,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        /**
         * Empty all tables so they can be repopulated for tests
         *
-        * @param DatabaseBase $db|null Database to reset
+        * @param Database $db|null Database to reset
         * @param array $tablesUsed Tables to reset
         */
        private function resetDB( $db, $tablesUsed ) {
@@ -1296,18 +1313,21 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        /**
         * @since 1.18
         *
-        * @param DatabaseBase $db
+        * @param Database $db
         *
         * @return array
         */
-       public static function listTables( $db ) {
+       public static function listTables( Database $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 c76666d..8fbca6c 100644 (file)
@@ -11,7 +11,7 @@ class WfBCP47Test extends MediaWikiTestCase {
         * This test is used to verify our formatting against all lower and
         * all upper cases language code.
         *
-        * @see http://tools.ietf.org/html/bcp47
+        * @see https://tools.ietf.org/html/bcp47
         * @dataProvider provideLanguageCodes()
         */
        public function testBCP47( $code, $expected ) {
index 85c95e4..bc50966 100644 (file)
@@ -514,10 +514,6 @@ class HtmlTest extends MediaWikiTestCase {
                        'canvas', [ 'width' => 300 ]
                ];
 
-               $cases[] = [ '<command/>',
-                       'command', [ 'type' => 'command' ]
-               ];
-
                $cases[] = [ '<form></form>',
                        'form', [ 'action' => 'GET' ]
                ];
index 8c2b143..f054c0e 100644 (file)
@@ -147,9 +147,6 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
                        ->disableOriginalConstructor()
                        ->getMock();
 
-               $lbFactory->expects( $this->once() )
-                       ->method( 'destroy' );
-
                $newServices->redefineService(
                        'DBLoadBalancerFactory',
                        function() use ( $lbFactory ) {
@@ -164,12 +161,11 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
 
                try {
                        MediaWikiServices::getInstance()->getService( 'DBLoadBalancerFactory' );
-                       $this->fail( 'DBLoadBalancerFactory shoudl have been disabled' );
+                       $this->fail( 'DBLoadBalancerFactory should have been disabled' );
                }
                catch ( ServiceDisabledException $ex ) {
                        // ok, as expected
-               }
-               catch ( Throwable $ex ) {
+               } catch ( Throwable $ex ) {
                        $this->fail( 'ServiceDisabledException expected, caught ' . get_class( $ex ) );
                }
 
@@ -316,6 +312,7 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
                        'DBLoadBalancer' => [ 'DBLoadBalancer', 'LoadBalancer' ],
                        'WatchedItemStore' => [ 'WatchedItemStore', WatchedItemStore::class ],
                        'WatchedItemQueryService' => [ 'WatchedItemQueryService', WatchedItemQueryService::class ],
+                       'CryptRand' => [ 'CryptRand', CryptRand::class ],
                        'MediaHandlerFactory' => [ 'MediaHandlerFactory', MediaHandlerFactory::class ],
                        'GenderCache' => [ 'GenderCache', GenderCache::class ],
                        'LinkCache' => [ 'LinkCache', LinkCache::class ],
@@ -324,6 +321,10 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
                        '_MediaWikiTitleCodec' => [ '_MediaWikiTitleCodec', MediaWikiTitleCodec::class ],
                        'TitleFormatter' => [ 'TitleFormatter', TitleFormatter::class ],
                        'TitleParser' => [ 'TitleParser', TitleParser::class ],
+                       'ProxyLookup' => [ 'ProxyLookup', ProxyLookup::class ],
+                       'MainObjectStash' => [ 'MainObjectStash', BagOStuff::class ],
+                       'MainWANObjectCache' => [ 'MainWANObjectCache', WANObjectCache::class ],
+                       'LocalServerObjectCache' => [ 'LocalServerObjectCache', BagOStuff::class ],
                        'VirtualRESTServiceClient' => [ 'VirtualRESTServiceClient', VirtualRESTServiceClient::class ]
                ];
        }
index 0ec200c..bc43709 100644 (file)
@@ -2,8 +2,11 @@
 /**
  * @group Search
  * @group Database
+ * @covers PrefixSearch
  */
 class PrefixSearchTest extends MediaWikiLangTestCase {
+       const NS_NONCAP = 12346;
+
        private $originalHandlers;
 
        public function addDBDataOnce() {
@@ -31,6 +34,10 @@ class PrefixSearchTest extends MediaWikiLangTestCase {
                $this->insertPage( 'Talk:Example' );
 
                $this->insertPage( 'User:Example' );
+
+               $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'Bar' ) );
+               $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'Upper' ) );
+               $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'sandbox' ) );
        }
 
        protected function setUp() {
@@ -44,11 +51,17 @@ class PrefixSearchTest extends MediaWikiLangTestCase {
                $this->setMwGlobals( [
                        'wgSpecialPages' => [],
                        'wgHooks' => [],
+                       'wgExtraNamespaces' => [ self::NS_NONCAP => 'NonCap' ],
+                       'wgCapitalLinkOverrides' => [ self::NS_NONCAP => false ],
                ] );
 
                $this->originalHandlers = TestingAccessWrapper::newFromClass( 'Hooks' )->handlers;
                TestingAccessWrapper::newFromClass( 'Hooks' )->handlers = [];
 
+               // Clear caches so that our new namespace appears
+               MWNamespace::getCanonicalNamespaces( true );
+               Language::factory( 'en' )->resetNamespaces();
+
                SpecialPageFactory::resetList();
        }
 
@@ -158,6 +171,29 @@ class PrefixSearchTest extends MediaWikiLangTestCase {
                                        'Special:EditWatchlist/clear',
                                ],
                        ] ],
+                       [ [
+                               'Namespace with case sensitive first letter',
+                               'query' => 'NonCap:upper',
+                               'results' => []
+                       ] ],
+                       [ [
+                               'Multinamespace search',
+                               'query' => 'B',
+                               'results' => [
+                                       'Bar',
+                                       'NonCap:Bar',
+                               ],
+                               'namespaces' => [ NS_MAIN, self::NS_NONCAP ],
+                       ] ],
+                       [ [
+                               'Multinamespace search with lowercase first letter',
+                               'query' => 'sand',
+                               'results' => [
+                                       'Sandbox',
+                                       'NonCap:sandbox',
+                               ],
+                               'namespaces' => [ NS_MAIN, self::NS_NONCAP ],
+                       ] ],
                ];
        }
 
@@ -168,8 +204,11 @@ class PrefixSearchTest extends MediaWikiLangTestCase {
         */
        public function testSearch( array $case ) {
                $this->searchProvision( null );
+
+               $namespaces = isset( $case['namespaces'] ) ? $case['namespaces'] : [];
+
                $searcher = new StringPrefixSearch;
-               $results = $searcher->search( $case['query'], 3 );
+               $results = $searcher->search( $case['query'], 3, $namespaces );
                $this->assertEquals(
                        $case['results'],
                        $results,
@@ -184,8 +223,11 @@ class PrefixSearchTest extends MediaWikiLangTestCase {
         */
        public function testSearchWithOffset( array $case ) {
                $this->searchProvision( null );
+
+               $namespaces = isset( $case['namespaces'] ) ? $case['namespaces'] : [];
+
                $searcher = new StringPrefixSearch;
-               $results = $searcher->search( $case['query'], 3, [], 1 );
+               $results = $searcher->search( $case['query'], 3, $namespaces, 1 );
 
                // We don't expect the first result when offsetting
                array_shift( $case['results'] );
index 26529e8..c915b70 100644 (file)
@@ -314,6 +314,8 @@ class SanitizerTest extends MediaWikiTestCase {
                                '/* insecure input */',
                                'background-image: -moz-image-set("asdf.png" 1x, "asdf.png" 2x);'
                        ],
+                       [ '/* insecure input */', 'foo: attr( title, url );' ],
+                       [ '/* insecure input */', 'foo: attr( title url );' ],
                ];
        }
 
index 474a481..7e56ebf 100644 (file)
@@ -57,9 +57,11 @@ class StatusTest extends MediaWikiLangTestCase {
        }
 
        /**
+        * Test 'ok' and 'errors' getters.
         *
+        * @covers Status::__get
         */
-       public function testOkAndErrors() {
+       public function testOkAndErrorsGetters() {
                $status = Status::newGood( 'foo' );
                $this->assertTrue( $status->ok );
                $status = Status::newFatal( 'foo', 1, 2 );
@@ -76,6 +78,19 @@ class StatusTest extends MediaWikiLangTestCase {
                );
        }
 
+       /**
+        * Test 'ok' setter.
+        *
+        * @covers Status::__set
+        */
+       public function testOkSetter() {
+               $status = new Status();
+               $status->ok = false;
+               $this->assertFalse( $status->isOK() );
+               $status->ok = true;
+               $this->assertTrue( $status->isOK() );
+       }
+
        /**
         * @dataProvider provideSetResult
         * @covers Status::setResult
@@ -98,11 +113,12 @@ class StatusTest extends MediaWikiLangTestCase {
 
        /**
         * @dataProvider provideIsOk
-        * @covers Status::isOk
+        * @covers Status::setOK
+        * @covers Status::isOK
         */
        public function testIsOk( $ok ) {
                $status = new Status();
-               $status->ok = $ok;
+               $status->setOK( $ok );
                $this->assertEquals( $ok, $status->isOK() );
        }
 
@@ -128,7 +144,7 @@ class StatusTest extends MediaWikiLangTestCase {
         */
        public function testIsGood( $ok, $errors, $expected ) {
                $status = new Status();
-               $status->ok = $ok;
+               $status->setOK( $ok );
                foreach ( $errors as $error ) {
                        $status->warning( $error );
                }
@@ -171,6 +187,7 @@ class StatusTest extends MediaWikiLangTestCase {
         * @covers Status::error
         * @covers Status::getErrorsArray
         * @covers Status::getStatusArray
+        * @covers Status::getErrors
         */
        public function testErrorWithMessage( $mockDetails ) {
                $status = new Status();
@@ -361,7 +378,7 @@ class StatusTest extends MediaWikiLangTestCase {
                ];
 
                $status = new Status();
-               $status->ok = false;
+               $status->setOK( false );
                $testCases['GoodButNoError'] = [
                        $status,
                        "Internal error: Status::getWikiText: Invalid result object: no error text but not OK\n",
@@ -475,7 +492,7 @@ class StatusTest extends MediaWikiLangTestCase {
                ];
 
                $status = new Status();
-               $status->ok = false;
+               $status->setOK( false );
                $testCases['GoodButNoError'] = [
                        $status,
                        [ "Status::getMessage: Invalid result object: no error text but not OK\n" ],
@@ -645,4 +662,66 @@ class StatusTest extends MediaWikiLangTestCase {
                ];
        }
 
+       /**
+        * @dataProvider provideErrorsWarningsOnly
+        * @covers Status::splitByErrorType
+        * @covers StatusValue::splitByErrorType
+        */
+       public function testGetErrorsWarningsOnlyStatus( $errorText, $warningText, $type, $errorResult,
+               $warningResult
+       ) {
+               $status = Status::newGood();
+               if ( $errorText ) {
+                       $status->fatal( $errorText );
+               }
+               if ( $warningText ) {
+                       $status->warning( $warningText );
+               }
+               $testStatus = $status->splitByErrorType()[$type];
+               $this->assertEquals( $errorResult, $testStatus->getErrorsByType( 'error' ) );
+               $this->assertEquals( $warningResult, $testStatus->getErrorsByType( 'warning' ) );
+       }
+
+       public static function provideErrorsWarningsOnly() {
+               return [
+                       [
+                               'Just an error',
+                               'Just a warning',
+                               0,
+                               [
+                                       0 => [
+                                               'type' => 'error',
+                                               'message' => 'Just an error',
+                                               'params' => []
+                                       ],
+                               ],
+                               [],
+                       ], [
+                               'Just an error',
+                               'Just a warning',
+                               1,
+                               [],
+                               [
+                                       0 => [
+                                               'type' => 'warning',
+                                               'message' => 'Just a warning',
+                                               'params' => []
+                                       ],
+                               ],
+                       ], [
+                               null,
+                               null,
+                               1,
+                               [],
+                               [],
+                       ], [
+                               null,
+                               null,
+                               0,
+                               [],
+                               [],
+                       ]
+               ];
+       }
+
 }
index 1d232fe..93e0b57 100644 (file)
@@ -6,10 +6,10 @@
 class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
 
        /**
-        * @return PHPUnit_Framework_MockObject_MockObject|DatabaseBase
+        * @return PHPUnit_Framework_MockObject_MockObject|Database
         */
        private function getMockDb() {
-               $mock = $this->getMockBuilder( DatabaseBase::class )
+               $mock = $this->getMockBuilder( Database::class )
                        ->disableOriginalConstructor()
                        ->getMock();
 
index 030d9d5..c51d496 100644 (file)
@@ -28,12 +28,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->getMock();
                if ( $expectedConnectionType !== null ) {
                        $mock->expects( $this->any() )
-                               ->method( 'getConnection' )
+                               ->method( 'getConnectionRef' )
                                ->with( $expectedConnectionType )
                                ->will( $this->returnValue( $mockDb ) );
                } else {
                        $mock->expects( $this->any() )
-                               ->method( 'getConnection' )
+                               ->method( 'getConnectionRef' )
                                ->will( $this->returnValue( $mockDb ) );
                }
                $mock->expects( $this->any() )
index aade490..041e7e3 100644 (file)
@@ -10,12 +10,10 @@ class WebRequestTest extends MediaWikiTestCase {
                parent::setUp();
 
                $this->oldServer = $_SERVER;
-               IP::clearCaches();
        }
 
        protected function tearDown() {
                $_SERVER = $this->oldServer;
-               IP::clearCaches();
 
                parent::tearDown();
        }
@@ -367,7 +365,6 @@ class WebRequestTest extends MediaWikiTestCase {
        public function testGetIP( $expected, $input, $squid, $xffList, $private, $description ) {
                $_SERVER = $input;
                $this->setMwGlobals( [
-                       'wgSquidServersNoPurge' => $squid,
                        'wgUsePrivateIPs' => $private,
                        'wgHooks' => [
                                'IsTrustedProxy' => [
@@ -379,6 +376,8 @@ class WebRequestTest extends MediaWikiTestCase {
                        ]
                ] );
 
+               $this->setService( 'ProxyLookup', new ProxyLookup( [], $squid ) );
+
                $request = new WebRequest();
                $result = $request->getIP();
                $this->assertEquals( $expected, $result, $description );
@@ -564,6 +563,7 @@ class WebRequestTest extends MediaWikiTestCase {
                        'wgUsePrivateIPs' => false,
                        'wgHooks' => [],
                ] );
+               $this->setService( 'ProxyLookup', new ProxyLookup( [], [] ) );
 
                $request = new WebRequest();
                # Next call throw an exception about lacking an IP
index 334e3b8..c111949 100644 (file)
@@ -58,6 +58,29 @@ class ApiMainTest extends ApiTestCase {
                }
        }
 
+       /**
+        * Tests the assertuser= functionality
+        *
+        * @covers ApiMain::checkAsserts
+        */
+       public function testAssertUser() {
+               $user = $this->getTestUser()->getUser();
+               $this->doApiRequest( [
+                       'action' => 'query',
+                       'assertuser' => $user->getName(),
+               ], null, null, $user );
+
+               try {
+                       $this->doApiRequest( [
+                               'action' => 'query',
+                               'assertuser' => $user->getName() . 'X',
+                       ], null, null, $user );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( UsageException $e ) {
+                       $this->assertEquals( $e->getCodeString(), 'assertnameduserfailed' );
+               }
+       }
+
        /**
         * Test if all classes in the main module manager exists
         */
index 39e90c2..5358f29 100644 (file)
@@ -9,11 +9,12 @@ class ApiOpenSearchTest extends MediaWikiTestCase {
                        ->method( 'getSearchTypes' )
                        ->will( $this->returnValue( [ 'the one ring' ] ) );
 
+               $api = $this->createApi();
                $engine = $this->replaceSearchEngine();
                $engine->expects( $this->any() )
                        ->method( 'getProfiles' )
                        ->will( $this->returnValueMap( [
-                               [ SearchEngine::COMPLETION_PROFILE_TYPE, [
+                               [ SearchEngine::COMPLETION_PROFILE_TYPE, $api->getUser(), [
                                        [
                                                'name' => 'normal',
                                                'desc-message' => 'normal-message',
@@ -26,7 +27,6 @@ class ApiOpenSearchTest extends MediaWikiTestCase {
                                ] ],
                        ] ) );
 
-               $api = $this->createApi();
                $params = $api->getAllowedParams();
 
                $this->assertArrayNotHasKey( 'offset', $params );
index 582c076..d6f315d 100644 (file)
@@ -477,6 +477,7 @@ class ApiQueryWatchlistRawIntegrationTest extends ApiTestCase {
                        new TitleValue( 1, 'ApiQueryWatchlistRawIntegrationTestPage1' ),
                ] );
 
+               ObjectCache::getMainWANInstance()->clearProcessCache();
                $result = $this->doListWatchlistRawRequest( [
                        'wrowner' => $otherUser->getName(),
                        'wrtoken' => '1234567890',
index 7c0063d..48472cf 100644 (file)
@@ -1323,350 +1323,6 @@ class ApiResultTest extends MediaWikiTestCase {
                ], ApiResult::addMetadataToResultVars( $arr ) );
        }
 
-       /**
-        * @covers ApiResult
-        */
-       public function testDeprecatedFunctions() {
-               // Ignore ApiResult deprecation warnings during this test
-               set_error_handler( function ( $errno, $errstr ) use ( &$warnings ) {
-                       if ( preg_match( '/Use of ApiResult::\S+ was deprecated in MediaWiki \d+.\d+\./', $errstr ) ) {
-                               return true;
-                       }
-                       if ( preg_match( '/Use of ApiMain to ApiResult::__construct ' .
-                               'was deprecated in MediaWiki \d+.\d+\./', $errstr ) ) {
-                               return true;
-                       }
-                       return false;
-               } );
-               $reset = new ScopedCallback( 'restore_error_handler' );
-
-               $context = new DerivativeContext( RequestContext::getMain() );
-               $context->setConfig( new HashConfig( [
-                       'APIModules' => [],
-                       'APIFormatModules' => [],
-                       'APIMaxResultSize' => 42,
-               ] ) );
-               $main = new ApiMain( $context );
-               $result = TestingAccessWrapper::newFromObject( new ApiResult( $main ) );
-               $this->assertSame( 42, $result->maxSize );
-               $this->assertSame( $main->getErrorFormatter(), $result->errorFormatter );
-               $this->assertSame( $main, $result->mainForContinuation );
-
-               $result = new ApiResult( 8388608 );
-
-               $result->addContentValue( null, 'test', 'content' );
-               $result->addContentValue( [ 'foo', 'bar' ], 'test', 'content' );
-               $result->addIndexedTagName( null, 'itn' );
-               $result->addSubelementsList( null, [ 'sub' ] );
-               $this->assertSame( [
-                       'foo' => [
-                               'bar' => [
-                                       '*' => 'content',
-                               ],
-                       ],
-                       '*' => 'content',
-               ], $result->getData() );
-
-               $arr = [];
-               ApiResult::setContent( $arr, 'value' );
-               ApiResult::setContent( $arr, 'value2', 'foobar' );
-               $this->assertSame( [
-                       ApiResult::META_CONTENT => 'content',
-                       'content' => 'value',
-                       'foobar' => [
-                               ApiResult::META_CONTENT => 'content',
-                               'content' => 'value2',
-                       ],
-               ], $arr );
-
-               $result = new ApiResult( 3 );
-               $formatter = new ApiErrorFormatter_BackCompat( $result );
-               $result->setErrorFormatter( $formatter );
-               $result->disableSizeCheck();
-               $this->assertTrue( $result->addValue( null, 'foo', '1234567890' ) );
-               $result->enableSizeCheck();
-               $this->assertSame( 0, $result->getSize() );
-               $this->assertFalse( $result->addValue( null, 'foo', '1234567890' ) );
-
-               $arr = [ 'foo' => [ 'bar' => 1 ] ];
-               $result->setIndexedTagName_recursive( $arr, 'itn' );
-               $this->assertSame( [
-                       'foo' => [
-                               'bar' => 1,
-                               ApiResult::META_INDEXED_TAG_NAME => 'itn'
-                       ],
-               ], $arr );
-
-               $status = Status::newGood();
-               $status->fatal( 'parentheses', '1' );
-               $status->fatal( 'parentheses', '2' );
-               $status->warning( 'parentheses', '3' );
-               $status->warning( 'parentheses', '4' );
-               $this->assertSame( [
-                       [
-                               'type' => 'error',
-                               'message' => 'parentheses',
-                               'params' => [
-                                       0 => '1',
-                                       ApiResult::META_INDEXED_TAG_NAME => 'param',
-                               ],
-                       ],
-                       [
-                               'type' => 'error',
-                               'message' => 'parentheses',
-                               'params' => [
-                                       0 => '2',
-                                       ApiResult::META_INDEXED_TAG_NAME => 'param',
-                               ],
-                       ],
-                       ApiResult::META_INDEXED_TAG_NAME => 'error',
-               ], $result->convertStatusToArray( $status, 'error' ) );
-               $this->assertSame( [
-                       [
-                               'type' => 'warning',
-                               'message' => 'parentheses',
-                               'params' => [
-                                       0 => '3',
-                                       ApiResult::META_INDEXED_TAG_NAME => 'param',
-                               ],
-                       ],
-                       [
-                               'type' => 'warning',
-                               'message' => 'parentheses',
-                               'params' => [
-                                       0 => '4',
-                                       ApiResult::META_INDEXED_TAG_NAME => 'param',
-                               ],
-                       ],
-                       ApiResult::META_INDEXED_TAG_NAME => 'warning',
-               ], $result->convertStatusToArray( $status, 'warning' ) );
-       }
-
-       /**
-        * @covers ApiResult
-        */
-       public function testDeprecatedContinuation() {
-               // Ignore ApiResult deprecation warnings during this test
-               set_error_handler( function ( $errno, $errstr ) use ( &$warnings ) {
-                       if ( preg_match( '/Use of ApiResult::\S+ was deprecated in MediaWiki \d+.\d+\./', $errstr ) ) {
-                               return true;
-                       }
-                       return false;
-               } );
-
-               $reset = new ScopedCallback( 'restore_error_handler' );
-               $allModules = [
-                       new MockApiQueryBase( 'mock1' ),
-                       new MockApiQueryBase( 'mock2' ),
-                       new MockApiQueryBase( 'mocklist' ),
-               ];
-               $generator = new MockApiQueryBase( 'generator' );
-
-               $main = new ApiMain( RequestContext::getMain() );
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $ret = $result->beginContinuation( null, $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( [ false, $allModules ], $ret );
-               $result->setContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
-               $result->setContinueParam( $allModules[2], 'mlcontinue', 2 );
-               $result->setGeneratorContinueParam( $generator, 'gcontinue', 3 );
-               $result->endContinuation( 'raw' );
-               $result->endContinuation( 'standard' );
-               $this->assertSame( [
-                       'mlcontinue' => 2,
-                       'm1continue' => '1|2',
-                       'continue' => '||mock2',
-               ], $result->getResultData( 'continue' ) );
-               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
-               $this->assertSame( [
-                       'mock1' => [ 'm1continue' => '1|2' ],
-                       'mocklist' => [ 'mlcontinue' => 2 ],
-                       'generator' => [ 'gcontinue' => 3 ],
-               ], $result->getResultData( 'query-continue' ) );
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $ret = $result->beginContinuation( null, $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( [ false, $allModules ], $ret );
-               $result->setContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
-               $result->setGeneratorContinueParam( $generator, 'gcontinue', [ 3, 4 ] );
-               $result->endContinuation( 'raw' );
-               $result->endContinuation( 'standard' );
-               $this->assertSame( [
-                       'm1continue' => '1|2',
-                       'continue' => '||mock2|mocklist',
-               ], $result->getResultData( 'continue' ) );
-               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
-               $this->assertSame( [
-                       'mock1' => [ 'm1continue' => '1|2' ],
-                       'generator' => [ 'gcontinue' => '3|4' ],
-               ], $result->getResultData( 'query-continue' ) );
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $ret = $result->beginContinuation( null, $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( [ false, $allModules ], $ret );
-               $result->setContinueParam( $allModules[2], 'mlcontinue', 2 );
-               $result->setGeneratorContinueParam( $generator, 'gcontinue', 3 );
-               $result->endContinuation( 'raw' );
-               $result->endContinuation( 'standard' );
-               $this->assertSame( [
-                       'mlcontinue' => 2,
-                       'gcontinue' => 3,
-                       'continue' => 'gcontinue||',
-               ], $result->getResultData( 'continue' ) );
-               $this->assertSame( true, $result->getResultData( 'batchcomplete' ) );
-               $this->assertSame( [
-                       'mocklist' => [ 'mlcontinue' => 2 ],
-                       'generator' => [ 'gcontinue' => 3 ],
-               ], $result->getResultData( 'query-continue' ) );
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $ret = $result->beginContinuation( null, $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( [ false, $allModules ], $ret );
-               $result->setGeneratorContinueParam( $generator, 'gcontinue', 3 );
-               $result->endContinuation( 'raw' );
-               $result->endContinuation( 'standard' );
-               $this->assertSame( [
-                       'gcontinue' => 3,
-                       'continue' => 'gcontinue||mocklist',
-               ], $result->getResultData( 'continue' ) );
-               $this->assertSame( true, $result->getResultData( 'batchcomplete' ) );
-               $this->assertSame( [
-                       'generator' => [ 'gcontinue' => 3 ],
-               ], $result->getResultData( 'query-continue' ) );
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $ret = $result->beginContinuation( null, $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( [ false, $allModules ], $ret );
-               $result->setContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
-               $result->setContinueParam( $allModules[2], 'mlcontinue', 2 );
-               $result->endContinuation( 'raw' );
-               $result->endContinuation( 'standard' );
-               $this->assertSame( [
-                       'mlcontinue' => 2,
-                       'm1continue' => '1|2',
-                       'continue' => '||mock2',
-               ], $result->getResultData( 'continue' ) );
-               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
-               $this->assertSame( [
-                       'mock1' => [ 'm1continue' => '1|2' ],
-                       'mocklist' => [ 'mlcontinue' => 2 ],
-               ], $result->getResultData( 'query-continue' ) );
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $ret = $result->beginContinuation( null, $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( [ false, $allModules ], $ret );
-               $result->setContinueParam( $allModules[0], 'm1continue', [ 1, 2 ] );
-               $result->endContinuation( 'raw' );
-               $result->endContinuation( 'standard' );
-               $this->assertSame( [
-                       'm1continue' => '1|2',
-                       'continue' => '||mock2|mocklist',
-               ], $result->getResultData( 'continue' ) );
-               $this->assertSame( null, $result->getResultData( 'batchcomplete' ) );
-               $this->assertSame( [
-                       'mock1' => [ 'm1continue' => '1|2' ],
-               ], $result->getResultData( 'query-continue' ) );
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $ret = $result->beginContinuation( null, $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( [ false, $allModules ], $ret );
-               $result->setContinueParam( $allModules[2], 'mlcontinue', 2 );
-               $result->endContinuation( 'raw' );
-               $result->endContinuation( 'standard' );
-               $this->assertSame( [
-                       'mlcontinue' => 2,
-                       'continue' => '-||mock1|mock2',
-               ], $result->getResultData( 'continue' ) );
-               $this->assertSame( true, $result->getResultData( 'batchcomplete' ) );
-               $this->assertSame( [
-                       'mocklist' => [ 'mlcontinue' => 2 ],
-               ], $result->getResultData( 'query-continue' ) );
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $ret = $result->beginContinuation( null, $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame( [ false, $allModules ], $ret );
-               $result->endContinuation( 'raw' );
-               $result->endContinuation( 'standard' );
-               $this->assertSame( null, $result->getResultData( 'continue' ) );
-               $this->assertSame( true, $result->getResultData( 'batchcomplete' ) );
-               $this->assertSame( null, $result->getResultData( 'query-continue' ) );
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $ret = $result->beginContinuation( '||mock2', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame(
-                       [ false, array_values( array_diff_key( $allModules, [ 1 => 1 ] ) ) ],
-                       $ret
-               );
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $ret = $result->beginContinuation( '-||', $allModules, [ 'mock1', 'mock2' ] );
-               $this->assertSame(
-                       [ true, array_values( array_diff_key( $allModules, [ 0 => 0, 1 => 1 ] ) ) ],
-                       $ret
-               );
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               try {
-                       $result->beginContinuation( 'foo', $allModules, [ 'mock1', 'mock2' ] );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( UsageException $ex ) {
-                       $this->assertSame(
-                               'Invalid continue param. You should pass the original value returned by the previous query',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               $main->setContinuationManager( null );
-
-               $result = new ApiResult( 8388608 );
-               $result->setMainForContinuation( $main );
-               $result->beginContinuation( '||mock2', array_slice( $allModules, 0, 2 ),
-                       [ 'mock1', 'mock2' ] );
-               try {
-                       $result->setContinueParam( $allModules[1], 'm2continue', 1 );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( UnexpectedValueException $ex ) {
-                       $this->assertSame(
-                               'Module \'mock2\' was not supposed to have been executed, but it was executed anyway',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               try {
-                       $result->setContinueParam( $allModules[2], 'mlcontinue', 1 );
-                       $this->fail( 'Expected exception not thrown' );
-               } catch ( UnexpectedValueException $ex ) {
-                       $this->assertSame(
-                               'Module \'mocklist\' called ApiContinuationManager::addContinueParam ' .
-                                       'but was not passed to ApiContinuationManager::__construct',
-                               $ex->getMessage(),
-                               'Expected exception'
-                       );
-               }
-               $main->setContinuationManager( null );
-
-       }
-
        public function testObjectSerialization() {
                $arr = [];
                ApiResult::setValue( $arr, 'foo', (object)[ 'a' => 1, 'b' => 2 ] );
index 78cb7fb..d5c17ee 100644 (file)
@@ -218,7 +218,7 @@ class RandomImageGenerator {
        }
 
        /**
-        * Given array( array('x' => 10, 'y' => 20), array( 'x' => 30, y=> 5 ) )
+        * Given [ [ 'x' => 10, 'y' => 20 ], [ 'x' => 30, y=> 5 ] ]
         * returns "10,20 30,5"
         * Useful for SVG and imagemagick command line arguments
         * @param array $shape Array of arrays, each array containing x & y keys mapped to numeric values
@@ -430,7 +430,7 @@ class RandomImageGenerator {
 
        /**
         * Get an array of random pairs of random words, like
-        * array( array( 'foo', 'bar' ), array( 'quux', 'baz' ) );
+        * [ [ 'foo', 'bar' ], [ 'quux', 'baz' ] ];
         *
         * @param int $number Number of pairs
         * @return array Two-element arrays
index f679f63..dc6fc62 100644 (file)
@@ -2674,7 +2674,7 @@ class AuthManagerTest extends \MediaWikiTestCase {
                $this->assertEquals( 0, $session->getUser()->getId() );
                $this->assertSame( [
                        [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
-                       [ LogLevel::ERROR, '{username} failed with message {message}' ],
+                       [ LogLevel::ERROR, '{username} failed with message {msg}' ],
                ], $logger->getBuffer() );
                $logger->clearBuffer();
                $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
index 477b161..194b49e 100644 (file)
@@ -16,6 +16,7 @@ class AuthenticationResponseTest extends \MediaWikiTestCase {
        public function testConstructors( $constructor, $args, $expect ) {
                if ( is_array( $expect ) ) {
                        $res = new AuthenticationResponse();
+                       $res->messageType = 'warning';
                        foreach ( $expect as $field => $value ) {
                                $res->$field = $value;
                        }
@@ -51,6 +52,7 @@ class AuthenticationResponseTest extends \MediaWikiTestCase {
                        [ 'newFail', [ $msg ], [
                                'status' => AuthenticationResponse::FAIL,
                                'message' => $msg,
+                               'messageType' => 'error',
                        ] ],
 
                        [ 'newRestart', [ $msg ], [
@@ -66,6 +68,21 @@ class AuthenticationResponseTest extends \MediaWikiTestCase {
                                'status' => AuthenticationResponse::UI,
                                'neededRequests' => [ $req ],
                                'message' => $msg,
+                               'messageType' => 'warning',
+                       ] ],
+
+                       [ 'newUI', [ [ $req ], $msg, 'warning' ], [
+                               'status' => AuthenticationResponse::UI,
+                               'neededRequests' => [ $req ],
+                               'message' => $msg,
+                               'messageType' => 'warning',
+                       ] ],
+
+                       [ 'newUI', [ [ $req ], $msg, 'error' ], [
+                               'status' => AuthenticationResponse::UI,
+                               'neededRequests' => [ $req ],
+                               'message' => $msg,
+                               'messageType' => 'error',
                        ] ],
                        [ 'newUI', [ [], $msg ],
                                new \InvalidArgumentException( '$reqs may not be empty' )
index 515a5b3..8161ed4 100644 (file)
@@ -122,6 +122,8 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
        }
 
        public function testTestUserCanAuthenticate() {
+               $user = self::getMutableTestUser()->getUser();
+
                $dbw = wfGetDB( DB_MASTER );
 
                $passwordFactory = new \PasswordFactory();
@@ -142,9 +144,9 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                                'user_newpassword' => \PasswordFactory::newInvalidPassword()->toString(),
                                'user_newpass_time' => null,
                        ],
-                       [ 'user_name' => 'UTSysop' ]
+                       [ 'user_id' => $user->getId() ]
                );
-               $this->assertFalse( $provider->testUserCanAuthenticate( 'UTSysop' ) );
+               $this->assertFalse( $provider->testUserCanAuthenticate( $user->getName() ) );
 
                $dbw->update(
                        'user',
@@ -152,10 +154,10 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                                'user_newpassword' => $pwhash,
                                'user_newpass_time' => null,
                        ],
-                       [ 'user_name' => 'UTSysop' ]
+                       [ 'user_id' => $user->getId() ]
                );
-               $this->assertTrue( $provider->testUserCanAuthenticate( 'UTSysop' ) );
-               $this->assertTrue( $provider->testUserCanAuthenticate( 'uTSysop' ) );
+               $this->assertTrue( $provider->testUserCanAuthenticate( $user->getName() ) );
+               $this->assertTrue( $provider->testUserCanAuthenticate( lcfirst( $user->getName() ) ) );
 
                $dbw->update(
                        'user',
@@ -163,12 +165,12 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                                'user_newpassword' => $pwhash,
                                'user_newpass_time' => $dbw->timestamp( time() - 10 ),
                        ],
-                       [ 'user_name' => 'UTSysop' ]
+                       [ 'user_id' => $user->getId() ]
                );
                $providerPriv->newPasswordExpiry = 100;
-               $this->assertTrue( $provider->testUserCanAuthenticate( 'UTSysop' ) );
+               $this->assertTrue( $provider->testUserCanAuthenticate( $user->getName() ) );
                $providerPriv->newPasswordExpiry = 1;
-               $this->assertFalse( $provider->testUserCanAuthenticate( 'UTSysop' ) );
+               $this->assertFalse( $provider->testUserCanAuthenticate( $user->getName() ) );
 
                $dbw->update(
                        'user',
@@ -176,7 +178,7 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                                'user_newpassword' => \PasswordFactory::newInvalidPassword()->toString(),
                                'user_newpass_time' => null,
                        ],
-                       [ 'user_name' => 'UTSysop' ]
+                       [ 'user_id' => $user->getId() ]
                );
        }
 
@@ -229,13 +231,15 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
        }
 
        public function testAuthentication() {
+               $user = self::getMutableTestUser()->getUser();
+
                $password = 'TemporaryPassword';
                $hash = ':A:' . md5( $password );
                $dbw = wfGetDB( DB_MASTER );
                $dbw->update(
                        'user',
                        [ 'user_newpassword' => $hash, 'user_newpass_time' => $dbw->timestamp( time() - 10 ) ],
-                       [ 'user_name' => 'UTSysop' ]
+                       [ 'user_id' => $user->getId() ]
                );
 
                $req = new PasswordAuthenticationRequest();
@@ -284,7 +288,7 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                );
 
                // Validation failure
-               $req->username = 'UTSysop';
+               $req->username = $user->getName();
                $req->password = $password;
                $this->validity = \Status::newFatal( 'arbitrary-failure' );
                $ret = $provider->beginPrimaryAuthentication( $reqs );
@@ -301,20 +305,20 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                $this->manager->removeAuthenticationSessionData( null );
                $this->validity = \Status::newGood();
                $this->assertEquals(
-                       AuthenticationResponse::newPass( 'UTSysop' ),
+                       AuthenticationResponse::newPass( $user->getName() ),
                        $provider->beginPrimaryAuthentication( $reqs )
                );
                $this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
 
                $this->manager->removeAuthenticationSessionData( null );
                $this->validity = \Status::newGood();
-               $req->username = 'uTSysop';
+               $req->username = lcfirst( $user->getName() );
                $this->assertEquals(
-                       AuthenticationResponse::newPass( 'UTSysop' ),
+                       AuthenticationResponse::newPass( $user->getName() ),
                        $provider->beginPrimaryAuthentication( $reqs )
                );
                $this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
-               $req->username = 'UTSysop';
+               $req->username = $user->getName();
 
                // Expired password
                $providerPriv->newPasswordExpiry = 1;
@@ -408,20 +412,19 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                $oldpass = 'OldTempPassword';
                $newpass = 'NewTempPassword';
 
-               $hash = ':A:' . md5( $oldpass );
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->update(
-                       'user',
-                       [ 'user_newpassword' => $hash, 'user_newpass_time' => $dbw->timestamp( time() + 10 ) ],
-                       [ 'user_name' => 'UTSysop' ]
-               );
-
                $dbw = wfGetDB( DB_MASTER );
                $oldHash = $dbw->selectField( 'user', 'user_newpassword', [ 'user_name' => $cuser ] );
                $cb = new \ScopedCallback( function () use ( $dbw, $cuser, $oldHash ) {
                        $dbw->update( 'user', [ 'user_newpassword' => $oldHash ], [ 'user_name' => $cuser ] );
                } );
 
+               $hash = ':A:' . md5( $oldpass );
+               $dbw->update(
+                       'user',
+                       [ 'user_newpassword' => $hash, 'user_newpass_time' => $dbw->timestamp( time() + 10 ) ],
+                       [ 'user_name' => $cuser ]
+               );
+
                $provider = $this->getProvider();
 
                // Sanity check
@@ -500,22 +503,15 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
        }
 
        public function testProviderChangeAuthenticationDataEmail() {
+               $user = self::getMutableTestUser()->getUser();
+
                $dbw = wfGetDB( DB_MASTER );
                $dbw->update(
                        'user',
                        [ 'user_newpass_time' => $dbw->timestamp( time() - 5 * 3600 ) ],
-                       [ 'user_name' => 'UTSysop' ]
+                       [ 'user_id' => $user->getId() ]
                );
 
-               $user = \User::newFromName( 'UTSysop' );
-               $reset = new \ScopedCallback( function ( $email ) use ( $user ) {
-                       $user->setEmail( $email );
-                       $user->saveSettings();
-               }, [ $user->getEmail() ] );
-
-               $user->setEmail( 'test@localhost.localdomain' );
-               $user->saveSettings();
-
                $req = TemporaryPasswordAuthenticationRequest::newRandom();
                $req->username = $user->getName();
                $req->mailpassword = true;
@@ -539,7 +535,7 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                $dbw->update(
                        'user',
                        [ 'user_newpass_time' => $dbw->timestamp( time() + 5 * 3600 ) ],
-                       [ 'user_name' => 'UTSysop' ]
+                       [ 'user_id' => $user->getId() ]
                );
                $provider = $this->getProvider( [ 'passwordReminderResendTime' => 0 ] );
                $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
@@ -563,16 +559,16 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
                $this->assertEquals( \StatusValue::newGood(), $status );
 
-               $req->caller = 'UTSysop';
+               $req->caller = $user->getName();
                $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
                $this->assertEquals( \StatusValue::newGood(), $status );
 
                $mailed = false;
                $resetMailer = $this->hookMailer( function ( $headers, $to, $from, $subject, $body )
-                       use ( &$mailed, $req )
+                       use ( &$mailed, $req, $user )
                {
                        $mailed = true;
-                       $this->assertSame( 'test@localhost.localdomain', $to[0]->address );
+                       $this->assertSame( $user->getEmail(), $to[0]->address );
                        $this->assertContains( $req->password, $body );
                        return false;
                } );
@@ -658,12 +654,10 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                $this->assertEquals( $expect, $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) );
                $this->assertNull( $this->manager->getAuthenticationSessionData( 'no-email' ) );
 
-               // We have to cheat a bit to avoid having to add a new user to
-               // the database to test the actual setting of the password works right
-               $user = \User::newFromName( 'UTSysop' );
+               $user = self::getMutableTestUser()->getUser();
                $req->username = $authreq->username = $user->getName();
                $req->password = $authreq->password = 'NewPassword';
-               $expect = AuthenticationResponse::newPass( 'UTSysop' );
+               $expect = AuthenticationResponse::newPass( $user->getName() );
                $expect->createRequest = $req;
 
                $res2 = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
@@ -680,12 +674,8 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
 
        public function testAccountCreationEmail() {
                $creator = \User::newFromName( 'Foo' );
-               $user = \User::newFromName( 'UTSysop' );
-               $reset = new \ScopedCallback( function ( $email ) use ( $user ) {
-                       $user->setEmail( $email );
-                       $user->saveSettings();
-               }, [ $user->getEmail() ] );
 
+               $user = self::getMutableTestUser()->getUser();
                $user->setEmail( null );
 
                $req = TemporaryPasswordAuthenticationRequest::newRandom();
@@ -722,9 +712,9 @@ class TemporaryPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestC
                        return false;
                } );
 
-               $expect = AuthenticationResponse::newPass( 'UTSysop' );
+               $expect = AuthenticationResponse::newPass( $user->getName() );
                $expect->createRequest = clone( $req );
-               $expect->createRequest->username = 'UTSysop';
+               $expect->createRequest->username = $user->getName();
                $res = $provider->beginPrimaryAccountCreation( $user, $creator, [ $req ] );
                $this->assertEquals( $expect, $res );
                $this->assertTrue( $this->manager->getAuthenticationSessionData( 'no-email' ) );
index aa6f0e8..20f4cbc 100644 (file)
@@ -12,7 +12,10 @@ class ThrottlePreAuthenticationProviderTest extends \MediaWikiTestCase {
                $provider = new ThrottlePreAuthenticationProvider();
                $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
                $config = new \HashConfig( [
-                       'AccountCreationThrottle' => 123,
+                       'AccountCreationThrottle' => [ [
+                               'count' => 123,
+                               'seconds' => 86400,
+                       ] ],
                        'PasswordAttemptThrottle' => [ [
                                'count' => 5,
                                'seconds' => 300,
@@ -38,7 +41,10 @@ class ThrottlePreAuthenticationProviderTest extends \MediaWikiTestCase {
                ] );
                $providerPriv = \TestingAccessWrapper::newFromObject( $provider );
                $config = new \HashConfig( [
-                       'AccountCreationThrottle' => 123,
+                       'AccountCreationThrottle' => [ [
+                               'count' => 123,
+                               'seconds' => 86400,
+                       ] ],
                        'PasswordAttemptThrottle' => [ [
                                'count' => 5,
                                'seconds' => 300,
@@ -122,18 +128,18 @@ class ThrottlePreAuthenticationProviderTest extends \MediaWikiTestCase {
                }
 
                $this->assertEquals(
-                       \StatusValue::newGood(),
-                       $provider->testForAccountCreation( $user, $creator, [] ),
+                       true,
+                       $provider->testForAccountCreation( $user, $creator, [] )->isOK(),
                        'attempt #1'
                );
                $this->assertEquals(
-                       \StatusValue::newGood(),
-                       $provider->testForAccountCreation( $user, $creator, [] ),
+                       true,
+                       $provider->testForAccountCreation( $user, $creator, [] )->isOK(),
                        'attempt #2'
                );
                $this->assertEquals(
-                       $succeed ? \StatusValue::newGood() : \StatusValue::newFatal( 'acct_creation_throttle_hit', 2 ),
-                       $provider->testForAccountCreation( $user, $creator, [] ),
+                       $succeed ? true : false,
+                       $provider->testForAccountCreation( $user, $creator, [] )->isOK(),
                        'attempt #3'
                );
        }
diff --git a/tests/phpunit/includes/content/FileContentHandlerTest.php b/tests/phpunit/includes/content/FileContentHandlerTest.php
new file mode 100644 (file)
index 0000000..276a86e
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @group ContentHandler
+ */
+class FileContentHandlerTest extends MediaWikiLangTestCase {
+       /**
+        * @var FileContentHandler
+        */
+       private $handler;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->handler = new FileContentHandler();
+       }
+
+       public function testIndexMapping() {
+               $mockEngine = $this->getMock( 'SearchEngine' );
+
+               $mockEngine->expects( $this->atLeastOnce() )
+                       ->method( 'makeSearchFieldMapping' )
+                       ->willReturnCallback( function ( $name, $type ) {
+                               $mockField =
+                                       $this->getMockBuilder( 'SearchIndexFieldDefinition' )
+                                               ->setMethods( [ 'getMapping' ] )
+                                               ->setConstructorArgs( [ $name, $type ] )
+                                               ->getMock();
+                               return $mockField;
+                       } );
+
+               $map = $this->handler->getFieldsForSearchIndex( $mockEngine );
+               $expect = [
+                       'file_media_type' => 1,
+                       'file_mime' => 1,
+                       'file_size' => 1,
+                       'file_width' => 1,
+                       'file_height' => 1,
+                       'file_bits' => 1,
+                       'file_resolution' => 1,
+                       'file_text' => 1,
+               ];
+               foreach ( $map as $name => $field ) {
+                       $this->assertInstanceOf( 'SearchIndexField', $field );
+                       $this->assertEquals( $name, $field->getName() );
+                       unset( $expect[$name] );
+               }
+               $this->assertEmpty( $expect );
+       }
+}
index 9d4abe8..ec97d76 100644 (file)
@@ -249,11 +249,20 @@ class WikitextContentHandlerTest extends MediaWikiLangTestCase {
                $title = Title::newFromText( 'Somefile.jpg', NS_FILE );
                $page = new WikiPage( $title );
 
+               $fileHandler = $this->getMockBuilder( FileContentHandler::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'getDataForSearchIndex' ] )
+                       ->getMock();
+
                $handler = $this->getMockBuilder( WikitextContentHandler::class )
                        ->disableOriginalConstructor()
-                       ->setMethods( [ 'getFileText' ] )
+                       ->setMethods( [ 'getFileHandler' ] )
                        ->getMock();
-               $handler->method( 'getFileText' )->will( $this->returnValue( 'This is file content' ) );
+
+               $handler->method( 'getFileHandler' )->will( $this->returnValue( $fileHandler ) );
+               $fileHandler->expects( $this->once() )
+                       ->method( 'getDataForSearchIndex' )
+                       ->will( $this->returnValue( [ 'file_text' => 'This is file content' ] ) );
 
                $data = $handler->getDataForSearchIndex( $page, new ParserOutput(), $mockEngine );
                $this->assertArrayHasKey( 'file_text', $data );
index f13ead4..dbb126f 100644 (file)
  * Fake class around abstract class so we can call concrete methods.
  */
 class FakeDatabaseMysqlBase extends DatabaseMysqlBase {
-       // From DatabaseBase
+       // From Database
        function __construct() {
                $this->profiler = new ProfilerStub( [] );
                $this->trxProfiler = new TransactionProfiler();
+               $this->cliMode = true;
+               $this->connLogger = new \Psr\Log\NullLogger();
+               $this->queryLogger = new \Psr\Log\NullLogger();
+               $this->errorLogger = function ( Exception $e ) {
+                       wfWarn( get_class( $e ) . ": {$e->getMessage()}" );
+               };
+               $this->currentDomain = DatabaseDomain::newUnspecified();
        }
 
        protected function closeConnection() {
@@ -82,7 +89,6 @@ class FakeDatabaseMysqlBase extends DatabaseMysqlBase {
 
        }
 
-       // From interface DatabaseType
        function insertId() {
        }
 
@@ -171,16 +177,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;
        }
        /**
@@ -189,9 +191,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() );
 
@@ -206,42 +205,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
         */
index e7eeff9..656e661 100644 (file)
@@ -26,7 +26,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideSelect
-        * @covers DatabaseBase::select
+        * @covers Database::select
         */
        public function testSelect( $sql, $sqlText ) {
                $this->database->select(
@@ -132,7 +132,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideUpdate
-        * @covers DatabaseBase::update
+        * @covers Database::update
         */
        public function testUpdate( $sql, $sqlText ) {
                $this->database->update(
@@ -184,7 +184,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideDelete
-        * @covers DatabaseBase::delete
+        * @covers Database::delete
         */
        public function testDelete( $sql, $sqlText ) {
                $this->database->delete(
@@ -217,7 +217,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideUpsert
-        * @covers DatabaseBase::upsert
+        * @covers Database::upsert
         */
        public function testUpsert( $sql, $sqlText ) {
                $this->database->upsert(
@@ -253,7 +253,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideDeleteJoin
-        * @covers DatabaseBase::deleteJoin
+        * @covers Database::deleteJoin
         */
        public function testDeleteJoin( $sql, $sqlText ) {
                $this->database->deleteJoin(
@@ -300,7 +300,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideInsert
-        * @covers DatabaseBase::insert
+        * @covers Database::insert
         */
        public function testInsert( $sql, $sqlText ) {
                $this->database->insert(
@@ -353,7 +353,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideInsertSelect
-        * @covers DatabaseBase::insertSelect
+        * @covers Database::insertSelect
         */
        public function testInsertSelect( $sql, $sqlTextNative, $sqlSelect, $sqlInsert ) {
                $this->database->insertSelect(
@@ -440,7 +440,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideReplace
-        * @covers DatabaseBase::replace
+        * @covers Database::replace
         */
        public function testReplace( $sql, $sqlText ) {
                $this->database->replace(
@@ -555,7 +555,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideNativeReplace
-        * @covers DatabaseBase::nativeReplace
+        * @covers Database::nativeReplace
         */
        public function testNativeReplace( $sql, $sqlText ) {
                $this->database->nativeReplace(
@@ -582,7 +582,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideConditional
-        * @covers DatabaseBase::conditional
+        * @covers Database::conditional
         */
        public function testConditional( $sql, $sqlText ) {
                $this->assertEquals( trim( $this->database->conditional(
@@ -623,7 +623,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideBuildConcat
-        * @covers DatabaseBase::buildConcat
+        * @covers Database::buildConcat
         */
        public function testBuildConcat( $stringList, $sqlText ) {
                $this->assertEquals( trim( $this->database->buildConcat(
@@ -646,7 +646,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideBuildLike
-        * @covers DatabaseBase::buildLike
+        * @covers Database::buildLike
         */
        public function testBuildLike( $array, $sqlText ) {
                $this->assertEquals( trim( $this->database->buildLike(
@@ -677,7 +677,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideUnionQueries
-        * @covers DatabaseBase::unionQueries
+        * @covers Database::unionQueries
         */
        public function testUnionQueries( $sql, $sqlText ) {
                $this->assertEquals( trim( $this->database->unionQueries(
@@ -713,7 +713,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers DatabaseBase::commit
+        * @covers Database::commit
         */
        public function testTransactionCommit() {
                $this->database->begin( __METHOD__ );
@@ -722,7 +722,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers DatabaseBase::rollback
+        * @covers Database::rollback
         */
        public function testTransactionRollback() {
                $this->database->begin( __METHOD__ );
@@ -731,16 +731,16 @@ class DatabaseSQLTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers DatabaseBase::dropTable
+        * @covers Database::dropTable
         */
        public function testDropTable() {
                $this->database->setExistingTables( [ 'table' ] );
                $this->database->dropTable( 'table', __METHOD__ );
-               $this->assertLastSql( 'DROP TABLE table' );
+               $this->assertLastSql( 'DROP TABLE table CASCADE' );
        }
 
        /**
-        * @covers DatabaseBase::dropTable
+        * @covers Database::dropTable
         */
        public function testDropNonExistingTable() {
                $this->assertFalse(
@@ -750,7 +750,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideMakeList
-        * @covers DatabaseBase::makeList
+        * @covers Database::makeList
         */
        public function testMakeList( $list, $mode, $sqlText ) {
                $this->assertEquals( trim( $this->database->makeList(
@@ -827,4 +827,42 @@ class DatabaseSQLTest extends MediaWikiTestCase {
                        ],
                ];
        }
+
+       public function testSessionTempTables() {
+               $temp1 = $this->database->tableName( 'tmp_table_1' );
+               $temp2 = $this->database->tableName( 'tmp_table_2' );
+               $temp3 = $this->database->tableName( 'tmp_table_3' );
+
+               $this->database->query( "CREATE TEMPORARY TABLE $temp1 LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "CREATE TEMPORARY TABLE $temp2 LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "CREATE TEMPORARY TABLE $temp3 LIKE orig_tbl", __METHOD__ );
+
+               $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+               $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+               $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+
+               $this->database->dropTable( 'tmp_table_1', __METHOD__ );
+               $this->database->dropTable( 'tmp_table_2', __METHOD__ );
+               $this->database->dropTable( 'tmp_table_3', __METHOD__ );
+
+               $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+               $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+               $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+
+               $this->database->query( "CREATE TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "CREATE TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "CREATE TEMPORARY TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
+
+               $this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+               $this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+               $this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+
+               $this->database->query( "DROP TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "DROP TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
+               $this->database->query( "DROP TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
+
+               $this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
+               $this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
+               $this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
+       }
 }
index 80fb826..172d686 100644 (file)
@@ -7,7 +7,7 @@ class DatabaseSqliteMock extends DatabaseSqlite {
                $p['dbFilePath'] = ':memory:';
                $p['schema'] = false;
 
-               return DatabaseBase::factory( 'SqliteMock', $p );
+               return Database::factory( 'SqliteMock', $p );
        }
 
        function query( $sql, $fname = '', $tempIgnore = false ) {
index d4be6e4..606a209 100644 (file)
@@ -2,11 +2,11 @@
 
 /**
  * @group Database
- * @group DatabaseBase
+ * @group Database
  */
 class DatabaseTest extends MediaWikiTestCase {
        /**
-        * @var DatabaseBase
+        * @var Database
         */
        protected $db;
 
@@ -27,7 +27,7 @@ class DatabaseTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers DatabaseBase::dropTable
+        * @covers Database::dropTable
         */
        public function testAddQuotesNull() {
                $check = "NULL";
@@ -175,47 +175,6 @@ class DatabaseTest extends MediaWikiTestCase {
                );
        }
 
-       public function testFillPreparedEmpty() {
-               $sql = $this->db->fillPrepared(
-                       'SELECT * FROM interwiki', [] );
-               $this->assertEquals(
-                       "SELECT * FROM interwiki",
-                       $sql );
-       }
-
-       public function testFillPreparedQuestion() {
-               $sql = $this->db->fillPrepared(
-                       'SELECT * FROM cur WHERE cur_namespace=? AND cur_title=?',
-                       [ 4, "Snicker's_paradox" ] );
-
-               $check = "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker''s_paradox'";
-               if ( $this->db->getType() === 'mysql' ) {
-                       $check = "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker\'s_paradox'";
-               }
-               $this->assertEquals( $check, $sql );
-       }
-
-       public function testFillPreparedBang() {
-               $sql = $this->db->fillPrepared(
-                       'SELECT user_id FROM ! WHERE user_name=?',
-                       [ '"user"', "Slash's Dot" ] );
-
-               $check = "SELECT user_id FROM \"user\" WHERE user_name='Slash''s Dot'";
-               if ( $this->db->getType() === 'mysql' ) {
-                       $check = "SELECT user_id FROM \"user\" WHERE user_name='Slash\'s Dot'";
-               }
-               $this->assertEquals( $check, $sql );
-       }
-
-       public function testFillPreparedRaw() {
-               $sql = $this->db->fillPrepared(
-                       "SELECT * FROM cur WHERE cur_title='This_\\&_that,_WTF\\?\\!'",
-                       [ '"user"', "Slash's Dot" ] );
-               $this->assertEquals(
-                       "SELECT * FROM cur WHERE cur_title='This_&_that,_WTF?!'",
-                       $sql );
-       }
-
        public function testStoredFunctions() {
                if ( !in_array( wfGetDB( DB_MASTER )->getType(), [ 'mysql', 'postgres' ] ) ) {
                        $this->markTestSkipped( 'MySQL or Postgres required' );
@@ -307,7 +266,7 @@ class DatabaseTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers DatabaseBase::setTransactionListener()
+        * @covers Database::setTransactionListener()
         */
        public function testTransactionListener() {
                $db = $this->db;
@@ -339,7 +298,7 @@ class DatabaseTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers DatabaseBase::flushSnapshot()
+        * @covers Database::flushSnapshot()
         */
        public function testFlushSnapshot() {
                $db = $this->db;
@@ -391,33 +350,35 @@ class DatabaseTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers DatabaseBase::getFlag(
-        * @covers DatabaseBase::setFlag()
-        * @covers DatabaseBase::restoreFlags()
+        * @covers Database::getFlag(
+        * @covers Database::setFlag()
+        * @covers Database::restoreFlags()
         */
        public function testFlagSetting() {
                $db = $this->db;
                $origTrx = $db->getFlag( DBO_TRX );
                $origSsl = $db->getFlag( DBO_SSL );
 
-               if ( $origTrx ) {
-                       $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR );
-               } else {
-                       $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
-               }
+               $origTrx
+                       ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
+                       : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
                $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
 
-               if ( $origSsl ) {
-                       $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR );
-               } else {
-                       $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
-               }
+               $origSsl
+                       ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
+                       : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
                $this->assertEquals( !$origSsl, $db->getFlag( DBO_SSL ) );
 
-               $db2 = clone $db;
-               $db2->restoreFlags( $db::RESTORE_INITIAL );
-               $this->assertEquals( $origTrx, $db2->getFlag( DBO_TRX ) );
-               $this->assertEquals( $origSsl, $db2->getFlag( DBO_SSL ) );
+               $db->restoreFlags( $db::RESTORE_INITIAL );
+               $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
+               $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
+
+               $origTrx
+                       ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
+                       : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
+               $origSsl
+                       ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
+                       : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
 
                $db->restoreFlags();
                $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
@@ -427,4 +388,26 @@ class DatabaseTest extends MediaWikiTestCase {
                $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
                $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
        }
+
+       /**
+        * @covers Database::tablePrefix()
+        * @covers Database::dbSchema()
+        */
+       public function testMutators() {
+               $old = $this->db->tablePrefix();
+               $this->assertType( 'string', $old, 'Prefix is string' );
+               $this->assertEquals( $old, $this->db->tablePrefix(), "Prefix unchanged" );
+               $this->assertEquals( $old, $this->db->tablePrefix( 'xxx' ) );
+               $this->assertEquals( 'xxx', $this->db->tablePrefix(), "Prefix set" );
+               $this->db->tablePrefix( $old );
+               $this->assertNotEquals( 'xxx', $this->db->tablePrefix() );
+
+               $old = $this->db->dbSchema();
+               $this->assertType( 'string', $old, 'Schema is string' );
+               $this->assertEquals( $old, $this->db->dbSchema(), "Schema unchanged" );
+               $this->assertEquals( $old, $this->db->dbSchema( 'xxx' ) );
+               $this->assertEquals( 'xxx', $this->db->dbSchema(), "Schema set" );
+               $this->db->dbSchema( $old );
+               $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
+       }
 }
index 63322cc..c5603c4 100644 (file)
@@ -1,10 +1,10 @@
 <?php
 
 /**
- * Helper for testing the methods from the DatabaseBase class
+ * Helper for testing the methods from the Database class
  * @since 1.22
  */
-class DatabaseTestHelper extends DatabaseBase {
+class DatabaseTestHelper extends Database {
 
        /**
         * __CLASS__ of the test suite,
@@ -14,7 +14,7 @@ class DatabaseTestHelper extends DatabaseBase {
 
        /**
         * Array of lastSqls passed to query(),
-        * This is an array since some methods in DatabaseBase can do more than one
+        * This is an array since some methods in Database can do more than one
         * query. Cleared when calling getLastSqls().
         */
        protected $lastSqls = [];
@@ -34,6 +34,12 @@ class DatabaseTestHelper extends DatabaseBase {
                $this->profiler = new ProfilerStub( [] );
                $this->trxProfiler = new TransactionProfiler();
                $this->cliMode = isset( $opts['cliMode'] ) ? $opts['cliMode'] : true;
+               $this->connLogger = new \Psr\Log\NullLogger();
+               $this->queryLogger = new \Psr\Log\NullLogger();
+               $this->errorLogger = function ( Exception $e ) {
+                       wfWarn( get_class( $e ) . ": {$e->getMessage()}" );
+               };
+               $this->currentDomain = DatabaseDomain::newUnspecified();
        }
 
        /**
@@ -92,6 +98,11 @@ class DatabaseTestHelper extends DatabaseBase {
        }
 
        public function tableExists( $table, $fname = __METHOD__ ) {
+               $tableRaw = $this->tableName( $table, 'raw' );
+               if ( isset( $this->mSessionTempTables[$tableRaw] ) ) {
+                       return true; // already known to exist
+               }
+
                $this->checkFunctionName( $fname );
 
                return in_array( $table, (array)$this->tablesExists );
@@ -150,7 +161,7 @@ class DatabaseTestHelper extends DatabaseBase {
                return false;
        }
 
-       function indexInfo( $table, $index, $fname = 'DatabaseBase::indexInfo' ) {
+       function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
                return false;
        }
 
index 5affa9c..aed2d83 100644 (file)
@@ -43,7 +43,7 @@ class LBFactoryTest extends MediaWikiTestCase {
                ];
 
                $this->hideDeprecated( '$wgLBFactoryConf must be updated. See RELEASE-NOTES for details' );
-               $result = LBFactoryMW::getLBFactoryClass( $config );
+               $result = MWLBFactory::getLBFactoryClass( $config );
 
                $this->assertEquals( $expected, $result );
        }
@@ -58,9 +58,21 @@ class LBFactoryTest extends MediaWikiTestCase {
        }
 
        public function testLBFactorySimpleServer() {
-               $this->setMwGlobals( 'wgDBservers', false );
+               global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype;
 
-               $factory = new LBFactorySimple( [] );
+               $servers = [
+                       [
+                               'host'      => $wgDBserver,
+                               'dbname'    => $wgDBname,
+                               'user'      => $wgDBuser,
+                               'password'  => $wgDBpassword,
+                               'type'      => $wgDBtype,
+                               'load'      => 0,
+                               'flags'     => DBO_TRX // REPEATABLE-READ for consistency
+                       ],
+               ];
+
+               $factory = new LBFactorySimple( [ 'servers' => $servers ] );
                $lb = $factory->getMainLB();
 
                $dbw = $lb->getConnection( DB_MASTER );
@@ -76,28 +88,31 @@ class LBFactoryTest extends MediaWikiTestCase {
        public function testLBFactorySimpleServers() {
                global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype;
 
-               $this->setMwGlobals( 'wgDBservers', [
+               $servers = [
                        [ // master
-                               'host'          => $wgDBserver,
-                               'dbname'    => $wgDBname,
-                               'user'          => $wgDBuser,
-                               'password'      => $wgDBpassword,
-                               'type'          => $wgDBtype,
-                               'load'      => 0,
-                               'flags'     => DBO_TRX // REPEATABLE-READ for consistency
+                               'host'     => $wgDBserver,
+                               'dbname'   => $wgDBname,
+                               'user'     => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type'     => $wgDBtype,
+                               'load'     => 0,
+                               'flags'    => DBO_TRX // REPEATABLE-READ for consistency
                        ],
                        [ // emulated slave
-                               'host'          => $wgDBserver,
-                               'dbname'    => $wgDBname,
-                               'user'          => $wgDBuser,
-                               'password'      => $wgDBpassword,
-                               'type'          => $wgDBtype,
-                               'load'      => 100,
-                               'flags'     => DBO_TRX // REPEATABLE-READ for consistency
+                               'host'     => $wgDBserver,
+                               'dbname'   => $wgDBname,
+                               'user'     => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type'     => $wgDBtype,
+                               'load'     => 100,
+                               'flags'    => DBO_TRX // REPEATABLE-READ for consistency
                        ]
-               ] );
+               ];
 
-               $factory = new LBFactorySimple( [ 'loadMonitorClass' => 'LoadMonitorNull' ] );
+               $factory = new LBFactorySimple( [
+                       'servers' => $servers,
+                       'loadMonitorClass' => 'LoadMonitorNull'
+               ] );
                $lb = $factory->getMainLB();
 
                $dbw = $lb->getConnection( DB_MASTER );
@@ -216,4 +231,148 @@ class LBFactoryTest extends MediaWikiTestCase {
                $cp->shutdownLB( $lb );
                $cp->shutdown();
        }
+
+       private function newLBFactoryMulti( array $baseOverride = [], array $serverOverride = [] ) {
+               global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype;
+
+               return new LBFactoryMulti( $baseOverride + [
+                       'sectionsByDB' => [],
+                       'sectionLoads' => [
+                               'DEFAULT' => [
+                                       'test-db1' => 1,
+                               ],
+                       ],
+                       'serverTemplate' => $serverOverride + [
+                               'dbname' => $wgDBname,
+                               'user' => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type' => $wgDBtype,
+                               'flags' => DBO_DEFAULT
+                       ],
+                       'hostsByName' => [
+                               'test-db1' => $wgDBserver,
+                       ],
+                       'loadMonitorClass' => 'LoadMonitorNull',
+                       'localDomain' => wfWikiID()
+               ] );
+       }
+
+       public function testNiceDomains() {
+               global $wgDBname;
+
+               $factory = $this->newLBFactoryMulti();
+               $lb = $factory->getMainLB();
+
+               $db = $lb->getConnectionRef( DB_MASTER );
+               $this->assertEquals(
+                       $wgDBname,
+                       $db->getDomainID()
+               );
+               unset( $db );
+
+               /** @var Database $db */
+               $db = $lb->getConnection( DB_MASTER, [], '' );
+               $lb->reuseConnection( $db ); // don't care
+
+               $this->assertEquals(
+                       '',
+                       $db->getDomainID()
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'page' ),
+                       "Correct full table name"
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( $wgDBname ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( "$wgDBname.page" ),
+                       "Correct full table name"
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'nice_db' ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'nice_db.page' ),
+                       "Correct full table name"
+               );
+
+               $factory->setDomainPrefix( 'my_' );
+               $this->assertEquals(
+                       '',
+                       $db->getDomainID()
+               );
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'my_page' ),
+                       $db->tableName( 'page' ),
+                       "Correct full table name"
+               );
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'other_nice_db' ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'other_nice_db.page' ),
+                       "Correct full table name"
+               );
+
+               $factory->closeAll();
+               $factory->destroy();
+       }
+
+       public function testTrickyDomain() {
+               $dbname = 'unittest-domain';
+               $factory = $this->newLBFactoryMulti(
+                       [ 'localDomain' => $dbname ], [ 'dbname' => $dbname ] );
+               $lb = $factory->getMainLB();
+               /** @var Database $db */
+               $db = $lb->getConnection( DB_MASTER, [], '' );
+               $lb->reuseConnection( $db ); // don't care
+
+               $this->assertEquals(
+                       '',
+                       $db->getDomainID()
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'page' ),
+                       "Correct full table name"
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( $dbname ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( "$dbname.page" ),
+                       "Correct full table name"
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'nice_db' ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'nice_db.page' ),
+                       "Correct full table name"
+               );
+
+               $factory->setDomainPrefix( 'my_' );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'my_page' ),
+                       $db->tableName( 'page' ),
+                       "Correct full table name"
+               );
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'other_nice_db' ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'other_nice_db.page' ),
+                       "Correct full table name"
+               );
+
+               \MediaWiki\suppressWarnings();
+               $this->assertFalse( $db->selectDB( 'garbage-db' ) );
+               \MediaWiki\restoreWarnings();
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'garbage-db' ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'garbage-db.page' ),
+                       "Correct full table name"
+               );
+
+               $factory->closeAll();
+               $factory->destroy();
+       }
 }
index 254cfbd..c3d31d1 100644 (file)
@@ -268,7 +268,7 @@ class FileBackendTest extends MediaWikiTestCase {
        public static function provider_testStore() {
                $cases = [];
 
-               $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+               $tmpName = TempFSFile::factory( "unittests_", 'txt', wfTempDir() )->getPath();
                $toPath = self::baseStorePath() . '/unittest-cont1/e/fun/obj1.txt';
                $op = [ 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath ];
                $cases[] = [ $op ];
@@ -1786,9 +1786,9 @@ class FileBackendTest extends MediaWikiTestCase {
                $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
                $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
 
-               $tmpNameA = TempFSFile::factory( "unittests_", 'txt' )->getPath();
-               $tmpNameB = TempFSFile::factory( "unittests_", 'txt' )->getPath();
-               $tmpNameC = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+               $tmpNameA = TempFSFile::factory( "unittests_", 'txt', wfTempDir() )->getPath();
+               $tmpNameB = TempFSFile::factory( "unittests_", 'txt', wfTempDir() )->getPath();
+               $tmpNameC = TempFSFile::factory( "unittests_", 'txt', wfTempDir() )->getPath();
                $this->addTmpFiles( [ $tmpNameA, $tmpNameB, $tmpNameC ] );
                file_put_contents( $tmpNameA, $fileAContents );
                file_put_contents( $tmpNameB, $fileBContents );
@@ -1914,7 +1914,7 @@ class FileBackendTest extends MediaWikiTestCase {
                        // Does nothing
                ], [ 'force' => 1 ] );
 
-               $this->assertNotEquals( [], $status->errors, "Operation had warnings" );
+               $this->assertNotEquals( [], $status->getErrors(), "Operation had warnings" );
                $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
                $this->assertEquals( 8, count( $status->success ),
                        "Operation batch has correct success array" );
@@ -2371,25 +2371,25 @@ class FileBackendTest extends MediaWikiTestCase {
 
                for ( $i = 0; $i < 25; $i++ ) {
                        $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
-                       $this->assertEquals( print_r( [], true ), print_r( $status->errors, true ),
+                       $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
                                "Locking of files succeeded ($backendName) ($i)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Locking of files succeeded with OK status ($backendName) ($i)." );
 
                        $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH );
-                       $this->assertEquals( print_r( [], true ), print_r( $status->errors, true ),
+                       $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
                                "Locking of files succeeded ($backendName) ($i)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Locking of files succeeded with OK status ($backendName) ($i)." );
 
                        $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH );
-                       $this->assertEquals( print_r( [], true ), print_r( $status->errors, true ),
+                       $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
                                "Locking of files succeeded ($backendName) ($i)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Locking of files succeeded with OK status ($backendName) ($i)." );
 
                        $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX );
-                       $this->assertEquals( print_r( [], true ), print_r( $status->errors, true ),
+                       $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
                                "Locking of files succeeded ($backendName). ($i)" );
                        $this->assertEquals( true, $status->isOK(),
                                "Locking of files succeeded with OK status ($backendName) ($i)." );
@@ -2397,25 +2397,25 @@ class FileBackendTest extends MediaWikiTestCase {
                        # # Flip the acquire/release ordering around ##
 
                        $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH );
-                       $this->assertEquals( print_r( [], true ), print_r( $status->errors, true ),
+                       $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
                                "Locking of files succeeded ($backendName) ($i)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Locking of files succeeded with OK status ($backendName) ($i)." );
 
                        $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
-                       $this->assertEquals( print_r( [], true ), print_r( $status->errors, true ),
+                       $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
                                "Locking of files succeeded ($backendName) ($i)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Locking of files succeeded with OK status ($backendName) ($i)." );
 
                        $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX );
-                       $this->assertEquals( print_r( [], true ), print_r( $status->errors, true ),
+                       $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
                                "Locking of files succeeded ($backendName). ($i)" );
                        $this->assertEquals( true, $status->isOK(),
                                "Locking of files succeeded with OK status ($backendName) ($i)." );
 
                        $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH );
-                       $this->assertEquals( print_r( [], true ), print_r( $status->errors, true ),
+                       $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
                                "Locking of files succeeded ($backendName) ($i)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Locking of files succeeded with OK status ($backendName) ($i)." );
@@ -2425,7 +2425,7 @@ class FileBackendTest extends MediaWikiTestCase {
                $sl = $this->backend->getScopedFileLocks( $paths, LockManager::LOCK_EX, $status );
                $this->assertInstanceOf( 'ScopedLock', $sl,
                        "Scoped locking of files succeeded ($backendName)." );
-               $this->assertEquals( [], $status->errors,
+               $this->assertEquals( [], $status->getErrors(),
                        "Scoped locking of files succeeded ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
                        "Scoped locking of files succeeded with OK status ($backendName)." );
@@ -2433,7 +2433,7 @@ class FileBackendTest extends MediaWikiTestCase {
                ScopedLock::release( $sl );
                $this->assertEquals( null, $sl,
                        "Scoped unlocking of files succeeded ($backendName)." );
-               $this->assertEquals( [], $status->errors,
+               $this->assertEquals( [], $status->getErrors(),
                        "Scoped unlocking of files succeeded ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
                        "Scoped unlocking of files succeeded with OK status ($backendName)." );
@@ -2647,7 +2647,7 @@ class FileBackendTest extends MediaWikiTestCase {
                }
        }
 
-       function assertGoodStatus( $status, $msg ) {
-               $this->assertEquals( print_r( [], 1 ), print_r( $status->errors, 1 ), $msg );
+       function assertGoodStatus( StatusValue $status, $msg ) {
+               $this->assertEquals( print_r( [], 1 ), print_r( $status->getErrors(), 1 ), $msg );
        }
 }
index ed80c57..92a54fa 100644 (file)
@@ -59,7 +59,8 @@ class MigrateFileRepoLayoutTest extends MediaWikiTestCase {
                        ->method( 'getRepo' )
                        ->will( $this->returnValue( $repoMock ) );
 
-               $this->tmpFilepath = TempFSFile::factory( 'migratefilelayout-test-', 'png' )->getPath();
+               $this->tmpFilepath = TempFSFile::factory(
+                       'migratefilelayout-test-', 'png', wfTempDir() )->getPath();
 
                file_put_contents( $this->tmpFilepath, $this->text );
 
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 ],
+               ];
+       }
+}
index 81c9faf..22d52f0 100644 (file)
@@ -16,11 +16,18 @@ class DatabaseUpdaterTest extends MediaWikiTestCase {
        }
 }
 
-class FakeDatabase extends DatabaseBase {
+class FakeDatabase extends Database {
        public $lastInsertTable;
        public $lastInsertData;
 
        function __construct() {
+               $this->cliMode = true;
+               $this->connLogger = new \Psr\Log\NullLogger();
+               $this->queryLogger = new \Psr\Log\NullLogger();
+               $this->errorLogger = function ( Exception $e ) {
+                       wfWarn( get_class( $e ) . ": {$e->getMessage()}" );
+               };
+               $this->currentDomain = DatabaseDomain::newUnspecified();
        }
 
        function clearFlag( $arg, $remember = self::REMEMBER_NOTHING ) {
@@ -63,7 +70,7 @@ class FakeDatabase extends DatabaseBase {
         * member variables.
         * If no more rows are available, false is returned.
         *
-        * @param ResultWrapper|stdClass $res Object as returned from DatabaseBase::query(), etc.
+        * @param ResultWrapper|stdClass $res Object as returned from Database::query(), etc.
         * @return stdClass|bool
         * @throws DBUnexpectedError Thrown if the database returns an error
         */
@@ -76,7 +83,7 @@ class FakeDatabase extends DatabaseBase {
         * form. Fields are retrieved with $row['fieldname'].
         * If no more rows are available, false is returned.
         *
-        * @param ResultWrapper $res Result object as returned from DatabaseBase::query(), etc.
+        * @param ResultWrapper $res Result object as returned from Database::query(), etc.
         * @return array|bool
         * @throws DBUnexpectedError Thrown if the database returns an error
         */
diff --git a/tests/phpunit/includes/libs/IPTest.php b/tests/phpunit/includes/libs/IPTest.php
new file mode 100644 (file)
index 0000000..307652d
--- /dev/null
@@ -0,0 +1,670 @@
+<?php
+/**
+ * Tests for IP validity functions.
+ *
+ * Ported from /t/inc/IP.t by avar.
+ *
+ * @group IP
+ * @todo Test methods in this call should be split into a method and a
+ * dataprovider.
+ */
+
+class IPTest extends PHPUnit_Framework_TestCase {
+       /**
+        * @covers IP::isIPAddress
+        * @dataProvider provideInvalidIPs
+        */
+       public function isNotIPAddress( $val, $desc ) {
+               $this->assertFalse( IP::isIPAddress( $val ), $desc );
+       }
+
+       /**
+        * Provide a list of things that aren't IP addresses
+        */
+       public function provideInvalidIPs() {
+               return [
+                       [ false, 'Boolean false is not an IP' ],
+                       [ true, 'Boolean true is not an IP' ],
+                       [ '', 'Empty string is not an IP' ],
+                       [ 'abc', 'Garbage IP string' ],
+                       [ ':', 'Single ":" is not an IP' ],
+                       [ '2001:0DB8::A:1::1', 'IPv6 with a double :: occurrence' ],
+                       [ '2001:0DB8::A:1::', 'IPv6 with a double :: occurrence, last at end' ],
+                       [ '::2001:0DB8::5:1', 'IPv6 with a double :: occurrence, firt at beginning' ],
+                       [ '124.24.52', 'IPv4 not enough quads' ],
+                       [ '24.324.52.13', 'IPv4 out of range' ],
+                       [ '.24.52.13', 'IPv4 starts with period' ],
+                       [ 'fc:100:300', 'IPv6 with only 3 words' ],
+               ];
+       }
+
+       /**
+        * @covers IP::isIPAddress
+        */
+       public function testisIPAddress() {
+               $this->assertTrue( IP::isIPAddress( '::' ), 'RFC 4291 IPv6 Unspecified Address' );
+               $this->assertTrue( IP::isIPAddress( '::1' ), 'RFC 4291 IPv6 Loopback Address' );
+               $this->assertTrue( IP::isIPAddress( '74.24.52.13/20' ), 'IPv4 range' );
+               $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24' ), 'IPv6 range' );
+               $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96' ), 'IPv6 range with "::"' );
+
+               $validIPs = [ 'fc:100::', 'fc:100:a:d:1:e:ac::', 'fc::100', '::fc:100:a:d:1:e:ac',
+                       '::fc', 'fc::100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0', '124.24.52.13', '1.24.52.13' ];
+               foreach ( $validIPs as $ip ) {
+                       $this->assertTrue( IP::isIPAddress( $ip ), "$ip is a valid IP address" );
+               }
+       }
+
+       /**
+        * @covers IP::isIPv6
+        */
+       public function testisIPv6() {
+               $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' );
+               $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
+               $this->assertFalse( IP::isIPv6( 'fc:300' ), 'IPv6 with only 2 words' );
+               $this->assertFalse( IP::isIPv6( 'fc:100:300' ), 'IPv6 with only 3 words' );
+
+               $this->assertTrue( IP::isIPv6( 'fc:100::' ) );
+               $this->assertTrue( IP::isIPv6( 'fc:100:a::' ) );
+               $this->assertTrue( IP::isIPv6( 'fc:100:a:d::' ) );
+               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1::' ) );
+               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e::' ) );
+               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac::' ) );
+
+               $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' );
+               $this->assertFalse(
+                       IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ),
+                       'IPv6 with 9 words ending with "::"'
+               );
+
+               $this->assertFalse( IP::isIPv6( ':::' ) );
+               $this->assertFalse( IP::isIPv6( '::0:' ), 'IPv6 ending in a lone ":"' );
+
+               $this->assertTrue( IP::isIPv6( '::' ), 'IPv6 zero address' );
+               $this->assertTrue( IP::isIPv6( '::0' ) );
+               $this->assertTrue( IP::isIPv6( '::fc' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100:a' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100:a:d' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e' ) );
+               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e:ac' ) );
+
+               $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
+               $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
+
+               $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' );
+               $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending with lone ":"' );
+               $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' );
+
+               $this->assertTrue( IP::isIPv6( 'fc::100' ), 'IPv6 with "::" and 2 words' );
+               $this->assertTrue( IP::isIPv6( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
+               $this->assertTrue( IP::isIPv6( 'fc::100:a:d' ), 'IPv6 with "::" and 4 words' );
+               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
+               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ), 'IPv6 with "::" and 6 words' );
+               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
+               $this->assertTrue( IP::isIPv6( '2001::df' ), 'IPv6 with "::" and 2 words' );
+               $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' );
+               $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' );
+
+               $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
+               $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
+
+               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) );
+       }
+
+       /**
+        * @covers IP::isIPv4
+        * @dataProvider provideInvalidIPv4Addresses
+        */
+       public function testisNotIPv4( $bogusIP, $desc ) {
+               $this->assertFalse( IP::isIPv4( $bogusIP ), $desc );
+       }
+
+       public function provideInvalidIPv4Addresses() {
+               return [
+                       [ false, 'Boolean false is not an IP' ],
+                       [ true, 'Boolean true is not an IP' ],
+                       [ '', 'Empty string is not an IP' ],
+                       [ 'abc', 'Letters are not an IP' ],
+                       [ ':', 'A colon is not an IP' ],
+                       [ '124.24.52', 'IPv4 not enough quads' ],
+                       [ '24.324.52.13', 'IPv4 out of range' ],
+                       [ '.24.52.13', 'IPv4 starts with period' ],
+               ];
+       }
+
+       /**
+        * @covers IP::isIPv4
+        * @dataProvider provideValidIPv4Address
+        */
+       public function testIsIPv4( $ip, $desc ) {
+               $this->assertTrue( IP::isIPv4( $ip ), $desc );
+       }
+
+       /**
+        * Provide some IPv4 addresses and ranges
+        */
+       public function provideValidIPv4Address() {
+               return [
+                       [ '124.24.52.13', 'Valid IPv4 address' ],
+                       [ '1.24.52.13', 'Another valid IPv4 address' ],
+                       [ '74.24.52.13/20', 'An IPv4 range' ],
+               ];
+       }
+
+       /**
+        * @covers IP::isValid
+        */
+       public function testValidIPs() {
+               foreach ( range( 0, 255 ) as $i ) {
+                       $a = sprintf( "%03d", $i );
+                       $b = sprintf( "%02d", $i );
+                       $c = sprintf( "%01d", $i );
+                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
+                               $ip = "$f.$f.$f.$f";
+                               $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv4 address" );
+                       }
+               }
+               foreach ( range( 0x0, 0xFFFF, 0xF ) as $i ) {
+                       $a = sprintf( "%04x", $i );
+                       $b = sprintf( "%03x", $i );
+                       $c = sprintf( "%02x", $i );
+                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
+                               $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
+                               $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv6 address" );
+                       }
+               }
+               // test with some abbreviations
+               $this->assertFalse( IP::isValid( ':fc:100::' ), 'IPv6 starting with lone ":"' );
+               $this->assertFalse( IP::isValid( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
+               $this->assertFalse( IP::isValid( 'fc:300' ), 'IPv6 with only 2 words' );
+               $this->assertFalse( IP::isValid( 'fc:100:300' ), 'IPv6 with only 3 words' );
+
+               $this->assertTrue( IP::isValid( 'fc:100::' ) );
+               $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e::' ) );
+               $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e:ac::' ) );
+
+               $this->assertTrue( IP::isValid( 'fc::100' ), 'IPv6 with "::" and 2 words' );
+               $this->assertTrue( IP::isValid( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
+               $this->assertTrue( IP::isValid( '2001::df' ), 'IPv6 with "::" and 2 words' );
+               $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' );
+               $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' );
+               $this->assertTrue( IP::isValid( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
+               $this->assertTrue( IP::isValid( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
+
+               $this->assertFalse(
+                       IP::isValid( 'fc:100:a:d:1:e:ac:0::' ),
+                       'IPv6 with 8 words ending with "::"'
+               );
+               $this->assertFalse(
+                       IP::isValid( 'fc:100:a:d:1:e:ac:0:1::' ),
+                       'IPv6 with 9 words ending with "::"'
+               );
+       }
+
+       /**
+        * @covers IP::isValid
+        */
+       public function testInvalidIPs() {
+               // Out of range...
+               foreach ( range( 256, 999 ) as $i ) {
+                       $a = sprintf( "%03d", $i );
+                       $b = sprintf( "%02d", $i );
+                       $c = sprintf( "%01d", $i );
+                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
+                               $ip = "$f.$f.$f.$f";
+                               $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" );
+                       }
+               }
+               foreach ( range( 'g', 'z' ) as $i ) {
+                       $a = sprintf( "%04s", $i );
+                       $b = sprintf( "%03s", $i );
+                       $c = sprintf( "%02s", $i );
+                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
+                               $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
+                               $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv6 address" );
+                       }
+               }
+               // Have CIDR
+               $ipCIDRs = [
+                       '212.35.31.121/32',
+                       '212.35.31.121/18',
+                       '212.35.31.121/24',
+                       '::ff:d:321:5/96',
+                       'ff::d3:321:5/116',
+                       'c:ff:12:1:ea:d:321:5/120',
+               ];
+               foreach ( $ipCIDRs as $i ) {
+                       $this->assertFalse( IP::isValid( $i ),
+                               "$i is an invalid IP address because it is a block" );
+               }
+               // Incomplete/garbage
+               $invalid = [
+                       'www.xn--var-xla.net',
+                       '216.17.184.G',
+                       '216.17.184.1.',
+                       '216.17.184',
+                       '216.17.184.',
+                       '256.17.184.1'
+               ];
+               foreach ( $invalid as $i ) {
+                       $this->assertFalse( IP::isValid( $i ), "$i is an invalid IP address" );
+               }
+       }
+
+       /**
+        * Provide some valid IP blocks
+        */
+       public function provideValidBlocks() {
+               return [
+                       [ '116.17.184.5/32' ],
+                       [ '0.17.184.5/30' ],
+                       [ '16.17.184.1/24' ],
+                       [ '30.242.52.14/1' ],
+                       [ '10.232.52.13/8' ],
+                       [ '30.242.52.14/0' ],
+                       [ '::e:f:2001/96' ],
+                       [ '::c:f:2001/128' ],
+                       [ '::10:f:2001/70' ],
+                       [ '::fe:f:2001/1' ],
+                       [ '::6d:f:2001/8' ],
+                       [ '::fe:f:2001/0' ],
+               ];
+       }
+
+       /**
+        * @covers IP::isValidBlock
+        * @dataProvider provideValidBlocks
+        */
+       public function testValidBlocks( $block ) {
+               $this->assertTrue( IP::isValidBlock( $block ), "$block is a valid IP block" );
+       }
+
+       /**
+        * @covers IP::isValidBlock
+        * @dataProvider provideInvalidBlocks
+        */
+       public function testInvalidBlocks( $invalid ) {
+               $this->assertFalse( IP::isValidBlock( $invalid ), "$invalid is not a valid IP block" );
+       }
+
+       public function provideInvalidBlocks() {
+               return [
+                       [ '116.17.184.5/33' ],
+                       [ '0.17.184.5/130' ],
+                       [ '16.17.184.1/-1' ],
+                       [ '10.232.52.13/*' ],
+                       [ '7.232.52.13/ab' ],
+                       [ '11.232.52.13/' ],
+                       [ '::e:f:2001/129' ],
+                       [ '::c:f:2001/228' ],
+                       [ '::10:f:2001/-1' ],
+                       [ '::6d:f:2001/*' ],
+                       [ '::86:f:2001/ab' ],
+                       [ '::23:f:2001/' ],
+               ];
+       }
+
+       /**
+        * @covers IP::sanitizeIP
+        * @dataProvider provideSanitizeIP
+        */
+       public function testSanitizeIP( $expected, $input ) {
+               $result = IP::sanitizeIP( $input );
+               $this->assertEquals( $expected, $result );
+       }
+
+       /**
+        * Provider for IP::testSanitizeIP()
+        */
+       public static function provideSanitizeIP() {
+               return [
+                       [ '0.0.0.0', '0.0.0.0' ],
+                       [ '0.0.0.0', '00.00.00.00' ],
+                       [ '0.0.0.0', '000.000.000.000' ],
+                       [ '141.0.11.253', '141.000.011.253' ],
+                       [ '1.2.4.5', '1.2.4.5' ],
+                       [ '1.2.4.5', '01.02.04.05' ],
+                       [ '1.2.4.5', '001.002.004.005' ],
+                       [ '10.0.0.1', '010.0.000.1' ],
+                       [ '80.72.250.4', '080.072.250.04' ],
+                       [ 'Foo.1000.00', 'Foo.1000.00' ],
+                       [ 'Bar.01', 'Bar.01' ],
+                       [ 'Bar.010', 'Bar.010' ],
+                       [ null, '' ],
+                       [ null, ' ' ]
+               ];
+       }
+
+       /**
+        * @covers IP::toHex
+        * @dataProvider provideToHex
+        */
+       public function testToHex( $expected, $input ) {
+               $result = IP::toHex( $input );
+               $this->assertTrue( $result === false || is_string( $result ) );
+               $this->assertEquals( $expected, $result );
+       }
+
+       /**
+        * Provider for IP::testToHex()
+        */
+       public static function provideToHex() {
+               return [
+                       [ '00000001', '0.0.0.1' ],
+                       [ '01020304', '1.2.3.4' ],
+                       [ '7F000001', '127.0.0.1' ],
+                       [ '80000000', '128.0.0.0' ],
+                       [ 'DEADCAFE', '222.173.202.254' ],
+                       [ 'FFFFFFFF', '255.255.255.255' ],
+                       [ '8D000BFD', '141.000.11.253' ],
+                       [ false, 'IN.VA.LI.D' ],
+                       [ 'v6-00000000000000000000000000000001', '::1' ],
+                       [ 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ],
+                       [ 'v6-20010DB885A3000000008A2E03707334', '2001:db8:85a3::8a2e:0370:7334' ],
+                       [ false, 'IN:VA::LI:D' ],
+                       [ false, ':::1' ]
+               ];
+       }
+
+       /**
+        * @covers IP::isPublic
+        * @dataProvider provideIsPublic
+        */
+       public function testIsPublic( $expected, $input ) {
+               $result = IP::isPublic( $input );
+               $this->assertEquals( $expected, $result );
+       }
+
+       /**
+        * Provider for IP::testIsPublic()
+        */
+       public static function provideIsPublic() {
+               return [
+                       [ false, 'fc00::3' ], # RFC 4193 (local)
+                       [ false, 'fc00::ff' ], # RFC 4193 (local)
+                       [ false, '127.1.2.3' ], # loopback
+                       [ false, '::1' ], # loopback
+                       [ false, 'fe80::1' ], # link-local
+                       [ false, '169.254.1.1' ], # link-local
+                       [ false, '10.0.0.1' ], # RFC 1918 (private)
+                       [ false, '172.16.0.1' ], # RFC 1918 (private)
+                       [ false, '192.168.0.1' ], # RFC 1918 (private)
+                       [ true, '2001:5c0:1000:a::133' ], # public
+                       [ true, 'fc::3' ], # public
+                       [ true, '00FC::' ] # public
+               ];
+       }
+
+       // Private wrapper used to test CIDR Parsing.
+       private function assertFalseCIDR( $CIDR, $msg = '' ) {
+               $ff = [ false, false ];
+               $this->assertEquals( $ff, IP::parseCIDR( $CIDR ), $msg );
+       }
+
+       // Private wrapper to test network shifting using only dot notation
+       private function assertNet( $expected, $CIDR ) {
+               $parse = IP::parseCIDR( $CIDR );
+               $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" );
+       }
+
+       /**
+        * @covers IP::hexToQuad
+        * @dataProvider provideIPsAndHexes
+        */
+       public function testHexToQuad( $ip, $hex ) {
+               $this->assertEquals( $ip, IP::hexToQuad( $hex ) );
+       }
+
+       /**
+        * Provide some IP addresses and their equivalent hex representations
+        */
+       public function provideIPsandHexes() {
+               return [
+                       [ '0.0.0.1', '00000001' ],
+                       [ '255.0.0.0', 'FF000000' ],
+                       [ '255.255.255.255', 'FFFFFFFF' ],
+                       [ '10.188.222.255', '0ABCDEFF' ],
+                       // hex not left-padded...
+                       [ '0.0.0.0', '0' ],
+                       [ '0.0.0.1', '1' ],
+                       [ '0.0.0.255', 'FF' ],
+                       [ '0.0.255.0', 'FF00' ],
+               ];
+       }
+
+       /**
+        * @covers IP::hexToOctet
+        * @dataProvider provideOctetsAndHexes
+        */
+       public function testHexToOctet( $octet, $hex ) {
+               $this->assertEquals( $octet, IP::hexToOctet( $hex ) );
+       }
+
+       /**
+        * Provide some hex and octet representations of the same IPs
+        */
+       public function provideOctetsAndHexes() {
+               return [
+                       [ '0:0:0:0:0:0:0:1', '00000000000000000000000000000001' ],
+                       [ '0:0:0:0:0:0:FF:3', '00000000000000000000000000FF0003' ],
+                       [ '0:0:0:0:0:0:FF00:6', '000000000000000000000000FF000006' ],
+                       [ '0:0:0:0:0:0:FCCF:FAFF', '000000000000000000000000FCCFFAFF' ],
+                       [ 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ],
+                       // hex not left-padded...
+                       [ '0:0:0:0:0:0:0:0', '0' ],
+                       [ '0:0:0:0:0:0:0:1', '1' ],
+                       [ '0:0:0:0:0:0:0:FF', 'FF' ],
+                       [ '0:0:0:0:0:0:0:FFD0', 'FFD0' ],
+                       [ '0:0:0:0:0:0:FA00:0', 'FA000000' ],
+                       [ '0:0:0:0:0:0:FCCF:FAFF', 'FCCFFAFF' ],
+               ];
+       }
+
+       /**
+        * IP::parseCIDR() returns an array containing a signed IP address
+        * representing the network mask and the bit mask.
+        * @covers IP::parseCIDR
+        */
+       public function testCIDRParsing() {
+               $this->assertFalseCIDR( '192.0.2.0', "missing mask" );
+               $this->assertFalseCIDR( '192.0.2.0/', "missing bitmask" );
+
+               // Verify if statement
+               $this->assertFalseCIDR( '256.0.0.0/32', "invalid net" );
+               $this->assertFalseCIDR( '192.0.2.0/AA', "mask not numeric" );
+               $this->assertFalseCIDR( '192.0.2.0/-1', "mask < 0" );
+               $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" );
+
+               // Check internal logic
+               # 0 mask always result in array(0,0)
+               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '192.0.0.2/0' ) );
+               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '0.0.0.0/0' ) );
+               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '255.255.255.255/0' ) );
+
+               // @todo FIXME: Add more tests.
+
+               # This part test network shifting
+               $this->assertNet( '192.0.0.0', '192.0.0.2/24' );
+               $this->assertNet( '192.168.5.0', '192.168.5.13/24' );
+               $this->assertNet( '10.0.0.160', '10.0.0.161/28' );
+               $this->assertNet( '10.0.0.0', '10.0.0.3/28' );
+               $this->assertNet( '10.0.0.0', '10.0.0.3/30' );
+               $this->assertNet( '10.0.0.4', '10.0.0.4/30' );
+               $this->assertNet( '172.17.32.0', '172.17.35.48/21' );
+               $this->assertNet( '10.128.0.0', '10.135.0.0/9' );
+               $this->assertNet( '134.0.0.0', '134.0.5.1/8' );
+       }
+
+       /**
+        * @covers IP::canonicalize
+        */
+       public function testIPCanonicalizeOnValidIp() {
+               $this->assertEquals( '192.0.2.152', IP::canonicalize( '192.0.2.152' ),
+                       'Canonicalization of a valid IP returns it unchanged' );
+       }
+
+       /**
+        * @covers IP::canonicalize
+        */
+       public function testIPCanonicalizeMappedAddress() {
+               $this->assertEquals(
+                       '192.0.2.152',
+                       IP::canonicalize( '::ffff:192.0.2.152' )
+               );
+               $this->assertEquals(
+                       '192.0.2.152',
+                       IP::canonicalize( '::192.0.2.152' )
+               );
+       }
+
+       /**
+        * Issues there are most probably from IP::toHex() or IP::parseRange()
+        * @covers IP::isInRange
+        * @dataProvider provideIPsAndRanges
+        */
+       public function testIPIsInRange( $expected, $addr, $range, $message = '' ) {
+               $this->assertEquals(
+                       $expected,
+                       IP::isInRange( $addr, $range ),
+                       $message
+               );
+       }
+
+       /** Provider for testIPIsInRange() */
+       public static function provideIPsAndRanges() {
+               # Format: (expected boolean, address, range, optional message)
+               return [
+                       # IPv4
+                       [ true, '192.0.2.0', '192.0.2.0/24', 'Network address' ],
+                       [ true, '192.0.2.77', '192.0.2.0/24', 'Simple address' ],
+                       [ true, '192.0.2.255', '192.0.2.0/24', 'Broadcast address' ],
+
+                       [ false, '0.0.0.0', '192.0.2.0/24' ],
+                       [ false, '255.255.255', '192.0.2.0/24' ],
+
+                       # IPv6
+                       [ false, '::1', '2001:DB8::/32' ],
+                       [ false, '::', '2001:DB8::/32' ],
+                       [ false, 'FE80::1', '2001:DB8::/32' ],
+
+                       [ true, '2001:DB8::', '2001:DB8::/32' ],
+                       [ true, '2001:0DB8::', '2001:DB8::/32' ],
+                       [ true, '2001:DB8::1', '2001:DB8::/32' ],
+                       [ true, '2001:0DB8::1', '2001:DB8::/32' ],
+                       [ true, '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF',
+                               '2001:DB8::/32' ],
+
+                       [ false, '2001:0DB8:F::', '2001:DB8::/96' ],
+               ];
+       }
+
+       /**
+        * Test for IP::splitHostAndPort().
+        * @dataProvider provideSplitHostAndPort
+        */
+       public function testSplitHostAndPort( $expected, $input, $description ) {
+               $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description );
+       }
+
+       /**
+        * Provider for IP::splitHostAndPort()
+        */
+       public static function provideSplitHostAndPort() {
+               return [
+                       [ false, '[', 'Unclosed square bracket' ],
+                       [ false, '[::', 'Unclosed square bracket 2' ],
+                       [ [ '::', false ], '::', 'Bare IPv6 0' ],
+                       [ [ '::1', false ], '::1', 'Bare IPv6 1' ],
+                       [ [ '::', false ], '[::]', 'Bracketed IPv6 0' ],
+                       [ [ '::1', false ], '[::1]', 'Bracketed IPv6 1' ],
+                       [ [ '::1', 80 ], '[::1]:80', 'Bracketed IPv6 with port' ],
+                       [ false, '::x', 'Double colon but no IPv6' ],
+                       [ [ 'x', 80 ], 'x:80', 'Hostname and port' ],
+                       [ false, 'x:x', 'Hostname and invalid port' ],
+                       [ [ 'x', false ], 'x', 'Plain hostname' ]
+               ];
+       }
+
+       /**
+        * Test for IP::combineHostAndPort()
+        * @dataProvider provideCombineHostAndPort
+        */
+       public function testCombineHostAndPort( $expected, $input, $description ) {
+               list( $host, $port, $defaultPort ) = $input;
+               $this->assertEquals(
+                       $expected,
+                       IP::combineHostAndPort( $host, $port, $defaultPort ),
+                       $description );
+       }
+
+       /**
+        * Provider for IP::combineHostAndPort()
+        */
+       public static function provideCombineHostAndPort() {
+               return [
+                       [ '[::1]', [ '::1', 2, 2 ], 'IPv6 default port' ],
+                       [ '[::1]:2', [ '::1', 2, 3 ], 'IPv6 non-default port' ],
+                       [ 'x', [ 'x', 2, 2 ], 'Normal default port' ],
+                       [ 'x:2', [ 'x', 2, 3 ], 'Normal non-default port' ],
+               ];
+       }
+
+       /**
+        * Test for IP::sanitizeRange()
+        * @dataProvider provideIPCIDRs
+        */
+       public function testSanitizeRange( $input, $expected, $description ) {
+               $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description );
+       }
+
+       /**
+        * Provider for IP::testSanitizeRange()
+        */
+       public static function provideIPCIDRs() {
+               return [
+                       [ '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ],
+                       [ '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ],
+                       [ '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ],
+                       [ '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ],
+                       [ '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ],
+                       [ '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ],
+                       [ '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ],
+                       [ '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ],
+               ];
+       }
+
+       /**
+        * Test for IP::prettifyIP()
+        * @dataProvider provideIPsToPrettify
+        */
+       public function testPrettifyIP( $ip, $prettified ) {
+               $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" );
+       }
+
+       /**
+        * Provider for IP::testPrettifyIP()
+        */
+       public static function provideIPsToPrettify() {
+               return [
+                       [ '0:0:0:0:0:0:0:0', '::' ],
+                       [ '0:0:0::0:0:0', '::' ],
+                       [ '0:0:0:1:0:0:0:0', '0:0:0:1::' ],
+                       [ '0:0::f', '::f' ],
+                       [ '0::0:0:0:33:fef:b', '::33:fef:b' ],
+                       [ '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ],
+                       [ '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ],
+                       [ 'abbc:2004::0:0:0:0', 'abbc:2004::' ],
+                       [ 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ],
+                       [ '0:0:0:0:0:0:0:0/16', '::/16' ],
+                       [ '0:0:0::0:0:0/64', '::/64' ],
+                       [ '0:0::f/52', '::f/52' ],
+                       [ '::0:0:33:fef:b/52', '::33:fef:b/52' ],
+                       [ '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ],
+                       [ '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ],
+                       [ 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ],
+                       [ 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ],
+               ];
+       }
+}
index 6eb96b1..881f5e1 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /**
  * A MemoizedCallable subclass that stores function return values
- * in an instance property rather than APC.
+ * in an instance property rather than APC or APCu.
  */
 class ArrayBackedMemoizedCallable extends MemoizedCallable {
        private $cache = [];
@@ -44,7 +44,7 @@ class MemoizedCallableTest extends PHPUnit_Framework_TestCase {
         * Consecutive calls to the memoized callable with the same arguments
         * should result in just one invocation of the underlying callable.
         *
-        * @requires function apc_store
+        * @requires function apc_store/apcu_store
         */
        public function testCallableMemoized() {
                $observer = $this->getMock( 'stdClass', [ 'computeSomething' ] );
diff --git a/tests/phpunit/includes/libs/WaitConditionLoopTest.php b/tests/phpunit/includes/libs/WaitConditionLoopTest.php
deleted file mode 100644 (file)
index 9ce93d6..0000000
+++ /dev/null
@@ -1,179 +0,0 @@
-<?php
-
-class WaitConditionLoopFakeTime extends WaitConditionLoop {
-       protected $wallClock = 1;
-
-       function __construct( callable $condition, $timeout, array $busyCallbacks ) {
-               parent::__construct( $condition, $timeout, $busyCallbacks );
-       }
-
-       function usleep( $microseconds ) {
-               $this->wallClock += $microseconds / 1e6;
-       }
-
-       function getCpuTime() {
-               return 0.0;
-       }
-
-       function getWallTime() {
-               return $this->wallClock;
-       }
-
-       public function setWallClock( &$timestamp ) {
-               $this->wallClock =& $timestamp;
-       }
-}
-
-class WaitConditionLoopTest extends PHPUnit_Framework_TestCase {
-       public function testCallbackReached() {
-               $wallClock = microtime( true );
-
-               $count = 0;
-               $status = new StatusValue();
-               $loop = new WaitConditionLoopFakeTime(
-                       function () use ( &$count, $status ) {
-                               ++$count;
-                               $status->value = 'cookie';
-
-                               return WaitConditionLoop::CONDITION_REACHED;
-                       },
-                       10.0,
-                       $this->newBusyWork( $x, $y, $z )
-               );
-               $this->assertEquals( $loop::CONDITION_REACHED, $loop->invoke() );
-               $this->assertEquals( 1, $count );
-               $this->assertEquals( 'cookie', $status->value );
-               $this->assertEquals( [ 0, 0, 0 ], [ $x, $y, $z ], "No busy work done" );
-
-               $count = 0;
-               $loop = new WaitConditionLoopFakeTime(
-                       function () use ( &$count, &$wallClock ) {
-                               $wallClock += 1;
-                               ++$count;
-
-                               return $count >= 2 ? WaitConditionLoop::CONDITION_REACHED : false;
-                       },
-                       7.0,
-                       $this->newBusyWork( $x, $y, $z, $wallClock )
-               );
-               $this->assertEquals( $loop::CONDITION_REACHED, $loop->invoke(),
-                       "Busy work did not cause timeout" );
-               $this->assertEquals( [ 1, 0, 0 ], [ $x, $y, $z ] );
-
-               $count = 0;
-               $loop = new WaitConditionLoopFakeTime(
-                       function () use ( &$count, &$wallClock ) {
-                               $wallClock += .1;
-                               ++$count;
-
-                               return $count > 80 ? true : false;
-                       },
-                       50.0,
-                       $this->newBusyWork( $x, $y, $z, $wallClock, $dontCallMe, $badCalls )
-               );
-               $this->assertEquals( 0, $badCalls, "Callback exception not yet called" );
-               $this->assertEquals( $loop::CONDITION_REACHED, $loop->invoke() );
-               $this->assertEquals( [ 1, 1, 1 ], [ $x, $y, $z ], "Busy work done" );
-               $this->assertEquals( 1, $badCalls, "Bad callback ran and was exception caught" );
-
-               try {
-                       $e = null;
-                       $dontCallMe();
-               } catch ( Exception $e ) {
-               }
-
-               $this->assertInstanceOf( 'RunTimeException', $e );
-               $this->assertEquals( 1, $badCalls, "Callback exception cached" );
-       }
-
-       public function testCallbackTimeout() {
-               $count = 0;
-               $wallClock = microtime( true );
-               $loop = new WaitConditionLoopFakeTime(
-                       function () use ( &$count, &$wallClock ) {
-                               $wallClock += 3;
-                               ++$count;
-
-                               return $count > 300 ? true : false;
-                       },
-                       50.0,
-                       $this->newBusyWork( $x, $y, $z, $wallClock )
-               );
-               $loop->setWallClock( $wallClock );
-               $this->assertEquals( $loop::CONDITION_TIMED_OUT, $loop->invoke() );
-               $this->assertEquals( [ 1, 1, 1 ], [ $x, $y, $z ], "Busy work done" );
-
-               $loop = new WaitConditionLoopFakeTime(
-                       function () use ( &$count, &$wallClock ) {
-                               $wallClock += 3;
-                               ++$count;
-
-                               return true;
-                       },
-                       0.0,
-                       $this->newBusyWork( $x, $y, $z, $wallClock )
-               );
-               $this->assertEquals( $loop::CONDITION_REACHED, $loop->invoke() );
-
-               $count = 0;
-               $loop = new WaitConditionLoopFakeTime(
-                       function () use ( &$count, &$wallClock ) {
-                               $wallClock += 3;
-                               ++$count;
-
-                               return $count > 10 ? true : false;
-                       },
-                       0,
-                       $this->newBusyWork( $x, $y, $z, $wallClock )
-               );
-               $this->assertEquals( $loop::CONDITION_FAILED, $loop->invoke() );
-       }
-
-       public function testCallbackAborted() {
-               $x = 0;
-               $wallClock = microtime( true );
-               $loop = new WaitConditionLoopFakeTime(
-                       function () use ( &$x, &$wallClock ) {
-                               $wallClock += 2;
-                               ++$x;
-
-                               return $x > 2 ? WaitConditionLoop::CONDITION_ABORTED : false;
-                       },
-                       10.0,
-                       $this->newBusyWork( $x, $y, $z, $wallClock )
-               );
-               $loop->setWallClock( $wallClock );
-               $this->assertEquals( $loop::CONDITION_ABORTED, $loop->invoke() );
-       }
-
-       private function newBusyWork(
-               &$x, &$y, &$z, &$wallClock = 1, &$dontCallMe = null, &$badCalls = 0
-       ) {
-               $x = $y = $z = 0;
-               $badCalls = 0;
-
-               $list = [];
-               $list[] = function () use ( &$x, &$wallClock ) {
-                       $wallClock += 1;
-
-                       return ++$x;
-               };
-               $dontCallMe = function () use ( &$badCalls ) {
-                       ++$badCalls;
-                       throw new RuntimeException( "TrollyMcTrollFace" );
-               };
-               $list[] =& $dontCallMe;
-               $list[] = function () use ( &$y, &$wallClock ) {
-                       $wallClock += 15;
-
-                       return ++$y;
-               };
-               $list[] = function () use ( &$z, &$wallClock ) {
-                       $wallClock += 0.1;
-
-                       return ++$z;
-               };
-
-               return $list;
-       }
-}
index 2072752..3cde3e2 100644 (file)
@@ -28,6 +28,7 @@ class ComposerJsonTest extends MediaWikiTestCase {
        }
 
        /**
+        * @covers ComposerJson::__construct
         * @covers ComposerJson::getRequiredDependencies
         */
        public function testGetRequiredDependencies() {
index 75eb62c..3d5e8d3 100644 (file)
@@ -19,6 +19,7 @@ class ComposerLockTest extends MediaWikiTestCase {
        }
 
        /**
+        * @covers ComposerLock::__construct
         * @covers ComposerLock::getInstalledDependencies
         */
        public function testGetInstalledDependencies() {
index 92fb954..a1afa77 100644 (file)
@@ -1,4 +1,7 @@
 <?php
+
+use Wikimedia\ScopedCallback;
+
 /**
  * @author Matthias Mullie <mmullie@wikimedia.org>
  * @group BagOStuff
@@ -251,20 +254,20 @@ class BagOStuffTest extends MediaWikiTestCase {
                $value1 = $this->cache->getScopedLock( $key, 0 );
                $value2 = $this->cache->getScopedLock( $key, 0 );
 
-               $this->assertType( 'ScopedCallback', $value1, 'First call returned lock' );
+               $this->assertType( ScopedCallback::class, $value1, 'First call returned lock' );
                $this->assertNull( $value2, 'Duplicate call returned no lock' );
 
                unset( $value1 );
 
                $value3 = $this->cache->getScopedLock( $key, 0 );
-               $this->assertType( 'ScopedCallback', $value3, 'Lock returned callback after release' );
+               $this->assertType( ScopedCallback::class, $value3, 'Lock returned callback after release' );
                unset( $value3 );
 
                $value1 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' );
                $value2 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' );
 
-               $this->assertType( 'ScopedCallback', $value1, 'First reentrant call returned lock' );
-               $this->assertType( 'ScopedCallback', $value1, 'Second reentrant call returned lock' );
+               $this->assertType( ScopedCallback::class, $value1, 'First reentrant call returned lock' );
+               $this->assertType( ScopedCallback::class, $value1, 'Second reentrant call returned lock' );
        }
 
        /**
index 99b959b..f43a3f3 100644 (file)
@@ -22,6 +22,7 @@ class WANObjectCacheTest extends MediaWikiTestCase {
                }
 
                $wanCache = TestingAccessWrapper::newFromObject( $this->cache );
+               /** @noinspection PhpUndefinedFieldInspection */
                $this->internalCache = $wanCache->cache;
        }
 
@@ -29,13 +30,14 @@ class WANObjectCacheTest extends MediaWikiTestCase {
         * @dataProvider provideSetAndGet
         * @covers WANObjectCache::set()
         * @covers WANObjectCache::get()
+        * @covers WANObjectCache::makeKey()
         * @param mixed $value
         * @param integer $ttl
         */
        public function testSetAndGet( $value, $ttl ) {
                $curTTL = null;
                $asOf = null;
-               $key = wfRandomString();
+               $key = $this->cache->makeKey( 'x', wfRandomString() );
 
                $this->cache->get( $key, $curTTL, [], $asOf );
                $this->assertNull( $curTTL, "Current TTL is null" );
@@ -71,9 +73,10 @@ class WANObjectCacheTest extends MediaWikiTestCase {
 
        /**
         * @covers WANObjectCache::get()
+        * @covers WANObjectCache::makeGlobalKey()
         */
        public function testGetNotExists() {
-               $key = wfRandomString();
+               $key = $this->cache->makeGlobalKey( 'y', wfRandomString(), 'p' );
                $curTTL = null;
                $value = $this->cache->get( $key, $curTTL );
 
@@ -165,7 +168,7 @@ class WANObjectCacheTest extends MediaWikiTestCase {
                $priorAsOf = null;
                $wasSet = 0;
                $func = function( $old, &$ttl, &$opts, $asOf )
-                       use ( &$wasSet, &$priorValue, &$priorAsOf, $value )
+               use ( &$wasSet, &$priorValue, &$priorAsOf, $value )
                {
                        ++$wasSet;
                        $priorValue = $old;
@@ -188,9 +191,9 @@ class WANObjectCacheTest extends MediaWikiTestCase {
 
                $wasSet = 0;
                $v = $cache->getWithSetCallback( $key, 30, $func, [
-                       'lowTTL' => 0,
-                       'lockTSE' => 5,
-               ] + $extOpts );
+                               'lowTTL' => 0,
+                               'lockTSE' => 5,
+                       ] + $extOpts );
                $this->assertEquals( $value, $v, "Value returned" );
                $this->assertEquals( 0, $wasSet, "Value not regenerated" );
 
@@ -247,6 +250,150 @@ class WANObjectCacheTest extends MediaWikiTestCase {
                ];
        }
 
+       /**
+        * @dataProvider getMultiWithSetCallback_provider
+        * @covers WANObjectCache::geMultitWithSetCallback()
+        * @covers WANObjectCache::makeMultiKeys()
+        * @param array $extOpts
+        * @param bool $versioned
+        */
+       public function testGetMultiWithSetCallback( array $extOpts, $versioned ) {
+               $cache = $this->cache;
+
+               $keyA = wfRandomString();
+               $keyB = wfRandomString();
+               $keyC = wfRandomString();
+               $cKey1 = wfRandomString();
+               $cKey2 = wfRandomString();
+
+               $priorValue = null;
+               $priorAsOf = null;
+               $wasSet = 0;
+               $genFunc = function ( $id, $old, &$ttl, &$opts, $asOf ) use (
+                       &$wasSet, &$priorValue, &$priorAsOf
+               ) {
+                       ++$wasSet;
+                       $priorValue = $old;
+                       $priorAsOf = $asOf;
+                       $ttl = 20; // override with another value
+                       return "@$id$";
+               };
+
+               $wasSet = 0;
+               $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
+               $value = "@3353$";
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'lockTSE' => 5 ] + $extOpts );
+               $this->assertEquals( $value, $v[$keyA], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated" );
+               $this->assertFalse( $priorValue, "No prior value" );
+               $this->assertNull( $priorAsOf, "No prior value" );
+
+               $curTTL = null;
+               $cache->get( $keyA, $curTTL );
+               $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
+               $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
+
+               $wasSet = 0;
+               $value = "@efef$";
+               $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
+               $this->assertEquals( $value, $v[$keyB], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated" );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
+               $this->assertEquals( $value, $v[$keyB], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value not regenerated" );
+
+               $priorTime = microtime( true );
+               usleep( 1 );
+               $wasSet = 0;
+               $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
+               );
+               $this->assertEquals( $value, $v[$keyB], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
+               $this->assertEquals( $value, $priorValue, "Has prior value" );
+               $this->assertType( 'float', $priorAsOf, "Has prior value" );
+               $t1 = $cache->getCheckKeyTime( $cKey1 );
+               $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
+               $t2 = $cache->getCheckKeyTime( $cKey2 );
+               $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
+
+               $priorTime = microtime( true );
+               $value = "@43636$";
+               $wasSet = 0;
+               $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
+               );
+               $this->assertEquals( $value, $v[$keyC], "Value returned" );
+               $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
+               $t1 = $cache->getCheckKeyTime( $cKey1 );
+               $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
+               $t2 = $cache->getCheckKeyTime( $cKey2 );
+               $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
+
+               $curTTL = null;
+               $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
+               if ( $versioned ) {
+                       $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
+               } else {
+                       $this->assertEquals( $value, $v, "Value returned" );
+               }
+               $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
+
+               $wasSet = 0;
+               $key = wfRandomString();
+               $keyedIds = new ArrayIterator( [ $key => 242424 ] );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
+               $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
+               $cache->delete( $key );
+               $keyedIds = new ArrayIterator( [ $key => 242424 ] );
+               $v = $cache->getMultiWithSetCallback(
+                       $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
+               $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
+               $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
+
+               $calls = 0;
+               $ids = [ 1, 2, 3, 4, 5, 6 ];
+               $keyFunc = function ( $id, WANObjectCache $wanCache ) {
+                       return $wanCache->makeKey( 'test', $id );
+               };
+               $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
+               $genFunc = function ( $id, $oldValue, &$ttl, array &$setops ) use ( &$calls ) {
+                       ++$calls;
+
+                       return "val-{$id}";
+               };
+               $values = $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
+
+               $this->assertEquals(
+                       [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
+                       array_values( $values ),
+                       "Correct values in correct order"
+               );
+               $this->assertEquals(
+                       array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
+                       array_keys( $values ),
+                       "Correct keys in correct order"
+               );
+               $this->assertEquals( count( $ids ), $calls );
+
+               $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
+               $this->assertEquals( count( $ids ), $calls, "Values cached" );
+       }
+
+       public static function getMultiWithSetCallback_provider() {
+               return [
+                       [ [], false ],
+                       [ [ 'version' => 1 ], true ]
+               ];
+       }
+
        /**
         * @covers WANObjectCache::getWithSetCallback()
         * @covers WANObjectCache::doGetWithSetCallback()
@@ -777,9 +924,14 @@ class WANObjectCacheTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideAdaptiveTTL
         * @covers WANObjectCache::adaptiveTTL()
+        * @param float|int $ago
+        * @param int $maxTTL
+        * @param int $minTTL
+        * @param float $factor
+        * @param int $adaptiveTTL
         */
        public function testAdaptiveTTL( $ago, $maxTTL, $minTTL, $factor, $adaptiveTTL ) {
-               $mtime = is_int( $ago ) ? time() - $ago : $ago;
+               $mtime = $ago ? time() - $ago : $ago;
                $margin = 5;
                $ttl = $this->cache->adaptiveTTL( $mtime, $maxTTL, $minTTL, $factor );
 
diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php
new file mode 100644 (file)
index 0000000..d13fbf9
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @covers DatabaseDomain
+ */
+class DatabaseDomainTest extends PHPUnit_Framework_TestCase {
+       public static function provideConstruct() {
+               return [
+                       // All strings
+                       [ 'foo', 'bar', 'baz', 'foo-bar-baz' ],
+                       // Nothing
+                       [ null, null, '', '' ],
+                       // Invalid $database
+                       [ 0, 'bar', '', '', true ],
+                       // - in one of the fields
+                       [ 'foo-bar', 'baz', 'baa', 'foo?hbar-baz-baa' ],
+                       // ? in one of the fields
+                       [ 'foo?bar', 'baz', 'baa', 'foo??bar-baz-baa' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstruct
+        */
+       public function testConstruct( $db, $schema, $prefix, $id, $exception = false ) {
+               if ( $exception ) {
+                       $this->setExpectedException( InvalidArgumentException::class );
+               }
+
+               $domain = new DatabaseDomain( $db, $schema, $prefix );
+               $this->assertInstanceOf( DatabaseDomain::class, $domain );
+               $this->assertEquals( $db, $domain->getDatabase() );
+               $this->assertEquals( $schema, $domain->getSchema() );
+               $this->assertEquals( $prefix, $domain->getTablePrefix() );
+               $this->assertEquals( $id, $domain->getId() );
+       }
+
+       public static function provideNewFromId() {
+               return [
+                       // basic
+                       [ 'foo', 'foo', null, '' ],
+                       // <database>-<prefix>
+                       [ 'foo-bar', 'foo', null, 'bar' ],
+                       [ 'foo-bar-baz', 'foo', 'bar', 'baz' ],
+                       // ?h -> -
+                       [ 'foo?hbar-baz-baa', 'foo-bar', 'baz', 'baa' ],
+                       // ?? -> ?
+                       [ 'foo??bar-baz-baa', 'foo?bar', 'baz', 'baa' ],
+                       // ? is left alone
+                       [ 'foo?bar-baz-baa', 'foo?bar', 'baz', 'baa' ],
+                       // too many parts
+                       [ 'foo-bar-baz-baa', '', '', '', true ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewFromId
+        */
+       public function testNewFromId( $id, $db, $schema, $prefix, $exception = false ) {
+               if ( $exception ) {
+                       $this->setExpectedException( InvalidArgumentException::class );
+               }
+               $domain = DatabaseDomain::newFromId( $id );
+               $this->assertInstanceOf( DatabaseDomain::class, $domain );
+               $this->assertEquals( $db, $domain->getDatabase() );
+               $this->assertEquals( $schema, $domain->getSchema() );
+               $this->assertEquals( $prefix, $domain->getTablePrefix() );
+       }
+}
diff --git a/tests/phpunit/includes/libs/time/ConvertableTimestampTest.php b/tests/phpunit/includes/libs/time/ConvertableTimestampTest.php
deleted file mode 100644 (file)
index 88c2989..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-<?php
-
-/**
- * Tests timestamp parsing and output.
- */
-class ConvertableTimestampTest extends PHPUnit_Framework_TestCase {
-       /**
-        * @covers ConvertableTimestamp::__construct
-        */
-       public function testConstructWithNoTimestamp() {
-               $timestamp = new ConvertableTimestamp();
-               $this->assertInternalType( 'string', $timestamp->getTimestamp() );
-               $this->assertNotEmpty( $timestamp->getTimestamp() );
-               $this->assertNotEquals( false, strtotime( $timestamp->getTimestamp( TS_MW ) ) );
-       }
-
-       /**
-        * @covers ConvertableTimestamp::__toString
-        */
-       public function testToString() {
-               $timestamp = new ConvertableTimestamp( '1406833268' ); // Equivalent to 20140731190108
-               $this->assertEquals( '1406833268', $timestamp->__toString() );
-       }
-
-       public static function provideValidTimestampDifferences() {
-               return [
-                       [ '1406833268', '1406833269', '00 00 00 01' ],
-                       [ '1406833268', '1406833329', '00 00 01 01' ],
-                       [ '1406833268', '1406836929', '00 01 01 01' ],
-                       [ '1406833268', '1406923329', '01 01 01 01' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideValidTimestampDifferences
-        * @covers ConvertableTimestamp::diff
-        */
-       public function testDiff( $timestamp1, $timestamp2, $expected ) {
-               $timestamp1 = new ConvertableTimestamp( $timestamp1 );
-               $timestamp2 = new ConvertableTimestamp( $timestamp2 );
-               $diff = $timestamp1->diff( $timestamp2 );
-               $this->assertEquals( $expected, $diff->format( '%D %H %I %S' ) );
-       }
-
-       /**
-        * Test parsing of valid timestamps and outputing to MW format.
-        * @dataProvider provideValidTimestamps
-        * @covers ConvertableTimestamp::getTimestamp
-        */
-       public function testValidParse( $format, $original, $expected ) {
-               $timestamp = new ConvertableTimestamp( $original );
-               $this->assertEquals( $expected, $timestamp->getTimestamp( TS_MW ) );
-       }
-
-       /**
-        * Test outputting valid timestamps to different formats.
-        * @dataProvider provideValidTimestamps
-        * @covers ConvertableTimestamp::getTimestamp
-        */
-       public function testValidOutput( $format, $expected, $original ) {
-               $timestamp = new ConvertableTimestamp( $original );
-               $this->assertEquals( $expected, (string)$timestamp->getTimestamp( $format ) );
-       }
-
-       /**
-        * Test an invalid timestamp.
-        * @expectedException TimestampException
-        * @covers ConvertableTimestamp
-        */
-       public function testInvalidParse() {
-               new ConvertableTimestamp( "This is not a timestamp." );
-       }
-
-       /**
-        * Test an out of range timestamp
-        * @dataProvider provideOutOfRangeTimestamps
-        * @expectedException TimestampException
-        * @covers ConvertableTimestamp
-        */
-       public function testOutOfRangeTimestamps( $format, $input ) {
-               $timestamp = new ConvertableTimestamp( $input );
-               $timestamp->getTimestamp( $format );
-       }
-
-       /**
-        * Test requesting an invalid output format.
-        * @expectedException TimestampException
-        * @covers ConvertableTimestamp::getTimestamp
-        */
-       public function testInvalidOutput() {
-               $timestamp = new ConvertableTimestamp( '1343761268' );
-               $timestamp->getTimestamp( 98 );
-       }
-
-       /**
-        * Returns a list of valid timestamps in the format:
-        * [ type, timestamp_of_type, timestamp_in_MW ]
-        */
-       public static function provideValidTimestamps() {
-               return [
-                       // Various formats
-                       [ TS_UNIX, '1343761268', '20120731190108' ],
-                       [ TS_MW, '20120731190108', '20120731190108' ],
-                       [ TS_DB, '2012-07-31 19:01:08', '20120731190108' ],
-                       [ TS_ISO_8601, '2012-07-31T19:01:08Z', '20120731190108' ],
-                       [ TS_ISO_8601_BASIC, '20120731T190108Z', '20120731190108' ],
-                       [ TS_EXIF, '2012:07:31 19:01:08', '20120731190108' ],
-                       [ TS_RFC2822, 'Tue, 31 Jul 2012 19:01:08 GMT', '20120731190108' ],
-                       [ TS_ORACLE, '31-07-2012 19:01:08.000000', '20120731190108' ],
-                       [ TS_POSTGRES, '2012-07-31 19:01:08 GMT', '20120731190108' ],
-                       // Some extremes and weird values
-                       [ TS_ISO_8601, '9999-12-31T23:59:59Z', '99991231235959' ],
-                       [ TS_UNIX, '-62135596801', '00001231235959' ]
-               ];
-       }
-
-       /**
-        * Returns a list of out of range timestamps in the format:
-        * [ type, timestamp_of_type ]
-        */
-       public static function provideOutOfRangeTimestamps() {
-               return [
-                       // Various formats
-                       [ TS_MW, '-62167219201' ], // -0001-12-31T23:59:59Z
-                       [ TS_MW, '253402300800' ], // 10000-01-01T00:00:00Z
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/libs/time/ConvertibleTimestampTest.php b/tests/phpunit/includes/libs/time/ConvertibleTimestampTest.php
new file mode 100644 (file)
index 0000000..d48caf3
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * Tests timestamp parsing and output.
+ */
+class ConvertibleTimestampTest extends PHPUnit_Framework_TestCase {
+       /**
+        * @covers ConvertibleTimestamp::__construct
+        */
+       public function testConstructWithNoTimestamp() {
+               $timestamp = new ConvertibleTimestamp();
+               $this->assertInternalType( 'string', $timestamp->getTimestamp() );
+               $this->assertNotEmpty( $timestamp->getTimestamp() );
+               $this->assertNotEquals( false, strtotime( $timestamp->getTimestamp( TS_MW ) ) );
+       }
+
+       /**
+        * @covers ConvertibleTimestamp::__toString
+        */
+       public function testToString() {
+               $timestamp = new ConvertibleTimestamp( '1406833268' ); // Equivalent to 20140731190108
+               $this->assertEquals( '1406833268', $timestamp->__toString() );
+       }
+
+       public static function provideValidTimestampDifferences() {
+               return [
+                       [ '1406833268', '1406833269', '00 00 00 01' ],
+                       [ '1406833268', '1406833329', '00 00 01 01' ],
+                       [ '1406833268', '1406836929', '00 01 01 01' ],
+                       [ '1406833268', '1406923329', '01 01 01 01' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideValidTimestampDifferences
+        * @covers ConvertibleTimestamp::diff
+        */
+       public function testDiff( $timestamp1, $timestamp2, $expected ) {
+               $timestamp1 = new ConvertibleTimestamp( $timestamp1 );
+               $timestamp2 = new ConvertibleTimestamp( $timestamp2 );
+               $diff = $timestamp1->diff( $timestamp2 );
+               $this->assertEquals( $expected, $diff->format( '%D %H %I %S' ) );
+       }
+
+       /**
+        * Test parsing of valid timestamps and outputing to MW format.
+        * @dataProvider provideValidTimestamps
+        * @covers ConvertibleTimestamp::getTimestamp
+        */
+       public function testValidParse( $format, $original, $expected ) {
+               $timestamp = new ConvertibleTimestamp( $original );
+               $this->assertEquals( $expected, $timestamp->getTimestamp( TS_MW ) );
+       }
+
+       /**
+        * Test outputting valid timestamps to different formats.
+        * @dataProvider provideValidTimestamps
+        * @covers ConvertibleTimestamp::getTimestamp
+        */
+       public function testValidOutput( $format, $expected, $original ) {
+               $timestamp = new ConvertibleTimestamp( $original );
+               $this->assertEquals( $expected, (string)$timestamp->getTimestamp( $format ) );
+       }
+
+       /**
+        * Test an invalid timestamp.
+        * @expectedException TimestampException
+        * @covers ConvertibleTimestamp
+        */
+       public function testInvalidParse() {
+               new ConvertibleTimestamp( "This is not a timestamp." );
+       }
+
+       /**
+        * @dataProvider provideValidTimestamps
+        * @covers ConvertibleTimestamp::convert
+        */
+       public function testConvert( $format, $expected, $original ) {
+               $this->assertSame( $expected, ConvertibleTimestamp::convert( $format, $original ) );
+       }
+
+       /**
+        * Format an invalid timestamp.
+        * @covers ConvertibleTimestamp::convert
+        */
+       public function testConvertInvalid() {
+               $this->assertSame( false, ConvertibleTimestamp::convert( 'Not a timestamp', 0 ) );
+       }
+
+       /**
+        * Test an out of range timestamp
+        * @dataProvider provideOutOfRangeTimestamps
+        * @expectedException TimestampException
+        * @covers       ConvertibleTimestamp
+        */
+       public function testOutOfRangeTimestamps( $format, $input ) {
+               $timestamp = new ConvertibleTimestamp( $input );
+               $timestamp->getTimestamp( $format );
+       }
+
+       /**
+        * Test requesting an invalid output format.
+        * @expectedException TimestampException
+        * @covers ConvertibleTimestamp::getTimestamp
+        */
+       public function testInvalidOutput() {
+               $timestamp = new ConvertibleTimestamp( '1343761268' );
+               $timestamp->getTimestamp( 98 );
+       }
+
+       /**
+        * Returns a list of valid timestamps in the format:
+        * [ type, timestamp_of_type, timestamp_in_MW ]
+        */
+       public static function provideValidTimestamps() {
+               return [
+                       // Various formats
+                       [ TS_UNIX, '1343761268', '20120731190108' ],
+                       [ TS_MW, '20120731190108', '20120731190108' ],
+                       [ TS_DB, '2012-07-31 19:01:08', '20120731190108' ],
+                       [ TS_ISO_8601, '2012-07-31T19:01:08Z', '20120731190108' ],
+                       [ TS_ISO_8601_BASIC, '20120731T190108Z', '20120731190108' ],
+                       [ TS_EXIF, '2012:07:31 19:01:08', '20120731190108' ],
+                       [ TS_RFC2822, 'Tue, 31 Jul 2012 19:01:08 GMT', '20120731190108' ],
+                       [ TS_ORACLE, '31-07-2012 19:01:08.000000', '20120731190108' ],
+                       [ TS_POSTGRES, '2012-07-31 19:01:08 GMT', '20120731190108' ],
+                       // Some extremes and weird values
+                       [ TS_ISO_8601, '9999-12-31T23:59:59Z', '99991231235959' ],
+                       [ TS_UNIX, '-62135596801', '00001231235959' ]
+               ];
+       }
+
+       /**
+        * Returns a list of out of range timestamps in the format:
+        * [ type, timestamp_of_type ]
+        */
+       public static function provideOutOfRangeTimestamps() {
+               return [
+                       // Various formats
+                       [ TS_MW, '-62167219201' ], // -0001-12-31T23:59:59Z
+                       [ TS_MW, '253402300800' ], // 10000-01-01T00:00:00Z
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/libs/xmp/XMPTest.php b/tests/phpunit/includes/libs/xmp/XMPTest.php
new file mode 100644 (file)
index 0000000..ac52a39
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+
+/**
+ * @group Media
+ * @covers XMPReader
+ */
+class XMPTest extends PHPUnit_Framework_TestCase  {
+
+       protected function setUp() {
+               parent::setUp();
+               # Requires libxml to do XMP parsing
+               if ( !extension_loaded( 'exif' ) ) {
+                       $this->markTestSkipped( "PHP extension 'exif' is not loaded, skipping." );
+               }
+       }
+
+       /**
+        * Put XMP in, compare what comes out...
+        *
+        * @param string $xmp The actual xml data.
+        * @param array $expected Expected result of parsing the xmp.
+        * @param string $info Short sentence on what's being tested.
+        *
+        * @throws Exception
+        * @dataProvider provideXMPParse
+        *
+        * @covers XMPReader::parse
+        */
+       public function testXMPParse( $xmp, $expected, $info ) {
+               if ( !is_string( $xmp ) || !is_array( $expected ) ) {
+                       throw new Exception( "Invalid data provided to " . __METHOD__ );
+               }
+               $reader = new XMPReader;
+               $reader->parse( $xmp );
+               $this->assertEquals( $expected, $reader->getResults(), $info, 0.0000000001 );
+       }
+
+       public static function provideXMPParse() {
+               $xmpPath = __DIR__ . '/../../../data/xmp/';
+               $data = [];
+
+               // $xmpFiles format: array of arrays with first arg file base name,
+               // with the actual file having .xmp on the end for the xmp
+               // and .result.php on the end for a php file containing the result
+               // array. Second argument is some info on what's being tested.
+               $xmpFiles = [
+                       [ '1', 'parseType=Resource test' ],
+                       [ '2', 'Structure with mixed attribute and element props' ],
+                       [ '3', 'Extra qualifiers (that should be ignored)' ],
+                       [ '3-invalid', 'Test ignoring qualifiers that look like normal props' ],
+                       [ '4', 'Flash as qualifier' ],
+                       [ '5', 'Flash as qualifier 2' ],
+                       [ '6', 'Multiple rdf:Description' ],
+                       [ '7', 'Generic test of several property types' ],
+                       [ 'flash', 'Test of Flash property' ],
+                       [ 'invalid-child-not-struct', 'Test child props not in struct or ignored' ],
+                       [ 'no-recognized-props', 'Test namespace and no recognized props' ],
+                       [ 'no-namespace', 'Test non-namespaced attributes are ignored' ],
+                       [ 'bag-for-seq', "Allow bag's instead of seq's. (bug 27105)" ],
+                       [ 'utf16BE', 'UTF-16BE encoding' ],
+                       [ 'utf16LE', 'UTF-16LE encoding' ],
+                       [ 'utf32BE', 'UTF-32BE encoding' ],
+                       [ 'utf32LE', 'UTF-32LE encoding' ],
+                       [ 'xmpExt', 'Extended XMP missing second part' ],
+                       [ 'gps', 'Handling of exif GPS parameters in XMP' ],
+               ];
+
+               $xmpFiles[] = [ 'doctype-included', 'XMP includes doctype' ];
+
+               foreach ( $xmpFiles as $file ) {
+                       $xmp = file_get_contents( $xmpPath . $file[0] . '.xmp' );
+                       // I'm not sure if this is the best way to handle getting the
+                       // result array, but it seems kind of big to put directly in the test
+                       // file.
+                       $result = null;
+                       include $xmpPath . $file[0] . '.result.php';
+                       $data[] = [ $xmp, $result, '[' . $file[0] . '.xmp] ' . $file[1] ];
+               }
+
+               return $data;
+       }
+
+       /** Test ExtendedXMP block support. (Used when the XMP has to be split
+        * over multiple jpeg segments, due to 64k size limit on jpeg segments.
+        *
+        * @todo This is based on what the standard says. Need to find a real
+        * world example file to double check the support for this is right.
+        *
+        * @covers XMPReader::parseExtended
+        */
+       public function testExtendedXMP() {
+               $xmpPath = __DIR__ . '/../../../data/xmp/';
+               $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
+               $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
+
+               $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp
+               $length = pack( 'N', strlen( $extendedXMP ) );
+               $offset = pack( 'N', 0 );
+               $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
+
+               $reader = new XMPReader();
+               $reader->parse( $standardXMP );
+               $reader->parseExtended( $extendedPacket );
+               $actual = $reader->getResults();
+
+               $expected = [
+                       'xmp-exif' => [
+                               'DigitalZoomRatio' => '0/10',
+                               'Flash' => 9,
+                               'FNumber' => '2/10',
+                       ]
+               ];
+
+               $this->assertEquals( $expected, $actual );
+       }
+
+       /**
+        * This test has an extended XMP block with a wrong guid (md5sum)
+        * and thus should only return the StandardXMP, not the ExtendedXMP.
+        *
+        * @covers XMPReader::parseExtended
+        */
+       public function testExtendedXMPWithWrongGUID() {
+               $xmpPath = __DIR__ . '/../../../data/xmp/';
+               $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
+               $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
+
+               $md5sum = '28C74E0AC2D796886759006FBE2E57B9'; // Note last digit.
+               $length = pack( 'N', strlen( $extendedXMP ) );
+               $offset = pack( 'N', 0 );
+               $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
+
+               $reader = new XMPReader();
+               $reader->parse( $standardXMP );
+               $reader->parseExtended( $extendedPacket );
+               $actual = $reader->getResults();
+
+               $expected = [
+                       'xmp-exif' => [
+                               'DigitalZoomRatio' => '0/10',
+                               'Flash' => 9,
+                       ]
+               ];
+
+               $this->assertEquals( $expected, $actual );
+       }
+
+       /**
+        * Have a high offset to simulate a missing packet,
+        * which should cause it to ignore the ExtendedXMP packet.
+        *
+        * @covers XMPReader::parseExtended
+        */
+       public function testExtendedXMPMissingPacket() {
+               $xmpPath = __DIR__ . '/../../../data/xmp/';
+               $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
+               $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
+
+               $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp
+               $length = pack( 'N', strlen( $extendedXMP ) );
+               $offset = pack( 'N', 2048 );
+               $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
+
+               $reader = new XMPReader();
+               $reader->parse( $standardXMP );
+               $reader->parseExtended( $extendedPacket );
+               $actual = $reader->getResults();
+
+               $expected = [
+                       'xmp-exif' => [
+                               'DigitalZoomRatio' => '0/10',
+                               'Flash' => 9,
+                       ]
+               ];
+
+               $this->assertEquals( $expected, $actual );
+       }
+
+       /**
+        * Test for multi-section, hostile XML
+        * @covers XMPReader::checkParseSafety
+        */
+       public function testCheckParseSafety() {
+
+               // Test for detection
+               $xmpPath = __DIR__ . '/../../../data/xmp/';
+               $file = fopen( $xmpPath . 'doctype-included.xmp', 'rb' );
+               $valid = false;
+               $reader = new XMPReader();
+               do {
+                       $chunk = fread( $file, 10 );
+                       $valid = $reader->parse( $chunk, feof( $file ) );
+               } while ( !feof( $file ) );
+               $this->assertFalse( $valid, 'Check that doctype is detected in fragmented XML' );
+               $this->assertEquals(
+                       [],
+                       $reader->getResults(),
+                       'Check that doctype is detected in fragmented XML'
+               );
+               fclose( $file );
+               unset( $reader );
+
+               // Test for false positives
+               $file = fopen( $xmpPath . 'doctype-not-included.xmp', 'rb' );
+               $valid = false;
+               $reader = new XMPReader();
+               do {
+                       $chunk = fread( $file, 10 );
+                       $valid = $reader->parse( $chunk, feof( $file ) );
+               } while ( !feof( $file ) );
+               $this->assertTrue(
+                       $valid,
+                       'Check for false-positive detecting doctype in fragmented XML'
+               );
+               $this->assertEquals(
+                       [
+                               'xmp-exif' => [
+                                       'DigitalZoomRatio' => '0/10',
+                                       'Flash' => '9'
+                               ]
+                       ],
+                       $reader->getResults(),
+                       'Check that doctype is detected in fragmented XML'
+               );
+       }
+}
diff --git a/tests/phpunit/includes/libs/xmp/XMPValidateTest.php b/tests/phpunit/includes/libs/xmp/XMPValidateTest.php
new file mode 100644 (file)
index 0000000..7f7ea93
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+use Psr\Log\NullLogger;
+
+/**
+ * @group Media
+ */
+class XMPValidateTest extends PHPUnit_Framework_TestCase {
+
+       /**
+        * @dataProvider provideDates
+        * @covers XMPValidate::validateDate
+        */
+       public function testValidateDate( $value, $expected ) {
+               // The method should modify $value.
+               $validate = new XMPValidate( new NullLogger() );
+               $validate->validateDate( [], $value, true );
+               $this->assertEquals( $expected, $value );
+       }
+
+       public static function provideDates() {
+               /* For reference valid date formats are:
+                * YYYY
+                * YYYY-MM
+                * YYYY-MM-DD
+                * YYYY-MM-DDThh:mmTZD
+                * YYYY-MM-DDThh:mm:ssTZD
+                * YYYY-MM-DDThh:mm:ss.sTZD
+                * (Time zone is optional)
+                */
+               return [
+                       [ '1992', '1992' ],
+                       [ '1992-04', '1992:04' ],
+                       [ '1992-02-01', '1992:02:01' ],
+                       [ '2011-09-29', '2011:09:29' ],
+                       [ '1982-12-15T20:12', '1982:12:15 20:12' ],
+                       [ '1982-12-15T20:12Z', '1982:12:15 20:12' ],
+                       [ '1982-12-15T20:12+02:30', '1982:12:15 22:42' ],
+                       [ '1982-12-15T01:12-02:30', '1982:12:14 22:42' ],
+                       [ '1982-12-15T20:12:11', '1982:12:15 20:12:11' ],
+                       [ '1982-12-15T20:12:11Z', '1982:12:15 20:12:11' ],
+                       [ '1982-12-15T20:12:11+01:10', '1982:12:15 21:22:11' ],
+                       [ '2045-12-15T20:12:11', '2045:12:15 20:12:11' ],
+                       [ '1867-06-01T15:00:00', '1867:06:01 15:00:00' ],
+                       /* some invalid ones */
+                       [ '2001--12', null ],
+                       [ '2001-5-12', null ],
+                       [ '2001-5-12TZ', null ],
+                       [ '2001-05-12T15', null ],
+                       [ '2001-12T15:13', null ],
+               ];
+       }
+}
index 5042121..e854ab5 100644 (file)
@@ -26,7 +26,8 @@ abstract class MediaWikiMediaTestCase extends MediaWikiTestCase {
                $this->backend = new FSFileBackend( [
                        'name' => 'localtesting',
                        'wikiId' => wfWikiID(),
-                       'containerPaths' => $containers
+                       'containerPaths' => $containers,
+                       'tmpDirectory' => $this->getNewTempDirectory()
                ] );
                $this->repo = new FSRepo( $this->getRepoOptions() );
        }
diff --git a/tests/phpunit/includes/media/XMPTest.php b/tests/phpunit/includes/media/XMPTest.php
deleted file mode 100644 (file)
index bffe415..0000000
+++ /dev/null
@@ -1,223 +0,0 @@
-<?php
-
-/**
- * @group Media
- * @covers XMPReader
- */
-class XMPTest extends MediaWikiTestCase {
-
-       protected function setUp() {
-               parent::setUp();
-               $this->checkPHPExtension( 'exif' ); # Requires libxml to do XMP parsing
-       }
-
-       /**
-        * Put XMP in, compare what comes out...
-        *
-        * @param string $xmp The actual xml data.
-        * @param array $expected Expected result of parsing the xmp.
-        * @param string $info Short sentence on what's being tested.
-        *
-        * @throws Exception
-        * @dataProvider provideXMPParse
-        *
-        * @covers XMPReader::parse
-        */
-       public function testXMPParse( $xmp, $expected, $info ) {
-               if ( !is_string( $xmp ) || !is_array( $expected ) ) {
-                       throw new Exception( "Invalid data provided to " . __METHOD__ );
-               }
-               $reader = new XMPReader;
-               $reader->parse( $xmp );
-               $this->assertEquals( $expected, $reader->getResults(), $info, 0.0000000001 );
-       }
-
-       public static function provideXMPParse() {
-               $xmpPath = __DIR__ . '/../../data/xmp/';
-               $data = [];
-
-               // $xmpFiles format: array of arrays with first arg file base name,
-               // with the actual file having .xmp on the end for the xmp
-               // and .result.php on the end for a php file containing the result
-               // array. Second argument is some info on what's being tested.
-               $xmpFiles = [
-                       [ '1', 'parseType=Resource test' ],
-                       [ '2', 'Structure with mixed attribute and element props' ],
-                       [ '3', 'Extra qualifiers (that should be ignored)' ],
-                       [ '3-invalid', 'Test ignoring qualifiers that look like normal props' ],
-                       [ '4', 'Flash as qualifier' ],
-                       [ '5', 'Flash as qualifier 2' ],
-                       [ '6', 'Multiple rdf:Description' ],
-                       [ '7', 'Generic test of several property types' ],
-                       [ 'flash', 'Test of Flash property' ],
-                       [ 'invalid-child-not-struct', 'Test child props not in struct or ignored' ],
-                       [ 'no-recognized-props', 'Test namespace and no recognized props' ],
-                       [ 'no-namespace', 'Test non-namespaced attributes are ignored' ],
-                       [ 'bag-for-seq', "Allow bag's instead of seq's. (bug 27105)" ],
-                       [ 'utf16BE', 'UTF-16BE encoding' ],
-                       [ 'utf16LE', 'UTF-16LE encoding' ],
-                       [ 'utf32BE', 'UTF-32BE encoding' ],
-                       [ 'utf32LE', 'UTF-32LE encoding' ],
-                       [ 'xmpExt', 'Extended XMP missing second part' ],
-                       [ 'gps', 'Handling of exif GPS parameters in XMP' ],
-               ];
-
-               $xmpFiles[] = [ 'doctype-included', 'XMP includes doctype' ];
-
-               foreach ( $xmpFiles as $file ) {
-                       $xmp = file_get_contents( $xmpPath . $file[0] . '.xmp' );
-                       // I'm not sure if this is the best way to handle getting the
-                       // result array, but it seems kind of big to put directly in the test
-                       // file.
-                       $result = null;
-                       include $xmpPath . $file[0] . '.result.php';
-                       $data[] = [ $xmp, $result, '[' . $file[0] . '.xmp] ' . $file[1] ];
-               }
-
-               return $data;
-       }
-
-       /** Test ExtendedXMP block support. (Used when the XMP has to be split
-        * over multiple jpeg segments, due to 64k size limit on jpeg segments.
-        *
-        * @todo This is based on what the standard says. Need to find a real
-        * world example file to double check the support for this is right.
-        *
-        * @covers XMPReader::parseExtended
-        */
-       public function testExtendedXMP() {
-               $xmpPath = __DIR__ . '/../../data/xmp/';
-               $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
-               $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
-
-               $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp
-               $length = pack( 'N', strlen( $extendedXMP ) );
-               $offset = pack( 'N', 0 );
-               $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
-
-               $reader = new XMPReader();
-               $reader->parse( $standardXMP );
-               $reader->parseExtended( $extendedPacket );
-               $actual = $reader->getResults();
-
-               $expected = [
-                       'xmp-exif' => [
-                               'DigitalZoomRatio' => '0/10',
-                               'Flash' => 9,
-                               'FNumber' => '2/10',
-                       ]
-               ];
-
-               $this->assertEquals( $expected, $actual );
-       }
-
-       /**
-        * This test has an extended XMP block with a wrong guid (md5sum)
-        * and thus should only return the StandardXMP, not the ExtendedXMP.
-        *
-        * @covers XMPReader::parseExtended
-        */
-       public function testExtendedXMPWithWrongGUID() {
-               $xmpPath = __DIR__ . '/../../data/xmp/';
-               $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
-               $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
-
-               $md5sum = '28C74E0AC2D796886759006FBE2E57B9'; // Note last digit.
-               $length = pack( 'N', strlen( $extendedXMP ) );
-               $offset = pack( 'N', 0 );
-               $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
-
-               $reader = new XMPReader();
-               $reader->parse( $standardXMP );
-               $reader->parseExtended( $extendedPacket );
-               $actual = $reader->getResults();
-
-               $expected = [
-                       'xmp-exif' => [
-                               'DigitalZoomRatio' => '0/10',
-                               'Flash' => 9,
-                       ]
-               ];
-
-               $this->assertEquals( $expected, $actual );
-       }
-
-       /**
-        * Have a high offset to simulate a missing packet,
-        * which should cause it to ignore the ExtendedXMP packet.
-        *
-        * @covers XMPReader::parseExtended
-        */
-       public function testExtendedXMPMissingPacket() {
-               $xmpPath = __DIR__ . '/../../data/xmp/';
-               $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' );
-               $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' );
-
-               $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp
-               $length = pack( 'N', strlen( $extendedXMP ) );
-               $offset = pack( 'N', 2048 );
-               $extendedPacket = $md5sum . $length . $offset . $extendedXMP;
-
-               $reader = new XMPReader();
-               $reader->parse( $standardXMP );
-               $reader->parseExtended( $extendedPacket );
-               $actual = $reader->getResults();
-
-               $expected = [
-                       'xmp-exif' => [
-                               'DigitalZoomRatio' => '0/10',
-                               'Flash' => 9,
-                       ]
-               ];
-
-               $this->assertEquals( $expected, $actual );
-       }
-
-       /**
-        * Test for multi-section, hostile XML
-        * @covers XMPReader::checkParseSafety
-        */
-       public function testCheckParseSafety() {
-
-               // Test for detection
-               $xmpPath = __DIR__ . '/../../data/xmp/';
-               $file = fopen( $xmpPath . 'doctype-included.xmp', 'rb' );
-               $valid = false;
-               $reader = new XMPReader();
-               do {
-                       $chunk = fread( $file, 10 );
-                       $valid = $reader->parse( $chunk, feof( $file ) );
-               } while ( !feof( $file ) );
-               $this->assertFalse( $valid, 'Check that doctype is detected in fragmented XML' );
-               $this->assertEquals(
-                       [],
-                       $reader->getResults(),
-                       'Check that doctype is detected in fragmented XML'
-               );
-               fclose( $file );
-               unset( $reader );
-
-               // Test for false positives
-               $file = fopen( $xmpPath . 'doctype-not-included.xmp', 'rb' );
-               $valid = false;
-               $reader = new XMPReader();
-               do {
-                       $chunk = fread( $file, 10 );
-                       $valid = $reader->parse( $chunk, feof( $file ) );
-               } while ( !feof( $file ) );
-               $this->assertTrue(
-                       $valid,
-                       'Check for false-positive detecting doctype in fragmented XML'
-               );
-               $this->assertEquals(
-                       [
-                               'xmp-exif' => [
-                                       'DigitalZoomRatio' => '0/10',
-                                       'Flash' => '9'
-                               ]
-                       ],
-                       $reader->getResults(),
-                       'Check that doctype is detected in fragmented XML'
-               );
-       }
-}
diff --git a/tests/phpunit/includes/media/XMPValidateTest.php b/tests/phpunit/includes/media/XMPValidateTest.php
deleted file mode 100644 (file)
index 6a00629..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-use Psr\Log\NullLogger;
-
-/**
- * @group Media
- */
-class XMPValidateTest extends MediaWikiTestCase {
-
-       /**
-        * @dataProvider provideDates
-        * @covers XMPValidate::validateDate
-        */
-       public function testValidateDate( $value, $expected ) {
-               // The method should modify $value.
-               $validate = new XMPValidate( new NullLogger() );
-               $validate->validateDate( [], $value, true );
-               $this->assertEquals( $expected, $value );
-       }
-
-       public static function provideDates() {
-               /* For reference valid date formats are:
-                * YYYY
-                * YYYY-MM
-                * YYYY-MM-DD
-                * YYYY-MM-DDThh:mmTZD
-                * YYYY-MM-DDThh:mm:ssTZD
-                * YYYY-MM-DDThh:mm:ss.sTZD
-                * (Time zone is optional)
-                */
-               return [
-                       [ '1992', '1992' ],
-                       [ '1992-04', '1992:04' ],
-                       [ '1992-02-01', '1992:02:01' ],
-                       [ '2011-09-29', '2011:09:29' ],
-                       [ '1982-12-15T20:12', '1982:12:15 20:12' ],
-                       [ '1982-12-15T20:12Z', '1982:12:15 20:12' ],
-                       [ '1982-12-15T20:12+02:30', '1982:12:15 22:42' ],
-                       [ '1982-12-15T01:12-02:30', '1982:12:14 22:42' ],
-                       [ '1982-12-15T20:12:11', '1982:12:15 20:12:11' ],
-                       [ '1982-12-15T20:12:11Z', '1982:12:15 20:12:11' ],
-                       [ '1982-12-15T20:12:11+01:10', '1982:12:15 21:22:11' ],
-                       [ '2045-12-15T20:12:11', '2045:12:15 20:12:11' ],
-                       [ '1867-06-01T15:00:00', '1867:06:01 15:00:00' ],
-                       /* some invalid ones */
-                       [ '2001--12', null ],
-                       [ '2001-5-12', null ],
-                       [ '2001-5-12TZ', null ],
-                       [ '2001-05-12T15', null ],
-                       [ '2001-12T15:13', null ],
-               ];
-       }
-}
index e55efee..4b7ebd3 100644 (file)
@@ -92,6 +92,9 @@ class WikiPageTest extends MediaWikiLangTestCase {
 
        /**
         * @covers WikiPage::doEditContent
+        * @covers WikiPage::doModify
+        * @covers WikiPage::doCreate
+        * @covers WikiPage::doEditUpdates
         */
        public function testDoEditContent() {
                $page = $this->newPage( "WikiPageTest_testDoEditContent" );
@@ -213,30 +216,6 @@ class WikiPageTest extends MediaWikiLangTestCase {
                $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
        }
 
-       /**
-        * @covers WikiPage::doQuickEditContent
-        */
-       public function testDoQuickEditContent() {
-               global $wgUser;
-
-               $page = $this->createPage(
-                       "WikiPageTest_testDoQuickEditContent",
-                       "original text",
-                       CONTENT_MODEL_WIKITEXT
-               );
-
-               $content = ContentHandler::makeContent(
-                       "quick text",
-                       $page->getTitle(),
-                       CONTENT_MODEL_WIKITEXT
-               );
-               $page->doQuickEditContent( $content, $wgUser, "testing q" );
-
-               # ---------------------
-               $page = new WikiPage( $page->getTitle() );
-               $this->assertTrue( $content->equals( $page->getContent() ) );
-       }
-
        /**
         * @covers WikiPage::doDeleteArticle
         */
index f1dc9e9..0cc8ebf 100644 (file)
@@ -12,7 +12,7 @@ class TestUtils {
        /**
         * Override the singleton for unit testing
         * @param SessionManager|null $manager
-        * @return \\ScopedCallback|null
+        * @return \\Wikimedia\ScopedCallback|null
         */
        public static function setSessionManagerSingleton( SessionManager $manager = null ) {
                session_write_close();
index 560b6d2..ce6894e 100644 (file)
@@ -129,7 +129,7 @@ class BatchRowUpdateTest extends MediaWikiTestCase {
                $db = $this->mockDb();
                $db->expects( $this->once() )
                        ->method( 'select' )
-                       // only testing second parameter of DatabaseBase::select
+                       // only testing second parameter of Database::select
                        ->with( 'some_table', $columns )
                        ->will( $this->returnValue( new ArrayIterator( [] ) ) );
 
@@ -164,7 +164,7 @@ class BatchRowUpdateTest extends MediaWikiTestCase {
 
        /**
         * Slightly hackish to use reflection, but asserting different parameters
-        * to consecutive calls of DatabaseBase::select in phpunit is error prone
+        * to consecutive calls of Database::select in phpunit is error prone
         *
         * @dataProvider provider_readerSelectConditions
         */
@@ -214,7 +214,7 @@ class BatchRowUpdateTest extends MediaWikiTestCase {
        protected function consecutivelyReturnFromSelect( array $results ) {
                $retvals = [];
                foreach ( $results as $rows ) {
-                       // The DatabaseBase::select method returns iterators, so we do too.
+                       // The Database::select method returns iterators, so we do too.
                        $retvals[] = $this->returnValue( new ArrayIterator( $rows ) );
                }
 
@@ -235,8 +235,7 @@ class BatchRowUpdateTest extends MediaWikiTestCase {
        }
 
        protected function mockDb() {
-               // Cant mock from DatabaseType or DatabaseBase, they dont
-               // have the full gamut of methods
+               // @TODO: mock from Database
                // FIXME: the constructor normally sets mAtomicLevels and mSrvCache
                $databaseMysql = $this->getMockBuilder( 'DatabaseMysql' )
                        ->disableOriginalConstructor()
diff --git a/tests/phpunit/includes/utils/IPTest.php b/tests/phpunit/includes/utils/IPTest.php
deleted file mode 100644 (file)
index 5e0626b..0000000
+++ /dev/null
@@ -1,670 +0,0 @@
-<?php
-/**
- * Tests for IP validity functions.
- *
- * Ported from /t/inc/IP.t by avar.
- *
- * @group IP
- * @todo Test methods in this call should be split into a method and a
- * dataprovider.
- */
-
-class IPTest extends PHPUnit_Framework_TestCase {
-       /**
-        * @covers IP::isIPAddress
-        * @dataProvider provideInvalidIPs
-        */
-       public function isNotIPAddress( $val, $desc ) {
-               $this->assertFalse( IP::isIPAddress( $val ), $desc );
-       }
-
-       /**
-        * Provide a list of things that aren't IP addresses
-        */
-       public function provideInvalidIPs() {
-               return [
-                       [ false, 'Boolean false is not an IP' ],
-                       [ true, 'Boolean true is not an IP' ],
-                       [ '', 'Empty string is not an IP' ],
-                       [ 'abc', 'Garbage IP string' ],
-                       [ ':', 'Single ":" is not an IP' ],
-                       [ '2001:0DB8::A:1::1', 'IPv6 with a double :: occurrence' ],
-                       [ '2001:0DB8::A:1::', 'IPv6 with a double :: occurrence, last at end' ],
-                       [ '::2001:0DB8::5:1', 'IPv6 with a double :: occurrence, firt at beginning' ],
-                       [ '124.24.52', 'IPv4 not enough quads' ],
-                       [ '24.324.52.13', 'IPv4 out of range' ],
-                       [ '.24.52.13', 'IPv4 starts with period' ],
-                       [ 'fc:100:300', 'IPv6 with only 3 words' ],
-               ];
-       }
-
-       /**
-        * @covers IP::isIPAddress
-        */
-       public function testisIPAddress() {
-               $this->assertTrue( IP::isIPAddress( '::' ), 'RFC 4291 IPv6 Unspecified Address' );
-               $this->assertTrue( IP::isIPAddress( '::1' ), 'RFC 4291 IPv6 Loopback Address' );
-               $this->assertTrue( IP::isIPAddress( '74.24.52.13/20', 'IPv4 range' ) );
-               $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24' ), 'IPv6 range' );
-               $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96' ), 'IPv6 range with "::"' );
-
-               $validIPs = [ 'fc:100::', 'fc:100:a:d:1:e:ac::', 'fc::100', '::fc:100:a:d:1:e:ac',
-                       '::fc', 'fc::100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0', '124.24.52.13', '1.24.52.13' ];
-               foreach ( $validIPs as $ip ) {
-                       $this->assertTrue( IP::isIPAddress( $ip ), "$ip is a valid IP address" );
-               }
-       }
-
-       /**
-        * @covers IP::isIPv6
-        */
-       public function testisIPv6() {
-               $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' );
-               $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
-               $this->assertFalse( IP::isIPv6( 'fc:300' ), 'IPv6 with only 2 words' );
-               $this->assertFalse( IP::isIPv6( 'fc:100:300' ), 'IPv6 with only 3 words' );
-
-               $this->assertTrue( IP::isIPv6( 'fc:100::' ) );
-               $this->assertTrue( IP::isIPv6( 'fc:100:a::' ) );
-               $this->assertTrue( IP::isIPv6( 'fc:100:a:d::' ) );
-               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1::' ) );
-               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e::' ) );
-               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac::' ) );
-
-               $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' );
-               $this->assertFalse(
-                       IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ),
-                       'IPv6 with 9 words ending with "::"'
-               );
-
-               $this->assertFalse( IP::isIPv6( ':::' ) );
-               $this->assertFalse( IP::isIPv6( '::0:' ), 'IPv6 ending in a lone ":"' );
-
-               $this->assertTrue( IP::isIPv6( '::' ), 'IPv6 zero address' );
-               $this->assertTrue( IP::isIPv6( '::0' ) );
-               $this->assertTrue( IP::isIPv6( '::fc' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100:a' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100:a:d' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e' ) );
-               $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e:ac' ) );
-
-               $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
-               $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
-
-               $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' );
-               $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending with lone ":"' );
-               $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' );
-
-               $this->assertTrue( IP::isIPv6( 'fc::100' ), 'IPv6 with "::" and 2 words' );
-               $this->assertTrue( IP::isIPv6( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
-               $this->assertTrue( IP::isIPv6( 'fc::100:a:d', 'IPv6 with "::" and 4 words' ) );
-               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
-               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ), 'IPv6 with "::" and 6 words' );
-               $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
-               $this->assertTrue( IP::isIPv6( '2001::df' ), 'IPv6 with "::" and 2 words' );
-               $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' );
-               $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' );
-
-               $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
-               $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
-
-               $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) );
-       }
-
-       /**
-        * @covers IP::isIPv4
-        * @dataProvider provideInvalidIPv4Addresses
-        */
-       public function testisNotIPv4( $bogusIP, $desc ) {
-               $this->assertFalse( IP::isIPv4( $bogusIP ), $desc );
-       }
-
-       public function provideInvalidIPv4Addresses() {
-               return [
-                       [ false, 'Boolean false is not an IP' ],
-                       [ true, 'Boolean true is not an IP' ],
-                       [ '', 'Empty string is not an IP' ],
-                       [ 'abc', 'Letters are not an IP' ],
-                       [ ':', 'A colon is not an IP' ],
-                       [ '124.24.52', 'IPv4 not enough quads' ],
-                       [ '24.324.52.13', 'IPv4 out of range' ],
-                       [ '.24.52.13', 'IPv4 starts with period' ],
-               ];
-       }
-
-       /**
-        * @covers IP::isIPv4
-        * @dataProvider provideValidIPv4Address
-        */
-       public function testIsIPv4( $ip, $desc ) {
-               $this->assertTrue( IP::isIPv4( $ip ), $desc );
-       }
-
-       /**
-        * Provide some IPv4 addresses and ranges
-        */
-       public function provideValidIPv4Address() {
-               return [
-                       [ '124.24.52.13', 'Valid IPv4 address' ],
-                       [ '1.24.52.13', 'Another valid IPv4 address' ],
-                       [ '74.24.52.13/20', 'An IPv4 range' ],
-               ];
-       }
-
-       /**
-        * @covers IP::isValid
-        */
-       public function testValidIPs() {
-               foreach ( range( 0, 255 ) as $i ) {
-                       $a = sprintf( "%03d", $i );
-                       $b = sprintf( "%02d", $i );
-                       $c = sprintf( "%01d", $i );
-                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
-                               $ip = "$f.$f.$f.$f";
-                               $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv4 address" );
-                       }
-               }
-               foreach ( range( 0x0, 0xFFFF, 0xF ) as $i ) {
-                       $a = sprintf( "%04x", $i );
-                       $b = sprintf( "%03x", $i );
-                       $c = sprintf( "%02x", $i );
-                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
-                               $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
-                               $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv6 address" );
-                       }
-               }
-               // test with some abbreviations
-               $this->assertFalse( IP::isValid( ':fc:100::' ), 'IPv6 starting with lone ":"' );
-               $this->assertFalse( IP::isValid( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
-               $this->assertFalse( IP::isValid( 'fc:300' ), 'IPv6 with only 2 words' );
-               $this->assertFalse( IP::isValid( 'fc:100:300' ), 'IPv6 with only 3 words' );
-
-               $this->assertTrue( IP::isValid( 'fc:100::' ) );
-               $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e::' ) );
-               $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e:ac::' ) );
-
-               $this->assertTrue( IP::isValid( 'fc::100' ), 'IPv6 with "::" and 2 words' );
-               $this->assertTrue( IP::isValid( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
-               $this->assertTrue( IP::isValid( '2001::df' ), 'IPv6 with "::" and 2 words' );
-               $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' );
-               $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' );
-               $this->assertTrue( IP::isValid( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
-               $this->assertTrue( IP::isValid( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
-
-               $this->assertFalse(
-                       IP::isValid( 'fc:100:a:d:1:e:ac:0::' ),
-                       'IPv6 with 8 words ending with "::"'
-               );
-               $this->assertFalse(
-                       IP::isValid( 'fc:100:a:d:1:e:ac:0:1::' ),
-                       'IPv6 with 9 words ending with "::"'
-               );
-       }
-
-       /**
-        * @covers IP::isValid
-        */
-       public function testInvalidIPs() {
-               // Out of range...
-               foreach ( range( 256, 999 ) as $i ) {
-                       $a = sprintf( "%03d", $i );
-                       $b = sprintf( "%02d", $i );
-                       $c = sprintf( "%01d", $i );
-                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
-                               $ip = "$f.$f.$f.$f";
-                               $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" );
-                       }
-               }
-               foreach ( range( 'g', 'z' ) as $i ) {
-                       $a = sprintf( "%04s", $i );
-                       $b = sprintf( "%03s", $i );
-                       $c = sprintf( "%02s", $i );
-                       foreach ( array_unique( [ $a, $b, $c ] ) as $f ) {
-                               $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
-                               $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv6 address" );
-                       }
-               }
-               // Have CIDR
-               $ipCIDRs = [
-                       '212.35.31.121/32',
-                       '212.35.31.121/18',
-                       '212.35.31.121/24',
-                       '::ff:d:321:5/96',
-                       'ff::d3:321:5/116',
-                       'c:ff:12:1:ea:d:321:5/120',
-               ];
-               foreach ( $ipCIDRs as $i ) {
-                       $this->assertFalse( IP::isValid( $i ),
-                               "$i is an invalid IP address because it is a block" );
-               }
-               // Incomplete/garbage
-               $invalid = [
-                       'www.xn--var-xla.net',
-                       '216.17.184.G',
-                       '216.17.184.1.',
-                       '216.17.184',
-                       '216.17.184.',
-                       '256.17.184.1'
-               ];
-               foreach ( $invalid as $i ) {
-                       $this->assertFalse( IP::isValid( $i ), "$i is an invalid IP address" );
-               }
-       }
-
-       /**
-        * Provide some valid IP blocks
-        */
-       public function provideValidBlocks() {
-               return [
-                       [ '116.17.184.5/32' ],
-                       [ '0.17.184.5/30' ],
-                       [ '16.17.184.1/24' ],
-                       [ '30.242.52.14/1' ],
-                       [ '10.232.52.13/8' ],
-                       [ '30.242.52.14/0' ],
-                       [ '::e:f:2001/96' ],
-                       [ '::c:f:2001/128' ],
-                       [ '::10:f:2001/70' ],
-                       [ '::fe:f:2001/1' ],
-                       [ '::6d:f:2001/8' ],
-                       [ '::fe:f:2001/0' ],
-               ];
-       }
-
-       /**
-        * @covers IP::isValidBlock
-        * @dataProvider provideValidBlocks
-        */
-       public function testValidBlocks( $block ) {
-               $this->assertTrue( IP::isValidBlock( $block ), "$block is a valid IP block" );
-       }
-
-       /**
-        * @covers IP::isValidBlock
-        * @dataProvider provideInvalidBlocks
-        */
-       public function testInvalidBlocks( $invalid ) {
-               $this->assertFalse( IP::isValidBlock( $invalid ), "$invalid is not a valid IP block" );
-       }
-
-       public function provideInvalidBlocks() {
-               return [
-                       [ '116.17.184.5/33' ],
-                       [ '0.17.184.5/130' ],
-                       [ '16.17.184.1/-1' ],
-                       [ '10.232.52.13/*' ],
-                       [ '7.232.52.13/ab' ],
-                       [ '11.232.52.13/' ],
-                       [ '::e:f:2001/129' ],
-                       [ '::c:f:2001/228' ],
-                       [ '::10:f:2001/-1' ],
-                       [ '::6d:f:2001/*' ],
-                       [ '::86:f:2001/ab' ],
-                       [ '::23:f:2001/' ],
-               ];
-       }
-
-       /**
-        * @covers IP::sanitizeIP
-        * @dataProvider provideSanitizeIP
-        */
-       public function testSanitizeIP( $expected, $input ) {
-               $result = IP::sanitizeIP( $input );
-               $this->assertEquals( $expected, $result );
-       }
-
-       /**
-        * Provider for IP::testSanitizeIP()
-        */
-       public static function provideSanitizeIP() {
-               return [
-                       [ '0.0.0.0', '0.0.0.0' ],
-                       [ '0.0.0.0', '00.00.00.00' ],
-                       [ '0.0.0.0', '000.000.000.000' ],
-                       [ '141.0.11.253', '141.000.011.253' ],
-                       [ '1.2.4.5', '1.2.4.5' ],
-                       [ '1.2.4.5', '01.02.04.05' ],
-                       [ '1.2.4.5', '001.002.004.005' ],
-                       [ '10.0.0.1', '010.0.000.1' ],
-                       [ '80.72.250.4', '080.072.250.04' ],
-                       [ 'Foo.1000.00', 'Foo.1000.00' ],
-                       [ 'Bar.01', 'Bar.01' ],
-                       [ 'Bar.010', 'Bar.010' ],
-                       [ null, '' ],
-                       [ null, ' ' ]
-               ];
-       }
-
-       /**
-        * @covers IP::toHex
-        * @dataProvider provideToHex
-        */
-       public function testToHex( $expected, $input ) {
-               $result = IP::toHex( $input );
-               $this->assertTrue( $result === false || is_string( $result ) );
-               $this->assertEquals( $expected, $result );
-       }
-
-       /**
-        * Provider for IP::testToHex()
-        */
-       public static function provideToHex() {
-               return [
-                       [ '00000001', '0.0.0.1' ],
-                       [ '01020304', '1.2.3.4' ],
-                       [ '7F000001', '127.0.0.1' ],
-                       [ '80000000', '128.0.0.0' ],
-                       [ 'DEADCAFE', '222.173.202.254' ],
-                       [ 'FFFFFFFF', '255.255.255.255' ],
-                       [ '8D000BFD', '141.000.11.253' ],
-                       [ false, 'IN.VA.LI.D' ],
-                       [ 'v6-00000000000000000000000000000001', '::1' ],
-                       [ 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ],
-                       [ 'v6-20010DB885A3000000008A2E03707334', '2001:db8:85a3::8a2e:0370:7334' ],
-                       [ false, 'IN:VA::LI:D' ],
-                       [ false, ':::1' ]
-               ];
-       }
-
-       /**
-        * @covers IP::isPublic
-        * @dataProvider provideIsPublic
-        */
-       public function testIsPublic( $expected, $input ) {
-               $result = IP::isPublic( $input );
-               $this->assertEquals( $expected, $result );
-       }
-
-       /**
-        * Provider for IP::testIsPublic()
-        */
-       public static function provideIsPublic() {
-               return [
-                       [ false, 'fc00::3' ], # RFC 4193 (local)
-                       [ false, 'fc00::ff' ], # RFC 4193 (local)
-                       [ false, '127.1.2.3' ], # loopback
-                       [ false, '::1' ], # loopback
-                       [ false, 'fe80::1' ], # link-local
-                       [ false, '169.254.1.1' ], # link-local
-                       [ false, '10.0.0.1' ], # RFC 1918 (private)
-                       [ false, '172.16.0.1' ], # RFC 1918 (private)
-                       [ false, '192.168.0.1' ], # RFC 1918 (private)
-                       [ true, '2001:5c0:1000:a::133' ], # public
-                       [ true, 'fc::3' ], # public
-                       [ true, '00FC::' ] # public
-               ];
-       }
-
-       // Private wrapper used to test CIDR Parsing.
-       private function assertFalseCIDR( $CIDR, $msg = '' ) {
-               $ff = [ false, false ];
-               $this->assertEquals( $ff, IP::parseCIDR( $CIDR ), $msg );
-       }
-
-       // Private wrapper to test network shifting using only dot notation
-       private function assertNet( $expected, $CIDR ) {
-               $parse = IP::parseCIDR( $CIDR );
-               $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" );
-       }
-
-       /**
-        * @covers IP::hexToQuad
-        * @dataProvider provideIPsAndHexes
-        */
-       public function testHexToQuad( $ip, $hex ) {
-               $this->assertEquals( $ip, IP::hexToQuad( $hex ) );
-       }
-
-       /**
-        * Provide some IP addresses and their equivalent hex representations
-        */
-       public function provideIPsandHexes() {
-               return [
-                       [ '0.0.0.1', '00000001' ],
-                       [ '255.0.0.0', 'FF000000' ],
-                       [ '255.255.255.255', 'FFFFFFFF' ],
-                       [ '10.188.222.255', '0ABCDEFF' ],
-                       // hex not left-padded...
-                       [ '0.0.0.0', '0' ],
-                       [ '0.0.0.1', '1' ],
-                       [ '0.0.0.255', 'FF' ],
-                       [ '0.0.255.0', 'FF00' ],
-               ];
-       }
-
-       /**
-        * @covers IP::hexToOctet
-        * @dataProvider provideOctetsAndHexes
-        */
-       public function testHexToOctet( $octet, $hex ) {
-               $this->assertEquals( $octet, IP::hexToOctet( $hex ) );
-       }
-
-       /**
-        * Provide some hex and octet representations of the same IPs
-        */
-       public function provideOctetsAndHexes() {
-               return [
-                       [ '0:0:0:0:0:0:0:1', '00000000000000000000000000000001' ],
-                       [ '0:0:0:0:0:0:FF:3', '00000000000000000000000000FF0003' ],
-                       [ '0:0:0:0:0:0:FF00:6', '000000000000000000000000FF000006' ],
-                       [ '0:0:0:0:0:0:FCCF:FAFF', '000000000000000000000000FCCFFAFF' ],
-                       [ 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ],
-                       // hex not left-padded...
-                       [ '0:0:0:0:0:0:0:0', '0' ],
-                       [ '0:0:0:0:0:0:0:1', '1' ],
-                       [ '0:0:0:0:0:0:0:FF', 'FF' ],
-                       [ '0:0:0:0:0:0:0:FFD0', 'FFD0' ],
-                       [ '0:0:0:0:0:0:FA00:0', 'FA000000' ],
-                       [ '0:0:0:0:0:0:FCCF:FAFF', 'FCCFFAFF' ],
-               ];
-       }
-
-       /**
-        * IP::parseCIDR() returns an array containing a signed IP address
-        * representing the network mask and the bit mask.
-        * @covers IP::parseCIDR
-        */
-       public function testCIDRParsing() {
-               $this->assertFalseCIDR( '192.0.2.0', "missing mask" );
-               $this->assertFalseCIDR( '192.0.2.0/', "missing bitmask" );
-
-               // Verify if statement
-               $this->assertFalseCIDR( '256.0.0.0/32', "invalid net" );
-               $this->assertFalseCIDR( '192.0.2.0/AA', "mask not numeric" );
-               $this->assertFalseCIDR( '192.0.2.0/-1', "mask < 0" );
-               $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" );
-
-               // Check internal logic
-               # 0 mask always result in array(0,0)
-               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '192.0.0.2/0' ) );
-               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '0.0.0.0/0' ) );
-               $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '255.255.255.255/0' ) );
-
-               // @todo FIXME: Add more tests.
-
-               # This part test network shifting
-               $this->assertNet( '192.0.0.0', '192.0.0.2/24' );
-               $this->assertNet( '192.168.5.0', '192.168.5.13/24' );
-               $this->assertNet( '10.0.0.160', '10.0.0.161/28' );
-               $this->assertNet( '10.0.0.0', '10.0.0.3/28' );
-               $this->assertNet( '10.0.0.0', '10.0.0.3/30' );
-               $this->assertNet( '10.0.0.4', '10.0.0.4/30' );
-               $this->assertNet( '172.17.32.0', '172.17.35.48/21' );
-               $this->assertNet( '10.128.0.0', '10.135.0.0/9' );
-               $this->assertNet( '134.0.0.0', '134.0.5.1/8' );
-       }
-
-       /**
-        * @covers IP::canonicalize
-        */
-       public function testIPCanonicalizeOnValidIp() {
-               $this->assertEquals( '192.0.2.152', IP::canonicalize( '192.0.2.152' ),
-                       'Canonicalization of a valid IP returns it unchanged' );
-       }
-
-       /**
-        * @covers IP::canonicalize
-        */
-       public function testIPCanonicalizeMappedAddress() {
-               $this->assertEquals(
-                       '192.0.2.152',
-                       IP::canonicalize( '::ffff:192.0.2.152' )
-               );
-               $this->assertEquals(
-                       '192.0.2.152',
-                       IP::canonicalize( '::192.0.2.152' )
-               );
-       }
-
-       /**
-        * Issues there are most probably from IP::toHex() or IP::parseRange()
-        * @covers IP::isInRange
-        * @dataProvider provideIPsAndRanges
-        */
-       public function testIPIsInRange( $expected, $addr, $range, $message = '' ) {
-               $this->assertEquals(
-                       $expected,
-                       IP::isInRange( $addr, $range ),
-                       $message
-               );
-       }
-
-       /** Provider for testIPIsInRange() */
-       public static function provideIPsAndRanges() {
-               # Format: (expected boolean, address, range, optional message)
-               return [
-                       # IPv4
-                       [ true, '192.0.2.0', '192.0.2.0/24', 'Network address' ],
-                       [ true, '192.0.2.77', '192.0.2.0/24', 'Simple address' ],
-                       [ true, '192.0.2.255', '192.0.2.0/24', 'Broadcast address' ],
-
-                       [ false, '0.0.0.0', '192.0.2.0/24' ],
-                       [ false, '255.255.255', '192.0.2.0/24' ],
-
-                       # IPv6
-                       [ false, '::1', '2001:DB8::/32' ],
-                       [ false, '::', '2001:DB8::/32' ],
-                       [ false, 'FE80::1', '2001:DB8::/32' ],
-
-                       [ true, '2001:DB8::', '2001:DB8::/32' ],
-                       [ true, '2001:0DB8::', '2001:DB8::/32' ],
-                       [ true, '2001:DB8::1', '2001:DB8::/32' ],
-                       [ true, '2001:0DB8::1', '2001:DB8::/32' ],
-                       [ true, '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF',
-                               '2001:DB8::/32' ],
-
-                       [ false, '2001:0DB8:F::', '2001:DB8::/96' ],
-               ];
-       }
-
-       /**
-        * Test for IP::splitHostAndPort().
-        * @dataProvider provideSplitHostAndPort
-        */
-       public function testSplitHostAndPort( $expected, $input, $description ) {
-               $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description );
-       }
-
-       /**
-        * Provider for IP::splitHostAndPort()
-        */
-       public static function provideSplitHostAndPort() {
-               return [
-                       [ false, '[', 'Unclosed square bracket' ],
-                       [ false, '[::', 'Unclosed square bracket 2' ],
-                       [ [ '::', false ], '::', 'Bare IPv6 0' ],
-                       [ [ '::1', false ], '::1', 'Bare IPv6 1' ],
-                       [ [ '::', false ], '[::]', 'Bracketed IPv6 0' ],
-                       [ [ '::1', false ], '[::1]', 'Bracketed IPv6 1' ],
-                       [ [ '::1', 80 ], '[::1]:80', 'Bracketed IPv6 with port' ],
-                       [ false, '::x', 'Double colon but no IPv6' ],
-                       [ [ 'x', 80 ], 'x:80', 'Hostname and port' ],
-                       [ false, 'x:x', 'Hostname and invalid port' ],
-                       [ [ 'x', false ], 'x', 'Plain hostname' ]
-               ];
-       }
-
-       /**
-        * Test for IP::combineHostAndPort()
-        * @dataProvider provideCombineHostAndPort
-        */
-       public function testCombineHostAndPort( $expected, $input, $description ) {
-               list( $host, $port, $defaultPort ) = $input;
-               $this->assertEquals(
-                       $expected,
-                       IP::combineHostAndPort( $host, $port, $defaultPort ),
-                       $description );
-       }
-
-       /**
-        * Provider for IP::combineHostAndPort()
-        */
-       public static function provideCombineHostAndPort() {
-               return [
-                       [ '[::1]', [ '::1', 2, 2 ], 'IPv6 default port' ],
-                       [ '[::1]:2', [ '::1', 2, 3 ], 'IPv6 non-default port' ],
-                       [ 'x', [ 'x', 2, 2 ], 'Normal default port' ],
-                       [ 'x:2', [ 'x', 2, 3 ], 'Normal non-default port' ],
-               ];
-       }
-
-       /**
-        * Test for IP::sanitizeRange()
-        * @dataProvider provideIPCIDRs
-        */
-       public function testSanitizeRange( $input, $expected, $description ) {
-               $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description );
-       }
-
-       /**
-        * Provider for IP::testSanitizeRange()
-        */
-       public static function provideIPCIDRs() {
-               return [
-                       [ '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ],
-                       [ '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ],
-                       [ '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ],
-                       [ '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ],
-                       [ '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ],
-                       [ '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ],
-                       [ '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ],
-                       [ '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ],
-               ];
-       }
-
-       /**
-        * Test for IP::prettifyIP()
-        * @dataProvider provideIPsToPrettify
-        */
-       public function testPrettifyIP( $ip, $prettified ) {
-               $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" );
-       }
-
-       /**
-        * Provider for IP::testPrettifyIP()
-        */
-       public static function provideIPsToPrettify() {
-               return [
-                       [ '0:0:0:0:0:0:0:0', '::' ],
-                       [ '0:0:0::0:0:0', '::' ],
-                       [ '0:0:0:1:0:0:0:0', '0:0:0:1::' ],
-                       [ '0:0::f', '::f' ],
-                       [ '0::0:0:0:33:fef:b', '::33:fef:b' ],
-                       [ '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ],
-                       [ '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ],
-                       [ 'abbc:2004::0:0:0:0', 'abbc:2004::' ],
-                       [ 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ],
-                       [ '0:0:0:0:0:0:0:0/16', '::/16' ],
-                       [ '0:0:0::0:0:0/64', '::/64' ],
-                       [ '0:0::f/52', '::f/52' ],
-                       [ '::0:0:33:fef:b/52', '::33:fef:b/52' ],
-                       [ '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ],
-                       [ '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ],
-                       [ 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ],
-                       [ 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ],
-               ];
-       }
-}
index fafd4fa..760d41e 100644 (file)
@@ -37,7 +37,7 @@ class MWCryptHKDFTest extends MediaWikiTestCase {
        }
 
        /**
-        * Test vectors from Appendix A on http://tools.ietf.org/html/rfc5869
+        * Test vectors from Appendix A on https://tools.ietf.org/html/rfc5869
         */
        public static function providerRfc5869() {
 
index bdeed58..6797f59 100644 (file)
@@ -50,15 +50,11 @@ class MockFSFile extends FSFile {
                return wfTimestamp( TS_MW );
        }
 
-       public function getMimeType() {
-               return 'text/mock';
-       }
-
        public function getProps( $ext = true ) {
                return [
                        'fileExists' => $this->exists(),
                        'size' => $this->getSize(),
-                       'file-mime' => $this->getMimeType(),
+                       'file-mime' => 'text/mock',
                        'sha1' => $this->getSha1Base36(),
                ];
        }
diff --git a/tests/phpunit/mocks/filerepo/MockLocalRepo.php b/tests/phpunit/mocks/filerepo/MockLocalRepo.php
new file mode 100644 (file)
index 0000000..eeaf05a
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * Class simulating a local file repo.
+ *
+ * @ingroup FileRepo
+ * @since 1.28
+ */
+class MockLocalRepo extends LocalRepo {
+       function getLocalCopy( $virtualUrl ) {
+               return new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) );
+       }
+
+       function getLocalReference( $virtualUrl ) {
+               return new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) );
+       }
+
+       function getFileProps( $virtualUrl ) {
+               $fsFile = $this->getLocalReference( $virtualUrl );
+
+               return $fsFile->getProps();
+       }
+}
index 018d978..0e0b943 100644 (file)
  */
 
 class MockDjVuHandler extends DjVuHandler {
+       function isEnabled() {
+               return true;
+       }
+
        function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
                if ( !$this->normaliseParams( $image, $params ) ) {
                        return new TransformParameterError( $params );
index 711eab6..ad61284 100644 (file)
@@ -16,6 +16,9 @@
  * http://www.gnu.org/copyleft/gpl.html
  */
 
+use Composer\Spdx\SpdxLicenses;
+use JsonSchema\Validator;
+
 /**
  * Validates all loaded extensions and skins using the ExtensionRegistry
  * against the extension.json schema in the docs/ folder.
@@ -24,7 +27,7 @@ class ExtensionJsonValidationTest extends PHPUnit_Framework_TestCase {
 
        public function setUp() {
                parent::setUp();
-               if ( !class_exists( 'JsonSchema\Uri\UriRetriever' ) ) {
+               if ( !class_exists( Validator::class ) ) {
                        $this->markTestSkipped(
                                'The JsonSchema library cannot be found,' .
                                ' please install it through composer to run extension.json validation tests.'
@@ -75,9 +78,22 @@ class ExtensionJsonValidationTest extends PHPUnit_Framework_TestCase {
                        "$path is using a non-supported schema version"
                );
 
-               $validator = new JsonSchema\Validator;
+               $licenseError = false;
+               if ( class_exists( SpdxLicenses::class ) && isset( $data->{'license-name'} )
+                       // Check if it's a string, if not, schema validation will display an error
+                       && is_string( $data->{'license-name'} )
+               ) {
+                       $licenses = new SpdxLicenses();
+                       $valid = $licenses->validate( $data->{'license-name'} );
+                       if ( !$valid ) {
+                               $licenseError = '[license-name] Invalid SPDX license identifier, '
+                                       . 'see <https://spdx.org/licenses/>';
+                       }
+               }
+
+               $validator = new Validator;
                $validator->check( $data, (object) [ '$ref' => 'file://' . $schemaPath ] );
-               if ( $validator->isValid() ) {
+               if ( $validator->isValid() && !$licenseError ) {
                        // All good.
                        $this->assertTrue( true );
                } else {
@@ -85,6 +101,9 @@ class ExtensionJsonValidationTest extends PHPUnit_Framework_TestCase {
                        foreach ( $validator->getErrors() as $error ) {
                                $out .= "[{$error['property']}] {$error['message']}\n";
                        }
+                       if ( $licenseError ) {
+                               $out .= "$licenseError\n";
+                       }
                        $this->assertTrue( false, $out );
                }
        }
index 8bc087b..815a3b4 100644 (file)
@@ -1,2 +1,6 @@
-var x = require( 'test.require.define' );
-module.exports = 'Require worked.' + x;
+module.exports = {
+       immediate: require( 'test.require.define' ),
+       later: function () {
+               return require( 'test.require.define' );
+       }
+};
index 41d800a..b69c9e8 100644 (file)
                } );
        } );
 
-       QUnit.test( 'require() in debug mode', 1, function ( assert ) {
+       QUnit.test( 'require() in debug mode', function ( assert ) {
                var path = mw.config.get( 'wgScriptPath' );
                mw.loader.register( [
                        [ 'test.require.define', '0' ],
                mw.loader.implement( 'test.require.define', [ QUnit.fixurl( path + '/tests/qunit/data/defineCallMwLoaderTestCallback.js' ) ] );
 
                return mw.loader.using( 'test.require.callback' ).then( function ( require ) {
-                       var exported = require( 'test.require.callback' );
-                       assert.strictEqual( exported, 'Require worked.Define worked.',
-                               'module.exports worked in debug mode' );
+                       var cb = require( 'test.require.callback' );
+                       assert.strictEqual( cb.immediate, 'Defined.', 'module.exports and require work in debug mode' );
+                       // Must use try-catch because cb.later() will throw if require is undefined,
+                       // which doesn't work well inside Deferred.then() when using jQuery 1.x with QUnit
+                       try {
+                               assert.strictEqual( cb.later(), 'Defined.', 'require works asynchrously in debug mode' );
+                       } catch ( e ) {
+                               assert.equal( null, String( e ), 'require works asynchrously in debug mode' );
+                       }
                }, function () {
                        assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' );
                } );
index 7a09964..df02693 100644 (file)
@@ -95,8 +95,9 @@
        if ( window.requestIdleCallback ) {
                QUnit.test( 'native', function ( assert ) {
                        var done = assert.async();
-                       // Remove polyfill
+                       // Remove polyfill and clock stub
                        mw.requestIdleCallback.restore();
+                       this.clock.restore();
                        mw.requestIdleCallback( function () {
                                assert.expect( 0 );
                                done();