combinerCallback = function ( RenderedRevision $rr, array $hints = [] ) {
return $this->combineOutput( $rr, $hints );
};
}
private function combineOutput( RenderedRevision $rrev, array $hints = [] ) {
// NOTE: the is a slightly simplified version of RevisionRenderer::combineSlotOutput
$withHtml = $hints['generate-html'] ?? true;
$revision = $rrev->getRevision();
$slots = $revision->getSlots()->getSlots();
$combinedOutput = new ParserOutput( null );
$slotOutput = [];
foreach ( $slots as $role => $slot ) {
$out = $rrev->getSlotParserOutput( $role, $hints );
$slotOutput[$role] = $out;
$combinedOutput->mergeInternalMetaDataFrom( $out );
$combinedOutput->mergeTrackingMetaDataFrom( $out );
}
if ( $withHtml ) {
$html = '';
/** @var ParserOutput $out */
foreach ( $slotOutput as $role => $out ) {
if ( $html !== '' ) {
// skip header for the first slot
$html .= "(($role))";
}
$html .= $out->getRawText();
$combinedOutput->mergeHtmlMetaDataFrom( $out );
}
$combinedOutput->setText( $html );
}
return $combinedOutput;
}
/**
* @param $articleId
* @param $revisionId
* @return Title
*/
private function getMockTitle( $articleId, $revisionId ) {
/** @var Title|MockObject $mock */
$mock = $this->getMockBuilder( Title::class )
->disableOriginalConstructor()
->getMock();
$mock->expects( $this->any() )
->method( 'getNamespace' )
->will( $this->returnValue( NS_MAIN ) );
$mock->expects( $this->any() )
->method( 'getText' )
->will( $this->returnValue( 'RenderTestPage' ) );
$mock->expects( $this->any() )
->method( 'getPrefixedText' )
->will( $this->returnValue( 'RenderTestPage' ) );
$mock->expects( $this->any() )
->method( 'getDBkey' )
->will( $this->returnValue( 'RenderTestPage' ) );
$mock->expects( $this->any() )
->method( 'getArticleID' )
->will( $this->returnValue( $articleId ) );
$mock->expects( $this->any() )
->method( 'getLatestRevId' )
->will( $this->returnValue( $revisionId ) );
$mock->expects( $this->any() )
->method( 'getContentModel' )
->will( $this->returnValue( CONTENT_MODEL_WIKITEXT ) );
$mock->expects( $this->any() )
->method( 'getPageLanguage' )
->will( $this->returnValue( Language::factory( 'en' ) ) );
$mock->expects( $this->any() )
->method( 'isContentPage' )
->will( $this->returnValue( true ) );
$mock->expects( $this->any() )
->method( 'equals' )
->willReturnCallback( function ( Title $other ) use ( $mock ) {
return $mock->getPrefixedText() === $other->getPrefixedText();
} );
$mock->expects( $this->any() )
->method( 'userCan' )
->willReturnCallback( function ( $perm, User $user ) use ( $mock ) {
return $user->isAllowed( $perm );
} );
return $mock;
}
/**
* @param string $class
* @param Title $title
* @param null|int $id
* @param int $visibility
* @return RevisionRecord
*/
private function getMockRevision(
$class,
$title,
$id = null,
$visibility = 0,
array $content = null
) {
$frank = new UserIdentityValue( 9, 'Frank', 0 );
if ( !$content ) {
$text = "";
$text .= "* page:{{PAGENAME}}!\n";
$text .= "* rev:{{REVISIONID}}!\n";
$text .= "* user:{{REVISIONUSER}}!\n";
$text .= "* time:{{REVISIONTIMESTAMP}}!\n";
$text .= "* [[Link It]]\n";
$content = [ 'main' => new WikitextContent( $text ) ];
}
/** @var MockObject|RevisionRecord $mock */
$mock = $this->getMockBuilder( $class )
->disableOriginalConstructor()
->setMethods( [
'getId',
'getPageId',
'getPageAsLinkTarget',
'getUser',
'getVisibility',
'getTimestamp',
] )->getMock();
$mock->method( 'getId' )->willReturn( $id );
$mock->method( 'getPageId' )->willReturn( $title->getArticleID() );
$mock->method( 'getPageAsLinkTarget' )->willReturn( $title );
$mock->method( 'getUser' )->willReturn( $frank );
$mock->method( 'getVisibility' )->willReturn( $visibility );
$mock->method( 'getTimestamp' )->willReturn( '20180101000003' );
/** @var object $mockAccess */
$mockAccess = TestingAccessWrapper::newFromObject( $mock );
$mockAccess->mSlots = new MutableRevisionSlots();
foreach ( $content as $role => $cnt ) {
$mockAccess->mSlots->setContent( $role, $cnt );
}
return $mock;
}
public function testGetRevisionParserOutput_new() {
$title = $this->getMockTitle( 0, 21 );
$rev = $this->getMockRevision( RevisionStoreRecord::class, $title );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
$this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( 'page:RenderTestPage!', $html );
$this->assertContains( 'user:Frank!', $html );
$this->assertContains( 'time:20180101000003!', $html );
}
public function testGetRevisionParserOutput_previewWithSelfTransclusion() {
$title = $this->getMockTitle( 0, 21 );
$name = $title->getPrefixedText();
$text = "(ONE)(TWO)#{{:$name}}#";
$content = [
'main' => new WikitextContent( $text )
];
$rev = $this->getMockRevision( RevisionStoreRecord::class, $title, null, 0, $content );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( '(ONE)#(ONE)(TWO)#', $html );
}
public function testGetRevisionParserOutput_current() {
$title = $this->getMockTitle( 7, 21 );
$rev = $this->getMockRevision( RevisionStoreRecord::class, $title, 21 );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
$this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( 'page:RenderTestPage!', $html );
$this->assertContains( 'rev:21!', $html );
$this->assertContains( 'user:Frank!', $html );
$this->assertContains( 'time:20180101000003!', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
}
public function testGetRevisionParserOutput_old() {
$title = $this->getMockTitle( 7, 21 );
$rev = $this->getMockRevision( RevisionStoreRecord::class, $title, 11 );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
$this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( 'page:RenderTestPage!', $html );
$this->assertContains( 'rev:11!', $html );
$this->assertContains( 'user:Frank!', $html );
$this->assertContains( 'time:20180101000003!', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
}
public function testGetRevisionParserOutput_archive() {
$title = $this->getMockTitle( 7, 21 );
$rev = $this->getMockRevision( RevisionArchiveRecord::class, $title, 11 );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
$this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( 'page:RenderTestPage!', $html );
$this->assertContains( 'rev:11!', $html );
$this->assertContains( 'user:Frank!', $html );
$this->assertContains( 'time:20180101000003!', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
}
public function testGetRevisionParserOutput_suppressed() {
$title = $this->getMockTitle( 7, 21 );
$rev = $this->getMockRevision(
RevisionStoreRecord::class,
$title,
11,
RevisionRecord::DELETED_TEXT
);
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
$this->setExpectedException( SuppressedDataException::class );
$rr->getRevisionParserOutput();
}
public function testGetRevisionParserOutput_privileged() {
$title = $this->getMockTitle( 7, 21 );
$rev = $this->getMockRevision(
RevisionStoreRecord::class,
$title,
11,
RevisionRecord::DELETED_TEXT
);
$options = ParserOptions::newCanonical( 'canonical' );
$sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
$rr = new RenderedRevision(
$title,
$rev,
$options,
$this->combinerCallback,
RevisionRecord::FOR_THIS_USER,
$sysop
);
$this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
// Suppressed content should be visible for sysops
$this->assertContains( 'page:RenderTestPage!', $html );
$this->assertContains( 'rev:11!', $html );
$this->assertContains( 'user:Frank!', $html );
$this->assertContains( 'time:20180101000003!', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
}
public function testGetRevisionParserOutput_raw() {
$title = $this->getMockTitle( 7, 21 );
$rev = $this->getMockRevision(
RevisionStoreRecord::class,
$title,
11,
RevisionRecord::DELETED_TEXT
);
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision(
$title,
$rev,
$options,
$this->combinerCallback,
RevisionRecord::RAW
);
$this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
$this->assertSame( $rev, $rr->getRevision() );
$this->assertSame( $options, $rr->getOptions() );
$html = $rr->getRevisionParserOutput()->getText();
// Suppressed content should be visible for sysops
$this->assertContains( 'page:RenderTestPage!', $html );
$this->assertContains( 'rev:11!', $html );
$this->assertContains( 'user:Frank!', $html );
$this->assertContains( 'time:20180101000003!', $html );
$this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
}
public function testGetRevisionParserOutput_multi() {
$content = [
'main' => new WikitextContent( '[[Kittens]]' ),
'aux' => new WikitextContent( '[[Goats]]' ),
];
$title = $this->getMockTitle( 7, 21 );
$rev = $this->getMockRevision( RevisionStoreRecord::class, $title, 11, 0, $content );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
$combinedOutput = $rr->getRevisionParserOutput();
$mainOutput = $rr->getSlotParserOutput( SlotRecord::MAIN );
$auxOutput = $rr->getSlotParserOutput( 'aux' );
$combinedHtml = $combinedOutput->getText();
$mainHtml = $mainOutput->getText();
$auxHtml = $auxOutput->getText();
$this->assertContains( 'Kittens', $mainHtml );
$this->assertContains( 'Goats', $auxHtml );
$this->assertNotContains( 'Goats', $mainHtml );
$this->assertNotContains( 'Kittens', $auxHtml );
$this->assertContains( 'Kittens', $combinedHtml );
$this->assertContains( 'Goats', $combinedHtml );
$this->assertContains( 'aux', $combinedHtml, 'slot section header' );
$combinedLinks = $combinedOutput->getLinks();
$mainLinks = $mainOutput->getLinks();
$auxLinks = $auxOutput->getLinks();
$this->assertTrue( isset( $combinedLinks[NS_MAIN]['Kittens'] ), 'links from main slot' );
$this->assertTrue( isset( $combinedLinks[NS_MAIN]['Goats'] ), 'links from aux slot' );
$this->assertFalse( isset( $mainLinks[NS_MAIN]['Goats'] ), 'no aux links in main' );
$this->assertFalse( isset( $auxLinks[NS_MAIN]['Kittens'] ), 'no main links in aux' );
}
public function testGetRevisionParserOutput_incompleteNoId() {
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$text = "";
$text .= "* page:{{PAGENAME}}!\n";
$text .= "* rev:{{REVISIONID}}!\n";
$text .= "* user:{{REVISIONUSER}}!\n";
$text .= "* time:{{REVISIONTIMESTAMP}}!\n";
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
// MutableRevisionRecord without ID should be used by the parser.
// USeful for fake
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( 'page:RenderTestPage!', $html );
$this->assertContains( 'rev:!', $html );
$this->assertContains( 'user:!', $html );
$this->assertContains( 'time:!', $html );
}
public function testGetRevisionParserOutput_incompleteWithId() {
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setId( 21 );
$text = "";
$text .= "* page:{{PAGENAME}}!\n";
$text .= "* rev:{{REVISIONID}}!\n";
$text .= "* user:{{REVISIONUSER}}!\n";
$text .= "* time:{{REVISIONTIMESTAMP}}!\n";
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$actualRevision = $this->getMockRevision(
RevisionStoreRecord::class,
$title,
21,
RevisionRecord::DELETED_TEXT
);
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
// MutableRevisionRecord with ID should not be used by the parser,
// revision should be loaded instead!
$revisionStore = $this->getMockBuilder( RevisionStore::class )
->disableOriginalConstructor()
->getMock();
$revisionStore->expects( $this->once() )
->method( 'getKnownCurrentRevision' )
->with( $title, 0 )
->willReturn( $actualRevision );
$this->setService( 'RevisionStore', $revisionStore );
$html = $rr->getRevisionParserOutput()->getText();
$this->assertContains( 'page:RenderTestPage!', $html );
$this->assertContains( 'rev:21!', $html );
$this->assertContains( 'user:Frank!', $html );
$this->assertContains( 'time:20180101000003!', $html );
}
public function testNoHtml() {
/** @var MockObject|Content $mockContent */
$mockContent = $this->getMockBuilder( WikitextContent::class )
->setMethods( [ 'getParserOutput' ] )
->setConstructorArgs( [ 'Whatever' ] )
->getMock();
$mockContent->method( 'getParserOutput' )
->willReturnCallback( function ( Title $title, $revId = null,
ParserOptions $options = null, $generateHtml = true
) {
if ( !$generateHtml ) {
return new ParserOutput( null );
} else {
$this->fail( 'Should not be called with $generateHtml == true' );
return null; // never happens, make analyzer happy
}
} );
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$rev->setContent( SlotRecord::MAIN, $mockContent );
$rev->setContent( 'aux', $mockContent );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
$output = $rr->getSlotParserOutput( SlotRecord::MAIN, [ 'generate-html' => false ] );
$this->assertFalse( $output->hasText(), 'hasText' );
$output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
$this->assertFalse( $output->hasText(), 'hasText' );
}
public function testUpdateRevision() {
$title = $this->getMockTitle( 7, 21 );
$rev = new MutableRevisionRecord( $title );
$text = "";
$text .= "* page:{{PAGENAME}}!\n";
$text .= "* rev:{{REVISIONID}}!\n";
$text .= "* user:{{REVISIONUSER}}!\n";
$text .= "* time:{{REVISIONTIMESTAMP}}!\n";
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
$options = ParserOptions::newCanonical( 'canonical' );
$rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
$firstOutput = $rr->getRevisionParserOutput();
$mainOutput = $rr->getSlotParserOutput( SlotRecord::MAIN );
$auxOutput = $rr->getSlotParserOutput( 'aux' );
// emulate a saved revision
$savedRev = new MutableRevisionRecord( $title );
$savedRev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
$savedRev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
$savedRev->setId( 23 ); // saved, new
$savedRev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
$savedRev->setTimestamp( '20180101000003' );
$rr->updateRevision( $savedRev );
$this->assertNotSame( $mainOutput, $rr->getSlotParserOutput( SlotRecord::MAIN ), 'Reset main' );
$this->assertSame( $auxOutput, $rr->getSlotParserOutput( 'aux' ), 'Keep aux' );
$updatedOutput = $rr->getRevisionParserOutput();
$html = $updatedOutput->getText();
$this->assertNotSame( $firstOutput, $updatedOutput, 'Reset merged' );
$this->assertContains( 'page:RenderTestPage!', $html );
$this->assertContains( 'rev:23!', $html );
$this->assertContains( 'user:Frank!', $html );
$this->assertContains( 'time:20180101000003!', $html );
$this->assertContains( 'Goats', $html );
$rr->updateRevision( $savedRev ); // should do nothing
$this->assertSame( $updatedOutput, $rr->getRevisionParserOutput(), 'no more reset needed' );
}
}