[MCR] Make PageArchive aware of MCR
authordaniel <daniel.kinzler@wikimedia.de>
Fri, 13 Apr 2018 12:01:18 +0000 (14:01 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Tue, 10 Jul 2018 14:36:57 +0000 (16:36 +0200)
Bug: T194015
Change-Id: I92afda87961860983f080d96fa0616a6fcca64e4

includes/page/PageArchive.php
tests/common/TestsAutoLoader.php
tests/phpunit/includes/PageArchiveTest.php [deleted file]
tests/phpunit/includes/page/PageArchiveMcrTest.php [new file with mode: 0644]
tests/phpunit/includes/page/PageArchivePreMcrTest.php [new file with mode: 0644]
tests/phpunit/includes/page/PageArchiveTestBase.php [new file with mode: 0644]

index 8b42020..d051c1b 100644 (file)
  */
 
 use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\SqlBlobStore;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 
 /**
@@ -50,6 +54,17 @@ class PageArchive {
                $this->config = $config;
        }
 
+       /**
+        * @return RevisionStore
+        */
+       private function getRevisionStore() {
+               // TODO: Refactor: delete()/undelete() should live in a PageStore service;
+               //       Methods in PageArchive and RevisionStore that deal with archive revisions
+               //       should move into an ArchiveStore service (but could still be implemented
+               //       together with RevisionStore).
+               return MediaWikiServices::getInstance()->getRevisionStore();
+       }
+
        public function doesWrites() {
                return true;
        }
@@ -59,9 +74,13 @@ class PageArchive {
         * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
         * namespace/title.
         *
-        * @return ResultWrapper
+        * @deprecated since 1.32.
+        *
+        * @return IResultWrapper
         */
        public static function listAllPages() {
+               wfDeprecated( __METHOD__, '1.32' );
+
                $dbr = wfGetDB( DB_REPLICA );
 
                return self::listPages( $dbr, '' );
@@ -73,7 +92,7 @@ class PageArchive {
         * Returns result wrapper with (ar_namespace, ar_title, count) fields.
         *
         * @param string $term Search term
-        * @return ResultWrapper
+        * @return IResultWrapper
         */
        public static function listPagesBySearch( $term ) {
                $title = Title::newFromText( $term );
@@ -123,7 +142,7 @@ class PageArchive {
         * Returns result wrapper with (ar_namespace, ar_title, count) fields.
         *
         * @param string $prefix Title prefix
-        * @return ResultWrapper
+        * @return IResultWrapper
         */
        public static function listPagesByPrefix( $prefix ) {
                $dbr = wfGetDB( DB_REPLICA );
@@ -149,7 +168,7 @@ class PageArchive {
        /**
         * @param IDatabase $dbr
         * @param string|array $condition
-        * @return bool|ResultWrapper
+        * @return bool|IResultWrapper
         */
        protected static function listPages( $dbr, $condition ) {
                return $dbr->select(
@@ -173,16 +192,20 @@ class PageArchive {
         * List the revisions of the given page. Returns result wrapper with
         * various archive table fields.
         *
-        * @return ResultWrapper
+        * @return IResultWrapper
         */
        public function listRevisions() {
-               $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+               $revisionStore = $this->getRevisionStore();
                $queryInfo = $revisionStore->getArchiveQueryInfo();
 
                $conds = [
                        'ar_namespace' => $this->title->getNamespace(),
                        'ar_title' => $this->title->getDBkey(),
                ];
+
+               // TODO: ORDER BY ar_timestamp DESC, ar_rev_id DESC, to remove ambiguity.
+               // HOWEVER this requires  ar_rev_id to be added to the name_title_timestamp index first!
+               // Otherwise, adding this ar_rev_id to the order will result in a file sort!
                $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
 
                ChangeTags::modifyDisplayQuery(
@@ -210,7 +233,7 @@ class PageArchive {
         * Returns a result wrapper with various filearchive fields, or null
         * if not a file page.
         *
-        * @return ResultWrapper
+        * @return IResultWrapper
         * @todo Does this belong in Image for fuller encapsulation?
         */
        public function listFiles() {
@@ -232,30 +255,60 @@ class PageArchive {
 
        /**
         * Return a Revision object containing data for the deleted revision.
-        * Note that the result *may* or *may not* have a null page ID.
+        *
+        * @deprecated since 1.32, use getArchivedRevision() instead.
         *
         * @param string $timestamp
         * @return Revision|null
         */
        public function getRevision( $timestamp ) {
                $dbr = wfGetDB( DB_REPLICA );
-               $arQuery = Revision::getArchiveQueryInfo();
+               $rec = $this->getRevisionByConditions(
+                       [ 'ar_timestamp' => $dbr->timestamp( $timestamp ) ]
+               );
+               return $rec ? new Revision( $rec ) : null;
+       }
+
+       /**
+        * Return the archived revision with the given ID.
+        *
+        * @param int $revId
+        * @return Revision|null
+        */
+       public function getArchivedRevision( $revId ) {
+               // Protect against code switching from getRevision() passing in a timestamp.
+               Assert::parameterType( 'integer', $revId, '$revId' );
+
+               $rec = $this->getRevisionByConditions( [ 'ar_rev_id' => $revId ] );
+               return $rec ? new Revision( $rec ) : null;
+       }
+
+       /**
+        * @param array $conditions
+        * @param array $options
+        *
+        * @return RevisionRecord|null
+        */
+       private function getRevisionByConditions( array $conditions, array $options = [] ) {
+               $dbr = wfGetDB( DB_REPLICA );
+               $arQuery = $this->getRevisionStore()->getArchiveQueryInfo();
+
+               $conditions = $conditions + [
+                       'ar_namespace' => $this->title->getNamespace(),
+                       'ar_title' => $this->title->getDBkey(),
+               ];
 
                $row = $dbr->selectRow(
                        $arQuery['tables'],
                        $arQuery['fields'],
-                       [
-                               'ar_namespace' => $this->title->getNamespace(),
-                               'ar_title' => $this->title->getDBkey(),
-                               'ar_timestamp' => $dbr->timestamp( $timestamp )
-                       ],
+                       $conditions,
                        __METHOD__,
-                       [],
+                       $options,
                        $arQuery['joins']
                );
 
                if ( $row ) {
-                       return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
+                       return $this->getRevisionStore()->newRevisionFromArchiveRow( $row, 0, $this->title );
                }
 
                return null;
@@ -276,7 +329,7 @@ class PageArchive {
 
                // Check the previous deleted revision...
                $row = $dbr->selectRow( 'archive',
-                       'ar_timestamp',
+                       [ 'ar_id', 'ar_timestamp' ],
                        [ 'ar_namespace' => $this->title->getNamespace(),
                                'ar_title' => $this->title->getDBkey(),
                                'ar_timestamp < ' .
@@ -286,6 +339,7 @@ class PageArchive {
                                'ORDER BY' => 'ar_timestamp DESC',
                                'LIMIT' => 1 ] );
                $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
+               $prevDeletedId = $row ? intval( $row->ar_rev_id ) : null;
 
                $row = $dbr->selectRow( [ 'page', 'revision' ],
                        [ 'rev_id', 'rev_timestamp' ],
@@ -304,31 +358,40 @@ class PageArchive {
 
                if ( $prevLive && $prevLive > $prevDeleted ) {
                        // Most prior revision was live
-                       return Revision::newFromId( $prevLiveId );
+                       $rec = $this->getRevisionStore()->getRevisionById( $prevLiveId );
+                       $rec = $rec ? new Revision( $rec ) : null;
                } elseif ( $prevDeleted ) {
                        // Most prior revision was deleted
-                       return $this->getRevision( $prevDeleted );
+                       $rec = $this->getArchivedRevision( $prevDeletedId );
+               } else {
+                       $rec = null;
                }
 
-               // No prior revision on this page.
-               return null;
+               return $rec;
        }
 
        /**
-        * Get the text from an archive row containing ar_text_id
+        * Get the text from an archive row containing ar_text_id.
+        *
+        * @deprecated since 1.32. In the MCR schema, ar_text_id no longer exists.
+        * Calling code should switch to getArchiveRevision().
+        *
+        * @todo remove in 1.33
         *
-        * @deprecated since 1.31
         * @param object $row Database row
         * @return string
         */
        public function getTextFromRow( $row ) {
-               $dbr = wfGetDB( DB_REPLICA );
-               $text = $dbr->selectRow( 'text',
-                       [ 'old_text', 'old_flags' ],
-                       [ 'old_id' => $row->ar_text_id ],
-                       __METHOD__ );
+               wfDeprecated( __METHOD__, '1.32' );
+
+               if ( empty( $row->ar_text_id ) ) {
+                       throw new InvalidArgumentException( '$row->ar_text_id must be set and not empty!' );
+               }
+
+               $address = SqlBlobStore::makeAddressFromTextId( $row->ar_text_id );
+               $blobStore = MediaWikiServices::getInstance()->getBlobStore();
 
-               return Revision::getRevisionText( $text );
+               return $blobStore->getBlob( $address );
        }
 
        /**
@@ -337,41 +400,65 @@ class PageArchive {
         *
         * If there are no archived revisions for the page, returns NULL.
         *
+        * @note this bypasses any audience checks.
+        *
+        * @deprecated since 1.32. For compatibility with the MCR schema,
+        * calling code should switch to getLastRevisionId() and getArchiveRevision().
+        *
+        * @todo remove in 1.33
+        *
         * @return string|null
         */
        public function getLastRevisionText() {
+               wfDeprecated( __METHOD__, '1.32' );
+
+               $revId = $this->getLastRevisionId();
+
+               if ( $revId ) {
+                       $rev = $this->getArchivedRevision( $revId );
+                       $content = $rev->getContent( RevisionRecord::RAW );
+                       return $content->serialize();
+               }
+
+               return null;
+       }
+
+       /**
+        * Returns the ID of the latest deleted revision.
+        *
+        * @return int|false The revision's ID, or false if there is no deleted revision.
+        */
+       public function getLastRevisionId() {
                $dbr = wfGetDB( DB_REPLICA );
-               $row = $dbr->selectRow(
-                       [ 'archive', 'text' ],
-                       [ 'old_text', 'old_flags' ],
+               $revId = $dbr->selectField(
+                       'archive',
+                       'ar_rev_id',
                        [ 'ar_namespace' => $this->title->getNamespace(),
                                'ar_title' => $this->title->getDBkey() ],
                        __METHOD__,
-                       [ 'ORDER BY' => 'ar_timestamp DESC, ar_id DESC' ],
-                       [ 'text' => [ 'JOIN', 'old_id = ar_text_id' ] ]
+                       [ 'ORDER BY' => 'ar_timestamp DESC, ar_id DESC' ]
                );
 
-               if ( $row ) {
-                       return Revision::getRevisionText( $row );
-               }
-
-               return null;
+               return $revId ? intval( $revId ) : false;
        }
 
        /**
         * Quick check if any archived revisions are present for the page.
+        * This says nothing about whether the page currently exists in the page table or not.
         *
         * @return bool
         */
        public function isDeleted() {
                $dbr = wfGetDB( DB_REPLICA );
-               $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
+               $row = $dbr->selectRow(
+                       [ 'archive' ],
+                       '1', // We don't care about the value. Allow the database to optimize.
                        [ 'ar_namespace' => $this->title->getNamespace(),
                                'ar_title' => $this->title->getDBkey() ],
                        __METHOD__
                );
 
-               return ( $n > 0 );
+               return (bool)$row;
        }
 
        /**
@@ -527,7 +614,7 @@ class PageArchive {
                        $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
                }
 
-               $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+               $revisionStore = $this->getRevisionStore();
                $queryInfo = $revisionStore->getArchiveQueryInfo();
                $queryInfo['tables'][] = 'revision';
                $queryInfo['fields'][] = 'rev_id';
@@ -600,27 +687,35 @@ class PageArchive {
                if ( $latestRestorableRow !== null ) {
                        $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
 
-                       // grab the content to check consistency with global state before restoring the page.
-                       $revision = Revision::newFromArchiveRow( $latestRestorableRow,
-                               [
-                                       'title' => $article->getTitle(), // used to derive default content model
-                               ]
+                       // Grab the content to check consistency with global state before restoring the page.
+                       // XXX: The only current use case is Wikibase, which tries to enforce uniqueness of
+                       // certain things across all pages. There may be a better way to do that.
+                       $revision = $revisionStore->newRevisionFromArchiveRow(
+                               $latestRestorableRow,
+                               0,
+                               $this->title
                        );
-                       $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
-                       $content = $revision->getContent( Revision::RAW );
 
-                       // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
-                       $status = $content->prepareSave( $article, 0, -1, $user );
-                       if ( !$status->isOK() ) {
-                               $dbw->endAtomic( __METHOD__ );
+                       // TODO: use User::newFromUserIdentity from If610c68f4912e
+                       // TODO: The User isn't used for anything in prepareSave()! We should drop it.
+                       $user = User::newFromName( $revision->getUser( RevisionRecord::RAW )->getName(), false );
 
-                               return $status;
+                       foreach ( $revision->getSlotRoles() as $role ) {
+                               $content = $revision->getContent( $role, RevisionRecord::RAW );
+
+                               // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
+                               $status = $content->prepareSave( $article, 0, -1, $user );
+                               if ( !$status->isOK() ) {
+                                       $dbw->endAtomic( __METHOD__ );
+
+                                       return $status;
+                               }
                        }
                }
 
                $newid = false; // newly created page ID
                $restored = 0; // number of revisions restored
-               /** @var Revision $revision */
+               /** @var RevisionRecord|null $revision */
                $revision = null;
                $restoredPages = [];
                // If there are no restorable revisions, we can skip most of the steps.
@@ -630,7 +725,7 @@ class PageArchive {
                        if ( $makepage ) {
                                // Check the state of the newest to-be version...
                                if ( !$unsuppress
-                                       && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
+                                       && ( $latestRestorableRow->ar_deleted & RevisionRecord::DELETED_TEXT )
                                ) {
                                        $dbw->endAtomic( __METHOD__ );
 
@@ -648,7 +743,7 @@ class PageArchive {
                                if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
                                        // Check the state of the newest to-be version...
                                        if ( !$unsuppress
-                                               && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
+                                               && ( $latestRestorableRow->ar_deleted & RevisionRecord::DELETED_TEXT )
                                        ) {
                                                $dbw->endAtomic( __METHOD__ );
 
@@ -667,20 +762,24 @@ class PageArchive {
                                }
                                // Insert one revision at a time...maintaining deletion status
                                // unless we are specifically removing all restrictions...
-                               $revision = Revision::newFromArchiveRow( $row,
+                               $revision = $revisionStore->newRevisionFromArchiveRow(
+                                       $row,
+                                       0,
+                                       $this->title,
                                        [
                                                'page' => $pageId,
-                                               'title' => $this->title,
                                                'deleted' => $unsuppress ? 0 : $row->ar_deleted
-                                       ] );
+                                       ]
+                               );
 
                                // This will also copy the revision to ip_changes if it was an IP edit.
-                               $revision->insertOn( $dbw );
+                               $revisionStore->insertRevisionOn( $revision, $dbw );
 
                                $restored++;
 
+                               $legacyRevision = new Revision( $revision );
                                Hooks::run( 'ArticleRevisionUndeleted',
-                                       [ &$this->title, $revision, $row->ar_page_id ] );
+                                       [ &$this->title, $legacyRevision, $row->ar_page_id ] );
                                $restoredPages[$row->ar_page_id] = true;
                        }
 
@@ -708,12 +807,14 @@ class PageArchive {
                if ( $restored ) {
                        $created = (bool)$newid;
                        // Attach the latest revision to the page...
-                       $wasnew = $article->updateIfNewerOn( $dbw, $revision );
+                       // XXX: updateRevisionOn should probably move into a PageStore service.
+                       $wasnew = $article->updateIfNewerOn( $dbw, $legacyRevision );
                        if ( $created || $wasnew ) {
                                // Update site stats, link tables, etc
+                               // TODO: use DerivedPageDataUpdater from If610c68f4912e!
                                $article->doEditUpdates(
-                                       $revision,
-                                       User::newFromName( $revision->getUserText( Revision::RAW ), false ),
+                                       $legacyRevision,
+                                       User::newFromName( $revision->getUser( RevisionRecord::RAW )->getName(), false ),
                                        [
                                                'created' => $created,
                                                'oldcountable' => $oldcountable,
index 3911faa..06d789b 100644 (file)
@@ -67,6 +67,7 @@ $wgAutoloadClasses += [
        'HamcrestPHPUnitIntegration' => "$testDir/phpunit/HamcrestPHPUnitIntegration.php",
 
        # tests/phpunit/includes
+       'PageArchiveTestBase' => "$testDir/phpunit/includes/page/PageArchiveTestBase.php",
        'RevisionDbTestBase' => "$testDir/phpunit/includes/RevisionDbTestBase.php",
        'RevisionTestModifyableContent' => "$testDir/phpunit/includes/RevisionTestModifyableContent.php",
        'RevisionTestModifyableContentHandler' => "$testDir/phpunit/includes/RevisionTestModifyableContentHandler.php",
diff --git a/tests/phpunit/includes/PageArchiveTest.php b/tests/phpunit/includes/PageArchiveTest.php
deleted file mode 100644 (file)
index 15a991e..0000000
+++ /dev/null
@@ -1,267 +0,0 @@
-<?php
-
-/**
- * Test class for page archiving.
- *
- * @group ContentHandler
- * @group Database
- * ^--- important, causes temporary tables to be used instead of the real database
- *
- * @group medium
- * ^--- important, causes tests not to fail with timeout
- */
-class PageArchiveTest extends MediaWikiTestCase {
-
-       /**
-        * @var PageArchive $archivedPage
-        */
-       private $archivedPage;
-
-       /**
-        * A logged out user who edited the page before it was archived.
-        * @var string $ipEditor
-        */
-       private $ipEditor;
-
-       /**
-        * Revision ID of the IP edit
-        * @var int $ipRevId
-        */
-       private $ipRevId;
-
-       function __construct( $name = null, array $data = [], $dataName = '' ) {
-               parent::__construct( $name, $data, $dataName );
-
-               $this->tablesUsed = array_merge(
-                       $this->tablesUsed,
-                       [
-                               'page',
-                               'revision',
-                               'ip_changes',
-                               'text',
-                               'archive',
-                               'recentchanges',
-                               'logging',
-                               'page_props',
-                       ]
-               );
-       }
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
-               $this->setMwGlobals( 'wgMultiContentRevisionSchemaMigrationStage', SCHEMA_COMPAT_OLD );
-               $this->overrideMwServices();
-
-               // First create our dummy page
-               $page = Title::newFromText( 'PageArchiveTest_thePage' );
-               $page = new WikiPage( $page );
-               $content = ContentHandler::makeContent(
-                       'testing',
-                       $page->getTitle(),
-                       CONTENT_MODEL_WIKITEXT
-               );
-               $page->doEditContent( $content, 'testing', EDIT_NEW );
-
-               // Insert IP revision
-               $this->ipEditor = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7';
-               $rev = new Revision( [
-                       'text' => 'Lorem Ipsum',
-                       'comment' => 'just a test',
-                       'page' => $page->getId(),
-                       'user_text' => $this->ipEditor,
-               ] );
-               $dbw = wfGetDB( DB_MASTER );
-               $this->ipRevId = $rev->insertOn( $dbw );
-
-               // Delete the page
-               $page->doDeleteArticleReal( 'Just a test deletion' );
-
-               $this->archivedPage = new PageArchive( $page->getTitle() );
-       }
-
-       /**
-        * @covers PageArchive::undelete
-        * @covers PageArchive::undeleteRevisions
-        */
-       public function testUndeleteRevisions() {
-               // First make sure old revisions are archived
-               $dbr = wfGetDB( DB_REPLICA );
-               $arQuery = Revision::getArchiveQueryInfo();
-               $res = $dbr->select(
-                       $arQuery['tables'],
-                       $arQuery['fields'],
-                       [ 'ar_rev_id' => $this->ipRevId ],
-                       __METHOD__,
-                       [],
-                       $arQuery['joins']
-               );
-               $row = $res->fetchObject();
-               $this->assertEquals( $this->ipEditor, $row->ar_user_text );
-
-               // Should not be in revision
-               $res = $dbr->select( 'revision', '1', [ 'rev_id' => $this->ipRevId ] );
-               $this->assertFalse( $res->fetchObject() );
-
-               // Should not be in ip_changes
-               $res = $dbr->select( 'ip_changes', '1', [ 'ipc_rev_id' => $this->ipRevId ] );
-               $this->assertFalse( $res->fetchObject() );
-
-               // Restore the page
-               $this->archivedPage->undelete( [] );
-
-               // Should be back in revision
-               $revQuery = Revision::getQueryInfo();
-               $res = $dbr->select(
-                       $revQuery['tables'],
-                       $revQuery['fields'],
-                       [ 'rev_id' => $this->ipRevId ],
-                       __METHOD__,
-                       [],
-                       $revQuery['joins']
-               );
-               $row = $res->fetchObject();
-               $this->assertEquals( $this->ipEditor, $row->rev_user_text );
-
-               // Should be back in ip_changes
-               $res = $dbr->select( 'ip_changes', [ 'ipc_hex' ], [ 'ipc_rev_id' => $this->ipRevId ] );
-               $row = $res->fetchObject();
-               $this->assertEquals( IP::toHex( $this->ipEditor ), $row->ipc_hex );
-       }
-
-       /**
-        * @covers PageArchive::listRevisions
-        */
-       public function testListRevisions() {
-               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
-               $this->setMwGlobals( 'wgMultiContentRevisionSchemaMigrationStage', SCHEMA_COMPAT_OLD );
-               $this->overrideMwServices();
-
-               $revisions = $this->archivedPage->listRevisions();
-               $this->assertEquals( 2, $revisions->numRows() );
-
-               // Get the rows as arrays
-               $row1 = (array)$revisions->current();
-               $row2 = (array)$revisions->next();
-               // Unset the timestamps (we assume they will be right...
-               $this->assertInternalType( 'string', $row1['ar_timestamp'] );
-               $this->assertInternalType( 'string', $row2['ar_timestamp'] );
-               unset( $row1['ar_timestamp'] );
-               unset( $row2['ar_timestamp'] );
-
-               $this->assertEquals(
-                       [
-                               'ar_minor_edit' => '0',
-                               'ar_user' => '0',
-                               'ar_user_text' => '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7',
-                               'ar_actor' => null,
-                               'ar_len' => '11',
-                               'ar_deleted' => '0',
-                               'ar_rev_id' => '3',
-                               'ar_sha1' => '0qdrpxl537ivfnx4gcpnzz0285yxryy',
-                               'ar_page_id' => '2',
-                               'ar_comment_text' => 'just a test',
-                               'ar_comment_data' => null,
-                               'ar_comment_cid' => null,
-                               'ar_content_format' => null,
-                               'ar_content_model' => null,
-                               'ts_tags' => null,
-                               'ar_id' => '2',
-                               'ar_namespace' => '0',
-                               'ar_title' => 'PageArchiveTest_thePage',
-                               'ar_text_id' => '3',
-                               'ar_parent_id' => '2',
-                       ],
-                       $row1
-               );
-               $this->assertEquals(
-                       [
-                               'ar_minor_edit' => '0',
-                               'ar_user' => '0',
-                               'ar_user_text' => '127.0.0.1',
-                               'ar_actor' => null,
-                               'ar_len' => '7',
-                               'ar_deleted' => '0',
-                               'ar_rev_id' => '2',
-                               'ar_sha1' => 'pr0s8e18148pxhgjfa0gjrvpy8fiyxc',
-                               'ar_page_id' => '2',
-                               'ar_comment_text' => 'testing',
-                               'ar_comment_data' => null,
-                               'ar_comment_cid' => null,
-                               'ar_content_format' => null,
-                               'ar_content_model' => null,
-                               'ts_tags' => null,
-                               'ar_id' => '1',
-                               'ar_namespace' => '0',
-                               'ar_title' => 'PageArchiveTest_thePage',
-                               'ar_text_id' => '2',
-                               'ar_parent_id' => '0',
-                       ],
-                       $row2
-               );
-       }
-
-       /**
-        * @covers PageArchive::listPagesBySearch
-        */
-       public function testListPagesBySearch() {
-               $pages = PageArchive::listPagesBySearch( 'PageArchiveTest_thePage' );
-               $this->assertSame( 1, $pages->numRows() );
-
-               $page = (array)$pages->current();
-
-               $this->assertSame(
-                       [
-                               'ar_namespace' => '0',
-                               'ar_title' => 'PageArchiveTest_thePage',
-                               'count' => '2',
-                       ],
-                       $page
-               );
-       }
-
-       /**
-        * @covers PageArchive::listPagesBySearch
-        */
-       public function testListPagesByPrefix() {
-               $pages = PageArchive::listPagesByPrefix( 'PageArchiveTest' );
-               $this->assertSame( 1, $pages->numRows() );
-
-               $page = (array)$pages->current();
-
-               $this->assertSame(
-                       [
-                               'ar_namespace' => '0',
-                               'ar_title' => 'PageArchiveTest_thePage',
-                               'count' => '2',
-                       ],
-                       $page
-               );
-       }
-
-       /**
-        * @covers PageArchive::getTextFromRow
-        */
-       public function testGetTextFromRow() {
-               $row = (object)[ 'ar_text_id' => 2 ];
-               $text = $this->archivedPage->getTextFromRow( $row );
-               $this->assertSame( 'testing', $text );
-       }
-
-       /**
-        * @covers PageArchive::getLastRevisionText
-        */
-       public function testGetLastRevisionText() {
-               $text = $this->archivedPage->getLastRevisionText();
-               $this->assertSame( 'Lorem Ipsum', $text );
-       }
-
-       /**
-        * @covers PageArchive::isDeleted
-        */
-       public function testIsDeleted() {
-               $this->assertTrue( $this->archivedPage->isDeleted() );
-       }
-}
diff --git a/tests/phpunit/includes/page/PageArchiveMcrTest.php b/tests/phpunit/includes/page/PageArchiveMcrTest.php
new file mode 100644 (file)
index 0000000..d2a8016
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Tests\Storage\McrSchemaOverride;
+
+/**
+ * Test class for page archiving, using the new MCR schema.
+ *
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ *
+ * @group medium
+ * ^--- important, causes tests not to fail with timeout
+ */
+class PageArchiveMcrTest extends PageArchiveTestBase {
+
+       use McrSchemaOverride;
+
+       /**
+        * @covers PageArchive::listRevisions
+        */
+       public function testListRevisions_slots() {
+               $revisions = $this->archivedPage->listRevisions();
+
+               $revisionStore = MediaWikiServices::getInstance()->getInstance()->getRevisionStore();
+               $slotsQuery = $revisionStore->getSlotsQueryInfo( [ 'content' ] );
+
+               foreach ( $revisions as $row ) {
+                       $this->assertSelect(
+                               $slotsQuery['tables'],
+                               'count(*)',
+                               [ 'slot_revision_id' => $row->ar_rev_id ],
+                               [ [ 1 ] ],
+                               [],
+                               $slotsQuery['joins']
+                       );
+               }
+       }
+
+       protected function getExpectedArchiveRows() {
+               return [
+                       [
+                               'ar_minor_edit' => '0',
+                               'ar_user' => '0',
+                               'ar_user_text' => $this->ipEditor,
+                               'ar_actor' => null,
+                               'ar_len' => '11',
+                               'ar_deleted' => '0',
+                               'ar_rev_id' => strval( $this->ipRev->getId() ),
+                               'ar_timestamp' => $this->db->timestamp( $this->ipRev->getTimestamp() ),
+                               'ar_sha1' => '0qdrpxl537ivfnx4gcpnzz0285yxryy',
+                               'ar_page_id' => strval( $this->ipRev->getPageId() ),
+                               'ar_comment_text' => 'just a test',
+                               'ar_comment_data' => null,
+                               'ar_comment_cid' => null,
+                               'ts_tags' => null,
+                               'ar_id' => '2',
+                               'ar_namespace' => '0',
+                               'ar_title' => 'PageArchiveTest_thePage',
+                               'ar_parent_id' => strval( $this->ipRev->getParentId() ),
+                       ],
+                       [
+                               'ar_minor_edit' => '0',
+                               'ar_user' => (string)$this->getTestUser()->getUser()->getId(),
+                               'ar_user_text' => $this->getTestUser()->getUser()->getName(),
+                               'ar_actor' => null,
+                               'ar_len' => '7',
+                               'ar_deleted' => '0',
+                               'ar_rev_id' => strval( $this->firstRev->getId() ),
+                               'ar_timestamp' => $this->db->timestamp( $this->firstRev->getTimestamp() ),
+                               'ar_sha1' => 'pr0s8e18148pxhgjfa0gjrvpy8fiyxc',
+                               'ar_page_id' => strval( $this->firstRev->getPageId() ),
+                               'ar_comment_text' => 'testing',
+                               'ar_comment_data' => null,
+                               'ar_comment_cid' => null,
+                               'ts_tags' => null,
+                               'ar_id' => '1',
+                               'ar_namespace' => '0',
+                               'ar_title' => 'PageArchiveTest_thePage',
+                               'ar_parent_id' => '0',
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/page/PageArchivePreMcrTest.php b/tests/phpunit/includes/page/PageArchivePreMcrTest.php
new file mode 100644 (file)
index 0000000..6757e78
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+
+/**
+ * Test class for page archiving, using the pre-MCR schema.
+ *
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ *
+ * @group medium
+ * ^--- important, causes tests not to fail with timeout
+ */
+class PageArchivePreMcrTest extends PageArchiveTestBase {
+
+       use PreMcrSchemaOverride;
+
+       /**
+        * @covers PageArchive::getTextFromRow
+        */
+       public function testGetTextFromRow() {
+               $this->hideDeprecated( PageArchive::class . '::getTextFromRow' );
+
+               /** @var SqlBlobStore $blobStore */
+               $blobStore = MediaWikiServices::getInstance()->getBlobStore();
+
+               $textId = $blobStore->getTextIdFromAddress(
+                       $this->firstRev->getSlot( 'main' )->getAddress()
+               );
+
+               $row = (object)[ 'ar_text_id' => $textId ];
+               $text = $this->archivedPage->getTextFromRow( $row );
+               $this->assertSame( 'testing', $text );
+       }
+
+       protected function getExpectedArchiveRows() {
+               /** @var SqlBlobStore $blobStore */
+               $blobStore = MediaWikiServices::getInstance()->getBlobStore();
+
+               return [
+                       [
+                               'ar_minor_edit' => '0',
+                               'ar_user' => '0',
+                               'ar_user_text' => $this->ipEditor,
+                               'ar_actor' => null,
+                               'ar_len' => '11',
+                               'ar_deleted' => '0',
+                               'ar_rev_id' => strval( $this->ipRev->getId() ),
+                               'ar_timestamp' => $this->db->timestamp( $this->ipRev->getTimestamp() ),
+                               'ar_sha1' => '0qdrpxl537ivfnx4gcpnzz0285yxryy',
+                               'ar_page_id' => strval( $this->ipRev->getPageId() ),
+                               'ar_comment_text' => 'just a test',
+                               'ar_comment_data' => null,
+                               'ar_comment_cid' => null,
+                               'ar_content_format' => null,
+                               'ar_content_model' => null,
+                               'ts_tags' => null,
+                               'ar_id' => '2',
+                               'ar_namespace' => '0',
+                               'ar_title' => 'PageArchiveTest_thePage',
+                               'ar_text_id' => (string)$blobStore->getTextIdFromAddress(
+                                       $this->ipRev->getSlot( 'main' )->getAddress()
+                               ),
+                               'ar_parent_id' => strval( $this->ipRev->getParentId() ),
+                       ],
+                       [
+                               'ar_minor_edit' => '0',
+                               'ar_user' => (string)$this->getTestUser()->getUser()->getId(),
+                               'ar_user_text' => $this->getTestUser()->getUser()->getName(),
+                               'ar_actor' => null,
+                               'ar_len' => '7',
+                               'ar_deleted' => '0',
+                               'ar_rev_id' => strval( $this->firstRev->getId() ),
+                               'ar_timestamp' => $this->db->timestamp( $this->firstRev->getTimestamp() ),
+                               'ar_sha1' => 'pr0s8e18148pxhgjfa0gjrvpy8fiyxc',
+                               'ar_page_id' => strval( $this->firstRev->getPageId() ),
+                               'ar_comment_text' => 'testing',
+                               'ar_comment_data' => null,
+                               'ar_comment_cid' => null,
+                               'ar_content_format' => null,
+                               'ar_content_model' => null,
+                               'ts_tags' => null,
+                               'ar_id' => '1',
+                               'ar_namespace' => '0',
+                               'ar_title' => 'PageArchiveTest_thePage',
+                               'ar_text_id' => (string)$blobStore->getTextIdFromAddress(
+                                       $this->firstRev->getSlot( 'main' )->getAddress()
+                               ),
+                               'ar_parent_id' => '0',
+                       ],
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/page/PageArchiveTestBase.php b/tests/phpunit/includes/page/PageArchiveTestBase.php
new file mode 100644 (file)
index 0000000..5a666a8
--- /dev/null
@@ -0,0 +1,315 @@
+<?php
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\RevisionRecord;
+
+/**
+ * Base class for tests of PageArchive against different database schemas.
+ */
+abstract class PageArchiveTestBase extends MediaWikiTestCase {
+
+       /**
+        * @var int
+        */
+       protected $pageId;
+
+       /**
+        * @var PageArchive $archivedPage
+        */
+       protected $archivedPage;
+
+       /**
+        * A logged out user who edited the page before it was archived.
+        * @var string $ipEditor
+        */
+       protected $ipEditor;
+
+       /**
+        * Revision of the first (initial) edit
+        * @var RevisionRecord
+        */
+       protected $firstRev;
+
+       /**
+        * Revision of the IP edit (the second edit)
+        * @var RevisionRecord
+        */
+       protected $ipRev;
+
+       function __construct( $name = null, array $data = [], $dataName = '' ) {
+               parent::__construct( $name, $data, $dataName );
+
+               $this->tablesUsed = array_merge(
+                       $this->tablesUsed,
+                       [
+                               'page',
+                               'revision',
+                               'ip_changes',
+                               'text',
+                               'archive',
+                               'recentchanges',
+                               'logging',
+                               'page_props',
+                       ]
+               );
+       }
+
+       protected function addCoreDBData() {
+               // Blank out to avoid failures when schema overrides imposed by subclasses
+               // affect revision storage.
+       }
+
+       /**
+        * @return int
+        */
+       abstract protected function getMcrMigrationStage();
+
+       /**
+        * @return string[]
+        */
+       abstract protected function getMcrTablesToReset();
+
+       /**
+        * @return bool
+        */
+       protected function getContentHandlerUseDB() {
+               return true;
+       }
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->tablesUsed += $this->getMcrTablesToReset();
+
+               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() );
+               $this->setMwGlobals(
+                       'wgMultiContentRevisionSchemaMigrationStage',
+                       $this->getMcrMigrationStage()
+               );
+               $this->overrideMwServices();
+
+               // First create our dummy page
+               $page = Title::newFromText( 'PageArchiveTest_thePage' );
+               $page = new WikiPage( $page );
+               $content = ContentHandler::makeContent(
+                       'testing',
+                       $page->getTitle(),
+                       CONTENT_MODEL_WIKITEXT
+               );
+
+               $user = $this->getTestUser()->getUser();
+               $page->doEditContent( $content, 'testing', EDIT_NEW, false, $user );
+
+               $this->pageId = $page->getId();
+               $this->firstRev = $page->getRevision()->getRevisionRecord();
+
+               // Insert IP revision
+               $this->ipEditor = '2001:db8::1';
+
+               $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+
+               $ipTimestamp = wfTimestamp(
+                       TS_MW,
+                       wfTimestamp( TS_UNIX, $this->firstRev->getTimestamp() ) + 1
+               );
+
+               $rev = $revisionStore->newMutableRevisionFromArray( [
+                       'text' => 'Lorem Ipsum',
+                       'comment' => 'just a test',
+                       'page' => $page->getId(),
+                       'user_text' => $this->ipEditor,
+                       'timestamp' => $ipTimestamp,
+               ] );
+
+               $dbw = wfGetDB( DB_MASTER );
+               $this->ipRev = $revisionStore->insertRevisionOn( $rev, $dbw );
+
+               // Delete the page
+               $page->doDeleteArticleReal( 'Just a test deletion' );
+
+               $this->archivedPage = new PageArchive( $page->getTitle() );
+       }
+
+       /**
+        * @covers PageArchive::undelete
+        * @covers PageArchive::undeleteRevisions
+        */
+       public function testUndeleteRevisions() {
+               // TODO: MCR: Test undeletion with multiple slots. Check that slots remain untouched.
+
+               // First make sure old revisions are archived
+               $dbr = wfGetDB( DB_REPLICA );
+               $arQuery = Revision::getArchiveQueryInfo();
+               $row = $dbr->selectRow(
+                       $arQuery['tables'],
+                       $arQuery['fields'],
+                       [ 'ar_rev_id' => $this->ipRev->getId() ],
+                       __METHOD__,
+                       [],
+                       $arQuery['joins']
+               );
+               $this->assertEquals( $this->ipEditor, $row->ar_user_text );
+
+               // Should not be in revision
+               $row = $dbr->selectRow( 'revision', '1', [ 'rev_id' => $this->ipRev->getId() ] );
+               $this->assertFalse( $row );
+
+               // Should not be in ip_changes
+               $row = $dbr->selectRow( 'ip_changes', '1', [ 'ipc_rev_id' => $this->ipRev->getId() ] );
+               $this->assertFalse( $row );
+
+               // Restore the page
+               $this->archivedPage->undelete( [] );
+
+               // Should be back in revision
+               $revQuery = Revision::getQueryInfo();
+               $row = $dbr->selectRow(
+                       $revQuery['tables'],
+                       $revQuery['fields'],
+                       [ 'rev_id' => $this->ipRev->getId() ],
+                       __METHOD__,
+                       [],
+                       $revQuery['joins']
+               );
+               $this->assertNotFalse( $row, 'row exists in revision table' );
+               $this->assertEquals( $this->ipEditor, $row->rev_user_text );
+
+               // Should be back in ip_changes
+               $row = $dbr->selectRow( 'ip_changes', [ 'ipc_hex' ], [ 'ipc_rev_id' => $this->ipRev->getId() ] );
+               $this->assertNotFalse( $row, 'row exists in ip_changes table' );
+               $this->assertEquals( IP::toHex( $this->ipEditor ), $row->ipc_hex );
+       }
+
+       abstract protected function getExpectedArchiveRows();
+
+       /**
+        * @covers PageArchive::listRevisions
+        */
+       public function testListRevisions() {
+               $revisions = $this->archivedPage->listRevisions();
+               $this->assertEquals( 2, $revisions->numRows() );
+
+               // Get the rows as arrays
+               $row0 = (array)$revisions->current();
+               $row1 = (array)$revisions->next();
+
+               $expectedRows = $this->getExpectedArchiveRows();
+
+               $this->assertEquals(
+                       $expectedRows[0],
+                       $row0
+               );
+               $this->assertEquals(
+                       $expectedRows[1],
+                       $row1
+               );
+       }
+
+       /**
+        * @covers PageArchive::listPagesBySearch
+        */
+       public function testListPagesBySearch() {
+               $pages = PageArchive::listPagesBySearch( 'PageArchiveTest_thePage' );
+               $this->assertSame( 1, $pages->numRows() );
+
+               $page = (array)$pages->current();
+
+               $this->assertSame(
+                       [
+                               'ar_namespace' => '0',
+                               'ar_title' => 'PageArchiveTest_thePage',
+                               'count' => '2',
+                       ],
+                       $page
+               );
+       }
+
+       /**
+        * @covers PageArchive::listPagesBySearch
+        */
+       public function testListPagesByPrefix() {
+               $pages = PageArchive::listPagesByPrefix( 'PageArchiveTest' );
+               $this->assertSame( 1, $pages->numRows() );
+
+               $page = (array)$pages->current();
+
+               $this->assertSame(
+                       [
+                               'ar_namespace' => '0',
+                               'ar_title' => 'PageArchiveTest_thePage',
+                               'count' => '2',
+                       ],
+                       $page
+               );
+       }
+
+       public function provideGetTextFromRowThrowsInvalidArgumentException() {
+               yield 'missing ar_text_id field' => [ [] ];
+               yield 'ar_text_id is null' => [ [ 'ar_text_id' => null ] ];
+               yield 'ar_text_id is zero' => [ [ 'ar_text_id' => 0 ] ];
+               yield 'ar_text_id is "0"' => [ [ 'ar_text_id' => '0' ] ];
+       }
+
+       /**
+        * @dataProvider provideGetTextFromRowThrowsInvalidArgumentException
+        * @covers PageArchive::getTextFromRow
+        */
+       public function testGetTextFromRowThrowsInvalidArgumentException( array $row ) {
+               $this->hideDeprecated( PageArchive::class . '::getTextFromRow' );
+               $this->setExpectedException( InvalidArgumentException::class );
+
+               $this->archivedPage->getTextFromRow( (object)$row );
+       }
+
+       /**
+        * @covers PageArchive::getLastRevisionText
+        */
+       public function testGetLastRevisionText() {
+               $this->hideDeprecated( PageArchive::class . '::getLastRevisionText' );
+
+               $text = $this->archivedPage->getLastRevisionText();
+               $this->assertSame( 'Lorem Ipsum', $text );
+       }
+
+       /**
+        * @covers PageArchive::getLastRevisionId
+        */
+       public function testGetLastRevisionId() {
+               $id = $this->archivedPage->getLastRevisionId();
+               $this->assertSame( $this->ipRev->getId(), $id );
+       }
+
+       /**
+        * @covers PageArchive::isDeleted
+        */
+       public function testIsDeleted() {
+               $this->assertTrue( $this->archivedPage->isDeleted() );
+       }
+
+       /**
+        * @covers PageArchive::getRevision
+        */
+       public function testGetRevision() {
+               $rev = $this->archivedPage->getRevision( $this->ipRev->getTimestamp() );
+               $this->assertNotNull( $rev );
+               $this->assertSame( $this->pageId, $rev->getPage() );
+
+               $rev = $this->archivedPage->getRevision( '22991212115555' );
+               $this->assertNull( $rev );
+       }
+
+       /**
+        * @covers PageArchive::getRevision
+        */
+       public function testGetArchivedRevision() {
+               $rev = $this->archivedPage->getArchivedRevision( $this->ipRev->getId() );
+               $this->assertNotNull( $rev );
+               $this->assertSame( $this->ipRev->getTimestamp(), $rev->getTimestamp() );
+               $this->assertSame( $this->pageId, $rev->getPage() );
+
+               $rev = $this->archivedPage->getArchivedRevision( 632546 );
+               $this->assertNull( $rev );
+       }
+
+}