use RecentChange;
use Revision;
use RuntimeException;
+use StatusValue;
use stdClass;
use Title;
+use Traversable;
use User;
use WANObjectCache;
use Wikimedia\Assert\Assert;
return $rev;
}
+ /**
+ * Construct a RevisionRecord instance for each row in $rows,
+ * and return them as an associative array indexed by revision ID.
+ * @param Traversable|array $rows the rows to construct revision records from
+ * @param array $options Supports the following options:
+ * 'slots' - whether metadata about revision slots should be
+ * loaded immediately. Supports falsy or truthy value as well
+ * as an explicit list of slot role names.
+ * 'content'- whether the actual content of the slots should be
+ * preloaded. TODO: no supported yet.
+ * @param int $queryFlags
+ * @param Title|null $title
+ * @return StatusValue a status with a RevisionRecord[] of successfully fetched revisions
+ * and an array of errors for the revisions failed to fetch.
+ */
+ public function newRevisionsFromBatch(
+ $rows,
+ array $options = [],
+ $queryFlags = 0,
+ Title $title = null
+ ) {
+ $result = new StatusValue();
+
+ $rowsByRevId = [];
+ $pageIds = [];
+ $titlesByPageId = [];
+ foreach ( $rows as $row ) {
+ if ( isset( $rowsByRevId[$row->rev_id] ) ) {
+ throw new InvalidArgumentException( "Duplicate rows in newRevisionsFromBatch {$row->rev_id}" );
+ }
+ if ( $title && $row->rev_page != $title->getArticleID() ) {
+ throw new InvalidArgumentException(
+ "Revision {$row->rev_id} doesn't belong to page {$title->getArticleID()}"
+ );
+ }
+ $pageIds[] = $row->rev_page;
+ $rowsByRevId[$row->rev_id] = $row;
+ }
+
+ if ( empty( $rowsByRevId ) ) {
+ $result->setResult( true, [] );
+ return $result;
+ }
+
+ // If the title is not supplied, batch-fetch Title objects.
+ if ( $title ) {
+ $titlesByPageId[$title->getArticleID()] = $title;
+ } else {
+ $pageIds = array_unique( $pageIds );
+ foreach ( Title::newFromIDs( $pageIds ) as $t ) {
+ $titlesByPageId[$t->getArticleID()] = $t;
+ }
+ }
+
+ if ( !isset( $options['slots'] ) || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+ $result->setResult( true,
+ array_map( function ( $row ) use ( $queryFlags, $titlesByPageId, $result ) {
+ try {
+ return $this->newRevisionFromRow(
+ $row,
+ $queryFlags,
+ $titlesByPageId[$row->rev_page]
+ );
+ } catch ( MWException $e ) {
+ $result->warning( 'internalerror', $e->getMessage() );
+ return null;
+ }
+ }, $rowsByRevId )
+ );
+ return $result;
+ }
+
+ $slotQueryConds = [ 'slot_revision_id' => array_keys( $rowsByRevId ) ];
+ if ( is_array( $options['slots'] ) ) {
+ $slotQueryConds['slot_role_id'] = array_map( function ( $slot_name ) {
+ return $this->slotRoleStore->getId( $slot_name );
+ }, $options['slots'] );
+ }
+
+ // TODO: Support optional fetching of the content
+ $queryInfo = self::getSlotsQueryInfo( [ 'content' ] );
+ $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+ $slotRows = $db->select(
+ $queryInfo['tables'],
+ $queryInfo['fields'],
+ $slotQueryConds,
+ __METHOD__,
+ [],
+ $queryInfo['joins']
+ );
+
+ $slotRowsByRevId = [];
+ foreach ( $slotRows as $slotRow ) {
+ $slotRowsByRevId[$slotRow->slot_revision_id][] = $slotRow;
+ }
+ $result->setResult( true, array_map( function ( $row ) use
+ ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $result ) {
+ if ( !isset( $slotRowsByRevId[$row->rev_id] ) ) {
+ $result->warning(
+ 'internalerror',
+ "Couldn't find slots for rev {$row->rev_id}"
+ );
+ return null;
+ }
+ try {
+ return $this->newRevisionFromRowAndSlots(
+ $row,
+ $slotRowsByRevId[$row->rev_id],
+ $queryFlags,
+ $titlesByPageId[$row->rev_page]
+ );
+ } catch ( MWException $e ) {
+ $result->warning( 'internalerror', $e->getMessage() );
+ return null;
+ }
+ }, $rowsByRevId ) );
+ return $result;
+ }
+
/**
* Constructs a new MutableRevisionRecord based on the given associative array following
* the MW1.29 convention for the Revision constructor.
return [ 'slot_revision_id' => $revId ];
}
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
+ * @throws \MWException
+ */
+ public function testNewRevisionsFromBatch_error() {
+ $page = $this->getTestPage();
+ $text = __METHOD__ . 'b-ä';
+ /** @var Revision $rev1 */
+ $rev1 = $page->doEditContent(
+ new WikitextContent( $text . '1' ),
+ __METHOD__ . 'b',
+ 0,
+ false,
+ $this->getTestUser()->getUser()
+ )->value['revision'];
+ $invalidRow = $this->revisionToRow( $rev1 );
+ $invalidRow->rev_id = 100500;
+ $result = MediaWikiServices::getInstance()->getRevisionStore()
+ ->newRevisionsFromBatch(
+ [ $this->revisionToRow( $rev1 ), $invalidRow ],
+ [
+ 'slots' => [ SlotRecord::MAIN ],
+ 'content' => true
+ ]
+ );
+ $this->assertFalse( $result->isGood() );
+ $this->assertNotEmpty( $result->getErrors() );
+ $records = $result->getValue();
+ $this->assertRevisionRecordMatchesRevision( $rev1, $records[$rev1->getId()] );
+ $this->assertSame( $text . '1',
+ $records[$rev1->getId()]->getContent( SlotRecord::MAIN )->serialize() );
+ $this->assertEquals( $page->getTitle()->getDBkey(),
+ $records[$rev1->getId()]->getPageAsLinkTarget()->getDBkey() );
+ $this->assertNull( $records[$invalidRow->rev_id] );
+ $this->assertSame( [ [
+ 'type' => 'warning',
+ 'message' => 'internalerror',
+ 'params' => [
+ "Couldn't find slots for rev 100500"
+ ]
+ ] ], $result->getErrors() );
+ }
}
}
/**
+ * @param string|null $pageTitle whether to force-create a new page
* @return WikiPage
*/
- protected function getTestPage() {
- if ( $this->testPage ) {
+ protected function getTestPage( $pageTitle = null ) {
+ if ( !is_null( $pageTitle ) && $this->testPage ) {
return $this->testPage;
}
- $title = $this->getTestPageTitle();
- $this->testPage = WikiPage::factory( $title );
+ $title = is_null( $pageTitle ) ? $this->getTestPageTitle() : Title::newFromText( $pageTitle );
+ $page = WikiPage::factory( $title );
- if ( !$this->testPage->exists() ) {
+ if ( !$page->exists() ) {
// Make sure we don't write to the live db.
$this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) );
$user = static::getTestSysop()->getUser();
- $this->testPage->doEditContent(
+ $page->doEditContent(
new WikitextContent( 'UTContent-' . __CLASS__ ),
'UTPageSummary-' . __CLASS__,
EDIT_NEW | EDIT_SUPPRESS_RC,
);
}
- return $this->testPage;
+ if ( is_null( $pageTitle ) ) {
+ $this->testPage = $page;
+ }
+ return $page;
}
/**
$this->assertSame( RevisionRecord::DELETED_TEXT, $deletedAfter );
}
+ public function provideNewRevisionsFromBatchOptions() {
+ yield 'No preload slots or content, single page' => [
+ null,
+ []
+ ];
+ yield 'Preload slots and content, single page' => [
+ null,
+ [
+ 'slots' => [ SlotRecord::MAIN ],
+ 'content' => true
+ ]
+ ];
+ yield 'No preload slots or content, multiple pages' => [
+ 'Other_Page',
+ []
+ ];
+ yield 'Preload slots and content, multiple pages' => [
+ 'Other_Page',
+ [
+ 'slots' => [ SlotRecord::MAIN ],
+ 'content' => true
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider provideNewRevisionsFromBatchOptions
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
+ * @param string|null $otherPageTitle
+ * @param array|null $options
+ * @throws \MWException
+ */
+ public function testNewRevisionsFromBatch_preloadContent(
+ $otherPageTitle = null,
+ array $options = []
+ ) {
+ $page1 = $this->getTestPage();
+ $text = __METHOD__ . 'b-ä';
+ /** @var Revision $rev1 */
+ $rev1 = $page1->doEditContent(
+ new WikitextContent( $text . '1' ),
+ __METHOD__ . 'b',
+ 0,
+ false,
+ $this->getTestUser()->getUser()
+ )->value['revision'];
+ $page2 = $this->getTestPage( $otherPageTitle );
+ /** @var Revision $rev2 */
+ $rev2 = $page2->doEditContent(
+ new WikitextContent( $text . '2' ),
+ __METHOD__ . 'b',
+ 0,
+ false,
+ $this->getTestUser()->getUser()
+ )->value['revision'];
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $result = $store->newRevisionsFromBatch(
+ [ $this->revisionToRow( $rev1 ), $this->revisionToRow( $rev2 ) ],
+ $options
+ );
+ $this->assertTrue( $result->isGood() );
+ $this->assertEmpty( $result->getErrors() );
+ $records = $result->getValue();
+ $this->assertRevisionRecordMatchesRevision( $rev1, $records[$rev1->getId()] );
+ $this->assertRevisionRecordMatchesRevision( $rev2, $records[$rev2->getId()] );
+
+ $this->assertSame( $text . '1',
+ $records[$rev1->getId()]->getContent( SlotRecord::MAIN )->serialize() );
+ $this->assertSame( $text . '2',
+ $records[$rev2->getId()]->getContent( SlotRecord::MAIN )->serialize() );
+ $this->assertEquals( $page1->getTitle()->getDBkey(),
+ $records[$rev1->getId()]->getPageAsLinkTarget()->getDBkey() );
+ $this->assertEquals( $page2->getTitle()->getDBkey(),
+ $records[$rev2->getId()]->getPageAsLinkTarget()->getDBkey() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch
+ */
+ public function testNewRevisionsFromBatch_emptyBatch() {
+ $result = MediaWikiServices::getInstance()->getRevisionStore()
+ ->newRevisionsFromBatch(
+ [],
+ [
+ 'slots' => [ SlotRecord::MAIN ],
+ 'content' => true
+ ]
+ );
+ $this->assertTrue( $result->isGood() );
+ $this->assertEmpty( $result->getValue() );
+ $this->assertEmpty( $result->getErrors() );
+ }
}