Merge "refreshLinks: Use vslow group for DB_REPLICA connections"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 22 Sep 2016 03:01:51 +0000 (03:01 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 22 Sep 2016 03:01:51 +0000 (03:01 +0000)
319 files changed:
.jscsrc
RELEASE-NOTES-1.28
autoload.php
docs/extension.schema.json
docs/extension.schema.v1.json
includes/DefaultSettings.php
includes/Defines.php
includes/FileDeleteForm.php
includes/GlobalFunctions.php
includes/Html.php
includes/MWTimestamp.php
includes/MediaWiki.php
includes/MimeMagic.php
includes/OutputPage.php
includes/PrefixSearch.php
includes/ServiceWiring.php
includes/Setup.php
includes/Status.php
includes/Title.php
includes/WebResponse.php
includes/actions/HistoryAction.php
includes/api/ApiBase.php
includes/api/ApiFormatBase.php
includes/api/ApiFormatJson.php
includes/api/ApiImageRotate.php
includes/api/ApiMain.php
includes/api/ApiQuery.php
includes/api/ApiQueryBacklinksprop.php
includes/api/ApiResult.php
includes/api/SearchApi.php
includes/api/i18n/diq.json
includes/api/i18n/es.json
includes/auth/AuthenticationResponse.php
includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php
includes/auth/EmailNotificationSecondaryAuthenticationProvider.php
includes/auth/ResetPasswordSecondaryAuthenticationProvider.php
includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php
includes/cache/FileCacheBase.php
includes/cache/HTMLFileCache.php
includes/changes/RecentChange.php
includes/changetags/ChangeTags.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/loadbalancer/LBFactoryMW.php
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/AtomicSectionUpdate.php
includes/deferred/AutoCommitUpdate.php
includes/deferred/LinksDeletionUpdate.php
includes/deferred/LinksUpdate.php
includes/deferred/MWCallableUpdate.php
includes/exception/MWExceptionRenderer.php
includes/exception/TimestampException.php [deleted file]
includes/externalstore/ExternalStoreDB.php
includes/filebackend/FSFile.php
includes/filebackend/FSFileBackend.php
includes/filebackend/FileBackend.php [deleted file]
includes/filebackend/FileBackendGroup.php
includes/filebackend/FileBackendMultiWrite.php
includes/filebackend/FileBackendStore.php
includes/filebackend/FileOp.php
includes/filebackend/FileOpBatch.php
includes/filebackend/MemoryFileBackend.php
includes/filebackend/SwiftFileBackend.php
includes/filebackend/TempFSFile.php
includes/filebackend/filejournal/DBFileJournal.php
includes/filebackend/filejournal/FileJournal.php [deleted file]
includes/filebackend/lockmanager/DBLockManager.php
includes/filebackend/lockmanager/FSLockManager.php [deleted file]
includes/filebackend/lockmanager/LockManager.php [deleted file]
includes/filebackend/lockmanager/MemcLockManager.php
includes/filebackend/lockmanager/MySqlLockManager.php
includes/filebackend/lockmanager/PostgreSqlLockManager.php
includes/filebackend/lockmanager/QuorumLockManager.php [deleted file]
includes/filebackend/lockmanager/RedisLockManager.php
includes/filebackend/lockmanager/ScopedLock.php [deleted file]
includes/filerepo/FSRepo.php
includes/filerepo/FileBackendDBRepoWrapper.php
includes/filerepo/FileRepo.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/LocalFile.php
includes/gallery/TraditionalImageGallery.php
includes/htmlform/HTMLForm.php
includes/htmlform/OOUIHTMLForm.php
includes/import/WikiRevision.php
includes/installer/DatabaseInstaller.php
includes/installer/SqliteInstaller.php
includes/installer/i18n/bg.json
includes/installer/i18n/it.json
includes/jobqueue/JobQueueDB.php
includes/jobqueue/JobQueueGroup.php
includes/jobqueue/jobs/RecentChangesUpdateJob.php
includes/jobqueue/utils/PurgeJobUtils.php
includes/libs/MultiHttpClient.php
includes/libs/StatusValue.php
includes/libs/filebackend/FileBackend.php [new file with mode: 0644]
includes/libs/filebackend/FileBackendException.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/lockmanager/FSLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/LockManager.php [new file with mode: 0644]
includes/libs/lockmanager/NullLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/QuorumLockManager.php [new file with mode: 0644]
includes/libs/lockmanager/ScopedLock.php [new file with mode: 0644]
includes/libs/objectcache/HashBagOStuff.php
includes/libs/rdbms/chronologyprotector/ChronologyProtector.php
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php [new file with mode: 0644]
includes/libs/rdbms/database/DatabaseBase.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/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 [new file with mode: 0644]
includes/libs/rdbms/field/PostgresField.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 [new file with mode: 0644]
includes/libs/rdbms/loadmonitor/LoadMonitor.php
includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php
includes/libs/rdbms/loadmonitor/LoadMonitorNull.php
includes/libs/time/ConvertableTimestamp.php [new file with mode: 0644]
includes/libs/time/TimestampException.php [new file with mode: 0644]
includes/libs/time/defines.php [new file with mode: 0644]
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/media/BMP.php
includes/media/DjVu.php
includes/media/ExifBitmap.php
includes/media/MediaHandler.php
includes/media/PNG.php
includes/media/SVG.php
includes/media/Tiff.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/page/Article.php
includes/page/WikiPage.php
includes/registration/ExtensionProcessor.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderContext.php
includes/resourceloader/ResourceLoaderModule.php
includes/revisiondelete/RevDelList.php
includes/search/SearchEngine.php
includes/search/SearchEngineFactory.php
includes/skins/BaseTemplate.php
includes/skins/SkinTemplate.php
includes/specialpage/LoginSignupSpecialPage.php
includes/specials/SpecialContributions.php
includes/upload/UploadBase.php
includes/upload/UploadFromChunks.php
includes/upload/UploadFromUrl.php
includes/upload/UploadStash.php
includes/user/User.php
includes/user/UserRightsProxy.php
includes/utils/MWFileProps.php [new file with mode: 0644]
index.php
languages/Language.php
languages/i18n/an.json
languages/i18n/ar.json
languages/i18n/ast.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/cs.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/dty.json
languages/i18n/egl.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/gsw.json
languages/i18n/gu.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/ia.json
languages/i18n/ilo.json
languages/i18n/it.json
languages/i18n/jv.json
languages/i18n/ka.json
languages/i18n/kiu.json
languages/i18n/kk-cyrl.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/lij.json
languages/i18n/lt.json
languages/i18n/lv.json
languages/i18n/mai.json
languages/i18n/mk.json
languages/i18n/my.json
languages/i18n/nap.json
languages/i18n/nb.json
languages/i18n/ne.json
languages/i18n/nl.json
languages/i18n/nn.json
languages/i18n/pl.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/sk.json
languages/i18n/sl.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/tr.json
languages/i18n/uk.json
languages/i18n/ur.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
maintenance/Maintenance.php
maintenance/doMaintenance.php
maintenance/importImages.php
maintenance/importTextFiles.php
maintenance/jsduck/custom_tags.rb
maintenance/jsduck/external.js
maintenance/rebuildFileCache.php
maintenance/rebuildrecentchanges.php
maintenance/sql.php
resources/Resources.php
resources/lib/oojs-ui/oojs-ui-core-apex.css
resources/lib/oojs-ui/oojs-ui-core-mediawiki.css
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.userlogin.login.css
resources/src/mediawiki.ui/components/forms.less
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/api/upload.js
resources/src/mediawiki/mediawiki.Upload.BookletLayout.css
resources/src/mediawiki/mediawiki.js
resources/src/mediawiki/mediawiki.requestIdleCallback.js
resources/src/mediawiki/mediawiki.user.js
tests/common/TestsAutoLoader.php
tests/parser/ParserTestRunner.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/HtmlTest.php
tests/phpunit/includes/MWTimestampTest.php
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/PrefixSearchTest.php
tests/phpunit/includes/StatusTest.php
tests/phpunit/includes/api/ApiOpenSearchTest.php
tests/phpunit/includes/api/ApiResultTest.php
tests/phpunit/includes/auth/AuthenticationResponseTest.php
tests/phpunit/includes/db/DatabaseMysqlBaseTest.php
tests/phpunit/includes/db/DatabaseSQLTest.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/installer/DatabaseUpdaterTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/time/ConvertableTimestampTest.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/resourceloader/ResourceLoaderTest.php
tests/phpunit/includes/search/SearchEngineTest.php
tests/phpunit/mocks/filebackend/MockFSFile.php
tests/phpunit/mocks/filerepo/MockLocalRepo.php [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.user.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 0b86bc2..fd6c613 100644 (file)
@@ -26,7 +26,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').
@@ -114,6 +114,36 @@ production.
   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::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)
 
 === Languages updated in 1.28 ===
 
@@ -171,6 +201,7 @@ changes to languages because of Phabricator reports.
   phpunit.php: --regex and --keep-uploads. Instead of --regex, use --filter.
   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.
 
 == Compatibility ==
 
index 9fd83eb..f5c334b 100644 (file)
@@ -281,6 +281,7 @@ $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',
        'Cookie' => __DIR__ . '/includes/libs/Cookie.php',
        'CookieJar' => __DIR__ . '/includes/libs/CookieJar.php',
@@ -315,18 +316,19 @@ $wgAutoloadLocalClasses = [
        'DBTransactionSizeError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php',
        'DBUnexpectedError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.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/DatabaseBase.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',
@@ -436,7 +438,7 @@ $wgAutoloadLocalClasses = [
        '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',
+       'FSLockManager' => __DIR__ . '/includes/libs/lockmanager/FSLockManager.php',
        'FSRepo' => __DIR__ . '/includes/filerepo/FSRepo.php',
        'FakeAuthTemplate' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php',
        'FakeConverter' => __DIR__ . '/languages/FakeConverter.php',
@@ -452,10 +454,10 @@ $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/FileBackendException.php',
+       'FileBackendException' => __DIR__ . '/includes/libs/filebackend/FileBackendException.php',
        'FileBackendGroup' => __DIR__ . '/includes/filebackend/FileBackendGroup.php',
        'FileBackendMultiWrite' => __DIR__ . '/includes/filebackend/FileBackendMultiWrite.php',
        'FileBackendStore' => __DIR__ . '/includes/filebackend/FileBackendStore.php',
@@ -469,7 +471,7 @@ $wgAutoloadLocalClasses = [
        'FileDeleteForm' => __DIR__ . '/includes/FileDeleteForm.php',
        'FileDependency' => __DIR__ . '/includes/cache/CacheDependency.php',
        'FileDuplicateSearchPage' => __DIR__ . '/includes/specials/SpecialFileDuplicateSearch.php',
-       'FileJournal' => __DIR__ . '/includes/filebackend/filejournal/FileJournal.php',
+       'FileJournal' => __DIR__ . '/includes/libs/filebackend/filejournal/FileJournal.php',
        'FileOp' => __DIR__ . '/includes/filebackend/FileOp.php',
        'FileOpBatch' => __DIR__ . '/includes/filebackend/FileOpBatch.php',
        'FileRepo' => __DIR__ . '/includes/filerepo/FileRepo.php',
@@ -583,6 +585,7 @@ $wgAutoloadLocalClasses = [
        'IExpiringStore' => __DIR__ . '/includes/libs/objectcache/IExpiringStore.php',
        'IJobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php',
        'ILoadBalancer' => __DIR__ . '/includes/libs/rdbms/loadbalancer/ILoadBalancer.php',
+       'ILoadMonitor' => __DIR__ . '/includes/libs/rdbms/loadmonitor/ILoadMonitor.php',
        'IP' => __DIR__ . '/includes/utils/IP.php',
        'IPSet' => __DIR__ . '/includes/compat/IPSetCompat.php',
        'IPTC' => __DIR__ . '/includes/media/IPTC.php',
@@ -656,9 +659,9 @@ $wgAutoloadLocalClasses = [
        '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',
@@ -730,7 +733,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',
@@ -744,7 +747,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',
@@ -770,6 +773,7 @@ $wgAutoloadLocalClasses = [
        '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',
        'MWMemcached' => __DIR__ . '/includes/compat/MemcachedClientCompat.php',
@@ -972,11 +976,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',
+       'NullFileJournal' => __DIR__ . '/includes/libs/filebackend/filejournal/NullFileJournal.php',
        'NullFileOp' => __DIR__ . '/includes/filebackend/FileOp.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',
@@ -1068,7 +1072,7 @@ $wgAutoloadLocalClasses = [
        'PopulateRevisionSha1' => __DIR__ . '/maintenance/populateRevisionSha1.php',
        'PostgreSqlLockManager' => __DIR__ . '/includes/filebackend/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',
@@ -1107,7 +1111,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',
@@ -1223,9 +1227,9 @@ $wgAutoloadLocalClasses = [
        'SVGReader' => __DIR__ . '/includes/media/SVGMetadataExtractor.php',
        'SamplingStatsdClient' => __DIR__ . '/includes/libs/SamplingStatsdClient.php',
        'Sanitizer' => __DIR__ . '/includes/Sanitizer.php',
-       'SavepointPostgres' => __DIR__ . '/includes/db/DatabasePostgres.php',
+       'SavepointPostgres' => __DIR__ . '/includes/libs/rdbms/database/utils/SavepointPostgres.php',
        'ScopedCallback' => __DIR__ . '/includes/libs/ScopedCallback.php',
-       'ScopedLock' => __DIR__ . '/includes/filebackend/lockmanager/ScopedLock.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',
@@ -1414,7 +1418,7 @@ $wgAutoloadLocalClasses = [
        'ThumbnailRenderJob' => __DIR__ . '/includes/jobqueue/jobs/ThumbnailRenderJob.php',
        'TidyUpBug37714' => __DIR__ . '/maintenance/tidyUpBug37714.php',
        'TiffHandler' => __DIR__ . '/includes/media/Tiff.php',
-       'TimestampException' => __DIR__ . '/includes/exception/TimestampException.php',
+       'TimestampException' => __DIR__ . '/includes/libs/time/TimestampException.php',
        'Timing' => __DIR__ . '/includes/libs/Timing.php',
        'Title' => __DIR__ . '/includes/Title.php',
        'TitleArray' => __DIR__ . '/includes/TitleArray.php',
@@ -1558,9 +1562,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 c010014..384bfb4 100644 (file)
                        "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..c4a1a8d 100644 (file)
                        "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 3ab8829..f0e9e83 100644 (file)
@@ -1835,13 +1835,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 +2085,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
  *
index ab02a8e..529dfb3 100644 (file)
  * @defgroup Constants MediaWiki constants
  */
 
-/**@{
- * 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 );
-/**@}*/
+# Obsolete aliases
+define( 'DB_SLAVE', -1 );
 
 /**@{
- * Valid database indexes
- * Operation-based indexes
+ * Obsolete IDatabase::makeList() constants
+ * These are also available as Database class constants
  */
-define( 'DB_REPLICA', -1 );     # Read from a replica (or only server)
-define( 'DB_MASTER', -2 );    # Write to master (or only server)
+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 );
 /**@}*/
 
-# Obsolete aliases
-define( 'DB_SLAVE', -1 );
-
 /**@{
  * Virtual namespaces; don't appear in the page database
  */
@@ -185,16 +173,10 @@ define( 'EDIT_AUTOSUMMARY', 64 );
 define( 'EDIT_INTERNAL', 128 );
 /**@}*/
 
-/**@{
- * Flags for Database::makeList()
- * These are also available as Database class constants
+/**
+ * Database related
  */
-define( 'LIST_COMMA', 0 );
-define( 'LIST_AND', 1 );
-define( 'LIST_SET', 2 );
-define( 'LIST_NAMES', 3 );
-define( 'LIST_OR', 4 );
-/**@}*/
+require_once __DIR__ . '/libs/rdbms/defines.php';
 
 /**
  * Unicode and normalisation related
index 65638f2..47360df 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
index 0e59653..e5f518f 100644 (file)
@@ -1992,56 +1992,7 @@ function wfRestoreWarnings() {
 
 # Autodetect, convert and provide timestamps of various types
 
-/**
- * Unix time - the number of seconds since 1970-01-01 00:00:00 UTC
- */
-define( 'TS_UNIX', 0 );
-
-/**
- * MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS)
- */
-define( 'TS_MW', 1 );
-
-/**
- * MySQL DATETIME (YYYY-MM-DD HH:MM:SS)
- */
-define( 'TS_DB', 2 );
-
-/**
- * RFC 2822 format, for E-mail and HTTP headers
- */
-define( 'TS_RFC2822', 3 );
-
-/**
- * ISO 8601 format with no timezone: 1986-02-09T20:00:00Z
- *
- * This is used by Special:Export
- */
-define( 'TS_ISO_8601', 4 );
-
-/**
- * An Exif timestamp (YYYY:MM:DD HH:MM:SS)
- *
- * @see http://exif.org/Exif2-2.PDF The Exif 2.2 spec, see page 28 for the
- *       DateTime tag and page 36 for the DateTimeOriginal and
- *       DateTimeDigitized tags.
- */
-define( 'TS_EXIF', 5 );
-
-/**
- * Oracle format time.
- */
-define( 'TS_ORACLE', 6 );
-
-/**
- * Postgres format time.
- */
-define( 'TS_POSTGRES', 7 );
-
-/**
- * ISO 8601 basic format with no timezone: 19860209T200000Z.  This is used by ResourceLoader
- */
-define( 'TS_ISO_8601_BASIC', 9 );
+require_once __DIR__ . '/libs/time/defines.php';
 
 /**
  * Get a timestamp string in one of various formats
@@ -2127,35 +2078,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();
 }
 
 /**
index 8c01448..2ef891d 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',
@@ -339,7 +338,6 @@ class Html {
                                'height' => '150',
                                'width' => '300',
                        ],
-                       'command' => [ 'type' => 'command' ],
                        'form' => [
                                'action' => 'GET',
                                'autocomplete' => 'on',
index defdc96..201e9b6 100644 (file)
  *
  * @since 1.20
  */
-class MWTimestamp {
+class MWTimestamp extends 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.
-        *
-        * @since 1.20
-        *
-        * @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.
-        *
-        * @since 1.20
-        *
-        * @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.
-        *
-        * @since 1.20
+        * Get a timestamp instance in GMT
         *
-        * @param int $style Constant Output format for timestamp
-        * @throws TimestampException
-        * @return string The formatted timestamp
+        * @param bool|string $ts Timestamp to set, or false for current time
+        * @return MWTimestamp The instance
         */
-       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;
+       public static function getInstance( $ts = false ) {
+               return new static( $ts );
        }
 
        /**
@@ -329,52 +173,6 @@ class MWTimestamp {
                return $ts;
        }
 
-       /**
-        * @since 1.20
-        *
-        * @return string
-        */
-       public function __toString() {
-               return $this->getTimestamp();
-       }
-
-       /**
-        * Calculate the difference between two MWTimestamp objects.
-        *
-        * @since 1.22
-        * @param MWTimestamp $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( MWTimestamp $relativeTo ) {
-               return $this->timestamp->diff( $relativeTo->timestamp );
-       }
-
-       /**
-        * Set the timezone of this timestamp to the specified timezone.
-        *
-        * @since 1.22
-        * @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.
-        *
-        * @since 1.22
-        * @return DateTimeZone The timezone
-        */
-       public function getTimezone() {
-               return $this->timestamp->getTimezone();
-       }
-
        /**
         * Get the localized timezone message, if available.
         *
@@ -395,17 +193,6 @@ class MWTimestamp {
                }
        }
 
-       /**
-        * Format the timestamp in a given format.
-        *
-        * @since 1.22
-        * @param string $format Pattern to format in
-        * @return string The formatted timestamp
-        */
-       public function format( $format ) {
-               return $this->timestamp->format( $format );
-       }
-
        /**
         * Get a timestamp instance in the server local timezone ($wgLocaltimezone)
         *
@@ -419,15 +206,4 @@ class MWTimestamp {
                $timestamp->setTimezone( $wgLocaltimezone );
                return $timestamp;
        }
-
-       /**
-        * Get a timestamp instance in GMT
-        *
-        * @since 1.22
-        * @param bool|string $ts Timestamp to set, or false for current time
-        * @return MWTimestamp The instance
-        */
-       public static function getInstance( $ts = false ) {
-               return new self( $ts );
-       }
 }
index 9bbbd35..8cf009f 100644 (file)
@@ -517,6 +517,7 @@ class MediaWiki {
         */
        public function run() {
                try {
+                       $this->setDBProfilingAgent();
                        try {
                                $this->main();
                        } catch ( ErrorPageError $e ) {
@@ -527,12 +528,40 @@ class MediaWiki {
                                $e->report(); // display the GUI error
                        }
                } catch ( Exception $e ) {
+                       $context = $this->context;
+                       $action = $context->getRequest()->getVal( 'action', 'view' );
+                       if (
+                               $e instanceof DBConnectionError &&
+                               $context->hasTitle() &&
+                               $context->getTitle()->canExist() &&
+                               in_array( $action, [ 'view', 'history' ], true ) &&
+                               HTMLFileCache::useFileCache( $this->context, HTMLFileCache::MODE_OUTAGE )
+                       ) {
+                               // Try to use any (even stale) file during outages...
+                               $cache = new HTMLFileCache( $context->getTitle(), 'view' );
+                               if ( $cache->isCached() ) {
+                                       $cache->loadFromFileCache( $context, HTMLFileCache::MODE_OUTAGE );
+                                       print MWExceptionRenderer::getHTML( $e );
+                                       exit;
+                               }
+
+                       }
+
                        MWExceptionHandler::handleException( $e );
                }
 
                $this->doPostOutputShutdown( 'normal' );
        }
 
+       private function setDBProfilingAgent() {
+               $services = MediaWikiServices::getInstance();
+               // Add a comment for easy SHOW PROCESSLIST interpretation
+               $name = $this->context->getUser()->getName();
+               $services->getDBLoadBalancerFactory()->setAgentName(
+                       mb_strlen( $name ) > 15 ? mb_substr( $name, 0, 15 ) . '...' : $name
+               );
+       }
+
        /**
         * @see MediaWiki::preOutputCommit()
         * @param callable $postCommitWork [default: null]
@@ -809,24 +838,22 @@ class MediaWiki {
                        }
                }
 
-               if ( $this->config->get( 'UseFileCache' ) && $title->getNamespace() >= 0 ) {
-                       if ( HTMLFileCache::useFileCache( $this->context ) ) {
-                               // Try low-level file cache hit
-                               $cache = new HTMLFileCache( $title, $action );
-                               if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
-                                       // Check incoming headers to see if client has this cached
-                                       $timestamp = $cache->cacheTimestamp();
-                                       if ( !$output->checkLastModified( $timestamp ) ) {
-                                               $cache->loadFromFileCache( $this->context );
-                                       }
-                                       // Do any stats increment/watchlist stuff
-                                       // Assume we're viewing the latest revision (this should always be the case with file cache)
-                                       $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
-                                       // Tell OutputPage that output is taken care of
-                                       $output->disable();
-
-                                       return;
+               if ( $title->canExist() && HTMLFileCache::useFileCache( $this->context ) ) {
+                       // Try low-level file cache hit
+                       $cache = new HTMLFileCache( $title, $action );
+                       if ( $cache->isCacheGood( /* Assume up to date */ ) ) {
+                               // Check incoming headers to see if client has this cached
+                               $timestamp = $cache->cacheTimestamp();
+                               if ( !$output->checkLastModified( $timestamp ) ) {
+                                       $cache->loadFromFileCache( $this->context );
                                }
+                               // Do any stats increment/watchlist stuff, assuming user is viewing the
+                               // latest revision (which should always be the case for file cache)
+                               $this->context->getWikiPage()->doViewUpdates( $this->context->getUser() );
+                               // Tell OutputPage that output is taken care of
+                               $output->disable();
+
+                               return;
                        }
                }
 
index a432d44..5551865 100644 (file)
@@ -1046,6 +1046,7 @@ class MimeMagic {
                        }
                }
 
+               $type = null;
                // Check for entry for full MIME type
                if ( $mime ) {
                        $type = $this->findMediaType( $mime );
index 4c4fb1c..c57e219 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 = '';
 
@@ -2268,7 +2261,7 @@ class OutputPage extends ContextSource {
                ob_start();
 
                $response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
-               $response->header( 'Content-language: ' . $config->get( 'LanguageCode' ) );
+               $response->header( 'Content-language: ' . $config->get( 'ContLang' )->getHtmlCode() );
 
                // Avoid Internet Explorer "compatibility view" in IE 8-10, so that
                // jQuery etc. can work correctly.
index 49e596d..98bc885 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
@@ -254,43 +274,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;
                }
 
-               if ( $ns == NS_SPECIAL ) {
-                       return $this->specialSearch( $search, $limit, $offset );
+               // 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 );
+                       }
+
+                       $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 ) );
        }
 
        /**
index 4ab412e..8c7d802 100644 (file)
@@ -43,15 +43,15 @@ 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 = LBFactoryMW::applyDefaultConfig(
+                       $mainConfig->get( 'LBFactoryConf' ),
+                       $mainConfig
+               );
+               $class = LBFactoryMW::getLBFactoryClass( $lbConf );
 
-               return new $class( $config );
+               return new $class( $lbConf );
        },
 
        'DBLoadBalancer' => function( MediaWikiServices $services ) {
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 6d9ddd6..27a334d 100644 (file)
@@ -1079,7 +1079,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 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 41378fb..f3ef3b3 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' ] );
index f763e45..809d567 100644 (file)
@@ -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
index c826bba..5011f48 100644 (file)
@@ -300,144 +300,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 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..8d5af59 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();
@@ -1816,119 +1815,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 5eb86ab..5e3c709 100644 (file)
@@ -495,61 +495,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 236fb9e..8e89c32 100644 (file)
@@ -264,6 +264,12 @@ class ApiQueryBacklinksprop extends ApiQueryGeneratorBase {
                        }
                }
 
+               // MySQL (or at least 5.5.5-10.0.23-MariaDB) chooses a really bad query
+               // plan if it thinks there will be more matching rows in the linktable
+               // than are in page. Use STRAIGHT_JOIN here to force it to use the
+               // intended, fast plan. See T145079 for details.
+               $this->addOption( 'STRAIGHT_JOIN' );
+
                $this->addOption( 'LIMIT', $params['limit'] + 1 );
 
                $res = $this->select( __METHOD__ );
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 0b86f10..31bb5fa 100644 (file)
@@ -35,7 +35,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 68b88f4..8ed1cf5 100644 (file)
@@ -25,7 +25,7 @@
                        "Copper12"
                ]
        },
-       "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentación]]\n* [[mw:API:FAQ|Preguntas frecuentes]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de correo]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anuncios de la API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Errores y peticiones]\n</div>\n<strong>Estado:</strong> Todas las características que se muestran en esta página deberían funcionar, pero la API aún se encuentra en desarrollo activo y puede cambiar en cualquier momento. Suscríbete a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la lista de correo de mediawiki-api-announce] para estar al día de las actualizaciones.\n\n<strong>Solicitudes erróneas:</strong> Cuando se envían solicitudes erróneas a la API, se envía una cabecera HTTP con la clave \"MediaWiki-API-Error\". El valor de la cabecera y el código de error devuelto tomarán el mismo valor. Para más información, véase [[mw:API:Errors_and_warnings|API: Errores y advertencias]].\n\n<strong>Pruebas:</strong> Para facilitar las pruebas de solicitudes a la API, consulta [[Special:ApiSandbox]].",
+       "apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentación]]\n* [[mw:API:FAQ|Preguntas frecuentes]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de correo]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anuncios de la API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Errores y peticiones]\n</div>\n<strong>Estado:</strong> Todas las características que se muestran en esta página deberían funcionar, pero la API aún se encuentra en desarrollo activo y puede cambiar en cualquier momento. Suscríbete a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la lista de correo de mediawiki-api-announce] para estar al día de las actualizaciones.\n\n<strong>Solicitudes erróneas:</strong> Cuando se envían solicitudes erróneas a la API, se envía una cabecera HTTP con la clave \"MediaWiki-API-Error\". El valor de la cabecera y el código de error devuelto tomarán el mismo valor. Para más información, véase [[mw:API:Errors_and_warnings|API: Errores y advertencias]].\n\n<strong>Pruebas:</strong> para facilitar las pruebas de solicitudes a la API, consulta [[Special:ApiSandbox]].",
        "apihelp-main-param-action": "Qué acción se realizará.",
        "apihelp-main-param-format": "El formato de la salida.",
        "apihelp-main-param-maxlag": "El retraso (lag) máximo puede ser utilizado cuando MediaWiki está instalado en un conjunto de bases de datos replicadas. Para evitar cualquier acción que pudiera causar un retraso aún mayor en la replicación del sitio, este parámetro puede causar que el cliente espere hasta que el retraso de replicación sea menor que el valor especificado. En caso de exceso de retraso, se devuelve un código de error <samp>maxlag</samp> con un mensaje similar a <samp>Esperando a $host: $lag segundos de retraso</samp>.<br />Véase [[mw:Manual:Maxlag_parameter|Manual:Parámetro maxlag]] para más información.",
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 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 a82f018..a485531 100644 (file)
@@ -51,15 +51,18 @@ class EmailNotificationSecondaryAuthenticationProvider
                        && !$this->manager->getAuthenticationSessionData( 'no-email' )
                ) {
                        // TODO show 'confirmemail_oncreate'/'confirmemail_sendfailed' message
-                       wfGetDB( DB_MASTER )->onTransactionIdle( function () use ( $user ) {
-                               $user = $user->getInstanceForUpdate();
-                               $status = $user->sendConfirmationMail();
-                               $user->saveSettings();
-                               if ( !$status->isGood() ) {
-                                       $this->logger->warning( 'Could not send confirmation email: ' .
-                                               $status->getWikiText( false, false, 'en' ) );
-                               }
-                       } );
+                       wfGetDB( DB_MASTER )->onTransactionIdle(
+                               function () use ( $user ) {
+                                       $user = $user->getInstanceForUpdate();
+                                       $status = $user->sendConfirmationMail();
+                                       $user->saveSettings();
+                                       if ( !$status->isGood() ) {
+                                               $this->logger->warning( 'Could not send confirmation email: ' .
+                                                       $status->getWikiText( false, false, 'en' ) );
+                                       }
+                               },
+                               __METHOD__
+                       );
                }
 
                return AuthenticationResponse::newPass();
index dd97830..f11a12c 100644 (file)
@@ -112,17 +112,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 f5571c7..f16423d 100644 (file)
@@ -304,10 +304,13 @@ class TemporaryPasswordPrimaryAuthenticationProvider
 
                if ( $sendMail ) {
                        // Send email after DB commit
-                       $dbw->onTransactionIdle( function () use ( $req ) {
-                               /** @var TemporaryPasswordAuthenticationRequest $req */
-                               $this->sendPasswordResetEmail( $req );
-                       } );
+                       $dbw->onTransactionIdle(
+                               function () use ( $req ) {
+                                       /** @var TemporaryPasswordAuthenticationRequest $req */
+                                       $this->sendPasswordResetEmail( $req );
+                               },
+                               __METHOD__
+                       );
                }
        }
 
@@ -375,9 +378,12 @@ class TemporaryPasswordPrimaryAuthenticationProvider
 
                if ( $mailpassword ) {
                        // Send email after DB commit
-                       wfGetDB( DB_MASTER )->onTransactionIdle( function () use ( $user, $creator, $req ) {
-                               $this->sendNewAccountEmail( $user, $creator, $req->password );
-                       } );
+                       wfGetDB( DB_MASTER )->onTransactionIdle(
+                               function () use ( $user, $creator, $req ) {
+                                       $this->sendNewAccountEmail( $user, $creator, $req->password );
+                               },
+                               __METHOD__
+                       );
                }
 
                return $mailpassword ? 'byemail' : null;
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 1bab0f5..a85639f 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
  * @ingroup Cache
  */
 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
         *
@@ -49,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.' );
@@ -93,14 +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 (since 1.28)
         * @return bool
         */
-       public static function useFileCache( IContextSource $context ) {
-               global $wgUseFileCache, $wgDebugToolbar, $wgContLang;
-               if ( !$wgUseFileCache ) {
+       public static function useFileCache( IContextSource $context, $mode = self::MODE_NORMAL ) {
+               $config = MediaWikiServices::getInstance()->getMainConfig();
+
+               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;
@@ -121,15 +129,23 @@ class HTMLFileCache extends FileCacheBase {
 
                        return false;
                }
+
                $user = $context->getUser();
                // Check for non-standard user language; this covers uselang,
                // and extensions for auto-detecting user language.
                $ulang = $context->getLanguage();
 
                // Check that there are no other sources of variation
-               if ( $user->getId() || $user->getNewtalk() || !$ulang->equals( $wgContLang ) ) {
+               if ( $user->getId() || $ulang->getCode() !== $config->get( 'LanguageCode' ) ) {
                        return false;
                }
+
+               if ( $mode === self::MODE_NORMAL ) {
+                       if ( $user->getNewtalk() ) {
+                               return false;
+                       }
+               }
+
                // Allow extensions to disable caching
                return Hooks::run( 'HTMLFileCache::useFileCache', [ $context ] );
        }
@@ -137,17 +153,23 @@ class HTMLFileCache extends FileCacheBase {
        /**
         * Read from cache to context output
         * @param IContextSource $context
+        * @param integer $mode One of the HTMLFileCache::MODE_* constants
         * @return void
         */
-       public function loadFromFileCache( IContextSource $context ) {
-               global $wgMimeType, $wgLanguageCode;
+       public function loadFromFileCache( IContextSource $context, $mode = self::MODE_NORMAL ) {
+               $config = MediaWikiServices::getInstance()->getMainConfig();
 
                wfDebug( __METHOD__ . "()\n" );
                $filename = $this->cachePath();
 
+               if ( $mode === self::MODE_OUTAGE ) {
+                       // Avoid DB errors for queries in sendCacheControl()
+                       $context->getTitle()->resetArticleID( 0 );
+               }
+
                $context->getOutput()->sendCacheControl();
-               header( "Content-Type: $wgMimeType; charset=UTF-8" );
-               header( "Content-Language: $wgLanguageCode" );
+               header( "Content-Type: {$config->get( 'MimeType' )}; charset=UTF-8" );
+               header( "Content-Language: {$config->get( 'LanguageCode' )}" );
                if ( $this->useGzip() ) {
                        if ( wfClientAcceptsGzip() ) {
                                header( 'Content-Encoding: gzip' );
@@ -160,19 +182,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;
                }
@@ -215,9 +242,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 8e74674..794865e 100644 (file)
@@ -297,7 +297,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 +313,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'] );
                }
 
@@ -342,18 +343,21 @@ class RecentChange {
                        ) {
                                // @FIXME: This would be better as an extension hook
                                // Send emails or email jobs once this row is safely committed
-                               $dbw->onTransactionIdle( function () use ( $editor, $title ) {
-                                       $enotif = new EmailNotification();
-                                       $enotif->notifyOnPageChange(
-                                               $editor,
-                                               $title,
-                                               $this->mAttribs['rc_timestamp'],
-                                               $this->mAttribs['rc_comment'],
-                                               $this->mAttribs['rc_minor'],
-                                               $this->mAttribs['rc_last_oldid'],
-                                               $this->mExtra['pageStatus']
-                                       );
-                               } );
+                               $dbw->onTransactionIdle(
+                                       function () use ( $editor, $title ) {
+                                               $enotif = new EmailNotification();
+                                               $enotif->notifyOnPageChange(
+                                                       $editor,
+                                                       $title,
+                                                       $this->mAttribs['rc_timestamp'],
+                                                       $this->mAttribs['rc_comment'],
+                                                       $this->mAttribs['rc_minor'],
+                                                       $this->mAttribs['rc_last_oldid'],
+                                                       $this->mExtra['pageStatus']
+                                               );
+                                       },
+                                       __METHOD__
+                               );
                        }
                }
 
index 3ef9641..6455a3a 100644 (file)
@@ -571,7 +571,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() ) {
@@ -1038,7 +1038,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
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 3fa1335..0000000
+++ /dev/null
@@ -1,3673 +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 */
-       protected $cliMode;
-
-       /** @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 array[] Map of (name => (callable, method name)) */
-       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->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 = [] ) {
-               global $wgCommandLineMode;
-
-               $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;
-                       $p['cliMode'] = $wgCommandLineMode;
-
-                       $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 = [ MWExceptionHandler::class, 'logException' ];
-                       }
-               } 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|null Previous value of the flag
-        */
-       public function debug( $debug = null ) {
-               return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
-       }
-
-       public function bufferResults( $buffer = null ) {
-               if ( is_null( $buffer ) ) {
-                       return !(bool)( $this->mFlags & DBO_NOBUFFER );
-               } else {
-                       return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
-               }
-       }
-
-       /**
-        * 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 ) {
-               return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
-       }
-
-       public function trxLevel() {
-               return $this->mTrxLevel;
-       }
-
-       public function trxTimestamp() {
-               return $this->mTrxLevel ? $this->mTrxTimestamp : null;
-       }
-
-       public function tablePrefix( $prefix = null ) {
-               return wfSetVar( $this->mTablePrefix, $prefix );
-       }
-
-       public function dbSchema( $schema = null ) {
-               return wfSetVar( $this->mSchema, $schema );
-       }
-
-       /**
-        * 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 : [];
-       }
-
-       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 ) {
-               global $wgUser;
-
-               $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 a comment for easy SHOW PROCESSLIST interpretation
-               if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
-                       $userName = $wgUser->getName();
-                       if ( mb_strlen( $userName ) > 15 ) {
-                               $userName = mb_substr( $userName, 0, 15 ) . '...';
-                       }
-                       $userName = str_replace( '/', '', $userName );
-               } else {
-                       $userName = '';
-               }
-
-               // 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 $userName */ ", $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 ) {
-               if ( !$this->mTrxLevel ) {
-                       throw new DBUnexpectedError( $this, "No transaction is active." );
-               }
-               $this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
-       }
-
-       final public function onTransactionIdle( callable $callback ) {
-               $this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
-               if ( !$this->mTrxLevel ) {
-                       $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
-               }
-       }
-
-       final public function onTransactionPreCommitOrIdle( callable $callback ) {
-               if ( $this->mTrxLevel ) {
-                       $this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
-               } 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, wfGetCaller() ];
-               } 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 $callback ) {
-                       try {
-                               list( $phpCallback ) = $callback;
-                               $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 = wfRandomString( 12 );
-               $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
-                       throw new DBUnexpectedError(
-                               $this,
-                               "$fname: Cannot COMMIT to clear snapshot because writes are pending."
-                       );
-               }
-
-               $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 ) {
-               return wfTimestamp( TS_MW, $ts );
-       }
-
-       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
-                       throw new DBUnexpectedError(
-                               $this,
-                               "$fname: Cannot COMMIT to clear snapshot because writes are pending."
-                       );
-               }
-
-               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 );
-                               } );
-                       } else {
-                               $this->unlock( $lockKey, $fname );
-                       }
-               } );
-
-               $this->commit( __METHOD__, 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 ) {
-               return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
-                       ? 'infinity'
-                       : wfTimestamp( $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;
-       }
-
-       /**
-        * @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})." );
-               }
-               $danglingCallbacks = array_merge(
-                       $this->mTrxIdleCallbacks,
-                       $this->mTrxPreCommitCallbacks,
-                       $this->mTrxEndCallbacks
-               );
-               if ( $danglingCallbacks ) {
-                       $callers = [];
-                       foreach ( $danglingCallbacks as $callbackInfo ) {
-                               $callers[] = $callbackInfo[1];
-                       }
-                       $callers = implode( ', ', $callers );
-                       trigger_error( "DB transaction callbacks still pending (from $callers)." );
-               }
-       }
-}
-
-/**
- * @since 1.27
- */
-abstract class Database extends DatabaseBase {
-       // B/C until nothing type hints for DatabaseBase
-       // @TODO: finish renaming DatabaseBase => Database
-}
index 59c8952..eb061d8 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;
        }
@@ -165,7 +145,7 @@ class DatabaseMssql extends Database {
         * @throws DBUnexpectedError
         */
        protected function doQuery( $sql ) {
-               if ( $this->debug() ) {
+               if ( $this->getFlag( DBO_DEBUG ) ) {
                        wfDebug( "SQL: [$sql]\n" );
                }
                $this->offset = 0;
@@ -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
index 87fd81b..9821da1 100644 (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 */
+abstract class LBFactoryMW {
        /**
-        * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
-        * @param array $conf
-        * @TODO: inject objects via dependency framework
+        * @param array $lbConf Config for LBFactory::__construct()
+        * @param Config $mainConfig Main config object from MediaWikiServices
+        * @return array
         */
-       public function __construct( array $conf ) {
-               $defaults = [
-                       'domain' => wfWikiID(),
-                       'hostname' => wfHostname(),
+       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( 'wfLogDBError' ),
-                       'connLogger' => LoggerFactory::getInstance( 'wfLogDBError' ),
+                       'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ),
+                       'connLogger' => LoggerFactory::getInstance( 'DBConnection' ),
                        'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ),
-                       'errorLogger' => [ MWExceptionHandler::class, 'logException' ]
+                       '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 = ObjectCache::getLocalServerInstance();
                if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) {
-                       $defaults['srvCache'] = $sCache;
+                       $lbConf['srvCache'] = $sCache;
                }
                $cCache = ObjectCache::getLocalClusterInstance();
                if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) {
-                       $defaults['memCache'] = $cCache;
+                       $lbConf['memCache'] = $cCache;
                }
                $wCache = ObjectCache::getMainWANInstance();
                if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
-                       $defaults['wanCache'] = $wCache;
+                       $lbConf['wanCache'] = $wCache;
                }
 
-               parent::__construct( $conf + $defaults );
+               return $lbConf;
        }
 
        /**
@@ -93,57 +155,4 @@ abstract class LBFactoryMW extends LBFactory implements DestructibleService {
 
                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..3318ceb 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,29 @@ 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 ( $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] )
+                       && isset( self::$levelMapping[$level] )
+                       && self::$levelMapping[$level] >= 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'] ) {
@@ -298,6 +324,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 +459,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 a348719..6585575 100644 (file)
@@ -24,7 +24,7 @@ class AtomicSectionUpdate implements DeferrableUpdate, DeferrableCallback {
                $this->callback = $callback;
 
                if ( $this->dbw->trxLevel() ) {
-                       $this->dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ] );
+                       $this->dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ], $fname );
                }
        }
 
index d26cf9d..d61dec2 100644 (file)
@@ -23,7 +23,7 @@ class AutoCommitUpdate implements DeferrableUpdate, DeferrableCallback {
                $this->callback = $callback;
 
                if ( $this->dbw->trxLevel() ) {
-                       $this->dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ] );
+                       $this->dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ], $fname );
                }
        }
 
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 e24a9c0..d18349b 100644 (file)
@@ -174,9 +174,12 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
                // Commit and release the lock (if set)
                ScopedCallback::consume( $scopedLock );
                // Run post-commit hooks without DBO_TRX
-               $this->getDB()->onTransactionIdle( function() {
-                       Hooks::run( 'LinksUpdateComplete', [ &$this ] );
-               } );
+               $this->getDB()->onTransactionIdle(
+                       function () {
+                               Hooks::run( 'LinksUpdateComplete', [ &$this ] );
+                       },
+                       __METHOD__
+               );
        }
 
        /**
index 47b162c..5247e97 100644 (file)
@@ -19,7 +19,7 @@ class MWCallableUpdate implements DeferrableUpdate, DeferrableCallback {
                $this->fname = $fname;
 
                if ( $dbw && $dbw->trxLevel() ) {
-                       $dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ] );
+                       $dbw->onTransactionResolution( [ $this, 'cancelOnRollback' ], $fname );
                }
        }
 
index 58b4ac7..e242da3 100644 (file)
@@ -28,11 +28,11 @@ 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 ) {
@@ -88,12 +88,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 +129,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 +150,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 ) ) {
@@ -206,10 +206,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
         */
-       private 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 +254,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 +269,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 +324,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(
diff --git a/includes/exception/TimestampException.php b/includes/exception/TimestampException.php
deleted file mode 100644 (file)
index b9c0c35..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-/**
- * @since 1.20
- */
-class TimestampException extends MWException {
-}
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;
index 8aa11b6..e7e2608 100644 (file)
@@ -85,15 +85,6 @@ class FSFile {
                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.
@@ -117,45 +108,34 @@ class FSFile {
         * @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'] ) {
+                       $info['size'] = $this->getSize(); // bytes
+                       $info['sha1'] = $this->getSha1Base36();
+                       // @TODO: replace the code below with bare FileInfo use so this can go in /libs
                        $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['file-mime'] = $magic->guessMimeType( $this->path, false );
+                       # Logical MIME type
+                       $ext = ( $ext === true ) ? FileBackend::extensionFromPath( $this->path ) : $ext;
                        $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'] );
+                               $info['metadata'] = $handler->getMetadata( $this, $this->path );
+                               /** @noinspection PhpMethodParametersCountMismatchInspection */
+                               $gis = $handler->getImageSize( $this, $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;
@@ -166,7 +146,11 @@ class FSFile {
         *
         * Resulting array fields include:
         *   - fileExists
+        *   - size
+        *   - file-mime (as major/minor)
         *   - mime (as major/minor)
+        *   - major_mime
+        *   - minor_mime
         *   - media_type (value to be used with the MEDIATYPE_xxx constants)
         *   - metadata (handler specific)
         *   - sha1 (in base 36)
@@ -179,6 +163,10 @@ class FSFile {
        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['media_type'] = MEDIATYPE_UNKNOWN;
                $info['metadata'] = '';
index efe78ee..45951ec 100644 (file)
@@ -32,7 +32,7 @@
  * 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.
+ * StatusValue messages should avoid mentioning the internal FS paths.
  * PHP warnings are assumed to be logged rather than output.
  *
  * @ingroup FileBackend
@@ -185,7 +185,7 @@ class FSFileBackend extends FileBackendStore {
        }
 
        protected function doCreateInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $dest = $this->resolveToFSPath( $params['dst'] );
                if ( $dest === null ) {
@@ -195,7 +195,7 @@ class FSFileBackend extends FileBackendStore {
                }
 
                if ( !empty( $params['async'] ) ) { // deferred
-                       $tempFile = TempFSFile::factory( 'create_', 'tmp' );
+                       $tempFile = TempFSFile::factory( 'create_', 'tmp', $this->tmpDirectory );
                        if ( !$tempFile ) {
                                $status->fatal( 'backend-fail-create', $params['dst'] );
 
@@ -214,7 +214,7 @@ class FSFileBackend extends FileBackendStore {
                                wfEscapeShellArg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
                                wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
                        ] );
-                       $handler = function ( $errors, Status $status, array $params, $cmd ) {
+                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
                                if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
                                        $status->fatal( 'backend-fail-create', $params['dst'] );
                                        trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
@@ -238,7 +238,7 @@ class FSFileBackend extends FileBackendStore {
        }
 
        protected function doStoreInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $dest = $this->resolveToFSPath( $params['dst'] );
                if ( $dest === null ) {
@@ -253,7 +253,7 @@ class FSFileBackend extends FileBackendStore {
                                wfEscapeShellArg( $this->cleanPathSlashes( $params['src'] ) ),
                                wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
                        ] );
-                       $handler = function ( $errors, Status $status, array $params, $cmd ) {
+                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
                                if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
                                        $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
                                        trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
@@ -281,7 +281,7 @@ class FSFileBackend extends FileBackendStore {
        }
 
        protected function doCopyInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
@@ -311,7 +311,7 @@ class FSFileBackend extends FileBackendStore {
                                wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
                                wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
                        ] );
-                       $handler = function ( $errors, Status $status, array $params, $cmd ) {
+                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
                                if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
                                        $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
                                        trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
@@ -341,7 +341,7 @@ class FSFileBackend extends FileBackendStore {
        }
 
        protected function doMoveInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
@@ -371,7 +371,7 @@ class FSFileBackend extends FileBackendStore {
                                wfEscapeShellArg( $this->cleanPathSlashes( $source ) ),
                                wfEscapeShellArg( $this->cleanPathSlashes( $dest ) )
                        ] );
-                       $handler = function ( $errors, Status $status, array $params, $cmd ) {
+                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
                                if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
                                        $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
                                        trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
@@ -394,7 +394,7 @@ class FSFileBackend extends FileBackendStore {
        }
 
        protected function doDeleteInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
@@ -416,7 +416,7 @@ class FSFileBackend extends FileBackendStore {
                                wfIsWindows() ? 'DEL' : 'unlink',
                                wfEscapeShellArg( $this->cleanPathSlashes( $source ) )
                        ] );
-                       $handler = function ( $errors, Status $status, array $params, $cmd ) {
+                       $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
                                if ( $errors !== '' && !( wfIsWindows() && $errors[0] === " " ) ) {
                                        $status->fatal( 'backend-fail-delete', $params['src'] );
                                        trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
@@ -441,10 +441,10 @@ class FSFileBackend extends FileBackendStore {
         * @param string $fullCont
         * @param string $dirRel
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
                list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
                $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
                $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
@@ -471,7 +471,7 @@ class FSFileBackend extends FileBackendStore {
        }
 
        protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
                list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
                $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
                $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
@@ -499,7 +499,7 @@ class FSFileBackend extends FileBackendStore {
        }
 
        protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
                list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
                $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
                $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
@@ -527,7 +527,7 @@ class FSFileBackend extends FileBackendStore {
        }
 
        protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
                list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
                $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
                $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
@@ -653,7 +653,7 @@ class FSFileBackend extends FileBackendStore {
                        } else {
                                // Create a new temporary file with the same extension...
                                $ext = FileBackend::extensionFromPath( $src );
-                               $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
+                               $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
                                if ( !$tmpFile ) {
                                        $tmpFiles[$src] = null;
                                } else {
@@ -682,7 +682,7 @@ class FSFileBackend extends FileBackendStore {
        /**
         * @param FSFileOpHandle[] $fileOpHandles
         *
-        * @return Status[]
+        * @return StatusValue[]
         */
        protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
                $statuses = [];
@@ -701,7 +701,7 @@ class FSFileBackend extends FileBackendStore {
                }
 
                foreach ( $fileOpHandles as $index => $fileOpHandle ) {
-                       $status = Status::newGood();
+                       $status = $this->newStatus();
                        $function = $fileOpHandle->call;
                        $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
                        $statuses[$index] = $status;
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..b560e94 100644 (file)
@@ -167,6 +167,8 @@ class FileBackendGroup {
                                : FileJournal::factory( [ 'class' => 'NullFileJournal' ], $name );
                        $config['wanCache'] = ObjectCache::getMainWANInstance();
                        $config['mimeCallback'] = [ $this, 'guessMimeInternal' ];
+                       $config['statusWrapper'] = [ 'Status', 'wrap' ];
+                       $config['tmpDirectory'] = wfTempDir();
 
                        $this->backends[$name]['instance'] = new $class( $config );
                }
@@ -221,7 +223,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 );
                }
index 3b20048..c1cc7bb 100644 (file)
@@ -148,7 +148,7 @@ class FileBackendMultiWrite extends FileBackend {
        }
 
        final protected function doOperationsInternal( array $ops, array $opts ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $mbe = $this->backends[$this->masterIndex]; // convenience
 
@@ -233,10 +233,10 @@ class FileBackendMultiWrite extends FileBackend {
         * Check that a set of files are consistent across all internal backends
         *
         * @param array $paths List of storage paths
-        * @return Status
+        * @return StatusValue
         */
        public function consistencyCheck( array $paths ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
                if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
                        return $status; // skip checks
                }
@@ -305,10 +305,10 @@ class FileBackendMultiWrite extends FileBackend {
         * Check that a set of file paths are usable across all internal backends
         *
         * @param array $paths List of storage paths
-        * @return Status
+        * @return StatusValue
         */
        public function accessibilityCheck( array $paths ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
                if ( count( $this->backends ) <= 1 ) {
                        return $status; // skip checks
                }
@@ -331,10 +331,10 @@ class FileBackendMultiWrite extends FileBackend {
         *
         * @param array $paths List of storage paths
         * @param string|bool $resyncMode False, True, or "conservative"; see __construct()
-        * @return Status
+        * @return StatusValue
         */
        public function resyncFiles( array $paths, $resyncMode = true ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $mBackend = $this->backends[$this->masterIndex];
                foreach ( $paths as $path ) {
@@ -502,8 +502,8 @@ class FileBackendMultiWrite extends FileBackend {
        }
 
        protected function doQuickOperationsInternal( array $ops ) {
-               $status = Status::newGood();
-               // Do the operations on the master backend; setting Status fields...
+               $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 );
@@ -553,10 +553,10 @@ class FileBackendMultiWrite extends FileBackend {
        /**
         * @param string $method One of (doPrepare,doSecure,doPublish,doClean)
         * @param array $params Method arguments
-        * @return Status
+        * @return StatusValue
         */
        protected function doDirectoryOp( $method, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
                $masterStatus = $this->backends[$this->masterIndex]->$method( $realParams );
@@ -736,7 +736,7 @@ class FileBackendMultiWrite extends FileBackend {
                return $this->backends[$index]->preloadFileStat( $realParams );
        }
 
-       public function getScopedLocksForOps( array $ops, Status $status ) {
+       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
index bc4d81d..4e25ce7 100644 (file)
@@ -106,19 +106,19 @@ abstract class FileBackendStore extends FileBackend {
         *   - 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
+        *   - 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 Status
+        * @return StatusValue
         */
        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',
+                       $status = $this->newStatus( 'backend-fail-maxsize',
                                $params['dst'], $this->maxFileSizeInternal() );
                } else {
                        $status = $this->doCreateInternal( $params );
@@ -134,7 +134,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::createInternal()
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        abstract protected function doCreateInternal( array $params );
 
@@ -147,19 +147,19 @@ abstract class FileBackendStore extends FileBackend {
         *   - 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
+        *   - 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 Status
+        * @return StatusValue
         */
        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',
+                       $status = $this->newStatus( 'backend-fail-maxsize',
                                $params['dst'], $this->maxFileSizeInternal() );
                } else {
                        $status = $this->doStoreInternal( $params );
@@ -175,7 +175,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::storeInternal()
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        abstract protected function doStoreInternal( array $params );
 
@@ -189,14 +189,14 @@ abstract class FileBackendStore extends FileBackend {
         *   - 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
+        *   - 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 Status
+        * @return StatusValue
         */
        final public function copyInternal( array $params ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
@@ -212,7 +212,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::copyInternal()
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        abstract protected function doCopyInternal( array $params );
 
@@ -223,12 +223,12 @@ abstract class FileBackendStore extends FileBackend {
         * $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
+        *   - 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 Status
+        * @return StatusValue
         */
        final public function deleteInternal( array $params ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
@@ -241,7 +241,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::deleteInternal()
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        abstract protected function doDeleteInternal( array $params );
 
@@ -255,14 +255,14 @@ abstract class FileBackendStore extends FileBackend {
         *   - 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
+        *   - 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 Status
+        * @return StatusValue
         */
        final public function moveInternal( array $params ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
@@ -279,7 +279,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::moveInternal()
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function doMoveInternal( array $params ) {
                unset( $params['async'] ); // two steps, won't work here :)
@@ -303,12 +303,12 @@ abstract class FileBackendStore extends FileBackend {
         * $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
+        *   - 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 Status
+        * @return StatusValue
         */
        final public function describeInternal( array $params ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
@@ -317,7 +317,7 @@ abstract class FileBackendStore extends FileBackend {
                        $this->clearCache( [ $params['src'] ] );
                        $this->deleteFileCache( $params['src'] ); // persistent cache
                } else {
-                       $status = Status::newGood(); // nothing to do
+                       $status = $this->newStatus(); // nothing to do
                }
 
                return $status;
@@ -326,10 +326,10 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::describeInternal()
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function doDescribeInternal( array $params ) {
-               return Status::newGood();
+               return $this->newStatus();
        }
 
        /**
@@ -337,15 +337,15 @@ abstract class FileBackendStore extends FileBackend {
         * Do not call this function from places outside FileBackend and FileOp.
         *
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        final public function nullInternal( array $params ) {
-               return Status::newGood();
+               return $this->newStatus();
        }
 
        final public function concatenate( array $params ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                // Try to lock the source files for the scope of this function
                $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
@@ -366,10 +366,10 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::concatenate()
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function doConcatenate( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
                $tmpPath = $params['dst']; // convenience
                unset( $params['latest'] ); // sanity
 
@@ -438,7 +438,7 @@ abstract class FileBackendStore extends FileBackend {
 
        final protected function doPrepare( array $params ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
@@ -465,15 +465,15 @@ abstract class FileBackendStore extends FileBackend {
         * @param string $container
         * @param string $dir
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function doPrepareInternal( $container, $dir, array $params ) {
-               return Status::newGood();
+               return $this->newStatus();
        }
 
        final protected function doSecure( array $params ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
@@ -500,15 +500,15 @@ abstract class FileBackendStore extends FileBackend {
         * @param string $container
         * @param string $dir
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function doSecureInternal( $container, $dir, array $params ) {
-               return Status::newGood();
+               return $this->newStatus();
        }
 
        final protected function doPublish( array $params ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
@@ -535,15 +535,15 @@ abstract class FileBackendStore extends FileBackend {
         * @param string $container
         * @param string $dir
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function doPublishInternal( $container, $dir, array $params ) {
-               return Status::newGood();
+               return $this->newStatus();
        }
 
        final protected function doClean( array $params ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                // Recursive: first delete all empty subdirs recursively
                if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
@@ -591,10 +591,10 @@ abstract class FileBackendStore extends FileBackend {
         * @param string $container
         * @param string $dir
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function doCleanInternal( $container, $dir, array $params ) {
-               return Status::newGood();
+               return $this->newStatus();
        }
 
        final public function fileExists( array $params ) {
@@ -842,7 +842,7 @@ abstract class FileBackendStore extends FileBackend {
 
        final public function streamFile( array $params ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                // Always set some fields for subclass convenience
                $params['options'] = isset( $params['options'] ) ? $params['options'] : [];
@@ -863,10 +863,10 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * @see FileBackendStore::streamFile()
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function doStreamFile( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $flags = 0;
                $flags |= !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
@@ -992,7 +992,7 @@ abstract class FileBackendStore extends FileBackend {
         * An exception is thrown if an unsupported operation is requested.
         *
         * @param array $ops Same format as doOperations()
-        * @return array List of FileOp objects
+        * @return FileOp[] List of FileOp objects
         * @throws FileBackendError
         */
        final public function getOperationsInternal( array $ops ) {
@@ -1052,7 +1052,7 @@ abstract class FileBackendStore extends FileBackend {
                ];
        }
 
-       public function getScopedLocksForOps( array $ops, Status $status ) {
+       public function getScopedLocksForOps( array $ops, StatusValue $status ) {
                $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
 
                return $this->getScopedFileLocks( $paths, 'mixed', $status );
@@ -1060,7 +1060,7 @@ abstract class FileBackendStore extends FileBackend {
 
        final protected function doOperationsInternal( array $ops, array $opts ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                // Fix up custom header name/value pairs...
                $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
@@ -1106,7 +1106,7 @@ abstract class FileBackendStore extends FileBackend {
                        $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 );
+                       $subStatus = $this->newStatus( 'backend-fail-internal', $this->name );
                        foreach ( $ops as $i => $op ) { // mark each op as failed
                                $subStatus->success[$i] = false;
                                ++$subStatus->failCount;
@@ -1115,7 +1115,7 @@ abstract class FileBackendStore extends FileBackend {
                                " stat failure; aborted operations: " . FormatJson::encode( $ops ) );
                }
 
-               // Merge errors into status fields
+               // Merge errors into StatusValue fields
                $status->merge( $subStatus );
                $status->success = $subStatus->success; // not done in merge()
 
@@ -1127,7 +1127,7 @@ abstract class FileBackendStore extends FileBackend {
 
        final protected function doQuickOperationsInternal( array $ops ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                // Fix up custom header name/value pairs...
                $ops = array_map( [ $this, 'sanitizeOpHeaders' ], $ops );
@@ -1139,8 +1139,8 @@ abstract class FileBackendStore extends FileBackend {
                // 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)
+               /** @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...
@@ -1184,13 +1184,13 @@ abstract class FileBackendStore extends FileBackend {
 
        /**
         * Execute a list of FileBackendStoreOpHandle handles in parallel.
-        * The resulting Status object fields will correspond
+        * The resulting StatusValue object fields will correspond
         * to the order in which the handles where given.
         *
         * @param FileBackendStoreOpHandle[] $fileOpHandles
         *
         * @throws FileBackendError
-        * @return array Map of Status objects
+        * @return StatusValue[] Map of StatusValue objects
         */
        final public function executeOpHandlesInternal( array $fileOpHandles ) {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
@@ -1216,7 +1216,7 @@ abstract class FileBackendStore extends FileBackend {
         * @param FileBackendStoreOpHandle[] $fileOpHandles
         *
         * @throws FileBackendError
-        * @return Status[] List of corresponding Status objects
+        * @return StatusValue[] List of corresponding StatusValue objects
         */
        protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
                if ( count( $fileOpHandles ) ) {
@@ -1844,7 +1844,7 @@ abstract class FileBackendStore extends FileBackend {
  * 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.
+ * 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().
  */
index 56a4073..916366c 100644 (file)
@@ -239,14 +239,14 @@ abstract class FileOp {
        /**
         * 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.
+        * except when a failing StatusValue object is returned.
         *
         * @param array $predicates
-        * @return Status
+        * @return StatusValue
         */
        final public function precheck( array &$predicates ) {
                if ( $this->state !== self::STATE_NEW ) {
-                       return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
+                       return StatusValue::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
                }
                $this->state = self::STATE_CHECKED;
                $status = $this->doPrecheck( $predicates );
@@ -259,22 +259,22 @@ abstract class FileOp {
 
        /**
         * @param array $predicates
-        * @return Status
+        * @return StatusValue
         */
        protected function doPrecheck( array &$predicates ) {
-               return Status::newGood();
+               return StatusValue::newGood();
        }
 
        /**
         * Attempt the operation
         *
-        * @return Status
+        * @return StatusValue
         */
        final public function attempt() {
                if ( $this->state !== self::STATE_CHECKED ) {
-                       return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
+                       return StatusValue::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
                } elseif ( $this->failed ) { // failed precheck
-                       return Status::newFatal( 'fileop-fail-attempt-precheck' );
+                       return StatusValue::newFatal( 'fileop-fail-attempt-precheck' );
                }
                $this->state = self::STATE_ATTEMPTED;
                if ( $this->doOperation ) {
@@ -284,23 +284,23 @@ abstract class FileOp {
                                $this->logFailure( 'attempt' );
                        }
                } else { // no-op
-                       $status = Status::newGood();
+                       $status = StatusValue::newGood();
                }
 
                return $status;
        }
 
        /**
-        * @return Status
+        * @return StatusValue
         */
        protected function doAttempt() {
-               return Status::newGood();
+               return StatusValue::newGood();
        }
 
        /**
         * Attempt the operation in the background
         *
-        * @return Status
+        * @return StatusValue
         */
        final public function attemptAsync() {
                $this->async = true;
@@ -350,13 +350,13 @@ abstract class FileOp {
        /**
         * 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.
+        * A bad StatusValue will be returned if there is no chance it can be overwritten.
         *
         * @param array $predicates
-        * @return Status
+        * @return StatusValue
         */
        protected function precheckDestExistence( array $predicates ) {
-               $status = Status::newGood();
+               $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?
@@ -476,7 +476,7 @@ class CreateFileOp extends FileOp {
        }
 
        protected function doPrecheck( array &$predicates ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
                // Check if the source data is too big
                if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
                        $status->fatal( 'backend-fail-maxsize',
@@ -509,7 +509,7 @@ class CreateFileOp extends FileOp {
                        return $this->backend->createInternal( $this->setFlags( $this->params ) );
                }
 
-               return Status::newGood();
+               return StatusValue::newGood();
        }
 
        protected function getSourceSha1Base36() {
@@ -535,7 +535,7 @@ class StoreFileOp extends FileOp {
        }
 
        protected function doPrecheck( array &$predicates ) {
-               $status = Status::newGood();
+               $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'] );
@@ -573,7 +573,7 @@ class StoreFileOp extends FileOp {
                        return $this->backend->storeInternal( $this->setFlags( $this->params ) );
                }
 
-               return Status::newGood();
+               return StatusValue::newGood();
        }
 
        protected function getSourceSha1Base36() {
@@ -606,7 +606,7 @@ class CopyFileOp extends FileOp {
        }
 
        protected function doPrecheck( array &$predicates ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
                // Check if the source file exists
                if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
                        if ( $this->getParam( 'ignoreMissingSource' ) ) {
@@ -642,7 +642,7 @@ class CopyFileOp extends FileOp {
 
        protected function doAttempt() {
                if ( $this->overwriteSameCase ) {
-                       $status = Status::newGood(); // nothing to do
+                       $status = StatusValue::newGood(); // nothing to do
                } elseif ( $this->params['src'] === $this->params['dst'] ) {
                        // Just update the destination file headers
                        $headers = $this->getParam( 'headers' ) ?: [];
@@ -680,7 +680,7 @@ class MoveFileOp extends FileOp {
        }
 
        protected function doPrecheck( array &$predicates ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
                // Check if the source file exists
                if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
                        if ( $this->getParam( 'ignoreMissingSource' ) ) {
@@ -720,7 +720,7 @@ class MoveFileOp extends FileOp {
                if ( $this->overwriteSameCase ) {
                        if ( $this->params['src'] === $this->params['dst'] ) {
                                // Do nothing to the destination (which is also the source)
-                               $status = Status::newGood();
+                               $status = StatusValue::newGood();
                        } else {
                                // Just delete the source as the destination file needs no changes
                                $status = $this->backend->deleteInternal( $this->setFlags(
@@ -760,7 +760,7 @@ class DeleteFileOp extends FileOp {
        }
 
        protected function doPrecheck( array &$predicates ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
                // Check if the source file exists
                if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
                        if ( $this->getParam( 'ignoreMissingSource' ) ) {
@@ -809,7 +809,7 @@ class DescribeFileOp extends FileOp {
        }
 
        protected function doPrecheck( array &$predicates ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
                // Check if the source file exists
                if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
                        $status->fatal( 'backend-fail-notexists', $this->params['src'] );
index 78209d8..e34ad8c 100644 (file)
@@ -45,17 +45,17 @@ class FileOpBatch {
         *   - 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:
+        * 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 Status
+        * @return StatusValue
         */
        public static function attempt( array $performOps, array $opts, FileJournal $journal ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
 
                $n = count( $performOps );
                if ( $n > self::MAX_BATCH_SIZE ) {
@@ -119,7 +119,9 @@ class FileOpBatch {
                if ( count( $entries ) ) {
                        $subStatus = $journal->logChangeBatch( $entries, $batchId );
                        if ( !$subStatus->isOK() ) {
-                               return $subStatus; // abort
+                               $status->merge( $subStatus );
+
+                               return $status; // abort
                        }
                }
 
@@ -142,9 +144,9 @@ class FileOpBatch {
         * This will abort remaining ops on failure.
         *
         * @param array $pPerformOps Batches of file ops (batches use original indexes)
-        * @param Status $status
+        * @param StatusValue $status
         */
-       protected static function runParallelBatches( array $pPerformOps, Status $status ) {
+       protected static function runParallelBatches( array $pPerformOps, StatusValue $status ) {
                $aborted = false; // set to true on unexpected errors
                foreach ( $pPerformOps as $performOpsBatch ) {
                        /** @var FileOp[] $performOpsBatch */
@@ -158,13 +160,13 @@ class FileOpBatch {
                                }
                                continue;
                        }
-                       /** @var Status[] $statuses */
+                       /** @var StatusValue[] $statuses */
                        $statuses = [];
                        $opHandles = [];
                        // Get the backend; all sub-batch ops belong to a single backend
                        $backend = reset( $performOpsBatch )->getBackend();
                        // Get the operation handles or actually do it if there is just one.
-                       // If attemptAsync() returns a Status, it was either due to an error
+                       // 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()
index e2c1ede..44fe2cb 100644 (file)
@@ -44,7 +44,7 @@ class MemoryFileBackend extends FileBackendStore {
        }
 
        protected function doCreateInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $dst = $this->resolveHashKey( $params['dst'] );
                if ( $dst === null ) {
@@ -62,7 +62,7 @@ class MemoryFileBackend extends FileBackendStore {
        }
 
        protected function doStoreInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $dst = $this->resolveHashKey( $params['dst'] );
                if ( $dst === null ) {
@@ -89,7 +89,7 @@ class MemoryFileBackend extends FileBackendStore {
        }
 
        protected function doCopyInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $src = $this->resolveHashKey( $params['src'] );
                if ( $src === null ) {
@@ -122,7 +122,7 @@ class MemoryFileBackend extends FileBackendStore {
        }
 
        protected function doDeleteInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $src = $this->resolveHashKey( $params['src'] );
                if ( $src === null ) {
@@ -169,7 +169,7 @@ class MemoryFileBackend extends FileBackendStore {
                        } else {
                                // Create a new temporary file with the same extension...
                                $ext = FileBackend::extensionFromPath( $src );
-                               $fsFile = TempFSFile::factory( 'localcopy_', $ext );
+                               $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'] ) ) {
index 2adf934..0a0e9f5 100644 (file)
@@ -26,7 +26,7 @@
 /**
  * @brief Class for an OpenStack Swift (or Ceph RGW) based file backend.
  *
- * Status messages should avoid mentioning the Swift account name.
+ * StatusValue messages should avoid mentioning the Swift account name.
  * Likewise, error suppression should be used to avoid path disclosure.
  *
  * @ingroup FileBackend
@@ -252,7 +252,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doCreateInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
                if ( $dstRel === null ) {
@@ -279,7 +279,7 @@ class SwiftFileBackend extends FileBackendStore {
                ] ];
 
                $method = __METHOD__;
-               $handler = function ( array $request, Status $status ) use ( $method, $params ) {
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
                        if ( $rcode === 201 ) {
                                // good
@@ -301,7 +301,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doStoreInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
                if ( $dstRel === null ) {
@@ -343,7 +343,7 @@ class SwiftFileBackend extends FileBackendStore {
                ] ];
 
                $method = __METHOD__;
-               $handler = function ( array $request, Status $status ) use ( $method, $params ) {
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
                        if ( $rcode === 201 ) {
                                // good
@@ -365,7 +365,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doCopyInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
                if ( $srcRel === null ) {
@@ -391,7 +391,7 @@ class SwiftFileBackend extends FileBackendStore {
                ] ];
 
                $method = __METHOD__;
-               $handler = function ( array $request, Status $status ) use ( $method, $params ) {
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
                        if ( $rcode === 201 ) {
                                // good
@@ -413,7 +413,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doMoveInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
                if ( $srcRel === null ) {
@@ -448,7 +448,7 @@ class SwiftFileBackend extends FileBackendStore {
                }
 
                $method = __METHOD__;
-               $handler = function ( array $request, Status $status ) use ( $method, $params ) {
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
                        if ( $request['method'] === 'PUT' && $rcode === 201 ) {
                                // good
@@ -472,7 +472,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doDeleteInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
                if ( $srcRel === null ) {
@@ -488,7 +488,7 @@ class SwiftFileBackend extends FileBackendStore {
                ] ];
 
                $method = __METHOD__;
-               $handler = function ( array $request, Status $status ) use ( $method, $params ) {
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
                        if ( $rcode === 204 ) {
                                // good
@@ -512,7 +512,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doDescribeInternal( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
                if ( $srcRel === null ) {
@@ -546,7 +546,7 @@ class SwiftFileBackend extends FileBackendStore {
                ] ];
 
                $method = __METHOD__;
-               $handler = function ( array $request, Status $status ) use ( $method, $params ) {
+               $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
                        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
                        if ( $rcode === 202 ) {
                                // good
@@ -568,7 +568,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doPrepareInternal( $fullCont, $dir, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                // (a) Check if container already exists
                $stat = $this->getContainerStat( $fullCont );
@@ -591,7 +591,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doSecureInternal( $fullCont, $dir, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
                if ( empty( $params['noAccess'] ) ) {
                        return $status; // nothing to do
                }
@@ -615,7 +615,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doPublishInternal( $fullCont, $dir, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $stat = $this->getContainerStat( $fullCont );
                if ( is_array( $stat ) ) {
@@ -636,7 +636,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doCleanInternal( $fullCont, $dir, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                // Only containers themselves can be removed, all else is virtual
                if ( $dir != '' ) {
@@ -719,7 +719,7 @@ class SwiftFileBackend extends FileBackendStore {
                // Find prior metadata headers
                $postHeaders += $this->getMetadataHeaders( $objHdrs );
 
-               $status = Status::newGood();
+               $status = $this->newStatus();
                /** @noinspection PhpUnusedLocalVariableInspection */
                $scopeLockS = $this->getScopedFileLocks( [ $path ], LockManager::LOCK_UW, $status );
                if ( $status->isOK() ) {
@@ -1043,7 +1043,7 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        protected function doStreamFile( array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $flags = !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
 
@@ -1127,7 +1127,7 @@ class SwiftFileBackend extends FileBackendStore {
                        // Get source file extension
                        $ext = FileBackend::extensionFromPath( $path );
                        // Create a new temporary file...
-                       $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
+                       $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
                        if ( $tmpFile ) {
                                $handle = fopen( $tmpFile->getPath(), 'wb' );
                                if ( $handle ) {
@@ -1254,7 +1254,7 @@ class SwiftFileBackend extends FileBackendStore {
        /**
         * @param FileBackendStoreOpHandle[] $fileOpHandles
         *
-        * @return Status[]
+        * @return StatusValue[]
         */
        protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
                $statuses = [];
@@ -1262,7 +1262,7 @@ class SwiftFileBackend extends FileBackendStore {
                $auth = $this->getAuthentication();
                if ( !$auth ) {
                        foreach ( $fileOpHandles as $index => $fileOpHandle ) {
-                               $statuses[$index] = Status::newFatal( 'backend-fail-connect', $this->name );
+                               $statuses[$index] = $this->newStatus( 'backend-fail-connect', $this->name );
                        }
 
                        return $statuses;
@@ -1280,7 +1280,7 @@ class SwiftFileBackend extends FileBackendStore {
                                $req['headers'] = $this->authTokenHeaders( $auth ) + $req['headers'];
                                $httpReqsByStage[$stage][$index] = $req;
                        }
-                       $statuses[$index] = Status::newGood();
+                       $statuses[$index] = $this->newStatus();
                }
 
                // Run all requests for the first stage, then the next, and so on
@@ -1325,10 +1325,10 @@ class SwiftFileBackend extends FileBackendStore {
         * @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
+        * @return StatusValue
         */
        protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
                $auth = $this->getAuthentication();
 
                if ( !$auth ) {
@@ -1411,10 +1411,10 @@ class SwiftFileBackend extends FileBackendStore {
         *
         * @param string $container Container name
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function createContainer( $container, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $auth = $this->getAuthentication();
                if ( !$auth ) {
@@ -1456,10 +1456,10 @@ class SwiftFileBackend extends FileBackendStore {
         *
         * @param string $container Container name
         * @param array $params
-        * @return Status
+        * @return StatusValue
         */
        protected function deleteContainer( $container, array $params ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $auth = $this->getAuthentication();
                if ( !$auth ) {
@@ -1497,12 +1497,12 @@ class SwiftFileBackend extends FileBackendStore {
         * @param string|null $after
         * @param string|null $prefix
         * @param string|null $delim
-        * @return Status With the list as value
+        * @return StatusValue With the list as value
         */
        private function objectListing(
                $fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
        ) {
-               $status = Status::newGood();
+               $status = $this->newStatus();
 
                $auth = $this->getAuthentication();
                if ( !$auth ) {
@@ -1739,17 +1739,17 @@ class SwiftFileBackend extends FileBackendStore {
 
        /**
         * Log an unexpected exception for this backend.
-        * This also sets the Status object to have a fatal error.
+        * This also sets the StatusValue object to have a fatal error.
         *
-        * @param Status|null $status
+        * @param StatusValue|null $status
         * @param string $func
         * @param array $params
         * @param string $err Error string
         * @param int $code HTTP status
-        * @param string $desc HTTP status description
+        * @param string $desc HTTP StatusValue description
         */
        public function onError( $status, $func, array $params, $err = '', $code = 0, $desc = '' ) {
-               if ( $status instanceof Status ) {
+               if ( $status instanceof StatusValue ) {
                        $status->fatal( 'backend-fail-internal', $this->name );
                }
                if ( $code == 401 ) { // possibly a stale token
index f572840..fed6812 100644 (file)
@@ -48,15 +48,20 @@ class TempFSFile extends FSFile {
         * Temporary files may be purged when the file object falls out of scope.
         *
         * @param string $prefix
-        * @param string $extension
+        * @param string $extension Optional file extension
+        * @param string|null $tmpDirectory Optional parent directory
         * @return TempFSFile|null
         */
-       public static function factory( $prefix, $extension = '' ) {
+       public static function factory( $prefix, $extension = '', $tmpDirectory = null ) {
                $ext = ( $extension != '' ) ? ".{$extension}" : '';
 
                $attempts = 5;
                while ( $attempts-- ) {
-                       $path = wfTempDir() . '/' . $prefix . wfRandomString( 12 ) . $ext;
+                       $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();
@@ -73,6 +78,40 @@ class TempFSFile extends FSFile {
                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
         *
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();
-       }
-}
index cccf71a..4667dde 100644 (file)
@@ -104,7 +104,7 @@ abstract class DBLockManager extends QuorumLockManager {
 
        // @todo change this code to work in one batch
        protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
                foreach ( $pathsByType as $type => $paths ) {
                        $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) );
                }
@@ -115,7 +115,7 @@ abstract class DBLockManager extends QuorumLockManager {
        abstract protected function doGetLocksOnServer( $lockSrv, array $paths, $type );
 
        protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
-               return Status::newGood();
+               return StatusValue::newGood();
        }
 
        /**
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 2f17e27..81ce424 100644 (file)
@@ -90,7 +90,7 @@ class MemcLockManager extends QuorumLockManager {
 
        // @todo Change this code to work in one batch
        protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
 
                $lockedPaths = [];
                foreach ( $pathsByType as $type => $paths ) {
@@ -112,7 +112,7 @@ class MemcLockManager extends QuorumLockManager {
 
        // @todo Change this code to work in one batch
        protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
 
                foreach ( $pathsByType as $type => $paths ) {
                        $status->merge( $this->doFreeLocksOnServer( $lockSrv, $paths, $type ) );
@@ -126,10 +126,10 @@ class MemcLockManager extends QuorumLockManager {
         * @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();
 
                $memc = $this->getCache( $lockSrv );
                $keys = array_map( [ $this, 'recordKeyForPath' ], $paths ); // lock records
@@ -202,10 +202,10 @@ class MemcLockManager extends QuorumLockManager {
         * @param string $lockSrv
         * @param array $paths
         * @param string $type
-        * @return Status
+        * @return StatusValue
         */
        protected function doFreeLocksOnServer( $lockSrv, array $paths, $type ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
 
                $memc = $this->getCache( $lockSrv );
                $keys = array_map( [ $this, 'recordKeyForPath' ], $paths ); // lock records
@@ -254,10 +254,10 @@ class MemcLockManager extends QuorumLockManager {
 
        /**
         * @see QuorumLockManager::releaseAllLocks()
-        * @return Status
+        * @return StatusValue
         */
        protected function releaseAllLocks() {
-               return Status::newGood(); // not supported
+               return StatusValue::newGood(); // not supported
        }
 
        /**
index 0536091..124d410 100644 (file)
@@ -35,10 +35,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()
 
@@ -105,10 +105,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
index d55b5ae..d6b1ce8 100644 (file)
@@ -14,7 +14,7 @@ class PostgreSqlLockManager extends DBLockManager {
        ];
 
        protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
                if ( !count( $paths ) ) {
                        return $status; // nothing to lock
                }
@@ -61,10 +61,10 @@ class PostgreSqlLockManager extends DBLockManager {
 
        /**
         * @see QuorumLockManager::releaseAllLocks()
-        * @return Status
+        * @return StatusValue
         */
        protected function releaseAllLocks() {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
 
                foreach ( $this->conns as $lockDb => $db ) {
                        try {
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();
-}
index 4121ecb..6fd819d 100644 (file)
@@ -79,7 +79,7 @@ class RedisLockManager extends QuorumLockManager {
        }
 
        protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
 
                $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
 
@@ -172,7 +172,7 @@ LUA;
        }
 
        protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = Status::newGood();
+               $status = StatusValue::newGood();
 
                $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
 
@@ -242,7 +242,7 @@ LUA;
        }
 
        protected function releaseAllLocks() {
-               return Status::newGood(); // not supported
+               return StatusValue::newGood(); // not supported
        }
 
        protected function isServerUp( $lockSrv ) {
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..b5c5bf3 100644 (file)
@@ -825,7 +825,7 @@ class FileRepo {
 
                $status = $this->storeBatch( [ [ $srcPath, $dstZone, $dstRel ] ], $flags );
                if ( $status->successCount == 0 ) {
-                       $status->ok = false;
+                       $status->setOK( false );
                }
 
                return $status;
@@ -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];
@@ -1539,9 +1539,15 @@ class FileRepo {
         * @return array
         */
        public function getFileProps( $virtualUrl ) {
-               $path = $this->resolveToStoragePath( $virtualUrl );
+               $fsFile = $this->getLocalReference( $virtualUrl );
+               if ( $fsFile ) {
+                       $mwProps = new MWFileProps( MimeMagic::singleton() );
+                       $props = $mwProps->getPropsFromPath( $fsFile->getPath(), true );
+               } else {
+                       $props = FSFile::placeholderProps();
+               }
 
-               return $this->backend->getFileProps( [ 'src' => $path ] );
+               return $props;
        }
 
        /**
index f8b1ed9..55df1af 100644 (file)
@@ -42,6 +42,9 @@ class ForeignDBViaLBRepo extends LocalRepo {
        /** @var array */
        protected $fileFromRowFactory = [ 'ForeignDBFile', 'newFromRow' ];
 
+       /** @var bool */
+       protected $hasSharedCache;
+
        /**
         * @param array|null $info
         */
index fccb755..7b40a7b 100644 (file)
@@ -500,9 +500,12 @@ class LocalRepo extends FileRepo {
        function invalidateImageRedirect( Title $title ) {
                $key = $this->getSharedCacheKey( 'image_redirect', md5( $title->getDBkey() ) );
                if ( $key ) {
-                       $this->getMasterDB()->onTransactionPreCommitOrIdle( function() use ( $key ) {
-                               ObjectCache::getMainWANInstance()->delete( $key );
-                       } );
+                       $this->getMasterDB()->onTransactionPreCommitOrIdle(
+                               function () use ( $key ) {
+                                       ObjectCache::getMainWANInstance()->delete( $key );
+                               },
+                               __METHOD__
+                       );
                }
        }
 
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..c48866b 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() );
        }
 
        /**
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 d63a91b..60cfdac 100644 (file)
@@ -313,9 +313,12 @@ class LocalFile extends File {
                        return;
                }
 
-               $this->repo->getMasterDB()->onTransactionPreCommitOrIdle( function() use ( $key ) {
-                       ObjectCache::getMainWANInstance()->delete( $key );
-               } );
+               $this->repo->getMasterDB()->onTransactionPreCommitOrIdle(
+                       function () use ( $key ) {
+                               ObjectCache::getMainWANInstance()->delete( $key );
+                       },
+                       __METHOD__
+               );
        }
 
        /**
@@ -1176,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 );
                        }
                }
 
@@ -1477,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.
@@ -2002,12 +2008,15 @@ class LocalFile extends File {
                        }
                        // Release the lock *after* commit to avoid row-level contention.
                        // Make sure it triggers on rollback() as well as commit() (T132921).
-                       $dbw->onTransactionResolution( function () use ( $logger ) {
-                               $status = $this->releaseFileLock();
-                               if ( !$status->isGood() ) {
-                                       $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
-                               }
-                       } );
+                       $dbw->onTransactionResolution(
+                               function () use ( $logger ) {
+                                       $status = $this->releaseFileLock();
+                                       if ( !$status->isGood() ) {
+                                               $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
+                                       }
+                               },
+                               __METHOD__
+                       );
                        // Callers might care if the SELECT snapshot is safely fresh
                        $this->lockedOwnTrx = $makesTransaction;
                }
@@ -2686,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;
@@ -2946,7 +2955,7 @@ class LocalFileMoveBatch {
                if ( !$statusDb->isGood() ) {
                        $destFile->unlock();
                        $this->file->unlock();
-                       $statusDb->ok = false;
+                       $statusDb->setOK( false );
 
                        return $statusDb;
                }
@@ -2965,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;
                        }
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..567e692 100644 (file)
@@ -1014,7 +1014,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 +1231,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 0b22727..bbd3473 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' )
                                        $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,
                                                ]
                                        )
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..4f10367 100644 (file)
@@ -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 );
                } );
 
        }
index d59c162..0adeddf 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 = DatabaseBase::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 = DatabaseBase::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 4e34c7d..d3973a1 100644 (file)
@@ -44,7 +44,7 @@
        "config-help-restart": "Необходимо е потвърждение за изтриване на всички въведени и съхранени данни и започване отначало на процеса по инсталация.",
        "config-restart": "Да, започване отначало",
        "config-welcome": "=== Проверка на средата ===\nЩе бъдат извършени основни проверки, които да установят дали средата е подходяща за инсталиране на МедияУики.\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-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] е инсталиран",
index 6ed2722..dc24830 100644 (file)
@@ -18,7 +18,8 @@
                        "C.R.",
                        "Macofe",
                        "Matteocng",
-                       "Einreiher"
+                       "Einreiher",
+                       "Tosky"
                ]
        },
        "config-desc": "Programma di installazione per MediaWiki",
        "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-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:",
index 50727a2..856cdfd 100644 (file)
@@ -186,7 +186,8 @@ class JobQueueDB extends JobQueue {
                $dbw->onTransactionIdle(
                        function () use ( $dbw, $jobs, $flags, $method ) {
                                $this->doBatchPushInternal( $dbw, $jobs, $flags, $method );
-                       }
+                       },
+                       __METHOD__
                );
        }
 
@@ -494,15 +495,18 @@ class JobQueueDB extends JobQueue {
                // jobs to become no-ops without any actual jobs that made them redundant.
                $dbw = $this->getMasterDB();
                $cache = $this->dupCache;
-               $dbw->onTransactionIdle( function () use ( $cache, $params, $key, $dbw ) {
-                       $timestamp = $cache->get( $key ); // current last timestamp of this job
-                       if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
-                               return true; // a newer version of this root job was enqueued
-                       }
+               $dbw->onTransactionIdle(
+                       function () use ( $cache, $params, $key, $dbw ) {
+                               $timestamp = $cache->get( $key ); // current last timestamp of this job
+                               if ( $timestamp && $timestamp >= $params['rootJobTimestamp'] ) {
+                                       return true; // a newer version of this root job was enqueued
+                               }
 
-                       // Update the timestamp of the last root job started at the location...
-                       return $cache->set( $key, $params['rootJobTimestamp'], JobQueueDB::ROOTJOB_TTL );
-               } );
+                               // Update the timestamp of the last root job started at the location...
+                               return $cache->set( $key, $params['rootJobTimestamp'], JobQueueDB::ROOTJOB_TTL );
+                       },
+                       __METHOD__
+               );
 
                return true;
        }
index de5f410..71d68d9 100644 (file)
@@ -120,6 +120,8 @@ class JobQueueGroup {
         * @return void
         */
        public function push( $jobs ) {
+               global $wgJobTypesExcludedFromDefaultQueue;
+
                $jobs = is_array( $jobs ) ? $jobs : [ $jobs ];
                if ( !count( $jobs ) ) {
                        return;
@@ -149,7 +151,7 @@ class JobQueueGroup {
                        'true',
                        15
                );
-               if ( array_intersect( array_keys( $jobsByType ), $this->getDefaultQueueTypes() ) ) {
+               if ( array_diff( array_keys( $jobsByType ), $wgJobTypesExcludedFromDefaultQueue ) ) {
                        $cache->set(
                                $cache->makeGlobalKey( 'jobqueue', $this->wiki, 'hasjobs', self::TYPE_DEFAULT ),
                                'true',
index 809fb63..0e90674 100644 (file)
@@ -19,6 +19,7 @@
  * @author Aaron Schulz
  * @ingroup JobQueue
  */
+use MediaWiki\MediaWikiServices;
 
 /**
  * Job for pruning recent changes
@@ -81,7 +82,7 @@ class RecentChangesUpdateJob extends Job {
                        return; // already in progress
                }
 
-               $factory = wfGetLBFactory();
+               $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
                $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
                $cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
                do {
@@ -119,109 +120,112 @@ class RecentChangesUpdateJob extends Job {
                $dbw = wfGetDB( DB_MASTER );
                // JobRunner uses DBO_TRX, but doesn't call begin/commit itself;
                // onTransactionIdle() will run immediately since there is no trx.
-               $dbw->onTransactionIdle( function() use ( $dbw, $days, $window ) {
-                       $factory = wfGetLBFactory();
-                       $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
-                       // Avoid disconnect/ping() cycle that makes locks fall off
-                       $dbw->setSessionOptions( [ 'connTimeout' => 900 ] );
-
-                       $lockKey = wfWikiID() . '-activeusers';
-                       if ( !$dbw->lock( $lockKey, __METHOD__, 1 ) ) {
-                               return; // exclusive update (avoids duplicate entries)
-                       }
-
-                       $nowUnix = time();
-                       // Get the last-updated timestamp for the cache
-                       $cTime = $dbw->selectField( 'querycache_info',
-                               'qci_timestamp',
-                               [ 'qci_type' => 'activeusers' ]
-                       );
-                       $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1;
-
-                       // Pick the date range to fetch from. This is normally from the last
-                       // update to till the present time, but has a limited window for sanity.
-                       // If the window is limited, multiple runs are need to fully populate it.
-                       $sTimestamp = max( $cTimeUnix, $nowUnix - $days * 86400 );
-                       $eTimestamp = min( $sTimestamp + $window, $nowUnix );
-
-                       // Get all the users active since the last update
-                       $res = $dbw->select(
-                               [ 'recentchanges' ],
-                               [ 'rc_user_text', 'lastedittime' => 'MAX(rc_timestamp)' ],
-                               [
-                                       'rc_user > 0', // actual accounts
-                                       'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata
-                                       'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ),
-                                       'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ),
-                                       'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) )
-                               ],
-                               __METHOD__,
-                               [
-                                       'GROUP BY' => [ 'rc_user_text' ],
-                                       'ORDER BY' => 'NULL' // avoid filesort
-                               ]
-                       );
-                       $names = [];
-                       foreach ( $res as $row ) {
-                               $names[$row->rc_user_text] = $row->lastedittime;
-                       }
-
-                       // Rotate out users that have not edited in too long (according to old data set)
-                       $dbw->delete( 'querycachetwo',
-                               [
-                                       'qcc_type' => 'activeusers',
-                                       'qcc_value < ' . $dbw->addQuotes( $nowUnix - $days * 86400 ) // TS_UNIX
-                               ],
-                               __METHOD__
-                       );
+               $dbw->onTransactionIdle(
+                       function () use ( $dbw, $days, $window ) {
+                               $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+                               $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
+                               // Avoid disconnect/ping() cycle that makes locks fall off
+                               $dbw->setSessionOptions( [ 'connTimeout' => 900 ] );
+
+                               $lockKey = wfWikiID() . '-activeusers';
+                               if ( !$dbw->lock( $lockKey, __METHOD__, 1 ) ) {
+                                       return; // exclusive update (avoids duplicate entries)
+                               }
 
-                       // Find which of the recently active users are already accounted for
-                       if ( count( $names ) ) {
-                               $res = $dbw->select( 'querycachetwo',
-                                       [ 'user_name' => 'qcc_title' ],
+                               $nowUnix = time();
+                               // Get the last-updated timestamp for the cache
+                               $cTime = $dbw->selectField( 'querycache_info',
+                                       'qci_timestamp',
+                                       [ 'qci_type' => 'activeusers' ]
+                               );
+                               $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1;
+
+                               // Pick the date range to fetch from. This is normally from the last
+                               // update to till the present time, but has a limited window for sanity.
+                               // If the window is limited, multiple runs are need to fully populate it.
+                               $sTimestamp = max( $cTimeUnix, $nowUnix - $days * 86400 );
+                               $eTimestamp = min( $sTimestamp + $window, $nowUnix );
+
+                               // Get all the users active since the last update
+                               $res = $dbw->select(
+                                       [ 'recentchanges' ],
+                                       [ 'rc_user_text', 'lastedittime' => 'MAX(rc_timestamp)' ],
                                        [
-                                               'qcc_type' => 'activeusers',
-                                               'qcc_namespace' => NS_USER,
-                                               'qcc_title' => array_keys( $names ) ],
-                                       __METHOD__
+                                               'rc_user > 0', // actual accounts
+                                               'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata
+                                               'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ),
+                                               'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ),
+                                               'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) )
+                                       ],
+                                       __METHOD__,
+                                       [
+                                               'GROUP BY' => [ 'rc_user_text' ],
+                                               'ORDER BY' => 'NULL' // avoid filesort
+                                       ]
                                );
+                               $names = [];
                                foreach ( $res as $row ) {
-                                       unset( $names[$row->user_name] );
+                                       $names[$row->rc_user_text] = $row->lastedittime;
                                }
-                       }
 
-                       // Insert the users that need to be added to the list
-                       if ( count( $names ) ) {
-                               $newRows = [];
-                               foreach ( $names as $name => $lastEditTime ) {
-                                       $newRows[] = [
+                               // Rotate out users that have not edited in too long (according to old data set)
+                               $dbw->delete( 'querycachetwo',
+                                       [
                                                'qcc_type' => 'activeusers',
-                                               'qcc_namespace' => NS_USER,
-                                               'qcc_title' => $name,
-                                               'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ),
-                                               'qcc_namespacetwo' => 0, // unused
-                                               'qcc_titletwo' => '' // unused
-                                       ];
+                                               'qcc_value < ' . $dbw->addQuotes( $nowUnix - $days * 86400 ) // TS_UNIX
+                                       ],
+                                       __METHOD__
+                               );
+
+                               // Find which of the recently active users are already accounted for
+                               if ( count( $names ) ) {
+                                       $res = $dbw->select( 'querycachetwo',
+                                               [ 'user_name' => 'qcc_title' ],
+                                               [
+                                                       'qcc_type' => 'activeusers',
+                                                       'qcc_namespace' => NS_USER,
+                                                       'qcc_title' => array_keys( $names ) ],
+                                               __METHOD__
+                                       );
+                                       foreach ( $res as $row ) {
+                                               unset( $names[$row->user_name] );
+                                       }
                                }
-                               foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
-                                       $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
-                                       $factory->commitAndWaitForReplication( __METHOD__, $ticket );
+
+                               // Insert the users that need to be added to the list
+                               if ( count( $names ) ) {
+                                       $newRows = [];
+                                       foreach ( $names as $name => $lastEditTime ) {
+                                               $newRows[] = [
+                                                       'qcc_type' => 'activeusers',
+                                                       'qcc_namespace' => NS_USER,
+                                                       'qcc_title' => $name,
+                                                       'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ),
+                                                       'qcc_namespacetwo' => 0, // unused
+                                                       'qcc_titletwo' => '' // unused
+                                               ];
+                                       }
+                                       foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
+                                               $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
+                                               $factory->commitAndWaitForReplication( __METHOD__, $ticket );
+                                       }
                                }
-                       }
 
-                       // If a transaction was already started, it might have an old
-                       // snapshot, so kludge the timestamp range back as needed.
-                       $asOfTimestamp = min( $eTimestamp, (int)$dbw->trxTimestamp() );
+                               // If a transaction was already started, it might have an old
+                               // snapshot, so kludge the timestamp range back as needed.
+                               $asOfTimestamp = min( $eTimestamp, (int)$dbw->trxTimestamp() );
 
-                       // Touch the data freshness timestamp
-                       $dbw->replace( 'querycache_info',
-                               [ 'qci_type' ],
-                               [ 'qci_type' => 'activeusers',
-                                       'qci_timestamp' => $dbw->timestamp( $asOfTimestamp ) ], // not always $now
-                               __METHOD__
-                       );
+                               // Touch the data freshness timestamp
+                               $dbw->replace( 'querycache_info',
+                                       [ 'qci_type' ],
+                                       [ 'qci_type' => 'activeusers',
+                                               'qci_timestamp' => $dbw->timestamp( $asOfTimestamp ) ], // not always $now
+                                       __METHOD__
+                               );
 
-                       $dbw->unlock( $lockKey, __METHOD__ );
-               } );
+                               $dbw->unlock( $lockKey, __METHOD__ );
+                       },
+                       __METHOD__
+               );
        }
 }
index 5eafcb3..d76d866 100644 (file)
@@ -36,42 +36,45 @@ class PurgeJobUtils {
                        return;
                }
 
-               $dbw->onTransactionIdle( function() use ( $dbw, $namespace, $dbkeys ) {
-                       $services = MediaWikiServices::getInstance();
-                       $lbFactory = $services->getDBLoadBalancerFactory();
-                       // Determine which pages need to be updated.
-                       // This is necessary to prevent the job queue from smashing the DB with
-                       // large numbers of concurrent invalidations of the same page.
-                       $now = $dbw->timestamp();
-                       $ids = $dbw->selectFieldValues(
-                               'page',
-                               'page_id',
-                               [
-                                       'page_namespace' => $namespace,
-                                       'page_title' => $dbkeys,
-                                       'page_touched < ' . $dbw->addQuotes( $now )
-                               ],
-                               __METHOD__
-                       );
-
-                       if ( !$ids ) {
-                               return;
-                       }
-
-                       $batchSize = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
-                       $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
-                       foreach ( array_chunk( $ids, $batchSize ) as $idBatch ) {
-                               $dbw->update(
+               $dbw->onTransactionIdle(
+                       function () use ( $dbw, $namespace, $dbkeys ) {
+                               $services = MediaWikiServices::getInstance();
+                               $lbFactory = $services->getDBLoadBalancerFactory();
+                               // Determine which pages need to be updated.
+                               // This is necessary to prevent the job queue from smashing the DB with
+                               // large numbers of concurrent invalidations of the same page.
+                               $now = $dbw->timestamp();
+                               $ids = $dbw->selectFieldValues(
                                        'page',
-                                       [ 'page_touched' => $now ],
+                                       'page_id',
                                        [
-                                               'page_id' => $idBatch,
-                                               'page_touched < ' . $dbw->addQuotes( $now ) // handle races
+                                               'page_namespace' => $namespace,
+                                               'page_title' => $dbkeys,
+                                               'page_touched < ' . $dbw->addQuotes( $now )
                                        ],
                                        __METHOD__
                                );
-                               $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
-                       }
-               } );
+
+                               if ( !$ids ) {
+                                       return;
+                               }
+
+                               $batchSize = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
+                               $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
+                               foreach ( array_chunk( $ids, $batchSize ) as $idBatch ) {
+                                       $dbw->update(
+                                               'page',
+                                               [ 'page_touched' => $now ],
+                                               [
+                                                       'page_id' => $idBatch,
+                                                       'page_touched < ' . $dbw->addQuotes( $now ) // handle races
+                                               ],
+                                               __METHOD__
+                                       );
+                                       $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+                               }
+                       },
+                       __METHOD__
+               );
        }
 }
index 320a0b6..fdcbf49 100644 (file)
@@ -351,7 +351,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 {
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/filebackend/FileBackend.php b/includes/libs/filebackend/FileBackend.php
new file mode 100644 (file)
index 0000000..1317b65
--- /dev/null
@@ -0,0 +1,1572 @@
+<?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 string Temporary file directory */
+       protected $tmpDirectory;
+
+       /** @var LockManager */
+       protected $lockManager;
+
+       /** @var FileJournal */
+       protected $fileJournal;
+
+       /** @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.
+        *   - 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.
+        *   - 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.
+        * @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;
+               $this->statusWrapper = isset( $config['statusWrapper'] ) ? $config['statusWrapper'] : null;
+               $this->tmpDirectory = isset( $config['tmpDirectory'] ) ? $config['tmpDirectory'] : null;
+       }
+
+       /**
+        * 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 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 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;
+       }
+
+       /**
+        * 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;
+       }
+}
diff --git a/includes/libs/filebackend/FileBackendException.php b/includes/libs/filebackend/FileBackendException.php
new file mode 100644 (file)
index 0000000..949bce8
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+/**
+ * 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 {
+}
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/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..80add5b
--- /dev/null
@@ -0,0 +1,244 @@
+<?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'] : '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 );
+               }
+       }
+
+       /**
+        * 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/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/QuorumLockManager.php b/includes/libs/lockmanager/QuorumLockManager.php
new file mode 100644 (file)
index 0000000..8b5e7fd
--- /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 = []; // (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 = 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/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 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 b102f0f..94a3b6c 100644 (file)
@@ -22,7 +22,6 @@
  */
 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.
index 3a18fc0..1cb8906 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() );
        }
@@ -444,15 +457,15 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function onTransactionResolution( callable $callback ) {
+       public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function onTransactionIdle( callable $callback ) {
+       public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function onTransactionPreCommitOrIdle( callable $callback ) {
+       public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
                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..04ce8ab
--- /dev/null
@@ -0,0 +1,3558 @@
+<?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 Database 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 = ';';
+       /** @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 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 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;
+
+       /**
+        * @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 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 & 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 = $params['profiler'];
+               $this->trxProfiler = $params['trxProfiler'];
+               $this->connLogger = $params['connLogger'];
+               $this->queryLogger = $params['queryLogger'];
+
+               // 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 ) {
+                       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( 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;
+               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
+        */
+       public 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( 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( 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 DatabaseBase::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 DatabaseBase::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 DatabaseBase::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 '';
+       }
+
+       // 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, 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 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( '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 );
+       }
+
+       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;
+       }
+
+       /**
+        * 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 = '';
+               } 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;
+       }
+
+       /**
+        * 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
+        */
+       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 ) {
+               // 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';
+               } 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 );
+       }
+
+       /**
+        * 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;
+       }
+
+       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 no use the class 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;
+       }
+
+       /**
+        * 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' );
+       }
+
+       public 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
+        */
+       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 ) {
+       }
+
+       /**
+        * 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 ) . " 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';
+               }
+
+               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;
+       }
+
+       /**
+        * @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->mTrxLevel = 0; // no trx 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;
+               }
+       }
+}
diff --git a/includes/libs/rdbms/database/DatabaseBase.php b/includes/libs/rdbms/database/DatabaseBase.php
new file mode 100644 (file)
index 0000000..e008705
--- /dev/null
@@ -0,0 +1,42 @@
+<?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
+ */
+
+/**
+ * Database abstraction object
+ * @ingroup Database
+ */
+abstract class DatabaseBase extends Database {
+       /**
+        * Get search engine class. All subclasses of this need to implement this
+        * if they wish to use searching.
+        *
+        * @return string
+        * @deprecated since 1.27; use SearchEngineFactory::getSearchEngineClass()
+        */
+       public function getSearchEngine() {
+               return 'SearchEngineDummy';
+       }
+}
diff --git a/includes/libs/rdbms/database/DatabaseDomain.php b/includes/libs/rdbms/database/DatabaseDomain.php
new file mode 100644 (file)
index 0000000..01b6b21
--- /dev/null
@@ -0,0 +1,203 @@
+<?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;
+               $this->equivalentString = $this->convertToString();
+       }
+
+       /**
+        * @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->equivalentString === $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() {
+               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..87330b0
--- /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 & 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/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php
new file mode 100644 (file)
index 0000000..5134a4a
--- /dev/null
@@ -0,0 +1,1355 @@
+<?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 DatabaseBase {
+       /** @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->queryLogger->error(
+                               "Error connecting to {db_server}: {error}",
+                               $this->getLogContext( [
+                                       'method' => __METHOD__,
+                                       'error' => $error,
+                               ] )
+                       );
+                       $this->queryLogger->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->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 ) {
+               $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 );
+       }
+
+       /**
+        * @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/libs/rdbms/database/DatabaseMysqli.php b/includes/libs/rdbms/database/DatabaseMysqli.php
new file mode 100644 (file)
index 0000000..fb983bd
--- /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 & 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 ( $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..69488af
--- /dev/null
@@ -0,0 +1,1434 @@
+<?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
+ */
+
+/**
+ * @ingroup Database
+ */
+class DatabasePostgres extends DatabaseBase {
+       /** @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 & 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 ConvertableTimestamp( $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 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;
+                               $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 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 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..2281f23
--- /dev/null
@@ -0,0 +1,1059 @@
+<?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 DatabaseBase {
+       /** @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 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 ) {
+                       $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 ) );
+       }
+
+       /**
+        * 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;
+       }
+
+       /**
+        * 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 026cbc5..21ffa8d 100644 (file)
@@ -63,6 +63,17 @@ 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;
+
        /**
         * A string describing the current software version, and possibly
         * other details in a user-friendly way. Will be listed on Special:Version, etc.
@@ -302,6 +313,13 @@ interface IDatabase {
        /**
         * @return string
         */
+       public function getDomainID();
+
+       /**
+        * Alias for getDomainID()
+        *
+        * @return string
+        */
        public function getWikiID();
 
        /**
@@ -890,18 +908,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 +944,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 +1002,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 +1032,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 );
 
@@ -1284,10 +1330,11 @@ interface IDatabase {
         *   - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK)
         *
         * @param callable $callback
+        * @param string $fname Caller name
         * @return mixed
         * @since 1.28
         */
-       public function onTransactionResolution( callable $callback );
+       public function onTransactionResolution( callable $callback, $fname = __METHOD__ );
 
        /**
         * Run a callback as soon as there is no transaction pending.
@@ -1306,9 +1353,10 @@ interface IDatabase {
         *   - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_IDLE)
         *
         * @param callable $callback
+        * @param string $fname Caller name
         * @since 1.20
         */
-       public function onTransactionIdle( callable $callback );
+       public function onTransactionIdle( callable $callback, $fname = __METHOD__ );
 
        /**
         * Run a callback before the current transaction commits or now if there is none.
@@ -1322,9 +1370,10 @@ interface IDatabase {
         * Updates will execute in the order they were enqueued.
         *
         * @param callable $callback
+        * @param string $fname Caller name
         * @since 1.22
         */
-       public function onTransactionPreCommitOrIdle( callable $callback );
+       public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ );
 
        /**
         * Run a callback each time any transaction commits or rolls back
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..768511b 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 class MssqlResultWrapper extends ResultWrapper {
+       /** @var integer|null */
        private $mSeekTo = null;
 
        /**
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;
+       }
+}
diff --git a/includes/libs/rdbms/defines.php b/includes/libs/rdbms/defines.php
new file mode 100644 (file)
index 0000000..b420ca1
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+/**@{
+ * 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 );
+/**@}*/
+
+/**@{
+ * 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)
+/**@}*/
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;
+               }
+       }
+}
index feae4bd..d75ba93 100644 (file)
@@ -30,6 +30,8 @@ use Psr\Log\LoggerInterface;
 abstract class LBFactory {
        /** @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 */
@@ -62,6 +67,11 @@ abstract class LBFactory {
        /** @var callable[] */
        protected $replicationWaitCallbacks = [];
 
+       /** @var bool Whether this PHP instance is for a CLI script */
+       protected $cliMode;
+       /** @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
@@ -70,11 +80,32 @@ abstract class LBFactory {
                [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
 
        /**
-        * @TODO: document base params here
-        * @param array $conf
+        * Construct a manager of ILoadBalancer objects
+        *
+        * Sub-classes will extend the required keys in $conf with additional parameters
+        *
+        * @param $conf $params Array with keys:
+        *  - localDomain: A DatabaseDomain or domain ID string.
+        *  - readOnlyReason : Reason the master DB is read-only if so [optional]
+        *  - srvCache : BagOStuff object for server cache [optional]
+        *  - memCache : BagOStuff object for cluster memory cache [optional]
+        *  - wanCache : WANObjectCache object [optional]
+        *  - hostname : The name of the current server [optional]
+        *  - cliMode: Whether the execution context is a CLI script. [optional]
+        *  - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
+        *  - trxProfiler: TransactionProfiler instance. [optional]
+        *  - replLogger: PSR-3 logger instance. [optional]
+        *  - connLogger: PSR-3 logger instance. [optional]
+        *  - queryLogger: PSR-3 logger instance. [optional]
+        *  - perfLogger: PSR-3 logger instance. [optional]
+        *  - errorLogger : Callback that takes an Exception and logs it. [optional]
+        * @throws InvalidArgumentException
         */
        public function __construct( array $conf ) {
-               $this->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'];
                }
@@ -91,26 +122,31 @@ abstract class LBFactory {
                $this->errorLogger = isset( $conf['errorLogger'] )
                        ? $conf['errorLogger']
                        : function ( Exception $e ) {
-                               trigger_error( E_WARNING, $e->getMessage() );
+                               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->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()
+        * @see ILoadBalancer::disable()
         */
        public function destroy() {
                $this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
@@ -121,7 +157,7 @@ abstract class LBFactory {
         * 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
+        * @param bool|string $domain Domain ID, or false for the current domain
         * @return ILoadBalancer
         */
        abstract public function newMainLB( $domain = false );
@@ -129,7 +165,7 @@ abstract class LBFactory {
        /**
         * Get a cached (tracked) load balancer object.
         *
-        * @param bool|string $domain Wiki ID, or false for the current wiki
+        * @param bool|string $domain Domain ID, or false for the current domain
         * @return ILoadBalancer
         */
        abstract public function getMainLB( $domain = false );
@@ -140,7 +176,7 @@ abstract class LBFactory {
         * 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
+        * @param bool|string $domain Domain ID, or false for the current domain
         * @return ILoadBalancer
         */
        abstract protected function newExternalLB( $cluster, $domain = false );
@@ -149,7 +185,7 @@ abstract class LBFactory {
         * 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
+        * @param bool|string $domain Domain ID, or false for the current domain
         * @return ILoadBalancer
         */
        abstract public function getExternalLB( $cluster, $domain = false );
@@ -172,10 +208,11 @@ abstract class LBFactory {
        public function shutdown(
                $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
        ) {
+               $chronProt = $this->getChronologyProtector();
                if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
-                       $this->shutdownChronologyProtector( $this->chronProt, $workCallback, 'sync' );
+                       $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
                } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
-                       $this->shutdownChronologyProtector( $this->chronProt, null, 'async' );
+                       $this->shutdownChronologyProtector( $chronProt, null, 'async' );
                }
 
                $this->commitMasterChanges( __METHOD__ ); // sanity
@@ -373,14 +410,14 @@ abstract class LBFactory {
         * 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,
+        * 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:
-        *   - wiki : wait on the load balancer DBs that handles the given wiki
+        *   - 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
@@ -389,19 +426,23 @@ abstract class LBFactory {
         */
        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;
@@ -437,8 +478,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() );
                                }
@@ -527,7 +567,7 @@ abstract class LBFactory {
         * @since 1.28
         */
        public function getChronologyProtectorTouched( $dbName ) {
-               return $this->chronProt->getTouched( $dbName );
+               return $this->getChronologyProtector()->getTouched( $dbName );
        }
 
        /**
@@ -538,27 +578,39 @@ abstract class LBFactory {
         * @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 );
-               if ( PHP_SAPI === 'cli' ) {
-                       $chronProt->setEnabled( false );
+               $this->chronProt->setLogger( $this->replLogger );
+
+               if ( $this->cliMode ) {
+                       $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;
        }
 
        /**
@@ -600,16 +652,19 @@ 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,
                        'replLogger' => $this->replLogger,
                        'errorLogger' => $this->errorLogger,
-                       'hostname' => $this->hostname
+                       'hostname' => $this->hostname,
+                       'cliMode' => $this->cliMode,
+                       'agent' => $this->agent
                ];
        }
 
@@ -623,15 +678,21 @@ abstract class LBFactory {
        }
 
        /**
-        * Define a new local domain (for testing)
+        * Set a new table prefix for the existing local domain ID for testing
         *
-        * Caller should make sure no local connection are open to the old local domain
-        *
-        * @param string $domain
+        * @param string $prefix
         * @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 );
+               } );
        }
 
        /**
@@ -641,4 +702,50 @@ abstract class LBFactory {
        public function closeAll() {
                $this->forEachLBCallMethod( 'closeAll', [] );
        }
+
+       /**
+        * @param string $agent Agent name for query profiling
+        * @since 1.28
+        */
+       public function setAgentName( $agent ) {
+               $this->agent = $agent;
+       }
+
+       /**
+        * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed
+        *
+        * Note that unlike cookies, this works accross domains
+        *
+        * @param string $url
+        * @param float $time UNIX timestamp just before shutdown() was called
+        * @return string
+        * @since 1.28
+        */
+       public function appendPreShutdownTimeAsQuery( $url, $time ) {
+               $usedCluster = 0;
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
+                       $usedCluster |= ( $lb->getServerCount() > 1 );
+               } );
+
+               if ( !$usedCluster ) {
+                       return $url; // no master/replica clusters touched
+               }
+
+               return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
+       }
+
+       /**
+        * @param array $info Map of fields, including:
+        *   - IPAddress : IP address
+        *   - UserAgent : User-Agent HTTP header
+        *   - ChronologyProtection : cookie/header value specifying ChronologyProtector usage
+        * @since 1.28
+        */
+       public function setRequestInfo( array $info ) {
+               $this->requestInfo = $info + $this->requestInfo;
+       }
+
+       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..25e1fe0
--- /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;
+
+       /** @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
+        */
+       protected 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' => $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;
+       }
+
+       /**
+        * @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..4ed4347
--- /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']
+                       : null;
+       }
+
+       /**
+        * @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
+        */
+       protected 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' => $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..4beb5d8
--- /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
+        */
+       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 ) );
+       }
+}
index 0f6bea3..3b2479f 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 {
        /**
-        * @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 +105,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 +153,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 +179,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 +193,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 +209,10 @@ 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
         */
-       public function openConnection( $i, $wiki = false );
+       public function openConnection( $i, $domain = false );
 
        /**
         * @return int
@@ -352,10 +407,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 +420,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 +466,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 +478,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
@@ -471,4 +526,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 cea7523..c030cb2 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
  */
@@ -45,7 +45,7 @@ class LoadBalancer implements ILoadBalancer {
        /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
        private $tableAliases = [];
 
-       /** @var LoadMonitor */
+       /** @var ILoadMonitor */
        private $mLoadMonitor;
        /** @var BagOStuff */
        private $srvCache;
@@ -53,6 +53,8 @@ class LoadBalancer implements ILoadBalancer {
        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,10 +86,16 @@ 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 */
+       protected $cliMode;
+       /** @var string Agent name for query profiling */
+       protected $agent;
 
        /** @var callable Exception logger */
        private $errorLogger;
@@ -109,16 +117,29 @@ class LoadBalancer implements ILoadBalancer {
                        throw new InvalidArgumentException( __CLASS__ . ': missing servers parameter' );
                }
                $this->mServers = $params['servers'];
+
+               $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']
                        : self::POS_WAIT_TIMEOUT;
-               $this->localDomain = isset( $params['localDomain'] ) ? $params['localDomain'] : '';
 
                $this->mReadIndex = -1;
                $this->mConns = [
-                       'local' => [],
+                       'local'       => [],
                        'foreignUsed' => [],
-                       'foreignFree' => [] ];
+                       'foreignFree' => []
+               ];
                $this->mLoads = [];
                $this->mWaitForPos = false;
                $this->mErrorConnection = false;
@@ -166,6 +187,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 {
@@ -175,7 +197,7 @@ class LoadBalancer implements ILoadBalancer {
                $this->errorLogger = isset( $params['errorLogger'] )
                        ? $params['errorLogger']
                        : function ( Exception $e ) {
-                               trigger_error( E_WARNING, $e->getMessage() );
+                               trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
                        };
 
                foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) {
@@ -185,12 +207,14 @@ class LoadBalancer implements ILoadBalancer {
                $this->host = isset( $params['hostname'] )
                        ? $params['hostname']
                        : ( gethostname() ?: 'unknown' );
+               $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
+               $this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
        }
 
        /**
         * Get a LoadMonitor instance
         *
-        * @return LoadMonitor
+        * @return ILoadMonitor
         */
        private function getLoadMonitor() {
                if ( !isset( $this->mLoadMonitor ) ) {
@@ -383,16 +407,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;
 
@@ -508,8 +522,8 @@ class LoadBalancer implements ILoadBalancer {
                                ' 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 === [] )
@@ -585,21 +599,18 @@ class LoadBalancer implements ILoadBalancer {
                        return;
                }
 
-               $dbName = $conn->getDBname();
-               $prefix = $conn->tablePrefix();
-               if ( strval( $prefix ) !== '' ) {
-                       $domain = "$dbName-$prefix";
-               } else {
-                       $domain = $dbName;
-               }
+               $domain = $conn->getDomainID();
                if ( $this->mConns['foreignUsed'][$serverIndex][$domain] !== $conn ) {
-                       throw new InvalidArgumentException( __METHOD__ . ": connection not found, has " .
-                               "the connection been freed already?" );
+                       throw new InvalidArgumentException( __METHOD__ .
+                               ": connection not found, has the connection been freed already?" );
                }
                $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__ .
@@ -607,36 +618,12 @@ 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;
 
@@ -644,6 +631,10 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        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] ) ) {
@@ -696,7 +687,9 @@ class LoadBalancer implements ILoadBalancer {
         * @return IDatabase
         */
        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
@@ -712,11 +705,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;
@@ -775,7 +767,7 @@ class LoadBalancer implements ILoadBalancer {
         * @access private
         *
         * @param array $server
-        * @param bool $dbNameOverride
+        * @param string|bool $dbNameOverride Use "" to not select any database
         * @return IDatabase
         * @throws DBAccessError
         * @throws InvalidArgumentException
@@ -806,14 +798,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['agent'] = $this->agent;
+               // Use DBO_DEFAULT flags by default for LoadBalancer managed databases. Assume that the
+               // application calls LoadBalancer::commitMasterChanges() before the PHP script completes.
+               $server['flags'] = isset( $server['flags'] ) ? $server['flags'] : DBO_DEFAULT;
 
                // 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
@@ -822,7 +822,7 @@ class LoadBalancer implements ILoadBalancer {
 
                $db->setLBInfo( $server );
                $db->setLazyMasterHandle(
-                       $this->getLazyConnectionRef( DB_MASTER, [], $db->getWikiID() )
+                       $this->getLazyConnectionRef( DB_MASTER, [], $db->getDomainID() )
                );
                $db->setTableAliases( $this->tableAliases );
 
@@ -931,12 +931,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;
@@ -944,6 +938,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();
                } );
 
@@ -964,6 +960,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;
@@ -1001,11 +999,6 @@ 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 ) {
                        // Any error should cause all DB transactions to be rolled back together
@@ -1016,13 +1009,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 ) {
@@ -1056,19 +1042,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(
@@ -1132,12 +1105,6 @@ 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 ) {
@@ -1171,12 +1138,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;
@@ -1192,11 +1153,6 @@ 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 ) {
                        $conn->setTrxEndCallbackSuppression( true );
@@ -1226,31 +1182,16 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-       /**
-        * 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 ) {
@@ -1260,11 +1201,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 ) {
@@ -1274,14 +1210,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;
 
@@ -1289,12 +1217,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 ) {
@@ -1330,11 +1252,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;
        }
@@ -1348,13 +1265,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;
@@ -1390,6 +1300,9 @@ class LoadBalancer implements ILoadBalancer {
                                try {
                                        $dbw = $conn ?: $this->getConnection( DB_MASTER, [], $domain );
                                        $readOnly = (int)$dbw->serverIsReadOnly();
+                                       if ( !$conn ) {
+                                               $this->reuseConnection( $dbw );
+                                       }
                                } catch ( DBError $e ) {
                                        $readOnly = 0;
                                }
@@ -1431,12 +1344,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 ) {
@@ -1450,12 +1357,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 ) {
@@ -1493,73 +1394,64 @@ 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
+                       $dbw = $this->getConnection( DB_MASTER );
+                       $pos = $dbw->getMasterPos();
+                       $this->reuseConnection( $dbw );
                }
 
-               $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;
@@ -1573,32 +1465,35 @@ 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;
        }
 
-       /**
-        * Set a new table prefix for the existing local domain ID for testing
-        *
-        * @param string $prefix
-        * @since 1.28
-        */
        public function setDomainPrefix( $prefix ) {
-               list( $dbName, ) = explode( '-', $this->localDomain, 2 );
+               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->localDomain = "{$dbName}-{$prefix}";
+               $this->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix ) {
+                       $db->tablePrefix( $prefix );
+               } );
+       }
+
+       function __destruct() {
+               // Avoid connection leaks for sanity
+               $this->closeAll();
        }
 }
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;
+       }
+}
diff --git a/includes/libs/rdbms/loadmonitor/ILoadMonitor.php b/includes/libs/rdbms/loadmonitor/ILoadMonitor.php
new file mode 100644 (file)
index 0000000..e355c03
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Database load monitoring interface
+ *
+ * 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;
+
+/**
+ * An interface for database load monitoring
+ *
+ * @ingroup Database
+ */
+interface ILoadMonitor extends LoggerAwareInterface {
+       /**
+        * Construct a new LoadMonitor with a given LoadBalancer parent
+        *
+        * @param ILoadBalancer $lb LoadBalancer this instance serves
+        * @param BagOStuff $sCache Local server memory cache
+        * @param BagOStuff $cCache Local cluster memory cache
+        */
+       public function __construct( ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache );
+
+       /**
+        * 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
+        */
+       public function scaleLoads( &$loads, $group = false, $domain = false );
+
+       /**
+        * Get an estimate of replication lag (in seconds) for each server
+        *
+        * Values may be "false" if replication is too broken to estimate
+        *
+        * @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();
+}
index 460746b..1da8f4e 100644 (file)
@@ -1,7 +1,5 @@
 <?php
 /**
- * Database load monitoring.
- *
  * 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
  * @file
  * @ingroup Database
  */
-use Psr\Log\LoggerAwareInterface;
+
+use Psr\Log\LoggerInterface;
 
 /**
- * An interface for database load monitoring
+ * Basic DB load monitor with no external dependencies
+ * Uses memcached to cache the replication lag for a short time
  *
  * @ingroup Database
  */
-interface LoadMonitor extends LoggerAwareInterface {
-       /**
-        * Construct a new LoadMonitor with a given LoadBalancer parent
-        *
-        * @param ILoadBalancer $lb LoadBalancer this instance serves
-        * @param BagOStuff $sCache Local server memory cache
-        * @param BagOStuff $cCache Local cluster memory cache
-        */
-       public function __construct( ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache );
-
-       /**
-        * 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
-        */
-       public function scaleLoads( &$loads, $group = false, $domain = false );
-
-       /**
-        * Get an estimate of replication lag (in seconds) for each server
-        *
-        * Values may be "false" if replication is too broken to estimate
-        *
-        * @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();
+class LoadMonitor implements ILoadMonitor {
+       /** @var ILoadBalancer */
+       protected $parent;
+       /** @var BagOStuff */
+       protected $srvCache;
+       /** @var BagOStuff */
+       protected $mainCache;
+       /** @var LoggerInterface */
+       protected $replLogger;
+
+       public function __construct( ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache ) {
+               $this->parent = $lb;
+               $this->srvCache = $srvCache;
+               $this->mainCache = $cache;
+               $this->replLogger = new \Psr\Log\NullLogger();
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->replLogger = $logger;
+       }
+
+       public function scaleLoads( &$loads, $group = false, $domain = false ) {
+       }
+
+       public function getLagTimes( $serverIndexes, $domain ) {
+               if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
+                       # Single server only, just return zero without caching
+                       return [ 0 => 0 ];
+               }
+
+               $key = $this->getLagTimeCacheKey();
+               # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
+               $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
+               # Keep keys around longer as fallbacks
+               $staleTTL = 60;
+
+               # (a) Check the local APC cache
+               $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
+               }
+               $staleValue = $value ?: false;
+
+               # (b) Check the shared cache and backfill APC
+               $value = $this->mainCache->get( $key );
+               if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
+                       $this->srvCache->set( $key, $value, $staleTTL );
+                       $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from main cache" );
+
+                       return $value['lagTimes']; // cache hit
+               }
+               $staleValue = $value ?: $staleValue;
+
+               # (c) Cache key missing or expired; regenerate and backfill
+               if ( $this->mainCache->lock( $key, 0, 10 ) ) {
+                       # Let this process alone update the cache value
+                       $cache = $this->mainCache;
+                       /** @noinspection PhpUnusedLocalVariableInspection */
+                       $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
+                               $cache->unlock( $key );
+                       } );
+               } elseif ( $staleValue ) {
+                       # Could not acquire lock but an old cache exists, so use it
+                       return $staleValue['lagTimes'];
+               }
+
+               $lagTimes = [];
+               foreach ( $serverIndexes as $i ) {
+                       if ( $i == $this->parent->getWriterIndex() ) {
+                               $lagTimes[$i] = 0; // master always has no lag
+                               continue;
+                       }
+
+                       $conn = $this->parent->getAnyOpenConnection( $i );
+                       if ( $conn ) {
+                               $close = false; // already open
+                       } else {
+                               $conn = $this->parent->openConnection( $i, $domain );
+                               $close = true; // new connection
+                       }
+
+                       if ( !$conn ) {
+                               $lagTimes[$i] = false;
+                               $host = $this->parent->getServerName( $i );
+                               $this->replLogger->error( __METHOD__ . ": host $host (#$i) 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?" );
+                       }
+
+                       if ( $close ) {
+                               # Close the connection to avoid sleeper connections piling up.
+                               # Note that the caller will pick one of these DBs and reconnect,
+                               # which is slightly inefficient, but this only matters for the lag
+                               # time cache miss cache, which is far less common that cache hits.
+                               $this->parent->closeConnection( $conn );
+                       }
+               }
+
+               # Add a timestamp key so we know when it was cached
+               $value = [ 'lagTimes' => $lagTimes, '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'];
+       }
+
+       public function clearCaches() {
+               $key = $this->getLagTimeCacheKey();
+               $this->srvCache->delete( $key );
+               $this->mainCache->delete( $key );
+       }
+
+       private function getLagTimeCacheKey() {
+               $writerIndex = $this->parent->getWriterIndex();
+               // Lag is per-server, not per-DB, so key on the master DB name
+               return $this->srvCache->makeGlobalKey(
+                       'lag-times',
+                       $this->parent->getServerName( $writerIndex )
+               );
+       }
 }
index 02babfe..7286417 100644 (file)
  * @ingroup Database
  */
 
-use Psr\Log\LoggerInterface;
-
 /**
  * Basic MySQL load monitor with no external dependencies
  * Uses memcached to cache the replication lag for a short time
  *
  * @ingroup Database
  */
-class LoadMonitorMySQL implements LoadMonitor {
-       /** @var ILoadBalancer */
-       protected $parent;
-       /** @var BagOStuff */
-       protected $srvCache;
-       /** @var BagOStuff */
-       protected $mainCache;
-       /** @var LoggerInterface */
-       protected $replLogger;
-
-       public function __construct( ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache ) {
-               $this->parent = $lb;
-               $this->srvCache = $srvCache;
-               $this->mainCache = $cache;
-               $this->replLogger = new \Psr\Log\NullLogger();
-       }
-
-       public function setLogger( LoggerInterface $logger ) {
-               $this->replLogger = $logger;
-       }
-
-       public function scaleLoads( &$loads, $group = false, $wiki = false ) {
-       }
-
-       public function getLagTimes( $serverIndexes, $wiki ) {
-               if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
-                       # Single server only, just return zero without caching
-                       return [ 0 => 0 ];
-               }
-
-               $key = $this->getLagTimeCacheKey();
-               # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
-               $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
-               # Keep keys around longer as fallbacks
-               $staleTTL = 60;
-
-               # (a) Check the local APC cache
-               $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
-               }
-               $staleValue = $value ?: false;
-
-               # (b) Check the shared cache and backfill APC
-               $value = $this->mainCache->get( $key );
-               if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
-                       $this->srvCache->set( $key, $value, $staleTTL );
-                       $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from main cache" );
-
-                       return $value['lagTimes']; // cache hit
-               }
-               $staleValue = $value ?: $staleValue;
-
-               # (c) Cache key missing or expired; regenerate and backfill
-               if ( $this->mainCache->lock( $key, 0, 10 ) ) {
-                       # Let this process alone update the cache value
-                       $cache = $this->mainCache;
-                       /** @noinspection PhpUnusedLocalVariableInspection */
-                       $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
-                               $cache->unlock( $key );
-                       } );
-               } elseif ( $staleValue ) {
-                       # Could not acquire lock but an old cache exists, so use it
-                       return $staleValue['lagTimes'];
-               }
-
-               $lagTimes = [];
-               foreach ( $serverIndexes as $i ) {
-                       if ( $i == $this->parent->getWriterIndex() ) {
-                               $lagTimes[$i] = 0; // master always has no lag
-                               continue;
-                       }
-
-                       $conn = $this->parent->getAnyOpenConnection( $i );
-                       if ( $conn ) {
-                               $close = false; // already open
-                       } else {
-                               $conn = $this->parent->openConnection( $i, $wiki );
-                               $close = true; // new connection
-                       }
-
-                       if ( !$conn ) {
-                               $lagTimes[$i] = false;
-                               $host = $this->parent->getServerName( $i );
-                               $this->replLogger->error( __METHOD__ . ": host $host (#$i) 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?" );
-                       }
-
-                       if ( $close ) {
-                               # Close the connection to avoid sleeper connections piling up.
-                               # Note that the caller will pick one of these DBs and reconnect,
-                               # which is slightly inefficient, but this only matters for the lag
-                               # time cache miss cache, which is far less common that cache hits.
-                               $this->parent->closeConnection( $conn );
-                       }
-               }
-
-               # Add a timestamp key so we know when it was cached
-               $value = [ 'lagTimes' => $lagTimes, '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'];
-       }
-
-       public function clearCaches() {
-               $key = $this->getLagTimeCacheKey();
-               $this->srvCache->delete( $key );
-               $this->mainCache->delete( $key );
-       }
-
-       private function getLagTimeCacheKey() {
-               $writerIndex = $this->parent->getWriterIndex();
-               // Lag is per-server, not per-DB, so key on the master DB name
-               return $this->srvCache->makeGlobalKey(
-                       'lag-times',
-                       $this->parent->getServerName( $writerIndex )
-               );
+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
        }
 }
index 1a40b8f..8062001 100644 (file)
@@ -20,7 +20,7 @@
  */
 use Psr\Log\LoggerInterface;
 
-class LoadMonitorNull implements LoadMonitor {
+class LoadMonitorNull implements ILoadMonitor {
        public function __construct( ILoadBalancer $lb, BagOStuff $sCache, BagOStuff $cCache ) {
 
        }
diff --git a/includes/libs/time/ConvertableTimestamp.php b/includes/libs/time/ConvertableTimestamp.php
new file mode 100644 (file)
index 0000000..af7eca6
--- /dev/null
@@ -0,0 +1,243 @@
+<?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/TimestampException.php b/includes/libs/time/TimestampException.php
new file mode 100644 (file)
index 0000000..36ffdee
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+
+/**
+ * @since 1.20
+ */
+class TimestampException extends Exception {
+}
diff --git a/includes/libs/time/defines.php b/includes/libs/time/defines.php
new file mode 100644 (file)
index 0000000..ff4dde8
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * Unix time - the number of seconds since 1970-01-01 00:00:00 UTC
+ */
+define( 'TS_UNIX', 0 );
+
+/**
+ * MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS)
+ */
+define( 'TS_MW', 1 );
+
+/**
+ * MySQL DATETIME (YYYY-MM-DD HH:MM:SS)
+ */
+define( 'TS_DB', 2 );
+
+/**
+ * RFC 2822 format, for E-mail and HTTP headers
+ */
+define( 'TS_RFC2822', 3 );
+
+/**
+ * ISO 8601 format with no timezone: 1986-02-09T20:00:00Z
+ *
+ * This is used by Special:Export
+ */
+define( 'TS_ISO_8601', 4 );
+
+/**
+ * An Exif timestamp (YYYY:MM:DD HH:MM:SS)
+ *
+ * @see http://exif.org/Exif2-2.PDF The Exif 2.2 spec, see page 28 for the
+ *       DateTime tag and page 36 for the DateTimeOriginal and
+ *       DateTimeDigitized tags.
+ */
+define( 'TS_EXIF', 5 );
+
+/**
+ * Oracle format time.
+ */
+define( 'TS_ORACLE', 6 );
+
+/**
+ * Postgres format time.
+ */
+define( 'TS_POSTGRES', 7 );
+
+/**
+ * ISO 8601 basic format with no timezone: 19860209T200000Z.  This is used by ResourceLoader
+ */
+define( 'TS_ISO_8601_BASIC', 9 );
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..32a3340
--- /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 = ( new ConvertableTimestamp(
+                               $res[1] . $res[2] . $res[3] . $res[4] . $res[5] . $res[6]
+                       ) )->getTimestamp( TS_UNIX );
+                       $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 = ( new ConvertableTimestamp( $unix + $offset ) )->getTimestamp( TS_EXIF );
+
+                       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 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 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 70a43f2..4ca2663 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())
         */
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 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..14809a8 100644 (file)
@@ -23,7 +23,6 @@
 
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Services\ServiceDisabledException;
 
 /**
  * Functions to get cache objects
@@ -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_subclass_of( $class, SqlBagOStuff::class ) ) {
+                               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." );
                }
@@ -253,7 +276,7 @@ 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 ) {
@@ -298,13 +321,13 @@ class ObjectCache {
         * @since 1.26
         * @param string $id A key in $wgWANObjectCaches.
         * @return WANObjectCache
-        * @throws MWException
+        * @throws InvalidArgumentException
         */
        public static function newWANCacheFromId( $id ) {
                global $wgWANObjectCaches;
 
                if ( !isset( $wgWANObjectCaches[$id] ) ) {
-                       throw new MWException( "Invalid object cache type \"$id\" requested. " .
+                       throw new InvalidArgumentException( "Invalid object cache type \"$id\" requested. " .
                                "It is not present in \$wgWANObjectCaches." );
                }
 
index db6df86..ba0a484 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;
@@ -1932,12 +1932,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.
index 6c0c4a8..fe0fffc 100644 (file)
@@ -3017,10 +3017,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';
@@ -3031,10 +3028,13 @@ class WikiPage implements Page, IDBAccessObject {
                $logEntry->setComment( $reason );
                $logid = $logEntry->insert();
 
-               $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) {
-                       // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
-                       $logEntry->publish( $logid );
-               } );
+               $dbw->onTransactionPreCommitOrIdle(
+                       function () use ( $dbw, $logEntry, $logid ) {
+                               // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
+                               $logEntry->publish( $logid );
+                       },
+                       __METHOD__
+               );
 
                $dbw->endAtomic( __METHOD__ );
 
@@ -3672,7 +3672,8 @@ class WikiPage implements Page, IDBAccessObject {
                                                $cat->refreshCounts();
                                        }
                                }
-                       }
+                       },
+                       __METHOD__
                );
        }
 
@@ -3734,7 +3735,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 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 97a86c3..34a0a99 100644 (file)
@@ -1031,9 +1031,23 @@ MESSAGE;
                                                $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
                                                break;
                                        default:
+                                               $scripts = isset( $content['scripts'] ) ? $content['scripts'] : '';
+                                               if ( is_string( $scripts ) ) {
+                                                       if ( $name === 'site' || $name === 'user' ) {
+                                                               // Legacy scripts that run in the global scope without a closure.
+                                                               // mw.loader.implement will use globalEval if scripts is a string.
+                                                               // Minify manually here, because general response minification is
+                                                               // not effective due it being a string literal, not a function.
+                                                               if ( !ResourceLoader::inDebugMode() ) {
+                                                                       $scripts = self::filter( 'minify-js', $scripts ); // T107377
+                                                               }
+                                                       } else {
+                                                               $scripts = new XmlJsCode( $scripts );
+                                                       }
+                                               }
                                                $strContent = self::makeLoaderImplementScript(
                                                        $name,
-                                                       isset( $content['scripts'] ) ? $content['scripts'] : '',
+                                                       $scripts,
                                                        isset( $content['styles'] ) ? $content['styles'] : [],
                                                        isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
                                                        isset( $content['templates'] ) ? $content['templates'] : []
@@ -1112,7 +1126,8 @@ MESSAGE;
         * Return JS code that calls mw.loader.implement with given module properties.
         *
         * @param string $name Module name
-        * @param mixed $scripts List of URLs to JavaScript files or String of JavaScript code
+        * @param XmlJsCode|array|string $scripts Code as XmlJsCode (to be wrapped in a closure),
+        *  list of URLs to JavaScript files, or a string of JavaScript for `$.globalEval`.
         * @param mixed $styles Array of CSS strings keyed by media type, or an array of lists of URLs
         *   to CSS files keyed by media type
         * @param mixed $messages List of messages associated with this module. May either be an
@@ -1123,22 +1138,13 @@ MESSAGE;
         * @throws MWException
         * @return string
         */
-       public static function makeLoaderImplementScript(
+       protected static function makeLoaderImplementScript(
                $name, $scripts, $styles, $messages, $templates
        ) {
-               if ( is_string( $scripts ) ) {
-                       // Site and user module are a legacy scripts that run in the global scope (no closure).
-                       // Transportation as string instructs mw.loader.implement to use globalEval.
-                       if ( $name === 'site' || $name === 'user' ) {
-                               // Minify manually because the general makeModuleResponse() minification won't be
-                               // effective here due to the script being a string instead of a function. (T107377)
-                               if ( !ResourceLoader::inDebugMode() ) {
-                                       $scripts = self::filter( 'minify-js', $scripts );
-                               }
-                       } else {
-                               $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts}\n}" );
-                       }
-               } elseif ( !is_array( $scripts ) ) {
+
+               if ( $scripts instanceof XmlJsCode ) {
+                       $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
+               } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
                        throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
                }
                // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
index 30fe3ae..4a2f759 100644 (file)
@@ -219,7 +219,11 @@ class ResourceLoaderContext {
         */
        public function msg() {
                return call_user_func_array( 'wfMessage', func_get_args() )
-                       ->inLanguage( $this->getLanguage() );
+                       ->inLanguage( $this->getLanguage() )
+                       // Use a dummy title because there is no real title
+                       // for this endpoint, and the cache won't vary on it
+                       // anyways.
+                       ->title( Title::newFromText( 'Dwimmerlaik' ) );
        }
 
        /**
index 2351efd..3e94460 100644 (file)
@@ -487,9 +487,12 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
                                );
 
                                if ( $dbw->trxLevel() ) {
-                                       $dbw->onTransactionResolution( function () use ( &$scopeLock ) {
-                                               ScopedCallback::consume( $scopeLock ); // release after commit
-                                       } );
+                                       $dbw->onTransactionResolution(
+                                               function () use ( &$scopeLock ) {
+                                                       ScopedCallback::consume( $scopeLock ); // release after commit
+                                               },
+                                               __METHOD__
+                                       );
                                }
                        }
                } catch ( Exception $e ) {
index 48604e1..674846d 100644 (file)
@@ -120,10 +120,13 @@ abstract class RevDelList extends RevisionListBase {
                }
 
                $dbw->startAtomic( __METHOD__ );
-               $dbw->onTransactionResolution( function () {
-                       // Release locks on commit or error
-                       $this->releaseItemLocks();
-               } );
+               $dbw->onTransactionResolution(
+                       function () {
+                               // Release locks on commit or error
+                               $this->releaseItemLocks();
+                       },
+                       __METHOD__
+               );
 
                $missing = array_flip( $this->ids );
                $this->clearFileOps();
index 1eba141..696facb 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 ];
        }
 
        /**
@@ -648,10 +672,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 e30869e..a767bc3 100644 (file)
@@ -5,7 +5,6 @@
  * Allows to create engine of the specific type.
  */
 class SearchEngineFactory {
-
        /**
         * Configuration for SearchEngine classes.
         * @var SearchEngineConfig
@@ -33,10 +32,32 @@ class SearchEngineFactory {
                        $class = $configType;
                } else {
                        $dbr = wfGetDB( DB_REPLICA );
-                       $class = $dbr->getSearchEngine();
+                       $class = self::getSearchEngineClass( $dbr );
                }
 
                $search = new $class( $dbr );
                return $search;
        }
+
+       /**
+        * @param IDatabase $db
+        * @return string SearchEngine subclass name
+        * @since 1.28
+        */
+       public static function getSearchEngineClass( IDatabase $db ) {
+               switch ( $db->getType() ) {
+                       case 'sqlite':
+                               return 'SearchSqlite';
+                       case 'mysql':
+                               return 'SearchMySQL';
+                       case 'postgres':
+                               return 'SearchPostgres';
+                       case 'mssql':
+                               return 'SearchMssql';
+                       case 'oracle':
+                               return 'SearchOracle';
+                       default:
+                               return 'SearchEngineDummy';
+               }
+       }
 }
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..ed7c6df 100644 (file)
@@ -1302,6 +1302,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 +1313,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 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 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 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 2af0324..0d06c7b 100644 (file)
@@ -2358,7 +2358,8 @@ class User implements IDBAccessObject {
                        wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle(
                                function() use ( $cache, $key ) {
                                        $cache->delete( $key );
-                               }
+                               },
+                               __METHOD__
                        );
                }
        }
@@ -4927,9 +4928,12 @@ class User implements IDBAccessObject {
         * Deferred version of incEditCountImmediate()
         */
        public function incEditCount() {
-               wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle( function() {
-                       $this->incEditCountImmediate();
-               } );
+               wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle(
+                       function () {
+                               $this->incEditCountImmediate();
+                       },
+                       __METHOD__
+               );
        }
 
        /**
index 09244a4..69bc503 100644 (file)
@@ -273,15 +273,20 @@ class UserRightsProxy {
         * Replaces User::touchUser()
         */
        function invalidateCache() {
-               $this->db->update( 'user',
+               $this->db->update(
+                       'user',
                        [ 'user_touched' => $this->db->timestamp() ],
                        [ 'user_id' => $this->id ],
-                       __METHOD__ );
+                       __METHOD__
+               );
 
                $wikiId = $this->db->getWikiID();
                $userId = $this->id;
-               $this->db->onTransactionPreCommitOrIdle( function() use ( $wikiId, $userId ) {
-                       User::purge( $wikiId, $userId );
-               } );
+               $this->db->onTransactionPreCommitOrIdle(
+                       function () use ( $wikiId, $userId ) {
+                               User::purge( $wikiId, $userId );
+                       },
+                       __METHOD__
+               );
        }
 }
diff --git a/includes/utils/MWFileProps.php b/includes/utils/MWFileProps.php
new file mode 100644 (file)
index 0000000..b8ecd4c
--- /dev/null
@@ -0,0 +1,115 @@
+<?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 = FSFile::placeholderProps();
+               $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;
+       }
+}
index 2423092..743f77b 100644 (file)
--- a/index.php
+++ b/index.php
@@ -8,7 +8,7 @@
  * See the README, INSTALL, and UPGRADE files for basic setup instructions
  * and pointers to the online documentation.
  *
- * https://www.mediawiki.org/
+ * https://www.mediawiki.org/wiki/Special:MyLanguage/MediaWiki
  *
  * ----------
  *
index 169e0ff..db71c5c 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 /**
  * Internationalisation code.
+ * See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more information.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
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 043d151..db567fc 100644 (file)
@@ -95,7 +95,7 @@
        "tog-enotifminoredits": "أرسل إلي رسالة إلكترونية بشأن التعديلات الطفيفة للصفحات والملفات",
        "tog-enotifrevealaddr": "أظهر عنوان بريدي الإلكتروني في إشعارات البريد الإلكتروني",
        "tog-shownumberswatching": "اعرض عدد المستخدمين المراقبين",
-       "tog-oldsig": "اÙ\84تÙ\88Ù\82Ù\8aع الحالي:",
+       "tog-oldsig": "تÙ\88Ù\82Ù\8aعÙ\83 الحالي:",
        "tog-fancysig": "وضع الوصلة يدوياً واستعمال نص الويكي",
        "tog-uselivepreview": "استعمال المعاينة المباشرة",
        "tog-forceeditsummary": "نبهني عند عدم إدخال ملخص تعديل",
        "tog-watchlisthideliu": "أخف تعديلات المستخدمين المسجلين في قائمة المراقبة",
        "tog-watchlistreloadautomatically": "أعد تحميل قائمة المراقبة بصفة آلية حينما يتغير مرشح ما (يتطلب جافاسكربت)",
        "tog-watchlisthideanons": "أخف تعديلات المستخدمين المجهولين في قائمة المراقبة",
-       "tog-watchlisthidepatrolled": " أخف التعديلات المراجعة في قائمة المراقبة",
+       "tog-watchlisthidepatrolled": "أخف التعديلات المراجعة في قائمة المراقبة",
        "tog-watchlisthidecategorization": "أخف تصنيف الصفحات",
        "tog-ccmeonemails": "أرسل إلي نسخا من الرسائل الإلكترونية التي أرسلها إلى المستخدمين الآخرين",
        "tog-diffonly": "لا تعرض محتوى الصفحة أسفل الفرق",
        "tog-showhiddencats": "أظهر التصنيفات المخفية",
        "tog-norollbackdiff": "عدم إظهار الاختلافات بعد تنفيذ التراجع",
        "tog-useeditwarning": "حذّرني عندما أغادر تحرير صفحة فيها تغييرات لم أحفظها",
-       "tog-prefershttps": "دائÙ\85ا Ø§Ø³ØªØ®Ø¯Ù\85 Ø§ØªØµØ§Ù\84ا Ø¢Ù\85Ù\86ا Ø¨Ø¹Ø¯ الدخول",
+       "tog-prefershttps": "دائÙ\85ا Ø§Ø³ØªØ®Ø¯Ù\85 Ø§ØªØµØ§Ù\84ا Ø¢Ù\85Ù\86ا Ø¹Ù\86د ØªØ³Ø¬Ù\8aÙ\84 الدخول",
        "underline-always": "دائما",
        "underline-never": "أبدا",
        "underline-default": "وفق المظهر أو المتصفح",
        "newwindow": "(تفتح في نافذة جديدة)",
        "cancel": "ألغِ",
        "moredotdotdot": "المزيد...",
-       "morenotlisted": "هذه القائمة غير مكتملة.",
+       "morenotlisted": "Ù\87Ø°Ù\87 Ø§Ù\84Ù\82ائÙ\85Ø© Ø±Ø¨Ù\85ا ØªÙ\83Ù\88Ù\86 ØºÙ\8aر Ù\85Ù\83تÙ\85Ù\84Ø©.",
        "mypage": "صفحة",
        "mytalk": "نقاش",
        "anontalk": "نقاش",
        "exif-lens": "العدسة المستخدمة",
        "exif-serialnumber": "الرقم التسلسلي للكاميرا",
        "exif-cameraownername": "مالك الكاميرا",
-       "exif-label": "عÙ\84اÙ\85ة",
+       "exif-label": "اÙ\84تسÙ\85Ù\8aة",
        "exif-datetimemetadata": "آخر تعديل للبيانات التعريفية",
        "exif-nickname": "الاسم غير الرسمي للصورة",
        "exif-rating": "التقييم (من 5)",
        "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",
index f12c554..0920f68 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",
        "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",
index 79ea5c9..62ce77a 100644 (file)
@@ -43,7 +43,7 @@
        "tog-enotifminoredits": "Паведамляць праз электронную пошту таксама пра дробныя зьмены старонак і файлаў",
        "tog-enotifrevealaddr": "Не хаваць мой адрас электроннай пошты ў паведамленьнях",
        "tog-shownumberswatching": "Паказваць колькасьць назіральнікаў",
-       "tog-oldsig": "Цяперашні подпіс:",
+       "tog-oldsig": "Ð\92аÑ\88 Ñ\86яперашні подпіс:",
        "tog-fancysig": "Апрацоўваць подпіс як вікітэкст (без аўтаматычнай спасылкі)",
        "tog-uselivepreview": "Выкарыстоўваць хуткі папярэдні прагляд",
        "tog-forceeditsummary": "Папярэджваць пра адсутнасьць кароткага апісаньня зьменаў",
        "newwindow": "(адкрываецца ў новым акне)",
        "cancel": "Скасаваць",
        "moredotdotdot": "Далей…",
-       "morenotlisted": "Гэта ня поўны сьпіс.",
+       "morenotlisted": "Гэты сьпіс можа быць няпоўным.",
        "mypage": "Старонка",
        "mytalk": "Гутаркі",
        "anontalk": "Гутаркі",
        "talk": "Абмеркаваньне",
        "views": "Рэжымы",
        "toolbox": "Інструмэнты",
+       "tool-link-userrights": "Зьмяніць групы {{GENDER:$1|ўдзельніка|ўдзельніцы}}",
+       "tool-link-emailuser": "Даслаць {{GENDER:$1|удзельніку|удзельніцы}} ліст электроннай поштай",
        "userpage": "Паказаць старонку ўдзельніка",
        "projectpage": "Паказаць старонку праекту",
        "imagepage": "Паказаць старонку файла",
        "changeemail-nochange": "Калі ласка, увядзіце іншы новы адрас электроннай пошты",
        "resettokens": "Скіданьне токенаў",
        "resettokens-text": "Тут вы можаце скінуць токены, якія даюць вам доступ да пэўных прыватных зьвестак, асацыяваных з вашым рахункам.\n\nКалі вы выпадкова падзяліліся токенамі зь іншымі, або калі ваш рахунак быў скампрамэтаваны, скарыстайцеся гэтай магчымасьцю і скіньце токены.",
-       "resettokens-no-tokens": "Ð\9dÑ\8fма Ñ\88Ñ\82о Ñ\81кÑ\96даÑ\86Ñ\8c.",
+       "resettokens-no-tokens": "Ð\9dÑ\8fма Ñ\82окенаÑ\9e Ð´Ð»Ñ\8f Ñ\81кÑ\96данÑ\8cнÑ\8f.",
        "resettokens-tokens": "Токены:",
-       "resettokens-token-label": "$1 (бягучае значэньне: $2)",
+       "resettokens-token-label": "$1 (цяперашняе значэньне: $2)",
        "resettokens-watchlist-token": "Токен стужкі (Atom/RSS) [[Special:Watchlist|зьменаў у вашым сьпісе назіраньня]]",
        "resettokens-done": "Токены скінутыя.",
        "resettokens-resetbutton": "Скінуць вылучаныя токены",
        "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-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",
        "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-all": "Усе",
        "log-action-filter-block-block": "Заблякаваць",
        "log-action-filter-block-reblock": "Зьмяненьне блякаваньня",
index 55f2e8a..ac0322a 100644 (file)
@@ -56,7 +56,7 @@
        "tog-enotifminoredits": "Паведамяць мне на эл.пошту пра дробныя праўкі старонак і файлаў",
        "tog-enotifrevealaddr": "Паказваць мой адрас эл.пошты ў паведамленнях",
        "tog-shownumberswatching": "Паказваць колькасць назіральнікаў",
-       "tog-oldsig": "Ð\86Ñ\81нÑ\83Ñ\8eÑ\87Ñ\8b подпіс:",
+       "tog-oldsig": "Ð\92аÑ\88 Ñ\86Ñ\8fпеÑ\80аÑ\88нÑ\96 подпіс:",
        "tog-fancysig": "Апрацоўваць подпіс як вікі-тэкст (без аўтаматычнай спасылкі)",
        "tog-uselivepreview": "Жывы перадпаказ",
        "tog-forceeditsummary": "Папярэджваць пра пустое поле тлумачэння праўкі",
@@ -73,7 +73,7 @@
        "tog-showhiddencats": "Паказаць схаваныя катэгорыі",
        "tog-norollbackdiff": "Не паказваць розніцу ў выніку адкату",
        "tog-useeditwarning": "Папярэдзіць мяне, калі я пакідаю старонку з незахаванымі праўкамі",
-       "tog-prefershttps": "Ð\97аÑ\9eÑ\81Ñ\91дÑ\8b Ð²Ñ\8bкаÑ\80Ñ\8bÑ\81Ñ\82оÑ\9eваÑ\86Ñ\8c Ð°Ð±Ð°Ñ\80оненае Ð·Ð»Ñ\83Ñ\87Ñ\8dнне Ð¿Ð°Ñ\81лÑ\8f Ñ\9eваÑ\85одÑ\83 ў сістэму",
+       "tog-prefershttps": "Ð\97аÑ\9eÑ\81Ñ\91дÑ\8b Ð²Ñ\8bкаÑ\80Ñ\8bÑ\81Ñ\82оÑ\9eваÑ\86Ñ\8c Ð±Ñ\8fÑ\81пеÑ\87нае Ð·Ð»Ñ\83Ñ\87Ñ\8dнне Ð¿Ð° Ñ\9eваÑ\85одзе ў сістэму",
        "underline-always": "Заўсёды",
        "underline-never": "Ніколі",
        "underline-default": "Як у браўзеры",
        "newwindow": "(адкрыецца ў новым акне)",
        "cancel": "Нічога",
        "moredotdotdot": "Яшчэ...",
-       "morenotlisted": "Гэты спіс не поўны.",
+       "morenotlisted": "Ð\93Ñ\8dÑ\82Ñ\8b Ñ\81пÑ\96Ñ\81 Ð¼Ð¾Ð¶Ð° Ð±Ñ\8bÑ\86Ñ\8c Ð½Ðµ Ð¿Ð¾Ñ\9eнÑ\8b.",
        "mypage": "Старонка",
        "mytalk": "Размовы",
        "anontalk": "Размовы",
        "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> (Для старых робатаў, якія патрабуюць, каб імя для ўваходу супадала з патэнцыяльным імем удзельніка, можаце таксама выкарыстоўваць <strong>$3</strong> як імя і <strong>$4</strong> у якасці пароля.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider недаступны.",
        "botpasswords-restriction-failed": "Уваход не выкананы з-за абмежаванняў на пароль робата.",
        "botpasswords-invalid-name": "Паказанае імя ўдзельніка не ўтрымлівае падзяляльнік паролю робата (\"$1\").",
        "invalid-content-data": "Недапушчальнае змесціва",
        "content-not-allowed-here": "\"$1\" не дазволены на старонцы [[$2]]",
        "editwarning-warning": "Пераход на іншую старонку можа прывесці да страты правак, зробленых Вамі. \nКалі Вы ўвайшлі ў сістэму, Вы можаце адключыць гэта папярэджанне ў раздзеле \"{{int:prefs-editing}}\" Вашых настроек.",
+       "editpage-invalidcontentmodel-title": "Мадэль змесціва не падтрымліваецца",
+       "editpage-invalidcontentmodel-text": "Мадэль змесціва \"$1\" не падтрымліваецца.",
        "editpage-notsupportedcontentformat-title": "Фармат змесціва не падтрымліваецца",
        "editpage-notsupportedcontentformat-text": "Фармат змесціва $1 не падтрымліваецца мадэллю змесціва $2.",
        "content-model-wikitext": "вікі-тэкст",
        "uploaded-animate-svg": "У ўкладзеным SVG файле знойдзены тэг \"animate\", здольны змяніць спасылку з дапамогай атрыбута \"from\" <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-setting-event-handler-svg": "Устаноўка атрыбутаў апрацоўкі падзей заблакавана, у ўкладзеным SVG-файле знойдзены код <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-setting-href-svg": "Выкарыстанне тэга \"set\" для дадання атрыбута \"href\" у бацькоўскі элемент заблакавана.",
+       "uploaded-wrong-setting-svg": "Ужыванне тэга \"set\" для задання ў якасці мэты аддаленага адраса/звестак/сцэнарыя для любога атрыбута заблакавана. У ўкладзеным SVG-файле знойдзены <code>&lt;set to=\"$1\"&gt;</code>.",
        "uploadscriptednamespace": "Гэты файл SVG утрымлівае недапушчальную прастору імёнаў \"$1\".",
        "uploadinvalidxml": "Немагчыма прааналізаваць XML ва ўкладзеным файле.",
        "uploadvirus": "Файл утрымлівае вірус! Падрабязнасці: $1",
        "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-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": "Журнал аховы",
        "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]",
        "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 c8eb76b..e426cc6 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": "Домейн:",
        "tags-actions-header": "Действия",
        "tags-active-yes": "Да",
        "tags-active-no": "Не",
-       "tags-source-extension": "Ð\94еÑ\84иниÑ\80ан Ð¾Ñ\82 Ñ\80азÑ\88иÑ\80ение",
+       "tags-source-extension": "Ð\94еÑ\84иниÑ\80ан Ð¾Ñ\82 Ñ\81оÑ\84Ñ\82Ñ\83еÑ\80а",
        "tags-source-none": "Вече не се използва",
        "tags-edit": "редактиране",
        "tags-delete": "изтриване",
        "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-revision": "$1 {{GENDER:$2|промени}} видимостта на {{PLURAL:$5|една редакция|$5 редакции}} в страница $3: $4",
index 65e1021..d00c9d7 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": "হয় কোন বহিঃস্থ যাচাইকরণ ডাটাবেজ ত্রুটি ঘটেছে অথবা আপনার বহিঃস্থ অ্যাকাউন্ট হালনাগাদ করার অনুমতি নেই।",
        "botpasswords": "বট পাসওয়ার্ড",
        "botpasswords-disabled": "বট পাসওয়ার্ড নিষ্ক্রিয় করা।",
        "botpasswords-no-central-id": "বট পাসওয়ার্ড ব্যবহার করার জন্য, আপনাকে একটি কেন্দ্রীভূত অ্যাকাউন্টে প্রবেশ করতে হবে।",
+       "botpasswords-existing": "বিদ্যমান বট শব্দচাবি",
        "botpasswords-createnew": "একটি নতুন বট পাসওয়ার্ড তৈরি করুন",
+       "botpasswords-editexisting": "একটি বিদ্যমান বট শব্দচাবি পরিবর্তন করুন",
        "botpasswords-label-appid": "বটের নাম:",
        "botpasswords-label-create": "তৈরি করো",
        "botpasswords-label-update": "হালনাগাদ",
        "invalid-content-data": "ভুল কন্টেন্ট ডাটা",
        "content-not-allowed-here": "\"$1\" কন্টেন্টটি [[$2]] পাতায় অনুমোদিত নয়",
        "editwarning-warning": "এই পাতাটি ত্যাগ করলে আপনার আপনার করা পরিবর্তনগুলো হারিয়ে যেতে পারে।\nআপনি যদি প্রবেশ করা থাকেন, আপনি এই সতর্কীকরণ বার্তাটি আপনার পছন্দের \"সম্পাদনা\" অনুচ্ছেদ থেকে নিস্ক্রিয় করতে পারেন।",
+       "editpage-invalidcontentmodel-title": "বিষয়বস্তু মডেল সমর্থিত নয়",
        "editpage-notsupportedcontentformat-title": "উল্লেখিত পদ্ধতি সমর্থনযোগ্য নয়।",
        "editpage-notsupportedcontentformat-text": "$1 লেখার ফরম্যাট, $2 কন্টেন্ট মডেলের উপযোগী নয়।",
        "content-model-wikitext": "উইকিটেক্সট",
        "grant-editmycssjs": "আপনার সিএসএস/জাভাস্ক্রিপ্ট সম্পাদনা করুন",
        "grant-editmyoptions": "আপনার ব্যবহারকারী পছন্দসমূহ সম্পাদনা করুন",
        "grant-editmywatchlist": "আপনার নজরতালিকা সম্পাদনা করুন",
+       "grant-editpage": "বিদ্যমান পাতা সম্পাদনা করুন",
        "grant-editprotected": "সংরক্ষিত পাতা সম্পাদনা করুন",
        "grant-privateinfo": "ব্যক্তিগত তথ্যে প্রবেশাধিকার",
        "grant-sendemail": "অন্য ব্যবহারকারীকে ইমেইল পাঠান",
        "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",
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 1cb20f4..92c4cb3 100644 (file)
@@ -41,6 +41,7 @@
        "tog-watchdefault": "Dodaj stranice i datoteke koje uređujem na moj spisak praćenih članaka",
        "tog-watchmoves": "Dodaj stranice i datoteke koje premjestim na moj spisak praćenih članaka",
        "tog-watchdeletion": "Dodaj stranice i datoteke koje izbrišem na moj spisak praćenih članaka",
+       "tog-watchuploads": "Dodaj datoteke koje postavim na moj spisak praćenja",
        "tog-watchrollback": "Sve stranice za koje sam izvršio povrat izmjena stavi na spisak praćenja",
        "tog-minordefault": "Označi sve izmjene manjim isprva",
        "tog-previewontop": "Prikaži pregled iznad okvira za uređivanje",
        "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",
        "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",
index 9e9e6b6..d5dbe64 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",
        "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..8d380cd 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": "自動編號其標題",
@@ -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": "Hĭ-uông 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-talk": "Nô̤i-ṳ̀ng gì tō̤-lâung",
        "tooltip-ca-edit": "汝會使修改茲蜀頁。起動敆保存以前使預覽按鈕",
-       "tooltip-ca-addsection": "開始蜀萆新其部分",
+       "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-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-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-t-whatlinkshere": "鏈遘嚽塊其所有維基頁面其單單\nCuò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]",
        "file-nohires": "無更高決斷",
        "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 83a72b4..cf16049 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",
        "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",
index c6bd656..4272464 100644 (file)
        "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..a1035dc 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",
        "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",
index 8b3c0fe..81380ce 100644 (file)
        "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": "{{PLURAL:$2|Na kategoriye de $1 bınkategoriy estê.|$2 kategoriyan ra $1 kategoriyê bınêni asenê.}} \n{| border=\"1\" cellpadding=\"2\" cellspacing=\"0\" align=\"left\" style=\"margin-left:1em; background:peru; border: 1px #aaa solid; border-collapse: collapse; font-size: 95%;\"\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": "{{PLURAL:$2|Na kategoriye de teyna ena pele esta.|Ebe $2 ra pêro piya {{PLURAL:$1|ena pela na kategoriye dera|$1 enê peli na kategoriye derê.}}}}",
+       "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ê.",
        "about": "Heqa cı de",
        "article": "Pela zerreki",
        "newwindow": "(pençereyê newey de beno a)",
-       "cancel": "Bıtexelne",
+       "cancel": "Bıterkın",
        "moredotdotdot": "Vêşi...",
        "morenotlisted": "Vêşi lista nêbi...",
        "mypage": "Pele",
        "newpage": "Pela newiye",
        "talkpage": "Ena pele sero werêne",
        "talkpagelinktext": "werênayış",
-       "specialpage": "Pela xısusiye<br>Perra Bağsi",
+       "specialpage": "Perra bağsi",
        "personaltools": "Hacetê şexsiy",
        "articlepage": "Pera zerreki bıvin",
-       "talk": "Werênayış",
+       "talk": "Vaten",
        "views": "Asayışi",
        "toolbox": "Haceti",
        "userpage": "Pela karberi bıvêne",
        "resetpass_forbidden": "parolayi nêvuryayi",
        "resetpass-no-info": "şıma gani hesab akere u hona bıeşke bırese cı",
        "resetpass-submit-loggedin": "Parola bıvurne",
-       "resetpass-submit-cancel": "Bıterkın (Bıtexelne)",
+       "resetpass-submit-cancel": "Bıtexelne",
        "resetpass-wrong-oldpass": "parolayo parola maqbul niyo.\nşıma ya parolaye xo vurnayo ya zi parolayo muwaqqat waşto.",
        "resetpass-recycled": "Parolaya şımaya newiye wa paroloya şımaya verêne ra ferqıne bo.",
        "resetpass-temp-emailed": "E postaya rışyayê yubkoda şıma ronıştış akerdo.  Ronıştışi xo temammkerdışi rê yu parolaya newi lazım a",
        "summary": "Xulasa:",
        "subject": "Mewzu:",
        "minoredit": "Vurriyayışo werdiyo",
-       "watchthis": "Na pele seyr ke",
+       "watchthis": "Perrer bıpaw",
        "savearticle": "Qeyd ke",
        "savechanges": "Vurnayışan qeyd ke",
        "publishpage": "Perer bıhesırne",
        "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.",
        "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)",
        "prefs-labs": "Xacetê labs",
        "prefs-user-pages": "Pelê karberi",
        "prefs-personal": "Pela karberi",
-       "prefs-rc": "Vurriyayışê peyêni",
+       "prefs-rc": "Vırnayışë newey",
        "prefs-watchlist": "Lista seyrkerdışi",
        "prefs-editwatchlist": "Lista seyrkerdışi bıvurne",
        "prefs-editwatchlist-label": "Listey serkerdışanê cıkewtışi timar kerê",
        "nchanges": "$1 {{PLURAL:$1|vurnayış|vurnayışi}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ziyaretê peyêni ra nata}}",
        "enhancedrc-history": "tarix",
-       "recentchanges": "Vurriyayışê peyêni",
+       "recentchanges": "Vırnayışë newey",
        "recentchanges-legend": "Tercihê vurnayışanê peyênan",
        "recentchanges-summary": "Wiki sero vurriyayışê peyêni asenê.",
        "recentchanges-noresult": "Goreyê kriteranê kıfşkerdeyan ra qet yew vurnayış nêvêniya.",
        "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-show": "Bımusn",
+       "rcshowhideminor-show": "Bımocne",
        "rcshowhideminor-hide": "Bınımne",
        "rcshowhidebots": "botan $1",
        "rcshowhidebots-show": "Bımocne",
        "boteditletter": "b",
        "unpatrolledletter": "!",
        "number_of_watching_users_pageview": "[$1 ho seyr keno {{PLURAL:$1|karber|karberî}}]",
-       "rc_categories": "Pelê ke kategoriyan ra (be „|“ ciya biyê):",
+       "rc_categories": "Kategoriyan rêz kı ( \"|“ ya ciya yo):",
        "rc_categories_any": "Weçinayiyan ra her yew",
        "rc-change-size": "$1",
        "rc-change-size-new": "Vurnayışa dıma $1 {{PLURAL:$1|bayt|bayt}}",
        "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",
        "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ê",
        "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ê",
        "cant-move-user-page": "desturê şıma çino, şıma pelanê karberani bıkırışi (bê pelê cerıni).",
        "cant-move-to-user-page": "desturê şıma çino, şıma yew peli bıkırışi pelê yew karberi.",
        "newtitle": "Sernameyo newe:",
-       "move-watch": "Na pele/perr seyr ke",
+       "move-watch": "Pela çıme u meqsedi seyr ke",
        "movepagebtn": "Pele bere",
        "pagemovedsub": "Berdışi kerd temam",
        "movepage-moved": "'''\"$1\" berd \"$2\"'''",
        "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",
        "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}}",
index 7dd59f3..02def5d 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": "शीर्षकहरूलाई स्वत:अङ्कित गर",
@@ -46,7 +46,7 @@
        "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": "लुकाइएका श्रेणीहरू धेखाउन्या",
        "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|",
        "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": "पृष्ठहरू पढ",
        "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:|तमरा}} योगदानअनऐ सूची",
index 8eacae0..f43b50c 100644 (file)
@@ -33,7 +33,7 @@
        "tog-enotifminoredits": "Mândom un avîş ânca p'r al mudéfichi céchi ed pàgini e file",
        "tog-enotifrevealaddr": "Fà vèder al mé indirés ed la pôsta eletrônica int i mesâg 'd avîş",
        "tog-shownumberswatching": "Fà vèder al nómer ed j utèint che gh'àn la pàgina sòta uservasiòun",
-       "tog-oldsig": "La fîrma 'd adèsa",
+       "tog-oldsig": "La tó fîrma 'd adèsa",
        "tog-fancysig": "Trâta la fîrma cme wikitèst (sèinsa colegamèint avtomâtich)",
        "tog-uselivepreview": "Permèt la funsiòun \"Live preview\" (guêrda préma 'd salvêr in dirèta)",
        "tog-forceeditsummary": "Dmânda s'l'è vèira che al câmp argumèint l' é vōd",
@@ -48,7 +48,7 @@
        "tog-showhiddencats": "Fà vèder al categoréi lughêdi",
        "tog-norollbackdiff": "An fêr mia vèder al cunfrûnt tr' al versiòun dôp avèir fât un \"rollback\"",
        "tog-useeditwarning": "Avîşom quând a vâgh fōra da 'na pàgina d' mudéfica e an n'ò mìa salvê al mudéfichi fâti",
-       "tog-prefershttps": "Drōva sèimper un colegamèint sicûr quând ét fê l'ingrès",
+       "tog-prefershttps": "Drōva sèimper un colegamèint sicûr mèinter té coleghê.",
        "underline-always": "Sèimper",
        "underline-never": "Mài",
        "underline-default": "Mantî al j impustasiòun dal navigadōr o 'd la skin",
        "category-file-count-limited": "In cla categoréia ché a gh'é {{PLURAL:$1|al file nutê|i $1 file nutê}} ché 'd sègvit.",
        "listingcontinuesabbrev": "cunt.",
        "index-category": "Pàgini gancêdi",
-       "noindex-category": "Pàgini mìa gancêdi",
+       "noindex-category": "Pàgini mìa coleghêdi.",
        "broken-file-category": "Pàgini cun dèinter di file ch' an gh'în mìa.",
        "about": "Infumasiòun",
        "article": "Còl che gh'é int la pàgina",
        "newwindow": "(a s'arvés 'na fnèstra nōva)",
        "cancel": "Scanşèla",
        "moredotdotdot": "Êter...",
-       "morenotlisted": "Cl'elèinch ché an n'é mìa finî.",
+       "morenotlisted": "Cl'elèinch ché 'l pré èser mìa finî.",
        "mypage": "Pàgina",
        "mytalk": "Al mē discusiòun",
        "anontalk": "Discusiòun",
        "copyrightwarning": "Per piaşèir tîn cûnt che tót al colaborasiòun a {{SITENAME}} a vînen cunsidrêdi publichêdi sòta la licèinsa $2 (per i particulêr guêrda $1). S' an 't vō mìa che i tō tèst a pôsen èser cambiê e turnê a publichêr da tót sèinsa lémit, an publichêri mìa ché.<br /> In pió, se 't  i póblich ché, a 't dichiâr, sòta la tó responsabilitê, che còl ch' è stê scrét a 't l'ê scrét té personalmèint opór l'é ste cupiê da documèint sèinsa ch' al sìa quacê da nisûn dirét 'd autōr. <strong> Ché insém an pubblichêr mìa materiêl quacê da dirét 'd autōr sèinsa autorişâsiòun! </strong>",
        "copyrightwarning2": "Per piaşèir tîn cûnt che tót al colaborasiòun a {{SITENAME}} a pōlen èser mudifichê, arversê o scanşlê da êtra gînta cla dà 'na mân. S' an 't vō mìa che i tō tèst a pôsen èser cambiê alōra an publichêri mìa ché.<br />In pió, se 't  i póblich ché, a 't dichiâr, sòta la tó responsabilitê, che còl ch' è stê scrét a 't l'ê scrét té personalmèint opór l'é ste cupiê da documèint sèinsa ch' al sìa quacê da nisûn dirét 'd autōr (per i particulêr guêrda $1). <strong> Ché insém an pubblichêr mìa materiêl quacê da dirét 'd autōr sèinsa autorişâsiòun! </strong>",
        "longpageerror": "<strong> Erōr: al tèst spidî l'é lòngh {{PLURAL:$1|1|$1}} kilobyte, ch'l'é pió grôs ed l'amzûra mâsima permésa ({{PLURAL:$2|1|$2}} kilobyte). </strong> Al tèst al pôl mìa èser salvê.",
-       "readonlywarning": "<strong>Atensiòun: al databêş l'é stê bluchê per justadûri e dòunca l'é impusébil salvê al mudéfichi in cól mumèint ché.</strong> P'r an pêrdi mìa, l'é pusébil cupiêri còl ch' é stê més dèinter fîn a dès int la caşèla di cambiamèint, incolêrel in un prugrâma tèst e salvêrel per spetêr al şblôch dal databêş. \n\nL'aministradōr ch' l'à bluchê al databêş l'à dê cla spiegasiòun ché: $1",
+       "readonlywarning": "<strong>Atensiòun</strong>: al databêş l'é l'é bluchê per justadûri e per adès l'é impusébil salvê al mudéfichi fâti. P'r an pêrdi mìa,  côpi in un file tèst e sêlvel mèinter té spèt al şblôch dal databêş. \n\nL'aministradōr ch' l'à bluchê al databêş l'à dê cla spiegasiòun ché: $1",
        "protectedpagewarning": "<strong> Atensiòun: cla pàgina ché l'é stêda bluchêda in môd che sōl j utèint cun i privilèg 'd aministradōr a pôsen cambiêrla.</strong> \nPer infurmasiòun ché 'd sègvit a vîn scrét l'ûltem elemèint dal regéster:",
        "semiprotectedpagewarning": "<strong>Nôta:</strong> cla pàgina ché l'é stêda bluchê in môd che sōl j utèint registrê a pôsen cambiêrla. \nPer infurmasiòun ché 'd sègvit è scrét l'ûltem elemèint dal regéster.",
        "cascadeprotectedwarning": "<strong> Atensiòun: </strong>cla pàgina ché l'é stêda bluchêda in môd che sōl j utèint cun i privilèg 'd aministradō a pôsen cambiêrla. Còst a sucēd perchè la pàgina l'é dèinter in cl'elèinch sòta protesiòun ché 'd sègvit {{PLURAL:$1|pàgina|pàgini}}",
        "revdelete-unsuppress": "Tó via i lémit al revisiòun armési",
        "revdelete-log": "Mutîv:",
        "revdelete-submit": "Drōva {{PLURAL:$1|int la revisiòun sernîda|int al revisiòun sernîdi}}",
-       "revdelete-success": "Vésta ed la revisòun arnuvêda int al môd gióst.",
+       "revdelete-success": "Vésta ed la revisòun arnuvêda.",
        "revdelete-failure": "La vésta 'd la versiòun l'an pōl mìa èser arnuvêda:\n$1",
-       "logdelete-success": "Vésta dal fât impustêda int al môd gióst.",
+       "logdelete-success": "Vésta impustêda dal fât .",
        "logdelete-failure": "La vésta dal fât l'an pōl mìa èser impustêda:\n$1",
        "revdel-restore": "Câmbia la vidûda.",
        "pagehist": "Stòria 'd la pàgina",
        "prefs-watchlist-token": "Token tgnî d'ôc specêl:",
        "prefs-misc": "Divêrs",
        "prefs-resetpass": "Câmbia la cêva 'd ingrès",
-       "prefs-changeemail": "Câmbia l'indirés ed la pôsta eletrônica",
+       "prefs-changeemail": "Câmbia o scanşèla l'indirés ed la pôsta eletrônica",
        "prefs-setemail": "Impôsta un indirés ed pôsta eletrônica",
        "prefs-email": "Siēlta pôsta eletrônica",
        "prefs-rendering": "Aspèt",
        "rows": "Rîghi",
        "columns": "Clòuni:",
        "searchresultshead": "Sērca",
-       "stub-threshold": "Valōr ménim per i <a href=\"#\" class=\"stub\">colegamèint a i stub</a>, in byte:",
+       "stub-threshold": "Pôrta per i colegamèint a j abòs ($1):",
        "stub-threshold-disabled": "Bluchê",
        "recentchangesdays": "Nómer di dé da fêr vèder int al j ûltmi mudéfichi:",
        "recentchangesdays-max": "Mâsim $1 {{PLURAL:$1|dé}}",
        "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ó",
index fc5871b..b3781c2 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",
        "tooltip-watchlistedit-raw-submit": "Update watchlist",
        "tooltip-recreate": "Recreate the page even though it has been deleted",
        "tooltip-upload": "Start upload",
-       "tooltip-rollback": "\"Rollback\" reverts edit(s) to this page of the last contributor in one click",
+       "tooltip-rollback": "\"Rollback\" reverts the last contributor's edit(s) to this page in one click",
        "tooltip-undo": "\"Undo\" reverts this edit and opens the edit form in preview mode. It allows adding a reason in the summary.",
        "tooltip-preferences-save": "Save preferences",
        "tooltip-summary": "Enter a short summary",
        "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",
index f0a61b3..7d9c922 100644 (file)
        "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:",
        "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",
        "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",
index 15f5303..814fcf8 100644 (file)
        "tog-enotifminoredits": "Notificarme también por correo electrónico los cambios menores de las páginas y archivos",
        "tog-enotifrevealaddr": "Revelar mi dirección de correo electrónico en los correos de notificación",
        "tog-shownumberswatching": "Mostrar el número de usuarios que la vigilan",
-       "tog-oldsig": "Firma actual:",
+       "tog-oldsig": "Tu firma actual:",
        "tog-fancysig": "Tratar la firma como wikitexto (sin un enlace automático)",
        "tog-uselivepreview": "Usar previsualización dinámica",
        "tog-forceeditsummary": "Avisarme cuando deje en blanco el resumen de la edición",
        "newwindow": "(se abre en una ventana nueva)",
        "cancel": "Cancelar",
        "moredotdotdot": "Más...",
-       "morenotlisted": "Esta lista no está completa.",
+       "morenotlisted": "Esta lista puede estar incompleta.",
        "mypage": "Página",
        "mytalk": "Discusión",
        "anontalk": "Discusión",
        "accmailtext": "Se ha enviado a $2 una contraseña generada aleatoriamente para [[User talk:$1|$1]]. Una vez iniciada la sesión, se puede cambiar en la página [[Special:ChangePassword|destinada para ello]].",
        "newarticle": "(Nuevo)",
        "newarticletext": "Has seguido un enlace a una página que aún no existe.\nPara crear esta página, escribe en el cuadro que aparece a continuación. Para más información, consulta la [$1 página de ayuda].\nSi llegaste aquí por error, vuelve a la página anterior.",
-       "anontalkpagetext": "----\n<em>Esta es la página de discusión de un usuario anónimo que aún no ha creado una cuenta, o no la usa.</em>\nPor lo tanto, tenemos que usar su dirección IP para identificarlo.\nPuede que varios usuarios compartan una misma dirección IP.\nSi eres un usuario anónimo y crees que se han dirigido a ti con comentarios improcedentes, [[Special:CreateAccount|crea una cuenta]] o [[Special:UserLogin|inicia sessión]] para evitar confusiones futuras con otros usuarios anónimos.",
+       "anontalkpagetext": "----\n<em>Esta es la página de discusión de un usuario anónimo que aún no ha creado una cuenta, o no la usa.</em>\nPor lo tanto, tenemos que usar su dirección IP para identificarlo.\nPuede que varios usuarios compartan una misma dirección IP.\nSi eres un usuario anónimo y crees que se han dirigido a ti con comentarios improcedentes, [[Special:CreateAccount|crea una cuenta]] o [[Special:UserLogin|inicia sesión]] para evitar confusiones futuras con otros usuarios anónimos.",
        "noarticletext": "En este momento no hay texto en esta página.\nPuedes [[Special:Search/{{PAGENAME}}|buscar el título de esta página]] en otras páginas,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar en los registros relacionados],\no [{{fullurl:{{FULLPAGENAME}}|action=edit}} crear esta página]</span>.",
        "noarticletext-nopermission": "Actualmente no hay texto en esta página.\nPuedes [[Special:Search/{{PAGENAME}}|buscar este título de página]] en otras páginas, o <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} buscar en los registros relacionados]</span>, pero no tienes permiso para crear esta página.",
        "missing-revision": "La revisión n.º $1 de la página «{{FULLPAGENAME}}» no existe.\n\nEsto suele ocurrir cuando se sigue un enlace de historial obsoleto que apunta a una página ya borrada.\nPuedes encontrar detalles en el [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro de borrados].",
        "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]",
index d544a4e..f7a01e8 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",
        "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",
        "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..476e3f1 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",
        "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}}",
        "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:",
        "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-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",
        "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..863e3d7 100644 (file)
        "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",
index 21f2e7c..f07e45c 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",
        "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",
index 3dc1e86..ab12a38 100644 (file)
@@ -51,7 +51,7 @@
        "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-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": "Sinatura actual:",
+       "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",
@@ -68,7 +68,7 @@
        "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 unha conexión segura sempre que acceda ao sistema",
+       "tog-prefershttps": "Utilizar sempre unha conexión segura mentres acceda ao sistema",
        "underline-always": "Sempre",
        "underline-never": "Nunca",
        "underline-default": "Opción predeterminada da aparencia ou do navegador",
        "newwindow": "(abre unha ventá nova)",
        "cancel": "Cancelar",
        "moredotdotdot": "Máis...",
-       "morenotlisted": "Esta lista non está completa.",
+       "morenotlisted": "Esta lista pode estar incompleta.",
        "mypage": "Páxina",
        "mytalk": "Conversa",
        "anontalk": "Conversa",
        "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, rexistra isto para referencia futura.</em><br/>(Para bots vellos que requiren que o nome de acceso sexa o mesmo que o nome de usuario eventual, podes usar tamén <strong>$3</strong> como nome de usuario e <strong>$4</strong>  como contrasinal.)",
+       "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-no-provider": "BotPasswordsSessionProvider non está dispoñible.",
        "botpasswords-restriction-failed": "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\").",
        "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",
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 d6fedba..adaaab4 100644 (file)
        "minoredit": "આ એક નાનો સુધારો છે",
        "watchthis": "આ પાનાને ધ્યાનમાં રાખો",
        "savearticle": "પાનું સાચવો",
+       "savechanges": "પરિવર્તન સાચવો",
        "publishpage": "પાનું પ્રકાશિત કરો",
        "publishchanges": "ફેરફારો પ્રકાશિત કરો",
        "preview": "પૂર્વાવલોકન",
        "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]]}} વડે કરેલ છેલ્લા પુનરાવર્તન પર પાછા લઇ જવાયું.",
        "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ફરી પ્રયત્ન કરો",
        "svg-long-desc": "SVG ફાઇલ, માત્ર $1 × $2 પીક્સલ, ફાઇલનું કદ: $3",
        "svg-long-error": "અયોગ્ય SVG ફાઇલ: $1",
        "show-big-image": "મૂળભુત ફાઇલ",
-       "show-big-image-preview": "àª\86 àª®àª¹àª¾àªµàª°àª¾ àª¦à«\8dરશà«\8dયનà«\81àª\82 àª®àª¾àªª: $1.",
+       "show-big-image-preview": "àª\86 àª\9dલàª\95àª\9aિતà«\8dરનà«\81àª\82 àª\95દ: $1.",
        "show-big-image-other": "અન્ય {{PLURAL:$2|આવર્તન|આવર્તનો}}: $1.",
        "show-big-image-size": "$1 × $2 પિક્સેલ",
        "file-info-gif-looped": "આવર્તન  (લુપ)",
        "htmlform-select-badoption": "તમે બતાવેલ વિકલ્પ અવૈધ છે",
        "htmlform-int-invalid": "તમે લખેલ વિકલ્પ અંક નથી",
        "htmlform-float-invalid": "તમે લખેલ વિકલ્પ અંક નથી",
-       "htmlform-int-toolow": "$1 આ કિંમત આ ન્યૂનતમ કિંમત છે",
+       "htmlform-int-toolow": "મુલ્ય જે તમે દર્શાવ્યું છે તે ન્યૂનતમ મુલ્ય $1 કરતાં ઓછું છે.",
        "htmlform-int-toohigh": "તમે પુરી પાડેલ માહિતી મહત્તમ $1થી વધુ છે",
        "htmlform-required": "આ કિઁમત જોઈએ છે",
        "htmlform-submit": "જમા કરો",
        "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 e335b9a..113821b 100644 (file)
@@ -61,9 +61,9 @@
        "tog-enotifwatchlistpages": "לשלוח אליי דוא\"ל כאשר משתנה דף או קובץ ברשימת המעקב שלי",
        "tog-enotifusertalkpages": "לשלוח אליי דוא\"ל כאשר נעשה שינוי בדף שיחת המשתמש שלי",
        "tog-enotifminoredits": "לשלוח אליי דוא\"ל גם על עריכות משניות בדפים וקבצים",
-       "tog-enotifrevealaddr": "×\97ש×\99פת כתובת הדוא\"ל שלי בהתראות דוא\"ל",
+       "tog-enotifrevealaddr": "×\9c×\97ש×\95×£ ×\90ת כתובת הדוא\"ל שלי בהתראות דוא\"ל",
        "tog-shownumberswatching": "הצגת מספר המשתמשים העוקבים",
-       "tog-oldsig": "החתימה הנוכחית:",
+       "tog-oldsig": "החתימה הנוכחית שלך:",
        "tog-fancysig": "התייחסות לחתימה כקוד ויקי (ללא קישור אוטומטי)",
        "tog-uselivepreview": "שימוש בתצוגה מקדימה מהירה",
        "tog-forceeditsummary": "הצגת אזהרה בעת הכנסת תקציר עריכה ריק",
        "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ות מחוקות}}",
+       "viewdeleted_short": "×\94צ×\92ת {{PLURAL:$1|×\92רס×\94 ×\9e×\97×\95ק×\94 ×\90×\97ת|$1 ×\92רס×\90ות מחוקות}}",
        "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).",
        "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ייתכן שהוא כבר נמחק על־ידי משתמש אחר.",
        "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-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 ef1b90a..850fdba 100644 (file)
        "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",
        "cannotloginnow-title": "Prijava trenutno nije moguća.",
        "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",
        "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",
        "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",
index d8780e1..1c828d2 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",
        "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",
        "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.",
        "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",
index fa83ac9..3fc5dfe 100644 (file)
        "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",
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 636cf1d..c4b08ba 100644 (file)
                        "Urielejh",
                        "Matma Rex",
                        "Matteocng",
-                       "Einreiher"
+                       "Einreiher",
+                       "Anto",
+                       "Saracrovetto",
+                       "Tosky"
                ]
        },
        "tog-underline": "Sottolinea i collegamenti:",
        "tog-enotifminoredits": "Inviami una email anche per le modifiche minori di pagine e file",
        "tog-enotifrevealaddr": "Mostra il mio indirizzo nelle e-mail di notifica",
        "tog-shownumberswatching": "Mostra il numero di utenti che hanno la pagina in osservazione",
-       "tog-oldsig": "Firma attuale:",
+       "tog-oldsig": "La tua firma attuale:",
        "tog-fancysig": "Gestisci la firma come wikitesto (senza collegamento automatico)",
        "tog-uselivepreview": "Abilita la funzione ''Live preview'' (anteprima in diretta)",
        "tog-forceeditsummary": "Chiedi conferma se il campo oggetto è vuoto",
        "newwindow": "(si apre in una nuova finestra)",
        "cancel": "Annulla",
        "moredotdotdot": "Altro...",
-       "morenotlisted": "Questo elenco non è completo.",
+       "morenotlisted": "Questo elenco potrebbe essere incompleto.",
        "mypage": "Pagina",
        "mytalk": "discussioni",
        "anontalk": "discussioni",
        "talk": "Discussione",
        "views": "Visite",
        "toolbox": "Strumenti",
+       "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": "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",
+       "cannotcreateaccount-text": "La creazione diretta dell'utenza non è attivata su questo wiki.",
        "yourdomainname": "Specificare il dominio",
        "password-change-forbidden": "Non è possibile modificare le password su questo wiki.",
        "externaldberror": "Si è verificato un errore con il server di autenticazione esterno, oppure non si dispone delle autorizzazioni necessarie per aggiornare il proprio accesso esterno.",
        "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:",
        "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",
index 195cf81..50acfa0 100644 (file)
        "infiniteblock": "salawasé",
        "expiringblock": "kadaluwarsa ing $1, $2",
        "anononlyblock": "namung anon",
-       "noautoblockblock": "pamblokiran otomatis dipatèni",
+       "noautoblockblock": "blokir otomatis dipatèni",
        "createaccountblock": "ndamelipun akun dipunblokir",
        "emailblock": "layang e-mail diblokir",
        "blocklist-nousertalk": "ora kena mbesut kaca guneman dhéwé",
-       "ipblocklist-empty": "Daftar pamblokiran kosong.",
+       "ipblocklist-empty": "Pratélan blokir kosong.",
        "ipblocklist-no-results": "alamat IP utawa panganggo sing disuwun ora diblokir.",
        "blocklink": "palang",
        "unblocklink": "jabel blokir",
        "contribslink": "sumbangan",
        "emaillink": "kirim layang èlèktronik",
        "autoblocker": "Panjenengan otomatis dipun-blok amargi nganggé alamat protokol internet (IP) ingkang sami kaliyan \"[[User:$1|$1]]\". Alesanipun $1 dipun blok inggih punika \"'''$2'''\"",
-       "blocklogpage": "Log pamblokiran",
+       "blocklogpage": "Log blokir",
        "blocklog-showlog": "Panganggo iki wis tau diblokir sakdurungé.\nLog blokiran sumadhiya nèng ngisor kanggo rujukan:",
        "blocklog-showsuppresslog": "Panganggo iki wis tau diblokir lan didhelikaké sakdurungé.\nLog brèdèlan sumadhiya nèng ngisor kanggo rujukan:",
        "blocklogentry": "mblokir [[$1]] kanthi wektu kadaluwarsa $2 $3",
        "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",
index e9e2f92..a105962 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": "ელ. ფოსტის მისამართი (არასავალდებულო)",
        "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 5eda368..1580a47 100644 (file)
@@ -7,7 +7,8 @@
                        "Mirzali",
                        "아라",
                        "Macofe",
-                       "Kumkumuk"
+                       "Kumkumuk",
+                       "Asmen"
                ]
        },
        "tog-underline": "Bınê gırey de xete bonce:",
        "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ê.}}}}",
        "specialpage": "Pela xısusiye",
        "personaltools": "Hacetê keşi",
        "articlepage": "Pela zerreki bıvêne",
-       "talk": "Hurênais",
+       "talk": "Hurênayis",
        "views": "Asaişi",
        "toolbox": "Qutiya hacetu",
        "userpage": "Pela karberi bıvêne",
index 08b84e8..6d3296c 100644 (file)
        "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": "Түзету мәтінін жасыр",
        "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 aa90652..922b555 100644 (file)
        "tog-showhiddencats": "숨은 분류 보이기",
        "tog-norollbackdiff": "되돌리기 후 차이를 보지 않기",
        "tog-useeditwarning": "바꾼 내용을 저장하지 않고 편집 페이지를 벗어날 때 내게 경고하기",
-       "tog-prefershttps": "ë¡\9cê·¸ì\9d¸í\95  ë\95\8c 항상 보안 연결 사용",
+       "tog-prefershttps": "ë¡\9cê·¸ì\9d¸í\95\98ë\8a\94 ë\8f\99ì\95\88 항상 보안 연결 사용",
        "underline-always": "항상",
        "underline-never": "항상 치지 않기",
        "underline-default": "스킨 또는 브라우저 기본값",
        "newwindow": "(새 창에서 열림)",
        "cancel": "취소",
        "moredotdotdot": "더 보기...",
-       "morenotlisted": "ì\9d´ ëª©ë¡\9dì\9d\80 ì\99\84ì\84±ë\90\98ì§\80 ì\95\8aì\95\98습니다.",
+       "morenotlisted": "ì\9d´ ëª©ë¡\9dì\9d\80 ì\99\84ì \84í\95\98ì§\80 ì\95\8aì\9d\84 ì\88\98 ì\9e\88습니다.",
        "mypage": "문서",
        "mytalk": "토론",
        "anontalk": "토론",
        "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",
index f0aca89..08c5a65 100644 (file)
@@ -45,7 +45,7 @@
        "tog-enotifminoredits": "Schéckt mir och bei klengen Ännerungen op vu mir iwwerwaachte Säiten oder Fichieren eng E-Mail.",
        "tog-enotifrevealaddr": "Meng E-Mail-Adress an de Benoriichtegungsmaile weisen.",
        "tog-shownumberswatching": "D'Zuel vun de Benotzer déi dës Säit iwwerwaache weisen",
-       "tog-oldsig": "Aktuell Ënnerschrëft:",
+       "tog-oldsig": "Är Aktuell Ënnerschrëft:",
        "tog-fancysig": "Ënnerschrëft als Wiki-Text behandelen (Ouni automatesche Link)",
        "tog-uselivepreview": "Live-Preview benotzen",
        "tog-forceeditsummary": "Warnen, wa beim Späicheren de Resumé feelt",
@@ -62,7 +62,7 @@
        "tog-showhiddencats": "Verstoppt Kategorië weisen",
        "tog-norollbackdiff": "Ënnerscheed nom Zrécksetzen net weisen",
        "tog-useeditwarning": "Mech warne wann ech d'Ännerung vun enger Säit verloossen, ouni Ännerunge gespäichert ze hunn",
-       "tog-prefershttps": "Ëmmer eng sécher Verbindung benotze wann ageloggt",
+       "tog-prefershttps": "Benotzt ëmmer eng sécher Verbindung wann ageloggt",
        "underline-always": "Ëmmer",
        "underline-never": "Ni",
        "underline-default": "Skin oder Standard vum Browser",
        "newwindow": "(geet an enger neier Fënster op)",
        "cancel": "Zréck",
        "moredotdotdot": "Méi …",
-       "morenotlisted": "Dës Lëscht ass net komplett.",
+       "morenotlisted": "Dës Lëscht ass eventuell net komplett.",
        "mypage": "Säit",
        "mytalk": "Diskussioun",
        "anontalk": "Diskussioun",
        "botpasswords-updated-body": "D'Botpasswuert fir de Bot-Numm \"$1\" vum Benotzer ''$2'' gouf 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>",
+       "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-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",
        "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}}",
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..8644764 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",
        "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",
        "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",
index e528dde..380e2b3 100644 (file)
@@ -51,7 +51,7 @@
        "tog-enotifminoredits": "Paziņot pa e-pastu arī par maznozīmīgiem labojumiem rakstos un failos",
        "tog-enotifrevealaddr": "Atklāt manu e-pasta adresi paziņojumu vēstulēs",
        "tog-shownumberswatching": "Rādīt uzraudzītāju skaitu",
-       "tog-oldsig": "Pašreizējais paraksts:",
+       "tog-oldsig": "Esošais paraksts:",
        "tog-fancysig": "Vienkāršs paraksts (bez automātiskās saites)",
        "tog-uselivepreview": "Lietot tūlītējo priekšskatījumu",
        "tog-forceeditsummary": "Atgādināt man, ja kopsavilkuma ailīte ir tukša",
        "newwindow": "(atveras jaunā logā)",
        "cancel": "Atcelt",
        "moredotdotdot": "Vairāk...",
-       "morenotlisted": "Šis saraksts nav pilnīgs.",
+       "morenotlisted": "Šis saraksts var nebūt pilnīgs.",
        "mypage": "Lapa",
        "mytalk": "Diskusijas",
        "anontalk": "Diskusijas",
        "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",
index d9d8ea0..8f29932 100644 (file)
        "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-patrol": "दोसराक सम्पादनकेँ संचालित देखाउ",
        "right-autopatrol": "अपन सम्पादनकेँ स्वचालित रूपेँ संचालित देखाउ",
        "right-patrolmarks": "हालक परिवर्तनमे संचालन चेन्ह देखू",
-       "right-unwatchedpages": "बिना à¤¸à¤\82à¤\9aालित à¤ªà¤¨à¥\8dना à¤¸à¤­à¤\95 à¤¸à¥\82à¤\9aà¥\80à¤\95à¥\87à¤\81 à¤¦à¥\87à¤\96à¥\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": "पन्नाक इतिहास सभकेँ मिज्झर करू",
        "right-userrights": "सभटा प्रयोक्ता अधिकारकेँ सम्पादित करू",
        "right-userrights-interwiki": "दोसर विकीपर प्रयोक्ताक प्रयोक्ता अधिकारक सम्पादन करी",
        "action-createaccount": "ई प्रयोक्ता खाता बनाबी",
        "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": "ई संचिकाकेँ साझी बखारीमे नजरि नै दिअ",
        "statistics-users": "पञ्जीकृत [[Special:ListUsers|प्रयोक्ता]]",
        "statistics-users-active": "सक्रिय प्रयोक्ता",
        "statistics-users-active-desc": "प्रयोक्ता जे अन्तिम {{PLURAL:$1|दिन|$1 दिन}} मे कोनो काज केने छथि",
-       "pageswithprop-submit": "à¤\9cाà¤\8a",
+       "pageswithprop-submit": "à¤\9cाà¤\8f",
        "doubleredirects": "द्वितीयक लागएबला बदलेन",
        "doubleredirectstext": "ई पन्ना ओइ पन्ना सभक संकलन छी जे बदलेन करैए दोसर बदलेनबला पन्नासँ।\nप्रत्येक पाँती पहिल आ दोसर बदलेनक लागि रखने अछि आ संगे दोसर बदलेनक लक्ष्य सेहो, जे वास्तवमे \"वास्तव\" लक्ष्य पन्ना अछि, जकरापर पहिल बदलेनकेँ जेबाक चाही। \n <del>Crossed out</del> प्रविष्टिक हल भेटल अछि।",
        "double-redirect-fixed-move": "[[$1]] घसकाएल गेल।\nई आब [[$2]] दिस जा रहल अछि।",
        "newpages-username": "प्रयोक्तानाम:",
        "ancientpages": "सभसँ पुरान पन्नासभ",
        "move": "स्थानान्तरण",
-       "movethispage": "à¤\88 à¤ªà¥\83षà¥\8dठà¤\95à¥\87 à¤\98सà¤\95ाबी",
+       "movethispage": "पà¥\83षà¥\8dठà¤\95 à¤¨à¤¾à¤® à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\95री",
        "unusedimagestext": "ई सभ संचिका अछि मुदा कोनो पन्नामे निवशित नै अछि।\nकृपा कऽ ई बुझू जे दोसर जालस्थल सभ सोझ सार्वत्रिक विभव संकेतबला कोनो संचिकासँ लागि बना सकैए, आ तँए सरिय प्रयोगक बादो अखनो एतए सूचित कएल जा सकैए।",
        "unusedcategoriestext": "ई संवर्ग पन्ना सभ अछि, ओना कोनो दोसर पन्ना वा संवर्ग ओकर प्रयोग करैत अछि।",
        "notargettitle": "बिन लक्ष्यक",
        "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}}''''' क्लिक करू छाँटल मौलिक आकारमे अनबा लेल, संशोधन सभकेँ अनबा लेल सम्बन्धित बक्सा सभमे चेन्ह लगाउ।",
        "cannotundelete": "फेरसँ नै आबि सकल:\n$",
        "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-title": "मà¥\87à¤\9fाà¤\8fल à¤\97à¥\87ल à¤ªà¥\83षà¥\8dठ à¤¤à¤¾à¤\95à¥\80",
        "undelete-search-box": "मेटाएल पन्ना सभकेँ ताकू",
        "undelete-search-prefix": "से शुरु भेल पन्ना देखाबू.",
        "undelete-search-submit": "ताकू",
        "lockfilenotwritable": "दत्तांशनिधि प्रतिबन्ध संचिका लिखबा योग्य नै अछि।\nदत्तांशनिधिकेँ प्रतिबन्धित वा अप्रतिबन्धित करबा लेल एकरा जाल वितरक द्वारा लिखबा योग्य हेबाक चाही।",
        "databasenotlocked": "दत्तांशनिधि प्रतिबन्धित नै अछि।",
        "lockedbyandtime": "(द्वारा {{GENDER:$1|$1}} केँ $2 बजे $3)",
-       "move-page": "$1हटाउ",
-       "move-page-legend": "पनà¥\8dना à¤\98सà¤\95ाà¤\89",
+       "move-page": "$1 स्थानान्तरित करी",
+       "move-page-legend": "पà¥\83षà¥\8dठ à¤¸à¥\8dथानानà¥\8dतरण",
        "movepagetext": "नीचाँक फॉर्मक प्रयोग पन्नाक नाम बदलि देत, एकर सभटा इतिहासकेँ नव नामक अन्तर्गत राखि देत।\nपुरान शीर्षक नव पन्ना लेल एकटा घुरबैबला पन्ना बनि जाएत।\nअहाँ घुरबैबला पन्नाकेँ अद्यतन कऽ सकै छी जे मूल शीर्षकपर स्वचालित रूपेँ जाइत अछि।\nजौं अहाँ ई नै करबाक निर्णय करै छी, निश्चय करू तकबा लेल [[Special:DoubleRedirects|double]] वा\n[[Special:BrokenRedirects|broken redirects]]\nअहाँ ऐ लेल जिम्मीदार छी जे सम्बन्धित लिंक ओतै जाए जतए ओकरा जेबाक चाही।\n\nमोन राखू कि पन्ना '''नै''' घसकाउ जौं नव शीर्षकपर पहिनहियेसँ पन्ना अछि, आ तखने ई करू जखन ओ खाली हुअए वा ओ एकटा घुमबैबला पन्ना हुअए वा ओइ पन्नाक कोनो भूतकालक सम्पादन इतिहास नै हुअए।\nएकर माने भेल जे अहाँ कोनो पन्नाक नाम परिवर्तन कऽ पाछाँ लऽ जा सकै छी जतए एकर नाममे परिवर्तन कएल गेल रहए जौं अहाँसँ गलती भेल अछि, आ अहाँ ओइ पन्नाकेँ फेरसँ दोबारा नै लिख सकै छी।\n\n\n'''चेतौनी!'''\nई एकटा लोकप्रिय पन्नाक लेल एकटा भयंकर आ बिना आशाक कएल परिवर्तन भऽ सकैए।\nआगाँ बढ़ैसँ पहिने अहाँ ई सुनिश्चित करू जे अहाँ एकर परिणाम बुझै छी।",
        "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ताइ परिस्थितिमे, अहाँकेँ अपनेसँ पन्नाकेँ, आवश्यकतानुसार, घसकाबऽ वा मिज्झर करऽ पड़त।",
        "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ाबी",
+       "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 पहिनहियेसँ अछि आ स्वचालित रूपेँ मेटाएल नै जा सकैए।",
        "imagetypemismatch": "नव संचिका विस्तारक अपन प्रकारसँ मेल नै खाइए।",
        "imageinvalidfilename": "लक्ष्यित संचिकाक नाम अवैध अछि",
        "fix-double-redirects": "मूल शीर्षक धरि जाहि बला सभटा पुनर्निर्देशनों के सेहो बदलु.",
-       "move-leave-redirect": "एकटा बदलेन के पांछा छोडि के जाऊ",
+       "move-leave-redirect": "एक पुनर्निर्देशन पाछा छोडी",
        "protectedpagemovewarning": "''' चेतौनी: ई पन्ना संरक्षित अछि से खाली संचालन अधिकारयुक्त प्रयोक्ता एकरा घुसका सकैत छथि।'''\nनव वृतलेख उल्लेख नीचाँ सन्दर्भ लेल देल जा रहल अछि:",
        "semiprotectedpagemovewarning": "'''नोट:''' ई पन्ना संरक्षित अछि से खाली पंजीकृत प्रयोक्ता एकरा घुसका सकैत छथि।\nनव वृतलेख उल्लेख नीचाँ सन्दर्भ लेल देल जा रहल अछि:",
        "move-over-sharedrepo": "[[:$1]] अछि एकटा साझी बखारीमे। कोनो संचिकाकेँ ऐ नामसँ अनलापर साझीबला एकटा संचिका मेटा जाएत।",
        "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' संग नब पृष्ठ बनाओल गेल",
        "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": "à¤\9cाà¤\8a",
+       "redirect-submit": "à¤\9cाà¤\8f",
        "redirect-lookup": "ताकू:",
        "redirect-value": "मूल्य:",
        "redirect-user": "प्रयोक्ता आई॰डी॰",
        "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-revision": "$1 द्वारा $3 पृष्ठक {{PLURAL:$5|एक अवतरण|$5 अवतरणसभ}}क दृश्यता {{GENDER:$2|परिवर्तित}}: $4",
        "logentry-delete-event-legacy": "$1 {{GENDER:$2|changed}}  $3 पर वृत्तलेख दृश्य",
        "logentry-delete-revision-legacy": "$1 {{GENDER:$2|changed}}  $3 पर वृत्तलेख संशोधन",
        "logentry-suppress-delete": "$1 {{GENDER:$2|दबाएल}} page $3",
        "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|बनाएल}} एकटा प्रयोक्ता खाता",
index 7f522e6..199eb7c 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": "Преглед на страницата на податотеката",
        "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 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 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 0eb17e9..dc3b4ff 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",
        "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.",
        "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\").",
        "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",
        "prefs-help-prefershttps": "Denne preferansen vil virke etter neste innlogging.",
        "prefswarning-warning": "Du har gjort endringer i dine innstillinger som ikke er lagret ennå.\nDersom du forlater denne siden utenk å klikke på \"$1\" blir ikke innstillingene dine oppdatert.",
        "prefs-tabs-navigation-hint": "Tips: Du kan bruke venstre- og høyrepiltastene for å navigere mellom fanene i fanelisten",
-       "userrights": "Rettighets&shy;kontroll",
+       "userrights": "Bruker&shy;rettighets&shy;kontroll",
        "userrights-lookup-user": "Ordne brukergrupper",
        "userrights-user-editname": "Fyll inn et brukernavn:",
        "editusergroup": "Endre {{GENDER:$1|brukergrupper}}",
        "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-title": "Bidrag av nye kontoer",
        "sp-contributions-blocklog": "blokkeringslogg",
        "sp-contributions-suppresslog": "undertrykte brukerbidrag",
-       "sp-contributions-deleted": "slettede 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-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-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-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-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-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.",
+       "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."
 }
index fd3ccea..c30c40c 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": "वार्ता",
        "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/ ट्रान्सलेटविकि.नेट]को प्रयोग गर्नुहोस।",
        "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": "प्रयोगकर्ताको अधिकार परिवर्तनमा मतभेद भयो ! कृपया तपाईंको परिवर्तन पुनरावलोकन तथा पुष्टि गर्नुहोस् ।",
        "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",
index 442c685..e9cfa37 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",
        "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.",
        "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-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.",
        "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",
        "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",
        "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",
        "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..f9dc952 100644 (file)
        "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 c66b261..0953ab2 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",
        "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",
        "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",
        "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-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",
        "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.",
index 5dac084..3d38947 100644 (file)
@@ -99,7 +99,8 @@
                        "Anderson Costa",
                        "LucyDiniz",
                        "Tusca",
-                       "Cristofer Alves"
+                       "Cristofer Alves",
+                       "Tark"
                ]
        },
        "tog-underline": "Sublinhar links:",
        "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 3c52e12..00896dc 100644 (file)
        "tog-enotifminoredits": "Notificar-me por correio electrónico também sobre edições menores de páginas ou ficheiros",
        "tog-enotifrevealaddr": "Revelar o meu endereço de correio electrónico nas notificações",
        "tog-shownumberswatching": "Mostrar o número de utilizadores a vigiar",
-       "tog-oldsig": "Assinatura atual:",
+       "tog-oldsig": "A sua assinatura atual:",
        "tog-fancysig": "Tratar assinatura como texto wiki (sem hiperligações automáticas)",
        "tog-uselivepreview": "Usar a antevisão ao vivo",
        "tog-forceeditsummary": "Avisar-me se deixar o resumo da edição vazio",
        "newwindow": "(abre numa janela nova)",
        "cancel": "Cancelar",
        "moredotdotdot": "Mais...",
-       "morenotlisted": "Esta lista não está completa.",
+       "morenotlisted": "Esta lista pode estar incompleta.",
        "mypage": "Página",
        "mytalk": "Discussão",
        "anontalk": "Discussão",
        "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",
        "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.",
        "tags-actions-header": "Ações",
        "tags-active-yes": "Sim",
        "tags-active-no": "Não",
-       "tags-source-extension": "Definida por uma extensão",
+       "tags-source-extension": "Definida pelo software",
        "tags-source-manual": "Aplicada manualmente pelos utilizadores e robôs",
        "tags-source-none": "Já não está em uso",
        "tags-edit": "editar",
        "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",
index fbf95cc..4ec5cec 100644 (file)
        "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 who would have their rights changed",
+       "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.",
        "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",
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..8a7a974 100644 (file)
        "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.",
        "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",
index 7e6289d..87c43ed 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": "Просмотреть страницу файла",
        "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",
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 d17cdac..b080fcf 100644 (file)
        "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",
index 221e53a..4c8a234 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",
        "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",
index b04c775..176528f 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": "викитекст",
        "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",
index 155e8c6..252b76a 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:",
        "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",
index 3ea3fc4..e392e0d 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",
        "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\").",
        "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",
        "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-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",
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 3fa93b8..1f7fa45 100644 (file)
@@ -38,7 +38,7 @@
        "tog-enotifminoredits": "ಎಲ್ಯೆಲ್ಯ ಬದಲಾವನೆ ಆಂಡಲ ಎಂಕ್ ಇ-ಅಂಚೆ ಕಡಪುಡ್ಲೆ",
        "tog-enotifrevealaddr": "ಪ್ರಕಟಣೆ ಇ-ಮೇಲ್‍ಡ್ ಎನ್ನ ಇ-ಮೇಲ್ ವಿಳಾಸನ್ ತೊಜ್ಪಾಲೆ",
        "tog-shownumberswatching": "ಪುಟೊನು ತೂವೊಂದುಪ್ಪುನಂಚಿನ ಸದಸ್ಯೆರ್‍ನ ಸಂಖ್ಯೆನ್ ತೊಜ್ಪಾಲೆ",
-       "tog-oldsig": "à²\87ತà³\8dತà³\86ದ à²¸à²¹à²¿",
+       "tog-oldsig": "à²\87ತà³\8dತà³\86ದ à²¸à³\88ನà³\8d.",
        "tog-fancysig": "ವಿಕಿಟೆಕ್ಸ್‌ಟ್‍ಗ್ ದಸ್ಕತ್ತ್‌ದ ಉಪಚಾರೊ(ಸ್ವಂತೊ ಚಾಲನೆದ ಕೊಂಡಿ ಇದ್ಯಂದಿಲೆಕ)",
        "tog-uselivepreview": "ನೇರೊ ಮುನ್ನೋಟೊನು ಉಪಯೋಗ ಮಲ್ಪುಲೆ",
        "tog-forceeditsummary": "ಸಂಪಾದನೆ ಸಾರಾಂಸೊನು ಕಾಲಿ ಬುಡ್‍ಂದ್ ಎಂಕ್ ನೆನಪು ಮಲ್ಪುಲೆ",
        "createacct-yourpasswordagain-ph": "ಪ್ರವೇಸೊ ಪದೊನು ನನ ಒರ ನಮೂದಿಸಲೆ",
        "userlogin-remembermypassword": "ಎನನ್ ಲಾಗಿನ್ ಆತೇ ದೀಡ್ಲೆ",
        "userlogin-signwithsecure": "ರಕ್ಷಣೆದ ಕನೆಕ್ಷನ್ ಉಪಯೋಗಿಸಲೆ.",
+       "cannotlogin-title": "ಇತ್ತೆ ಉಲಾಯಿ ಪೋಯರ್ ಸಾದ್ಯೊ ಅವೊಂತಿಜ್ಜಿ",
        "cannotloginnow-title": "ಇತ್ತೆ ಉಲಾಯಿ ಪೋಯರ್ ಸಾದ್ಯೊ ಇದ್ದಿ",
+       "cannotcreateaccount-title": "ಕಾತೆ ನಿರ್ಮಾಣೊ ಮಲ್ಪೆರೆ ಆವೊಂತಿಜ್ಜಿ",
        "yourdomainname": "ಈರೆನ ಕಾರ್ಯಕ್ಷೇತ್ರ",
        "password-change-forbidden": "ಈರ್ ಈ ವಿಕಿಡ್ ಪ್ರರವೇಸ ಪದೊನು ಬದಲ್ಪೆರೆ ಸಾದ್ಯೊ ಇದ್ದಿ.",
        "login": "ಲಾಗಿನ್ ಆಲೆ",
index a63aac3..1cf4767 100644 (file)
@@ -87,7 +87,8 @@
                        "Hbseren",
                        "Kumkumuk",
                        "Basak",
-                       "Ece Alpdeniz"
+                       "Ece Alpdeniz",
+                       "Superyetkin"
                ]
        },
        "tog-underline": "Bağlantıların altını çiz:",
        "spam_blanking": "Tüm revizyonlar $1 sayfasına bağlantı içeriyor, boşaltılıyor",
        "spam_deleting": "Tüm revizyonlar $1 sayfasına bağlantı içeriyor, siliniyor",
        "simpleantispam-label": "Anti-spam denetimi.\nBunu <strong>doldurmayın</strong>!",
-       "pageinfo-title": "Bilgi için \"$1\"",
+       "pageinfo-title": "\"$1\" sayfa bilgisi",
        "pageinfo-not-current": "Üzgünüz, eski sürümler için bu bilgileri sağlamamız mümkün değildir.",
        "pageinfo-header-basic": "Temel bilgiler",
        "pageinfo-header-edits": "Düzenleme geçmişi",
        "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 402a034..b708f01 100644 (file)
@@ -93,7 +93,7 @@
        "tog-enotifminoredits": "Надсилати мені електронного листа навіть при незначних редагуваннях сторінок та файлів",
        "tog-enotifrevealaddr": "Показувати мою поштову адресу в повідомленнях",
        "tog-shownumberswatching": "Показувати число користувачів, які додали сторінку до свого списку спостереження",
-       "tog-oldsig": "Ð\9fоточний підпис:",
+       "tog-oldsig": "Ð\92аÑ\88 Ð¿оточний підпис:",
        "tog-fancysig": "Сприймати підпис як вікітекст (без автоматичного посилання)",
        "tog-uselivepreview": "Використовувати швидкий попередній перегляд",
        "tog-forceeditsummary": "Попереджати, коли не зазначений короткий опис редагування",
        "tog-showhiddencats": "Показувати приховані категорії",
        "tog-norollbackdiff": "Не показувати різницю версій після виконання відкоту",
        "tog-useeditwarning": "Попереджати мене, якщо я залишаю сторінку редагування з незбереженими змінами",
-       "tog-prefershttps": "Завжди використовувати безпечне з'єднання при вході у систему",
+       "tog-prefershttps": "Завжди використовувати безпечне з'єднання при вході в систему",
        "underline-always": "Завжди",
        "underline-never": "Ніколи",
        "underline-default": "Використовувати налаштування браузера",
        "newwindow": "(відкривається в новому вікні)",
        "cancel": "Скасувати",
        "moredotdotdot": "Більше…",
-       "morenotlisted": "Цей Ñ\81пиÑ\81ок Ð½ÐµÐ¿Ð¾Ð²Ð½Ð¸Ð¹.",
+       "morenotlisted": "Цей Ñ\81пиÑ\81ок Ð¼Ð¾Ð¶Ðµ Ð±Ñ\83Ñ\82и Ð½ÐµÐ¿Ð¾Ð²Ð½Ð¸Ð¼.",
        "mypage": "Сторінка",
        "mytalk": "Обговорення",
        "anontalk": "Обговорення",
        "talk": "Обговорення",
        "views": "Перегляди",
        "toolbox": "Інструменти",
+       "tool-link-userrights": "Змінити групи {{GENDER:$1|користувачів}}",
+       "tool-link-emailuser": "Надіслати електронного листа {{GENDER:$1|цьому користувачеві|цій користувачці}}",
        "userpage": "Переглянути сторінку користувача",
        "projectpage": "Переглянути сторінку проекту",
        "imagepage": "Переглянути сторінку файлу",
        "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": "Сталася помилка при автентифікації за допомогою зовнішньої бази даних, або у вас недостатньо прав для внесення змін до свого зовнішнього облікового запису.",
        "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> (Для старих ботів, які вимагають, щоб логін був такий же, як і ім'я користувача, Ви також можете використовувати <strong>$3</strong> як ім'я користувача і <strong>$4</strong> як пароль.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider не доступний.",
        "botpasswords-restriction-failed": "Вхід не було здійснено через обмеження для паролю бота.",
        "botpasswords-invalid-name": "Вказане ім'я користувача не містить роздільник для пароля бота («$1»).",
        "invalid-content-data": "Неприпустимі дані",
        "content-not-allowed-here": "Вміст «$1» недопустимий на сторінці [[$2]]",
        "editwarning-warning": "Перехід на іншу сторінку призведе до втрати ваших змін.\nЯкщо ви ввійшли до системи, то ви можете відключити це попередження в розділі \"{{int:prefs-editing}}\" ваших налаштувань.",
+       "editpage-invalidcontentmodel-title": "Контентна модель не підтримується",
+       "editpage-invalidcontentmodel-text": "Контентна модель «$1» не підтримується.",
        "editpage-notsupportedcontentformat-title": "Формат вмісту не підтримується",
        "editpage-notsupportedcontentformat-text": "Формат вмісту $1 не підтримується моделлю вмісту $2.",
        "content-model-wikitext": "вікітекст",
        "file-thumbnail-no": "Назва файлу починається на <strong>$1</strong>.\nМожливо, це зменшена копія зображення ''(мініатюра)''.\nЯкщо у вас є це зображення в повному розмірі, завантажте його, інакше змініть назву файлу.",
        "fileexists-forbidden": "Файл з такою назвою вже існує і не може бути перезаписаний.\nЯкщо ви все одно хочете завантажити цей файл, будь ласка, поверніться назад і оберіть іншу назву.\n[[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Файл із такою назвою вже існує у спільному сховищі файлів.\nЯкщо ви все ж хочете завантажити цей файл, будь ласка, поверніться назад і змініть назву файлу. [[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "Це завантаження є точною копією поточної версії <strong>[[:$1]]</strong>.",
+       "fileexists-duplicate-version": "Це завантаження є точною копією {{PLURAL:$2|старішої версії|старіших версій}} <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Цей файл є дублікатом {{PLURAL:$1|1=файлу|таких файлів}}:",
        "file-deleted-duplicate": "Такий самий файл ([[:$1]]) уже вилучався раніше. Будь ласка, ознайомтеся з історією вилучень файлу перед тим, як завантажити його знову.",
        "file-deleted-duplicate-notitle": "Файл, ідентичний до цього файлу, був раніше видалений, і назву було усунено.\nВам слід попросити кого-небудь з можливістю перегляду усуненого файлу даних, щоб проаналізувати ситуацію, перш ніж приступити до повторного завантаження.",
        "pageinfo-article-id": "ID сторінки",
        "pageinfo-language": "Мова вмісту сторінки",
        "pageinfo-content-model": "Модель вмісту сторінки",
+       "pageinfo-content-model-change": "змінити",
        "pageinfo-robot-policy": "Індексація пошуковими системами",
        "pageinfo-robot-index": "Індексується",
        "pageinfo-robot-noindex": "Не індексується",
        "tag-filter": "Фільтр&nbsp;[[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-actions-header": "Дії",
        "tags-active-yes": "Так",
        "tags-active-no": "Ні",
-       "tags-source-extension": "Визначається розширенням",
+       "tags-source-extension": "Визначається програмним забезпеченням",
        "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 запису журналу|$5 записів журналу}} на $3: $4",
index 36e1edd..0abdfa8 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": "میرے ترمیم شدہ صفحات اور فائلوں کو میری زیر نظر فہرست میں شامل کریں",
@@ -56,7 +57,7 @@
        "tog-enotifminoredits": "مجھے صفحات اور فائلوں میں کی جانے والی معمولی ترامیم کی خبر بھی بذریعہ برقی ڈاک بھیجیں",
        "tog-enotifrevealaddr": "اطلاعاتی برقی خطوط میں میرا برقی ڈاک پتہ ظاہر کریں",
        "tog-shownumberswatching": "دیکھنے والے صارفین کی تعداد دکھائیں",
-       "tog-oldsig": "موجودہ دستخط:",
+       "tog-oldsig": "آپ کے موجودہ دستخط:",
        "tog-fancysig": "سادہ دستخط (بلا خودکار ربط)",
        "tog-uselivepreview": "راست نمائش استعمال کریں",
        "tog-forceeditsummary": "خلاصہ ترمیم خالی چھوڑنے پر مجھے آگاہ کریں",
@@ -73,7 +74,7 @@
        "tog-showhiddencats": "پوشیدہ زمرہ جات دکھائیں",
        "tog-norollbackdiff": "استرجع کے بعد فرق نہ دکھائیں",
        "tog-useeditwarning": "غیر محفوظ تبدیلیاں چھوڑنے پر مجھے آگاہ کریں",
-       "tog-prefershttps": "لاگ ان رہنے کے دوران ہمیشہ محفوظ کنیکشن استعمال کریں",
+       "tog-prefershttps": "داخل رہنے کے دوران میں ہمیشہ محفوظ کنیکشن استعمال کریں",
        "underline-always": "ہمیشہ",
        "underline-never": "کبھی نہیں",
        "underline-default": "پوشاک یا براؤزر کا طے شدہ",
        "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|اس زمرہ میں صرف درج ذیل فائل موجود ہے۔|اس زمرہ کی کل $2 فائلوں میں سے $1 {{PLURAL:$1|فائل|فائلیں}} درج ذیل {{PLURAL:$1|ہے|ہیں}}}}۔",
        "newwindow": "(نـئی ونـڈو میـں کھولیں)",
        "cancel": "منسوخ",
        "moredotdotdot": "مزید...",
-       "morenotlisted": "یہ فہرست مکمل نہیں ہے۔",
+       "morenotlisted": "شاید یہ فہرست مکمل نہیں۔",
        "mypage": "صفحہ",
        "mytalk": "تبادلۂ خیال",
        "anontalk": "اس آئی پی پتہ کا تبادلۂ خیال",
        "qbpageoptions": "یہ صفحہ",
        "qbmyoptions": "میرے صفحات",
        "faq": "عام طور پر پوچھے جانے والے سوالات",
-       "faqpage": "Project:معلوماتِ عامہ",
-       "actions": "ایکشنز",
-       "namespaces": "جائے نام",
+       "faqpage": "Project:عمومی سوالات",
+       "actions": "اقدامات",
+       "namespaces": "نام فضا",
        "variants": "متغیرات",
        "navigation-heading": "قائمہ رہنمائی",
-       "errorpagetitle": "خطاء",
-       "returnto": "واپس $1۔",
+       "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 سے پلٹایا گیا)",
+       "redirectedfrom": "($1 سے رجوع مکرر)",
        "redirectpagesub": "لوٹایا گیا صفحہ",
-       "redirectto": "لوٹایا گیا صفحہ:",
-       "lastmodifiedat": "آخرÛ\8c Ø¨Ø§Ø± ØªØ¯Ù\88Û\8cÙ\86 $2, $1 Ú©Ù\88 کی گئی۔",
+       "redirectto": "رجوعِ مکرر از:",
+       "lastmodifiedat": "اس ØµÙ\81Ø­Û\81 Ù\85Û\8cÚº Ø¢Ø®Ø±Û\8c Ø¨Ø§Ø± Ù\85Ù\88رخÛ\81 $1Ø¡ Ú©Ù\88 $2 Ø¨Ø¬Û\92 ØªØ±Ù\85Û\8cÙ\85 کی گئی۔",
        "viewcount": "اِس صفحہ تک {{PLURAL:$1|ایک‌بار|$1 مرتبہ}} رسائی کی گئی",
        "protectedpage": "محفوظ شدہ صفحہ",
        "jumpto": ":چھلانگ بطرف",
        "copyrightpage": "{{ns:project}}:حقوق تصانیف",
        "currentevents": "حالیہ واقعات",
        "currentevents-url": "Project:حالیہ واقعات",
-       "disclaimers": "اعÙ\84اÙ\86ات",
+       "disclaimers": "اظÛ\81ار Ù\84ا ØªØ¹Ù\84Ù\82Û\8c",
        "disclaimerpage": "Project:عام اعلان",
        "edithelp": "معاونت براۓ ترمیم",
        "helppage-top-gethelp": "مدد",
        "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’’ مستعادہ منجانب",
+       "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' بھیجا گیا لیکن یہ ایک اے پی آئی نویس ماڈیول کو بھیجی گئی درخواست تھی۔",
        "internalerror": "خطائے اندرونی",
        "internalerror_info": "خطائے اندرونی: $1",
+       "internalerror-fatal-exception": "\"$1\" قسم کا تباہ کن استثنا",
        "filecopyerror": "\"$1\" مسل کو \"$2\" کی طرف نقل نہیں کیا جاسکا.",
        "filerenameerror": "مسل \"$1\" کو \"$2\" میں بازنام نہیں کیا جاسکا.",
        "filedeleteerror": "مسل \"$1\" کو حذف نہیں کیا جاسکا.",
        "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شاید اِس میں ایک یا زیادہ ایسے حروف موجود ہوں جو عنوانات میں استعمال نہیں ہوسکتے.",
+       "title-invalid-empty": "درخواست شدہ عنوان خالی ہے یا اس میں محض نام فضا کا نام ہے۔",
+       "title-invalid-utf8": "درخواست شدہ عنوان میں نادرست یونیکوڈ حروف موجود ہیں۔",
+       "title-invalid-interwiki": "درخواست شدہ عنوان میں ایسا بین الویکی ربط موجود ہے جسے عنوانات میں استعمال نہیں کیا جا سکتا۔",
+       "title-invalid-talk-namespace": "درخواست شدہ عنوان اس تبادلۂ خیال صفحہ کا ہے جو موجود نہیں۔",
+       "title-invalid-characters": "درخواست شدہ عنوان میں نادرست حروف «$1» موجود ہیں۔",
+       "title-invalid-relative": "عنوان میں علامت موجود ہے۔ علامتوں (./, ../) پر مشتمل عنوانات نادرست ہیں، اس لیے کہ یہ علامتیں صارف کے براؤزر کے لیے ناقابل رسائی ہوتی ہیں۔",
+       "title-invalid-magic-tilde": "درخواست شدہ عنوان میں نادرست جادوئی علامتیں مسلسل استعمال کی گئی ہیں (<nowiki>~~~</nowiki>)۔",
+       "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": "مسودہ",
        "viewsource-title": "$1 کا مسودہ دیکھیں",
        "protectedpagetext": "اس صفحہ کو تدوین سے محفوظ رکھنے کیلیے مقفل کر دیا گیا ہے۔",
        "viewsourcetext": "آپ صرف مسودہ دیکھ سکتے ہیں اور اسکی نقل اتار سکتے ہیں۔",
        "viewyourtext": "آپ اس مواد کو دیکھ سکتے ہیں اور اٹھا (کاپی) سکتے ہیں <strong>آپ کی ترامیم</strong> اس صفحہ پر۔",
-       "protectedinterface": "{{fmbox \n| type = editnotice\n| image = [[File:Namespace MediaWiki.svg|45px]]\n| text = '''اس [[mw:Manual:System message|انٹرفیس پیغام]] یا [[ویکیپیڈیا:جلد|جلد]] کی دستاویز [[mw:Manual:Interface/{{PAGENAME}}|میڈیاویکی ڈاٹ آرگ]] یا [[translatewiki:MediaWiki:{{PAGENAME}}/qqq|ٹرانسلیٹ ویکی ڈاٹ نیٹ]] پر بنائی جا سکتی ہے۔''' <br /> چونکہ یہ صفحہ میڈیاویکی سوفٹویئر کے لیے انٹرفیس متن فراہم کرتا ہے، اس لیے اس میں محض [[ویکیپیڈیا:منتظمین|منتظمین]] ہی ترمیم کر سکتے ہیں۔ البتہ اگر آپ کو اس میں کسی قسم کی ترمیم مطلوب ہو تو ذیل میں موجود بٹن پر کلک کرکے اس پیغام میں ترمیم کی درخواست کر سکتے ہیں۔\n<div class=\"center\" style=\"margin-top: 4px;\">{{درخواست ترمیم روانہ کریں|type=full}}</div>\n}}{{editnotice load\n| notice action = view \n}}",
-       "editinginterface": "<strong>انتباہ: </strong> آپ ایک ایسے صفحے میں ترمیم کر رہے ہیں جو سوفٹ ویئر کے لیے انٹرفیس متن فراہم کرتا ہے۔ اس صفحہ میں کی جانے والی تبدیلی سے اس ویکی پر دیگر صارفین کے لیے انٹرفیس متاثر ہوگی۔",
-       "translateinterface": "تمام ویکیوں میں تبدیلی یا شامل کرنے کے لیے، [https://translatewiki.net/ translatewiki.net]کو استعمال کریں ، میڈیا ویکی دارالترجمہ.",
+       "protectedinterface": "یہ صفحہ سافٹ ویئر کا انٹرفیس متن فراہم کرتا ہے اور غلط استعمال سے بچنے کے لیے اسے محفوظ رکھا گیا ہے۔\nتمام ویکیوں میں ترجمہ شامل کرنے یا اس میں تبدیلی کرنے کے لیے میڈیاویکی دار الترجمہ [https://translatewiki.net/ translatewiki.net]کو استعمال کریں۔",
+       "editinginterface": "<strong>انتباہ:</strong> آپ ایک ایسے صفحے میں ترمیم کر رہے ہیں جو سافٹ ویئر کا انٹرفیس متن فراہم کرتا ہے۔ اس صفحہ میں کی جانے والی ترمیم، دیگر صارفین کے انٹرفیس کو تبدیل کردے گی۔",
+       "translateinterface": "تمام ویکیوں میں تراجم کو تبدیل یا شامل کرنے کے لیے  میڈیاویکی کے دار الترجمہ [https://translatewiki.net/ translatewiki.net] کو استعمال کریں۔",
+       "cascadeprotected": "درج ذیل محفوظ کردہ {{PLURAL:$1|صفحہ|صفحات}} کی «آبشاری» حفاظت میں شامل ہونے کی وجہ سے یہ صفحہ بھی محفوظ ہے:\n$2",
        "namespaceprotected": "آپ کو '''$1''' فضائے نام میں صفحات تدوین کرنے کی اِجازت نہیں ہے.",
+       "customcssprotected": "آپ کو اس سی ایس ایس میں ترمیم کرنے کی اجازت نہیں کیونکہ اس میں کسی دوسرے صارف کی ذاتی ترتیبات موجود ہیں۔",
+       "customjsprotected": "آپ کو اس جاوا اسکرپٹ میں ترمیم کرنے کی اجازت نہیں کیونکہ اس میں کسی دوسرے صارف کی ذاتی ترتیبات موجود ہیں۔",
        "mycustomcssprotected": "آپ اس سی ایس ایس (CSS) صفحہ میں ترمیم کرنے کا اختیار نہیں رکھتے۔",
        "mycustomjsprotected": "آپ اس جاوا اسکپرٹ (JavaScript) صفحہ میں ترمیم کرنے کا اختیار نہیں رکھتے۔",
        "myprivateinfoprotected": "آپ ان ذاتی معلوات (private information) میں ترمیم کرنے کا اختیار نہیں رکھتے۔",
        "mypreferencesprotected": "آپ اپنی ان ترجیحات (preferences) میں ترمیم کرنے کا اختیار نہیں رکھتے۔",
        "ns-specialprotected": "خاص صفحات کی تدوین نہیں کی جاسکتی.",
        "titleprotected": "اس عنوان کو [[User:$1|$1]] نے تخلیق سے محفوظ کیا ہے.\nوجہ یہ بتائی گئی ہے: <em>$2</em>.",
+       "filereadonlyerror": "فائل «$1» میں تبدیلی ممکن نہیں کیونکہ خزانہ فائل «$2» فقط خواندگی حالت میں ہے۔\n\nانتظامیہ کی جانب سے مقفل کرنے کی حسب ذیل توجیہ پیش کی گئی ہے:\n\n«$3»",
+       "invalidtitle-knownnamespace": "«$2» نام فضا میں «$3» متن پر مشتمل عنوان نادرست ہے",
+       "invalidtitle-unknownnamespace": "نامعلوم نام فضا عدد «$1» اور «$2» متن پر مشتمل عنوان نادرست ہے",
        "exception-nologin": "غیر داخل نوشتہ",
        "exception-nologin-text": "براہ مہربانی! اس صفحہ تک رسائی یا ترمیم کے لیے لاگ ان ہوں۔",
        "exception-nologin-text-manual": "$1 براہ مہربانی! اس صفحہ تک رسائی یا ترمیم کے لیے لاگ ان ہوں۔",
        "virus-scanfailed": "تفریس ناکام (رمز $1)",
        "virus-unknownscanner": "انجان ضدوائرس:",
        "logouttext": "<strong>اب آپ خارج ہوچکے ہیں۔</strong>\n\nیاد رکھیں! کچھ صفحات ایسے نظر آتے رہیں گے کہ جیسے ابھی آپ خارج نہیں ہوئے، جب تک آپ اپنے کیشے صاف نہیں کرتے۔",
+       "cannotlogoutnow-title": "ابھی خارج نہیں ہو سکتے",
+       "cannotlogoutnow-text": "$1 کے استعمال کے دوران میں خارج ہونا ممکن نہیں۔",
        "welcomeuser": "خوش آمدید، $1!",
+       "welcomecreation-msg": "آپ کا کھاتہ بن گیا۔\nاگر آپ چاہیں تو [[Special:Preferences|اپنی ترجیحات]] میں حسب منشا تبدیلی کر سکتے ہیں۔",
        "yourname": "اسمِ رکنیت",
        "userlogin-yourname": "صارف نام",
        "userlogin-yourname-ph": "اپنا صارف نام درج کریں",
        "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": "داخلِ نوشتہ ہوجائیے",
        "userlogin-resetpassword-link": "کلمہ شناخت بھول گئے؟",
        "userlogin-helplink2": "داخل نوشتگی میں معاونت درکار ہے؟",
        "userlogin-loggedin": "آپ ویکیپیڈیا میں بطور صارف {{GENDER:$1|$1}}  پہلے سے داخل نوشتہ (logged in) ہیں۔\nدوسرے کھاتہ سے داخل ہونے کے لیے درج ذیل خانے پر کریں۔",
+       "userlogin-reauth": "آپ {{GENDER:$1|$1}} ہیں، اس کی تصدیق کے لیے آپ کا داخل ہونا ناگزیر ہے۔",
        "userlogin-createanother": "دوسرا کھاتہ تخلیق کریں",
        "createacct-emailrequired": "ای میل پتہ",
        "createacct-emailoptional": "ای میل ایڈریس (اختیاری)",
        "createacct-email-ph": "اپنا برقی پتہ لکھیں",
        "createacct-another-email-ph": "برقی پتہ لکھیں",
        "createaccountmail": "عارضی پاسورڈ استعمال کریں اور اسے متعینہ برقی ڈاک پتہ پر ارسال کریں",
+       "createaccountmail-help": "پاس ورڈ معلوم کیے بغیر کسی دوسرے شخص کا کھاتہ بنانے کے لیے اسے استعمال کیا جا سکتا ہے۔",
        "createacct-realname": "اصلی نام (اختیاری)",
        "createaccountreason": "وجہ:",
        "createacct-reason": "وجہ",
        "createacct-reason-ph": "آپ دوسرا کھاتہ کیوں تخلیق کررہے ہیں",
+       "createacct-reason-help": "نوشتہ کھاتہ سازی میں نظر آنے والا پیغام",
        "createacct-submit": "آپ کا کھاتا بنائیں",
        "createacct-another-submit": "کھاتہ بنائیں",
+       "createacct-continue-submit": "کھاتہ سازی جاری رکھیں",
+       "createacct-another-continue-submit": "کھاتہ سازی جاری رکھیں",
        "createacct-benefit-heading": "{{SITENAME}} آپ جیسے لوگوں کی طرف سے بنایا گیا ہے ۔",
        "createacct-benefit-body1": "{{PLURAL:$1|ترمیم|ترامیم}}",
        "createacct-benefit-body2": "$1 {{PLURAL:$1|صفحہ|صفحات}}",
        "nocookiesnew": "کھاتۂ صارف بنادیا گیا ہے، لیکن آپ کا داخلہ نہیں ہوا.\nصارفین کے داخلہ کیلئے {{SITENAME}} کوکیز استعمال کرتا ہے.\nآپ کے ہاں کوکیز غیر فعال ہیں.\nبراہِ کرم، انہیں فعال کیجئے، اور پھر اپنے نئے اسمِ صارف اور کلمۂ شناخت کے ساتھ داخل ہوجائیے.",
        "nocookieslogin": "صارفین کے داخل ہونے کیلئے {{SITENAME}} کوکیز استعمال کرتا ہے.\nآپ کے ہاں کوکیز غیر فعال ہیں.\nانہیں فعال کرنے کے بعد پھر کوشش کیجئے.",
        "nocookiesfornew": "اس صارف نام کا کھاتہ نہیں بن سکا۔ہم اس بات کی وضاحت نہیں کر سکتے (کہ ایسا کیوں ہوا)، براہ مہربانی! آپ\nیقین کر لیں کہ آپ کی کوکیز فعال ہیں، صفحہ تازہ کریں اور پھر کوشش کریں۔",
+       "createacct-loginerror": "کھاتہ بن چکا ہے لیکن آپ اس میں خودکار طور پر داخل نہیں ہو سکے۔ براہ کرم [[Special:UserLogin|دستی طور پر داخل ہونے کی کوشش کریں]]۔",
        "noname": "آپ نے صحیح اسم صارف نہیں چنا.",
        "loginsuccesstitle": "داخلہ کامیاب",
        "loginsuccess": "'''اب آپ {{SITENAME}} میں بنام \"$1\" داخل ہوچکے ہیں۔'''",
-       "nosuchuser": "\"$1\" کے نام سے کوئی صارف موجود نہیں ہے.\nبرائے مہربانی! ہجوں کے درست اندراج کی تصدیق کرلیجئے.\nاگر آپ چاہیں تو [[Special:CreateAccount|نیا کھاتہ بھی بناسکتے ہیں]].",
+       "nosuchuser": "\"$1\" کے نام سے کوئی صارف موجود نہیں ہے۔\nبراہ کرم ہجوں کو جانچ لیں۔\nنیز اگر آپ چاہیں تو [[Special:CreateAccount|نیا کھاتہ بھی بنا سکتے ہیں]]۔",
        "nosuchusershort": "\"$1\" کے نام سے کوئی صارف موجود نہیں.\nاپنا ہجہ جانچئے.",
        "nouserspecified": "آپ کو ایک اسمِ صارف مخصوص کرنا ہے.",
        "login-userblocked": "اِس صارف پر پابندی ہے. داخلِ نوشتہ ہونے کی اجازت نہیں.",
        "noemail": "صارف \"$1\" کیلئے کوئی برقی پتہ درج نہیں کیا گیا.",
        "noemailcreate": "صحیح برقی پتہ مہیّا کریں",
        "passwordsent": "ایک نیا کلمۂ شناخت \"$1\" کے نام سے بننے والی برقی ڈاک کے پتے کیلیے بھیج دیا گیا ہے۔\nجب وہ موصول ہو جاۓ تو براہ کرم اسکے ذریعے دوبارہ داخل ہوں۔",
-       "blocked-mailpassword": "آپ کا آئی.پی پتہ تدوین سے روک لیا گیا ہے، سو، ناجائز استعمال کو روکنے کیلئے، آپ کے آئی.پی پتہ کو کلمۂ شناخت کی بحالی کا فعل استعمال کرنے کی اِجازت نہیں ہے.",
+       "blocked-mailpassword": "آپ کے آئی پی پتے کی ترمیم کاری پر پابندی لگا دی گئی ہے۔ غلط استعمال سے بچنے کے لیے اس آئی پی پتے سے پاس ورڈ کی بازیابی کی اجازت منسوخ کر دی گئی ہے۔",
        "eauthentsent": "ایک تصدیقی برقی خط نامزد کیے گئے برقی پتہ پر ارسال کردیا گیا ہے۔\nآپ کو موصول ہوئے برقی خط میں ہدایات پر عمل کرکے اس بات کی توثیق کرلیں کہ مذکورہ برقی پتہ آپ کا ہی ہے۔",
        "throttled-mailpassword": "گزشتہ {{PLURAL:$1|گھنٹے|$1 گھنٹوں}} کے دوران پہلے سے ہی پارلفظ (پاسورڈ) کی تبدیلی کے لیے برقی خط بھیجا گيا ہے۔\nناجائز استعمال کے سدّباب کیلئے، {{PLURAL:$1|گھنٹہ|$1 گھنٹوں}} کے دوران صرف ایک برقی خط بھیجا جاسکتا ہے۔",
        "mailerror": "مسلہ دوران ترسیل خط:$1",
        "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 انتظار فرمائیے۔",
-       "login-abort-generic": "آپ داخل (لاگ ان) نہیں ہو پائے - ساقط",
+       "login-abort-generic": "لاگ ان ناکام - منسوخ شد",
+       "login-migrated-generic": "آپ کا کھاتہ منتقل کر دیا گیا، اب اس ویکی پر آپ کا صارف نام موجود نہیں۔",
        "loginlanguagelabel": "زبان: $1",
+       "suspicious-userlogout": "کھاتے سے خارج ہونے کی درخواست رد کر دی گئی ہے کیونکہ ایسا معلوم ہوتا ہے یہ درخواست کسی شکستہ براؤزر یا کیشے کی حامل پراکسی سے بھیجی گئی تھی۔",
+       "createacct-another-realname-tip": "حقیقی نام اختیاری ہے۔\nاگر آپ اسے فراہم کریں تو آپ کے کاموں کو اس نام سے منسوب کرنے کے لیے استعمال کیا جائے گا۔",
        "pt-login": "داخل ہوجائیے",
        "pt-login-button": "داخل ہو",
+       "pt-login-continue-button": "داخل ہوں",
        "pt-createaccount": "کھاتا بنائیں",
        "pt-userlogout": "خارج ہوجائیں",
+       "php-mail-error-unknown": "پی ایچ پی کے mail() فنکشن میں نامعلوم نقص۔",
        "user-mail-no-addy": "برقی ڈاک بھیجنے کی کوشش بغیر برقی ڈاک پتہ",
        "user-mail-no-body": "خالی یا بہت ہی مختصر برقی خط بھیجنے کی کوشش",
        "changepassword": "کلمۂ شناخت تبدیل کریں",
        "newpassword": "نیا کلمۂ شناخت",
        "retypenew": "نیا کلمۂ شناخت دوبارہ درج کریں:",
        "resetpass_submit": "پارلفظ بناؤ اور داخل ہوجاؤ",
-       "changepassword-success": "آپ کا پارلفظ کامیابی سے تبدیل ہوگیا!",
+       "changepassword-success": "آپ کا پاس ورڈ تبدیل کر دیا گیا!",
+       "changepassword-throttled": "آپ نے حال ہی میں متعدد مرتبہ داخل ہونے کی کوشش کی ہے۔\nدوبارہ کوشش کرنے سے پہلے $1 انتظار کریں۔",
+       "botpasswords": "روبہ پاس ورڈ",
+       "botpasswords-summary": "<em>روبہ کے پاس ورڈ</em> کے ذریعہ اصل کھاتے کی لاگ ان معلومات کے بغیر اے پی آئی کی مدد سے صارف کھاتے میں رسائی حاصل ہوتی ہے۔\n\nاگر آپ اس سے واقف نہیں ہیں تو بہتر ہوگا کہ آپ اسے نہ چھیڑیں۔ کوئی دوسرا صارف کبھی اس پاس ورڈ کے بنانے اور اسے سپرد کرنے کا آپ سے مطالبہ نہیں کرے گا۔",
+       "botpasswords-disabled": "روبہ کے پاس ورڈ غیر فعال ہیں۔",
+       "botpasswords-no-central-id": "روبہ کے پاس ورڈ کو استعمال کرنے کے لیے آپ کا مرکزی کھاتے میں داخل رہنا ضروری ہے۔",
+       "botpasswords-existing": "روبہ کے موجودہ پاس ورڈ",
+       "botpasswords-createnew": "روبہ کا نیا پاس ورڈ بنائیں",
+       "botpasswords-editexisting": "روبہ کے موجودہ پاس ورڈ میں ترمیم کریں",
+       "botpasswords-label-appid": "روبہ کا نام:",
+       "botpasswords-label-create": "تخلیق کریں",
+       "botpasswords-label-update": "تجدید کریں",
+       "botpasswords-label-cancel": "منسوخ کریں",
+       "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-update-failed": "روبہ نام \"$1\" کی تجدید میں ناکامی۔ کیا اسے حذف کر دیا گیا ہے؟",
+       "botpasswords-created-title": "روبہ کا پاس ورڈ تخلیق ہو چکا ہے",
+       "botpasswords-created-body": "صارف \"$2\" کے روبہ نام \"$1\" کا پاس ورڈ تخلیق ہو چکا ہے۔",
+       "botpasswords-updated-title": "روبہ کا پاس ورڈ تازہ کر دیا گیا",
+       "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\")۔",
+       "botpasswords-not-exist": "صارف \"$1\" کے پاس \"$2\" کے نام سے روبہ کا پاس ورڈ نہیں ہے۔",
        "resetpass_forbidden": "پارلفظ تبدیل نہیں ہوسکتا",
+       "resetpass_forbidden-reason": "پاس ورڈ تبدیل نہیں کیا جا سکتا: $1",
        "resetpass-no-info": "اِس صفحہ تک براہِ راست رسائی کیلئے آپ کو داخلِ نوشتہ ہونا پڑے گا.",
        "resetpass-submit-loggedin": "پارلفظ کی تبدیلی",
        "resetpass-submit-cancel": "منسوخ",
-       "resetpass-wrong-oldpass": "عارضÛ\8c Û\8cا Ù\85Ù\88جÙ\88دÛ\81 Ù¾Ø§Ø±Ù\84Ù\81ظ Ù\86اÙ\82ص Û\81Û\92.\nآپ Û\8cا ØªÙ\88 Ù¾Û\81Ù\84Û\92 Û\81Û\8c Ø³Û\92 Ø¢Ù¾Ù\86ا Ù¾Ø§Ø±Ù\84Ù\81ظ Ú©Ø§Ù\85Û\8cابÛ\8c Ø³Û\92 ØªØ¨Ø¯Û\8cÙ\84 Ú©Ø±Ú\86Ú©Û\92 Û\81Û\8cÚº Ø§Ù\88ر Û\8cا Ø¢Ù¾ Ù\86Û\92 Ù\86ئÛ\92 Ø¹Ø§Ø±Ø¶Û\8c Ù¾Ø§Ø±Ù\84Ù\81ظ Ú©Û\8c Ø¯Ø±Ø®Ù\88است Ú©Û\8c Û\81Û\92.",
+       "resetpass-wrong-oldpass": "عارضÛ\8c Û\8cا Ù\85Ù\88جÙ\88دÛ\81 Ù¾Ø§Ø³ Ù\88رÚ\88 Ù\86ادرست Û\81Û\92Û\94\nشاÛ\8cد Ø¢Ù¾ Ù\86Û\92 Ù¾Û\81Ù\84Û\92 Û\81Û\8c Ø§Ù¾Ù\86ا Ù¾Ø§Ø³ Ù\88رÚ\88 ØªØ¨Ø¯Û\8cÙ\84 Ú©Ø± Ù\84Û\8cا Û\81Û\92 Û\8cا Ù\86ئÛ\92 Ø¹Ø§Ø±Ø¶Û\8c Ù¾Ø§Ø³ Ù\88رÚ\88 Ú©Û\8c Ø¯Ø±Ø®Ù\88است Ú©Ø± Ú\86Ú©Û\92 Û\81Û\8cÚºÛ\94",
        "resetpass-recycled": "براہ کرم اپنے موجودہ کلمۂ شناخت (پاسورڈ) سے مختلف کلمۂ شناخت سے بازترتیب (ری‌سیٹ) کریں",
        "resetpass-temp-emailed": "آپ عارضی برقی خط سے بھیجے گئے کوڈ سے لاگ ان ہیں\nمکمل طور پر لاگ ان ہونے کے لیے آپ کو نیا پاسورڈ سیٹ کرنا پڑے گا",
        "resetpass-temp-password": "عارضی پارلفظ:",
+       "resetpass-abort-generic": "کسی توسیع نے پاس ورڈ کی تبدیلی کو منسوخ کر دیا ہے۔",
+       "resetpass-expired": "آپ کے پاس ورد کی مدت ختم ہو چکی ہے۔ داخل ہونے کے لیے براہ کرم نیا پاس ورڈ بنائیں۔",
+       "resetpass-expired-soft": "آپ کے پاس ورڈ کی مدت ختم ہو چکی ہے، لہذا اسے دوبارہ بنانے کی ضرورت ہے۔\nبراہ کرم نیا پاس ورڈ بنائیں، تاہم اگر مستقبل میں اس کی ترتیب نو مقصود ہو تو «{{int:authprovider-resetpass-skip-label}}» پر کلک کریں۔",
+       "resetpass-validity-soft": "آپ کا پاس ورڈ درست نہیں: $1\n\nبراہ کرم نیا پاس ورڈ بنائیں، تاہم اگر مستقبل میں اس کی ترتیب نو مقصود ہو تو «{{int:authprovider-resetpass-skip-label}}» پر کلک کریں۔",
        "passwordreset": "پارلفظ کی بازتعینی",
+       "passwordreset-text-one": "برقی خط کے ذریعہ عارضی پاس ورڈ حاصل کرنے کے لیے اس فارم کو پُر کریں۔",
+       "passwordreset-text-many": "{{PLURAL:$1|برقی خط کے ذریعہ عارضی پاس ورڈ حاصل کرنے کے لیے کسی ایک خانے کو پُر کریں۔}}",
+       "passwordreset-disabled": "اس ویکی پر پاس ورڈ کی ترتیب نو کی سہولت فعال نہیں ہے۔",
+       "passwordreset-emaildisabled": "اس ویکی پر برقی خط کی سہولت غیر فعال ہیں۔",
        "passwordreset-username": "اسمِ صارف:",
        "passwordreset-domain": "ساحہ:",
+       "passwordreset-capture": "برقی خط کی حتمی شکل دیکھیں؟",
+       "passwordreset-capture-help": "اس خانے کو نشان زد کرنے کی صورت میں (عارضی پاس ورڈ کا حامل) برقی خط آپ کو بھی نظر آئے گا اور صارف کو بھی روانہ کیا جائے گا۔",
        "passwordreset-email": "برقی ڈاک پتہ:",
+       "passwordreset-emailtitle": "{{SITENAME}} کھاتہ کی تفصیلات",
+       "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-nocaller": "کالر کا فراہم کیا جانا لازمی ہے",
+       "passwordreset-nosuchcaller": "کالر موجود نہیں: $1",
+       "passwordreset-ignored": "پاس ورڈ کی ترتیب نو مکمل نہیں ہو سکی۔ شاید کوئی پرووائڈر فراہم نہیں کیا گیا تھا؟",
+       "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-no-tokens": "ترتیب نو کے لیے کوئی ٹوکن موجود نہیں۔",
        "resettokens-tokens": "ٹوکن:",
        "resettokens-token-label": "$1 (موجودہ قدر: $2)",
+       "resettokens-watchlist-token": "[[Special:Watchlist|آپ کی زیرنظر فہرست میں موجود صفحات کی تبدیلیوں]] کی ویب فیڈ (آٹوم/آر ایس ایس) کا ٹوکن",
+       "resettokens-done": "ٹوکنوں کی ترتیب نو مکمل۔",
+       "resettokens-resetbutton": "منتخب ٹوکنوں کو دوبارہ مرتب کریں",
        "bold_sample": "دبیز متن",
        "bold_tip": "دبیز متن",
        "italic_sample": "ترچھا متن",
        "nowiki_sample": "غیرشکلبندشدہ متن یہاں درج کریں",
        "nowiki_tip": "ویکی شکلبندی نظرانداز کریں",
        "image_tip": "پیوستہ ملف",
-       "media_tip": "ربطِ ملف",
+       "media_tip": "فائل کا ربط",
        "sig_tip": "آپکا دستخط بمع مہرِوقت",
        "hr_tip": "اُفقی لکیر (زیادہ استعمال نہ کریں)",
        "summary": "خلاصہ:",
        "subject": "عنوان:",
        "minoredit": "معمولی ترمیم",
-       "watchthis": "یہ صفحہ زیر نظر کیجیۓ",
+       "watchthis": "اس صفحہ کو زیر نظر کیحیے",
        "savearticle": "محفوظ",
        "savechanges": "تبدیلیاں محفوظ کریں",
        "publishpage": "شائع کریں",
        "preview": "نمائش",
        "showpreview": "نمائش",
        "showdiff": "تبدیلیاں دکھائیں",
+       "blankarticle": "<strong>انتباہ:</strong> آپ خالی صفحہ تخلیق کر رہے ہیں۔\nاگر آپ «{{int:savearticle}}» پر دوبارہ کلک کریں تو یہ صفحہ بغیر کسی مواد کے محفوظ ہو جائے گا۔",
        "anoneditwarning": "<strong>انتباہ:</strong> آپ ویکیپیڈیا میں داخل نہیں ہوئے ہیں۔ لہذا اگر آپ اس صفحہ میں کوئی ترمیم کرتے ہیں تو آپکا آئی پی ایڈریس (IP) اس صفحہ کے تاریخچہ ترمیم میں محفوظ ہوجائے گا۔ اگر آپ  <strong>[$1 لاگ ان]</strong> ہوتے ہیں یا کھاتہ نہ ہونے کی صورت میں <strong>[$2 کھاتہ بنا لیتے ہیں]</strong> تو تو آپ کی ترامیم آپ کے صارف نام سے محفوظ ہوگی، جنھیں آپ کسی بھی وقت ملاحظہ کر سکتے ہیں۔",
+       "anonpreviewwarning": "<em>آپ داخل نہیں ہیں، چنانچہ تبدیلیاں محفوظ کرنے کی صورت میں اس صفحہ کا تاریخچہ آپ کا آئی پی پتا محفوظ کر لے گا۔</em>",
        "missingsummary": "'''انتباہ:''' آپ نے ترمیمی خلاصہ مہیّا نہیں کیا.\nاگر آپ نے محفوظ کا بٹن دوبارہ دبایا تو آپ کی ترمیم بغیر کسی خلاصہ کے محفوظ ہوجائے گی.",
+       "selfredirect": "<strong>انتباہ:</strong> آپ پیش نظر صفحہ کو خود اسی کی جانب رجوع مکرر کر رہے ہیں۔\nشاید آپ نے رجوع مکرر کا غلط ہدف درج کیا ہے یا غلط صفحہ میں ترمیم کر رہے ہیں۔\nتاہم اگر آپ «{{int:savearticle}}» پر دوبارہ کلک کریں تو بہرصورت اس رجوع مکرر کو بنا دیا جائے گا۔",
        "missingcommenttext": "براہِ کرم! تبصرہ نیچے درج کیجئے.",
        "missingcommentheader": "<strong>یاددہانی:</strong>  آپ نے اِس تبصرہ کیلئے عنوان یا شہ سرخی مہیّا نہیں کی ہے۔\nاگر آپ نے \"{{int:savearticle}}\" کا بٹن دوبارہ دبایا تو آپ کا تبصرہ بغیر کسی عنوان کے محفوظ ہوجائے گا۔",
        "summary-preview": "نمائش خلاصہ:",
        "subject-preview": "عنوان/شہ سرخی کی نمائش:",
+       "previewerrortext": "آپ کی تبدیلیوں کی نمائش دکھانے کے دوران میں کوئی نقص واقع ہو گیا ہے۔",
        "blockedtitle": "صارف مسدود ہے",
        "blockedtext": "'''آپکا اسمِ صارف یا آئی پی پتہ پر پابندی ہے.'''\n\n$1 نے پابندی لگائی تھی.\nوجہ یہ بتائی گئی کہ ''$2''.\n\n* پابندی کی ابتداء : $8\n* پابندی کا اختتام : $6\n* Intended blockee: $7\n\nآپ $1 یا کسی دوسرے [[{{MediaWiki:Grouppage-sysop}}|منتظم]] سے رابطہ کرکے پابندی پر بات چیت کرسکتے ہیں.\nآپ ‘صارف کو برقی خط ارسال کریں’ کی خاصیت اُس وقت تک استعمال نہیں کرسکتے جب تک آپ اپنے [[Special:Preferences|کھاتہ کے ترجیحات]] میں صحیح برقی پتہ معیّن نہ کریں، اور آپ کو اِسے استعمال کرنے سے پابند نہیں کیا گیا ہے.\nآپکا موجودہ آئی پی پتہ $3 ہے، اور پابندی کی شناخت #$5 ہے.\nبراہِ مہربانی کسی بھی قسم کے استفسار میں درج بالا تمام تفاصیل شامل کریں.",
        "blockednoreason": "کوئی وجہ نہیں دی گئی",
        "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اگر آپ اس صفحہ کو تخلیق یا اس میں ترمیم کرنا چاہتے ہیں تو براہ کرم پہلے جانچ لیں۔",
        "userpage-userdoesnotexist-view": "صارف کھاتہ \"$1\" مندرج نہیں ہے۔",
+       "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}}» کی بٹن استعمال کریں۔",
+       "usercsspreview": "<strong>یاد رہے کہ اس وقت آپ اپنی سی ایس کی محض نمائش دیکھ رہے ہیں، یہ اب تک محفوظ نہیں ہوئی ہے!</strong>",
+       "userjspreview": "<strong>یاد رہے کہ اس وقت آپ اپنی جاوا اسکرپٹ کی محض نمائش دیکھ/جانچ رہے ہیں، یہ اب تک محفوظ نہیں ہوئی ہے!</strong>",
+       "sitecsspreview": "<strong>یاد رہے کہ اس وقت آپ اس سی ایس کی محض نمائش دیکھ رہے ہیں، یہ اب تک محفوظ نہیں ہوئی ہے!</strong>",
+       "sitejspreview": "<strong>یاد رہے کہ اس وقت آپ اس جاوا اسکرپٹ کوڈ کی محض نمائش دیکھ رہے ہیں، یہ اب تک محفوظ نہیں ہوئی ہے!</strong>",
+       "userinvalidcssjstitle": "<strong>انتباہ:</strong> یہاں «$1» نام سے کوئی پوشاک موجود نہیں۔ شخصی .css اور .js کے صفحات اپنے عنوان میں چھوٹے حروف استعمال کرتے ہیں، مثلاً {{ns:user}}:Foo/Vector.css کی بجائے {{ns:user}}:Foo/vector.css",
        "updated": "(اپ ڈیٹڈ)",
        "note": "'''نوٹ:'''",
        "previewnote": "'''یاد رکھیں، یہ صرف نمائش ہے ۔آپ کی ترامیم ابھی محفوظ نہیں کی گئیں۔'''",
-       "session_fail_preview": "معاف کیجئے! نشست کے مواد میں خامی کی وجہ سے آپکی  ترمیم پر عمل نہیں کیا جاسکا.\nبرائے مہربانی دوبارہ کوشش کیجئے.\nاگر آپکو پھر بھی مشکل پیش آرہی ہے تو [[Special:UserLogout|خارجِ نوشتہ]] ہوکر واپس داخلِ نوشتہ ہوجایئے.",
+       "continue-editing": "خانہ ترمیم میں جائیں",
+       "previewconflict": "اس نمائش میں خانہ ترمیم کے اوپر موجود متن جس انداز میں ظاہر ہو رہا ہے، محفوظ کرنے کے بعد اسی طرح نظر آئے گا۔",
+       "session_fail_preview": "معذرت! نشست کے مواد میں خامی کی وجہ سے آپ کی  ترمیم مکمل نہیں ہو سکی۔\n\nشاید آپ اپنے کھاتے سے خارج ہو گئے ہیں۔ <strong>براہ کرم اس بات کی تصدیق کر لیں کہ آپ داخل ہیں اور دوبارہ کوشش کریں۔</strong> اگر آپ کو پھر بھی مشکل پیش آرہی ہو تو ایک بار [[Special:UserLogout|خارج ہو کر]] واپس داخل ہو جائیں اور اپنے براؤزر کو جانچ لیں کہ آیا وہ اس سائٹ کی کوکیز اخذ کر رہا ہے یا نہیں۔",
+       "edit_form_incomplete": "<strong>خانہ ترمیم سے کچھ حصے سرور تک نہیں پہنچ سکے ہیں؛ براہ کرم اپنی ترامیم کو دوبارہ جانچ لیں کہ آیا وہ برقرار ہیں یا نہیں اور دوبارہ کوشش کریں۔</strong>",
        "editing": "آپ \"$1\" میں ترمیم کر رہے ہیں۔",
        "creating": "زیر تخلیق $1",
        "editingsection": "$1 کے قطعہ کی تدوین",
        "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-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": "حـالیـہ تـجدید",
        "history-edit-tags": "منتخب نظرثانیوں کے ٹیگوں میں ترمیم کریں",
        "rev-deleted-comment": "(تبصرہ حذف کی گيا ہے)",
        "rev-deleted-user": "(صارف نام حذف کیا گيا ہے)",
+       "rev-deleted-event": "(نوشتہ کی تفصیلات ہٹا دی گئیں)",
+       "rev-deleted-user-contribs": "[صارف نام یا آئی پی پتہ ہٹا دیا گیا - شراکتوں سے ترمیم پوشیدہ ہو گئی]",
+       "rev-deleted-text-permission": "پیش نظر صفحہ کی یہ ترمیم <strong>حذف کر دی گئی ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-suppressed-text-permission": "پیش نظر صفحہ کی یہ ترمیم <strong>پوشیدہ کر دی گئی ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/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-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:",
        "compareselectedversions": "منتخب متـن کا موازنہ",
+       "showhideselectedversions": "منتخب نسخوں کی مرئیت تبدیل کریں",
        "editundo": "رد ترمیم",
        "diff-empty": "(کوئی فرق نہیں)",
-       "diff-multi-sameuser": "({{PLURAL: $1 | ایک متوسط نظرثانی | $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}}",
        "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]]\" نامی ایک صفحہ موجود ہے'''",
+       "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": "کثیرالوسیط",
        "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": "(مزید)",
        "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>$1</strong> از <strong>$3</strong>|نتائج <strong>$1 - $2</strong> از <strong>$3</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": "زیر نظر فہرست کی کلید:",
        "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-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": "صفحہ کا حجم تبدیل شدہ بلحاظ بائٹ مقدار",
-       "recentchanges-legend-heading": "<strong>اختیارات</strong>",
+       "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> تک دکھائی جا رہی ہیں۔",
        "rcshowhidebots-show": "دکھائیں",
        "rcshowhidebots-hide": "چھپائیں",
        "rcshowhideliu": "داخل شدہ صارف $1",
-       "rcshowhideliu-show": "دکھاؤ",
+       "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-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": "زبراثقال ملف (اپ لوڈ فائل)",
+       "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 کا ربط درج کرتے ہیں تو ربط کام نہیں کرے گا۔",
+       "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": "درج ذیل میں حالیہ زبراثقال (اپ لوڈ) کی گئی املاف (فائلوں) کی فہرست دی گئی ہے۔",
+       "filename": "فائل کا نام",
        "filedesc": "خلاصہ",
        "fileuploadsummary": "خلاصہ :",
+       "filereuploadsummary": "فائل کی تبدیلیاں:",
+       "filestatus": "کاپی رائٹ کی صورت حال:",
        "filesource": "ذرائع",
        "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]]",
+       "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": "فائل محفوظ کریں",
+       "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-done": "مکمل",
        "upload-dialog-button-save": "محفوظ",
        "upload-form-label-own-work": "یہ میرا ذاتی کام ہے",
        "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-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-errclear": "فائل کی صفائی ناکام۔",
+       "uploadstash-refresh": "فائلوں کی فہرست کو تازہ کریں",
+       "uploadstash-thumbnail": "تھمب نیل دیکھیں",
+       "invalid-chunk-offset": "آفسیٹ کا قطعہ نادرست ہے",
+       "img-auth-accessdenied": "رسائی معطل",
+       "img-auth-nofile": "فائل «$1» موجود نہیں ہے۔",
+       "http-invalid-url": "نادرست یوآرایل: $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": "حذف",
+       "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-show-all": "تصویروں کے پرانے نسخے شامل کریں",
        "listfiles-latestversion": "موجودہ ورژن",
        "listfiles-latestversion-yes": "ہاں",
        "listfiles-latestversion-no": "نہیں",
        "filehist-datetime": "تاریخ/وقت",
        "filehist-thumb": "اظفورہ",
        "filehist-thumbtext": "$1 کا تھمب نیل (thumbnail) ورژن",
+       "filehist-nothumb": "تھمب نیل نہیں ہے",
        "filehist-user": "صارف",
        "filehist-dimensions": "ابعاد",
        "filehist-filesize": "تصویر کا حجم",
        "imagelinks": "ملف کا استعمال",
        "linkstoimage": "اِس ملف کے ساتھ درج ذیل {{PLURAL:$1|صفحہ مربوط ہے|$1 صفحات مربوط ہیں}}",
        "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-comment": "وجہ:",
+       "filerevert-defaultcomment": "مورخہ $1 $2 بجے ($3) کے نسخے کی جانب واپس پھیر دیا گیا",
+       "filerevert-submit": "استرجع کریں",
+       "filedelete": "$1 کو حذف کریں",
+       "filedelete-legend": "فائل حذف کریں",
        "filedelete-comment": "وجہ:",
        "filedelete-submit": "حذف کریں",
        "filedelete-success": " (\"اقدام مکمل ہوا\")۔",
        "filedelete-success-old": " (\"اقدام مکمل ہوا\")",
+       "filedelete-nofile": "<strong>$1</strong> موجود نہیں ہے۔",
+       "filedelete-otherreason": "دوسری/اضافی وجہ:",
+       "filedelete-reason-otherlist": "دوسری وجہ",
+       "filedelete-reason-dropdown": "* عمومی وجوہات حذف\n** کاپی رائٹ کی خلاف ورزی\n** دوہری فائل",
+       "filedelete-edit-reasonlist": "حذف کی وجوہات میں ترمیم کریں",
+       "filedelete-maintenance-title": "فائل حذف نہیں کی جا سکتی",
+       "mimesearch": "MIME تلاش",
+       "mimetype": "MIME قسم:",
        "download": "زیراثقال (ڈاؤن لوڈ)",
+       "unwatchedpages": "نادیدہ صفحات",
        "listredirects": "فہرست متبادل ربط",
+       "listduplicatedfiles": "مکررات کے ساتھ فائلوں کی فہرست",
        "unusedtemplates": "غیر استعمال شدہ سانچے",
        "unusedtemplateswlh": "دیگر روابط",
        "randompage": "بےترتیب صفحہ",
+       "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": "متحرک صارفین",
+       "pageswithprop": "صفحات مع خاصیت صفحہ",
+       "pageswithprop-legend": "صفحات مع خاصیت صفحہ",
+       "pageswithprop-text": "اس صفحہ میں ان تمام صفحات کی فہرست موجود ہے جس کسی مخصوص خاصیت صفحہ کو استعمال کر رہے ہیں۔",
+       "pageswithprop-prop": "نام خاصیت:",
        "pageswithprop-submit": "ٹھیک",
        "doubleredirects": "دوہرے متبادل ربط",
+       "double-redirect-fixed-move": "[[$1]] کو منتقل کر دیا گیا۔\nیہ از خود تازہ ہو گیا اور اب [[$2]] سے رجوع مکرر ہے۔",
        "brokenredirects": "نامکمل متبادل ربط",
+       "brokenredirectstext": "ذیل کے رجوع مکررات غیر موجود صفحات سے مربوط ہیں:",
        "brokenredirects-edit": "ترمیم کریں",
        "brokenredirects-delete": "حذف",
+       "withoutinterwiki": "صفحات بدون بین الویکی روابط",
+       "withoutinterwiki-summary": "درج ذیل صفحات دوسری زبان کے صفحات سے مربوط نہیں ہیں۔",
        "withoutinterwiki-legend": "سابقہ",
        "withoutinterwiki-submit": "دکھائیں",
        "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|صفحہ|صفحات}}",
        "uncategorizedtemplates": "غیر زمرہ بند سانچہ جات",
        "unusedcategories": "غیر استعمال شدہ زمرہ جات",
        "unusedimages": "غیر استعمال شدہ فائلیں",
-       "wantedcategories": "طلب شدہ زمرہ جات",
+       "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": "تمام مقالات",
+       "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-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": "صفحات مع نظرانداز کردہ عناوین",
+       "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": "زیرنظرفہرست",
        "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  دیکھیۓ",
        "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-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-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": "مدت خاتمہ  $1 (یو ٹی سی)",
        "protect-expiring-local": "مدت خاتمہ  $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": "اپلوڈ",
        "undeletepagetitle": "'''ذیل میں [[:$1|$1]] کے حذف شدہ ترامیم درج ہیں۔'''",
        "viewdeletedpage": "حذف شدہ صفحات دیکھیے",
        "undelete-fieldset-title": "ترامیم بحال کریں",
-       "undeletehistory": "اگر آپ اس صفحہ کو بحال کرتے ہیں، تو اس صفحہ کے تاریخچہ میں تمام ترامیم بھی بحال ہوجائیگی۔\nاگر حذف شدگی کے بعد کوئی نیا صفحہ اسی نام سے تخلیق کیا گیا ہو، تو تمام بحال شدہ ترامیم گذشتہ تاریخچہ میں ظاہر ہوگی۔",
-       "undeleterevdel": "بحالیٔ صفحہ کا اقدام مکمل نہیں ہوگا اگر اس کا تنیجہ صفحہ کے اوپر کے حصہ کی ترمیم یا ملف کا اعادہ جزوی طور پر حذف کیا جارہا ہو۔\nایسی صورت میں لازمی طور آپ حالیہ حذف شدہ اعادے کو ظاہر کریں۔",
+       "undeleterevisions": "$1 {{PLURAL:$1|نسخہ حذف کیا گیا|نسخے حذف کیے گئے}}",
+       "undeletehistory": "اگر آپ اس صفحہ کو بحال کرتے ہیں، تو اس صفحہ کے تاریخچہ میں موجود تمام ترامیم بھی بحال ہو جائیں گی۔\nاگر حذف شدگی کے بعد کوئی نیا صفحہ اسی نام سے بنایا گیا ہو، تو تمام بحال شدہ ترامیم گذشتہ تاریخچہ میں ظاہر ہوگی۔",
+       "undeleterevdel": "اگر صفحہ یا فائل کے آخری نسخے کو جزوی طور پر حذف کیا جا رہا ہو تو بحالیٔ صفحہ کا اقدام مکمل نہیں ہوگا۔\nایسی صورت میں لازمی طور آپ حالیہ حذف شدہ نسخے کو بھی بحال کریں۔",
+       "undeletehistorynoadmin": "اس صفحہ کو حذف کر دیا گیا ہے۔\nذیل میں صفحہ حذف کرنے کی وجہ درج ہے، اور ساتھ ہی ان صارفین کی تفصیلات بھی موجود ہیں جنہوں نے صفحہ حذف ہونے سے قبل اس میں ترمیم کی تھی۔\nحذف شدہ نسخوں کا اصل متن محض منتظمین کے لیے دستیاب ہے۔",
+       "undelete-revision": "$3 کی جانب سے (مورخہ $4 بوقت $5 بجے) تحریر کردہ $1 کا حذف شدہ نسخہ:",
+       "undelete-nodiff": "کوئی پرانا نسخہ نہیں ملا۔",
        "undeletebtn": "بحال",
        "undeletelink": "دیکھو/بحال کرو",
        "undeleteviewlink": "دکھاؤ",
        "undeletedrevisions": "{{PLURAL:$1|1 نظر ثانی|$1 نظر ثانیاں}} بحال",
        "undeletedrevisions-files": "{{PLURAL:$1|1 نظر ثانی|$1 نظر ثانیاں}} اور {{PLURAL:$2|1 ملف|$2 املاف}} بحال",
        "undeletedfiles": "{{PLURAL:$1|1 ملف|$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-cleanup-error": "غیر مستعمل تاریخچہ «$1» کو حذف کرنے کے دوران میں نقص۔",
+       "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": "متعلقہ نام فضا",
+       "invert": "انتخاب معکوس",
+       "tooltip-invert": "منتخب نام فضا (اور مُلحقہ نام فضا) میں شامل صفحات کی تبدیلیوں کو چھپانے کے لیے اس خانہ کو نشان زد کریں۔",
+       "tooltip-whatlinkshere-invert": "منتخب نام فضا میں موجود صفحات کے روابط چھپانے کے لیے اس خانہ کو نشان زد کریں۔",
+       "namespace_association": "ملحقہ نام فضا",
+       "tooltip-namespace_association": "منتخب نام فضا سے منسلک تبادلۂ خیال یا ذیلی نام فضا کو شامل کرنے کے لیے اس خانہ کو نشان زد کریں",
        "blanknamespace": "(مرکز)",
-       "contributions": "{{GENDER:$1|صارف}} شراکتیں",
-       "contributions-title": "مساہماتِ صارف برائے $1",
+       "contributions": "{{GENDER:$1|صارف}} کی شراکتیں",
+       "contributions-title": "صارف $1 کی شراکتیں",
        "mycontris": "شراکت",
        "anoncontribs": "شراکتیں",
        "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-page": "صفحہ:",
-       "linkshere": "'''[[:$1]]''' سے درج ذیل صفحات مربوط ہیں:",
+       "linkshere": "<strong>[[:$1]]</strong> سے درج ذیل صفحات مربوط ہیں:",
        "nolinkshere": "'''[[:$1]]''' سے کوئی روابط نہیں۔",
-       "isredirect": "لوٹایا گیا صفحہ",
+       "isredirect": "رجوع مکرر صفحہ",
        "istemplate": "شامل شدہ",
-       "isimage": "ربطِ ملف",
+       "isimage": "فائل کا ربط",
        "whatlinkshere-prev": "{{PLURAL:$1|پچھلا|پچھلے $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|اگلا|اگلے $1}}",
        "whatlinkshere-links": "روابط ←",
        "whatlinkshere-hideredirs": "رجوع مکررات $1",
        "whatlinkshere-hidetrans": "$1 استعمالات",
        "whatlinkshere-hidelinks": "روابط $1",
-       "whatlinkshere-hideimages": "رÙ\88ابطÙ\90 ØªØµØ§Ù\88Û\8cر $1",
+       "whatlinkshere-hideimages": "تصÙ\88Û\8cر Ú©Û\92 Ø±Ù\88ابط $1",
        "whatlinkshere-filters": "فلٹرذ",
        "whatlinkshere-submit": "ٹھیک",
+       "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-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": "ای میل بھیجیں",
        "blocklogpage": "نوشتۂ پابندی",
+       "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": "صارف نام پوشیدہ ہے",
+       "ipb_expiry_invalid": "وقت اختتام نادرست ہے۔",
+       "ipb_expiry_old": "وقت اختتام گزر چکا ہے۔",
+       "ipb_already_blocked": "«$1» پر پہلے ہی پابندی لگا دی گئی ہے۔",
+       "ipb-needreblock": "«$1» پر پہلے ہی پابندی لگا دی گئی ہے۔ کیا آپ ان ترتیبات کو تبدیل کرنا چاہتے ہیں؟",
+       "ipb-otherblocks-header": "دیگر {{PLURAL:$1|پابندی|پابندیاں}}",
+       "unblock-hideuser": "چونکہ اس صارف کا نام پوشیدہ ہے لہذا آپ اس صارف سے پابندی نہیں ہٹا سکتے۔",
+       "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": "اس عنوان سے کوئی صفحہ پہلے ہی موجود ہے، یا آپکا منتخب کردہ نام مستعمل نہیں۔ براۓ مہربانی دوسرا نام منتخب کیجیۓ۔",
+       "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": "اس ہدف عنوان کی جانب منتقل نہیں کیا جا سکتا۔",
+       "imagenocrossnamespace": "فائل کو غیر فائل نام فضا میں منتقل نہیں کیا جا سکتا۔",
+       "nonfile-cannot-move-to-file": "غیر فائل کو فائل نام فضا میں منتقل نہیں کیا جا سکتا۔",
+       "imagetypemismatch": "نئی فائل کی توسیع اس کی نوعیت کے مطابق نہیں ہے۔",
+       "imageinvalidfilename": "ہدف فائل کا نام نادرست ہے۔",
+       "fix-double-redirects": "اصل عنوان کی جانب موجود تمام رجوع مکررات کو بھی تازہ کریں",
+       "move-leave-redirect": "پیچھے رجوع مکرر بنائیں",
        "protectedpagemovewarning": "<strong>انتباہ:</strong> اس صفحہ کو محفوظ کر دیا گیا ہے اور اب محض منتظمین ہی اسے منتقل کر سکتے ہیں۔\nحوالہ کے لیے نوشتہ کا جدید اندراج ذیل میں درج ہے:",
+       "semiprotectedpagemovewarning": "<strong>اطلاع:</strong> یہ صفحہ نیم محفوظ ہے اور اسے محض مندرج صارفین ہی منتقل کر سکتے ہیں۔\nذیل میں حوالہ کے لیے نوشتہ کا تازہ ترین اندراج موجود ہے:",
        "export": "برآمد صفحات",
+       "exportall": "تمام صفحات برآمد کریں",
+       "exportcuronly": "مکمل تاریخچہ کی بجائے محض موجودہ نسخہ کو شامل کریں",
+       "exportnohistory": "----\n<strong>اطلاع:</strong> اس فارم کے ذریعہ صفحات کے مکمل تاریخچہ کی برآمد کو بوجوہ غیر فعال کر دیا گیا ہے۔",
+       "exportlistauthors": "ہر صفحہ کے مشارکت کنندگان کی مکمل فہرست شامل کریں",
+       "export-submit": "برآمد کریں",
+       "export-addcattext": "اس زمرہ سے صفحات شامل کریں:",
+       "export-addcat": "شامل کریں",
+       "export-addnstext": "اس نام فضا سے صفحات شامل کریں:",
+       "export-addns": "شامل کریں",
+       "export-download": "فائل کے طور پر محفوظ کریں",
+       "export-templates": "سانچے شامل کریں",
+       "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_image-type": "تصویر کی نوعیت معاونت یافتہ نہیں ہے",
+       "thumbnail_image-missing": "معلوم ہوتا ہے کہ یہ فائل موجود نہیں: $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": "تبصرہ:",
+       "importstart": "صفحات درآمد کیے جا رہے ہیں۔۔۔",
+       "import-revision-count": "$1 {{PLURAL:$1|نسخہ|نسخے}}",
+       "importnopages": "درآمد کرنے کے لیے کوئی صفحہ نہیں ہے۔",
+       "imported-log-entries": "درآمد کردہ $1 {{PLURAL:$1|اندراج نوشتہ|اندراجات نوشتہ}}۔",
+       "importfailed": "درآمد ناکام: <nowiki>$1</nowiki>",
+       "importcantopen": "درآمد فائل کھل نہیں سکی",
+       "importbadinterwiki": "غلط بین الویکی ربط",
+       "importsuccess": "درآمد مکمل!",
+       "importnofile": "کسی درآمد فائل کو اپلوڈ نہیں کیا گیا۔",
+       "import-noarticle": "درآمد کرنے کے لیے کوئی صفحہ موجود نہیں!",
+       "import-invalid-interwiki": "اس ویکی سے درآمد نہیں کیا جا سکتا۔",
+       "import-error-edit": "صفحہ «$1» درآمد نہیں کیا جا سکا کیونکہ آپ کو اس میں ترمیم کرنے کی اجازت نہیں ہے۔",
+       "import-error-create": "صفحہ «$1» درآمد نہیں کیا جا سکا کیونکہ آپ کو اسے تخلیق کرنے کی اجازت نہیں ہے۔",
+       "import-error-interwiki": "صفحہ «$1» درآمد نہیں کیا جا سکا کیونکہ اس کا نام بیرونی ربط (بین الویکی) کے لیے محفوظ ہے۔",
+       "import-error-special": "صفحہ «$1» درآمد نہیں کیا جا سکا کیونکہ یہ اس خصوصی نام فضا سے متعلق ہے جس میں صفحات بنانے کی اجازت نہیں۔",
+       "import-error-invalid": "صفحہ «$1» درآمد نہیں کیا جا سکا کیونکہ درآمد کے بعد اس صفحہ کا جو نام ہوگا وہ اس ویکی پر نادرست ہے۔",
+       "import-options-wrong": "غلط {{PLURAL:$2|اختیار|اختیارات}}: <nowiki>$1</nowiki>",
+       "import-rootpage-invalid": "درج کردہ ماخذی صفحہ کا عنوان نادرست ہے۔",
+       "importlogpage": "نوشتہ درآمد",
+       "import-logentry-upload-detail": "$1 {{PLURAL:$1|نسخہ|نسخے}} درآمد {{PLURAL:$1|کیا گیا|کیے گئے}}",
+       "import-logentry-interwiki-detail": "$2 سے $1 {{PLURAL:$1|نسخہ|نسخے}} درآمد {{PLURAL:$1|کیا گیا|کیے گئے}}",
+       "javascripttest": "جاوا اسکرپٹ کی آزمائش",
+       "javascripttest-pagetext-unknownaction": "نامعلوم اقدام \"$1\"",
        "tooltip-pt-userpage": "آپ کا صارف صفحہ",
+       "tooltip-pt-anonuserpage": "آپ جس آئی پی سے ترمیم کاری کر رہے ہیں اس کا صارف صفحہ",
        "tooltip-pt-mytalk": "آپ کا تبادلہ خیال صفحہ",
+       "tooltip-pt-anontalk": "اس آئی پی پتے کی ترامیم سے متعلق گفتگو",
        "tooltip-pt-preferences": "آپ کی ترجیحات",
        "tooltip-pt-watchlist": "اُن صفحات کی فہرست جن کی تبدیلیاں آپ کی زیرِنظر ہیں",
        "tooltip-pt-mycontris": "آپ کی شراکتوں کی فہرست",
+       "tooltip-pt-anoncontribs": "اس آئی پی پتے سے انجام دی جانے والی تمام ترامیم کی فہرست",
        "tooltip-pt-login": "آپ کیلئے داخلِ نوشتہ ہونا اچھا ہے؛ تاہم، یہ ضروری نہیں",
        "tooltip-pt-logout": "خارجِ نوشتہ ہوجائیں",
        "tooltip-pt-createaccount": "آپ کو مدعو کیا جاتا ہے کہ کھاتہ بنائیں۔تاہم کھاتہ بنانا لازم نہیں۔",
        "tooltip-ca-viewsource": "یہ ایک محفوظ شدہ صفحہ ہے.\nآپ اِس کا مآخذ دیکھ سکتے ہیں",
        "tooltip-ca-history": "صفحۂ ہٰذا کی سابقہ نظرثانی",
        "tooltip-ca-protect": "یہ صفحہ محفوظ کیجئے",
+       "tooltip-ca-unprotect": "اس صفحہ کی حفاظت میں تبدیلی کریں",
        "tooltip-ca-delete": "یہ صفحہ حذف کریں",
        "tooltip-ca-move": "یہ صفحہ منتقل کریں",
        "tooltip-ca-watch": "اِس صفحہ کو اپنی زیرِنظرفہرست میں شامل کریں",
        "tooltip-t-recentchangeslinked": "اِس صفحہ سے مربوط صفحات میں حالیہ تبدیلیاں",
        "tooltip-feed-rss": "اِس صفحہ کیلئے اسس خورد",
        "tooltip-feed-atom": "اِس صفحہ کیلئے اٹوم خورد",
-       "tooltip-t-contributions": "نئی تدوین →",
-       "tooltip-t-emailuser": "اِس صارف کو برقی خط ارسال کریں",
+       "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-mediawiki": "نظامی پیغام دیکھیں",
        "tooltip-ca-nstab-template": "سانچہ دیکھئے",
+       "tooltip-ca-nstab-help": "صفحۂ معاونت دیکھیں",
        "tooltip-ca-nstab-category": "زمرہ‌جاتی صفحہ دیکھئے",
        "tooltip-minoredit": "اِس تدوین کو بطورِ معمولی ترمیم نشانزد کیجئے",
        "tooltip-save": "تبدیلیاں محفوظ کیجئے",
+       "tooltip-publish": "اپنی تبدیلیاں شائع کریں",
        "tooltip-preview": "برائے مہربانی! محفوظ کرنے سے پہلے تبدیلیوں کا پیش منظر دیکھيے",
        "tooltip-diff": "دیکھئے کہ اپنے متن میں کیا تبدیلیاں کیں",
        "tooltip-compareselectedversions": "اِس صفحہ کی دو منتخب نظرثانیوں میں فرق دیکھئے",
        "tooltip-watch": "اِس صفحہ کو اپنی زیرِنظرفہرست میں شامل کریں",
+       "tooltip-watchlistedit-normal-submit": "عناوین حذف کریں",
+       "tooltip-watchlistedit-raw-submit": "زیرنظر فہرست کی تجدید کریں",
+       "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": "اس صفحہ کے انتسابات سے متعلق معلومات دستیاب نہیں ہیں۔",
+       "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-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": "تعداد املاف",
+       "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۔",
+       "filedeleteerror-short": "فائل حذف کاری میں نقص: $1",
+       "filedeleteerror-long": "فائل حذف کرنے کے دوران میں نقص واقع ہوا:\n\n$1",
+       "filedelete-missing": "فائل «$1» کو حذف نہیں کیا جا سکتا کیونکہ یہ موجود نہیں ہے۔",
+       "filedelete-old-unregistered": "فائل «$1» کا منتخب نسخہ ڈیٹابیس میں موجود نہیں ہے۔",
+       "filedelete-current-unregistered": "«$1» کے عنوان سے کوئی فائل ڈیٹابیس میں موجود نہیں ہے۔",
        "previousdiff": "← پُرانی تدوین",
        "nextdiff": "صفحہ کا نام:",
        "imagemaxsize": "تصویر کی جسامت کی حد:<br /><em>(فائل کے توضیحی صفحات کے لیے)</em>",
        "thumbsize": "تھمب نیل کی جسامت:",
+       "widthheightpage": "$1×$2، $3 {{PLURAL:$3|صفحہ|صفحات}}",
+       "file-info": "فائل کا حجم: $1، MIME قسم: $2",
        "file-info-size": "\n$1 × $2 عکصر (پکسلز)، حجم ملف: $3، MIME قسم: $4",
+       "file-info-size-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-preview-differ": "اس $2 فائل کی $3 نمائش کا حجم: $1",
        "show-big-image-other": "دیگر {{PLURAL:$2|تجویز|تجویزیں}}: $1۔",
        "show-big-image-size": "$1 × $2 pixels",
+       "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|سیکنڈ|سیکنڈ}} قبل",
+       "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-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-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-datetime": "ملف کے تبدیلی کا تاریخ او وقت",
+       "exif-imagedescription": "تصویر کا عنوان",
        "exif-make": "کیمرے کا صانع",
        "exif-model": "کیمرے کا ماڈل",
        "exif-software": "سافٹویئر استعمال",
+       "exif-artist": "مصنف",
+       "exif-copyright": "کاپی رائٹ کا حامل",
        "exif-exifversion": "اکزیف ورژن",
        "exif-colorspace": "رنگ فضا",
+       "exif-pixelxdimension": "تصویر کی چوڑائی",
+       "exif-pixelydimension": "تصویر کی لمبائی",
+       "exif-usercomment": "صارف کے تبصرے",
        "exif-datetimeoriginal": "ڈیٹا بنانے کا تاریخ اور وقت",
        "exif-datetimedigitized": "معددی کا تاریخ اور وقت",
+       "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-lightsource": "روشنی کا ماخذ",
+       "exif-flash": "فلیش",
+       "exif-focallength": "عدسہ کی ماسکی لمبائی",
+       "exif-subjectarea": "شئی کی مساحت",
+       "exif-flashenergy": "فلیش توانائی",
+       "exif-focalplanexresolution": "X کی تحلیل کا درجہ ماسکہ",
+       "exif-focalplaneyresolution": "Y کی تحلیل کا درجہ ماسکہ",
+       "exif-focalplaneresolutionunit": "درجہ ماسکہ کی تحلیل کی اکائی",
+       "exif-subjectlocation": "شئی کا محل وقوع",
+       "exif-exposureindex": "نمائش کا اشاریہ",
+       "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-gpslatitude": "عرض البلد",
+       "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-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-copyrighted-true": "کاپی رائٹ شدہ",
+       "exif-copyrighted-false": "کاپی رائٹ کی صورت حال متعین نہیں کی گئی",
+       "exif-photometricinterpretation-1": "سیاہ اور سفید (سیاہ 0 ہے)",
+       "exif-unknowndate": "نامعلوم تاریخ",
        "exif-orientation-1": "عام",
+       "exif-componentsconfiguration-0": "موجود نہیں",
+       "exif-exposureprogram-0": "غیر متعین",
+       "exif-exposureprogram-1": "دستی",
+       "exif-exposureprogram-2": "عام پروگرام",
+       "exif-exposureprogram-4": "شٹر کی ترجیح",
+       "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-4": "فلیش",
+       "exif-lightsource-9": "اچھا موسم",
+       "exif-lightsource-10": "ابرآلود موسم",
+       "exif-lightsource-11": "سایہ",
+       "exif-focalplaneresolutionunit-2": "انچ",
+       "exif-sensingmethod-1": "غیر وضاحتی",
+       "exif-customrendered-0": "عام عمل",
+       "exif-customrendered-1": "اپنی مرضی کے مطابق عمل",
+       "exif-contrast-0": "عام",
+       "exif-contrast-1": "نرم",
+       "exif-contrast-2": "سخت",
+       "exif-saturation-0": "عام",
+       "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-gpsspeed-k": "کلو میٹر فی گھنٹہ",
+       "exif-gpsspeed-m": "میل فی گھنٹہ",
+       "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-dc-contributor": "ترمیم کنندگان",
+       "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)",
        "namespacesall": "تمام",
        "monthsall": "تمام",
-       "deletedwhileediting": "انتباہ: آپ کے ترمیم شروع کرنے کے بعد یہ صفحہ حذف کیا جا چکا ہے!",
+       "confirmemail": "اپنے برقی پتہ کی تصدیق کریں",
+       "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_invalidated": "برقی ڈاک پتے کی تصدیق منسوخ ہو گئی",
+       "invalidateemail": "برقی ڈاک کی تصدیق منسوخ کریں",
+       "scarytranscludetoolong": "[یوآرایل بہت طویل ہے]",
+       "deletedwhileediting": "<strong>انتباہ:</strong>: آپ کے ترمیم شروع کرنے کے بعد یہ صفحہ حذف کیا جا چکا ہے!",
+       "recreate": "دوبارہ تخلیق کریں",
        "confirm_purge_button": "جی!",
+       "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-go": "ٹھیک",
        "table_pager_next": "اگلا صفحہ",
        "table_pager_prev": "پچھلا صفحہ",
        "table_pager_first": "پہلا صفحہ",
        "table_pager_last": "آخری صفحہ",
        "autosumm-blank": "تمام مندرجات حذف",
        "autoredircomment": "[[$1]] سے رجوع مکرر",
-       "autosumm-new": "نیا صفحہ: $1",
+       "autosumm-new": "«$1» پر مشتمل نیا صفحہ بنایا",
+       "autosumm-newblank": "خالی صفحہ بنایا",
        "size-bytes": "$1 بائٹ",
+       "watchlistedit-normal-title": "زیر نظر فہرست میں ترمیم کریں",
+       "watchlistedit-normal-legend": "زیرنظر فہرست سے عناوین نکالیں",
+       "watchlistedit-normal-submit": "عناوین نکالیں",
+       "watchlistedit-raw-title": "خام زیرِنظرفہرست میں ترمیم کریں",
+       "watchlistedit-raw-legend": "خام زیرِنظرفہرست میں ترمیم کریں",
+       "watchlistedit-raw-titles": "عناوین:",
+       "watchlistedit-raw-submit": "زیرنظر فہرست کی تجدید کریں",
+       "watchlistedit-raw-done": "آپ کی زیرنظر فہرست کی تجدید ہو چکی ہے۔",
+       "watchlistedit-clear-title": "اپنی زیر نظر فہرست صاف کریں",
+       "watchlistedit-clear-legend": "اپنی زیر نظر فہرست صاف کریں",
+       "watchlistedit-clear-titles": "عناوین:",
        "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": "مقامی",
        "restricted-displaytitle": "<strong>انتباہ!:</strong> عنوان \"$1\" کو نظر انداز کر دیا گیا ہے کیونکہ یہ متعلقہ صفحہ کے عنوان کا حقیقی متبادل نہیں ہے۔",
        "version": "ورژن",
        "version-specialpages": "خاص صفحات",
        "version-other": "دیگر",
+       "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-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-submit": "چلیں",
+       "redirect-lookup": "تلاش:",
+       "redirect-value": "قدر:",
+       "redirect-user": "صارف کی شناخت",
+       "redirect-page": "صفحہ کی شناخت",
+       "redirect-revision": "صفحہ کا نسخہ",
+       "redirect-file": "فائل کا نام",
+       "redirect-logid": "نوشتہ کی شناخت",
+       "redirect-not-exists": "یہ قدر نہیں ملی",
+       "fileduplicatesearch": "مکرر فائلوں کی تلاش",
+       "fileduplicatesearch-filename": "فائل کا نام:",
        "fileduplicatesearch-submit": "تلاش",
+       "fileduplicatesearch-info": "$1 × $2 پکسل <br />فائل کا حجم: $3<br />MIME قسم: $4",
        "specialpages": "خصوصی صفحات",
        "specialpages-group-pages": "فہارست صفحات",
        "tag-filter": "[[Special:Tags|لوحہ]] فلٹر:",
        "tags-active-header": "فعال؟",
        "tags-active-yes": "ہاں",
        "tags-active-no": "نہیں",
+       "tags-edit": "ترمیم",
        "tags-delete": "حذف",
        "tags-activate": "فعال کریں",
        "tags-deactivate": "غیر فعال  کریں",
        "tags-hitcount": "$1 {{PLURAL:$1|تبدیلی|تبدیلیاں}}",
+       "tags-create-tag-name": "ٹیگ کا نام:",
+       "tags-create-reason": "وجہ:",
        "tags-create-submit": "تخلیق",
+       "tags-create-no-name": "آپ نے ایک ٹیگ کا نام دینا ہو گا۔",
        "tags-delete-title": "حذف ٹیگ",
        "tags-delete-reason": "وجہ:",
        "tags-activate-title": "ٹیگ فعال",
        "tags-deactivate-reason": "وجہ:",
        "tags-deactivate-submit": "غیر فعال",
        "tags-edit-title": "ٹیگ میں ترمیم",
+       "tags-edit-manage-link": "ٹیگ کا انتظام",
+       "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": "وجہ:",
+       "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": "چند منٹ انتظار کے بعد دوبارہ کوشش کریں۔",
+       "htmlform-submit": "ٹھیک ہے",
+       "htmlform-reset": "رد ترامیم",
        "htmlform-selectorother-other": "دیگر",
        "htmlform-no": "نہیں",
        "htmlform-yes": "ہاں",
+       "htmlform-chosen-placeholder": "ایک اختیار منتخب کریں",
+       "htmlform-cloner-create": "مزید اضافہ کریں",
+       "htmlform-cloner-delete": "حذف",
+       "htmlform-cloner-required": "کم ازکم ایک قدر درکار ہے۔",
        "logentry-delete-delete": "$1 {{GENDER:$2|حذف کیا گیا}} صفحہ $3",
        "logentry-move-move": "$1 نے صفحہ $3 کو $4 کی جانب منتقل کیا",
+       "logentry-move-move_redir-noredirect": "$1 نے صفحہ $3 کو رجوع مکرر چھوڑے بغیر $4 کی جانب جو رجوع مکر تھا {{GENDER:$2|منتقل کیا}}",
        "logentry-newusers-create": "صارف کھاتہ $1 {{GENDER:$2|بنایا گیا}}",
        "logentry-protect-move_prot": "$1 نے ترتیب درجہ حفاظت $4 سے $3 کی طرف {{GENDER:$2|منتقل کی}}",
        "logentry-protect-protect": "$1 نے $3 کو {{GENDER:$2|محفوظ کیا}}  $4",
        "logentry-upload-upload": "$1 {{GENDER:$2|اپلوڈ}} $3",
        "rightsnone": "(کچھ نہیں)",
        "revdelete-summary": "خلاصۂ تدوین",
+       "feedback-back": "واپس",
+       "feedback-cancel": "منسوخ",
+       "feedback-close": "مکمل",
        "feedback-thanks-title": "شکریہ!",
        "searchsuggest-search": "تلاش",
+       "searchsuggest-containing": "نتائج...",
+       "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-walltime-value": "$1 {{PLURAL:$1|سیکنڈ}}",
+       "limitreport-postexpandincludesize-value": "$1/$2 {{PLURAL:$2|بائٹ}}",
        "expandtemplates": "سانچے کو وسیع کریں",
        "expand_templates_input": "ان پٹ متن:",
        "expand_templates_output": "نتیجہ",
        "expand_templates_ok": "ٹھیک ہے",
        "expand_templates_remove_comments": "تبصرے حذف کریں",
        "expand_templates_preview": "پیش نظارہ",
+       "pagelanguage": "صفحے کی زبان تبدیل کریں",
        "pagelang-name": "صفحہ",
        "pagelang-language": "زبان",
+       "pagelang-use-default": "طے شدہ زبان استعمال کرتا ہے",
+       "pagelang-select-lang": "زبان کا انتخاب کریں",
+       "pagelang-submit": "ٹھیک ہے",
+       "right-pagelang": "صفحے کی زبان تبدیل کریں",
+       "action-pagelang": "صفحے کی زبان تبدیل کریں",
+       "log-name-pagelang": "نوشتہ تبدیلی زبان",
+       "log-description-pagelang": "ذیل میں زبانوں کے صفحہ میں ہونے والی تبدیلیوں کا نوشتہ ہے۔",
+       "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (فعال)",
+       "mediastatistics": "میڈیا کے اعداد و شمار",
+       "mediastatistics-header-unknown": "نامعلوم",
+       "mediastatistics-header-office": "دفتر",
        "special-characters-group-latin": "لاطینی محارف",
        "special-characters-group-latinextended": "وسیع لاطینی",
        "special-characters-group-symbols": "علامات",
index 27b2c14..24eafd7 100644 (file)
        "tog-enotifminoredits": "当我的监视列表中的页面和文件有小编辑时也发送电子邮件通知我",
        "tog-enotifrevealaddr": "在通知电子邮件中显示我的电子邮件地址",
        "tog-shownumberswatching": "显示监视用户数",
-       "tog-oldsig": "当前签名:",
+       "tog-oldsig": "您现有的签名:",
        "tog-fancysig": "将签名视为维基文本(不自动生成链接)",
        "tog-uselivepreview": "使用实时预览",
        "tog-forceeditsummary": "未输入编辑摘要时提醒我",
        "tog-showhiddencats": "显示隐藏分类",
        "tog-norollbackdiff": "执行回退后不显示差异",
        "tog-useeditwarning": "当我离开编辑页面时,如果有尚未保存的更改,请提醒我",
-       "tog-prefershttps": "登录时始终使用安全连接",
+       "tog-prefershttps": "在登录时总是使用安全连接",
        "underline-always": "始终",
        "underline-never": "从不",
        "underline-default": "皮肤或浏览器默认设置",
        "newwindow": "(在新窗口中打开)",
        "cancel": "取消",
        "moredotdotdot": "更多...",
-       "morenotlisted": "æ\9c¬å\88\97表不完整。",
+       "morenotlisted": "æ­¤å\88\97表å\8f¯è\83½不完整。",
        "mypage": "页面",
        "mytalk": "讨论",
        "anontalk": "讨论",
        "talk": "讨论",
        "views": "视图",
        "toolbox": "工具",
+       "tool-link-userrights": "更改{{GENDER:$1|用户}}组",
+       "tool-link-emailuser": "电邮联系该{{GENDER:$1|用户}}",
        "userpage": "查看用户页面",
        "projectpage": "查看项目页面",
        "imagepage": "查看文件页面",
        "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",
index 37d164b..0f38d8e 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",
        "logentry-delete-restore": "$1 還原頁面 $3",
        "logentry-delete-event": "$1 {{GENDER:$2|已更改}} $3 中 {{PLURAL:$5|1 筆日誌|$5 筆日誌}}的可見性:$4",
index 1fbca14..7e0fb45 100644 (file)
@@ -553,8 +553,19 @@ abstract class Maintenance {
         * Set triggers like when to try to run deferred updates
         * @since 1.28
         */
-       public function setTriggers() {
+       public function setAgentAndTriggers() {
+               if ( function_exists( 'posix_getpwuid' ) ) {
+                       $agent = posix_getpwuid( posix_geteuid() )['name'];
+               } else {
+                       $agent = 'sysadmin';
+               }
+               $agent .= '@' . wfHostname();
+
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               // Add a comment for easy SHOW PROCESSLIST interpretation
+               $lbFactory->setAgentName(
+                       mb_strlen( $agent ) > 15 ? mb_substr( $agent, 0, 15 ) . '...' : $agent
+               );
                self::setLBFactoryTriggers( $lbFactory );
        }
 
index 890fe45..60b24a2 100644 (file)
@@ -104,7 +104,7 @@ $maintenance->checkRequiredExtensions();
 
 // A good time when no DBs have writes pending is around lag checks.
 // This avoids having long running scripts just OOM and lose all the updates.
-$maintenance->setTriggers();
+$maintenance->setAgentAndTriggers();
 
 // Do the work
 $maintenance->execute();
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 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 b278e98..2287559 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 );
@@ -90,6 +96,7 @@ class RebuildFileCache extends Maintenance {
                $blockEnd = $start + $this->mBatchSize - 1;
 
                $dbw = $this->getDB( DB_MASTER );
+               $mainContext = RequestContext::getMain();
                // Go through each page and save the output
                while ( $blockEnd <= $end ) {
                        // Get the pages
@@ -104,7 +111,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 +118,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 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 e6a30a3..a9fe45a 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,12 +68,13 @@ 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 );
@@ -98,14 +101,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 +130,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 +143,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 +155,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 ) {
index 63c3490..89168db 100644 (file)
@@ -166,6 +166,7 @@ return [
                'scripts' => 'resources/lib/jquery/jquery.appear.js',
        ],
        'jquery.arrowSteps' => [
+               'deprecated' => true,
                'scripts' => 'resources/src/jquery/jquery.arrowSteps.js',
                'styles' => 'resources/src/jquery/jquery.arrowSteps.css',
                'targets' => [ 'desktop', 'mobile' ],
index a4479f7..6437ca8 100644 (file)
@@ -401,7 +401,7 @@ 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;
@@ -421,7 +421,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..08d91b4 100644 (file)
@@ -524,7 +524,7 @@ 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;
@@ -544,7 +544,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;
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..cb4919f 100644 (file)
@@ -162,6 +162,7 @@ input#wpSummary {
 
 .mw-input-with-label {
        white-space: nowrap;
+       display: inline-block;
 }
 
 /**
@@ -656,33 +657,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..3cc94b8 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;
        }
 }
@@ -61,7 +61,7 @@
 
 .button-colors( @bgColor, @highlightColor, @activeColor ) when ( lightness( @bgColor ) >= 70% ) {
        color: @colorButtonText;
-       border: 1px solid @colorGray12;
+       border: 1px solid @colorFieldBorder;
 
        &:hover,
        &:active,
 .button-colors-quiet( @textColor, @highlightColor, @activeColor ) {
        // Quiet buttons all start gray, and reveal
        // constructive/progressive/destructive color on hover and active.
-       color: @colorButtonText;
+       color: @textColor;
 
-       &:hover,
-       &:focus {
+       &:hover {
                background-color: transparent;
-               color: @textColor;
+               color: @highlightColor;
        }
 
        &:active,
                color: @activeColor;
        }
 
+       &:focus {
+               background-color: transparent;
+               color: @textColor;
+       }
+
        &:disabled {
                color: @colorDisabledText;
        }
index 507109a..77e80b0 100644 (file)
@@ -2,13 +2,13 @@
 
 // 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;
 @colorGray6: #666;
-@colorGray7: #777;
+@colorGray7: #72777d;
 @colorGray8: #888;
 @colorGray9: #999;
 @colorGray10: #aaa;
 @colorGray12: #ccc;
 @colorGray13: #ddd;
 @colorGray14: #eee;
-@colorGray15: #f9f9f9; // lightest
+@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 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 cc96a5c..aedec5b 100644 (file)
        //
        // 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 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 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();
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 db5a4fb..04807f4 100644 (file)
                                 * response contain calls to this function.
                                 *
                                 * @param {string} module Name of module
-                                * @param {Function|Array} [script] Function with module code or Array of URLs to
-                                *  be used as the src attribute of a new `<script>` tag.
+                                * @param {Function|Array|string} [script] Function with module code, list of URLs
+                                *  to load via `<script src>`, or string of module code for `$.globalEval()`.
                                 * @param {Object} [style] Should follow one of the following patterns:
                                 *
                                 *     { "css": [css, ..] }
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 7df778f..52a1efb 100644 (file)
                /**
                 * Get date user registered, if available
                 *
-                * @return {Date|boolean|null} Date user registered, or false for anonymous users, or
-                *  null when data is not available
+                * @return {boolean|null|Date} False for anonymous users, null if data is
+                *  unavailable, or Date for when the user registered.
                 */
                getRegistration: function () {
-                       var registration = mw.config.get( 'wgUserRegistration' );
                        if ( mw.user.isAnon() ) {
                                return false;
                        }
-                       if ( registration === null ) {
-                               // Information may not be available if they signed up before
-                               // MW began storing this.
-                               return null;
-                       }
-                       return new Date( registration );
+                       var registration = mw.config.get( 'wgUserRegistration' );
+                       // Registration may be unavailable if the user signed up before MediaWiki
+                       // began tracking this.
+                       return !registration ? null : new Date( registration );
                },
 
                /**
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..d2968a1 100644 (file)
@@ -348,7 +348,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 +380,7 @@ class ParserTestRunner {
 
                return new RepoGroup(
                        [
-                               'class' => 'LocalRepo',
+                               'class' => 'MockLocalRepo',
                                'name' => 'local',
                                'url' => 'http://example.com/images',
                                'hashLevels' => 2,
index 920dbb3..43577ca 100644 (file)
@@ -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.
@@ -920,13 +920,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.';
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 70e4aea..4bca478 100644 (file)
@@ -4,7 +4,6 @@
  * Tests timestamp parsing and output.
  */
 class MWTimestampTest extends MediaWikiLangTestCase {
-
        protected function setUp() {
                parent::setUp();
 
@@ -12,128 +11,6 @@ class MWTimestampTest extends MediaWikiLangTestCase {
                $this->setMwGlobals( 'wgHooks', [] );
        }
 
-       /**
-        * @covers MWTimestamp::__construct
-        */
-       public function testConstructWithNoTimestamp() {
-               $timestamp = new MWTimestamp();
-               $this->assertInternalType( 'string', $timestamp->getTimestamp() );
-               $this->assertNotEmpty( $timestamp->getTimestamp() );
-               $this->assertNotEquals( false, strtotime( $timestamp->getTimestamp( TS_MW ) ) );
-       }
-
-       /**
-        * @covers MWTimestamp::__toString
-        */
-       public function testToString() {
-               $timestamp = new MWTimestamp( '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 MWTimestamp::diff
-        */
-       public function testDiff( $timestamp1, $timestamp2, $expected ) {
-               $timestamp1 = new MWTimestamp( $timestamp1 );
-               $timestamp2 = new MWTimestamp( $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 MWTimestamp::getTimestamp
-        */
-       public function testValidParse( $format, $original, $expected ) {
-               $timestamp = new MWTimestamp( $original );
-               $this->assertEquals( $expected, $timestamp->getTimestamp( TS_MW ) );
-       }
-
-       /**
-        * Test outputting valid timestamps to different formats.
-        * @dataProvider provideValidTimestamps
-        * @covers MWTimestamp::getTimestamp
-        */
-       public function testValidOutput( $format, $expected, $original ) {
-               $timestamp = new MWTimestamp( $original );
-               $this->assertEquals( $expected, (string)$timestamp->getTimestamp( $format ) );
-       }
-
-       /**
-        * Test an invalid timestamp.
-        * @expectedException TimestampException
-        * @covers MWTimestamp
-        */
-       public function testInvalidParse() {
-               new MWTimestamp( "This is not a timestamp." );
-       }
-
-       /**
-        * Test an out of range timestamp
-        * @dataProvider provideOutOfRangeTimestamps
-        * @expectedException TimestampException
-        * @covers MWTimestamp
-        */
-       public function testOutOfRangeTimestamps( $format, $input ) {
-               $timestamp = new MWTimestamp( $input );
-               $timestamp->getTimestamp( $format );
-       }
-
-       /**
-        * Test requesting an invalid output format.
-        * @expectedException TimestampException
-        * @covers MWTimestamp::getTimestamp
-        */
-       public function testInvalidOutput() {
-               $timestamp = new MWTimestamp( '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
-               ];
-       }
-
        /**
         * @dataProvider provideHumanTimestampTests
         * @covers MWTimestamp::getHumanTimestamp
index 8c2b143..41f516a 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 ) );
                }
 
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 474a481..ebc2d10 100644 (file)
@@ -645,4 +645,66 @@ class StatusTest extends MediaWikiLangTestCase {
                ];
        }
 
+       /**
+        * @dataProvider provideErrorsWarningsOnly
+        * @covers Status::getErrorsOnlyStatus
+        * @covers Status::getWarningsOnlyStatus
+        */
+       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 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 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 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 f13ead4..9480c2d 100644 (file)
@@ -33,6 +33,13 @@ class FakeDatabaseMysqlBase extends DatabaseMysqlBase {
        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() {
index e7eeff9..68bc964 100644 (file)
@@ -736,7 +736,7 @@ class DatabaseSQLTest extends MediaWikiTestCase {
        public function testDropTable() {
                $this->database->setExistingTables( [ 'table' ] );
                $this->database->dropTable( 'table', __METHOD__ );
-               $this->assertLastSql( 'DROP TABLE table' );
+               $this->assertLastSql( 'DROP TABLE table CASCADE' );
        }
 
        /**
@@ -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 e884640..48dc332 100644 (file)
@@ -25,6 +25,7 @@ class DatabaseTest extends MediaWikiTestCase {
                }
                $this->db->restoreFlags( IDatabase::RESTORE_INITIAL );
        }
+
        /**
         * @covers DatabaseBase::dropTable
         */
@@ -174,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' );
@@ -231,7 +191,7 @@ class DatabaseTest extends MediaWikiTestCase {
 
        private function dropFunctions() {
                $this->db->query( 'DROP FUNCTION IF EXISTS mw_test_function'
-                               . ( $this->db->getType() == 'postgres' ? '()' : '' )
+                       . ( $this->db->getType() == 'postgres' ? '()' : '' )
                );
        }
 
@@ -247,26 +207,35 @@ class DatabaseTest extends MediaWikiTestCase {
                $db->setFlag( DBO_TRX );
                $called = false;
                $flagSet = null;
-               $db->onTransactionIdle( function() use ( $db, &$flagSet, &$called ) {
-                       $called = true;
-                       $flagSet = $db->getFlag( DBO_TRX );
-               } );
+               $db->onTransactionIdle(
+                       function () use ( $db, &$flagSet, &$called ) {
+                               $called = true;
+                               $flagSet = $db->getFlag( DBO_TRX );
+                       },
+                       __METHOD__
+               );
                $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
                $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
                $this->assertTrue( $called, 'Callback reached' );
 
                $db->clearFlag( DBO_TRX );
                $flagSet = null;
-               $db->onTransactionIdle( function() use ( $db, &$flagSet ) {
-                       $flagSet = $db->getFlag( DBO_TRX );
-               } );
+               $db->onTransactionIdle(
+                       function () use ( $db, &$flagSet ) {
+                               $flagSet = $db->getFlag( DBO_TRX );
+                       },
+                       __METHOD__
+               );
                $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
                $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
 
                $db->clearFlag( DBO_TRX );
-               $db->onTransactionIdle( function() use ( $db ) {
-                       $db->setFlag( DBO_TRX );
-               } );
+               $db->onTransactionIdle(
+                       function () use ( $db ) {
+                               $db->setFlag( DBO_TRX );
+                       },
+                       __METHOD__
+               );
                $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
        }
 
@@ -276,7 +245,7 @@ class DatabaseTest extends MediaWikiTestCase {
                $db->clearFlag( DBO_TRX );
                $db->begin( __METHOD__ );
                $called = false;
-               $db->onTransactionResolution( function() use ( $db, &$called ) {
+               $db->onTransactionResolution( function () use ( $db, &$called ) {
                        $called = true;
                        $db->setFlag( DBO_TRX );
                } );
@@ -287,7 +256,7 @@ class DatabaseTest extends MediaWikiTestCase {
                $db->clearFlag( DBO_TRX );
                $db->begin( __METHOD__ );
                $called = false;
-               $db->onTransactionResolution( function() use ( $db, &$called ) {
+               $db->onTransactionResolution( function () use ( $db, &$called ) {
                        $called = true;
                        $db->setFlag( DBO_TRX );
                } );
@@ -302,7 +271,7 @@ class DatabaseTest extends MediaWikiTestCase {
        public function testTransactionListener() {
                $db = $this->db;
 
-               $db->setTransactionListener( 'ping', function() use ( $db, &$called ) {
+               $db->setTransactionListener( 'ping', function () use ( $db, &$called ) {
                        $called = true;
                } );
 
@@ -390,24 +359,26 @@ class DatabaseTest extends MediaWikiTestCase {
                $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 ) );
@@ -417,4 +388,26 @@ class DatabaseTest extends MediaWikiTestCase {
                $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
                $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
        }
+
+       /**
+        * @covers DatabaseBase::tablePrefix()
+        * @covers DatabaseBase::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..31b692b 100644 (file)
@@ -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 );
index 5affa9c..d72768d 100644 (file)
@@ -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 DatabaseBase $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 DatabaseBase $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 );
 
index 81c9faf..f01c47d 100644 (file)
@@ -21,6 +21,13 @@ class FakeDatabase extends DatabaseBase {
        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 ) {
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
new file mode 100644 (file)
index 0000000..88c2989
--- /dev/null
@@ -0,0 +1,128 @@
+<?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/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 9a3e222..c24a321 100644 (file)
@@ -281,7 +281,6 @@ mw.example();
                                'name' => 'test.example',
                                'scripts' => [],
                                'styles' => [ 'css' => [ '.mw-example {}' ] ],
-                               'messages' => new XmlJsCode( '{}' ),
 
                                'expected' => 'mw.loader.implement( "test.example", [], {
     "css": [
@@ -320,18 +319,10 @@ mw.example();
 
                                'name' => 'user',
                                'scripts' => 'mw.example( 1 );',
+                               'wrap' => false,
 
                                'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
                        ] ],
-                       [ [
-                               'title' => 'Implement unwrapped user script',
-                               'debug' => false,
-
-                               'name' => 'user',
-                               'scripts' => 'mw.example( 1 );',
-
-                               'expected' => 'mw.loader.implement("user","mw.example(1);");',
-                       ] ],
                ];
        }
 
@@ -342,17 +333,20 @@ mw.example();
         */
        public function testMakeLoaderImplementScript( $case ) {
                $case += [
-                       'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' ),
-                       'debug' => true
+                       'wrap' => true,
+                       'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' )
                ];
                ResourceLoader::clearCache();
-               $this->setMwGlobals( 'wgResourceLoaderDebug', $case['debug'] );
+               $this->setMwGlobals( 'wgResourceLoaderDebug', true );
 
+               $rl = TestingAccessWrapper::newFromClass( 'ResourceLoader' );
                $this->assertEquals(
                        $case['expected'],
-                       ResourceLoader::makeLoaderImplementScript(
+                       $rl->makeLoaderImplementScript(
                                $case['name'],
-                               $case['scripts'],
+                               ( $case['wrap'] && is_string( $case['scripts'] ) )
+                                       ? new XmlJsCode( $case['scripts'] )
+                                       : $case['scripts'],
                                $case['styles'],
                                $case['messages'],
                                $case['templates']
@@ -365,7 +359,8 @@ mw.example();
         */
        public function testMakeLoaderImplementScriptInvalid() {
                $this->setExpectedException( 'MWException', 'Invalid scripts error' );
-               ResourceLoader::makeLoaderImplementScript(
+               $rl = TestingAccessWrapper::newFromClass( 'ResourceLoader' );
+               $rl->makeLoaderImplementScript(
                        'test', // name
                        123, // scripts
                        null, // styles
index 902fc9e..3fb4bbb 100644 (file)
@@ -30,7 +30,7 @@ class SearchEngineTest extends MediaWikiLangTestCase {
                        $this->markTestSkipped( "MySQL or SQLite with FTS3 only" );
                }
 
-               $searchType = $this->db->getSearchEngine();
+               $searchType = SearchEngineFactory::getSearchEngineClass( $this->db );
                $this->setMwGlobals( [
                        'wgSearchType' => $searchType
                ] );
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 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();
index 3332c08..5122dcd 100644 (file)
                }
        } ) );
 
-       QUnit.test( 'options', 1, function ( assert ) {
+       QUnit.test( 'options', function ( assert ) {
                assert.ok( mw.user.options instanceof mw.Map, 'options instance of mw.Map' );
        } );
 
-       QUnit.test( 'user status', 7, function ( assert ) {
-
+       QUnit.test( 'getters (anonymous)', function ( assert ) {
                // Forge an anonymous user
                mw.config.set( 'wgUserName', null );
                delete mw.config.values.wgUserId;
 
-               assert.strictEqual( mw.user.getName(), null, 'user.getName() returns null when anonymous' );
-               assert.assertTrue( mw.user.isAnon(), 'user.isAnon() returns true when anonymous' );
-               assert.strictEqual( mw.user.getId(), 0, 'user.getId() returns 0 when anonymous' );
+               assert.strictEqual( mw.user.getName(), null, 'getName()' );
+               assert.strictEqual( mw.user.isAnon(), true, 'isAnon()' );
+               assert.strictEqual( mw.user.getId(), 0, 'getId()' );
+       } );
 
-               // Not part of startUp module
+       QUnit.test( 'getters (logged-in)', function ( assert ) {
                mw.config.set( 'wgUserName', 'John' );
                mw.config.set( 'wgUserId', 123 );
 
-               assert.equal( mw.user.getName(), 'John', 'user.getName() returns username when logged-in' );
-               assert.assertFalse( mw.user.isAnon(), 'user.isAnon() returns false when logged-in' );
-               assert.strictEqual( mw.user.getId(), 123, 'user.getId() returns correct ID when logged-in' );
+               assert.equal( mw.user.getName(), 'John', 'getName()' );
+               assert.strictEqual( mw.user.isAnon(), false, 'isAnon()' );
+               assert.strictEqual( mw.user.getId(), 123, 'getId()' );
 
-               assert.equal( mw.user.id(), 'John', 'user.id Returns username when logged-in' );
+               assert.equal( mw.user.id(), 'John', 'user.id()' );
        } );
 
-       QUnit.test( 'getUserInfos', 3, function ( assert ) {
+       QUnit.test( 'getUserInfo', function ( assert ) {
                mw.config.set( 'wgUserGroups', [ '*', 'user' ] );
 
                mw.user.getGroups( function ( groups ) {
@@ -64,7 +64,7 @@
                this.server.respond();
        } );
 
-       QUnit.test( 'generateRandomSessionId', 4, function ( assert ) {
+       QUnit.test( 'generateRandomSessionId', function ( assert ) {
                var result, result2;
 
                result = mw.user.generateRandomSessionId();
@@ -77,7 +77,7 @@
 
        } );
 
-       QUnit.test( 'generateRandomSessionId (fallback)', 4, function ( assert ) {
+       QUnit.test( 'generateRandomSessionId (fallback)', function ( assert ) {
                var result, result2;
 
                // Pretend crypto API is not there to test the Math.random fallback