3 namespace MediaWiki\Tests\Revision
;
5 use CommentStoreComment
;
9 use MediaWiki\Revision\MutableRevisionRecord
;
10 use MediaWiki\Revision\MainSlotRoleHandler
;
11 use MediaWiki\Revision\RevisionRecord
;
12 use MediaWiki\Revision\RevisionRenderer
;
13 use MediaWiki\Revision\SlotRecord
;
14 use MediaWiki\Revision\SlotRoleRegistry
;
15 use MediaWiki\Storage\NameTableStore
;
16 use MediaWikiTestCase
;
17 use MediaWiki\User\UserIdentityValue
;
20 use PHPUnit\Framework\MockObject\MockObject
;
22 use Wikimedia\Rdbms\IDatabase
;
23 use Wikimedia\Rdbms\ILoadBalancer
;
27 * @covers \MediaWiki\Revision\RevisionRenderer
29 class RevisionRendererTest
extends MediaWikiTestCase
{
32 * @param int $articleId
33 * @param int $revisionId
36 private function getMockTitle( $articleId, $revisionId ) {
37 /** @var Title|MockObject $mock */
38 $mock = $this->getMockBuilder( Title
::class )
39 ->disableOriginalConstructor()
41 $mock->expects( $this->any() )
42 ->method( 'getNamespace' )
43 ->will( $this->returnValue( NS_MAIN
) );
44 $mock->expects( $this->any() )
46 ->will( $this->returnValue( __CLASS__
) );
47 $mock->expects( $this->any() )
48 ->method( 'getPrefixedText' )
49 ->will( $this->returnValue( __CLASS__
) );
50 $mock->expects( $this->any() )
51 ->method( 'getDBkey' )
52 ->will( $this->returnValue( __CLASS__
) );
53 $mock->expects( $this->any() )
54 ->method( 'getArticleID' )
55 ->will( $this->returnValue( $articleId ) );
56 $mock->expects( $this->any() )
57 ->method( 'getLatestRevId' )
58 ->will( $this->returnValue( $revisionId ) );
59 $mock->expects( $this->any() )
60 ->method( 'getContentModel' )
61 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT
) );
62 $mock->expects( $this->any() )
63 ->method( 'getPageLanguage' )
64 ->will( $this->returnValue( Language
::factory( 'en' ) ) );
65 $mock->expects( $this->any() )
66 ->method( 'isContentPage' )
67 ->will( $this->returnValue( true ) );
68 $mock->expects( $this->any() )
71 function ( Title
$other ) use ( $mock ) {
72 return $mock->getArticleID() === $other->getArticleID();
75 $mock->expects( $this->any() )
76 ->method( 'getRestrictions' )
84 * @param int $linkCount
88 private function getMockDatabaseConnection( $maxRev = 100, $linkCount = 0 ) {
89 /** @var IDatabase|MockObject $db */
90 $db = $this->getMock( IDatabase
::class );
91 $db->method( 'selectField' )
93 function ( $table, $fields, $cond ) use ( $maxRev, $linkCount ) {
94 return $this->selectFieldCallback(
108 * @return RevisionRenderer
110 private function newRevisionRenderer( $maxRev = 100, $useMaster = false ) {
111 $dbIndex = $useMaster ? DB_MASTER
: DB_REPLICA
;
113 $db = $this->getMockDatabaseConnection( $maxRev );
115 /** @var ILoadBalancer|MockObject $lb */
116 $lb = $this->getMock( ILoadBalancer
::class );
117 $lb->method( 'getConnection' )
120 $lb->method( 'getConnectionRef' )
123 $lb->method( 'getLazyConnectionRef' )
127 /** @var NameTableStore|MockObject $slotRoles */
128 $slotRoles = $this->getMockBuilder( NameTableStore
::class )
129 ->disableOriginalConstructor()
131 $slotRoles->method( 'getMap' )
134 $roleReg = new SlotRoleRegistry( $slotRoles );
135 $roleReg->defineRole( 'main', function () {
136 return new MainSlotRoleHandler( [] );
138 $roleReg->defineRoleWithModel( 'aux', CONTENT_MODEL_WIKITEXT
);
140 return new RevisionRenderer( $lb, $roleReg );
143 private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
144 if ( [ $table, $fields, $cond ] === [ 'revision', 'MAX(rev_id)', [] ] ) {
148 $this->fail( 'Unexpected call to selectField' );
149 throw new LogicException( 'Ooops' ); // Can't happen, make analyzer happy
152 public function testGetRenderedRevision_new() {
153 $renderer = $this->newRevisionRenderer( 100 );
154 $title = $this->getMockTitle( 7, 21 );
156 $rev = new MutableRevisionRecord( $title );
157 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
158 $rev->setTimestamp( '20180101000003' );
159 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
162 $text .= "* page:{{PAGENAME}}\n";
163 $text .= "* rev:{{REVISIONID}}\n";
164 $text .= "* user:{{REVISIONUSER}}\n";
165 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
166 $text .= "* [[Link It]]\n";
168 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
170 $options = ParserOptions
::newCanonical( 'canonical' );
171 $rr = $renderer->getRenderedRevision( $rev, $options );
173 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
175 $this->assertSame( $rev, $rr->getRevision() );
176 $this->assertSame( $options, $rr->getOptions() );
178 $html = $rr->getRevisionParserOutput()->getText();
180 $this->assertContains( 'page:' . __CLASS__
, $html );
181 $this->assertContains( 'rev:101', $html ); // from speculativeRevIdCallback
182 $this->assertContains( 'user:Frank', $html );
183 $this->assertContains( 'time:20180101000003', $html );
185 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
188 public function testGetRenderedRevision_current() {
189 $renderer = $this->newRevisionRenderer( 100 );
190 $title = $this->getMockTitle( 7, 21 );
192 $rev = new MutableRevisionRecord( $title );
193 $rev->setId( 21 ); // current!
194 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
195 $rev->setTimestamp( '20180101000003' );
196 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
199 $text .= "* page:{{PAGENAME}}\n";
200 $text .= "* rev:{{REVISIONID}}\n";
201 $text .= "* user:{{REVISIONUSER}}\n";
202 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
204 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
206 $options = ParserOptions
::newCanonical( 'canonical' );
207 $rr = $renderer->getRenderedRevision( $rev, $options );
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:21', $html );
218 $this->assertContains( 'user:Frank', $html );
219 $this->assertContains( 'time:20180101000003', $html );
221 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
224 public function testGetRenderedRevision_master() {
225 $renderer = $this->newRevisionRenderer( 100, true ); // use master
226 $title = $this->getMockTitle( 7, 21 );
228 $rev = new MutableRevisionRecord( $title );
229 $rev->setId( 21 ); // current!
230 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
231 $rev->setTimestamp( '20180101000003' );
232 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
235 $text .= "* page:{{PAGENAME}}\n";
236 $text .= "* rev:{{REVISIONID}}\n";
237 $text .= "* user:{{REVISIONUSER}}\n";
238 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
240 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
242 $options = ParserOptions
::newCanonical( 'canonical' );
243 $rr = $renderer->getRenderedRevision( $rev, $options, null, [ 'use-master' => true ] );
245 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
247 $html = $rr->getRevisionParserOutput()->getText();
249 $this->assertContains( 'rev:21', $html );
251 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
254 public function testGetRenderedRevision_known() {
255 $renderer = $this->newRevisionRenderer( 100, true ); // use master
256 $title = $this->getMockTitle( 7, 21 );
258 $rev = new MutableRevisionRecord( $title );
259 $rev->setId( 21 ); // current!
260 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
261 $rev->setTimestamp( '20180101000003' );
262 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
264 $text = "uncached text";
265 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
267 $output = new ParserOutput( 'cached text' );
269 $options = ParserOptions
::newCanonical( 'canonical' );
270 $rr = $renderer->getRenderedRevision(
274 [ 'known-revision-output' => $output ]
277 $this->assertSame( $output, $rr->getRevisionParserOutput() );
278 $this->assertSame( 'cached text', $rr->getRevisionParserOutput()->getText() );
279 $this->assertSame( 'cached text', $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
282 public function testGetRenderedRevision_old() {
283 $renderer = $this->newRevisionRenderer( 100 );
284 $title = $this->getMockTitle( 7, 21 );
286 $rev = new MutableRevisionRecord( $title );
287 $rev->setId( 11 ); // old!
288 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
289 $rev->setTimestamp( '20180101000003' );
290 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
293 $text .= "* page:{{PAGENAME}}\n";
294 $text .= "* rev:{{REVISIONID}}\n";
295 $text .= "* user:{{REVISIONUSER}}\n";
296 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
298 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
300 $options = ParserOptions
::newCanonical( 'canonical' );
301 $rr = $renderer->getRenderedRevision( $rev, $options );
303 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
305 $this->assertSame( $rev, $rr->getRevision() );
306 $this->assertSame( $options, $rr->getOptions() );
308 $html = $rr->getRevisionParserOutput()->getText();
310 $this->assertContains( 'page:' . __CLASS__
, $html );
311 $this->assertContains( 'rev:11', $html );
312 $this->assertContains( 'user:Frank', $html );
313 $this->assertContains( 'time:20180101000003', $html );
315 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
318 public function testGetRenderedRevision_suppressed() {
319 $renderer = $this->newRevisionRenderer( 100 );
320 $title = $this->getMockTitle( 7, 21 );
322 $rev = new MutableRevisionRecord( $title );
323 $rev->setId( 11 ); // old!
324 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
325 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
326 $rev->setTimestamp( '20180101000003' );
327 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
330 $text .= "* page:{{PAGENAME}}\n";
331 $text .= "* rev:{{REVISIONID}}\n";
332 $text .= "* user:{{REVISIONUSER}}\n";
333 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
335 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
337 $options = ParserOptions
::newCanonical( 'canonical' );
338 $rr = $renderer->getRenderedRevision( $rev, $options );
340 $this->assertNull( $rr, 'getRenderedRevision' );
343 public function testGetRenderedRevision_privileged() {
344 $renderer = $this->newRevisionRenderer( 100 );
345 $title = $this->getMockTitle( 7, 21 );
347 $rev = new MutableRevisionRecord( $title );
348 $rev->setId( 11 ); // old!
349 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
350 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
351 $rev->setTimestamp( '20180101000003' );
352 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
355 $text .= "* page:{{PAGENAME}}\n";
356 $text .= "* rev:{{REVISIONID}}\n";
357 $text .= "* user:{{REVISIONUSER}}\n";
358 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
360 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
362 $options = ParserOptions
::newCanonical( 'canonical' );
363 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
364 $rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
366 $this->assertNotNull( $rr, 'getRenderedRevision' );
367 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
369 $this->assertSame( $rev, $rr->getRevision() );
370 $this->assertSame( $options, $rr->getOptions() );
372 $html = $rr->getRevisionParserOutput()->getText();
374 // Suppressed content should be visible for sysops
375 $this->assertContains( 'page:' . __CLASS__
, $html );
376 $this->assertContains( 'rev:11', $html );
377 $this->assertContains( 'user:Frank', $html );
378 $this->assertContains( 'time:20180101000003', $html );
380 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
383 public function testGetRenderedRevision_raw() {
384 $renderer = $this->newRevisionRenderer( 100 );
385 $title = $this->getMockTitle( 7, 21 );
387 $rev = new MutableRevisionRecord( $title );
388 $rev->setId( 11 ); // old!
389 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
390 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
391 $rev->setTimestamp( '20180101000003' );
392 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
395 $text .= "* page:{{PAGENAME}}\n";
396 $text .= "* rev:{{REVISIONID}}\n";
397 $text .= "* user:{{REVISIONUSER}}\n";
398 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
400 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
402 $options = ParserOptions
::newCanonical( 'canonical' );
403 $rr = $renderer->getRenderedRevision(
407 [ 'audience' => RevisionRecord
::RAW
]
410 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
412 $this->assertSame( $rev, $rr->getRevision() );
413 $this->assertSame( $options, $rr->getOptions() );
415 $html = $rr->getRevisionParserOutput()->getText();
417 // Suppressed content should be visible in raw mode
418 $this->assertContains( 'page:' . __CLASS__
, $html );
419 $this->assertContains( 'rev:11', $html );
420 $this->assertContains( 'user:Frank', $html );
421 $this->assertContains( 'time:20180101000003', $html );
423 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
426 public function testGetRenderedRevision_multi() {
427 $renderer = $this->newRevisionRenderer();
428 $title = $this->getMockTitle( 7, 21 );
430 $rev = new MutableRevisionRecord( $title );
431 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
432 $rev->setTimestamp( '20180101000003' );
433 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
435 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( '[[Kittens]]' ) );
436 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
438 $rr = $renderer->getRenderedRevision( $rev );
440 $combinedOutput = $rr->getRevisionParserOutput();
441 $mainOutput = $rr->getSlotParserOutput( SlotRecord
::MAIN
);
442 $auxOutput = $rr->getSlotParserOutput( 'aux' );
444 $combinedHtml = $combinedOutput->getText();
445 $mainHtml = $mainOutput->getText();
446 $auxHtml = $auxOutput->getText();
448 $this->assertContains( 'Kittens', $mainHtml );
449 $this->assertContains( 'Goats', $auxHtml );
450 $this->assertNotContains( 'Goats', $mainHtml );
451 $this->assertNotContains( 'Kittens', $auxHtml );
452 $this->assertContains( 'Kittens', $combinedHtml );
453 $this->assertContains( 'Goats', $combinedHtml );
454 $this->assertContains( '>aux<', $combinedHtml, 'slot header' );
455 $this->assertNotContains( '<mw:slotheader', $combinedHtml, 'slot header placeholder' );
457 // make sure output wrapping works right
458 $this->assertContains( 'class="mw-parser-output"', $mainHtml );
459 $this->assertContains( 'class="mw-parser-output"', $auxHtml );
460 $this->assertContains( 'class="mw-parser-output"', $combinedHtml );
462 // there should be only one wrapper div
463 $this->assertSame( 1, preg_match_all( '#class="mw-parser-output"#', $combinedHtml ) );
464 $this->assertNotContains( 'class="mw-parser-output"', $combinedOutput->getRawText() );
466 $combinedLinks = $combinedOutput->getLinks();
467 $mainLinks = $mainOutput->getLinks();
468 $auxLinks = $auxOutput->getLinks();
469 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Kittens'] ), 'links from main slot' );
470 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Goats'] ), 'links from aux slot' );
471 $this->assertFalse( isset( $mainLinks[NS_MAIN
]['Goats'] ), 'no aux links in main' );
472 $this->assertFalse( isset( $auxLinks[NS_MAIN
]['Kittens'] ), 'no main links in aux' );
475 public function testGetRenderedRevision_noHtml() {
476 /** @var MockObject|Content $mockContent */
477 $mockContent = $this->getMockBuilder( WikitextContent
::class )
478 ->setMethods( [ 'getParserOutput' ] )
479 ->setConstructorArgs( [ 'Whatever' ] )
481 $mockContent->method( 'getParserOutput' )
482 ->willReturnCallback( function ( Title
$title, $revId = null,
483 ParserOptions
$options = null, $generateHtml = true
485 if ( !$generateHtml ) {
486 return new ParserOutput( null );
488 $this->fail( 'Should not be called with $generateHtml == true' );
489 return null; // never happens, make analyzer happy
493 $renderer = $this->newRevisionRenderer();
494 $title = $this->getMockTitle( 7, 21 );
496 $rev = new MutableRevisionRecord( $title );
497 $rev->setContent( SlotRecord
::MAIN
, $mockContent );
498 $rev->setContent( 'aux', $mockContent );
500 // NOTE: we are testing the private combineSlotOutput() callback here.
501 $rr = $renderer->getRenderedRevision( $rev );
503 $output = $rr->getSlotParserOutput( SlotRecord
::MAIN
, [ 'generate-html' => false ] );
504 $this->assertFalse( $output->hasText(), 'hasText' );
506 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
507 $this->assertFalse( $output->hasText(), 'hasText' );