3 namespace MediaWiki\Tests\Revision
;
7 use MediaWiki\Revision\RenderedRevision
;
8 use MediaWiki\Storage\MutableRevisionRecord
;
9 use MediaWiki\Storage\RevisionRecord
;
10 use MediaWiki\Storage\SuppressedDataException
;
11 use MediaWiki\User\UserIdentityValue
;
12 use MediaWikiTestCase
;
15 use PHPUnit\Framework\MockObject\MockObject
;
21 * @covers \MediaWiki\Revision\RenderedRevision
23 class RenderedRevisionTest
extends MediaWikiTestCase
{
26 private $combinerCallback;
28 public function setUp() {
31 $this->combinerCallback
= function ( RenderedRevision
$rr, array $hints = [] ) {
32 return $this->combineOutput( $rr, $hints );
36 private function combineOutput( RenderedRevision
$rrev, array $hints = [] ) {
37 // NOTE: the is a slightly simplified version of RevisionRenderer::combineSlotOutput
39 $withHtml = $hints['generate-html'] ??
true;
41 $revision = $rrev->getRevision();
42 $slots = $revision->getSlots()->getSlots();
44 $combinedOutput = new ParserOutput( null );
46 foreach ( $slots as $role => $slot ) {
47 $out = $rrev->getSlotParserOutput( $role, $hints );
48 $slotOutput[$role] = $out;
50 $combinedOutput->mergeInternalMetaDataFrom( $out );
51 $combinedOutput->mergeTrackingMetaDataFrom( $out );
56 /** @var ParserOutput $out */
57 foreach ( $slotOutput as $role => $out ) {
60 // skip header for the first slot
64 $html .= $out->getRawText();
65 $combinedOutput->mergeHtmlMetaDataFrom( $out );
68 $combinedOutput->setText( $html );
71 return $combinedOutput;
79 private function getMockTitle( $articleId, $revisionId ) {
80 /** @var Title|MockObject $mock */
81 $mock = $this->getMockBuilder( Title
::class )
82 ->disableOriginalConstructor()
84 $mock->expects( $this->any() )
85 ->method( 'getNamespace' )
86 ->will( $this->returnValue( NS_MAIN
) );
87 $mock->expects( $this->any() )
89 ->will( $this->returnValue( __CLASS__
) );
90 $mock->expects( $this->any() )
91 ->method( 'getPrefixedText' )
92 ->will( $this->returnValue( __CLASS__
) );
93 $mock->expects( $this->any() )
94 ->method( 'getDBkey' )
95 ->will( $this->returnValue( __CLASS__
) );
96 $mock->expects( $this->any() )
97 ->method( 'getArticleID' )
98 ->will( $this->returnValue( $articleId ) );
99 $mock->expects( $this->any() )
100 ->method( 'getLatestRevId' )
101 ->will( $this->returnValue( $revisionId ) );
102 $mock->expects( $this->any() )
103 ->method( 'getContentModel' )
104 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT
) );
105 $mock->expects( $this->any() )
106 ->method( 'getPageLanguage' )
107 ->will( $this->returnValue( Language
::factory( 'en' ) ) );
108 $mock->expects( $this->any() )
109 ->method( 'isContentPage' )
110 ->will( $this->returnValue( true ) );
111 $mock->expects( $this->any() )
113 ->willReturnCallback( function ( Title
$other ) use ( $mock ) {
114 return $mock->getArticleID() === $other->getArticleID();
116 $mock->expects( $this->any() )
117 ->method( 'userCan' )
118 ->willReturnCallback( function ( $perm, User
$user ) use ( $mock ) {
119 return $user->isAllowed( $perm );
125 public function testGetRevisionParserOutput_new() {
126 $title = $this->getMockTitle( 7, 21 );
128 $rev = new MutableRevisionRecord( $title );
129 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
130 $rev->setTimestamp( '20180101000003' );
133 $text .= "* page:{{PAGENAME}}\n";
134 $text .= "* rev:{{REVISIONID}}\n";
135 $text .= "* user:{{REVISIONUSER}}\n";
136 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
137 $text .= "* [[Link It]]\n";
139 $rev->setContent( 'main', new WikitextContent( $text ) );
141 $options = ParserOptions
::newCanonical( 'canonical' );
142 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
144 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
146 $this->assertSame( $rev, $rr->getRevision() );
147 $this->assertSame( $options, $rr->getOptions() );
149 $html = $rr->getRevisionParserOutput()->getText();
151 $this->assertContains( 'page:' . __CLASS__
, $html );
152 $this->assertContains( 'user:Frank', $html );
153 $this->assertContains( 'time:20180101000003', $html );
156 public function testGetRevisionParserOutput_current() {
157 $title = $this->getMockTitle( 7, 21 );
159 $rev = new MutableRevisionRecord( $title );
160 $rev->setId( 21 ); // current!
161 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
162 $rev->setTimestamp( '20180101000003' );
165 $text .= "* page:{{PAGENAME}}\n";
166 $text .= "* rev:{{REVISIONID}}\n";
167 $text .= "* user:{{REVISIONUSER}}\n";
168 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
170 $rev->setContent( 'main', new WikitextContent( $text ) );
172 $options = ParserOptions
::newCanonical( 'canonical' );
173 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
175 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
177 $this->assertSame( $rev, $rr->getRevision() );
178 $this->assertSame( $options, $rr->getOptions() );
180 $html = $rr->getRevisionParserOutput()->getText();
182 $this->assertContains( 'page:' . __CLASS__
, $html );
183 $this->assertContains( 'rev:21', $html );
184 $this->assertContains( 'user:Frank', $html );
185 $this->assertContains( 'time:20180101000003', $html );
187 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
190 public function testGetRevisionParserOutput_old() {
191 $title = $this->getMockTitle( 7, 21 );
193 $rev = new MutableRevisionRecord( $title );
194 $rev->setId( 11 ); // old!
195 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
196 $rev->setTimestamp( '20180101000003' );
199 $text .= "* page:{{PAGENAME}}\n";
200 $text .= "* rev:{{REVISIONID}}\n";
201 $text .= "* user:{{REVISIONUSER}}\n";
202 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
204 $rev->setContent( 'main', new WikitextContent( $text ) );
206 $options = ParserOptions
::newCanonical( 'canonical' );
207 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
209 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
211 $this->assertSame( $rev, $rr->getRevision() );
212 $this->assertSame( $options, $rr->getOptions() );
214 $html = $rr->getRevisionParserOutput()->getText();
216 $this->assertContains( 'page:' . __CLASS__
, $html );
217 $this->assertContains( 'rev:11', $html );
218 $this->assertContains( 'user:Frank', $html );
219 $this->assertContains( 'time:20180101000003', $html );
221 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
224 public function testGetRevisionParserOutput_suppressed() {
225 $title = $this->getMockTitle( 7, 21 );
227 $rev = new MutableRevisionRecord( $title );
228 $rev->setId( 11 ); // old!
229 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
230 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
231 $rev->setTimestamp( '20180101000003' );
234 $text .= "* page:{{PAGENAME}}\n";
235 $text .= "* rev:{{REVISIONID}}\n";
236 $text .= "* user:{{REVISIONUSER}}\n";
237 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
239 $rev->setContent( 'main', new WikitextContent( $text ) );
241 $options = ParserOptions
::newCanonical( 'canonical' );
242 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
244 $this->setExpectedException( SuppressedDataException
::class );
245 $rr->getRevisionParserOutput();
248 public function testGetRevisionParserOutput_privileged() {
249 $title = $this->getMockTitle( 7, 21 );
251 $rev = new MutableRevisionRecord( $title );
252 $rev->setId( 11 ); // old!
253 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
254 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
255 $rev->setTimestamp( '20180101000003' );
258 $text .= "* page:{{PAGENAME}}\n";
259 $text .= "* rev:{{REVISIONID}}\n";
260 $text .= "* user:{{REVISIONUSER}}\n";
261 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
263 $rev->setContent( 'main', new WikitextContent( $text ) );
265 $options = ParserOptions
::newCanonical( 'canonical' );
266 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
267 $rr = new RenderedRevision(
271 $this->combinerCallback
,
272 RevisionRecord
::FOR_THIS_USER
,
276 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
278 $this->assertSame( $rev, $rr->getRevision() );
279 $this->assertSame( $options, $rr->getOptions() );
281 $html = $rr->getRevisionParserOutput()->getText();
283 // Suppressed content should be visible for sysops
284 $this->assertContains( 'page:' . __CLASS__
, $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( 'main' )->getText() );
292 public function testGetRevisionParserOutput_raw() {
293 $title = $this->getMockTitle( 7, 21 );
295 $rev = new MutableRevisionRecord( $title );
296 $rev->setId( 11 ); // old!
297 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
298 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
299 $rev->setTimestamp( '20180101000003' );
302 $text .= "* page:{{PAGENAME}}\n";
303 $text .= "* rev:{{REVISIONID}}\n";
304 $text .= "* user:{{REVISIONUSER}}\n";
305 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
307 $rev->setContent( 'main', new WikitextContent( $text ) );
309 $options = ParserOptions
::newCanonical( 'canonical' );
310 $rr = new RenderedRevision(
314 $this->combinerCallback
,
318 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
320 $this->assertSame( $rev, $rr->getRevision() );
321 $this->assertSame( $options, $rr->getOptions() );
323 $html = $rr->getRevisionParserOutput()->getText();
325 // Suppressed content should be visible for sysops
326 $this->assertContains( 'page:' . __CLASS__
, $html );
327 $this->assertContains( 'rev:11', $html );
328 $this->assertContains( 'user:Frank', $html );
329 $this->assertContains( 'time:20180101000003', $html );
331 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
334 public function testGetRevisionParserOutput_multi() {
335 $title = $this->getMockTitle( 7, 21 );
337 $rev = new MutableRevisionRecord( $title );
338 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
339 $rev->setTimestamp( '20180101000003' );
341 $rev->setContent( 'main', new WikitextContent( '[[Kittens]]' ) );
342 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
344 $options = ParserOptions
::newCanonical( 'canonical' );
345 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
347 $combinedOutput = $rr->getRevisionParserOutput();
348 $mainOutput = $rr->getSlotParserOutput( 'main' );
349 $auxOutput = $rr->getSlotParserOutput( 'aux' );
351 $combinedHtml = $combinedOutput->getText();
352 $mainHtml = $mainOutput->getText();
353 $auxHtml = $auxOutput->getText();
355 $this->assertContains( 'Kittens', $mainHtml );
356 $this->assertContains( 'Goats', $auxHtml );
357 $this->assertNotContains( 'Goats', $mainHtml );
358 $this->assertNotContains( 'Kittens', $auxHtml );
359 $this->assertContains( 'Kittens', $combinedHtml );
360 $this->assertContains( 'Goats', $combinedHtml );
361 $this->assertContains( 'aux', $combinedHtml, 'slot section header' );
363 $combinedLinks = $combinedOutput->getLinks();
364 $mainLinks = $mainOutput->getLinks();
365 $auxLinks = $auxOutput->getLinks();
366 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Kittens'] ), 'links from main slot' );
367 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Goats'] ), 'links from aux slot' );
368 $this->assertFalse( isset( $mainLinks[NS_MAIN
]['Goats'] ), 'no aux links in main' );
369 $this->assertFalse( isset( $auxLinks[NS_MAIN
]['Kittens'] ), 'no main links in aux' );
372 public function testNoHtml() {
373 /** @var MockObject|Content $mockContent */
374 $mockContent = $this->getMockBuilder( WikitextContent
::class )
375 ->setMethods( [ 'getParserOutput' ] )
376 ->setConstructorArgs( [ 'Whatever' ] )
378 $mockContent->method( 'getParserOutput' )
379 ->willReturnCallback( function ( Title
$title, $revId = null,
380 ParserOptions
$options = null, $generateHtml = true
382 if ( !$generateHtml ) {
383 return new ParserOutput( null );
385 $this->fail( 'Should not be called with $generateHtml == true' );
386 return null; // never happens, make analyzer happy
390 $title = $this->getMockTitle( 7, 21 );
392 $rev = new MutableRevisionRecord( $title );
393 $rev->setContent( 'main', $mockContent );
394 $rev->setContent( 'aux', $mockContent );
396 $options = ParserOptions
::newCanonical( 'canonical' );
397 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
399 $output = $rr->getSlotParserOutput( 'main', [ 'generate-html' => false ] );
400 $this->assertFalse( $output->hasText(), 'hasText' );
402 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
403 $this->assertFalse( $output->hasText(), 'hasText' );
406 public function testUpdateRevision() {
407 $title = $this->getMockTitle( 7, 21 );
409 $rev = new MutableRevisionRecord( $title );
412 $text .= "* page:{{PAGENAME}}\n";
413 $text .= "* rev:{{REVISIONID}}\n";
414 $text .= "* user:{{REVISIONUSER}}\n";
415 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
417 $rev->setContent( 'main', new WikitextContent( $text ) );
418 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
420 $options = ParserOptions
::newCanonical( 'canonical' );
421 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback
);
423 $firstOutput = $rr->getRevisionParserOutput();
424 $mainOutput = $rr->getSlotParserOutput( 'main' );
425 $auxOutput = $rr->getSlotParserOutput( 'aux' );
427 // emulate a saved revision
428 $savedRev = new MutableRevisionRecord( $title );
429 $savedRev->setContent( 'main', new WikitextContent( $text ) );
430 $savedRev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
431 $savedRev->setId( 23 ); // saved, new
432 $savedRev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
433 $savedRev->setTimestamp( '20180101000003' );
435 $rr->updateRevision( $savedRev );
437 $this->assertNotSame( $mainOutput, $rr->getSlotParserOutput( 'main' ), 'Reset main' );
438 $this->assertSame( $auxOutput, $rr->getSlotParserOutput( 'aux' ), 'Keep aux' );
440 $updatedOutput = $rr->getRevisionParserOutput();
441 $html = $updatedOutput->getText();
443 $this->assertNotSame( $firstOutput, $updatedOutput, 'Reset merged' );
444 $this->assertContains( 'page:' . __CLASS__
, $html );
445 $this->assertContains( 'rev:23', $html );
446 $this->assertContains( 'user:Frank', $html );
447 $this->assertContains( 'time:20180101000003', $html );
448 $this->assertContains( 'Goats', $html );
450 $rr->updateRevision( $savedRev ); // should do nothing
451 $this->assertSame( $updatedOutput, $rr->getRevisionParserOutput(), 'no more reset needed' );