3 namespace MediaWiki\Tests\Revision
;
5 use CommentStoreComment
;
9 use MediaWiki\Revision\MutableRevisionRecord
;
10 use MediaWiki\Revision\RevisionRecord
;
11 use MediaWiki\Revision\RevisionRenderer
;
12 use MediaWiki\Revision\SlotRecord
;
13 use MediaWikiTestCase
;
14 use MediaWiki\User\UserIdentityValue
;
17 use PHPUnit\Framework\MockObject\MockObject
;
20 use Wikimedia\Rdbms\IDatabase
;
21 use Wikimedia\Rdbms\ILoadBalancer
;
25 * @covers \MediaWiki\Revision\RevisionRenderer
27 class RevisionRendererTest
extends MediaWikiTestCase
{
34 private function getMockTitle( $articleId, $revisionId ) {
35 /** @var Title|MockObject $mock */
36 $mock = $this->getMockBuilder( Title
::class )
37 ->disableOriginalConstructor()
39 $mock->expects( $this->any() )
40 ->method( 'getNamespace' )
41 ->will( $this->returnValue( NS_MAIN
) );
42 $mock->expects( $this->any() )
44 ->will( $this->returnValue( __CLASS__
) );
45 $mock->expects( $this->any() )
46 ->method( 'getPrefixedText' )
47 ->will( $this->returnValue( __CLASS__
) );
48 $mock->expects( $this->any() )
49 ->method( 'getDBkey' )
50 ->will( $this->returnValue( __CLASS__
) );
51 $mock->expects( $this->any() )
52 ->method( 'getArticleID' )
53 ->will( $this->returnValue( $articleId ) );
54 $mock->expects( $this->any() )
55 ->method( 'getLatestRevId' )
56 ->will( $this->returnValue( $revisionId ) );
57 $mock->expects( $this->any() )
58 ->method( 'getContentModel' )
59 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT
) );
60 $mock->expects( $this->any() )
61 ->method( 'getPageLanguage' )
62 ->will( $this->returnValue( Language
::factory( 'en' ) ) );
63 $mock->expects( $this->any() )
64 ->method( 'isContentPage' )
65 ->will( $this->returnValue( true ) );
66 $mock->expects( $this->any() )
69 function ( Title
$other ) use ( $mock ) {
70 return $mock->getArticleID() === $other->getArticleID();
73 $mock->expects( $this->any() )
76 function ( $perm, User
$user ) use ( $mock ) {
77 return $user->isAllowed( $perm );
86 * @param int $linkCount
90 private function getMockDatabaseConnection( $maxRev = 100, $linkCount = 0 ) {
91 /** @var IDatabase|MockObject $db */
92 $db = $this->getMock( IDatabase
::class );
93 $db->method( 'selectField' )
95 function ( $table, $fields, $cond ) use ( $maxRev, $linkCount ) {
96 return $this->selectFieldCallback(
110 * @return RevisionRenderer
112 private function newRevisionRenderer( $maxRev = 100, $useMaster = false ) {
113 $dbIndex = $useMaster ? DB_MASTER
: DB_REPLICA
;
115 $db = $this->getMockDatabaseConnection( $maxRev );
117 /** @var ILoadBalancer|MockObject $lb */
118 $lb = $this->getMock( ILoadBalancer
::class );
119 $lb->method( 'getConnection' )
122 $lb->method( 'getConnectionRef' )
125 $lb->method( 'getLazyConnectionRef' )
129 return new RevisionRenderer( $lb );
132 private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
133 if ( [ $table, $fields, $cond ] === [ 'revision', 'MAX(rev_id)', [] ] ) {
137 $this->fail( 'Unexpected call to selectField' );
138 throw new LogicException( 'Ooops' ); // Can't happen, make analyzer happy
141 public function testGetRenderedRevision_new() {
142 $renderer = $this->newRevisionRenderer( 100 );
143 $title = $this->getMockTitle( 7, 21 );
145 $rev = new MutableRevisionRecord( $title );
146 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
147 $rev->setTimestamp( '20180101000003' );
148 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
151 $text .= "* page:{{PAGENAME}}\n";
152 $text .= "* rev:{{REVISIONID}}\n";
153 $text .= "* user:{{REVISIONUSER}}\n";
154 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
155 $text .= "* [[Link It]]\n";
157 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
159 $options = ParserOptions
::newCanonical( 'canonical' );
160 $rr = $renderer->getRenderedRevision( $rev, $options );
162 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
164 $this->assertSame( $rev, $rr->getRevision() );
165 $this->assertSame( $options, $rr->getOptions() );
167 $html = $rr->getRevisionParserOutput()->getText();
169 $this->assertContains( 'page:' . __CLASS__
, $html );
170 $this->assertContains( 'rev:101', $html ); // from speculativeRevIdCallback
171 $this->assertContains( 'user:Frank', $html );
172 $this->assertContains( 'time:20180101000003', $html );
174 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
177 public function testGetRenderedRevision_current() {
178 $renderer = $this->newRevisionRenderer( 100 );
179 $title = $this->getMockTitle( 7, 21 );
181 $rev = new MutableRevisionRecord( $title );
182 $rev->setId( 21 ); // current!
183 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
184 $rev->setTimestamp( '20180101000003' );
185 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
188 $text .= "* page:{{PAGENAME}}\n";
189 $text .= "* rev:{{REVISIONID}}\n";
190 $text .= "* user:{{REVISIONUSER}}\n";
191 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
193 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
195 $options = ParserOptions
::newCanonical( 'canonical' );
196 $rr = $renderer->getRenderedRevision( $rev, $options );
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:' . __CLASS__
, $html );
206 $this->assertContains( 'rev:21', $html );
207 $this->assertContains( 'user:Frank', $html );
208 $this->assertContains( 'time:20180101000003', $html );
210 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
213 public function testGetRenderedRevision_master() {
214 $renderer = $this->newRevisionRenderer( 100, true ); // use master
215 $title = $this->getMockTitle( 7, 21 );
217 $rev = new MutableRevisionRecord( $title );
218 $rev->setId( 21 ); // current!
219 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
220 $rev->setTimestamp( '20180101000003' );
221 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
224 $text .= "* page:{{PAGENAME}}\n";
225 $text .= "* rev:{{REVISIONID}}\n";
226 $text .= "* user:{{REVISIONUSER}}\n";
227 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
229 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
231 $options = ParserOptions
::newCanonical( 'canonical' );
232 $rr = $renderer->getRenderedRevision( $rev, $options, null, [ 'use-master' => true ] );
234 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
236 $html = $rr->getRevisionParserOutput()->getText();
238 $this->assertContains( 'rev:21', $html );
240 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord
::MAIN
)->getText() );
243 public function testGetRenderedRevision_old() {
244 $renderer = $this->newRevisionRenderer( 100 );
245 $title = $this->getMockTitle( 7, 21 );
247 $rev = new MutableRevisionRecord( $title );
248 $rev->setId( 11 ); // old!
249 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
250 $rev->setTimestamp( '20180101000003' );
251 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
254 $text .= "* page:{{PAGENAME}}\n";
255 $text .= "* rev:{{REVISIONID}}\n";
256 $text .= "* user:{{REVISIONUSER}}\n";
257 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
259 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
261 $options = ParserOptions
::newCanonical( 'canonical' );
262 $rr = $renderer->getRenderedRevision( $rev, $options );
264 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
266 $this->assertSame( $rev, $rr->getRevision() );
267 $this->assertSame( $options, $rr->getOptions() );
269 $html = $rr->getRevisionParserOutput()->getText();
271 $this->assertContains( 'page:' . __CLASS__
, $html );
272 $this->assertContains( 'rev:11', $html );
273 $this->assertContains( 'user:Frank', $html );
274 $this->assertContains( 'time:20180101000003', $html );
276 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
279 public function testGetRenderedRevision_suppressed() {
280 $renderer = $this->newRevisionRenderer( 100 );
281 $title = $this->getMockTitle( 7, 21 );
283 $rev = new MutableRevisionRecord( $title );
284 $rev->setId( 11 ); // old!
285 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
286 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
287 $rev->setTimestamp( '20180101000003' );
288 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
291 $text .= "* page:{{PAGENAME}}\n";
292 $text .= "* rev:{{REVISIONID}}\n";
293 $text .= "* user:{{REVISIONUSER}}\n";
294 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
296 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
298 $options = ParserOptions
::newCanonical( 'canonical' );
299 $rr = $renderer->getRenderedRevision( $rev, $options );
301 $this->assertNull( $rr, 'getRenderedRevision' );
304 public function testGetRenderedRevision_privileged() {
305 $renderer = $this->newRevisionRenderer( 100 );
306 $title = $this->getMockTitle( 7, 21 );
308 $rev = new MutableRevisionRecord( $title );
309 $rev->setId( 11 ); // old!
310 $rev->setVisibility( RevisionRecord
::DELETED_TEXT
); // suppressed!
311 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
312 $rev->setTimestamp( '20180101000003' );
313 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
316 $text .= "* page:{{PAGENAME}}\n";
317 $text .= "* rev:{{REVISIONID}}\n";
318 $text .= "* user:{{REVISIONUSER}}\n";
319 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
321 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( $text ) );
323 $options = ParserOptions
::newCanonical( 'canonical' );
324 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
325 $rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
327 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
329 $this->assertSame( $rev, $rr->getRevision() );
330 $this->assertSame( $options, $rr->getOptions() );
332 $html = $rr->getRevisionParserOutput()->getText();
334 // Suppressed content should be visible for sysops
335 $this->assertContains( 'page:' . __CLASS__
, $html );
336 $this->assertContains( 'rev:11', $html );
337 $this->assertContains( 'user:Frank', $html );
338 $this->assertContains( 'time:20180101000003', $html );
340 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
343 public function testGetRenderedRevision_raw() {
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 $rr = $renderer->getRenderedRevision(
367 [ 'audience' => RevisionRecord
::RAW
]
370 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
372 $this->assertSame( $rev, $rr->getRevision() );
373 $this->assertSame( $options, $rr->getOptions() );
375 $html = $rr->getRevisionParserOutput()->getText();
377 // Suppressed content should be visible in raw mode
378 $this->assertContains( 'page:' . __CLASS__
, $html );
379 $this->assertContains( 'rev:11', $html );
380 $this->assertContains( 'user:Frank', $html );
381 $this->assertContains( 'time:20180101000003', $html );
383 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
386 public function testGetRenderedRevision_multi() {
387 $renderer = $this->newRevisionRenderer();
388 $title = $this->getMockTitle( 7, 21 );
390 $rev = new MutableRevisionRecord( $title );
391 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
392 $rev->setTimestamp( '20180101000003' );
393 $rev->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
395 $rev->setContent( SlotRecord
::MAIN
, new WikitextContent( '[[Kittens]]' ) );
396 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
398 $rr = $renderer->getRenderedRevision( $rev );
400 $combinedOutput = $rr->getRevisionParserOutput();
401 $mainOutput = $rr->getSlotParserOutput( SlotRecord
::MAIN
);
402 $auxOutput = $rr->getSlotParserOutput( 'aux' );
404 $combinedHtml = $combinedOutput->getText();
405 $mainHtml = $mainOutput->getText();
406 $auxHtml = $auxOutput->getText();
408 $this->assertContains( 'Kittens', $mainHtml );
409 $this->assertContains( 'Goats', $auxHtml );
410 $this->assertNotContains( 'Goats', $mainHtml );
411 $this->assertNotContains( 'Kittens', $auxHtml );
412 $this->assertContains( 'Kittens', $combinedHtml );
413 $this->assertContains( 'Goats', $combinedHtml );
414 $this->assertContains( '>aux<', $combinedHtml, 'slot header' );
415 $this->assertNotContains( '<mw:slotheader', $combinedHtml, 'slot header placeholder' );
417 // make sure output wrapping works right
418 $this->assertContains( 'class="mw-parser-output"', $mainHtml );
419 $this->assertContains( 'class="mw-parser-output"', $auxHtml );
420 $this->assertContains( 'class="mw-parser-output"', $combinedHtml );
422 // there should be only one wrapper div
423 $this->assertSame( 1, preg_match_all( '#class="mw-parser-output"#', $combinedHtml ) );
424 $this->assertNotContains( 'class="mw-parser-output"', $combinedOutput->getRawText() );
426 $combinedLinks = $combinedOutput->getLinks();
427 $mainLinks = $mainOutput->getLinks();
428 $auxLinks = $auxOutput->getLinks();
429 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Kittens'] ), 'links from main slot' );
430 $this->assertTrue( isset( $combinedLinks[NS_MAIN
]['Goats'] ), 'links from aux slot' );
431 $this->assertFalse( isset( $mainLinks[NS_MAIN
]['Goats'] ), 'no aux links in main' );
432 $this->assertFalse( isset( $auxLinks[NS_MAIN
]['Kittens'] ), 'no main links in aux' );
435 public function testGetRenderedRevision_noHtml() {
436 /** @var MockObject|Content $mockContent */
437 $mockContent = $this->getMockBuilder( WikitextContent
::class )
438 ->setMethods( [ 'getParserOutput' ] )
439 ->setConstructorArgs( [ 'Whatever' ] )
441 $mockContent->method( 'getParserOutput' )
442 ->willReturnCallback( function ( Title
$title, $revId = null,
443 ParserOptions
$options = null, $generateHtml = true
445 if ( !$generateHtml ) {
446 return new ParserOutput( null );
448 $this->fail( 'Should not be called with $generateHtml == true' );
449 return null; // never happens, make analyzer happy
453 $renderer = $this->newRevisionRenderer();
454 $title = $this->getMockTitle( 7, 21 );
456 $rev = new MutableRevisionRecord( $title );
457 $rev->setContent( SlotRecord
::MAIN
, $mockContent );
458 $rev->setContent( 'aux', $mockContent );
460 // NOTE: we are testing the private combineSlotOutput() callback here.
461 $rr = $renderer->getRenderedRevision( $rev );
463 $output = $rr->getSlotParserOutput( SlotRecord
::MAIN
, [ 'generate-html' => false ] );
464 $this->assertFalse( $output->hasText(), 'hasText' );
466 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
467 $this->assertFalse( $output->hasText(), 'hasText' );