071ea6834756f1c4f79ccd752351d4d083e3ef81
[lhc/web/wiklou.git] / tests / phpunit / includes / Revision / RevisionRendererTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Revision;
4
5 use CommentStoreComment;
6 use Content;
7 use Language;
8 use LogicException;
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;
18 use ParserOptions;
19 use ParserOutput;
20 use PHPUnit\Framework\MockObject\MockObject;
21 use Title;
22 use User;
23 use Wikimedia\Rdbms\IDatabase;
24 use Wikimedia\Rdbms\ILoadBalancer;
25 use WikitextContent;
26
27 /**
28 * @covers \MediaWiki\Revision\RevisionRenderer
29 */
30 class RevisionRendererTest extends MediaWikiTestCase {
31
32 /**
33 * @param int $articleId
34 * @param int $revisionId
35 * @return Title
36 */
37 private function getMockTitle( $articleId, $revisionId ) {
38 /** @var Title|MockObject $mock */
39 $mock = $this->getMockBuilder( Title::class )
40 ->disableOriginalConstructor()
41 ->getMock();
42 $mock->expects( $this->any() )
43 ->method( 'getNamespace' )
44 ->will( $this->returnValue( NS_MAIN ) );
45 $mock->expects( $this->any() )
46 ->method( 'getText' )
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() )
70 ->method( 'equals' )
71 ->willReturnCallback(
72 function ( Title $other ) use ( $mock ) {
73 return $mock->getArticleID() === $other->getArticleID();
74 }
75 );
76 $mock->expects( $this->any() )
77 ->method( 'userCan' )
78 ->willReturnCallback(
79 function ( $perm, User $user ) use ( $mock ) {
80 return $user->isAllowed( $perm );
81 }
82 );
83
84 return $mock;
85 }
86
87 /**
88 * @param int $maxRev
89 * @param int $linkCount
90 *
91 * @return IDatabase
92 */
93 private function getMockDatabaseConnection( $maxRev = 100, $linkCount = 0 ) {
94 /** @var IDatabase|MockObject $db */
95 $db = $this->getMock( IDatabase::class );
96 $db->method( 'selectField' )
97 ->willReturnCallback(
98 function ( $table, $fields, $cond ) use ( $maxRev, $linkCount ) {
99 return $this->selectFieldCallback(
100 $table,
101 $fields,
102 $cond,
103 $maxRev,
104 $linkCount
105 );
106 }
107 );
108
109 return $db;
110 }
111
112 /**
113 * @return RevisionRenderer
114 */
115 private function newRevisionRenderer( $maxRev = 100, $useMaster = false ) {
116 $dbIndex = $useMaster ? DB_MASTER : DB_REPLICA;
117
118 $db = $this->getMockDatabaseConnection( $maxRev );
119
120 /** @var ILoadBalancer|MockObject $lb */
121 $lb = $this->getMock( ILoadBalancer::class );
122 $lb->method( 'getConnection' )
123 ->with( $dbIndex )
124 ->willReturn( $db );
125 $lb->method( 'getConnectionRef' )
126 ->with( $dbIndex )
127 ->willReturn( $db );
128 $lb->method( 'getLazyConnectionRef' )
129 ->with( $dbIndex )
130 ->willReturn( $db );
131
132 /** @var NameTableStore|MockObject $slotRoles */
133 $slotRoles = $this->getMockBuilder( NameTableStore::class )
134 ->disableOriginalConstructor()
135 ->getMock();
136 $slotRoles->method( 'getMap' )
137 ->willReturn( [] );
138
139 $roleReg = new SlotRoleRegistry( $slotRoles );
140 $roleReg->defineRole( 'main', function () {
141 return new MainSlotRoleHandler( [] );
142 } );
143 $roleReg->defineRoleWithModel( 'aux', CONTENT_MODEL_WIKITEXT );
144
145 return new RevisionRenderer( $lb, $roleReg );
146 }
147
148 private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
149 if ( [ $table, $fields, $cond ] === [ 'revision', 'MAX(rev_id)', [] ] ) {
150 return $maxRev;
151 }
152
153 $this->fail( 'Unexpected call to selectField' );
154 throw new LogicException( 'Ooops' ); // Can't happen, make analyzer happy
155 }
156
157 public function testGetRenderedRevision_new() {
158 $renderer = $this->newRevisionRenderer( 100 );
159 $title = $this->getMockTitle( 7, 21 );
160
161 $rev = new MutableRevisionRecord( $title );
162 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
163 $rev->setTimestamp( '20180101000003' );
164 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
165
166 $text = "";
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";
172
173 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
174
175 $options = ParserOptions::newCanonical( 'canonical' );
176 $rr = $renderer->getRenderedRevision( $rev, $options );
177
178 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
179
180 $this->assertSame( $rev, $rr->getRevision() );
181 $this->assertSame( $options, $rr->getOptions() );
182
183 $html = $rr->getRevisionParserOutput()->getText();
184
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 );
189
190 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
191 }
192
193 public function testGetRenderedRevision_current() {
194 $renderer = $this->newRevisionRenderer( 100 );
195 $title = $this->getMockTitle( 7, 21 );
196
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( '' ) );
202
203 $text = "";
204 $text .= "* page:{{PAGENAME}}\n";
205 $text .= "* rev:{{REVISIONID}}\n";
206 $text .= "* user:{{REVISIONUSER}}\n";
207 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
208
209 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
210
211 $options = ParserOptions::newCanonical( 'canonical' );
212 $rr = $renderer->getRenderedRevision( $rev, $options );
213
214 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
215
216 $this->assertSame( $rev, $rr->getRevision() );
217 $this->assertSame( $options, $rr->getOptions() );
218
219 $html = $rr->getRevisionParserOutput()->getText();
220
221 $this->assertContains( 'page:' . __CLASS__, $html );
222 $this->assertContains( 'rev:21', $html );
223 $this->assertContains( 'user:Frank', $html );
224 $this->assertContains( 'time:20180101000003', $html );
225
226 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
227 }
228
229 public function testGetRenderedRevision_master() {
230 $renderer = $this->newRevisionRenderer( 100, true ); // use master
231 $title = $this->getMockTitle( 7, 21 );
232
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( '' ) );
238
239 $text = "";
240 $text .= "* page:{{PAGENAME}}\n";
241 $text .= "* rev:{{REVISIONID}}\n";
242 $text .= "* user:{{REVISIONUSER}}\n";
243 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
244
245 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
246
247 $options = ParserOptions::newCanonical( 'canonical' );
248 $rr = $renderer->getRenderedRevision( $rev, $options, null, [ 'use-master' => true ] );
249
250 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
251
252 $html = $rr->getRevisionParserOutput()->getText();
253
254 $this->assertContains( 'rev:21', $html );
255
256 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
257 }
258
259 public function testGetRenderedRevision_known() {
260 $renderer = $this->newRevisionRenderer( 100, true ); // use master
261 $title = $this->getMockTitle( 7, 21 );
262
263 $rev = new MutableRevisionRecord( $title );
264 $rev->setId( 21 ); // current!
265 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
266 $rev->setTimestamp( '20180101000003' );
267 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
268
269 $text = "uncached text";
270 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
271
272 $output = new ParserOutput( 'cached text' );
273
274 $options = ParserOptions::newCanonical( 'canonical' );
275 $rr = $renderer->getRenderedRevision(
276 $rev,
277 $options,
278 null,
279 [ 'known-revision-output' => $output ]
280 );
281
282 $this->assertSame( $output, $rr->getRevisionParserOutput() );
283 $this->assertSame( 'cached text', $rr->getRevisionParserOutput()->getText() );
284 $this->assertSame( 'cached text', $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
285 }
286
287 public function testGetRenderedRevision_old() {
288 $renderer = $this->newRevisionRenderer( 100 );
289 $title = $this->getMockTitle( 7, 21 );
290
291 $rev = new MutableRevisionRecord( $title );
292 $rev->setId( 11 ); // old!
293 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
294 $rev->setTimestamp( '20180101000003' );
295 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
296
297 $text = "";
298 $text .= "* page:{{PAGENAME}}\n";
299 $text .= "* rev:{{REVISIONID}}\n";
300 $text .= "* user:{{REVISIONUSER}}\n";
301 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
302
303 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
304
305 $options = ParserOptions::newCanonical( 'canonical' );
306 $rr = $renderer->getRenderedRevision( $rev, $options );
307
308 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
309
310 $this->assertSame( $rev, $rr->getRevision() );
311 $this->assertSame( $options, $rr->getOptions() );
312
313 $html = $rr->getRevisionParserOutput()->getText();
314
315 $this->assertContains( 'page:' . __CLASS__, $html );
316 $this->assertContains( 'rev:11', $html );
317 $this->assertContains( 'user:Frank', $html );
318 $this->assertContains( 'time:20180101000003', $html );
319
320 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
321 }
322
323 public function testGetRenderedRevision_suppressed() {
324 $renderer = $this->newRevisionRenderer( 100 );
325 $title = $this->getMockTitle( 7, 21 );
326
327 $rev = new MutableRevisionRecord( $title );
328 $rev->setId( 11 ); // old!
329 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
330 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
331 $rev->setTimestamp( '20180101000003' );
332 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
333
334 $text = "";
335 $text .= "* page:{{PAGENAME}}\n";
336 $text .= "* rev:{{REVISIONID}}\n";
337 $text .= "* user:{{REVISIONUSER}}\n";
338 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
339
340 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
341
342 $options = ParserOptions::newCanonical( 'canonical' );
343 $rr = $renderer->getRenderedRevision( $rev, $options );
344
345 $this->assertNull( $rr, 'getRenderedRevision' );
346 }
347
348 public function testGetRenderedRevision_privileged() {
349 $renderer = $this->newRevisionRenderer( 100 );
350 $title = $this->getMockTitle( 7, 21 );
351
352 $rev = new MutableRevisionRecord( $title );
353 $rev->setId( 11 ); // old!
354 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
355 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
356 $rev->setTimestamp( '20180101000003' );
357 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
358
359 $text = "";
360 $text .= "* page:{{PAGENAME}}\n";
361 $text .= "* rev:{{REVISIONID}}\n";
362 $text .= "* user:{{REVISIONUSER}}\n";
363 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
364
365 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
366
367 $options = ParserOptions::newCanonical( 'canonical' );
368 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
369 $rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
370
371 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
372
373 $this->assertSame( $rev, $rr->getRevision() );
374 $this->assertSame( $options, $rr->getOptions() );
375
376 $html = $rr->getRevisionParserOutput()->getText();
377
378 // Suppressed content should be visible for sysops
379 $this->assertContains( 'page:' . __CLASS__, $html );
380 $this->assertContains( 'rev:11', $html );
381 $this->assertContains( 'user:Frank', $html );
382 $this->assertContains( 'time:20180101000003', $html );
383
384 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
385 }
386
387 public function testGetRenderedRevision_raw() {
388 $renderer = $this->newRevisionRenderer( 100 );
389 $title = $this->getMockTitle( 7, 21 );
390
391 $rev = new MutableRevisionRecord( $title );
392 $rev->setId( 11 ); // old!
393 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
394 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
395 $rev->setTimestamp( '20180101000003' );
396 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
397
398 $text = "";
399 $text .= "* page:{{PAGENAME}}\n";
400 $text .= "* rev:{{REVISIONID}}\n";
401 $text .= "* user:{{REVISIONUSER}}\n";
402 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
403
404 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
405
406 $options = ParserOptions::newCanonical( 'canonical' );
407 $rr = $renderer->getRenderedRevision(
408 $rev,
409 $options,
410 null,
411 [ 'audience' => RevisionRecord::RAW ]
412 );
413
414 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
415
416 $this->assertSame( $rev, $rr->getRevision() );
417 $this->assertSame( $options, $rr->getOptions() );
418
419 $html = $rr->getRevisionParserOutput()->getText();
420
421 // Suppressed content should be visible in raw mode
422 $this->assertContains( 'page:' . __CLASS__, $html );
423 $this->assertContains( 'rev:11', $html );
424 $this->assertContains( 'user:Frank', $html );
425 $this->assertContains( 'time:20180101000003', $html );
426
427 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
428 }
429
430 public function testGetRenderedRevision_multi() {
431 $renderer = $this->newRevisionRenderer();
432 $title = $this->getMockTitle( 7, 21 );
433
434 $rev = new MutableRevisionRecord( $title );
435 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
436 $rev->setTimestamp( '20180101000003' );
437 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
438
439 $rev->setContent( SlotRecord::MAIN, new WikitextContent( '[[Kittens]]' ) );
440 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
441
442 $rr = $renderer->getRenderedRevision( $rev );
443
444 $combinedOutput = $rr->getRevisionParserOutput();
445 $mainOutput = $rr->getSlotParserOutput( SlotRecord::MAIN );
446 $auxOutput = $rr->getSlotParserOutput( 'aux' );
447
448 $combinedHtml = $combinedOutput->getText();
449 $mainHtml = $mainOutput->getText();
450 $auxHtml = $auxOutput->getText();
451
452 $this->assertContains( 'Kittens', $mainHtml );
453 $this->assertContains( 'Goats', $auxHtml );
454 $this->assertNotContains( 'Goats', $mainHtml );
455 $this->assertNotContains( 'Kittens', $auxHtml );
456 $this->assertContains( 'Kittens', $combinedHtml );
457 $this->assertContains( 'Goats', $combinedHtml );
458 $this->assertContains( '>aux<', $combinedHtml, 'slot header' );
459 $this->assertNotContains( '<mw:slotheader', $combinedHtml, 'slot header placeholder' );
460
461 // make sure output wrapping works right
462 $this->assertContains( 'class="mw-parser-output"', $mainHtml );
463 $this->assertContains( 'class="mw-parser-output"', $auxHtml );
464 $this->assertContains( 'class="mw-parser-output"', $combinedHtml );
465
466 // there should be only one wrapper div
467 $this->assertSame( 1, preg_match_all( '#class="mw-parser-output"#', $combinedHtml ) );
468 $this->assertNotContains( 'class="mw-parser-output"', $combinedOutput->getRawText() );
469
470 $combinedLinks = $combinedOutput->getLinks();
471 $mainLinks = $mainOutput->getLinks();
472 $auxLinks = $auxOutput->getLinks();
473 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Kittens'] ), 'links from main slot' );
474 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Goats'] ), 'links from aux slot' );
475 $this->assertFalse( isset( $mainLinks[NS_MAIN]['Goats'] ), 'no aux links in main' );
476 $this->assertFalse( isset( $auxLinks[NS_MAIN]['Kittens'] ), 'no main links in aux' );
477 }
478
479 public function testGetRenderedRevision_noHtml() {
480 /** @var MockObject|Content $mockContent */
481 $mockContent = $this->getMockBuilder( WikitextContent::class )
482 ->setMethods( [ 'getParserOutput' ] )
483 ->setConstructorArgs( [ 'Whatever' ] )
484 ->getMock();
485 $mockContent->method( 'getParserOutput' )
486 ->willReturnCallback( function ( Title $title, $revId = null,
487 ParserOptions $options = null, $generateHtml = true
488 ) {
489 if ( !$generateHtml ) {
490 return new ParserOutput( null );
491 } else {
492 $this->fail( 'Should not be called with $generateHtml == true' );
493 return null; // never happens, make analyzer happy
494 }
495 } );
496
497 $renderer = $this->newRevisionRenderer();
498 $title = $this->getMockTitle( 7, 21 );
499
500 $rev = new MutableRevisionRecord( $title );
501 $rev->setContent( SlotRecord::MAIN, $mockContent );
502 $rev->setContent( 'aux', $mockContent );
503
504 // NOTE: we are testing the private combineSlotOutput() callback here.
505 $rr = $renderer->getRenderedRevision( $rev );
506
507 $output = $rr->getSlotParserOutput( SlotRecord::MAIN, [ 'generate-html' => false ] );
508 $this->assertFalse( $output->hasText(), 'hasText' );
509
510 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
511 $this->assertFalse( $output->hasText(), 'hasText' );
512 }
513
514 }