3 namespace MediaWiki\Tests\Revision
;
7 use MediaWiki\Revision\MutableRevisionRecord
;
8 use MediaWiki\Revision\MutableRevisionSlots
;
9 use MediaWiki\Revision\RenderedRevision
;
10 use MediaWiki\Revision\RevisionArchiveRecord
;
11 use MediaWiki\Revision\RevisionRecord
;
12 use MediaWiki\Revision\RevisionStore
;
13 use MediaWiki\Revision\RevisionStoreRecord
;
14 use MediaWiki\Revision\SlotRecord
;
15 use MediaWiki\Revision\SuppressedDataException
;
16 use MediaWikiTestCase
;
17 use MediaWiki\User\UserIdentityValue
;
20 use PHPUnit\Framework\MockObject\MockObject
;
23 use Wikimedia\TestingAccessWrapper
;
27 * @covers \MediaWiki\Revision\RenderedRevision
29 class RenderedRevisionTest
extends MediaWikiTestCase
{
32 private $combinerCallback;
34 public function setUp() {
37 $this->combinerCallback
= function ( RenderedRevision
$rr, array $hints = [] ) {
38 return $this->combineOutput( $rr, $hints );
42 private function combineOutput( RenderedRevision
$rrev, array $hints = [] ) {
43 // NOTE: the is a slightly simplified version of RevisionRenderer::combineSlotOutput
45 $withHtml = $hints['generate-html'] ??
true;
47 $revision = $rrev->getRevision();
48 $slots = $revision->getSlots()->getSlots();
50 $combinedOutput = new ParserOutput( null );
52 foreach ( $slots as $role => $slot ) {
53 $out = $rrev->getSlotParserOutput( $role, $hints );
54 $slotOutput[$role] = $out;
56 $combinedOutput->mergeInternalMetaDataFrom( $out );
57 $combinedOutput->mergeTrackingMetaDataFrom( $out );
62 /** @var ParserOutput $out */
63 foreach ( $slotOutput as $role => $out ) {
66 // skip header for the first slot
70 $html .= $out->getRawText();
71 $combinedOutput->mergeHtmlMetaDataFrom( $out );
74 $combinedOutput->setText( $html );
77 return $combinedOutput;
85 private function getMockTitle( $articleId, $revisionId ) {
86 /** @var Title|MockObject $mock */
87 $mock = $this->getMockBuilder( Title
::class )
88 ->disableOriginalConstructor()
90 $mock->expects( $this->any() )
91 ->method( 'getNamespace' )
92 ->will( $this->returnValue( NS_MAIN
) );
93 $mock->expects( $this->any() )
95 ->will( $this->returnValue( 'RenderTestPage' ) );
96 $mock->expects( $this->any() )
97 ->method( 'getPrefixedText' )
98 ->will( $this->returnValue( 'RenderTestPage' ) );
99 $mock->expects( $this->any() )
100 ->method( 'getDBkey' )
101 ->will( $this->returnValue( 'RenderTestPage' ) );
102 $mock->expects( $this->any() )
103 ->method( 'getArticleID' )
104 ->will( $this->returnValue( $articleId ) );
105 $mock->expects( $this->any() )
106 ->method( 'getLatestRevId' )
107 ->will( $this->returnValue( $revisionId ) );
108 $mock->expects( $this->any() )
109 ->method( 'getContentModel' )
110 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT
) );
111 $mock->expects( $this->any() )
112 ->method( 'getPageLanguage' )
113 ->will( $this->returnValue( Language
::factory( 'en' ) ) );
114 $mock->expects( $this->any() )
115 ->method( 'isContentPage' )
116 ->will( $this->returnValue( true ) );
117 $mock->expects( $this->any() )
119 ->willReturnCallback( function ( Title
$other ) use ( $mock ) {
120 return $mock->getPrefixedText() === $other->getPrefixedText();
122 $mock->expects( $this->any() )
123 ->method( 'userCan' )
124 ->willReturnCallback( function ( $perm, User
$user ) use ( $mock ) {
125 return $user->isAllowed( $perm );
132 * @param string $class
133 * @param Title $title
134 * @param null|int $id
135 * @param int $visibility
136 * @return RevisionRecord
138 private function getMockRevision(
143 array $content = null
145 $frank = new UserIdentityValue( 9, 'Frank', 0 );
149 $text .= "* page:{{PAGENAME}}!\n";
150 $text .= "* rev:{{REVISIONID}}!\n";
151 $text .= "* user:{{REVISIONUSER}}!\n";
152 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
153 $text .= "* [[Link It]]\n";
155 $content = [ 'main' => new WikitextContent( $text ) ];
158 /** @var MockObject|RevisionRecord $mock */
159 $mock = $this->getMockBuilder( $class )
160 ->disableOriginalConstructor()
164 'getPageAsLinkTarget',
170 $mock->method( 'getId' )->willReturn( $id );
171 $mock->method( 'getPageId' )->willReturn( $title->getArticleID() );
172 $mock->method( 'getPageAsLinkTarget' )->willReturn( $title );
173 $mock->method( 'getUser' )->willReturn( $frank );
174 $mock->method( 'getVisibility' )->willReturn( $visibility );
175 $mock->method( 'getTimestamp' )->willReturn( '20180101000003' );
177 /** @var object $mockAccess */
178 $mockAccess = TestingAccessWrapper
::newFromObject( $mock );
179 $mockAccess->mSlots
= new MutableRevisionSlots();
181 foreach ( $content as $role => $cnt ) {
182 $mockAccess->mSlots
->setContent( $role, $cnt );
188 public function testGetRevisionParserOutput_new() {
189 $title = $this->getMockTitle( 0, 21 );
190 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title );
192 $options = ParserOptions
::newCanonical( 'canonical' );
193 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
195 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
197 $this->assertSame( $rev, $rr->getRevision() );
198 $this->assertSame( $options, $rr->getOptions() );
200 $html = $rr->getRevisionParserOutput()->getText();
202 $this->assertContains( 'page:RenderTestPage!', $html );
203 $this->assertContains( 'user:Frank!', $html );
204 $this->assertContains( 'time:20180101000003!', $html );
207 public function testGetRevisionParserOutput_previewWithSelfTransclusion() {
208 $title = $this->getMockTitle( 0, 21 );
209 $name = $title->getPrefixedText();
211 $text = "(ONE)<includeonly>(TWO)</includeonly><noinclude>#{{:$name}}#</noinclude>";
214 'main' => new WikitextContent( $text )
217 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title, null, 0, $content );
219 $options = ParserOptions
::newCanonical( 'canonical' );
220 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
222 $html = $rr->getRevisionParserOutput()->getText();
223 $this->assertContains( '(ONE)#(ONE)(TWO)#', $html );
226 public function testGetRevisionParserOutput_current() {
227 $title = $this->getMockTitle( 7, 21 );
228 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title, 21 );
230 $options = ParserOptions
::newCanonical( 'canonical' );
231 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
233 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
235 $this->assertSame( $rev, $rr->getRevision() );
236 $this->assertSame( $options, $rr->getOptions() );
238 $html = $rr->getRevisionParserOutput()->getText();
240 $this->assertContains( 'page:RenderTestPage!', $html );
241 $this->assertContains( 'rev:21!', $html );
242 $this->assertContains( 'user:Frank!', $html );
243 $this->assertContains( 'time:20180101000003!', $html );
245 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
248 public function testGetRevisionParserOutput_old() {
249 $title = $this->getMockTitle( 7, 21 );
250 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title, 11 );
252 $options = ParserOptions
::newCanonical( 'canonical' );
253 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
255 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
257 $this->assertSame( $rev, $rr->getRevision() );
258 $this->assertSame( $options, $rr->getOptions() );
260 $html = $rr->getRevisionParserOutput()->getText();
262 $this->assertContains( 'page:RenderTestPage!', $html );
263 $this->assertContains( 'rev:11!', $html );
264 $this->assertContains( 'user:Frank!', $html );
265 $this->assertContains( 'time:20180101000003!', $html );
267 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
270 public function testGetRevisionParserOutput_archive() {
271 $title = $this->getMockTitle( 7, 21 );
272 $rev = $this->getMockRevision( RevisionArchiveRecord
::class, $title, 11 );
274 $options = ParserOptions
::newCanonical( 'canonical' );
275 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
277 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
279 $this->assertSame( $rev, $rr->getRevision() );
280 $this->assertSame( $options, $rr->getOptions() );
282 $html = $rr->getRevisionParserOutput()->getText();
284 $this->assertContains( 'page:RenderTestPage!', $html );
285 $this->assertContains( 'rev:11!', $html );
286 $this->assertContains( 'user:Frank!', $html );
287 $this->assertContains( 'time:20180101000003!', $html );
289 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
292 public function testGetRevisionParserOutput_suppressed() {
293 $title = $this->getMockTitle( 7, 21 );
294 $rev = $this->getMockRevision(
295 RevisionStoreRecord
::class,
298 RevisionRecord
::DELETED_TEXT
301 $options = ParserOptions
::newCanonical( 'canonical' );
302 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
304 $this->setExpectedException( SuppressedDataException
::class );
305 $rr->getRevisionParserOutput();
308 public function testGetRevisionParserOutput_privileged() {
309 $title = $this->getMockTitle( 7, 21 );
310 $rev = $this->getMockRevision(
311 RevisionStoreRecord
::class,
314 RevisionRecord
::DELETED_TEXT
317 $options = ParserOptions
::newCanonical( 'canonical' );
318 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
319 $rr = new RenderedRevision(
323 $this->combinerCallback
,
324 RevisionRecord
::FOR_THIS_USER
,
328 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
330 $this->assertSame( $rev, $rr->getRevision() );
331 $this->assertSame( $options, $rr->getOptions() );
333 $html = $rr->getRevisionParserOutput()->getText();
335 // Suppressed content should be visible for sysops
336 $this->assertContains( 'page:RenderTestPage!', $html );
337 $this->assertContains( 'rev:11!', $html );
338 $this->assertContains( 'user:Frank!', $html );
339 $this->assertContains( 'time:20180101000003!', $html );
341 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
344 public function testGetRevisionParserOutput_raw() {
345 $title = $this->getMockTitle( 7, 21 );
346 $rev = $this->getMockRevision(
347 RevisionStoreRecord
::class,
350 RevisionRecord
::DELETED_TEXT
353 $options = ParserOptions
::newCanonical( 'canonical' );
354 $rr = new RenderedRevision(
358 $this->combinerCallback
,
362 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
364 $this->assertSame( $rev, $rr->getRevision() );
365 $this->assertSame( $options, $rr->getOptions() );
367 $html = $rr->getRevisionParserOutput()->getText();
369 // Suppressed content should be visible for sysops
370 $this->assertContains( 'page:RenderTestPage!', $html );
371 $this->assertContains( 'rev:11!', $html );
372 $this->assertContains( 'user:Frank!', $html );
373 $this->assertContains( 'time:20180101000003!', $html );
375 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
378 public function testGetRevisionParserOutput_multi() {
380 'main' => new WikitextContent( '[[Kittens]]' ),
381 'aux' => new WikitextContent( '[[Goats]]' ),
384 $title = $this->getMockTitle( 7, 21 );
385 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title, 11, 0, $content );
387 $options = ParserOptions
::newCanonical( 'canonical' );
388 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
390 $combinedOutput = $rr->getRevisionParserOutput();
391 $mainOutput = $rr->getSlotParserOutput( SlotRecord
::MAIN
);
392 $auxOutput = $rr->getSlotParserOutput( 'aux' );
394 $combinedHtml = $combinedOutput->getText();
395 $mainHtml = $mainOutput->getText();
396 $auxHtml = $auxOutput->getText();
398 $this->assertContains( 'Kittens', $mainHtml );
399 $this->assertContains( 'Goats', $auxHtml );
400 $this->assertNotContains( 'Goats', $mainHtml );
401 $this->assertNotContains( 'Kittens', $auxHtml );
402 $this->assertContains( 'Kittens', $combinedHtml );
403 $this->assertContains( 'Goats', $combinedHtml );
404 $this->assertContains( 'aux', $combinedHtml, 'slot section header' );
406 $combinedLinks = $combinedOutput->getLinks();
407 $mainLinks = $mainOutput->getLinks();
408 $auxLinks = $auxOutput->getLinks();
409 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Kittens'] ), 'links from main slot' );
410 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Goats'] ), 'links from aux slot' );
411 $this->assertFalse( isset( $mainLinks[NS_MAIN
]['Goats'] ), 'no aux links in main' );
412 $this->assertFalse( isset( $auxLinks[NS_MAIN
]['Kittens'] ), 'no main links in aux' );
415 public function testGetRevisionParserOutput_incompleteNoId() {
416 $title = $this->getMockTitle( 7, 21 );
418 $rev = new MutableRevisionRecord( $title );
421 $text .= "* page:{{PAGENAME}}!\n";
422 $text .= "* rev:{{REVISIONID}}!\n";
423 $text .= "* user:{{REVISIONUSER}}!\n";
424 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
426 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
428 $options = ParserOptions
::newCanonical( 'canonical' );
429 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
431 // MutableRevisionRecord without ID should be used by the parser.
433 $html = $rr->getRevisionParserOutput()->getText();
435 $this->assertContains( 'page:RenderTestPage!', $html );
436 $this->assertContains( 'rev:!', $html );
437 $this->assertContains( 'user:!', $html );
438 $this->assertContains( 'time:!', $html );
441 public function testGetRevisionParserOutput_incompleteWithId() {
442 $title = $this->getMockTitle( 7, 21 );
444 $rev = new MutableRevisionRecord( $title );
448 $text .= "* page:{{PAGENAME}}!\n";
449 $text .= "* rev:{{REVISIONID}}!\n";
450 $text .= "* user:{{REVISIONUSER}}!\n";
451 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
453 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
455 $actualRevision = $this->getMockRevision(
456 RevisionStoreRecord
::class,
459 RevisionRecord
::DELETED_TEXT
462 $options = ParserOptions
::newCanonical( 'canonical' );
463 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
465 // MutableRevisionRecord with ID should not be used by the parser,
466 // revision should be loaded instead!
467 $revisionStore = $this->getMockBuilder( RevisionStore
::class )
468 ->disableOriginalConstructor()
471 $revisionStore->expects( $this->once() )
472 ->method( 'getKnownCurrentRevision' )
474 ->willReturn( $actualRevision );
476 $this->setService( 'RevisionStore', $revisionStore );
478 $html = $rr->getRevisionParserOutput()->getText();
480 $this->assertContains( 'page:RenderTestPage!', $html );
481 $this->assertContains( 'rev:21!', $html );
482 $this->assertContains( 'user:Frank!', $html );
483 $this->assertContains( 'time:20180101000003!', $html );
486 public function testNoHtml() {
487 /** @var MockObject|Content $mockContent */
488 $mockContent = $this->getMockBuilder( WikitextContent
::class )
489 ->setMethods( [ 'getParserOutput' ] )
490 ->setConstructorArgs( [ 'Whatever' ] )
492 $mockContent->method( 'getParserOutput' )
493 ->willReturnCallback( function ( Title
$title, $revId = null,
494 ParserOptions
$options = null, $generateHtml = true
496 if ( !$generateHtml ) {
497 return new ParserOutput( null );
499 $this->fail( 'Should not be called with $generateHtml == true' );
500 return null; // never happens, make analyzer happy
504 $title = $this->getMockTitle( 7, 21 );
506 $rev = new MutableRevisionRecord( $title );
507 $rev->setContent( SlotRecord
::MAIN
, $mockContent );
508 $rev->setContent( 'aux', $mockContent );
510 $options = ParserOptions
::newCanonical( 'canonical' );
511 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
513 $output = $rr->getSlotParserOutput( SlotRecord
::MAIN
, [ 'generate-html' => false ] );
514 $this->assertFalse( $output->hasText(), 'hasText' );
516 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
517 $this->assertFalse( $output->hasText(), 'hasText' );
520 public function testUpdateRevision() {
521 $title = $this->getMockTitle( 7, 21 );
523 $rev = new MutableRevisionRecord( $title );
526 $text .= "* page:{{PAGENAME}}!\n";
527 $text .= "* rev:{{REVISIONID}}!\n";
528 $text .= "* user:{{REVISIONUSER}}!\n";
529 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
531 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
532 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
534 $options = ParserOptions
::newCanonical( 'canonical' );
535 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
537 $firstOutput = $rr->getRevisionParserOutput();
538 $mainOutput = $rr->getSlotParserOutput( SlotRecord
::MAIN
);
539 $auxOutput = $rr->getSlotParserOutput( 'aux' );
541 // emulate a saved revision
542 $savedRev = new MutableRevisionRecord( $title );
543 $savedRev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
544 $savedRev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
545 $savedRev->setId( 23 ); // saved, new
546 $savedRev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
547 $savedRev->setTimestamp( '20180101000003' );
549 $rr->updateRevision( $savedRev );
551 $this->assertNotSame( $mainOutput, $rr->getSlotParserOutput( SlotRecord
::MAIN
), 'Reset main' );
552 $this->assertSame( $auxOutput, $rr->getSlotParserOutput( 'aux' ), 'Keep aux' );
554 $updatedOutput = $rr->getRevisionParserOutput();
555 $html = $updatedOutput->getText();
557 $this->assertNotSame( $firstOutput, $updatedOutput, 'Reset merged' );
558 $this->assertContains( 'page:RenderTestPage!', $html );
559 $this->assertContains( 'rev:23!', $html );
560 $this->assertContains( 'user:Frank!', $html );
561 $this->assertContains( 'time:20180101000003!', $html );
562 $this->assertContains( 'Goats', $html );
564 $rr->updateRevision( $savedRev ); // should do nothing
565 $this->assertSame( $updatedOutput, $rr->getRevisionParserOutput(), 'no more reset needed' );