Merge "Rename various $wikiId fields/parameters to $dbDomain in user classes"
[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 Wikimedia\Rdbms\IDatabase;
23 use Wikimedia\Rdbms\ILoadBalancer;
24 use WikitextContent;
25
26 /**
27 * @covers \MediaWiki\Revision\RevisionRenderer
28 */
29 class RevisionRendererTest extends MediaWikiTestCase {
30
31 /**
32 * @param int $articleId
33 * @param int $revisionId
34 * @return Title
35 */
36 private function getMockTitle( $articleId, $revisionId ) {
37 /** @var Title|MockObject $mock */
38 $mock = $this->getMockBuilder( Title::class )
39 ->disableOriginalConstructor()
40 ->getMock();
41 $mock->expects( $this->any() )
42 ->method( 'getNamespace' )
43 ->will( $this->returnValue( NS_MAIN ) );
44 $mock->expects( $this->any() )
45 ->method( 'getText' )
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() )
69 ->method( 'equals' )
70 ->willReturnCallback(
71 function ( Title $other ) use ( $mock ) {
72 return $mock->getArticleID() === $other->getArticleID();
73 }
74 );
75 $mock->expects( $this->any() )
76 ->method( 'getRestrictions' )
77 ->willReturn( [] );
78
79 return $mock;
80 }
81
82 /**
83 * @param int $maxRev
84 * @param int $linkCount
85 *
86 * @return IDatabase
87 */
88 private function getMockDatabaseConnection( $maxRev = 100, $linkCount = 0 ) {
89 /** @var IDatabase|MockObject $db */
90 $db = $this->getMock( IDatabase::class );
91 $db->method( 'selectField' )
92 ->willReturnCallback(
93 function ( $table, $fields, $cond ) use ( $maxRev, $linkCount ) {
94 return $this->selectFieldCallback(
95 $table,
96 $fields,
97 $cond,
98 $maxRev,
99 $linkCount
100 );
101 }
102 );
103
104 return $db;
105 }
106
107 /**
108 * @return RevisionRenderer
109 */
110 private function newRevisionRenderer( $maxRev = 100, $useMaster = false ) {
111 $dbIndex = $useMaster ? DB_MASTER : DB_REPLICA;
112
113 $db = $this->getMockDatabaseConnection( $maxRev );
114
115 /** @var ILoadBalancer|MockObject $lb */
116 $lb = $this->getMock( ILoadBalancer::class );
117 $lb->method( 'getConnection' )
118 ->with( $dbIndex )
119 ->willReturn( $db );
120 $lb->method( 'getConnectionRef' )
121 ->with( $dbIndex )
122 ->willReturn( $db );
123 $lb->method( 'getLazyConnectionRef' )
124 ->with( $dbIndex )
125 ->willReturn( $db );
126
127 /** @var NameTableStore|MockObject $slotRoles */
128 $slotRoles = $this->getMockBuilder( NameTableStore::class )
129 ->disableOriginalConstructor()
130 ->getMock();
131 $slotRoles->method( 'getMap' )
132 ->willReturn( [] );
133
134 $roleReg = new SlotRoleRegistry( $slotRoles );
135 $roleReg->defineRole( 'main', function () {
136 return new MainSlotRoleHandler( [] );
137 } );
138 $roleReg->defineRoleWithModel( 'aux', CONTENT_MODEL_WIKITEXT );
139
140 return new RevisionRenderer( $lb, $roleReg );
141 }
142
143 private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
144 if ( [ $table, $fields, $cond ] === [ 'revision', 'MAX(rev_id)', [] ] ) {
145 return $maxRev;
146 }
147
148 $this->fail( 'Unexpected call to selectField' );
149 throw new LogicException( 'Ooops' ); // Can't happen, make analyzer happy
150 }
151
152 public function testGetRenderedRevision_new() {
153 $renderer = $this->newRevisionRenderer( 100 );
154 $title = $this->getMockTitle( 7, 21 );
155
156 $rev = new MutableRevisionRecord( $title );
157 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
158 $rev->setTimestamp( '20180101000003' );
159 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
160
161 $text = "";
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";
167
168 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
169
170 $options = ParserOptions::newCanonical( 'canonical' );
171 $rr = $renderer->getRenderedRevision( $rev, $options );
172
173 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
174
175 $this->assertSame( $rev, $rr->getRevision() );
176 $this->assertSame( $options, $rr->getOptions() );
177
178 $html = $rr->getRevisionParserOutput()->getText();
179
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 );
184
185 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
186 }
187
188 public function testGetRenderedRevision_current() {
189 $renderer = $this->newRevisionRenderer( 100 );
190 $title = $this->getMockTitle( 7, 21 );
191
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( '' ) );
197
198 $text = "";
199 $text .= "* page:{{PAGENAME}}\n";
200 $text .= "* rev:{{REVISIONID}}\n";
201 $text .= "* user:{{REVISIONUSER}}\n";
202 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
203
204 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
205
206 $options = ParserOptions::newCanonical( 'canonical' );
207 $rr = $renderer->getRenderedRevision( $rev, $options );
208
209 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
210
211 $this->assertSame( $rev, $rr->getRevision() );
212 $this->assertSame( $options, $rr->getOptions() );
213
214 $html = $rr->getRevisionParserOutput()->getText();
215
216 $this->assertContains( 'page:' . __CLASS__, $html );
217 $this->assertContains( 'rev:21', $html );
218 $this->assertContains( 'user:Frank', $html );
219 $this->assertContains( 'time:20180101000003', $html );
220
221 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
222 }
223
224 public function testGetRenderedRevision_master() {
225 $renderer = $this->newRevisionRenderer( 100, true ); // use master
226 $title = $this->getMockTitle( 7, 21 );
227
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( '' ) );
233
234 $text = "";
235 $text .= "* page:{{PAGENAME}}\n";
236 $text .= "* rev:{{REVISIONID}}\n";
237 $text .= "* user:{{REVISIONUSER}}\n";
238 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
239
240 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
241
242 $options = ParserOptions::newCanonical( 'canonical' );
243 $rr = $renderer->getRenderedRevision( $rev, $options, null, [ 'use-master' => true ] );
244
245 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
246
247 $html = $rr->getRevisionParserOutput()->getText();
248
249 $this->assertContains( 'rev:21', $html );
250
251 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
252 }
253
254 public function testGetRenderedRevision_known() {
255 $renderer = $this->newRevisionRenderer( 100, true ); // use master
256 $title = $this->getMockTitle( 7, 21 );
257
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( '' ) );
263
264 $text = "uncached text";
265 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
266
267 $output = new ParserOutput( 'cached text' );
268
269 $options = ParserOptions::newCanonical( 'canonical' );
270 $rr = $renderer->getRenderedRevision(
271 $rev,
272 $options,
273 null,
274 [ 'known-revision-output' => $output ]
275 );
276
277 $this->assertSame( $output, $rr->getRevisionParserOutput() );
278 $this->assertSame( 'cached text', $rr->getRevisionParserOutput()->getText() );
279 $this->assertSame( 'cached text', $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
280 }
281
282 public function testGetRenderedRevision_old() {
283 $renderer = $this->newRevisionRenderer( 100 );
284 $title = $this->getMockTitle( 7, 21 );
285
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( '' ) );
291
292 $text = "";
293 $text .= "* page:{{PAGENAME}}\n";
294 $text .= "* rev:{{REVISIONID}}\n";
295 $text .= "* user:{{REVISIONUSER}}\n";
296 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
297
298 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
299
300 $options = ParserOptions::newCanonical( 'canonical' );
301 $rr = $renderer->getRenderedRevision( $rev, $options );
302
303 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
304
305 $this->assertSame( $rev, $rr->getRevision() );
306 $this->assertSame( $options, $rr->getOptions() );
307
308 $html = $rr->getRevisionParserOutput()->getText();
309
310 $this->assertContains( 'page:' . __CLASS__, $html );
311 $this->assertContains( 'rev:11', $html );
312 $this->assertContains( 'user:Frank', $html );
313 $this->assertContains( 'time:20180101000003', $html );
314
315 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
316 }
317
318 public function testGetRenderedRevision_suppressed() {
319 $renderer = $this->newRevisionRenderer( 100 );
320 $title = $this->getMockTitle( 7, 21 );
321
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( '' ) );
328
329 $text = "";
330 $text .= "* page:{{PAGENAME}}\n";
331 $text .= "* rev:{{REVISIONID}}\n";
332 $text .= "* user:{{REVISIONUSER}}\n";
333 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
334
335 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
336
337 $options = ParserOptions::newCanonical( 'canonical' );
338 $rr = $renderer->getRenderedRevision( $rev, $options );
339
340 $this->assertNull( $rr, 'getRenderedRevision' );
341 }
342
343 public function testGetRenderedRevision_privileged() {
344 $renderer = $this->newRevisionRenderer( 100 );
345 $title = $this->getMockTitle( 7, 21 );
346
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( '' ) );
353
354 $text = "";
355 $text .= "* page:{{PAGENAME}}\n";
356 $text .= "* rev:{{REVISIONID}}\n";
357 $text .= "* user:{{REVISIONUSER}}\n";
358 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
359
360 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
361
362 $options = ParserOptions::newCanonical( 'canonical' );
363 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
364 $rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
365
366 $this->assertNotNull( $rr, 'getRenderedRevision' );
367 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
368
369 $this->assertSame( $rev, $rr->getRevision() );
370 $this->assertSame( $options, $rr->getOptions() );
371
372 $html = $rr->getRevisionParserOutput()->getText();
373
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 );
379
380 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
381 }
382
383 public function testGetRenderedRevision_raw() {
384 $renderer = $this->newRevisionRenderer( 100 );
385 $title = $this->getMockTitle( 7, 21 );
386
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( '' ) );
393
394 $text = "";
395 $text .= "* page:{{PAGENAME}}\n";
396 $text .= "* rev:{{REVISIONID}}\n";
397 $text .= "* user:{{REVISIONUSER}}\n";
398 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
399
400 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
401
402 $options = ParserOptions::newCanonical( 'canonical' );
403 $rr = $renderer->getRenderedRevision(
404 $rev,
405 $options,
406 null,
407 [ 'audience' => RevisionRecord::RAW ]
408 );
409
410 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
411
412 $this->assertSame( $rev, $rr->getRevision() );
413 $this->assertSame( $options, $rr->getOptions() );
414
415 $html = $rr->getRevisionParserOutput()->getText();
416
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 );
422
423 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
424 }
425
426 public function testGetRenderedRevision_multi() {
427 $renderer = $this->newRevisionRenderer();
428 $title = $this->getMockTitle( 7, 21 );
429
430 $rev = new MutableRevisionRecord( $title );
431 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
432 $rev->setTimestamp( '20180101000003' );
433 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
434
435 $rev->setContent( SlotRecord::MAIN, new WikitextContent( '[[Kittens]]' ) );
436 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
437
438 $rr = $renderer->getRenderedRevision( $rev );
439
440 $combinedOutput = $rr->getRevisionParserOutput();
441 $mainOutput = $rr->getSlotParserOutput( SlotRecord::MAIN );
442 $auxOutput = $rr->getSlotParserOutput( 'aux' );
443
444 $combinedHtml = $combinedOutput->getText();
445 $mainHtml = $mainOutput->getText();
446 $auxHtml = $auxOutput->getText();
447
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' );
456
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 );
461
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() );
465
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' );
473 }
474
475 public function testGetRenderedRevision_noHtml() {
476 /** @var MockObject|Content $mockContent */
477 $mockContent = $this->getMockBuilder( WikitextContent::class )
478 ->setMethods( [ 'getParserOutput' ] )
479 ->setConstructorArgs( [ 'Whatever' ] )
480 ->getMock();
481 $mockContent->method( 'getParserOutput' )
482 ->willReturnCallback( function ( Title $title, $revId = null,
483 ParserOptions $options = null, $generateHtml = true
484 ) {
485 if ( !$generateHtml ) {
486 return new ParserOutput( null );
487 } else {
488 $this->fail( 'Should not be called with $generateHtml == true' );
489 return null; // never happens, make analyzer happy
490 }
491 } );
492
493 $renderer = $this->newRevisionRenderer();
494 $title = $this->getMockTitle( 7, 21 );
495
496 $rev = new MutableRevisionRecord( $title );
497 $rev->setContent( SlotRecord::MAIN, $mockContent );
498 $rev->setContent( 'aux', $mockContent );
499
500 // NOTE: we are testing the private combineSlotOutput() callback here.
501 $rr = $renderer->getRenderedRevision( $rev );
502
503 $output = $rr->getSlotParserOutput( SlotRecord::MAIN, [ 'generate-html' => false ] );
504 $this->assertFalse( $output->hasText(), 'hasText' );
505
506 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
507 $this->assertFalse( $output->hasText(), 'hasText' );
508 }
509
510 }