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
;
23 use Wikimedia\Rdbms\IDatabase
;
24 use Wikimedia\Rdbms\ILoadBalancer
;
28 * @covers \MediaWiki\Revision\RevisionRenderer
30 class RevisionRendererTest
extends MediaWikiTestCase
{
37 private function getMockTitle( $articleId, $revisionId ) {
38 /** @var Title|MockObject $mock */
39 $mock = $this->getMockBuilder( Title
::class )
40 ->disableOriginalConstructor()
42 $mock->expects( $this->any() )
43 ->method( 'getNamespace' )
44 ->will( $this->returnValue( NS_MAIN
) );
45 $mock->expects( $this->any() )
47 ->will( $this->returnValue( __CLASS__
) );
48 $mock->expects( $this->any() )
49 ->method( 'getPrefixedText' )
50 ->will( $this->returnValue( __CLASS__
) );
51 $mock->expects( $this->any() )
52 ->method( 'getDBkey' )
53 ->will( $this->returnValue( __CLASS__
) );
54 $mock->expects( $this->any() )
55 ->method( 'getArticleID' )
56 ->will( $this->returnValue( $articleId ) );
57 $mock->expects( $this->any() )
58 ->method( 'getLatestRevId' )
59 ->will( $this->returnValue( $revisionId ) );
60 $mock->expects( $this->any() )
61 ->method( 'getContentModel' )
62 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT
) );
63 $mock->expects( $this->any() )
64 ->method( 'getPageLanguage' )
65 ->will( $this->returnValue( Language
::factory( 'en' ) ) );
66 $mock->expects( $this->any() )
67 ->method( 'isContentPage' )
68 ->will( $this->returnValue( true ) );
69 $mock->expects( $this->any() )
72 function ( Title
$other ) use ( $mock ) {
73 return $mock->getArticleID() === $other->getArticleID();
76 $mock->expects( $this->any() )
79 function ( $perm, User
$user ) use ( $mock ) {
80 return $user->isAllowed( $perm );
89 * @param int $linkCount
93 private function getMockDatabaseConnection( $maxRev = 100, $linkCount = 0 ) {
94 /** @var IDatabase|MockObject $db */
95 $db = $this->getMock( IDatabase
::class );
96 $db->method( 'selectField' )
98 function ( $table, $fields, $cond ) use ( $maxRev, $linkCount ) {
99 return $this->selectFieldCallback(
113 * @return RevisionRenderer
115 private function newRevisionRenderer( $maxRev = 100, $useMaster = false ) {
116 $dbIndex = $useMaster ? DB_MASTER
: DB_REPLICA
;
118 $db = $this->getMockDatabaseConnection( $maxRev );
120 /** @var ILoadBalancer|MockObject $lb */
121 $lb = $this->getMock( ILoadBalancer
::class );
122 $lb->method( 'getConnection' )
125 $lb->method( 'getConnectionRef' )
128 $lb->method( 'getLazyConnectionRef' )
132 /** @var NameTableStore|MockObject $slotRoles */
133 $slotRoles = $this->getMockBuilder( NameTableStore
::class )
134 ->disableOriginalConstructor()
136 $slotRoles->method( 'getMap' )
139 $roleReg = new SlotRoleRegistry( $slotRoles );
140 $roleReg->defineRole( 'main', function () {
141 return new MainSlotRoleHandler( [] );
143 $roleReg->defineRoleWithModel( 'aux', CONTENT_MODEL_WIKITEXT
);
145 return new RevisionRenderer( $lb, $roleReg );
148 private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
149 if ( [ $table, $fields, $cond ] === [ 'revision', 'MAX(rev_id)', [] ] ) {
153 $this->fail( 'Unexpected call to selectField' );
154 throw new LogicException( 'Ooops' ); // Can't happen, make analyzer happy
157 public function testGetRenderedRevision_new() {
158 $renderer = $this->newRevisionRenderer( 100 );
159 $title = $this->getMockTitle( 7, 21 );
161 $rev = new MutableRevisionRecord( $title );
162 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
163 $rev->setTimestamp( '20180101000003' );
164 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
167 $text .= "* page:{{PAGENAME}}\n";
168 $text .= "* rev:{{REVISIONID}}\n";
169 $text .= "* user:{{REVISIONUSER}}\n";
170 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
171 $text .= "* [[Link It]]\n";
173 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
175 $options = ParserOptions
::newCanonical( 'canonical' );
176 $rr = $renderer->getRenderedRevision( $rev, $options );
178 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
180 $this->assertSame( $rev, $rr->getRevision() );
181 $this->assertSame( $options, $rr->getOptions() );
183 $html = $rr->getRevisionParserOutput()->getText();
185 $this->assertContains( 'page:' . __CLASS__
, $html );
186 $this->assertContains( 'rev:101', $html ); // from speculativeRevIdCallback
187 $this->assertContains( 'user:Frank', $html );
188 $this->assertContains( 'time:20180101000003', $html );
190 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
193 public function testGetRenderedRevision_current() {
194 $renderer = $this->newRevisionRenderer( 100 );
195 $title = $this->getMockTitle( 7, 21 );
197 $rev = new MutableRevisionRecord( $title );
198 $rev->setId( 21 ); // current!
199 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
200 $rev->setTimestamp( '20180101000003' );
201 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
204 $text .= "* page:{{PAGENAME}}\n";
205 $text .= "* rev:{{REVISIONID}}\n";
206 $text .= "* user:{{REVISIONUSER}}\n";
207 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
209 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
211 $options = ParserOptions
::newCanonical( 'canonical' );
212 $rr = $renderer->getRenderedRevision( $rev, $options );
214 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
216 $this->assertSame( $rev, $rr->getRevision() );
217 $this->assertSame( $options, $rr->getOptions() );
219 $html = $rr->getRevisionParserOutput()->getText();
221 $this->assertContains( 'page:' . __CLASS__
, $html );
222 $this->assertContains( 'rev:21', $html );
223 $this->assertContains( 'user:Frank', $html );
224 $this->assertContains( 'time:20180101000003', $html );
226 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
229 public function testGetRenderedRevision_master() {
230 $renderer = $this->newRevisionRenderer( 100, true ); // use master
231 $title = $this->getMockTitle( 7, 21 );
233 $rev = new MutableRevisionRecord( $title );
234 $rev->setId( 21 ); // current!
235 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
236 $rev->setTimestamp( '20180101000003' );
237 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
240 $text .= "* page:{{PAGENAME}}\n";
241 $text .= "* rev:{{REVISIONID}}\n";
242 $text .= "* user:{{REVISIONUSER}}\n";
243 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
245 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
247 $options = ParserOptions
::newCanonical( 'canonical' );
248 $rr = $renderer->getRenderedRevision( $rev, $options, null, [ 'use-master' => true ] );
250 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
252 $html = $rr->getRevisionParserOutput()->getText();
254 $this->assertContains( 'rev:21', $html );
256 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
259 public function testGetRenderedRevision_old() {
260 $renderer = $this->newRevisionRenderer( 100 );
261 $title = $this->getMockTitle( 7, 21 );
263 $rev = new MutableRevisionRecord( $title );
264 $rev->setId( 11 ); // old!
265 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
266 $rev->setTimestamp( '20180101000003' );
267 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
270 $text .= "* page:{{PAGENAME}}\n";
271 $text .= "* rev:{{REVISIONID}}\n";
272 $text .= "* user:{{REVISIONUSER}}\n";
273 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
275 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
277 $options = ParserOptions
::newCanonical( 'canonical' );
278 $rr = $renderer->getRenderedRevision( $rev, $options );
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:' . __CLASS__
, $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( 'main' )->getText() );
295 public function testGetRenderedRevision_suppressed() {
296 $renderer = $this->newRevisionRenderer( 100 );
297 $title = $this->getMockTitle( 7, 21 );
299 $rev = new MutableRevisionRecord( $title );
300 $rev->setId( 11 ); // old!
301 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
302 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
303 $rev->setTimestamp( '20180101000003' );
304 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
307 $text .= "* page:{{PAGENAME}}\n";
308 $text .= "* rev:{{REVISIONID}}\n";
309 $text .= "* user:{{REVISIONUSER}}\n";
310 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
312 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
314 $options = ParserOptions
::newCanonical( 'canonical' );
315 $rr = $renderer->getRenderedRevision( $rev, $options );
317 $this->assertNull( $rr, 'getRenderedRevision' );
320 public function testGetRenderedRevision_privileged() {
321 $renderer = $this->newRevisionRenderer( 100 );
322 $title = $this->getMockTitle( 7, 21 );
324 $rev = new MutableRevisionRecord( $title );
325 $rev->setId( 11 ); // old!
326 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
327 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
328 $rev->setTimestamp( '20180101000003' );
329 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
332 $text .= "* page:{{PAGENAME}}\n";
333 $text .= "* rev:{{REVISIONID}}\n";
334 $text .= "* user:{{REVISIONUSER}}\n";
335 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
337 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
339 $options = ParserOptions
::newCanonical( 'canonical' );
340 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
341 $rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
343 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
345 $this->assertSame( $rev, $rr->getRevision() );
346 $this->assertSame( $options, $rr->getOptions() );
348 $html = $rr->getRevisionParserOutput()->getText();
350 // Suppressed content should be visible for sysops
351 $this->assertContains( 'page:' . __CLASS__
, $html );
352 $this->assertContains( 'rev:11', $html );
353 $this->assertContains( 'user:Frank', $html );
354 $this->assertContains( 'time:20180101000003', $html );
356 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
359 public function testGetRenderedRevision_raw() {
360 $renderer = $this->newRevisionRenderer( 100 );
361 $title = $this->getMockTitle( 7, 21 );
363 $rev = new MutableRevisionRecord( $title );
364 $rev->setId( 11 ); // old!
365 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
366 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
367 $rev->setTimestamp( '20180101000003' );
368 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
371 $text .= "* page:{{PAGENAME}}\n";
372 $text .= "* rev:{{REVISIONID}}\n";
373 $text .= "* user:{{REVISIONUSER}}\n";
374 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
376 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
378 $options = ParserOptions
::newCanonical( 'canonical' );
379 $rr = $renderer->getRenderedRevision(
383 [ 'audience' => RevisionRecord
::RAW
]
386 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
388 $this->assertSame( $rev, $rr->getRevision() );
389 $this->assertSame( $options, $rr->getOptions() );
391 $html = $rr->getRevisionParserOutput()->getText();
393 // Suppressed content should be visible in raw mode
394 $this->assertContains( 'page:' . __CLASS__
, $html );
395 $this->assertContains( 'rev:11', $html );
396 $this->assertContains( 'user:Frank', $html );
397 $this->assertContains( 'time:20180101000003', $html );
399 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
402 public function testGetRenderedRevision_multi() {
403 $renderer = $this->newRevisionRenderer();
404 $title = $this->getMockTitle( 7, 21 );
406 $rev = new MutableRevisionRecord( $title );
407 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
408 $rev->setTimestamp( '20180101000003' );
409 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
411 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( '[[Kittens]]' ) );
412 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
414 $rr = $renderer->getRenderedRevision( $rev );
416 $combinedOutput = $rr->getRevisionParserOutput();
417 $mainOutput = $rr->getSlotParserOutput( SlotRecord
::MAIN
);
418 $auxOutput = $rr->getSlotParserOutput( 'aux' );
420 $combinedHtml = $combinedOutput->getText();
421 $mainHtml = $mainOutput->getText();
422 $auxHtml = $auxOutput->getText();
424 $this->assertContains( 'Kittens', $mainHtml );
425 $this->assertContains( 'Goats', $auxHtml );
426 $this->assertNotContains( 'Goats', $mainHtml );
427 $this->assertNotContains( 'Kittens', $auxHtml );
428 $this->assertContains( 'Kittens', $combinedHtml );
429 $this->assertContains( 'Goats', $combinedHtml );
430 $this->assertContains( '>aux<', $combinedHtml, 'slot header' );
431 $this->assertNotContains( '<mw:slotheader', $combinedHtml, 'slot header placeholder' );
433 // make sure output wrapping works right
434 $this->assertContains( 'class="mw-parser-output"', $mainHtml );
435 $this->assertContains( 'class="mw-parser-output"', $auxHtml );
436 $this->assertContains( 'class="mw-parser-output"', $combinedHtml );
438 // there should be only one wrapper div
439 $this->assertSame( 1, preg_match_all( '#class="mw-parser-output"#', $combinedHtml ) );
440 $this->assertNotContains( 'class="mw-parser-output"', $combinedOutput->getRawText() );
442 $combinedLinks = $combinedOutput->getLinks();
443 $mainLinks = $mainOutput->getLinks();
444 $auxLinks = $auxOutput->getLinks();
445 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Kittens'] ), 'links from main slot' );
446 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Goats'] ), 'links from aux slot' );
447 $this->assertFalse( isset( $mainLinks[NS_MAIN
]['Goats'] ), 'no aux links in main' );
448 $this->assertFalse( isset( $auxLinks[NS_MAIN
]['Kittens'] ), 'no main links in aux' );
451 public function testGetRenderedRevision_noHtml() {
452 /** @var MockObject|Content $mockContent */
453 $mockContent = $this->getMockBuilder( WikitextContent
::class )
454 ->setMethods( [ 'getParserOutput' ] )
455 ->setConstructorArgs( [ 'Whatever' ] )
457 $mockContent->method( 'getParserOutput' )
458 ->willReturnCallback( function ( Title
$title, $revId = null,
459 ParserOptions
$options = null, $generateHtml = true
461 if ( !$generateHtml ) {
462 return new ParserOutput( null );
464 $this->fail( 'Should not be called with $generateHtml == true' );
465 return null; // never happens, make analyzer happy
469 $renderer = $this->newRevisionRenderer();
470 $title = $this->getMockTitle( 7, 21 );
472 $rev = new MutableRevisionRecord( $title );
473 $rev->setContent( SlotRecord
::MAIN
, $mockContent );
474 $rev->setContent( 'aux', $mockContent );
476 // NOTE: we are testing the private combineSlotOutput() callback here.
477 $rr = $renderer->getRenderedRevision( $rev );
479 $output = $rr->getSlotParserOutput( SlotRecord
::MAIN
, [ 'generate-html' => false ] );
480 $this->assertFalse( $output->hasText(), 'hasText' );
482 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
483 $this->assertFalse( $output->hasText(), 'hasText' );