From: daniel Date: Fri, 13 Apr 2018 12:01:18 +0000 (+0200) Subject: [MCR] Make PageArchive aware of MCR X-Git-Tag: 1.34.0-rc.0~4830^2 X-Git-Url: http://git.cyclocoop.org/?a=commitdiff_plain;h=4943c74ac42f5019a6d567fa31513f8422769dd7;p=lhc%2Fweb%2Fwiklou.git [MCR] Make PageArchive aware of MCR Bug: T194015 Change-Id: I92afda87961860983f080d96fa0616a6fcca64e4 --- diff --git a/includes/page/PageArchive.php b/includes/page/PageArchive.php index 8b42020af2..d051c1b602 100644 --- a/includes/page/PageArchive.php +++ b/includes/page/PageArchive.php @@ -19,7 +19,11 @@ */ 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, diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index 3911faa59f..06d789b09e 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -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 index 15a991e387..0000000000 --- a/tests/phpunit/includes/PageArchiveTest.php +++ /dev/null @@ -1,267 +0,0 @@ -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 index 0000000000..d2a8016aad --- /dev/null +++ b/tests/phpunit/includes/page/PageArchiveMcrTest.php @@ -0,0 +1,85 @@ +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 index 0000000000..6757e78a18 --- /dev/null +++ b/tests/phpunit/includes/page/PageArchivePreMcrTest.php @@ -0,0 +1,96 @@ +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 index 0000000000..5a666a8b72 --- /dev/null +++ b/tests/phpunit/includes/page/PageArchiveTestBase.php @@ -0,0 +1,315 @@ +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 ); + } + +}