3 namespace MediaWiki\Tests\Revision
;
7 use MediaWiki\MediaWikiServices
;
8 use MediaWiki\Revision\MutableRevisionRecord
;
9 use MediaWiki\Revision\MutableRevisionSlots
;
10 use MediaWiki\Revision\RenderedRevision
;
11 use MediaWiki\Revision\RevisionArchiveRecord
;
12 use MediaWiki\Revision\RevisionRecord
;
13 use MediaWiki\Revision\RevisionStore
;
14 use MediaWiki\Revision\RevisionStoreRecord
;
15 use MediaWiki\Revision\SlotRecord
;
16 use MediaWiki\Revision\SuppressedDataException
;
17 use MediaWikiTestCase
;
18 use MediaWiki\User\UserIdentityValue
;
21 use PHPUnit\Framework\MockObject\MockObject
;
24 use Wikimedia\TestingAccessWrapper
;
28 * @covers \MediaWiki\Revision\RenderedRevision
30 class RenderedRevisionTest
extends MediaWikiTestCase
{
33 private $combinerCallback;
35 public function setUp() {
38 $this->combinerCallback
= function ( RenderedRevision
$rr, array $hints = [] ) {
39 return $this->combineOutput( $rr, $hints );
43 private function combineOutput( RenderedRevision
$rrev, array $hints = [] ) {
44 // NOTE: the is a slightly simplified version of RevisionRenderer::combineSlotOutput
46 $withHtml = $hints['generate-html'] ??
true;
48 $revision = $rrev->getRevision();
49 $slots = $revision->getSlots()->getSlots();
51 $combinedOutput = new ParserOutput( null );
53 foreach ( $slots as $role => $slot ) {
54 $out = $rrev->getSlotParserOutput( $role, $hints );
55 $slotOutput[$role] = $out;
57 $combinedOutput->mergeInternalMetaDataFrom( $out );
58 $combinedOutput->mergeTrackingMetaDataFrom( $out );
63 /** @var ParserOutput $out */
64 foreach ( $slotOutput as $role => $out ) {
67 // skip header for the first slot
71 $html .= $out->getRawText();
72 $combinedOutput->mergeHtmlMetaDataFrom( $out );
75 $combinedOutput->setText( $html );
78 return $combinedOutput;
82 * @param int $articleId
83 * @param int $revisionId
86 private function getMockTitle( $articleId, $revisionId ) {
87 /** @var Title|MockObject $mock */
88 $mock = $this->getMockBuilder( Title
::class )
89 ->disableOriginalConstructor()
91 $mock->expects( $this->any() )
92 ->method( 'getNamespace' )
93 ->will( $this->returnValue( NS_MAIN
) );
94 $mock->expects( $this->any() )
96 ->will( $this->returnValue( 'RenderTestPage' ) );
97 $mock->expects( $this->any() )
98 ->method( 'getPrefixedText' )
99 ->will( $this->returnValue( 'RenderTestPage' ) );
100 $mock->expects( $this->any() )
101 ->method( 'getDBkey' )
102 ->will( $this->returnValue( 'RenderTestPage' ) );
103 $mock->expects( $this->any() )
104 ->method( 'getArticleID' )
105 ->will( $this->returnValue( $articleId ) );
106 $mock->expects( $this->any() )
107 ->method( 'getLatestRevId' )
108 ->will( $this->returnValue( $revisionId ) );
109 $mock->expects( $this->any() )
110 ->method( 'getContentModel' )
111 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT
) );
112 $mock->expects( $this->any() )
113 ->method( 'getPageLanguage' )
114 ->will( $this->returnValue( Language
::factory( 'en' ) ) );
115 $mock->expects( $this->any() )
116 ->method( 'isContentPage' )
117 ->will( $this->returnValue( true ) );
118 $mock->expects( $this->any() )
120 ->willReturnCallback( function ( Title
$other ) use ( $mock ) {
121 return $mock->getPrefixedText() === $other->getPrefixedText();
123 $mock->expects( $this->any() )
124 ->method( 'userCan' )
125 ->willReturnCallback( function ( $perm, User
$user ) use ( $mock ) {
126 return MediaWikiServices
::getInstance()
127 ->getPermissionManager()
128 ->userHasRight( $user, $perm );
135 * @param string $class
136 * @param Title $title
137 * @param null|int $id
138 * @param int $visibility
139 * @return RevisionRecord
141 private function getMockRevision(
146 array $content = null
148 $frank = new UserIdentityValue( 9, 'Frank', 0 );
152 $text .= "* page:{{PAGENAME}}!\n";
153 $text .= "* rev:{{REVISIONID}}!\n";
154 $text .= "* user:{{REVISIONUSER}}!\n";
155 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
156 $text .= "* [[Link It]]\n";
158 $content = [ 'main' => new WikitextContent( $text ) ];
161 /** @var MockObject|RevisionRecord $mock */
162 $mock = $this->getMockBuilder( $class )
163 ->disableOriginalConstructor()
167 'getPageAsLinkTarget',
173 $mock->method( 'getId' )->willReturn( $id );
174 $mock->method( 'getPageId' )->willReturn( $title->getArticleID() );
175 $mock->method( 'getPageAsLinkTarget' )->willReturn( $title );
176 $mock->method( 'getUser' )->willReturn( $frank );
177 $mock->method( 'getVisibility' )->willReturn( $visibility );
178 $mock->method( 'getTimestamp' )->willReturn( '20180101000003' );
180 /** @var object $mockAccess */
181 $mockAccess = TestingAccessWrapper
::newFromObject( $mock );
182 $mockAccess->mSlots
= new MutableRevisionSlots();
184 foreach ( $content as $role => $cnt ) {
185 $mockAccess->mSlots
->setContent( $role, $cnt );
191 public function testGetRevisionParserOutput_new() {
192 $title = $this->getMockTitle( 0, 21 );
193 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title );
195 $options = ParserOptions
::newCanonical( 'canonical' );
196 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
198 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
200 $this->assertSame( $rev, $rr->getRevision() );
201 $this->assertSame( $options, $rr->getOptions() );
203 $html = $rr->getRevisionParserOutput()->getText();
205 $this->assertContains( 'page:RenderTestPage!', $html );
206 $this->assertContains( 'user:Frank!', $html );
207 $this->assertContains( 'time:20180101000003!', $html );
210 public function testGetRevisionParserOutput_previewWithSelfTransclusion() {
211 $title = $this->getMockTitle( 0, 21 );
212 $name = $title->getPrefixedText();
214 $text = "(ONE)<includeonly>(TWO)</includeonly><noinclude>#{{:$name}}#</noinclude>";
217 'main' => new WikitextContent( $text )
220 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title, null, 0, $content );
222 $options = ParserOptions
::newCanonical( 'canonical' );
223 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
225 $html = $rr->getRevisionParserOutput()->getText();
226 $this->assertContains( '(ONE)#(ONE)(TWO)#', $html );
229 public function testGetRevisionParserOutput_current() {
230 $title = $this->getMockTitle( 7, 21 );
231 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title, 21 );
233 $options = ParserOptions
::newCanonical( 'canonical' );
234 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
236 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
238 $this->assertSame( $rev, $rr->getRevision() );
239 $this->assertSame( $options, $rr->getOptions() );
241 $html = $rr->getRevisionParserOutput()->getText();
243 $this->assertContains( 'page:RenderTestPage!', $html );
244 $this->assertContains( 'rev:21!', $html );
245 $this->assertContains( 'user:Frank!', $html );
246 $this->assertContains( 'time:20180101000003!', $html );
248 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
251 public function testGetRevisionParserOutput_old() {
252 $title = $this->getMockTitle( 7, 21 );
253 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title, 11 );
255 $options = ParserOptions
::newCanonical( 'canonical' );
256 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
258 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
260 $this->assertSame( $rev, $rr->getRevision() );
261 $this->assertSame( $options, $rr->getOptions() );
263 $html = $rr->getRevisionParserOutput()->getText();
265 $this->assertContains( 'page:RenderTestPage!', $html );
266 $this->assertContains( 'rev:11!', $html );
267 $this->assertContains( 'user:Frank!', $html );
268 $this->assertContains( 'time:20180101000003!', $html );
270 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
273 public function testGetRevisionParserOutput_archive() {
274 $title = $this->getMockTitle( 7, 21 );
275 $rev = $this->getMockRevision( RevisionArchiveRecord
::class, $title, 11 );
277 $options = ParserOptions
::newCanonical( 'canonical' );
278 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
280 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
282 $this->assertSame( $rev, $rr->getRevision() );
283 $this->assertSame( $options, $rr->getOptions() );
285 $html = $rr->getRevisionParserOutput()->getText();
287 $this->assertContains( 'page:RenderTestPage!', $html );
288 $this->assertContains( 'rev:11!', $html );
289 $this->assertContains( 'user:Frank!', $html );
290 $this->assertContains( 'time:20180101000003!', $html );
292 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
295 public function testGetRevisionParserOutput_suppressed() {
296 $title = $this->getMockTitle( 7, 21 );
297 $rev = $this->getMockRevision(
298 RevisionStoreRecord
::class,
301 RevisionRecord
::DELETED_TEXT
304 $options = ParserOptions
::newCanonical( 'canonical' );
305 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
307 $this->setExpectedException( SuppressedDataException
::class );
308 $rr->getRevisionParserOutput();
311 public function testGetRevisionParserOutput_privileged() {
312 $title = $this->getMockTitle( 7, 21 );
313 $rev = $this->getMockRevision(
314 RevisionStoreRecord
::class,
317 RevisionRecord
::DELETED_TEXT
320 $options = ParserOptions
::newCanonical( 'canonical' );
321 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
322 $rr = new RenderedRevision(
326 $this->combinerCallback
,
327 RevisionRecord
::FOR_THIS_USER
,
331 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
333 $this->assertSame( $rev, $rr->getRevision() );
334 $this->assertSame( $options, $rr->getOptions() );
336 $html = $rr->getRevisionParserOutput()->getText();
338 // Suppressed content should be visible for sysops
339 $this->assertContains( 'page:RenderTestPage!', $html );
340 $this->assertContains( 'rev:11!', $html );
341 $this->assertContains( 'user:Frank!', $html );
342 $this->assertContains( 'time:20180101000003!', $html );
344 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
347 public function testGetRevisionParserOutput_raw() {
348 $title = $this->getMockTitle( 7, 21 );
349 $rev = $this->getMockRevision(
350 RevisionStoreRecord
::class,
353 RevisionRecord
::DELETED_TEXT
356 $options = ParserOptions
::newCanonical( 'canonical' );
357 $rr = new RenderedRevision(
361 $this->combinerCallback
,
365 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
367 $this->assertSame( $rev, $rr->getRevision() );
368 $this->assertSame( $options, $rr->getOptions() );
370 $html = $rr->getRevisionParserOutput()->getText();
372 // Suppressed content should be visible for sysops
373 $this->assertContains( 'page:RenderTestPage!', $html );
374 $this->assertContains( 'rev:11!', $html );
375 $this->assertContains( 'user:Frank!', $html );
376 $this->assertContains( 'time:20180101000003!', $html );
378 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
381 public function testGetRevisionParserOutput_multi() {
383 'main' => new WikitextContent( '[[Kittens]]' ),
384 'aux' => new WikitextContent( '[[Goats]]' ),
387 $title = $this->getMockTitle( 7, 21 );
388 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title, 11, 0, $content );
390 $options = ParserOptions
::newCanonical( 'canonical' );
391 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
393 $combinedOutput = $rr->getRevisionParserOutput();
394 $mainOutput = $rr->getSlotParserOutput( SlotRecord
::MAIN
);
395 $auxOutput = $rr->getSlotParserOutput( 'aux' );
397 $combinedHtml = $combinedOutput->getText();
398 $mainHtml = $mainOutput->getText();
399 $auxHtml = $auxOutput->getText();
401 $this->assertContains( 'Kittens', $mainHtml );
402 $this->assertContains( 'Goats', $auxHtml );
403 $this->assertNotContains( 'Goats', $mainHtml );
404 $this->assertNotContains( 'Kittens', $auxHtml );
405 $this->assertContains( 'Kittens', $combinedHtml );
406 $this->assertContains( 'Goats', $combinedHtml );
407 $this->assertContains( 'aux', $combinedHtml, 'slot section header' );
409 $combinedLinks = $combinedOutput->getLinks();
410 $mainLinks = $mainOutput->getLinks();
411 $auxLinks = $auxOutput->getLinks();
412 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Kittens'] ), 'links from main slot' );
413 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Goats'] ), 'links from aux slot' );
414 $this->assertFalse( isset( $mainLinks[NS_MAIN
]['Goats'] ), 'no aux links in main' );
415 $this->assertFalse( isset( $auxLinks[NS_MAIN
]['Kittens'] ), 'no main links in aux' );
418 public function testGetRevisionParserOutput_incompleteNoId() {
419 $title = $this->getMockTitle( 7, 21 );
421 $rev = new MutableRevisionRecord( $title );
424 $text .= "* page:{{PAGENAME}}!\n";
425 $text .= "* rev:{{REVISIONID}}!\n";
426 $text .= "* user:{{REVISIONUSER}}!\n";
427 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
429 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
431 $options = ParserOptions
::newCanonical( 'canonical' );
432 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
434 // MutableRevisionRecord without ID should be used by the parser.
436 $html = $rr->getRevisionParserOutput()->getText();
438 $this->assertContains( 'page:RenderTestPage!', $html );
439 $this->assertContains( 'rev:!', $html );
440 $this->assertContains( 'user:!', $html );
441 $this->assertContains( 'time:!', $html );
444 public function testGetRevisionParserOutput_incompleteWithId() {
445 $title = $this->getMockTitle( 7, 21 );
447 $rev = new MutableRevisionRecord( $title );
451 $text .= "* page:{{PAGENAME}}!\n";
452 $text .= "* rev:{{REVISIONID}}!\n";
453 $text .= "* user:{{REVISIONUSER}}!\n";
454 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
456 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
458 $actualRevision = $this->getMockRevision(
459 RevisionStoreRecord
::class,
462 RevisionRecord
::DELETED_TEXT
465 $options = ParserOptions
::newCanonical( 'canonical' );
466 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
468 // MutableRevisionRecord with ID should not be used by the parser,
469 // revision should be loaded instead!
470 $revisionStore = $this->getMockBuilder( RevisionStore
::class )
471 ->disableOriginalConstructor()
474 $revisionStore->expects( $this->once() )
475 ->method( 'getKnownCurrentRevision' )
477 ->willReturn( $actualRevision );
479 $this->setService( 'RevisionStore', $revisionStore );
481 $html = $rr->getRevisionParserOutput()->getText();
483 $this->assertContains( 'page:RenderTestPage!', $html );
484 $this->assertContains( 'rev:21!', $html );
485 $this->assertContains( 'user:Frank!', $html );
486 $this->assertContains( 'time:20180101000003!', $html );
489 public function testSetRevisionParserOutput() {
490 $title = $this->getMockTitle( 3, 21 );
491 $rev = $this->getMockRevision( RevisionStoreRecord
::class, $title );
493 $options = ParserOptions
::newCanonical( 'canonical' );
494 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
496 $output = new ParserOutput( 'Kittens' );
497 $rr->setRevisionParserOutput( $output );
499 $this->assertSame( $output, $rr->getRevisionParserOutput() );
500 $this->assertSame( 'Kittens', $rr->getRevisionParserOutput()->getText() );
502 $this->assertSame( $output, $rr->getSlotParserOutput( SlotRecord
::MAIN
) );
503 $this->assertSame( 'Kittens', $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
506 public function testNoHtml() {
507 /** @var MockObject|Content $mockContent */
508 $mockContent = $this->getMockBuilder( WikitextContent
::class )
509 ->setMethods( [ 'getParserOutput' ] )
510 ->setConstructorArgs( [ 'Whatever' ] )
512 $mockContent->method( 'getParserOutput' )
513 ->willReturnCallback( function ( Title
$title, $revId = null,
514 ParserOptions
$options = null, $generateHtml = true
516 if ( !$generateHtml ) {
517 return new ParserOutput( null );
519 $this->fail( 'Should not be called with $generateHtml == true' );
520 return null; // never happens, make analyzer happy
524 $title = $this->getMockTitle( 7, 21 );
526 $rev = new MutableRevisionRecord( $title );
527 $rev->setContent( SlotRecord
::MAIN
, $mockContent );
528 $rev->setContent( 'aux', $mockContent );
530 $options = ParserOptions
::newCanonical( 'canonical' );
531 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
533 $output = $rr->getSlotParserOutput( SlotRecord
::MAIN
, [ 'generate-html' => false ] );
534 $this->assertFalse( $output->hasText(), 'hasText' );
536 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
537 $this->assertFalse( $output->hasText(), 'hasText' );
540 public function testUpdateRevision() {
541 $title = $this->getMockTitle( 7, 21 );
543 $rev = new MutableRevisionRecord( $title );
546 $text .= "* page:{{PAGENAME}}!\n";
547 $text .= "* rev:{{REVISIONID}}!\n";
548 $text .= "* user:{{REVISIONUSER}}!\n";
549 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
551 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
552 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
554 $options = ParserOptions
::newCanonical( 'canonical' );
555 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
557 $firstOutput = $rr->getRevisionParserOutput();
558 $mainOutput = $rr->getSlotParserOutput( SlotRecord
::MAIN
);
559 $auxOutput = $rr->getSlotParserOutput( 'aux' );
561 // emulate a saved revision
562 $savedRev = new MutableRevisionRecord( $title );
563 $savedRev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
564 $savedRev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
565 $savedRev->setId( 23 ); // saved, new
566 $savedRev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
567 $savedRev->setTimestamp( '20180101000003' );
569 $rr->updateRevision( $savedRev );
571 $this->assertNotSame( $mainOutput, $rr->getSlotParserOutput( SlotRecord
::MAIN
), 'Reset main' );
572 $this->assertSame( $auxOutput, $rr->getSlotParserOutput( 'aux' ), 'Keep aux' );
574 $updatedOutput = $rr->getRevisionParserOutput();
575 $html = $updatedOutput->getText();
577 $this->assertNotSame( $firstOutput, $updatedOutput, 'Reset merged' );
578 $this->assertContains( 'page:RenderTestPage!', $html );
579 $this->assertContains( 'rev:23!', $html );
580 $this->assertContains( 'user:Frank!', $html );
581 $this->assertContains( 'time:20180101000003!', $html );
582 $this->assertContains( 'Goats', $html );
584 $rr->updateRevision( $savedRev ); // should do nothing
585 $this->assertSame( $updatedOutput, $rr->getRevisionParserOutput(), 'no more reset needed' );