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