combinerCallback = function ( RenderedRevision $rr, array $hints = [] ) { return $this->combineOutput( $rr, $hints ); }; $this->contentRenderer = $this->getServiceContainer()->getContentRenderer(); } 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->setRawText( $html ); } return $combinedOutput; } /** * @param string $class * @param PageIdentity $page * @param null|int $id * @param int $visibility * @param Content[]|null $content * @return RevisionRecord */ private function getMockRevision( $class, $page, $id = null, $visibility = 0, ?array $content = null ) { $frank = new UserIdentityValue( 9, 'Frank' ); if ( !$content ) { $text = ""; $text .= "* page:{{PAGENAME}}!\n"; $text .= "* rev:{{REVISIONID}}!\n"; $text .= "* user:{{REVISIONUSER}}!\n"; $text .= "* time:{{REVISIONTIMESTAMP}}!\n"; $text .= "* [[Link It]]\n"; $content = [ SlotRecord::MAIN => new WikitextContent( $text ) ]; } /** @var MockObject|RevisionRecord $mock */ $mock = $this->getMockBuilder( $class ) ->disableOriginalConstructor() ->onlyMethods( [ 'getId', 'getPageId', 'getPageAsLinkTarget', 'getPage', 'getUser', 'getVisibility', 'getTimestamp', ] )->getMock(); $mock->method( 'getId' )->willReturn( $id ); $mock->method( 'getPageId' )->willReturn( $page->getId() ); $mock->method( 'getPageAsLinkTarget' )->willReturn( TitleValue::castPageToLinkTarget( $page ) ); $mock->method( 'getPage' )->willReturn( $page ); $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 testConstructorInvalidArguments() { $rev = $this->getMockRevision( RevisionStoreRecord::class, PageIdentityValue::localIdentity( 0, NS_MAIN, __METHOD__ ) ); $options = ParserOptions::newFromAnon(); $this->expectException( InvalidArgumentException::class ); $this->expectExceptionMessage( 'User must be specified when setting audience to FOR_THIS_USER' ); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback, RevisionRecord::FOR_THIS_USER ); } public function testGetRevisionParserOutput_new() { $rev = $this->getMockRevision( RevisionStoreRecord::class, PageIdentityValue::localIdentity( 0, NS_MAIN, 'RenderTestPage' ) ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' ); $this->assertSame( $rev, $rr->getRevision() ); $this->assertSame( $options, $rr->getOptions() ); $html = $rr->getRevisionParserOutput()->getRawText(); $this->assertStringContainsString( 'page:RenderTestPage!', $html ); $this->assertStringContainsString( 'user:Frank!', $html ); $this->assertStringContainsString( 'time:20180101000003!', $html ); } public function testGetRevisionParserOutput_previewWithSelfTransclusion() { $title = PageIdentityValue::localIdentity( 0, NS_MAIN, __METHOD__ ); $name = $this->getServiceContainer()->getTitleFormatter()->getPrefixedText( $title ); $text = "(ONE)(TWO)#{{:$name}}#"; $content = [ SlotRecord::MAIN => new WikitextContent( $text ) ]; $rev = $this->getMockRevision( RevisionStoreRecord::class, $title, null, 0, $content ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); $html = $rr->getRevisionParserOutput()->getRawText(); $this->assertStringContainsString( '(ONE)#(ONE)(TWO)#', $html ); } public function testGetRevisionParserOutput_current() { $rev = $this->getMockRevision( RevisionStoreRecord::class, PageIdentityValue::localIdentity( 0, NS_MAIN, 'RenderTestPage' ), 21 ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' ); $this->assertSame( $rev, $rr->getRevision() ); $this->assertSame( $options, $rr->getOptions() ); $html = $rr->getRevisionParserOutput()->getRawText(); $this->assertStringContainsString( 'page:RenderTestPage!', $html ); $this->assertStringContainsString( 'rev:21!', $html ); $this->assertStringContainsString( 'user:Frank!', $html ); $this->assertStringContainsString( 'time:20180101000003!', $html ); $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getRawText() ); } public function testGetRevisionParserOutput_old() { $rev = $this->getMockRevision( RevisionStoreRecord::class, PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ), 11 ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' ); $this->assertSame( $rev, $rr->getRevision() ); $this->assertSame( $options, $rr->getOptions() ); $html = $rr->getRevisionParserOutput()->getRawText(); $this->assertStringContainsString( 'page:RenderTestPage!', $html ); $this->assertStringContainsString( 'rev:11!', $html ); $this->assertStringContainsString( 'user:Frank!', $html ); $this->assertStringContainsString( 'time:20180101000003!', $html ); $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getRawText() ); } public function testGetRevisionParserOutput_archive() { $rev = $this->getMockRevision( RevisionArchiveRecord::class, PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ), 11 ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback, RevisionRecord::RAW ); $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' ); $this->assertSame( $rev, $rr->getRevision() ); $this->assertSame( $options, $rr->getOptions() ); $html = $rr->getRevisionParserOutput()->getRawText(); $this->assertStringContainsString( 'page:RenderTestPage!', $html ); $this->assertStringContainsString( 'rev:11!', $html ); $this->assertStringContainsString( 'user:Frank!', $html ); $this->assertStringContainsString( 'time:20180101000003!', $html ); $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getRawText() ); } public function testGetRevisionParserOutput_suppressed() { $rev = $this->getMockRevision( RevisionStoreRecord::class, PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ), 11, RevisionRecord::DELETED_TEXT ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); $this->expectException( SuppressedDataException::class ); $rr->getRevisionParserOutput(); } public function testGetRevisionParserOutput_privileged() { $rev = $this->getMockRevision( RevisionStoreRecord::class, PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ), 11, RevisionRecord::DELETED_TEXT ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback, RevisionRecord::FOR_THIS_USER, $this->mockRegisteredUltimateAuthority() ); $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' ); $this->assertSame( $rev, $rr->getRevision() ); $this->assertSame( $options, $rr->getOptions() ); $html = $rr->getRevisionParserOutput()->getRawText(); // Suppressed content should be visible for sysops $this->assertStringContainsString( 'page:RenderTestPage!', $html ); $this->assertStringContainsString( 'rev:11!', $html ); $this->assertStringContainsString( 'user:Frank!', $html ); $this->assertStringContainsString( 'time:20180101000003!', $html ); $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getRawText() ); } public function testGetRevisionParserOutput_raw() { $rev = $this->getMockRevision( RevisionStoreRecord::class, PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ), 11, RevisionRecord::DELETED_TEXT ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback, RevisionRecord::RAW ); $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' ); $this->assertSame( $rev, $rr->getRevision() ); $this->assertSame( $options, $rr->getOptions() ); $html = $rr->getRevisionParserOutput()->getRawText(); // Suppressed content should be visible for sysops $this->assertStringContainsString( 'page:RenderTestPage!', $html ); $this->assertStringContainsString( 'rev:11!', $html ); $this->assertStringContainsString( 'user:Frank!', $html ); $this->assertStringContainsString( 'time:20180101000003!', $html ); $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getRawText() ); } public function testGetRevisionParserOutput_multi() { $content = [ SlotRecord::MAIN => new WikitextContent( '[[Kittens]]' ), 'aux' => new WikitextContent( '[[Goats]]' ), ]; $rev = $this->getMockRevision( RevisionStoreRecord::class, PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ), 11, 0, $content ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); $combinedOutput = $rr->getRevisionParserOutput(); $mainOutput = $rr->getSlotParserOutput( SlotRecord::MAIN ); $auxOutput = $rr->getSlotParserOutput( 'aux' ); $combinedHtml = $combinedOutput->getRawText(); $mainHtml = $mainOutput->getRawText(); $auxHtml = $auxOutput->getRawText(); $this->assertStringContainsString( 'Kittens', $mainHtml ); $this->assertStringContainsString( 'Goats', $auxHtml ); $this->assertStringNotContainsString( 'Goats', $mainHtml ); $this->assertStringNotContainsString( 'Kittens', $auxHtml ); $this->assertStringContainsString( 'Kittens', $combinedHtml ); $this->assertStringContainsString( 'Goats', $combinedHtml ); $this->assertStringContainsString( '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() { $rev = new MutableRevisionRecord( PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ) ); $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::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); // MutableRevisionRecord without ID should be used by the parser. // USeful for fake $html = $rr->getRevisionParserOutput()->getRawText(); $this->assertStringContainsString( 'page:RenderTestPage!', $html ); $this->assertStringContainsString( 'rev:!', $html ); $this->assertStringContainsString( 'user:!', $html ); // Per parser docs, if revision object does not contain a timestamp // then parser uses current time. Hence don't expect time to be // empty or a specific time. $this->assertStringContainsString( 'time:2', $html ); } public function testGetRevisionParserOutput_incompleteWithId() { $page = PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ); $rev = new MutableRevisionRecord( $page ); $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, $page, 21, RevisionRecord::DELETED_TEXT ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); // MutableRevisionRecord with ID should not be used by the parser, // revision should be loaded instead! $revisionStore = $this->createMock( RevisionStore::class ); $revisionStore->expects( $this->once() ) ->method( 'getKnownCurrentRevision' ) ->willReturn( $actualRevision ); $this->setService( 'RevisionStore', $revisionStore ); $html = $rr->getRevisionParserOutput()->getRawText(); $this->assertStringContainsString( 'page:RenderTestPage!', $html ); $this->assertStringContainsString( 'rev:21!', $html ); $this->assertStringContainsString( 'user:Frank!', $html ); $this->assertStringContainsString( 'time:20180101000003!', $html ); } public function testSetRevisionParserOutput() { $rev = $this->getMockRevision( RevisionStoreRecord::class, PageIdentityValue::localIdentity( 3, NS_MAIN, 'RenderTestPage' ) ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); $output = new ParserOutput( 'Kittens' ); $rr->setRevisionParserOutput( $output ); $this->assertSame( $output, $rr->getRevisionParserOutput() ); $this->assertSame( 'Kittens', $rr->getRevisionParserOutput()->getRawText() ); $this->assertSame( $output, $rr->getSlotParserOutput( SlotRecord::MAIN ) ); $this->assertSame( 'Kittens', $rr->getSlotParserOutput( SlotRecord::MAIN ) ->getRawText() ); } public function testNoHtml() { $content = new WikitextContent( 'whatever' ); /** @var MockObject|ContentRenderer $mockContentRenderer */ $mockContentRenderer = $this->getMockBuilder( ContentRenderer::class ) ->onlyMethods( [ 'getParserOutput' ] ) ->disableOriginalConstructor() ->getMock(); $mockContentRenderer->method( 'getParserOutput' ) ->willReturnCallback( function ( Content $content, PageReference $page, $revId = null, ?ParserOptions $options = null, $hints = [] ) { if ( is_bool( $hints ) ) { $hints = [ 'generate-html' => $hints ]; } $generateHtml = $hints['generate-html'] ?? true; if ( !$generateHtml ) { return new ParserOutput( null ); } else { $this->fail( 'Should not be called with $generateHtml == true' ); return null; // never happens, make analyzer happy } } ); $rev = new MutableRevisionRecord( PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ) ); $rev->setContent( SlotRecord::MAIN, $content ); $rev->setContent( 'aux', $content ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $mockContentRenderer, $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() { $page = PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ); $rev = new MutableRevisionRecord( $page ); $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::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); $firstOutput = $rr->getRevisionParserOutput(); $mainOutput = $rr->getSlotParserOutput( SlotRecord::MAIN ); $auxOutput = $rr->getSlotParserOutput( 'aux' ); // emulate a saved revision $savedRev = new MutableRevisionRecord( $page ); $savedRev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) ); $savedRev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) ); $savedRev->setId( 23 ); // saved, new $savedRev->setUser( new UserIdentityValue( 9, 'Frank' ) ); $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->getRawText(); $this->assertNotSame( $firstOutput, $updatedOutput, 'Reset merged' ); $this->assertStringContainsString( 'page:RenderTestPage!', $html ); $this->assertStringContainsString( 'rev:23!', $html ); $this->assertStringContainsString( 'user:Frank!', $html ); $this->assertStringContainsString( 'time:20180101000003!', $html ); $this->assertStringContainsString( 'Goats', $html ); $rr->updateRevision( $savedRev ); // should do nothing $this->assertSame( $updatedOutput, $rr->getRevisionParserOutput(), 'no more reset needed' ); } public function testUpdateRevision_revIdSet() { $page = PageIdentityValue::localIdentity( 7, NS_MAIN, 'RenderTestPage' ); $rev = new MutableRevisionRecord( $page ); $rev->setId( 123 ); $rev->setContent( SlotRecord::MAIN, new WikitextContent( 'FooBar' ) ); $options = ParserOptions::newFromAnon(); $rr = new RenderedRevision( $rev, $options, $this->contentRenderer, $this->combinerCallback ); $newRev = new MutableRevisionRecord( $page ); $newRev->setId( 321 ); // Different $newRev->setContent( SlotRecord::MAIN, new WikitextContent( 'FooBar' ) ); $this->expectException( LogicException::class ); $this->expectExceptionMessage( 'RenderedRevision already has a revision with ID 123, ' . 'can\'t update to revision with ID 321' ); $rr->updateRevision( $newRev ); } }