Merge "Chinese Conversion Table Update 2017-6"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 2 Jan 2018 07:21:52 +0000 (07:21 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 2 Jan 2018 07:21:52 +0000 (07:21 +0000)
542 files changed:
.travis.yml
RELEASE-NOTES-1.31
autoload.php
composer.json
docs/extension.schema.v1.json
docs/extension.schema.v2.json
docs/hooks.txt
includes/AutoLoader.php
includes/CategoryFinder.php
includes/DefaultSettings.php
includes/Feed.php
includes/GlobalFunctions.php
includes/HistoryBlob.php
includes/Linker.php
includes/MediaWiki.php
includes/MediaWikiServices.php
includes/MergeHistory.php
includes/Message.php
includes/OutputPage.php
includes/Preferences.php
includes/Revision.php
includes/ServiceWiring.php
includes/Storage/BlobAccessException.php [new file with mode: 0644]
includes/Storage/BlobStore.php [new file with mode: 0644]
includes/Storage/BlobStoreFactory.php [new file with mode: 0644]
includes/Storage/IncompleteRevisionException.php [new file with mode: 0644]
includes/Storage/MutableRevisionRecord.php [new file with mode: 0644]
includes/Storage/MutableRevisionSlots.php [new file with mode: 0644]
includes/Storage/RevisionAccessException.php [new file with mode: 0644]
includes/Storage/RevisionArchiveRecord.php [new file with mode: 0644]
includes/Storage/RevisionFactory.php [new file with mode: 0644]
includes/Storage/RevisionLookup.php [new file with mode: 0644]
includes/Storage/RevisionRecord.php [new file with mode: 0644]
includes/Storage/RevisionSlots.php [new file with mode: 0644]
includes/Storage/RevisionStore.php [new file with mode: 0644]
includes/Storage/RevisionStoreRecord.php [new file with mode: 0644]
includes/Storage/SlotRecord.php [new file with mode: 0644]
includes/Storage/SqlBlobStore.php [new file with mode: 0644]
includes/Storage/SuppressedDataException.php [new file with mode: 0644]
includes/Title.php
includes/actions/HistoryAction.php
includes/actions/WatchAction.php
includes/api/ApiBlock.php
includes/api/ApiEmailUser.php
includes/api/ApiFormatNone.php
includes/api/ApiMain.php
includes/api/ApiQuerySearch.php
includes/api/ApiTag.php
includes/api/i18n/ar.json
includes/api/i18n/de.json
includes/api/i18n/en.json
includes/api/i18n/es.json
includes/api/i18n/fr.json
includes/api/i18n/gl.json
includes/api/i18n/lb.json
includes/api/i18n/lt.json
includes/api/i18n/nb.json
includes/api/i18n/nl.json
includes/api/i18n/pl.json
includes/api/i18n/pt-br.json
includes/api/i18n/pt.json
includes/api/i18n/qqq.json
includes/api/i18n/ru.json
includes/api/i18n/zh-hans.json
includes/cache/CacheDependency.php
includes/cache/MessageCache.php
includes/cache/localisation/LCStoreStaticArray.php
includes/changes/ChangesListFilter.php
includes/changetags/ChangeTags.php
includes/clientpool/SquidPurgeClient.php
includes/config/ConfigFactory.php
includes/content/Content.php
includes/content/ContentHandler.php
includes/context/MutableContext.php
includes/context/RequestContext.php
includes/editpage/TextConflictHelper.php
includes/export/ExportProgressFilter.php [new file with mode: 0644]
includes/filebackend/FileBackendGroup.php
includes/htmlform/HTMLForm.php
includes/htmlform/HTMLFormElement.php
includes/htmlform/fields/HTMLCheckMatrix.php
includes/htmlform/fields/HTMLUsersMultiselectField.php
includes/import/WikiImporter.php
includes/installer/DatabaseUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/i18n/ca.json
includes/installer/i18n/es.json
includes/installer/i18n/eu.json
includes/installer/i18n/gl.json
includes/installer/i18n/nl.json
includes/installer/i18n/pl.json
includes/installer/i18n/pt.json
includes/installer/i18n/zh-hant.json
includes/interwiki/ClassicInterwikiLookup.php
includes/jobqueue/jobs/DeleteLinksJob.php
includes/jobqueue/jobs/EnqueueJob.php
includes/libs/CSSMin.php
includes/libs/JavaScriptMinifier.php
includes/libs/MWMessagePack.php
includes/libs/mime/XmlTypeCheck.php
includes/libs/objectcache/WANObjectCache.php
includes/libs/rdbms/database/Database.php
includes/libs/stats/BufferingStatsdDataFactory.php
includes/libs/stats/IBufferingStatsdDataFactory.php
includes/libs/stats/NullStatsdDataFactory.php
includes/libs/xmp/XMP.php
includes/media/Bitmap.php
includes/media/BitmapMetadataHandler.php
includes/media/XCF.php
includes/objectcache/SqlBagOStuff.php
includes/page/ImagePage.php
includes/page/PageArchive.php
includes/page/WikiPage.php
includes/parser/BlockLevelPass.php
includes/parser/Parser.php
includes/parser/ParserOutput.php
includes/parser/Preprocessor_DOM.php
includes/parser/Preprocessor_Hash.php
includes/parser/Sanitizer.php
includes/password/PasswordPolicyChecks.php
includes/registration/ExtensionProcessor.php
includes/registration/ExtensionRegistry.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderFilePath.php
includes/resourceloader/ResourceLoaderModule.php
includes/resourceloader/ResourceLoaderUserTokensModule.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/search/SearchEngine.php
includes/search/SearchExactMatchRescorer.php
includes/search/SearchMySQL.php
includes/shell/Command.php
includes/shell/FirejailCommand.php
includes/shell/Shell.php
includes/skins/BaseTemplate.php
includes/skins/SkinTemplate.php
includes/specialpage/ChangesListSpecialPage.php
includes/specials/SpecialEmailuser.php
includes/specials/SpecialListgrouprights.php
includes/specials/SpecialNewpages.php
includes/specials/SpecialProtectedpages.php
includes/specials/SpecialRecentchangeslinked.php
includes/specials/SpecialUncategorizedcategories.php
includes/specials/SpecialUserrights.php
includes/specials/SpecialWatchlist.php
includes/specials/pagers/ContribsPager.php
includes/specials/pagers/ProtectedPagesPager.php
includes/specials/pagers/UsersPager.php
includes/templates/AtomHeader.mustache [new file with mode: 0644]
includes/templates/AtomItem.mustache [new file with mode: 0644]
includes/templates/RSSHeader.mustache [new file with mode: 0644]
includes/templates/RSSItem.mustache [new file with mode: 0644]
includes/tidy/Balancer.php
includes/user/User.php
includes/user/UserGroupMembership.php
includes/user/UserIdentityValue.php [new file with mode: 0644]
includes/utils/AutoloadGenerator.php
includes/utils/AvroValidator.php
includes/utils/BatchRowWriter.php
includes/watcheditem/WatchedItem.php
includes/widget/ComplexNamespaceInputWidget.php
includes/widget/ComplexTitleInputWidget.php
includes/widget/DateInputWidget.php
includes/widget/DateTimeInputWidget.php
includes/widget/NamespaceInputWidget.php
includes/widget/SearchInputWidget.php
includes/widget/SelectWithInputWidget.php
includes/widget/TitleInputWidget.php
includes/widget/UserInputWidget.php
languages/Language.php
languages/classes/LanguageGa.php
languages/classes/LanguageLa.php
languages/data/CrhExceptions.php
languages/i18n/af.json
languages/i18n/ar.json
languages/i18n/ast.json
languages/i18n/atj.json
languages/i18n/az.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bg.json
languages/i18n/bho.json
languages/i18n/bn.json
languages/i18n/bs.json
languages/i18n/ca.json
languages/i18n/ce.json
languages/i18n/ckb.json
languages/i18n/cs.json
languages/i18n/csb.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/dty.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es-formal.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/frr.json
languages/i18n/gl.json
languages/i18n/got.json
languages/i18n/he.json
languages/i18n/hi.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/hy.json
languages/i18n/ia.json
languages/i18n/id.json
languages/i18n/is.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/jv.json
languages/i18n/ka.json
languages/i18n/kk-cyrl.json
languages/i18n/ko.json
languages/i18n/lad.json
languages/i18n/lb.json
languages/i18n/lt.json
languages/i18n/lv.json
languages/i18n/lzh.json
languages/i18n/map-bms.json
languages/i18n/mk.json
languages/i18n/ml.json
languages/i18n/mr.json
languages/i18n/ms.json
languages/i18n/mt.json
languages/i18n/mwl.json
languages/i18n/my.json
languages/i18n/nb.json
languages/i18n/ne.json
languages/i18n/nl.json
languages/i18n/nn.json
languages/i18n/nys.json
languages/i18n/pl.json
languages/i18n/ps.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/sat.json
languages/i18n/sd.json
languages/i18n/sgs.json
languages/i18n/shi.json
languages/i18n/shn.json
languages/i18n/skr-arab.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/sv.json
languages/i18n/th.json
languages/i18n/tl.json
languages/i18n/tr.json
languages/i18n/uk.json
languages/i18n/ur.json
languages/i18n/vi.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesKo.php
maintenance/Maintenance.php
maintenance/backup.inc
maintenance/benchmarks/Benchmarker.php
maintenance/cdb.php
maintenance/dumpIterator.php
maintenance/dumpTextPass.php
maintenance/generateLocalAutoload.php
maintenance/importDump.php
maintenance/oracle/archives/patch-auto_increment_triggers.sql
maintenance/oracle/archives/patch-externallinks-el_index_60.sql
maintenance/oracle/tables.sql
maintenance/oracle/user.sql
maintenance/renderDump.php
maintenance/storage/checkStorage.php
resources/Resources.php
resources/lib/jquery.chosen/LICENSE
resources/lib/jquery.chosen/README.md [new file with mode: 0644]
resources/lib/jquery.chosen/chosen-sprite.png
resources/lib/jquery.chosen/chosen-sprite@2x.png
resources/lib/jquery.chosen/chosen.css
resources/lib/jquery.chosen/chosen.jquery.js
resources/lib/oojs-ui/oojs-ui-core.js
resources/src/jquery/jquery.tablesorter.less
resources/src/mediawiki.action/mediawiki.action.edit.styles.less
resources/src/mediawiki.language/specialcharacters.json
resources/src/mediawiki.legacy/shared.css
resources/src/mediawiki.legacy/wikibits.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.MenuSelectWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.vector.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RclTargetPageWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RclToOrFromWidget.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MainWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MenuSelectWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTargetPageWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.js
resources/src/mediawiki.skinning/elements.css
resources/src/mediawiki.special/mediawiki.special.changeslist.css
resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js [new file with mode: 0644]
resources/src/mediawiki/mediawiki.debug.less
resources/src/mediawiki/mediawiki.editfont.css
resources/src/mediawiki/mediawiki.feedback.js
resources/src/mediawiki/mediawiki.js
resources/src/startup.js
tests/common/TestsAutoLoader.php
tests/parser/ParserTestRunner.php
tests/parser/TestFileEditor.php
tests/parser/parserTests.txt
tests/phpunit/MediaWikiCoversValidator.php [new file with mode: 0644]
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/AutopromoteTest.php
tests/phpunit/includes/BlockTest.php
tests/phpunit/includes/FauxRequestTest.php
tests/phpunit/includes/GlobalFunctions/GlobalTest.php
tests/phpunit/includes/HtmlTest.php
tests/phpunit/includes/LinkFilterTest.php
tests/phpunit/includes/MWNamespaceTest.php
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/MediaWikiVersionFetcherTest.php
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/PageArchiveTest.php
tests/phpunit/includes/PagePropsTest.php
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/SampleTest.php
tests/phpunit/includes/SanitizerValidateEmailTest.php
tests/phpunit/includes/Services/ServiceContainerTest.php [deleted file]
tests/phpunit/includes/Services/TestWiring1.php [deleted file]
tests/phpunit/includes/Services/TestWiring2.php [deleted file]
tests/phpunit/includes/StatusTest.php
tests/phpunit/includes/Storage/BlobStoreFactoryTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/MutableRevisionRecordTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/RevisionSlotsTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/RevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/RevisionStoreRecordTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/RevisionStoreTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/SlotRecordTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/SqlBlobStoreTest.php [new file with mode: 0644]
tests/phpunit/includes/TemplateParserTest.php
tests/phpunit/includes/TitleArrayFromResultTest.php
tests/phpunit/includes/TitleMethodsTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/WikiReferenceTest.php
tests/phpunit/includes/XmlJsTest.php
tests/phpunit/includes/XmlTest.php
tests/phpunit/includes/api/ApiBaseTest.php
tests/phpunit/includes/api/ApiCheckTokenTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiClearHasMsgTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiComparePagesTest.php
tests/phpunit/includes/api/ApiOpenSearchTest.php
tests/phpunit/includes/api/ApiPageSetTest.php
tests/phpunit/includes/api/ApiParseTest.php
tests/phpunit/includes/api/ApiQueryAllPagesTest.php
tests/phpunit/includes/api/ApiQueryRecentChangesIntegrationTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiTestCase.php
tests/phpunit/includes/api/ApiUploadTest.php
tests/phpunit/includes/api/ApiUsageExceptionTest.php
tests/phpunit/includes/api/ApiWatchTest.php
tests/phpunit/includes/collation/CollationFaTest.php
tests/phpunit/includes/collation/CustomUppercaseCollationTest.php
tests/phpunit/includes/composer/ComposerVersionNormalizerTest.php
tests/phpunit/includes/config/EtcdConfigTest.php
tests/phpunit/includes/content/ContentHandlerTest.php
tests/phpunit/includes/content/CssContentTest.php
tests/phpunit/includes/content/FileContentHandlerTest.php
tests/phpunit/includes/content/JavaScriptContentTest.php
tests/phpunit/includes/content/TextContentHandlerTest.php
tests/phpunit/includes/content/WikitextContentHandlerTest.php
tests/phpunit/includes/content/WikitextStructureTest.php
tests/phpunit/includes/db/DatabaseSqliteTest.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/db/LoadBalancerTest.php
tests/phpunit/includes/debug/logger/monolog/AvroFormatterTest.php
tests/phpunit/includes/debug/logger/monolog/KafkaHandlerTest.php
tests/phpunit/includes/deferred/LinksUpdateTest.php
tests/phpunit/includes/deferred/MWCallableUpdateTest.php
tests/phpunit/includes/deferred/TransactionRoundDefiningUpdateTest.php
tests/phpunit/includes/editpage/TextboxBuilderTest.php [new file with mode: 0644]
tests/phpunit/includes/exception/MWExceptionTest.php
tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php
tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php
tests/phpunit/includes/filerepo/RepoGroupTest.php
tests/phpunit/includes/filerepo/file/FileTest.php
tests/phpunit/includes/htmlform/HTMLAutoCompleteSelectFieldTest.php
tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php
tests/phpunit/includes/htmlform/HTMLFormTest.php
tests/phpunit/includes/htmlform/HTMLRestrictionsFieldTest.php
tests/phpunit/includes/http/HttpTest.php
tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php
tests/phpunit/includes/jobqueue/JobTest.php
tests/phpunit/includes/jobqueue/RefreshLinksPartitionTest.php
tests/phpunit/includes/libs/ArrayUtilsTest.php
tests/phpunit/includes/libs/DeferredStringifierTest.php
tests/phpunit/includes/libs/DnsSrvDiscovererTest.php
tests/phpunit/includes/libs/GenericArrayObjectTest.php
tests/phpunit/includes/libs/HashRingTest.php
tests/phpunit/includes/libs/HtmlArmorTest.php
tests/phpunit/includes/libs/IEUrlExtensionTest.php
tests/phpunit/includes/libs/IPTest.php
tests/phpunit/includes/libs/JavaScriptMinifierTest.php
tests/phpunit/includes/libs/MWMessagePackTest.php
tests/phpunit/includes/libs/MapCacheLRUTest.php
tests/phpunit/includes/libs/MemoizedCallableTest.php
tests/phpunit/includes/libs/ObjectFactoryTest.php
tests/phpunit/includes/libs/ProcessCacheLRUTest.php
tests/phpunit/includes/libs/SamplingStatsdClientTest.php
tests/phpunit/includes/libs/StringUtilsTest.php
tests/phpunit/includes/libs/TimingTest.php
tests/phpunit/includes/libs/XhprofDataTest.php
tests/phpunit/includes/libs/XhprofTest.php
tests/phpunit/includes/libs/XmlTypeCheckTest.php
tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php
tests/phpunit/includes/libs/objectcache/BagOStuffTest.php
tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php
tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
tests/phpunit/includes/libs/rdbms/TransactionProfilerTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php
tests/phpunit/includes/libs/xmp/XMPTest.php
tests/phpunit/includes/libs/xmp/XMPValidateTest.php
tests/phpunit/includes/logging/BlockLogFormatterTest.php
tests/phpunit/includes/logging/DeleteLogFormatterTest.php
tests/phpunit/includes/logging/ImportLogFormatterTest.php
tests/phpunit/includes/logging/MergeLogFormatterTest.php
tests/phpunit/includes/logging/MoveLogFormatterTest.php
tests/phpunit/includes/logging/NewUsersLogFormatterTest.php
tests/phpunit/includes/logging/PageLangLogFormatterTest.php
tests/phpunit/includes/logging/PatrolLogFormatterTest.php
tests/phpunit/includes/logging/ProtectLogFormatterTest.php
tests/phpunit/includes/logging/RightsLogFormatterTest.php
tests/phpunit/includes/logging/UploadLogFormatterTest.php
tests/phpunit/includes/media/ExifRotationTest.php
tests/phpunit/includes/media/GIFTest.php
tests/phpunit/includes/media/PNGTest.php
tests/phpunit/includes/media/SVGTest.php
tests/phpunit/includes/media/WebPTest.php
tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php
tests/phpunit/includes/objectcache/RESTBagOStuffTest.php
tests/phpunit/includes/objectcache/RedisBagOStuffTest.php
tests/phpunit/includes/page/ImagePage404Test.php
tests/phpunit/includes/page/ImagePageTest.php
tests/phpunit/includes/page/WikiPageDbTestBase.php
tests/phpunit/includes/parser/MagicVariableTest.php
tests/phpunit/includes/parser/ParserIntegrationTest.php
tests/phpunit/includes/parser/ParserOptionsTest.php
tests/phpunit/includes/parser/SanitizerTest.php
tests/phpunit/includes/parser/TagHooksTest.php
tests/phpunit/includes/poolcounter/PoolCounterTest.php
tests/phpunit/includes/registration/ExtensionProcessorTest.php
tests/phpunit/includes/registration/VersionCheckerTest.php
tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php
tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderSkinModuleTest.php
tests/phpunit/includes/services/ServiceContainerTest.php [new file with mode: 0644]
tests/phpunit/includes/services/TestWiring1.php [new file with mode: 0644]
tests/phpunit/includes/services/TestWiring2.php [new file with mode: 0644]
tests/phpunit/includes/shell/CommandFactoryTest.php
tests/phpunit/includes/shell/CommandTest.php
tests/phpunit/includes/shell/FirejailCommandTest.php
tests/phpunit/includes/shell/ShellTest.php
tests/phpunit/includes/site/FileBasedSiteLookupTest.php
tests/phpunit/includes/site/MediaWikiPageNameNormalizerTest.php
tests/phpunit/includes/site/SiteExporterTest.php
tests/phpunit/includes/site/SiteImporterTest.php
tests/phpunit/includes/site/SitesCacheFileBuilderTest.php
tests/phpunit/includes/specials/ContribsPagerTest.php
tests/phpunit/includes/specials/SpecialMIMESearchTest.php
tests/phpunit/includes/specials/SpecialUncategorizedcategoriesTest.php
tests/phpunit/includes/upload/UploadBaseTest.php
tests/phpunit/includes/user/PasswordResetTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/includes/utils/AvroValidatorTest.php
tests/phpunit/includes/utils/BatchRowUpdateTest.php
tests/phpunit/includes/utils/ClassCollectorTest.php
tests/phpunit/includes/utils/FileContentsHasherTest.php
tests/phpunit/includes/utils/MWCryptHKDFTest.php
tests/phpunit/includes/utils/MWCryptHashTest.php
tests/phpunit/includes/utils/MWRestrictionsTest.php
tests/phpunit/includes/utils/UIDGeneratorTest.php
tests/phpunit/includes/utils/ZipDirectoryReaderTest.php
tests/phpunit/includes/watcheditem/WatchedItemIntegrationTest.php [deleted file]
tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php
tests/phpunit/includes/watcheditem/WatchedItemUnitTest.php [deleted file]
tests/phpunit/languages/LanguageCodeTest.php
tests/phpunit/languages/LanguageConverterTest.php
tests/phpunit/languages/LanguageTest.php
tests/phpunit/languages/classes/LanguageArTest.php
tests/phpunit/languages/classes/LanguageBe_taraskTest.php
tests/phpunit/languages/classes/LanguageBsTest.php
tests/phpunit/languages/classes/LanguageCrhTest.php
tests/phpunit/languages/classes/LanguageCuTest.php
tests/phpunit/languages/classes/LanguageDsbTest.php
tests/phpunit/languages/classes/LanguageGanTest.php
tests/phpunit/languages/classes/LanguageHsbTest.php
tests/phpunit/languages/classes/LanguageHuTest.php
tests/phpunit/languages/classes/LanguageHyTest.php
tests/phpunit/languages/classes/LanguageIuTest.php
tests/phpunit/languages/classes/LanguageKkTest.php
tests/phpunit/languages/classes/LanguageKshTest.php
tests/phpunit/languages/classes/LanguageKuTest.php
tests/phpunit/languages/classes/LanguageMlTest.php
tests/phpunit/languages/classes/LanguageRuTest.php
tests/phpunit/languages/classes/LanguageShiTest.php
tests/phpunit/languages/classes/LanguageSlTest.php
tests/phpunit/languages/classes/LanguageSrTest.php
tests/phpunit/languages/classes/LanguageTgTest.php
tests/phpunit/languages/classes/LanguageTrTest.php
tests/phpunit/languages/classes/LanguageUkTest.php
tests/phpunit/languages/classes/LanguageUzTest.php
tests/phpunit/languages/classes/LanguageWaTest.php
tests/phpunit/languages/classes/LanguageZhTest.php
tests/phpunit/maintenance/BenchmarkerTest.php [new file with mode: 0644]
tests/phpunit/maintenance/categoriesRdfTest.php
tests/phpunit/skins/SideBarTest.php
tests/phpunit/structure/AutoLoaderTest.php
tests/phpunit/structure/AvailableRightsTest.php
tests/phpunit/structure/ExtensionJsonValidationTest.php
tests/phpunit/structure/ResourcesTest.php
tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js
tests/selenium/wdio.conf.jenkins.js
tests/selenium/wdio.conf.js

index 64414b5..32763b5 100644 (file)
@@ -27,8 +27,6 @@ matrix:
     - env: dbtype=postgres dbuser=travis
       php: 5.5
     # https://docs.travis-ci.com/user/languages/php#HHVM-versions
-    - env: dbtype=mysql dbuser=root
-      php: hhvm-3.12
     - env: dbtype=mysql dbuser=root
       php: hhvm-3.18
     - env: dbtype=mysql dbuser=root
index 4a2876d..a496b02 100644 (file)
@@ -19,6 +19,8 @@ production.
   maintenance/cleanupUsersWithNoId.php.
 * $wgResourceLoaderMinifierStatementsOnOwnLine and $wgResourceLoaderMinifierMaxLineLength
   were removed (deprecated since 1.27).
+* (T180921) $wgReferrerPolicy now supports having fallbacks for browsers that are not
+  using the latest version of the Referrer Policy specification.
 
 === New features in 1.31 ===
 * Wikimedia\Rdbms\IDatabase->select() and similar methods now support
@@ -39,6 +41,7 @@ production.
 === External library changes in 1.31 ===
 
 ==== Upgraded external libraries ====
+* Updated jquery.chosen from v0.9.14 to v1.8.2.
 * …
 
 ==== New external libraries ====
@@ -71,6 +74,10 @@ changes to languages because of Phabricator reports.
 * (T180052) Mirandese (mwl) now supports gendered NS_USER/NS_USER_TALK namespaces.
 
 === Other changes in 1.31 ===
+* Introducing multi-content-revision capability into the storage layer. For details,
+  see <https://www.mediawiki.org/wiki/Requests_for_comment/Multi-Content_Revisions>.
+* The Revision class was deprecated in favor of RevisionStore, BlobStore, and
+  RevisionRecord and its subclasses.
 * MessageBlobStore::insertMessageBlob() (deprecated in 1.27) was removed.
 * The global function wfBCP47 was renamed to LanguageCode::bcp47.
 * The global function wfBCP47 is now deprecated.
@@ -123,6 +130,9 @@ changes to languages because of Phabricator reports.
 * The Block class will no longer accept usable-but-missing usernames for
   'byText' or ->setBlocker(). Callers should either ensure the blocker exists
   locally or use a new interwiki-format username like "iw>Example".
+* The RevisionInsertComplete hook is now deprecated, use RevisionRecordInserted instead.
+  RevisionInsertComplete is still called, but the second and third parameter will always be null.
+  Hard deprecation is scheduled for 1.32.
 * The following methods that get and set ParserOutput state are deprecated.
   Callers should use the new stateless $options parameter to
   ParserOutput::getText() instead.
@@ -135,6 +145,20 @@ changes to languages because of Phabricator reports.
   * OutputPage::enableSectionEditLinks()
   * OutputPage::sectionEditLinksEnabled()
   * The public ParserOutput state fields $mTOCEnabled and $mEditSectionTokens are also deprecated.
+* The following methods and constants from the WatchedItem class were deprecated in
+  1.27 have been removed.
+  * WatchedItem::getTitle()
+  * WatchedItem::fromUserTitle()
+  * WatchedItem::addWatch()
+  * WatchedItem::removeWatch()
+  * WatchedItem::isWatched()
+  * WatchedItem::duplicateEntries()
+  * WatchedItem::IGNORE_USER_RIGHTS
+  * WatchedItem::CHECK_USER_RIGHTS
+  * WatchedItem::DEPRECATED_USAGE_TIMESTAMP
+* The $statementsOnOwnLine parameter of JavaScriptMinifier::minify was removed.
+  The corresponding configuration variable ($wgResourceLoaderMinifierStatementsOnOwnLine)
+  has been deprecated since 1.27 and was removed as well.
 
 == Compatibility ==
 MediaWiki 1.31 requires PHP 5.5.9 or later. There is experimental support for
index cd01828..c37d9f7 100644 (file)
@@ -449,7 +449,7 @@ $wgAutoloadLocalClasses = [
        'Exif' => __DIR__ . '/includes/media/Exif.php',
        'ExifBitmapHandler' => __DIR__ . '/includes/media/ExifBitmap.php',
        'ExplodeIterator' => __DIR__ . '/includes/libs/ExplodeIterator.php',
-       'ExportProgressFilter' => __DIR__ . '/maintenance/backup.inc',
+       'ExportProgressFilter' => __DIR__ . '/includes/export/ExportProgressFilter.php',
        'ExportSites' => __DIR__ . '/maintenance/exportSites.php',
        'ExtensionJsonValidationError' => __DIR__ . '/includes/registration/ExtensionJsonValidationError.php',
        'ExtensionJsonValidator' => __DIR__ . '/includes/registration/ExtensionJsonValidator.php',
@@ -892,9 +892,6 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Languages\\Data\\CrhExceptions' => __DIR__ . '/languages/data/CrhExceptions.php',
        'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php',
        'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php',
-       'MediaWiki\\Linker\\LinkRenderer' => __DIR__ . '/includes/linker/LinkRenderer.php',
-       'MediaWiki\\Linker\\LinkRendererFactory' => __DIR__ . '/includes/linker/LinkRendererFactory.php',
-       'MediaWiki\\Linker\\LinkTarget' => __DIR__ . '/includes/linker/LinkTarget.php',
        'MediaWiki\\Logger\\ConsoleLogger' => __DIR__ . '/includes/debug/logger/ConsoleLogger.php',
        'MediaWiki\\Logger\\ConsoleSpi' => __DIR__ . '/includes/debug/logger/ConsoleSpi.php',
        'MediaWiki\\Logger\\LegacyLogger' => __DIR__ . '/includes/debug/logger/LegacyLogger.php',
@@ -945,6 +942,23 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Shell\\Result' => __DIR__ . '/includes/shell/Result.php',
        'MediaWiki\\Shell\\Shell' => __DIR__ . '/includes/shell/Shell.php',
        'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php',
+       'MediaWiki\\Storage\\BlobAccessException' => __DIR__ . '/includes/Storage/BlobAccessException.php',
+       'MediaWiki\\Storage\\BlobStore' => __DIR__ . '/includes/Storage/BlobStore.php',
+       'MediaWiki\\Storage\\BlobStoreFactory' => __DIR__ . '/includes/Storage/BlobStoreFactory.php',
+       'MediaWiki\\Storage\\IncompleteRevisionException' => __DIR__ . '/includes/Storage/IncompleteRevisionException.php',
+       'MediaWiki\\Storage\\MutableRevisionRecord' => __DIR__ . '/includes/Storage/MutableRevisionRecord.php',
+       'MediaWiki\\Storage\\MutableRevisionSlots' => __DIR__ . '/includes/Storage/MutableRevisionSlots.php',
+       'MediaWiki\\Storage\\RevisionAccessException' => __DIR__ . '/includes/Storage/RevisionAccessException.php',
+       'MediaWiki\\Storage\\RevisionArchiveRecord' => __DIR__ . '/includes/Storage/RevisionArchiveRecord.php',
+       'MediaWiki\\Storage\\RevisionFactory' => __DIR__ . '/includes/Storage/RevisionFactory.php',
+       'MediaWiki\\Storage\\RevisionLookup' => __DIR__ . '/includes/Storage/RevisionLookup.php',
+       'MediaWiki\\Storage\\RevisionRecord' => __DIR__ . '/includes/Storage/RevisionRecord.php',
+       'MediaWiki\\Storage\\RevisionSlots' => __DIR__ . '/includes/Storage/RevisionSlots.php',
+       'MediaWiki\\Storage\\RevisionStore' => __DIR__ . '/includes/Storage/RevisionStore.php',
+       'MediaWiki\\Storage\\RevisionStoreRecord' => __DIR__ . '/includes/Storage/RevisionStoreRecord.php',
+       'MediaWiki\\Storage\\SlotRecord' => __DIR__ . '/includes/Storage/SlotRecord.php',
+       'MediaWiki\\Storage\\SqlBlobStore' => __DIR__ . '/includes/Storage/SqlBlobStore.php',
+       'MediaWiki\\Storage\\SuppressedDataException' => __DIR__ . '/includes/Storage/SuppressedDataException.php',
        'MediaWiki\\Tidy\\BalanceActiveFormattingElements' => __DIR__ . '/includes/tidy/Balancer.php',
        'MediaWiki\\Tidy\\BalanceElement' => __DIR__ . '/includes/tidy/Balancer.php',
        'MediaWiki\\Tidy\\BalanceMarker' => __DIR__ . '/includes/tidy/Balancer.php',
@@ -964,6 +978,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Tidy\\RemexMungerData' => __DIR__ . '/includes/tidy/RemexMungerData.php',
        'MediaWiki\\Tidy\\TidyDriverBase' => __DIR__ . '/includes/tidy/TidyDriverBase.php',
        'MediaWiki\\User\\UserIdentity' => __DIR__ . '/includes/user/UserIdentity.php',
+       'MediaWiki\\User\\UserIdentityValue' => __DIR__ . '/includes/user/UserIdentityValue.php',
        'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php',
        'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php',
        'MediaWiki\\Widget\\DateInputWidget' => __DIR__ . '/includes/widget/DateInputWidget.php',
index 35783f2..8b8697e 100644 (file)
@@ -39,7 +39,7 @@
                "wikimedia/php-session-serializer": "1.0.4",
                "wikimedia/purtle": "1.0.6",
                "wikimedia/relpath": "2.0.0",
-               "wikimedia/remex-html": "1.0.1",
+               "wikimedia/remex-html": "1.0.2",
                "wikimedia/running-stat": "1.1.0",
                "wikimedia/scoped-callback": "1.0.0",
                "wikimedia/utfnormal": "1.1.0",
index 7cfebca..ddf82e8 100644 (file)
                        "type": "object",
                        "description": "SpecialPages implemented in this extension (mapping of page name to class name)"
                },
+               "AutoloadNamespaces": {
+                       "type": "object",
+                       "description": "Mapping of PSR-4 compliant namespace to directory for autoloading"
+               },
                "AutoloadClasses": {
                        "type": "object"
                },
index 75a4f2c..0bdf97d 100644 (file)
                        "type": "object",
                        "description": "SpecialPages implemented in this extension (mapping of page name to class name)"
                },
+               "AutoloadNamespaces": {
+                       "type": "object",
+                       "description": "Mapping of PSR-4 compliant namespace to directory for autoloading"
+               },
                "AutoloadClasses": {
                        "type": "object"
                },
index 29883b2..45387a3 100644 (file)
@@ -951,7 +951,7 @@ $id: the page ID (original ID in case of page deletions)
 in a Category page. Gives extensions the opportunity to batch load any
 related data about the pages.
 $type: The category type. Either 'page', 'file' or 'subcat'
-$res: Query result from DatabaseBase::select()
+$res: Query result from Wikimedia\Rdbms\IDatabase::select()
 
 'CategoryViewer::generateLink': Before generating an output link allow
 extensions opportunity to generate a more specific or relevant link.
@@ -1840,7 +1840,7 @@ $revisionInfo: Array of revision information
 Return false to stop further processing of the tag
 $reader: XMLReader object
 
-'ImportHandleUnknownUser': When a user does exist locally, this hook is called
+'ImportHandleUnknownUser': When a user doesn't exist locally, this hook is called
 to give extensions an opportunity to auto-create it. If the auto-creation is
 successful, return false.
 $name: User name
@@ -2810,14 +2810,14 @@ called after the addition of 'qunit' and MediaWiki testing resources.
   added to any module.
 &$ResourceLoader: object
 
-'RevisionInsertComplete': Called after a revision is inserted into the database.
-&$revision: the Revision
-$data: the data stored in old_text.  The meaning depends on $flags: if external
-  is set, it's the URL of the revision text in external storage; otherwise,
-  it's the revision text itself.  In either case, if gzip is set, the revision
-  text is gzipped.
-$flags: a comma-delimited list of strings representing the options used.  May
-  include: utf8 (this will always be set for new revisions); gzip; external.
+'RevisionRecordInserted': Called after a revision is inserted into the database.
+$revisionRecord: the RevisionRecord that has just been inserted.
+
+'RevisionInsertComplete': DEPRECATED! Use RevisionRecordInserted hook instead.
+Called after a revision is inserted into the database.
+$revision: the Revision
+$data: DEPRECATED! Always null!
+$flags: DEPRECATED! Always null!
 
 'SearchableNamespaces': An option to modify which namespaces are searchable.
 &$arr: Array of namespaces ($nsId => $name) which will be used.
index 8dc7d40..52410fe 100644 (file)
@@ -30,6 +30,12 @@ require_once __DIR__ . '/../autoload.php';
 class AutoLoader {
        static protected $autoloadLocalClassesLower = null;
 
+       /**
+        * @private Only public for ExtensionRegistry
+        * @var string[] Namespace (ends with \) => Path (ends with /)
+        */
+       static public $psr4Namespaces = [];
+
        /**
         * autoload - take a class name and attempt to load it
         *
@@ -67,6 +73,28 @@ class AutoLoader {
                        }
                }
 
+               if ( !$filename && strpos( $className, '\\' ) !== false ) {
+                       // This class is namespaced, so try looking at the namespace map
+                       $prefix = $className;
+                       while ( false !== $pos = strrpos( $prefix, '\\' ) ) {
+                               // Check to see if this namespace prefix is in the map
+                               $prefix = substr( $className, 0, $pos + 1 );
+                               if ( isset( self::$psr4Namespaces[$prefix] ) ) {
+                                       $relativeClass = substr( $className, $pos + 1 );
+                                       // Build the expected filename, and see if it exists
+                                       $file = self::$psr4Namespaces[$prefix] .
+                                               str_replace( '\\', '/', $relativeClass ) . '.php';
+                                       if ( file_exists( $file ) ) {
+                                               $filename = $file;
+                                               break;
+                                       }
+                               }
+
+                               // Remove trailing separator for next iteration
+                               $prefix = rtrim( $prefix, '\\' );
+                       }
+               }
+
                if ( !$filename ) {
                        // Class not found; let the next autoloader try to find it
                        return;
@@ -88,6 +116,22 @@ class AutoLoader {
        static function resetAutoloadLocalClassesLower() {
                self::$autoloadLocalClassesLower = null;
        }
+
+       /**
+        * Get a mapping of namespace => file path
+        * The namespaces should follow the PSR-4 standard for autoloading
+        *
+        * @see <http://www.php-fig.org/psr/psr-4/>
+        * @private Only public for usage in AutoloadGenerator
+        * @since 1.31
+        * @return string[]
+        */
+       public static function getAutoloadNamespaces() {
+               return [
+                       'MediaWiki\\Linker\\' => __DIR__ .'/linker/'
+               ];
+       }
 }
 
+AutoLoader::$psr4Namespaces = AutoLoader::getAutoloadNamespaces();
 spl_autoload_register( [ 'AutoLoader', 'autoload' ] );
index 2a70f5f..3561f7f 100644 (file)
@@ -56,6 +56,9 @@ class CategoryFinder {
        /** @var array Array of article/category IDs */
        protected $next = [];
 
+       /** @var int Max layer depth **/
+       protected $maxdepth = -1;
+
        /** @var array Array of DBKEY category names */
        protected $targets = [];
 
@@ -73,12 +76,17 @@ class CategoryFinder {
         * @param array $articleIds Array of article IDs
         * @param array $categories FIXME
         * @param string $mode FIXME, default 'AND'.
+        * @param int $maxdepth Maximum layer depth. Where:
+        *      -1 means deep recursion (default);
+        *       0 means no-parents;
+        *       1 means one parent layer, etc.
         * @todo FIXME: $categories/$mode
         */
-       public function seed( $articleIds, $categories, $mode = 'AND' ) {
+       public function seed( $articleIds, $categories, $mode = 'AND', $maxdepth = -1 ) {
                $this->articles = $articleIds;
                $this->next = $articleIds;
                $this->mode = $mode;
+               $this->maxdepth = $maxdepth;
 
                # Set the list of target categories; convert them to DBKEY form first
                $this->targets = [];
@@ -98,8 +106,17 @@ class CategoryFinder {
         */
        public function run() {
                $this->dbr = wfGetDB( DB_REPLICA );
-               while ( count( $this->next ) > 0 ) {
+
+               $i = 0;
+               $dig = true;
+               while ( count( $this->next ) && $dig ) {
                        $this->scanNextLayer();
+
+                       // Is there any depth limit?
+                       if ( $this->maxdepth !== -1 ) {
+                               $dig = $i < $this->maxdepth;
+                               $i++;
+                       }
                }
 
                # Now check if this applies to the individual articles
index a7b34a1..8091428 100644 (file)
@@ -316,10 +316,20 @@ $wgAppleTouchIcon = false;
 
 /**
  * Value for the referrer policy meta tag.
- * One of 'never', 'default', 'origin', 'always'. Setting it to false just
- * prevents the meta tag from being output.
- * See https://www.w3.org/TR/referrer-policy/ for details.
- *
+ * One or more of the values defined in the Referrer Policy specification:
+ * https://w3c.github.io/webappsec-referrer-policy/
+ * ('no-referrer', 'no-referrer-when-downgrade', 'same-origin',
+ * 'origin', 'strict-origin', 'origin-when-cross-origin',
+ * 'strict-origin-when-cross-origin', or 'unsafe-url')
+ * Setting it to false prevents the meta tag from being output
+ * (which results in falling back to the Referrer-Policy header,
+ * or 'no-referrer-when-downgrade' if that's not set either.)
+ * Setting it to an array (supported since 1.31) will create a meta tag for
+ * each value, in the reverse of the order (meaning that the first array element
+ * will be the default and the others used as fallbacks for browsers which do not
+ * understand it).
+ *
+ * @var array|string|bool
  * @since 1.25
  */
 $wgReferrerPolicy = false;
@@ -3763,7 +3773,7 @@ $wgResourceLoaderValidateStaticJS = false;
  * @code
  *   $wgResourceLoaderLESSVars = [
  *     'exampleFontSize'  => '1em',
- *     'exampleBlue' => '#eee',
+ *     'exampleBlue' => '#36c',
  *   ];
  * @endcode
  * @since 1.22
@@ -4851,6 +4861,7 @@ $wgDefaultUserOptions = [
        'editfont' => 'monospace',
        'editondblclick' => 0,
        'editsectiononrightclick' => 0,
+       'email-allow-new-users' => 1,
        'enotifminoredits' => 0,
        'enotifrevealaddr' => 0,
        'enotifusertalkpages' => 1,
@@ -6952,6 +6963,7 @@ $wgUseTagFilter = true;
  * - 'mw-blank': Edit completely blanks the page
  * - 'mw-replace': Edit removes more than 90% of the content
  * - 'mw-rollback': Edit is a rollback, made through the rollback link or rollback API
+ * - 'mw-undo': Edit made through an undo link
  *
  * @var array
  * @since 1.31
@@ -6963,7 +6975,8 @@ $wgSoftwareTags = [
        'mw-changed-redirect-target' => true,
        'mw-blank' => true,
        'mw-replace' => true,
-       'mw-rollback' => true
+       'mw-rollback' => true,
+       'mw-undo' => true,
 ];
 
 /**
index 35f2ce9..0e715df 100644 (file)
@@ -84,13 +84,23 @@ class FeedItem {
        }
 
        /**
-        * Get the unique id of this item
-        *
+        * Get the unique id of this item; already xml-encoded
+        * @return string
+        */
+       public function getUniqueID() {
+               $id = $this->getUniqueIDUnescaped();
+               if ( $id ) {
+                       return $this->xmlEncode( $id );
+               }
+       }
+
+       /**
+        * Get the unique id of this item, without any escaping
         * @return string
         */
-       public function getUniqueId() {
+       public function getUniqueIdUnescaped() {
                if ( $this->uniqueId ) {
-                       return $this->xmlEncode( wfExpandUrl( $this->uniqueId, PROTO_CURRENT ) );
+                       return wfExpandUrl( $this->uniqueId, PROTO_CURRENT );
                }
        }
 
@@ -123,6 +133,14 @@ class FeedItem {
                return $this->xmlEncode( $this->url );
        }
 
+       /** Get the URL of this item without any escaping
+        *
+        * @return string
+        */
+       public function getUrlUnescaped() {
+               return $this->url;
+       }
+
        /**
         * Get the description of this item; already xml-encoded
         *
@@ -132,6 +150,14 @@ class FeedItem {
                return $this->xmlEncode( $this->description );
        }
 
+       /**
+        * Get the description of this item without any escaping
+        *
+        */
+       public function getDescriptionUnescaped() {
+               return $this->description;
+       }
+
        /**
         * Get the language of this item
         *
@@ -160,6 +186,15 @@ class FeedItem {
                return $this->xmlEncode( $this->author );
        }
 
+       /**
+        * Get the author of this item without any escaping
+        *
+        * @return string
+        */
+       public function getAuthorUnescaped() {
+               return $this->author;
+       }
+
        /**
         * Get the comment of this item; already xml-encoded
         *
@@ -169,6 +204,15 @@ class FeedItem {
                return $this->xmlEncode( $this->comments );
        }
 
+       /**
+        * Get the comment of this item without any escaping
+        *
+        * @return string
+        */
+       public function getCommentsUnescaped() {
+               return $this->comments;
+       }
+
        /**
         * Quickie hack... strip out wikilinks to more legible form from the comment.
         *
@@ -187,6 +231,23 @@ class FeedItem {
  * @ingroup Feed
  */
 abstract class ChannelFeed extends FeedItem {
+
+       /** @var TemplateParser */
+       protected $templateParser;
+
+       /**
+        * @param string|Title $title Feed's title
+        * @param string $description
+        * @param string $url URL uniquely designating the feed.
+        * @param string $date Feed's date
+        * @param string $author Author's user name
+        * @param string $comments
+        */
+       function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) {
+               parent::__construct( $title, $description, $url, $date, $author, $comments );
+               $this->templateParser = new TemplateParser();
+       }
+
        /**
         * Generate Header of the feed
         * @par Example:
@@ -279,13 +340,15 @@ abstract class ChannelFeed extends FeedItem {
 class RSSFeed extends ChannelFeed {
 
        /**
-        * Format a date given a timestamp
+        * Format a date given a timestamp. If a timestamp is not given, nothing is returned
         *
-        * @param int $ts Timestamp
-        * @return string Date string
+        * @param int|null $ts Timestamp
+        * @return string|null Date string
         */
        function formatTime( $ts ) {
-               return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
+               if ( $ts ) {
+                       return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
+               }
        }
 
        /**
@@ -295,15 +358,17 @@ class RSSFeed extends ChannelFeed {
                global $wgVersion;
 
                $this->outXmlHeader();
-               ?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
-       <channel>
-               <title><?php print $this->getTitle() ?></title>
-               <link><?php print wfExpandUrl( $this->getUrl(), PROTO_CURRENT ) ?></link>
-               <description><?php print $this->getDescription() ?></description>
-               <language><?php print $this->getLanguage() ?></language>
-               <generator>MediaWiki <?php print $wgVersion ?></generator>
-               <lastBuildDate><?php print $this->formatTime( wfTimestampNow() ) ?></lastBuildDate>
-<?php
+               // Manually escaping rather than letting Mustache do it because Mustache
+               // uses htmlentities, which does not work with XML
+               $templateParams = [
+                       'title' => $this->getTitle(),
+                       'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ),
+                       'description' => $this->getDescription(),
+                       'language' => $this->xmlEncode( $this->getLanguage() ),
+                       'version' => $this->xmlEncode( $wgVersion ),
+                       'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) )
+               ];
+               print $this->templateParser->processTemplate( 'RSSHeader', $templateParams );
        }
 
        /**
@@ -311,28 +376,30 @@ class RSSFeed extends ChannelFeed {
         * @param FeedItem $item Item to be output
         */
        function outItem( $item ) {
-               // @codingStandardsIgnoreStart Ignore long lines and formatting issues.
-       ?>
-               <item>
-                       <title><?php print $item->getTitle(); ?></title>
-                       <link><?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ); ?></link>
-                       <guid<?php if ( !$item->rssIsPermalink ) { print ' isPermaLink="false"'; } ?>><?php print $item->getUniqueId(); ?></guid>
-                       <description><?php print $item->getDescription() ?></description>
-                       <?php if ( $item->getDate() ) { ?><pubDate><?php print $this->formatTime( $item->getDate() ); ?></pubDate><?php } ?>
-                       <?php if ( $item->getAuthor() ) { ?><dc:creator><?php print $item->getAuthor(); ?></dc:creator><?php }?>
-                       <?php if ( $item->getComments() ) { ?><comments><?php print wfExpandUrl( $item->getComments(), PROTO_CURRENT ); ?></comments><?php }?>
-               </item>
-<?php
-               // @codingStandardsIgnoreEnd
+               // Manually escaping rather than letting Mustache do it because Mustache
+               // uses htmlentities, which does not work with XML
+               $templateParams = [
+                       "title" => $item->getTitle(),
+                       "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ),
+                       "permalink" => $item->rssIsPermalink,
+                       "uniqueID" => $item->getUniqueId(),
+                       "description" => $item->getDescription(),
+                       "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ),
+                       "author" => $item->getAuthor()
+               ];
+               $comments = $item->getCommentsUnescaped();
+               if ( $comments ) {
+                       $commentsEscaped = $this->xmlEncode( wfExpandUrl( $comments, PROTO_CURRENT ) );
+                       $templateParams["comments"] = $commentsEscaped;
+               }
+               print $this->templateParser->processTemplate( 'RSSItem', $templateParams );
        }
 
        /**
         * Output an RSS 2.0 footer
         */
        function outFooter() {
-       ?>
-       </channel>
-</rss><?php
+               print "</channel></rss>";
        }
 }
 
@@ -343,14 +410,16 @@ class RSSFeed extends ChannelFeed {
  */
 class AtomFeed extends ChannelFeed {
        /**
-        * Format a date given timestamp.
+        * Format a date given timestamp, if one is given.
         *
-        * @param string|int $timestamp
-        * @return string
+        * @param string|int|null $timestamp
+        * @return string|null
         */
        function formatTime( $timestamp ) {
-               // need to use RFC 822 time format at least for rss2.0
-               return gmdate( 'Y-m-d\TH:i:s', wfTimestamp( TS_UNIX, $timestamp ) );
+               if ( $timestamp ) {
+                       // need to use RFC 822 time format at least for rss2.0
+                       return gmdate( 'Y-m-d\TH:i:s', wfTimestamp( TS_UNIX, $timestamp ) );
+               }
        }
 
        /**
@@ -358,20 +427,20 @@ class AtomFeed extends ChannelFeed {
         */
        function outHeader() {
                global $wgVersion;
-
                $this->outXmlHeader();
-               // @codingStandardsIgnoreStart Ignore long lines and formatting issues.
-               ?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="<?php print $this->getLanguage() ?>">
-               <id><?php print $this->getFeedId() ?></id>
-               <title><?php print $this->getTitle() ?></title>
-               <link rel="self" type="application/atom+xml" href="<?php print wfExpandUrl( $this->getSelfUrl(), PROTO_CURRENT ) ?>"/>
-               <link rel="alternate" type="text/html" href="<?php print wfExpandUrl( $this->getUrl(), PROTO_CURRENT ) ?>"/>
-               <updated><?php print $this->formatTime( wfTimestampNow() ) ?>Z</updated>
-               <subtitle><?php print $this->getDescription() ?></subtitle>
-               <generator>MediaWiki <?php print $wgVersion ?></generator>
-
-<?php
-               // @codingStandardsIgnoreEnd
+               // Manually escaping rather than letting Mustache do it because Mustache
+               // uses htmlentities, which does not work with XML
+               $templateParams = [
+                       'language' => $this->xmlEncode( $this->getLanguage() ),
+                       'feedID' => $this->getFeedID(),
+                       'title' => $this->getTitle(),
+                       'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ),
+                       'selfUrl' => $this->getSelfUrl(),
+                       'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) ),
+                       'description' => $this->getDescription(),
+                       'version' => $this->xmlEncode( $wgVersion ),
+               ];
+               print $this->templateParser->processTemplate( 'AtomHeader', $templateParams );
        }
 
        /**
@@ -401,30 +470,24 @@ class AtomFeed extends ChannelFeed {
         */
        function outItem( $item ) {
                global $wgMimeType;
-               // @codingStandardsIgnoreStart Ignore long lines and formatting issues.
-       ?>
-       <entry>
-               <id><?php print $item->getUniqueId(); ?></id>
-               <title><?php print $item->getTitle(); ?></title>
-               <link rel="alternate" type="<?php print $wgMimeType ?>" href="<?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ); ?>"/>
-               <?php if ( $item->getDate() ) { ?>
-               <updated><?php print $this->formatTime( $item->getDate() ); ?>Z</updated>
-               <?php } ?>
-
-               <summary type="html"><?php print $item->getDescription() ?></summary>
-               <?php if ( $item->getAuthor() ) { ?><author><name><?php print $item->getAuthor(); ?></name></author><?php }?>
-       </entry>
-
-<?php /* @todo FIXME: Need to add comments
-       <?php if( $item->getComments() ) { ?><dc:comment><?php print $item->getComments() ?></dc:comment><?php }?>
-         */
+               // Manually escaping rather than letting Mustache do it because Mustache
+               // uses htmlentities, which does not work with XML
+               $templateParams = [
+                       "uniqueID" => $item->getUniqueId(),
+                       "title" => $item->getTitle(),
+                       "mimeType" => $this->xmlEncode( $wgMimeType ),
+                       "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ),
+                       "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ),
+                       "description" => $item->getDescription(),
+                       "author" => $item->getAuthor()
+               ];
+               print $this->templateParser->processTemplate( 'AtomItem', $templateParams );
        }
 
        /**
         * Outputs the footer for Atom 1.0 feed (basically '\</feed\>').
         */
-       function outFooter() {?>
-       </feed><?php
-               // @codingStandardsIgnoreEnd
+       function outFooter() {
+               print "</feed>";
        }
 }
index 1a33b76..523a0f9 100644 (file)
@@ -24,7 +24,6 @@ if ( !defined( 'MEDIAWIKI' ) ) {
        die( "This file is part of MediaWiki, it is not a valid entry point" );
 }
 
-use Liuggio\StatsdClient\Sender\SocketSender;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\ProcOpenError;
 use MediaWiki\Session\SessionManager;
@@ -1231,6 +1230,7 @@ function wfErrorLog( $text, $file, array $context = [] ) {
 
 /**
  * @todo document
+ * @todo Move logic to MediaWiki.php
  */
 function wfLogProfilingData() {
        global $wgDebugLogGroups, $wgDebugRawPage;
@@ -1242,23 +1242,13 @@ function wfLogProfilingData() {
        $profiler->setContext( $context );
        $profiler->logData();
 
-       $config = $context->getConfig();
-       $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
-       if ( $config->get( 'StatsdServer' ) && $stats->hasData() ) {
-               try {
-                       $statsdServer = explode( ':', $config->get( 'StatsdServer' ) );
-                       $statsdHost = $statsdServer[0];
-                       $statsdPort = isset( $statsdServer[1] ) ? $statsdServer[1] : 8125;
-                       $statsdSender = new SocketSender( $statsdHost, $statsdPort );
-                       $statsdClient = new SamplingStatsdClient( $statsdSender, true, false );
-                       $statsdClient->setSamplingRates( $config->get( 'StatsdSamplingRates' ) );
-                       $statsdClient->send( $stats->getData() );
-               } catch ( Exception $ex ) {
-                       MWExceptionHandler::logException( $ex );
-               }
-       }
+       // Send out any buffered statsd metrics as needed
+       MediaWiki::emitBufferedStatsdData(
+               MediaWikiServices::getInstance()->getStatsdDataFactory(),
+               $context->getConfig()
+       );
 
-       # Profiling must actually be enabled...
+       // Profiling must actually be enabled...
        if ( $profiler instanceof ProfilerStub ) {
                return;
        }
index 14d22ec..5193168 100644 (file)
@@ -560,26 +560,26 @@ class DiffHistoryBlob implements HistoryBlob {
                        $op = $x['op'];
                        ++$p;
                        switch ( $op ) {
-                       case self::XDL_BDOP_INS:
-                               $x = unpack( 'Csize', substr( $diff, $p, 1 ) );
-                               $p++;
-                               $out .= substr( $diff, $p, $x['size'] );
-                               $p += $x['size'];
-                               break;
-                       case self::XDL_BDOP_INSB:
-                               $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) );
-                               $p += 4;
-                               $out .= substr( $diff, $p, $x['csize'] );
-                               $p += $x['csize'];
-                               break;
-                       case self::XDL_BDOP_CPY:
-                               $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) );
-                               $p += 8;
-                               $out .= substr( $base, $x['off'], $x['csize'] );
-                               break;
-                       default:
-                               wfDebug( __METHOD__ . ": invalid op\n" );
-                               return false;
+                               case self::XDL_BDOP_INS:
+                                       $x = unpack( 'Csize', substr( $diff, $p, 1 ) );
+                                       $p++;
+                                       $out .= substr( $diff, $p, $x['size'] );
+                                       $p += $x['size'];
+                                       break;
+                               case self::XDL_BDOP_INSB:
+                                       $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) );
+                                       $p += 4;
+                                       $out .= substr( $diff, $p, $x['csize'] );
+                                       $p += $x['csize'];
+                                       break;
+                               case self::XDL_BDOP_CPY:
+                                       $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) );
+                                       $p += 8;
+                                       $out .= substr( $base, $x['off'], $x['csize'] );
+                                       break;
+                               default:
+                                       wfDebug( __METHOD__ . ": invalid op\n" );
+                                       return false;
                        }
                }
                return $out;
index a0332cf..c0255ac 100644 (file)
@@ -1192,12 +1192,12 @@ class Linker {
                                                $section = str_replace( '[[', '', $section );
                                                $section = str_replace( ']]', '', $section );
 
-                                               $section = Sanitizer::normalizeSectionNameWhitespace( $section ); # T24784
+                                               $section = substr( Parser::guessSectionNameFromStrippedText( $section ), 1 );
                                                if ( $local ) {
-                                                       $sectionTitle = Title::newFromText( '#' . $section );
+                                                       $sectionTitle = Title::makeTitleSafe( NS_MAIN, '', $section );
                                                } else {
                                                        $sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
-                                                               $title->getDBkey(), Sanitizer::decodeCharReferences( $section ) );
+                                                               $title->getDBkey(), $section );
                                                }
                                                if ( $sectionTitle ) {
                                                        $link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' );
@@ -1595,7 +1595,12 @@ class Linker {
                $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
 
                return '<div id="toc" class="toc">'
-                       . '<div class="toctitle"><h2>' . $title . "</h2></div>\n"
+                       . Html::openElement( 'div', [
+                               'class' => 'toctitle',
+                               'lang' => $lang->getHtmlCode(),
+                               'dir' => $lang->getDir(),
+                       ] )
+                       . '<h2>' . $title . "</h2></div>\n"
                        . $toc
                        . "</ul>\n</div>\n";
        }
@@ -2119,8 +2124,6 @@ class Linker {
                return Xml::tags( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
        }
 
-       /* Deprecated methods */
-
        /**
         * Returns the attributes for the tooltip and access key.
         *
index 9e3bc10..beb9de5 100644 (file)
@@ -26,6 +26,7 @@ use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\ChronologyProtector;
 use Wikimedia\Rdbms\LBFactory;
 use Wikimedia\Rdbms\DBConnectionError;
+use Liuggio\StatsdClient\Sender\SocketSender;
 
 /**
  * The MediaWiki class is the helper class for the index.php entry point.
@@ -912,6 +913,34 @@ class MediaWiki {
                wfDebug( "Request ended normally\n" );
        }
 
+       /**
+        * Send out any buffered statsd data according to sampling rules
+        *
+        * @param IBufferingStatsdDataFactory $stats
+        * @param Config $config
+        * @throws ConfigException
+        * @since 1.31
+        */
+       public static function emitBufferedStatsdData(
+               IBufferingStatsdDataFactory $stats, Config $config
+       ) {
+               if ( $config->get( 'StatsdServer' ) && $stats->hasData() ) {
+                       try {
+                               $statsdServer = explode( ':', $config->get( 'StatsdServer' ) );
+                               $statsdHost = $statsdServer[0];
+                               $statsdPort = isset( $statsdServer[1] ) ? $statsdServer[1] : 8125;
+                               $statsdSender = new SocketSender( $statsdHost, $statsdPort );
+                               $statsdClient = new SamplingStatsdClient( $statsdSender, true, false );
+                               $statsdClient->setSamplingRates( $config->get( 'StatsdSamplingRates' ) );
+                               $statsdClient->send( $stats->getData() );
+
+                               $stats->clearData(); // empty buffer for the next round
+                       } catch ( Exception $ex ) {
+                               MWExceptionHandler::logException( $ex );
+                       }
+               }
+       }
+
        /**
         * Potentially open a socket and sent an HTTP request back to the server
         * to run a specified number of jobs. This registers a callback to cleanup
index 19b71f1..04c67fb 100644 (file)
@@ -11,6 +11,9 @@ use GlobalVarConfig;
 use Hooks;
 use IBufferingStatsdDataFactory;
 use MediaWiki\Shell\CommandFactory;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\RevisionStore;
 use Wikimedia\Rdbms\LBFactory;
 use LinkCache;
 use Wikimedia\Rdbms\LoadBalancer;
@@ -698,6 +701,30 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'ExternalStoreFactory' );
        }
 
+       /**
+        * @since 1.31
+        * @return BlobStoreFactory
+        */
+       public function getBlobStoreFactory() {
+               return $this->getService( 'BlobStoreFactory' );
+       }
+
+       /**
+        * @since 1.31
+        * @return BlobStore
+        */
+       public function getBlobStore() {
+               return $this->getService( '_SqlBlobStore' );
+       }
+
+       /**
+        * @since 1.31
+        * @return RevisionStore
+        */
+       public function getRevisionStore() {
+               return $this->getService( 'RevisionStore' );
+       }
+
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service getter here, don't forget to add a test
        // case for it in MediaWikiServicesTest::provideGetters() and in
index 9d63869..b969e03 100644 (file)
@@ -24,6 +24,7 @@
  *
  * @file
  */
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Timestamp\TimestampException;
 use Wikimedia\Rdbms\IDatabase;
 
@@ -335,6 +336,10 @@ class MergeHistory {
                }
                $this->dest->invalidateCache(); // update histories
 
+               // Duplicate watchers of the old article to the new article on history merge
+               $store = MediaWikiServices::getInstance()->getWatchedItemStore();
+               $store->duplicateAllAssociatedEntries( $this->source, $this->dest );
+
                // Update our logs
                $logEntry = new ManualLogEntry( 'merge', 'merge' );
                $logEntry->setPerformer( $user );
index 16ae839..e55eaaf 100644 (file)
@@ -1308,16 +1308,15 @@ class Message implements MessageSpecifier, Serializable {
         */
        protected function formatPlaintext( $plaintext, $format ) {
                switch ( $format ) {
-               case self::FORMAT_TEXT:
-               case self::FORMAT_PLAIN:
-                       return $plaintext;
-
-               case self::FORMAT_PARSE:
-               case self::FORMAT_BLOCK_PARSE:
-               case self::FORMAT_ESCAPED:
-               default:
-                       return htmlspecialchars( $plaintext, ENT_QUOTES );
-
+                       case self::FORMAT_TEXT:
+                       case self::FORMAT_PLAIN:
+                               return $plaintext;
+
+                       case self::FORMAT_PARSE:
+                       case self::FORMAT_BLOCK_PARSE:
+                       case self::FORMAT_ESCAPED:
+                       default:
+                               return htmlspecialchars( $plaintext, ENT_QUOTES );
                }
        }
 
index 92963fd..a2d445f 100644 (file)
@@ -366,8 +366,8 @@ class OutputPage extends ContextSource {
         * Add a new "<meta>" tag
         * To add an http-equiv meta tag, precede the name with "http:"
         *
-        * @param string $name Tag name
-        * @param string $val Tag value
+        * @param string $name Name of the meta tag
+        * @param string $val Value of the meta tag
         */
        function addMeta( $name, $val ) {
                array_push( $this->mMetatags, [ $name, $val ] );
@@ -3243,6 +3243,8 @@ class OutputPage extends ContextSource {
                        && ( $relevantTitle->exists() || $relevantTitle->quickUserCan( 'create', $user ) );
 
                foreach ( $title->getRestrictionTypes() as $type ) {
+                       // Following keys are set in $vars:
+                       // wgRestrictionCreate, wgRestrictionEdit, wgRestrictionMove, wgRestrictionUpload
                        $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
                }
 
@@ -3331,10 +3333,14 @@ class OutputPage extends ContextSource {
                ] );
 
                if ( $config->get( 'ReferrerPolicy' ) !== false ) {
-                       $tags['meta-referrer'] = Html::element( 'meta', [
-                               'name' => 'referrer',
-                               'content' => $config->get( 'ReferrerPolicy' )
-                       ] );
+                       // Per https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
+                       // fallbacks should come before the primary value so we need to reverse the array.
+                       foreach ( array_reverse( (array)$config->get( 'ReferrerPolicy' ) ) as $i => $policy ) {
+                               $tags["meta-referrer-$i"] = Html::element( 'meta', [
+                                       'name' => 'referrer',
+                                       'content' => $policy,
+                               ] );
+                       }
                }
 
                $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
index 878462d..33a975d 100644 (file)
@@ -534,12 +534,22 @@ class Preferences {
 
                        if ( $config->get( 'EnableUserEmail' ) && $user->isAllowed( 'sendemail' ) ) {
                                $defaultPreferences['disablemail'] = [
+                                       'id' => 'wpAllowEmail',
                                        'type' => 'toggle',
                                        'invert' => true,
                                        'section' => 'personal/email',
                                        'label-message' => 'allowemail',
                                        'disabled' => $disableEmailPrefs,
                                ];
+
+                               $defaultPreferences['email-allow-new-users'] = [
+                                       'id' => 'wpAllowEmailFromNewUsers',
+                                       'type' => 'toggle',
+                                       'section' => 'personal/email',
+                                       'label-message' => 'email-allow-new-users-label',
+                                       'disabled' => $disableEmailPrefs,
+                               ];
+
                                $defaultPreferences['ccmeonemails'] = [
                                        'type' => 'toggle',
                                        'section' => 'personal/email',
@@ -547,10 +557,7 @@ class Preferences {
                                        'disabled' => $disableEmailPrefs,
                                ];
 
-                               if ( $config->get( 'EnableUserEmailBlacklist' )
-                                        && !$disableEmailPrefs
-                                        && !(bool)$user->getOption( 'disablemail' )
-                               ) {
+                               if ( $config->get( 'EnableUserEmailBlacklist' ) ) {
                                        $lookup = CentralIdLookup::factory();
                                        $ids = $user->getOption( 'email-blacklist', [] );
                                        $names = $ids ? $lookup->namesFromCentralIds( $ids, $user ) : [];
@@ -560,6 +567,7 @@ class Preferences {
                                                'label-message' => 'email-blacklist-label',
                                                'section' => 'personal/email',
                                                'default' => implode( "\n", $names ),
+                                               'disabled' => $disableEmailPrefs,
                                        ];
                                }
                        }
index 25c89c2..1d7e62c 100644 (file)
  * @file
  */
 
-use Wikimedia\Rdbms\Database;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionAccessException;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\RevisionStoreRecord;
+use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWiki\User\UserIdentityValue;
 use Wikimedia\Rdbms\IDatabase;
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
@@ -28,78 +35,54 @@ use Wikimedia\Rdbms\ResultWrapper;
 use Wikimedia\Rdbms\FakeResultWrapper;
 
 /**
- * @todo document
+ * @deprecated since 1.31, use RevisionRecord, RevisionStore, and BlobStore instead.
  */
 class Revision implements IDBAccessObject {
-       /** @var int|null */
-       protected $mId;
-       /** @var int|null */
-       protected $mPage;
-       /** @var string */
-       protected $mUserText;
-       /** @var string */
-       protected $mOrigUserText;
-       /** @var int */
-       protected $mUser;
-       /** @var bool */
-       protected $mMinorEdit;
-       /** @var string */
-       protected $mTimestamp;
-       /** @var int */
-       protected $mDeleted;
-       /** @var int */
-       protected $mSize;
-       /** @var string */
-       protected $mSha1;
-       /** @var int */
-       protected $mParentId;
-       /** @var string */
-       protected $mComment;
-       /** @var string */
-       protected $mText;
-       /** @var int */
-       protected $mTextId;
-       /** @var int */
-       protected $mUnpatrolled;
-
-       /** @var stdClass|null */
-       protected $mTextRow;
-
-       /**  @var null|Title */
-       protected $mTitle;
-       /** @var bool */
-       protected $mCurrent;
-       /** @var string */
-       protected $mContentModel;
-       /** @var string */
-       protected $mContentFormat;
-
-       /** @var Content|null|bool */
-       protected $mContent;
-       /** @var null|ContentHandler */
-       protected $mContentHandler;
-
-       /** @var int */
-       protected $mQueryFlags = 0;
-       /** @var bool Used for cached values to reload user text and rev_deleted */
-       protected $mRefreshMutableFields = false;
-       /** @var string Wiki ID; false means the current wiki */
-       protected $mWiki = false;
+
+       /** @var RevisionRecord */
+       protected $mRecord;
 
        // Revision deletion constants
-       const DELETED_TEXT = 1;
-       const DELETED_COMMENT = 2;
-       const DELETED_USER = 4;
-       const DELETED_RESTRICTED = 8;
-       const SUPPRESSED_USER = 12; // convenience
-       const SUPPRESSED_ALL = 15; // convenience
+       const DELETED_TEXT = RevisionRecord::DELETED_TEXT;
+       const DELETED_COMMENT = RevisionRecord::DELETED_COMMENT;
+       const DELETED_USER = RevisionRecord::DELETED_USER;
+       const DELETED_RESTRICTED = RevisionRecord::DELETED_RESTRICTED;
+       const SUPPRESSED_USER = RevisionRecord::SUPPRESSED_USER;
+       const SUPPRESSED_ALL = RevisionRecord::SUPPRESSED_ALL;
 
        // Audience options for accessors
-       const FOR_PUBLIC = 1;
-       const FOR_THIS_USER = 2;
-       const RAW = 3;
+       const FOR_PUBLIC = RevisionRecord::FOR_PUBLIC;
+       const FOR_THIS_USER = RevisionRecord::FOR_THIS_USER;
+       const RAW = RevisionRecord::RAW;
 
-       const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count
+       const TEXT_CACHE_GROUP = SqlBlobStore::TEXT_CACHE_GROUP;
+
+       /**
+        * @return RevisionStore
+        */
+       protected static function getRevisionStore() {
+               return MediaWikiServices::getInstance()->getRevisionStore();
+       }
+
+       /**
+        * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+        *
+        * @return SqlBlobStore
+        */
+       protected static function getBlobStore( $wiki = false ) {
+               $store = MediaWikiServices::getInstance()
+                       ->getBlobStoreFactory()
+                       ->newSqlBlobStore( $wiki );
+
+               if ( !$store instanceof SqlBlobStore ) {
+                       throw new RuntimeException(
+                               'The backwards compatibility code in Revision currently requires the BlobStore '
+                               . 'service to be an SqlBlobStore instance, but it is a ' . get_class( $store )
+                       );
+               }
+
+               return $store;
+       }
 
        /**
         * Load a page revision from a given revision ID number.
@@ -114,7 +97,8 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public static function newFromId( $id, $flags = 0 ) {
-               return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
+               $rec = self::getRevisionStore()->getRevisionById( $id, $flags );
+               return $rec === null ? null : new Revision( $rec, $flags );
        }
 
        /**
@@ -132,20 +116,8 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
-               $conds = [
-                       'page_namespace' => $linkTarget->getNamespace(),
-                       'page_title' => $linkTarget->getDBkey()
-               ];
-               if ( $id ) {
-                       // Use the specified ID
-                       $conds['rev_id'] = $id;
-                       return self::newFromConds( $conds, $flags );
-               } else {
-                       // Use a join to get the latest revision
-                       $conds[] = 'rev_id=page_latest';
-                       $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
-                       return self::loadFromConds( $db, $conds, $flags );
-               }
+               $rec = self::getRevisionStore()->getRevisionByTitle( $linkTarget, $id, $flags );
+               return $rec === null ? null : new Revision( $rec, $flags );
        }
 
        /**
@@ -163,22 +135,13 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
-               $conds = [ 'page_id' => $pageId ];
-               if ( $revId ) {
-                       $conds['rev_id'] = $revId;
-                       return self::newFromConds( $conds, $flags );
-               } else {
-                       // Use a join to get the latest revision
-                       $conds[] = 'rev_id = page_latest';
-                       $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
-                       return self::loadFromConds( $db, $conds, $flags );
-               }
+               $rec = self::getRevisionStore()->getRevisionByPageId( $pageId, $revId, $flags );
+               return $rec === null ? null : new Revision( $rec, $flags );
        }
 
        /**
         * Make a fake revision object from an archive table row. This is queried
         * for permissions or even inserted (as in Special:Undelete)
-        * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
         *
         * @param object $row
         * @param array $overrides
@@ -187,68 +150,79 @@ class Revision implements IDBAccessObject {
         * @return Revision
         */
        public static function newFromArchiveRow( $row, $overrides = [] ) {
-               global $wgContentHandlerUseDB;
-
-               $attribs = $overrides + [
-                       'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
-                       'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
-                       'comment'    => CommentStore::newKey( 'ar_comment' )
-                               // Legacy because $row may have come from self::selectArchiveFields()
-                               ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text,
-                       'user'       => $row->ar_user,
-                       'user_text'  => $row->ar_user_text,
-                       'timestamp'  => $row->ar_timestamp,
-                       'minor_edit' => $row->ar_minor_edit,
-                       'text_id'    => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
-                       'deleted'    => $row->ar_deleted,
-                       'len'        => $row->ar_len,
-                       'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
-                       'content_model'   => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
-                       'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
-               ];
-
-               if ( !$wgContentHandlerUseDB ) {
-                       unset( $attribs['content_model'] );
-                       unset( $attribs['content_format'] );
-               }
+               /**
+                * MCR Migration: https://phabricator.wikimedia.org/T183564
+                * This method used to overwrite attributes, then passed to Revision::__construct
+                * RevisionStore::newRevisionFromArchiveRow instead overrides row field names
+                * So do a conversion here.
+                */
+               if ( array_key_exists( 'page', $overrides ) ) {
+                       $overrides['page_id'] = $overrides['page'];
+                       unset( $overrides['page'] );
+               }
+
+               /**
+                * We require a Title for both the Revision object and the RevisionRecord.
+                * Below is duplicated logic from RevisionStore::newRevisionFromArchiveRow
+                * to fetch a title in order pass it into the Revision object.
+                */
+               $title = null;
+               if ( isset( $overrides['title'] ) ) {
+                       if ( !( $overrides['title'] instanceof Title ) ) {
+                               throw new MWException( 'title field override must contain a Title object.' );
+                       }
 
-               if ( !isset( $attribs['title'] )
-                       && isset( $row->ar_namespace )
-                       && isset( $row->ar_title )
-               ) {
-                       $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+                       $title = $overrides['title'];
                }
-
-               if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
-                       // Pre-1.5 ar_text row
-                       $attribs['text'] = self::getRevisionText( $row, 'ar_' );
-                       if ( $attribs['text'] === false ) {
-                               throw new MWException( 'Unable to load text from archive row (possibly T24624)' );
+               if ( $title !== null ) {
+                       if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
+                               $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+                       } else {
+                               throw new InvalidArgumentException(
+                                       'A Title or ar_namespace and ar_title must be given'
+                               );
                        }
                }
-               return new self( $attribs );
+
+               $rec = self::getRevisionStore()->newRevisionFromArchiveRow( $row, 0, null, $overrides );
+               return new Revision( $rec, self::READ_NORMAL, $title );
        }
 
        /**
         * @since 1.19
         *
-        * @param object $row
+        * MCR migration note: replaced by RevisionStore::newRevisionFromRow(). Note that
+        * newFromRow() also accepts arrays, while newRevisionFromRow() does not. Instead,
+        * a MutableRevisionRecord should be constructed directly. RevisionStore::newRevisionFromArray()
+        * can be used as a temporary replacement, but should be avoided.
+        *
+        * @param object|array $row
         * @return Revision
         */
        public static function newFromRow( $row ) {
-               return new self( $row );
+               if ( is_array( $row ) ) {
+                       $rec = self::getRevisionStore()->newMutableRevisionFromArray( $row );
+               } else {
+                       $rec = self::getRevisionStore()->newRevisionFromRow( $row );
+               }
+
+               return new Revision( $rec );
        }
 
        /**
         * Load a page revision from a given revision ID number.
         * Returns null if no such revision can be found.
         *
+        * @deprecated since 1.31, use RevisionStore::getRevisionById() instead.
+        *
         * @param IDatabase $db
         * @param int $id
         * @return Revision|null
         */
        public static function loadFromId( $db, $id ) {
-               return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
+               wfDeprecated( __METHOD__, '1.31' ); // no known callers
+               $rec = self::getRevisionStore()->loadRevisionFromId( $db, $id );
+               return $rec === null ? null : new Revision( $rec );
        }
 
        /**
@@ -256,19 +230,16 @@ class Revision implements IDBAccessObject {
         * that's attached to a given page. If not attached
         * to that page, will return null.
         *
+        * @deprecated since 1.31, use RevisionStore::getRevisionByPageId() instead.
+        *
         * @param IDatabase $db
         * @param int $pageid
         * @param int $id
         * @return Revision|null
         */
        public static function loadFromPageId( $db, $pageid, $id = 0 ) {
-               $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
-               if ( $id ) {
-                       $conds['rev_id'] = intval( $id );
-               } else {
-                       $conds[] = 'rev_id=page_latest';
-               }
-               return self::loadFromConds( $db, $conds );
+               $rec = self::getRevisionStore()->loadRevisionFromPageId( $db, $pageid, $id );
+               return $rec === null ? null : new Revision( $rec );
        }
 
        /**
@@ -276,24 +247,16 @@ class Revision implements IDBAccessObject {
         * that's attached to a given page. If not attached
         * to that page, will return null.
         *
+        * @deprecated since 1.31, use RevisionStore::getRevisionByTitle() instead.
+        *
         * @param IDatabase $db
         * @param Title $title
         * @param int $id
         * @return Revision|null
         */
        public static function loadFromTitle( $db, $title, $id = 0 ) {
-               if ( $id ) {
-                       $matchId = intval( $id );
-               } else {
-                       $matchId = 'page_latest';
-               }
-               return self::loadFromConds( $db,
-                       [
-                               "rev_id=$matchId",
-                               'page_namespace' => $title->getNamespace(),
-                               'page_title' => $title->getDBkey()
-                       ]
-               );
+               $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, $title, $id );
+               return $rec === null ? null : new Revision( $rec );
        }
 
        /**
@@ -301,73 +264,17 @@ class Revision implements IDBAccessObject {
         * WARNING: Timestamps may in some circumstances not be unique,
         * so this isn't the best key to use.
         *
+        * @deprecated since 1.31, use RevisionStore::loadRevisionFromTimestamp() instead.
+        *
         * @param IDatabase $db
         * @param Title $title
         * @param string $timestamp
         * @return Revision|null
         */
        public static function loadFromTimestamp( $db, $title, $timestamp ) {
-               return self::loadFromConds( $db,
-                       [
-                               'rev_timestamp' => $db->timestamp( $timestamp ),
-                               'page_namespace' => $title->getNamespace(),
-                               'page_title' => $title->getDBkey()
-                       ]
-               );
-       }
-
-       /**
-        * Given a set of conditions, fetch a revision
-        *
-        * This method is used then a revision ID is qualified and
-        * will incorporate some basic replica DB/master fallback logic
-        *
-        * @param array $conditions
-        * @param int $flags (optional)
-        * @return Revision|null
-        */
-       private static function newFromConds( $conditions, $flags = 0 ) {
-               $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
-
-               $rev = self::loadFromConds( $db, $conditions, $flags );
-               // Make sure new pending/committed revision are visibile later on
-               // within web requests to certain avoid bugs like T93866 and T94407.
-               if ( !$rev
-                       && !( $flags & self::READ_LATEST )
-                       && wfGetLB()->getServerCount() > 1
-                       && wfGetLB()->hasOrMadeRecentMasterChanges()
-               ) {
-                       $flags = self::READ_LATEST;
-                       $db = wfGetDB( DB_MASTER );
-                       $rev = self::loadFromConds( $db, $conditions, $flags );
-               }
-
-               if ( $rev ) {
-                       $rev->mQueryFlags = $flags;
-               }
-
-               return $rev;
-       }
-
-       /**
-        * Given a set of conditions, fetch a revision from
-        * the given database connection.
-        *
-        * @param IDatabase $db
-        * @param array $conditions
-        * @param int $flags (optional)
-        * @return Revision|null
-        */
-       private static function loadFromConds( $db, $conditions, $flags = 0 ) {
-               $row = self::fetchFromConds( $db, $conditions, $flags );
-               if ( $row ) {
-                       $rev = new Revision( $row );
-                       $rev->mWiki = $db->getDomainID();
-
-                       return $rev;
-               }
-
-               return null;
+               // XXX: replace loadRevisionFromTimestamp by getRevisionByTimestamp?
+               $rec = self::getRevisionStore()->loadRevisionFromTimestamp( $db, $title, $timestamp );
+               return $rec === null ? null : new Revision( $rec );
        }
 
        /**
@@ -377,52 +284,18 @@ class Revision implements IDBAccessObject {
         *
         * @param LinkTarget $title
         * @return ResultWrapper
-        * @deprecated Since 1.28
+        * @deprecated Since 1.28, no callers in core nor in known extensions. No-op since 1.31.
         */
        public static function fetchRevision( LinkTarget $title ) {
-               $row = self::fetchFromConds(
-                       wfGetDB( DB_REPLICA ),
-                       [
-                               'rev_id=page_latest',
-                               'page_namespace' => $title->getNamespace(),
-                               'page_title' => $title->getDBkey()
-                       ]
-               );
-
-               return new FakeResultWrapper( $row ? [ $row ] : [] );
-       }
-
-       /**
-        * Given a set of conditions, return a ResultWrapper
-        * which will return matching database rows with the
-        * fields necessary to build Revision objects.
-        *
-        * @param IDatabase $db
-        * @param array $conditions
-        * @param int $flags (optional)
-        * @return stdClass
-        */
-       private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
-               $revQuery = self::getQueryInfo( [ 'page', 'user' ] );
-               $options = [];
-               if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
-                       $options[] = 'FOR UPDATE';
-               }
-               return $db->selectRow(
-                       $revQuery['tables'],
-                       $revQuery['fields'],
-                       $conditions,
-                       __METHOD__,
-                       $options,
-                       $revQuery['joins']
-               );
+               wfDeprecated( __METHOD__, '1.31' );
+               return new FakeResultWrapper( [] );
        }
 
        /**
         * Return the value of a select() JOIN conds array for the user table.
         * This will get user table rows for logged-in users.
         * @since 1.19
-        * @deprecated since 1.31, use self::getQueryInfo( [ 'user' ] ) instead.
+        * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
         * @return array
         */
        public static function userJoinCond() {
@@ -434,7 +307,7 @@ class Revision implements IDBAccessObject {
         * Return the value of a select() page conds array for the page table.
         * This will assure that the revision(s) are not orphaned from live pages.
         * @since 1.19
-        * @deprecated since 1.31, use self::getQueryInfo( [ 'page' ] ) instead.
+        * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
         * @return array
         */
        public static function pageJoinCond() {
@@ -445,7 +318,7 @@ class Revision implements IDBAccessObject {
        /**
         * Return the list of revision fields that should be selected to create
         * a new revision.
-        * @deprecated since 1.31, use self::getQueryInfo() instead.
+        * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
         * @return array
         */
        public static function selectFields() {
@@ -480,7 +353,7 @@ class Revision implements IDBAccessObject {
        /**
         * Return the list of revision fields that should be selected to create
         * a new revision from an archive row.
-        * @deprecated since 1.31, use self::getArchiveQueryInfo() instead.
+        * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
         * @return array
         */
        public static function selectArchiveFields() {
@@ -516,7 +389,7 @@ class Revision implements IDBAccessObject {
        /**
         * Return the list of text fields that should be selected to read the
         * revision text
-        * @deprecated since 1.31, use self::getQueryInfo( [ 'text' ] ) instead.
+        * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'text' ] ) instead.
         * @return array
         */
        public static function selectTextFields() {
@@ -529,7 +402,7 @@ class Revision implements IDBAccessObject {
 
        /**
         * Return the list of page fields that should be selected from page table
-        * @deprecated since 1.31, use self::getQueryInfo( [ 'page' ] ) instead.
+        * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
         * @return array
         */
        public static function selectPageFields() {
@@ -546,7 +419,7 @@ class Revision implements IDBAccessObject {
 
        /**
         * Return the list of user fields that should be selected from user table
-        * @deprecated since 1.31, use self::getQueryInfo( [ 'user' ] ) instead.
+        * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
         * @return array
         */
        public static function selectUserFields() {
@@ -558,6 +431,7 @@ class Revision implements IDBAccessObject {
         * Return the tables, fields, and join conditions to be selected to create
         * a new revision object.
         * @since 1.31
+        * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
         * @param array $options Any combination of the following strings
         *  - 'page': Join with the page table, and select fields to identify the page
         *  - 'user': Join with the user table, and select the user name
@@ -568,104 +442,21 @@ class Revision implements IDBAccessObject {
         *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
         */
        public static function getQueryInfo( $options = [] ) {
-               global $wgContentHandlerUseDB;
-
-               $commentQuery = CommentStore::newKey( 'rev_comment' )->getJoin();
-               $ret = [
-                       'tables' => [ 'revision' ] + $commentQuery['tables'],
-                       'fields' => [
-                               'rev_id',
-                               'rev_page',
-                               'rev_text_id',
-                               'rev_timestamp',
-                               'rev_user_text',
-                               'rev_user',
-                               'rev_minor_edit',
-                               'rev_deleted',
-                               'rev_len',
-                               'rev_parent_id',
-                               'rev_sha1',
-                       ] + $commentQuery['fields'],
-                       'joins' => $commentQuery['joins'],
-               ];
-
-               if ( $wgContentHandlerUseDB ) {
-                       $ret['fields'][] = 'rev_content_format';
-                       $ret['fields'][] = 'rev_content_model';
-               }
-
-               if ( in_array( 'page', $options, true ) ) {
-                       $ret['tables'][] = 'page';
-                       $ret['fields'] = array_merge( $ret['fields'], [
-                               'page_namespace',
-                               'page_title',
-                               'page_id',
-                               'page_latest',
-                               'page_is_redirect',
-                               'page_len',
-                       ] );
-                       $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
-               }
-
-               if ( in_array( 'user', $options, true ) ) {
-                       $ret['tables'][] = 'user';
-                       $ret['fields'] = array_merge( $ret['fields'], [
-                               'user_name',
-                       ] );
-                       $ret['joins']['user'] = [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
-               }
-
-               if ( in_array( 'text', $options, true ) ) {
-                       $ret['tables'][] = 'text';
-                       $ret['fields'] = array_merge( $ret['fields'], [
-                               'old_text',
-                               'old_flags'
-                       ] );
-                       $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
-               }
-
-               return $ret;
+               return self::getRevisionStore()->getQueryInfo( $options );
        }
 
        /**
         * Return the tables, fields, and join conditions to be selected to create
         * a new archived revision object.
         * @since 1.31
+        * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
         * @return array With three keys:
         *   - tables: (string[]) to include in the `$table` to `IDatabase->select()`
         *   - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
         *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
         */
        public static function getArchiveQueryInfo() {
-               global $wgContentHandlerUseDB;
-
-               $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
-               $ret = [
-                       'tables' => [ 'archive' ] + $commentQuery['tables'],
-                       'fields' => [
-                               'ar_id',
-                               'ar_page_id',
-                               'ar_rev_id',
-                               'ar_text',
-                               'ar_text_id',
-                               'ar_timestamp',
-                               'ar_user_text',
-                               'ar_user',
-                               'ar_minor_edit',
-                               'ar_deleted',
-                               'ar_len',
-                               'ar_parent_id',
-                               'ar_sha1',
-                       ] + $commentQuery['fields'],
-                       'joins' => $commentQuery['joins'],
-               ];
-
-               if ( $wgContentHandlerUseDB ) {
-                       $ret['fields'][] = 'ar_content_format';
-                       $ret['fields'][] = 'ar_content_model';
-               }
-
-               return $ret;
+               return self::getRevisionStore()->getArchiveQueryInfo();
        }
 
        /**
@@ -675,203 +466,49 @@ class Revision implements IDBAccessObject {
         * @return array
         */
        public static function getParentLengths( $db, array $revIds ) {
-               $revLens = [];
-               if ( !$revIds ) {
-                       return $revLens; // empty
-               }
-               $res = $db->select( 'revision',
-                       [ 'rev_id', 'rev_len' ],
-                       [ 'rev_id' => $revIds ],
-                       __METHOD__ );
-               foreach ( $res as $row ) {
-                       $revLens[$row->rev_id] = $row->rev_len;
-               }
-               return $revLens;
+               return self::getRevisionStore()->listRevisionSizes( $db, $revIds );
        }
 
        /**
-        * @param object|array $row Either a database row or an array
-        * @throws MWException
+        * @param object|array|RevisionRecord $row Either a database row or an array
+        * @param int $queryFlags
+        * @param Title|null $title
+        *
         * @access private
         */
-       public function __construct( $row ) {
-               if ( is_object( $row ) ) {
-                       $this->constructFromDbRowObject( $row );
-               } elseif ( is_array( $row ) ) {
-                       $this->constructFromRowArray( $row );
-               } else {
-                       throw new MWException( 'Revision constructor passed invalid row format.' );
-               }
-               $this->mUnpatrolled = null;
-       }
+       function __construct( $row, $queryFlags = 0, Title $title = null ) {
+               global $wgUser;
 
-       /**
-        * @param object $row
-        */
-       private function constructFromDbRowObject( $row ) {
-               $this->mId = intval( $row->rev_id );
-               $this->mPage = intval( $row->rev_page );
-               $this->mTextId = intval( $row->rev_text_id );
-               $this->mComment = CommentStore::newKey( 'rev_comment' )
-                       // Legacy because $row may have come from self::selectFields()
-                       ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
-               $this->mUser = intval( $row->rev_user );
-               $this->mMinorEdit = intval( $row->rev_minor_edit );
-               $this->mTimestamp = $row->rev_timestamp;
-               $this->mDeleted = intval( $row->rev_deleted );
-
-               if ( !isset( $row->rev_parent_id ) ) {
-                       $this->mParentId = null;
-               } else {
-                       $this->mParentId = intval( $row->rev_parent_id );
-               }
-
-               if ( !isset( $row->rev_len ) ) {
-                       $this->mSize = null;
-               } else {
-                       $this->mSize = intval( $row->rev_len );
-               }
-
-               if ( !isset( $row->rev_sha1 ) ) {
-                       $this->mSha1 = null;
-               } else {
-                       $this->mSha1 = $row->rev_sha1;
-               }
-
-               if ( isset( $row->page_latest ) ) {
-                       $this->mCurrent = ( $row->rev_id == $row->page_latest );
-                       $this->mTitle = Title::newFromRow( $row );
-               } else {
-                       $this->mCurrent = false;
-                       $this->mTitle = null;
-               }
-
-               if ( !isset( $row->rev_content_model ) ) {
-                       $this->mContentModel = null; # determine on demand if needed
-               } else {
-                       $this->mContentModel = strval( $row->rev_content_model );
-               }
-
-               if ( !isset( $row->rev_content_format ) ) {
-                       $this->mContentFormat = null; # determine on demand if needed
-               } else {
-                       $this->mContentFormat = strval( $row->rev_content_format );
-               }
+               if ( $row instanceof RevisionRecord ) {
+                       $this->mRecord = $row;
+               } elseif ( is_array( $row ) ) {
+                       if ( !isset( $row['user'] ) && !isset( $row['user_text'] ) ) {
+                               $row['user'] = $wgUser;
+                       }
 
-               // Lazy extraction...
-               $this->mText = null;
-               if ( isset( $row->old_text ) ) {
-                       $this->mTextRow = $row;
+                       $this->mRecord = self::getRevisionStore()->newMutableRevisionFromArray(
+                               $row,
+                               $queryFlags,
+                               $title
+                       );
+               } elseif ( is_object( $row ) ) {
+                       $this->mRecord = self::getRevisionStore()->newRevisionFromRow(
+                               $row,
+                               $queryFlags,
+                               $title
+                       );
                } else {
-                       // 'text' table row entry will be lazy-loaded
-                       $this->mTextRow = null;
-               }
-
-               // Use user_name for users and rev_user_text for IPs...
-               $this->mUserText = null; // lazy load if left null
-               if ( $this->mUser == 0 ) {
-                       $this->mUserText = $row->rev_user_text; // IP user
-               } elseif ( isset( $row->user_name ) ) {
-                       $this->mUserText = $row->user_name; // logged-in user
+                       throw new InvalidArgumentException(
+                               '$row must be a row object, an associative array, or a RevisionRecord'
+                       );
                }
-               $this->mOrigUserText = $row->rev_user_text;
        }
 
        /**
-        * @param array $row
-        *
-        * @throws MWException
+        * @return RevisionRecord
         */
-       private function constructFromRowArray( array $row ) {
-               // Build a new revision to be saved...
-               global $wgUser; // ugh
-
-               # if we have a content object, use it to set the model and type
-               if ( !empty( $row['content'] ) ) {
-                       if ( !( $row['content'] instanceof Content ) ) {
-                               throw new MWException( '`content` field must contain a Content object.' );
-                       }
-
-                       // @todo when is that set? test with external store setup! check out insertOn() [dk]
-                       if ( !empty( $row['text_id'] ) ) {
-                               throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
-                                       "can't serialize content object" );
-                       }
-
-                       $row['content_model'] = $row['content']->getModel();
-                       # note: mContentFormat is initializes later accordingly
-                       # note: content is serialized later in this method!
-                       # also set text to null?
-               }
-
-               $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
-               $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
-               $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
-               $this->mUserText = isset( $row['user_text'] )
-                       ? strval( $row['user_text'] ) : $wgUser->getName();
-               $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
-               $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
-               $this->mTimestamp = isset( $row['timestamp'] )
-                       ? strval( $row['timestamp'] ) : wfTimestampNow();
-               $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
-               $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
-               $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
-               $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
-
-               $this->mContentModel = isset( $row['content_model'] )
-                       ? strval( $row['content_model'] ) : null;
-               $this->mContentFormat = isset( $row['content_format'] )
-                       ? strval( $row['content_format'] ) : null;
-
-               // Enforce spacing trimming on supplied text
-               $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
-               $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
-               $this->mTextRow = null;
-
-               $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
-
-               // if we have a Content object, override mText and mContentModel
-               if ( !empty( $row['content'] ) ) {
-                       $handler = $this->getContentHandler();
-                       $this->mContent = $row['content'];
-
-                       $this->mContentModel = $this->mContent->getModel();
-                       $this->mContentHandler = null;
-
-                       $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
-               } elseif ( $this->mText !== null ) {
-                       $handler = $this->getContentHandler();
-                       $this->mContent = $handler->unserializeContent( $this->mText );
-               }
-
-               // If we have a Title object, make sure it is consistent with mPage.
-               if ( $this->mTitle && $this->mTitle->exists() ) {
-                       if ( $this->mPage === null ) {
-                               // if the page ID wasn't known, set it now
-                               $this->mPage = $this->mTitle->getArticleID();
-                       } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
-                               // Got different page IDs. This may be legit (e.g. during undeletion),
-                               // but it seems worth mentioning it in the log.
-                               wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
-                                       $this->mTitle->getArticleID() . " provided by the Title object." );
-                       }
-               }
-
-               $this->mCurrent = false;
-
-               // If we still have no length, see it we have the text to figure it out
-               if ( !$this->mSize && $this->mContent !== null ) {
-                       $this->mSize = $this->mContent->getSize();
-               }
-
-               // Same for sha1
-               if ( $this->mSha1 === null ) {
-                       $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText );
-               }
-
-               // force lazy init
-               $this->getContentModel();
-               $this->getContentFormat();
+       public function getRevisionRecord() {
+               return $this->mRecord;
        }
 
        /**
@@ -880,19 +517,27 @@ class Revision implements IDBAccessObject {
         * @return int|null
         */
        public function getId() {
-               return $this->mId;
+               return $this->mRecord->getId();
        }
 
        /**
         * Set the revision ID
         *
-        * This should only be used for proposed revisions that turn out to be null edits
+        * This should only be used for proposed revisions that turn out to be null edits.
+        *
+        * @note Only supported on Revisions that were constructed based on associative arrays,
+        *       since they are mutable.
         *
         * @since 1.19
-        * @param int $id
+        * @param int|string $id
+        * @throws MWException
         */
        public function setId( $id ) {
-               $this->mId = (int)$id;
+               if ( $this->mRecord instanceof MutableRevisionRecord ) {
+                       $this->mRecord->setId( intval( $id ) );
+               } else {
+                       throw new MWException( __METHOD__ . ' is not supported on this instance' );
+               }
        }
 
        /**
@@ -900,106 +545,107 @@ class Revision implements IDBAccessObject {
         *
         * This should only be used for proposed revisions that turn out to be null edits
         *
+        * @note Only supported on Revisions that were constructed based on associative arrays,
+        *       since they are mutable.
+        *
         * @since 1.28
         * @deprecated since 1.31, please reuse old Revision object
         * @param int $id User ID
         * @param string $name User name
+        * @throws MWException
         */
        public function setUserIdAndName( $id, $name ) {
-               $this->mUser = (int)$id;
-               $this->mUserText = $name;
-               $this->mOrigUserText = $name;
+               if ( $this->mRecord instanceof MutableRevisionRecord ) {
+                       $user = new UserIdentityValue( intval( $id ), $name );
+                       $this->mRecord->setUser( $user );
+               } else {
+                       throw new MWException( __METHOD__ . ' is not supported on this instance' );
+               }
+       }
+
+       /**
+        * @return SlotRecord
+        */
+       private function getMainSlotRaw() {
+               return $this->mRecord->getSlot( 'main', RevisionRecord::RAW );
        }
 
        /**
-        * Get text row ID
+        * Get the ID of the row of the text table that contains the content of the
+        * revision's main slot, if that content is stored in the text table.
+        *
+        * If the content is stored elsewhere, this returns null.
+        *
+        * @deprecated since 1.31, use RevisionRecord()->getSlot()->getContentAddress() to
+        * get that actual address that can be used with BlobStore::getBlob(); or use
+        * RevisionRecord::hasSameContent() to check if two revisions have the same content.
         *
         * @return int|null
         */
        public function getTextId() {
-               return $this->mTextId;
+               $slot = $this->getMainSlotRaw();
+               return $slot->hasAddress()
+                       ? self::getBlobStore()->getTextIdFromAddress( $slot->getAddress() )
+                       : null;
        }
 
        /**
         * Get parent revision ID (the original previous page revision)
         *
-        * @return int|null
+        * @return int|null The ID of the parent revision. 0 indicates that there is no
+        * parent revision. Null indicates that the parent revision is not known.
         */
        public function getParentId() {
-               return $this->mParentId;
+               return $this->mRecord->getParentId();
        }
 
        /**
         * Returns the length of the text in this revision, or null if unknown.
         *
-        * @return int|null
+        * @return int
         */
        public function getSize() {
-               return $this->mSize;
+               return $this->mRecord->getSize();
        }
 
        /**
-        * Returns the base36 sha1 of the text in this revision, or null if unknown.
+        * Returns the base36 sha1 of the content in this revision, or null if unknown.
         *
-        * @return string|null
+        * @return string
         */
        public function getSha1() {
-               return $this->mSha1;
+               // XXX: we may want to drop all the hashing logic, it's not worth the overhead.
+               return $this->mRecord->getSha1();
        }
 
        /**
-        * Returns the title of the page associated with this entry or null.
+        * Returns the title of the page associated with this entry.
+        * Since 1.31, this will never return null.
         *
         * Will do a query, when title is not set and id is given.
         *
-        * @return Title|null
+        * @return Title
         */
        public function getTitle() {
-               if ( $this->mTitle !== null ) {
-                       return $this->mTitle;
-               }
-               // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
-               if ( $this->mId !== null ) {
-                       $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
-                       // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
-                       $row = $dbr->selectRow(
-                               [ 'revision', 'page' ],
-                               [
-                                       'page_namespace',
-                                       'page_title',
-                                       'page_id',
-                                       'page_latest',
-                                       'page_is_redirect',
-                                       'page_len',
-                               ],
-                               [ 'rev_id' => $this->mId ],
-                               __METHOD__,
-                               [],
-                               [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
-                       );
-                       if ( $row ) {
-                               // @TODO: better foreign title handling
-                               $this->mTitle = Title::newFromRow( $row );
-                       }
-               }
-
-               if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) {
-                       // Loading by ID is best, though not possible for foreign titles
-                       if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
-                               $this->mTitle = Title::newFromID( $this->mPage );
-                       }
-               }
-
-               return $this->mTitle;
+               $linkTarget = $this->mRecord->getPageAsLinkTarget();
+               return Title::newFromLinkTarget( $linkTarget );
        }
 
        /**
         * Set the title of the revision
         *
+        * @deprecated: since 1.31, this is now a noop. Pass the Title to the constructor instead.
+        *
         * @param Title $title
         */
        public function setTitle( $title ) {
-               $this->mTitle = $title;
+               if ( !$title->equals( $this->getTitle() ) ) {
+                       throw new InvalidArgumentException(
+                               $title->getPrefixedText()
+                                       . ' is not the same as '
+                                       . $this->mRecord->getPageAsLinkTarget()->__toString()
+                       );
+               }
        }
 
        /**
@@ -1008,7 +654,7 @@ class Revision implements IDBAccessObject {
         * @return int|null
         */
        public function getPage() {
-               return $this->mPage;
+               return $this->mRecord->getPageId();
        }
 
        /**
@@ -1025,13 +671,14 @@ class Revision implements IDBAccessObject {
         * @return int
         */
        public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
-               if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
-                       return 0;
-               } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
-                       return 0;
-               } else {
-                       return $this->mUser;
+               global $wgUser;
+
+               if ( $audience === self::FOR_THIS_USER && !$user ) {
+                       $user = $wgUser;
                }
+
+               $user = $this->mRecord->getUser( $audience, $user );
+               return $user ? $user->getId() : 0;
        }
 
        /**
@@ -1059,23 +706,14 @@ class Revision implements IDBAccessObject {
         * @return string
         */
        public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
-               $this->loadMutableFields();
+               global $wgUser;
 
-               if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
-                       return '';
-               } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
-                       return '';
-               } else {
-                       if ( $this->mUserText === null ) {
-                               $this->mUserText = User::whoIs( $this->mUser ); // load on demand
-                               if ( $this->mUserText === false ) {
-                                       # This shouldn't happen, but it can if the wiki was recovered
-                                       # via importing revs and there is no user table entry yet.
-                                       $this->mUserText = $this->mOrigUserText;
-                               }
-                       }
-                       return $this->mUserText;
+               if ( $audience === self::FOR_THIS_USER && !$user ) {
+                       $user = $wgUser;
                }
+
+               $user = $this->mRecord->getUser( $audience, $user );
+               return $user ? $user->getName() : '';
        }
 
        /**
@@ -1103,13 +741,14 @@ class Revision implements IDBAccessObject {
         * @return string
         */
        function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
-               if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
-                       return '';
-               } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
-                       return '';
-               } else {
-                       return $this->mComment;
+               global $wgUser;
+
+               if ( $audience === self::FOR_THIS_USER && !$user ) {
+                       $user = $wgUser;
                }
+
+               $comment = $this->mRecord->getComment( $audience, $user );
+               return $comment === null ? null : $comment->text;
        }
 
        /**
@@ -1127,23 +766,14 @@ class Revision implements IDBAccessObject {
         * @return bool
         */
        public function isMinor() {
-               return (bool)$this->mMinorEdit;
+               return $this->mRecord->isMinor();
        }
 
        /**
         * @return int Rcid of the unpatrolled row, zero if there isn't one
         */
        public function isUnpatrolled() {
-               if ( $this->mUnpatrolled !== null ) {
-                       return $this->mUnpatrolled;
-               }
-               $rc = $this->getRecentChange();
-               if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
-                       $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
-               } else {
-                       $this->mUnpatrolled = 0;
-               }
-               return $this->mUnpatrolled;
+               return self::getRevisionStore()->isUnpatrolled( $this->mRecord );
        }
 
        /**
@@ -1156,19 +786,7 @@ class Revision implements IDBAccessObject {
         * @return RecentChange|null
         */
        public function getRecentChange( $flags = 0 ) {
-               $dbr = wfGetDB( DB_REPLICA );
-
-               list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
-
-               return RecentChange::newFromConds(
-                       [
-                               'rc_user_text' => $this->getUserText( self::RAW ),
-                               'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
-                               'rc_this_oldid' => $this->getId()
-                       ],
-                       __METHOD__,
-                       $dbType
-               );
+               return self::getRevisionStore()->getRecentChange( $this->mRecord, $flags );
        }
 
        /**
@@ -1177,14 +795,7 @@ class Revision implements IDBAccessObject {
         * @return bool
         */
        public function isDeleted( $field ) {
-               if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
-                       // Current revisions of pages cannot have the content hidden. Skipping this
-                       // check is very useful for Parser as it fetches templates using newKnownCurrent().
-                       // Calling getVisibility() in that case triggers a verification database query.
-                       return false; // no need to check
-               }
-
-               return ( $this->getVisibility() & $field ) == $field;
+               return $this->mRecord->isDeleted( $field );
        }
 
        /**
@@ -1193,19 +804,17 @@ class Revision implements IDBAccessObject {
         * @return int
         */
        public function getVisibility() {
-               $this->loadMutableFields();
-
-               return (int)$this->mDeleted;
+               return $this->mRecord->getVisibility();
        }
 
        /**
         * Fetch revision content if it's available to the specified audience.
         * If the specified audience does not have the ability to view this
-        * revision, null will be returned.
+        * revision, or the content could not be loaded, null will be returned.
         *
         * @param int $audience One of:
         *   Revision::FOR_PUBLIC       to be displayed to all users
-        *   Revision::FOR_THIS_USER    to be displayed to $wgUser
+        *   Revision::FOR_THIS_USER    to be displayed to $user
         *   Revision::RAW              get the text regardless of permissions
         * @param User $user User object to check for, only if FOR_THIS_USER is passed
         *   to the $audience parameter
@@ -1213,12 +822,17 @@ class Revision implements IDBAccessObject {
         * @return Content|null
         */
        public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
-               if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
-                       return null;
-               } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
+               global $wgUser;
+
+               if ( $audience === self::FOR_THIS_USER && !$user ) {
+                       $user = $wgUser;
+               }
+
+               try {
+                       return $this->mRecord->getContent( 'main', $audience, $user );
+               }
+               catch ( RevisionAccessException $e ) {
                        return null;
-               } else {
-                       return $this->getContentInternal();
                }
        }
 
@@ -1226,86 +840,51 @@ class Revision implements IDBAccessObject {
         * Get original serialized data (without checking view restrictions)
         *
         * @since 1.21
+        * @deprecated since 1.31, use BlobStore::getBlob instead.
+        *
         * @return string
         */
        public function getSerializedData() {
-               if ( $this->mText === null ) {
-                       // Revision is immutable. Load on demand.
-                       $this->mText = $this->loadText();
-               }
-
-               return $this->mText;
+               $slot = $this->getMainSlotRaw();
+               return $slot->getContent()->serialize();
        }
 
        /**
-        * Gets the content object for the revision (or null on failure).
-        *
-        * Note that for mutable Content objects, each call to this method will return a
-        * fresh clone.
-        *
-        * @since 1.21
-        * @return Content|null The Revision's content, or null on failure.
-        */
-       protected function getContentInternal() {
-               if ( $this->mContent === null ) {
-                       $text = $this->getSerializedData();
-
-                       if ( $text !== null && $text !== false ) {
-                               // Unserialize content
-                               $handler = $this->getContentHandler();
-                               $format = $this->getContentFormat();
-
-                               $this->mContent = $handler->unserializeContent( $text, $format );
-                       }
-               }
-
-               // NOTE: copy() will return $this for immutable content objects
-               return $this->mContent ? $this->mContent->copy() : null;
-       }
-
-       /**
-        * Returns the content model for this revision.
+        * Returns the content model for the main slot of this revision.
         *
         * If no content model was stored in the database, the default content model for the title is
         * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
         * is used as a last resort.
         *
+        * @todo: drop this, with MCR, there no longer is a single model associated with a revision.
+        *
         * @return string The content model id associated with this revision,
         *     see the CONTENT_MODEL_XXX constants.
         */
        public function getContentModel() {
-               if ( !$this->mContentModel ) {
-                       $title = $this->getTitle();
-                       if ( $title ) {
-                               $this->mContentModel = ContentHandler::getDefaultModelFor( $title );
-                       } else {
-                               $this->mContentModel = CONTENT_MODEL_WIKITEXT;
-                       }
-
-                       assert( !empty( $this->mContentModel ) );
-               }
-
-               return $this->mContentModel;
+               return $this->getMainSlotRaw()->getModel();
        }
 
        /**
-        * Returns the content format for this revision.
+        * Returns the content format for the main slot of this revision.
         *
         * If no content format was stored in the database, the default format for this
         * revision's content model is returned.
         *
+        * @todo: drop this, the format is irrelevant to the revision!
+        *
         * @return string The content format id associated with this revision,
         *     see the CONTENT_FORMAT_XXX constants.
         */
        public function getContentFormat() {
-               if ( !$this->mContentFormat ) {
-                       $handler = $this->getContentHandler();
-                       $this->mContentFormat = $handler->getDefaultFormat();
+               $format = $this->getMainSlotRaw()->getFormat();
 
-                       assert( !empty( $this->mContentFormat ) );
+               if ( $format === null ) {
+                       // if no format was stored along with the blob, fall back to default format
+                       $format = $this->getContentHandler()->getDefaultFormat();
                }
 
-               return $this->mContentFormat;
+               return $format;
        }
 
        /**
@@ -1315,33 +894,21 @@ class Revision implements IDBAccessObject {
         * @return ContentHandler
         */
        public function getContentHandler() {
-               if ( !$this->mContentHandler ) {
-                       $model = $this->getContentModel();
-                       $this->mContentHandler = ContentHandler::getForModelID( $model );
-
-                       $format = $this->getContentFormat();
-
-                       if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
-                               throw new MWException( "Oops, the content format $format is not supported for "
-                                       . "this content model, $model" );
-                       }
-               }
-
-               return $this->mContentHandler;
+               return ContentHandler::getForModelID( $this->getContentModel() );
        }
 
        /**
         * @return string
         */
        public function getTimestamp() {
-               return wfTimestamp( TS_MW, $this->mTimestamp );
+               return $this->mRecord->getTimestamp();
        }
 
        /**
         * @return bool
         */
        public function isCurrent() {
-               return $this->mCurrent;
+               return ( $this->mRecord instanceof RevisionStoreRecord ) && $this->mRecord->isCurrent();
        }
 
        /**
@@ -1350,13 +917,8 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public function getPrevious() {
-               if ( $this->getTitle() ) {
-                       $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
-                       if ( $prev ) {
-                               return self::newFromTitle( $this->getTitle(), $prev );
-                       }
-               }
-               return null;
+               $rec = self::getRevisionStore()->getPreviousRevision( $this->mRecord );
+               return $rec === null ? null : new Revision( $rec );
        }
 
        /**
@@ -1365,38 +927,8 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public function getNext() {
-               if ( $this->getTitle() ) {
-                       $next = $this->getTitle()->getNextRevisionID( $this->getId() );
-                       if ( $next ) {
-                               return self::newFromTitle( $this->getTitle(), $next );
-                       }
-               }
-               return null;
-       }
-
-       /**
-        * Get previous revision Id for this page_id
-        * This is used to populate rev_parent_id on save
-        *
-        * @param IDatabase $db
-        * @return int
-        */
-       private function getPreviousRevisionId( $db ) {
-               if ( $this->mPage === null ) {
-                       return 0;
-               }
-               # Use page_latest if ID is not given
-               if ( !$this->mId ) {
-                       $prevId = $db->selectField( 'page', 'page_latest',
-                               [ 'page_id' => $this->mPage ],
-                               __METHOD__ );
-               } else {
-                       $prevId = $db->selectField( 'revision', 'rev_id',
-                               [ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ],
-                               __METHOD__,
-                               [ 'ORDER BY' => 'rev_id DESC' ] );
-               }
-               return intval( $prevId );
+               $rec = self::getRevisionStore()->getNextRevision( $this->mRecord );
+               return $rec === null ? null : new Revision( $rec );
        }
 
        /**
@@ -1429,35 +961,9 @@ class Revision implements IDBAccessObject {
                        return false;
                }
 
-               // Use external methods for external objects, text in table is URL-only then
-               if ( in_array( 'external', $flags ) ) {
-                       $url = $text;
-                       $parts = explode( '://', $url, 2 );
-                       if ( count( $parts ) == 1 || $parts[1] == '' ) {
-                               return false;
-                       }
-
-                       if ( isset( $row->old_id ) && $wiki === false ) {
-                               // Make use of the wiki-local revision text cache
-                               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
-                               // The cached value should be decompressed, so handle that and return here
-                               return $cache->getWithSetCallback(
-                                       $cache->makeKey( 'revisiontext', 'textid', $row->old_id ),
-                                       self::getCacheTTL( $cache ),
-                                       function () use ( $url, $wiki, $flags ) {
-                                               // No negative caching per Revision::loadText()
-                                               $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
-
-                                               return self::decompressRevisionText( $text, $flags );
-                                       },
-                                       [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
-                               );
-                       } else {
-                               $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
-                       }
-               }
+               $cacheKey = isset( $row->old_id ) ? ( 'tt:' . $row->old_id ) : null;
 
-               return self::decompressRevisionText( $text, $flags );
+               return self::getBlobStore( $wiki )->expandBlob( $text, $flags, $cacheKey );
        }
 
        /**
@@ -1471,28 +977,7 @@ class Revision implements IDBAccessObject {
         * @return string
         */
        public static function compressRevisionText( &$text ) {
-               global $wgCompressRevisions;
-               $flags = [];
-
-               # Revisions not marked this way will be converted
-               # on load if $wgLegacyCharset is set in the future.
-               $flags[] = 'utf-8';
-
-               if ( $wgCompressRevisions ) {
-                       if ( function_exists( 'gzdeflate' ) ) {
-                               $deflated = gzdeflate( $text );
-
-                               if ( $deflated === false ) {
-                                       wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
-                               } else {
-                                       $text = $deflated;
-                                       $flags[] = 'gzip';
-                               }
-                       } else {
-                               wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
-                       }
-               }
-               return implode( ',', $flags );
+               return self::getBlobStore()->compressData( $text );
        }
 
        /**
@@ -1503,46 +988,7 @@ class Revision implements IDBAccessObject {
         * @return string|bool Decompressed text, or false on failure
         */
        public static function decompressRevisionText( $text, $flags ) {
-               global $wgLegacyEncoding, $wgContLang;
-
-               if ( $text === false ) {
-                       // Text failed to be fetched; nothing to do
-                       return false;
-               }
-
-               if ( in_array( 'gzip', $flags ) ) {
-                       # Deal with optional compression of archived pages.
-                       # This can be done periodically via maintenance/compressOld.php, and
-                       # as pages are saved if $wgCompressRevisions is set.
-                       $text = gzinflate( $text );
-
-                       if ( $text === false ) {
-                               wfLogWarning( __METHOD__ . ': gzinflate() failed' );
-                               return false;
-                       }
-               }
-
-               if ( in_array( 'object', $flags ) ) {
-                       # Generic compressed storage
-                       $obj = unserialize( $text );
-                       if ( !is_object( $obj ) ) {
-                               // Invalid object
-                               return false;
-                       }
-                       $text = $obj->getText();
-               }
-
-               if ( $text !== false && $wgLegacyEncoding
-                       && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
-               ) {
-                       # Old revisions kept around in a legacy encoding?
-                       # Upconvert on demand.
-                       # ("utf8" checked for compatibility with some broken
-                       #  conversion scripts 2008-12-30)
-                       $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
-               }
-
-               return $text;
+               return self::getBlobStore()->decompressData( $text, $flags );
        }
 
        /**
@@ -1554,192 +1000,29 @@ class Revision implements IDBAccessObject {
         * @return int The revision ID
         */
        public function insertOn( $dbw ) {
-               global $wgDefaultExternalStore, $wgContentHandlerUseDB;
-
-               // We're inserting a new revision, so we have to use master anyway.
-               // If it's a null revision, it may have references to rows that
-               // are not in the replica yet (the text row).
-               $this->mQueryFlags |= self::READ_LATEST;
-
-               // Not allowed to have rev_page equal to 0, false, etc.
-               if ( !$this->mPage ) {
-                       $title = $this->getTitle();
-                       if ( $title instanceof Title ) {
-                               $titleText = ' for page ' . $title->getPrefixedText();
-                       } else {
-                               $titleText = '';
-                       }
-                       throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
-               }
-
-               $this->checkContentModel();
-
-               $data = $this->mText;
-               $flags = self::compressRevisionText( $data );
-
-               # Write to external storage if required
-               if ( $wgDefaultExternalStore ) {
-                       // Store and get the URL
-                       $data = ExternalStore::insertToDefault( $data );
-                       if ( !$data ) {
-                               throw new MWException( "Unable to store text to external storage" );
-                       }
-                       if ( $flags ) {
-                               $flags .= ',';
-                       }
-                       $flags .= 'external';
-               }
-
-               # Record the text (or external storage URL) to the text table
-               if ( $this->mTextId === null ) {
-                       $dbw->insert( 'text',
-                               [
-                                       'old_text' => $data,
-                                       'old_flags' => $flags,
-                               ], __METHOD__
-                       );
-                       $this->mTextId = $dbw->insertId();
-               }
-
-               if ( $this->mComment === null ) {
-                       $this->mComment = "";
-               }
-
-               # Record the edit in revisions
-               $row = [
-                       'rev_page'       => $this->mPage,
-                       'rev_text_id'    => $this->mTextId,
-                       'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
-                       'rev_user'       => $this->mUser,
-                       'rev_user_text'  => $this->mUserText,
-                       'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
-                       'rev_deleted'    => $this->mDeleted,
-                       'rev_len'        => $this->mSize,
-                       'rev_parent_id'  => $this->mParentId === null
-                               ? $this->getPreviousRevisionId( $dbw )
-                               : $this->mParentId,
-                       'rev_sha1'       => $this->mSha1 === null
-                               ? self::base36Sha1( $this->mText )
-                               : $this->mSha1,
-               ];
-               if ( $this->mId !== null ) {
-                       $row['rev_id'] = $this->mId;
-               }
-
-               list( $commentFields, $commentCallback ) =
-                       CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $this->mComment );
-               $row += $commentFields;
-
-               if ( $wgContentHandlerUseDB ) {
-                       // NOTE: Store null for the default model and format, to save space.
-                       // XXX: Makes the DB sensitive to changed defaults.
-                       // Make this behavior optional? Only in miser mode?
-
-                       $model = $this->getContentModel();
-                       $format = $this->getContentFormat();
+               global $wgUser;
 
-                       $title = $this->getTitle();
+               // Note that $this->mRecord->getId() will typically return null here, but not always,
+               // e.g. not when restoring a revision.
 
-                       if ( $title === null ) {
-                               throw new MWException( "Insufficient information to determine the title of the "
-                                       . "revision's page!" );
+               if ( $this->mRecord->getUser( RevisionRecord::RAW ) === null ) {
+                       if ( $this->mRecord instanceof MutableRevisionRecord ) {
+                               $this->mRecord->setUser( $wgUser );
+                       } else {
+                               throw new MWException( 'Cannot insert revision with no associated user.' );
                        }
-
-                       $defaultModel = ContentHandler::getDefaultModelFor( $title );
-                       $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
-
-                       $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
-                       $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
-               }
-
-               $dbw->insert( 'revision', $row, __METHOD__ );
-
-               if ( $this->mId === null ) {
-                       // Only if auto-increment was used
-                       $this->mId = $dbw->insertId();
                }
-               $commentCallback( $this->mId );
 
-               // Assertion to try to catch T92046
-               if ( (int)$this->mId === 0 ) {
-                       throw new UnexpectedValueException(
-                               'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' .
-                                       var_export( $row, 1 )
-                       );
-               }
+               $rec = self::getRevisionStore()->insertRevisionOn( $this->mRecord, $dbw );
 
-               // Insert IP revision into ip_changes for use when querying for a range.
-               if ( $this->mUser === 0 && IP::isValid( $this->mUserText ) ) {
-                       $ipcRow = [
-                               'ipc_rev_id'        => $this->mId,
-                               'ipc_rev_timestamp' => $row['rev_timestamp'],
-                               'ipc_hex'           => IP::toHex( $row['rev_user_text'] ),
-                       ];
-                       $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
-               }
+               $this->mRecord = $rec;
 
                // Avoid PHP 7.1 warning of passing $this by reference
                $revision = $this;
-               Hooks::run( 'RevisionInsertComplete', [ &$revision, $data, $flags ] );
+               // TODO: hard-deprecate in 1.32 (or even 1.31?)
+               Hooks::run( 'RevisionInsertComplete', [ &$revision, null, null ] );
 
-               return $this->mId;
-       }
-
-       protected function checkContentModel() {
-               global $wgContentHandlerUseDB;
-
-               // Note: may return null for revisions that have not yet been inserted
-               $title = $this->getTitle();
-
-               $model = $this->getContentModel();
-               $format = $this->getContentFormat();
-               $handler = $this->getContentHandler();
-
-               if ( !$handler->isSupportedFormat( $format ) ) {
-                       $t = $title->getPrefixedDBkey();
-
-                       throw new MWException( "Can't use format $format with content model $model on $t" );
-               }
-
-               if ( !$wgContentHandlerUseDB && $title ) {
-                       // if $wgContentHandlerUseDB is not set,
-                       // all revisions must use the default content model and format.
-
-                       $defaultModel = ContentHandler::getDefaultModelFor( $title );
-                       $defaultHandler = ContentHandler::getForModelID( $defaultModel );
-                       $defaultFormat = $defaultHandler->getDefaultFormat();
-
-                       if ( $this->getContentModel() != $defaultModel ) {
-                               $t = $title->getPrefixedDBkey();
-
-                               throw new MWException( "Can't save non-default content model with "
-                                       . "\$wgContentHandlerUseDB disabled: model is $model, "
-                                       . "default for $t is $defaultModel" );
-                       }
-
-                       if ( $this->getContentFormat() != $defaultFormat ) {
-                               $t = $title->getPrefixedDBkey();
-
-                               throw new MWException( "Can't use non-default content format with "
-                                       . "\$wgContentHandlerUseDB disabled: format is $format, "
-                                       . "default for $t is $defaultFormat" );
-                       }
-               }
-
-               $content = $this->getContent( self::RAW );
-               $prefixedDBkey = $title->getPrefixedDBkey();
-               $revId = $this->mId;
-
-               if ( !$content ) {
-                       throw new MWException(
-                               "Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
-                       );
-               }
-               if ( !$content->isValid() ) {
-                       throw new MWException(
-                               "Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
-                       );
-               }
+               return $rec->getId();
        }
 
        /**
@@ -1748,103 +1031,7 @@ class Revision implements IDBAccessObject {
         * @return string
         */
        public static function base36Sha1( $text ) {
-               return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 );
-       }
-
-       /**
-        * Get the text cache TTL
-        *
-        * @param WANObjectCache $cache
-        * @return int
-        */
-       private static function getCacheTTL( WANObjectCache $cache ) {
-               global $wgRevisionCacheExpiry;
-
-               if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) {
-                       // Do not cache RDBMs blobs in...the RDBMs store
-                       $ttl = $cache::TTL_UNCACHEABLE;
-               } else {
-                       $ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE;
-               }
-
-               return $ttl;
-       }
-
-       /**
-        * Lazy-load the revision's text.
-        * Currently hardcoded to the 'text' table storage engine.
-        *
-        * @return string|bool The revision's text, or false on failure
-        */
-       private function loadText() {
-               $cache = ObjectCache::getMainWANInstance();
-
-               // No negative caching; negative hits on text rows may be due to corrupted replica DBs
-               return $cache->getWithSetCallback(
-                       $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ),
-                       self::getCacheTTL( $cache ),
-                       function () {
-                               return $this->fetchText();
-                       },
-                       [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
-               );
-       }
-
-       private function fetchText() {
-               $textId = $this->getTextId();
-
-               // If we kept data for lazy extraction, use it now...
-               if ( $this->mTextRow !== null ) {
-                       $row = $this->mTextRow;
-                       $this->mTextRow = null;
-               } else {
-                       $row = null;
-               }
-
-               // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
-               // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
-               $flags = $this->mQueryFlags;
-               $flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST )
-                       ? self::READ_LATEST_IMMUTABLE
-                       : 0;
-
-               list( $index, $options, $fallbackIndex, $fallbackOptions ) =
-                       DBAccessObjectUtils::getDBOptions( $flags );
-
-               if ( !$row ) {
-                       // Text data is immutable; check replica DBs first.
-                       $row = wfGetDB( $index )->selectRow(
-                               'text',
-                               [ 'old_text', 'old_flags' ],
-                               [ 'old_id' => $textId ],
-                               __METHOD__,
-                               $options
-                       );
-               }
-
-               // Fallback to DB_MASTER in some cases if the row was not found
-               if ( !$row && $fallbackIndex !== null ) {
-                       // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
-                       // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
-                       $row = wfGetDB( $fallbackIndex )->selectRow(
-                               'text',
-                               [ 'old_text', 'old_flags' ],
-                               [ 'old_id' => $textId ],
-                               __METHOD__,
-                               $fallbackOptions
-                       );
-               }
-
-               if ( !$row ) {
-                       wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
-               }
-
-               $text = self::getRevisionText( $row );
-               if ( $row && $text === false ) {
-                       wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
-               }
-
-               return is_string( $text ) ? $text : false;
+               return SlotRecord::base36Sha1( $text );
        }
 
        /**
@@ -1863,58 +1050,17 @@ class Revision implements IDBAccessObject {
         * @return Revision|null Revision or null on error
         */
        public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
-               global $wgContentHandlerUseDB;
-
-               $fields = [ 'page_latest', 'page_namespace', 'page_title',
-                                               'rev_text_id', 'rev_len', 'rev_sha1' ];
-
-               if ( $wgContentHandlerUseDB ) {
-                       $fields[] = 'rev_content_model';
-                       $fields[] = 'rev_content_format';
+               global $wgUser;
+               if ( !$user ) {
+                       $user = $wgUser;
                }
 
-               $current = $dbw->selectRow(
-                       [ 'page', 'revision' ],
-                       $fields,
-                       [
-                               'page_id' => $pageId,
-                               'page_latest=rev_id',
-                       ],
-                       __METHOD__,
-                       [ 'FOR UPDATE' ] // T51581
-               );
-
-               if ( $current ) {
-                       if ( !$user ) {
-                               global $wgUser;
-                               $user = $wgUser;
-                       }
-
-                       $row = [
-                               'page'       => $pageId,
-                               'user_text'  => $user->getName(),
-                               'user'       => $user->getId(),
-                               'comment'    => $summary,
-                               'minor_edit' => $minor,
-                               'text_id'    => $current->rev_text_id,
-                               'parent_id'  => $current->page_latest,
-                               'len'        => $current->rev_len,
-                               'sha1'       => $current->rev_sha1
-                       ];
-
-                       if ( $wgContentHandlerUseDB ) {
-                               $row['content_model'] = $current->rev_content_model;
-                               $row['content_format'] = $current->rev_content_format;
-                       }
-
-                       $row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
+               $comment = CommentStoreComment::newUnsavedComment( $summary, null );
 
-                       $revision = new Revision( $row );
-               } else {
-                       $revision = null;
-               }
+               $title = Title::newFromID( $pageId );
+               $rec = self::getRevisionStore()->newNullRevision( $dbw, $title, $comment, $minor, $user );
 
-               return $revision;
+               return new Revision( $rec );
        }
 
        /**
@@ -1948,35 +1094,13 @@ class Revision implements IDBAccessObject {
        public static function userCanBitfield( $bitfield, $field, User $user = null,
                Title $title = null
        ) {
-               if ( $bitfield & $field ) { // aspect is deleted
-                       if ( $user === null ) {
-                               global $wgUser;
-                               $user = $wgUser;
-                       }
-                       if ( $bitfield & self::DELETED_RESTRICTED ) {
-                               $permissions = [ 'suppressrevision', 'viewsuppressed' ];
-                       } elseif ( $field & self::DELETED_TEXT ) {
-                               $permissions = [ 'deletedtext' ];
-                       } else {
-                               $permissions = [ 'deletedhistory' ];
-                       }
-                       $permissionlist = implode( ', ', $permissions );
-                       if ( $title === null ) {
-                               wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
-                               return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
-                       } else {
-                               $text = $title->getPrefixedText();
-                               wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
-                               foreach ( $permissions as $perm ) {
-                                       if ( $title->userCan( $perm, $user ) ) {
-                                               return true;
-                                       }
-                               }
-                               return false;
-                       }
-               } else {
-                       return true;
+               global $wgUser;
+
+               if ( !$user ) {
+                       $user = $wgUser;
                }
+
+               return RevisionRecord::userCanBitfield( $bitfield, $field, $user, $title );
        }
 
        /**
@@ -1988,18 +1112,7 @@ class Revision implements IDBAccessObject {
         * @return string|bool False if not found
         */
        static function getTimestampFromId( $title, $id, $flags = 0 ) {
-               $db = ( $flags & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_REPLICA );
-               // Casting fix for databases that can't take '' for rev_id
-               if ( $id == '' ) {
-                       $id = 0;
-               }
-               $conds = [ 'rev_id' => $id ];
-               $conds['rev_page'] = $title->getArticleID();
-               $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
-
-               return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
+               return self::getRevisionStore()->getTimestampFromId( $title, $id, $flags );
        }
 
        /**
@@ -2010,12 +1123,7 @@ class Revision implements IDBAccessObject {
         * @return int
         */
        static function countByPageId( $db, $id ) {
-               $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ],
-                       [ 'rev_page' => $id ], __METHOD__ );
-               if ( $row ) {
-                       return $row->revCount;
-               }
-               return 0;
+               return self::getRevisionStore()->countRevisionsByPageId( $db, $id );
        }
 
        /**
@@ -2026,11 +1134,7 @@ class Revision implements IDBAccessObject {
         * @return int
         */
        static function countByTitle( $db, $title ) {
-               $id = $title->getArticleID();
-               if ( $id ) {
-                       return self::countByPageId( $db, $id );
-               }
-               return 0;
+               return self::getRevisionStore()->countRevisionsByTitle( $db, $title );
        }
 
        /**
@@ -2050,28 +1154,11 @@ class Revision implements IDBAccessObject {
         * @return bool True if the given user was the only one to edit since the given timestamp
         */
        public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
-               if ( !$userId ) {
-                       return false;
-               }
-
                if ( is_int( $db ) ) {
                        $db = wfGetDB( $db );
                }
 
-               $res = $db->select( 'revision',
-                       'rev_user',
-                       [
-                               'rev_page' => $pageId,
-                               'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
-                       ],
-                       __METHOD__,
-                       [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] );
-               foreach ( $res as $row ) {
-                       if ( $row->rev_user != $userId ) {
-                               return false;
-                       }
-               }
-               return true;
+               return self::getRevisionStore()->userWasLastToEdit( $db, $pageId, $userId, $since );
        }
 
        /**
@@ -2079,54 +1166,24 @@ class Revision implements IDBAccessObject {
         *
         * This method allows for the use of caching, though accessing anything that normally
         * requires permission checks (aside from the text) will trigger a small DB lookup.
-        * The title will also be lazy loaded, though setTitle() can be used to preload it.
+        * The title will also be loaded if $pageIdOrTitle is an integer ID.
         *
-        * @param IDatabase $db
-        * @param int $pageId Page ID
-        * @param int $revId Known current revision of this page
+        * @param IDatabase $db ignored!
+        * @param int|Title $pageIdOrTitle Page ID or Title object
+        * @param int $revId Known current revision of this page. Determined automatically if not given.
         * @return Revision|bool Returns false if missing
         * @since 1.28
         */
-       public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) {
-               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
-               return $cache->getWithSetCallback(
-                       // Page/rev IDs passed in from DB to reflect history merges
-                       $cache->makeGlobalKey( 'revision', $db->getDomainID(), $pageId, $revId ),
-                       $cache::TTL_WEEK,
-                       function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
-                               $setOpts += Database::getCacheSetOptions( $db );
-
-                               $rev = Revision::loadFromPageId( $db, $pageId, $revId );
-                               // Reflect revision deletion and user renames
-                               if ( $rev ) {
-                                       $rev->mTitle = null; // mutable; lazy-load
-                                       $rev->mRefreshMutableFields = true;
-                               }
-
-                               return $rev ?: false; // don't cache negatives
-                       }
-               );
-       }
+       public static function newKnownCurrent( IDatabase $db, $pageIdOrTitle, $revId = 0 ) {
+               $title = $pageIdOrTitle instanceof Title
+                       ? $pageIdOrTitle
+                       : Title::newFromID( $pageIdOrTitle );
 
-       /**
-        * For cached revisions, make sure the user name and rev_deleted is up-to-date
-        */
-       private function loadMutableFields() {
-               if ( !$this->mRefreshMutableFields ) {
-                       return; // not needed
+               if ( !$title ) {
+                       return false;
                }
 
-               $this->mRefreshMutableFields = false;
-               $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
-               $row = $dbr->selectRow(
-                       [ 'revision', 'user' ],
-                       [ 'rev_deleted', 'user_name' ],
-                       [ 'rev_id' => $this->mId, 'user_id = rev_user' ],
-                       __METHOD__
-               );
-               if ( $row ) { // update values
-                       $this->mDeleted = (int)$row->rev_deleted;
-                       $this->mUserText = $row->user_name;
-               }
+               $record = self::getRevisionStore()->getKnownCurrentRevision( $title, $revId );
+               return $record ? new Revision( $record ) : false;
        }
 }
index dad0630..0d266fb 100644 (file)
@@ -42,6 +42,9 @@ use MediaWiki\Linker\LinkRendererFactory;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Shell\CommandFactory;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\SqlBlobStore;
 
 return [
        'DBLoadBalancerFactory' => function ( MediaWikiServices $services ) {
@@ -456,6 +459,40 @@ return [
                );
        },
 
+       'RevisionStore' => function ( MediaWikiServices $services ) {
+               /** @var SqlBlobStore $blobStore */
+               $blobStore = $services->getService( '_SqlBlobStore' );
+
+               $store = new RevisionStore(
+                       $services->getDBLoadBalancer(),
+                       $blobStore,
+                       $services->getMainWANObjectCache()
+               );
+
+               $config = $services->getMainConfig();
+               $store->setContentHandlerUseDB( $config->get( 'ContentHandlerUseDB' ) );
+
+               return $store;
+       },
+
+       'BlobStoreFactory' => function ( MediaWikiServices $services ) {
+               global $wgContLang;
+               return new BlobStoreFactory(
+                       $services->getDBLoadBalancer(),
+                       $services->getMainWANObjectCache(),
+                       $services->getMainConfig(),
+                       $wgContLang
+               );
+       },
+
+       'BlobStore' => function ( MediaWikiServices $services ) {
+               return $services->getService( '_SqlBlobStore' );
+       },
+
+       '_SqlBlobStore' => function ( MediaWikiServices $services ) {
+               return $services->getBlobStoreFactory()->newSqlBlobStore();
+       },
+
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service here, don't forget to add a getter function
        // in the MediaWikiServices class. The convenience getter should just call
diff --git a/includes/Storage/BlobAccessException.php b/includes/Storage/BlobAccessException.php
new file mode 100644 (file)
index 0000000..ffc5eca
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use RuntimeException;
+
+/**
+ * Exception representing a failure to access a data blob.
+ *
+ * @since 1.31
+ */
+class BlobAccessException extends RuntimeException {
+
+}
diff --git a/includes/Storage/BlobStore.php b/includes/Storage/BlobStore.php
new file mode 100644 (file)
index 0000000..28caf3a
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+/**
+ * Service for loading and storing data blobs.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+/**
+ * Service for loading and storing data blobs.
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ *       static methods in Revision.
+ *
+ * @since 1.31
+ */
+interface BlobStore {
+
+       /**
+        * Hint key for use with storeBlob, indicating the general role the block
+        * takes in the application. For instance, it should be "page-content" if
+        * the blob represents a Content object.
+        */
+       const DESIGNATION_HINT = 'designation';
+
+       /**
+        * Hint key for use with storeBlob, indicating the page the blob is associated with.
+        * This may be used for sharding.
+        */
+       const PAGE_HINT = 'page_id';
+
+       /**
+        * Hint key for use with storeBlob, indicating the slot the blob is associated with.
+        * May be relevant for reference counting.
+        */
+       const ROLE_HINT = 'role_name';
+
+       /**
+        * Hint key for use with storeBlob, indicating the revision the blob is associated with.
+        * This may be used for differential storage and reference counting.
+        */
+       const REVISION_HINT = 'rev_id';
+
+       /**
+        * Hint key for use with storeBlob, indicating the parent revision of the revision
+        * the blob is associated with. This may be used for differential storage.
+        */
+       const PARENT_HINT = 'rev_parent_id';
+
+       /**
+        * Hint key for use with storeBlob, providing the SHA1 hash of the blob as passed to the
+        * method. This can be used to avoid re-calculating the hash if it is needed by the BlobStore.
+        */
+       const SHA1_HINT = 'cont_sha1';
+
+       /**
+        * Hint key for use with storeBlob, indicating the model of the content encoded in the
+        * given blob. May be used to implement optimized storage for some well known models.
+        */
+       const MODEL_HINT = 'cont_model';
+
+       /**
+        * Hint key for use with storeBlob, indicating the serialization format used to create
+        * the blob, as a MIME type. May be used for optimized storage in the underlying database.
+        */
+       const FORMAT_HINT = 'cont_format';
+
+       /**
+        * Retrieve a blob, given an address.
+        *
+        * MCR migration note: this replaces Revision::loadText
+        *
+        * @param string $blobAddress The blob address as returned by storeBlob(),
+        *        such as "tt:12345" or "ex:DB://s16/456/9876".
+        * @param int $queryFlags See IDBAccessObject.
+        *
+        * @throws BlobAccessException
+        * @return string binary blob data
+        */
+       public function getBlob( $blobAddress, $queryFlags = 0 );
+
+       /**
+        * Stores an arbitrary blob of data and returns an address that can be used with
+        * getBlob() to retrieve the same blob of data,
+        *
+        * @param string $data raw binary data
+        * @param array $hints An array of hints. Implementations may use the hints to optimize storage.
+        * All hints are optional, supported hints depend on the implementation. Hint names by
+        * convention correspond to the names of fields in the database. Callers are encouraged to
+        * provide the well known hints as defined by the XXX_HINT constants.
+        *
+        * @throws BlobAccessException
+        * @return string an address that can be used with getBlob() to retrieve the data.
+        */
+       public function storeBlob( $data, $hints = [] );
+
+}
diff --git a/includes/Storage/BlobStoreFactory.php b/includes/Storage/BlobStoreFactory.php
new file mode 100644 (file)
index 0000000..63ca74d
--- /dev/null
@@ -0,0 +1,105 @@
+<?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
+ */
+
+namespace MediaWiki\Storage;
+
+use Config;
+use Language;
+use WANObjectCache;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * Service for instantiating BlobStores
+ *
+ * This can be used to create BlobStore objects for other wikis.
+ *
+ * @since 1.31
+ */
+class BlobStoreFactory {
+
+       /**
+        * @var LoadBalancer
+        */
+       private $loadBalancer;
+
+       /**
+        * @var WANObjectCache
+        */
+       private $cache;
+
+       /**
+        * @var Config
+        */
+       private $config;
+
+       /**
+        * @var Language
+        */
+       private $contLang;
+
+       public function __construct(
+               LoadBalancer $loadBalancer,
+               WANObjectCache $cache,
+               Config $mainConfig,
+               Language $contLang
+       ) {
+               $this->loadBalancer = $loadBalancer;
+               $this->cache = $cache;
+               $this->config = $mainConfig;
+               $this->contLang = $contLang;
+       }
+
+       /**
+        * @since 1.31
+        *
+        * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+        *
+        * @return BlobStore
+        */
+       public function newBlobStore( $wikiId = false ) {
+               return $this->newSqlBlobStore( $wikiId );
+       }
+
+       /**
+        * @internal Please call newBlobStore and use the BlobStore interface.
+        *
+        * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+        *
+        * @return SqlBlobStore
+        */
+       public function newSqlBlobStore( $wikiId = false ) {
+               $store = new SqlBlobStore(
+                       $this->loadBalancer,
+                       $this->cache,
+                       $wikiId
+               );
+
+               $store->setCompressBlobs( $this->config->get( 'CompressRevisions' ) );
+               $store->setCacheExpiry( $this->config->get( 'RevisionCacheExpiry' ) );
+               $store->setUseExternalStore( $this->config->get( 'DefaultExternalStore' ) !== false );
+
+               if ( $this->config->get( 'LegacyEncoding' ) ) {
+                       $store->setLegacyEncoding( $this->config->get( 'LegacyEncoding' ), $this->contLang );
+               }
+
+               return $store;
+       }
+
+}
diff --git a/includes/Storage/IncompleteRevisionException.php b/includes/Storage/IncompleteRevisionException.php
new file mode 100644 (file)
index 0000000..bf45b01
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+/**
+ * Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
+ *
+ * @since 1.31
+ */
+class IncompleteRevisionException extends RevisionAccessException {
+
+}
diff --git a/includes/Storage/MutableRevisionRecord.php b/includes/Storage/MutableRevisionRecord.php
new file mode 100644 (file)
index 0000000..