resourceloader: Configure eslint to disallow $ and require inside startup
[lhc/web/wiklou.git] / tests / phpunit / includes / Revision / RevisionRendererTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Revision;
4
5 use Content;
6 use Language;
7 use LogicException;
8 use MediaWiki\Revision\RevisionRenderer;
9 use MediaWiki\Storage\MutableRevisionRecord;
10 use MediaWiki\Storage\RevisionRecord;
11 use MediaWiki\User\UserIdentityValue;
12 use MediaWikiTestCase;
13 use ParserOptions;
14 use ParserOutput;
15 use PHPUnit\Framework\MockObject\MockObject;
16 use Title;
17 use User;
18 use Wikimedia\Rdbms\IDatabase;
19 use Wikimedia\Rdbms\ILoadBalancer;
20 use WikitextContent;
21
22 /**
23 * @covers \MediaWiki\Revision\RevisionRenderer
24 */
25 class RevisionRendererTest extends MediaWikiTestCase {
26
27 /**
28 * @param $articleId
29 * @param $revisionId
30 * @return Title
31 */
32 private function getMockTitle( $articleId, $revisionId ) {
33 /** @var Title|MockObject $mock */
34 $mock = $this->getMockBuilder( Title::class )
35 ->disableOriginalConstructor()
36 ->getMock();
37 $mock->expects( $this->any() )
38 ->method( 'getNamespace' )
39 ->will( $this->returnValue( NS_MAIN ) );
40 $mock->expects( $this->any() )
41 ->method( 'getText' )
42 ->will( $this->returnValue( __CLASS__ ) );
43 $mock->expects( $this->any() )
44 ->method( 'getPrefixedText' )
45 ->will( $this->returnValue( __CLASS__ ) );
46 $mock->expects( $this->any() )
47 ->method( 'getDBkey' )
48 ->will( $this->returnValue( __CLASS__ ) );
49 $mock->expects( $this->any() )
50 ->method( 'getArticleID' )
51 ->will( $this->returnValue( $articleId ) );
52 $mock->expects( $this->any() )
53 ->method( 'getLatestRevId' )
54 ->will( $this->returnValue( $revisionId ) );
55 $mock->expects( $this->any() )
56 ->method( 'getContentModel' )
57 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT ) );
58 $mock->expects( $this->any() )
59 ->method( 'getPageLanguage' )
60 ->will( $this->returnValue( Language::factory( 'en' ) ) );
61 $mock->expects( $this->any() )
62 ->method( 'isContentPage' )
63 ->will( $this->returnValue( true ) );
64 $mock->expects( $this->any() )
65 ->method( 'equals' )
66 ->willReturnCallback(
67 function ( Title $other ) use ( $mock ) {
68 return $mock->getArticleID() === $other->getArticleID();
69 }
70 );
71 $mock->expects( $this->any() )
72 ->method( 'userCan' )
73 ->willReturnCallback(
74 function ( $perm, User $user ) use ( $mock ) {
75 return $user->isAllowed( $perm );
76 }
77 );
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 return new RevisionRenderer( $lb );
128 }
129
130 private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
131 if ( [ $table, $fields, $cond ] === [ 'revision', 'MAX(rev_id)', [] ] ) {
132 return $maxRev;
133 }
134
135 $this->fail( 'Unexpected call to selectField' );
136 throw new LogicException( 'Ooops' ); // Can't happen, make analyzer happy
137 }
138
139 public function testGetRenderedRevision_new() {
140 $renderer = $this->newRevisionRenderer( 100 );
141 $title = $this->getMockTitle( 7, 21 );
142
143 $rev = new MutableRevisionRecord( $title );
144 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
145 $rev->setTimestamp( '20180101000003' );
146
147 $text = "";
148 $text .= "* page:{{PAGENAME}}\n";
149 $text .= "* rev:{{REVISIONID}}\n";
150 $text .= "* user:{{REVISIONUSER}}\n";
151 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
152 $text .= "* [[Link It]]\n";
153
154 $rev->setContent( 'main', new WikitextContent( $text ) );
155
156 $options = ParserOptions::newCanonical( 'canonical' );
157 $rr = $renderer->getRenderedRevision( $rev, $options );
158
159 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
160
161 $this->assertSame( $rev, $rr->getRevision() );
162 $this->assertSame( $options, $rr->getOptions() );
163
164 $html = $rr->getRevisionParserOutput()->getText();
165
166 $this->assertContains( 'page:' . __CLASS__, $html );
167 $this->assertContains( 'rev:101', $html ); // from speculativeRevIdCallback
168 $this->assertContains( 'user:Frank', $html );
169 $this->assertContains( 'time:20180101000003', $html );
170
171 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
172 }
173
174 public function testGetRenderedRevision_current() {
175 $renderer = $this->newRevisionRenderer( 100 );
176 $title = $this->getMockTitle( 7, 21 );
177
178 $rev = new MutableRevisionRecord( $title );
179 $rev->setId( 21 ); // current!
180 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
181 $rev->setTimestamp( '20180101000003' );
182
183 $text = "";
184 $text .= "* page:{{PAGENAME}}\n";
185 $text .= "* rev:{{REVISIONID}}\n";
186 $text .= "* user:{{REVISIONUSER}}\n";
187 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
188
189 $rev->setContent( 'main', new WikitextContent( $text ) );
190
191 $options = ParserOptions::newCanonical( 'canonical' );
192 $rr = $renderer->getRenderedRevision( $rev, $options );
193
194 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
195
196 $this->assertSame( $rev, $rr->getRevision() );
197 $this->assertSame( $options, $rr->getOptions() );
198
199 $html = $rr->getRevisionParserOutput()->getText();
200
201 $this->assertContains( 'page:' . __CLASS__, $html );
202 $this->assertContains( 'rev:21', $html );
203 $this->assertContains( 'user:Frank', $html );
204 $this->assertContains( 'time:20180101000003', $html );
205
206 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
207 }
208
209 public function testGetRenderedRevision_master() {
210 $renderer = $this->newRevisionRenderer( 100, true ); // use master
211 $title = $this->getMockTitle( 7, 21 );
212
213 $rev = new MutableRevisionRecord( $title );
214 $rev->setId( 21 ); // current!
215 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
216 $rev->setTimestamp( '20180101000003' );
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( 'main', new WikitextContent( $text ) );
225
226 $options = ParserOptions::newCanonical( 'canonical' );
227 $rr = $renderer->getRenderedRevision( $rev, $options, null, [ 'use-master' => true ] );
228
229 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
230
231 $html = $rr->getRevisionParserOutput()->getText();
232
233 $this->assertContains( 'rev:21', $html );
234
235 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
236 }
237
238 public function testGetRenderedRevision_old() {
239 $renderer = $this->newRevisionRenderer( 100 );
240 $title = $this->getMockTitle( 7, 21 );
241
242 $rev = new MutableRevisionRecord( $title );
243 $rev->setId( 11 ); // old!
244 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
245 $rev->setTimestamp( '20180101000003' );
246
247 $text = "";
248 $text .= "* page:{{PAGENAME}}\n";
249 $text .= "* rev:{{REVISIONID}}\n";
250 $text .= "* user:{{REVISIONUSER}}\n";
251 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
252
253 $rev->setContent( 'main', new WikitextContent( $text ) );
254
255 $options = ParserOptions::newCanonical( 'canonical' );
256 $rr = $renderer->getRenderedRevision( $rev, $options );
257
258 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
259
260 $this->assertSame( $rev, $rr->getRevision() );
261 $this->assertSame( $options, $rr->getOptions() );
262
263 $html = $rr->getRevisionParserOutput()->getText();
264
265 $this->assertContains( 'page:' . __CLASS__, $html );
266 $this->assertContains( 'rev:11', $html );
267 $this->assertContains( 'user:Frank', $html );
268 $this->assertContains( 'time:20180101000003', $html );
269
270 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
271 }
272
273 public function testGetRenderedRevision_suppressed() {
274 $renderer = $this->newRevisionRenderer( 100 );
275 $title = $this->getMockTitle( 7, 21 );
276
277 $rev = new MutableRevisionRecord( $title );
278 $rev->setId( 11 ); // old!
279 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
280 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
281 $rev->setTimestamp( '20180101000003' );
282
283 $text = "";
284 $text .= "* page:{{PAGENAME}}\n";
285 $text .= "* rev:{{REVISIONID}}\n";
286 $text .= "* user:{{REVISIONUSER}}\n";
287 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
288
289 $rev->setContent( 'main', new WikitextContent( $text ) );
290
291 $options = ParserOptions::newCanonical( 'canonical' );
292 $rr = $renderer->getRenderedRevision( $rev, $options );
293
294 $this->assertNull( $rr, 'getRenderedRevision' );
295 }
296
297 public function testGetRenderedRevision_privileged() {
298 $renderer = $this->newRevisionRenderer( 100 );
299 $title = $this->getMockTitle( 7, 21 );
300
301 $rev = new MutableRevisionRecord( $title );
302 $rev->setId( 11 ); // old!
303 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
304 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
305 $rev->setTimestamp( '20180101000003' );
306
307 $text = "";
308 $text .= "* page:{{PAGENAME}}\n";
309 $text .= "* rev:{{REVISIONID}}\n";
310 $text .= "* user:{{REVISIONUSER}}\n";
311 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
312
313 $rev->setContent( 'main', new WikitextContent( $text ) );
314
315 $options = ParserOptions::newCanonical( 'canonical' );
316 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
317 $rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
318
319 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
320
321 $this->assertSame( $rev, $rr->getRevision() );
322 $this->assertSame( $options, $rr->getOptions() );
323
324 $html = $rr->getRevisionParserOutput()->getText();
325
326 // Suppressed content should be visible for sysops
327 $this->assertContains( 'page:' . __CLASS__, $html );
328 $this->assertContains( 'rev:11', $html );
329 $this->assertContains( 'user:Frank', $html );
330 $this->assertContains( 'time:20180101000003', $html );
331
332 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
333 }
334
335 public function testGetRenderedRevision_raw() {
336 $renderer = $this->newRevisionRenderer( 100 );
337 $title = $this->getMockTitle( 7, 21 );
338
339 $rev = new MutableRevisionRecord( $title );
340 $rev->setId( 11 ); // old!
341 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
342 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
343 $rev->setTimestamp( '20180101000003' );
344
345 $text = "";
346 $text .= "* page:{{PAGENAME}}\n";
347 $text .= "* rev:{{REVISIONID}}\n";
348 $text .= "* user:{{REVISIONUSER}}\n";
349 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
350
351 $rev->setContent( 'main', new WikitextContent( $text ) );
352
353 $options = ParserOptions::newCanonical( 'canonical' );
354 $rr = $renderer->getRenderedRevision(
355 $rev,
356 $options,
357 null,
358 [ 'audience' => RevisionRecord::RAW ]
359 );
360
361 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
362
363 $this->assertSame( $rev, $rr->getRevision() );
364 $this->assertSame( $options, $rr->getOptions() );
365
366 $html = $rr->getRevisionParserOutput()->getText();
367
368 // Suppressed content should be visible in raw mode
369 $this->assertContains( 'page:' . __CLASS__, $html );
370 $this->assertContains( 'rev:11', $html );
371 $this->assertContains( 'user:Frank', $html );
372 $this->assertContains( 'time:20180101000003', $html );
373
374 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
375 }
376
377 public function testGetRenderedRevision_multi() {
378 $renderer = $this->newRevisionRenderer();
379 $title = $this->getMockTitle( 7, 21 );
380
381 $rev = new MutableRevisionRecord( $title );
382 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
383 $rev->setTimestamp( '20180101000003' );
384
385 $rev->setContent( 'main', new WikitextContent( '[[Kittens]]' ) );
386 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
387
388 $rr = $renderer->getRenderedRevision( $rev );
389
390 $combinedOutput = $rr->getRevisionParserOutput();
391 $mainOutput = $rr->getSlotParserOutput( 'main' );
392 $auxOutput = $rr->getSlotParserOutput( 'aux' );
393
394 $combinedHtml = $combinedOutput->getText();
395 $mainHtml = $mainOutput->getText();
396 $auxHtml = $auxOutput->getText();
397
398 $this->assertContains( 'Kittens', $mainHtml );
399 $this->assertContains( 'Goats', $auxHtml );
400 $this->assertNotContains( 'Goats', $mainHtml );
401 $this->assertNotContains( 'Kittens', $auxHtml );
402 $this->assertContains( 'Kittens', $combinedHtml );
403 $this->assertContains( 'Goats', $combinedHtml );
404 $this->assertContains( '>aux<', $combinedHtml, 'slot header' );
405 $this->assertNotContains( '<mw:slotheader', $combinedHtml, 'slot header placeholder' );
406
407 // make sure output wrapping works right
408 $this->assertContains( 'class="mw-parser-output"', $mainHtml );
409 $this->assertContains( 'class="mw-parser-output"', $auxHtml );
410 $this->assertContains( 'class="mw-parser-output"', $combinedHtml );
411
412 // there should be only one wrapper div
413 $this->assertSame( 1, preg_match_all( '#class="mw-parser-output"#', $combinedHtml ) );
414 $this->assertNotContains( 'class="mw-parser-output"', $combinedOutput->getRawText() );
415
416 $combinedLinks = $combinedOutput->getLinks();
417 $mainLinks = $mainOutput->getLinks();
418 $auxLinks = $auxOutput->getLinks();
419 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Kittens'] ), 'links from main slot' );
420 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Goats'] ), 'links from aux slot' );
421 $this->assertFalse( isset( $mainLinks[NS_MAIN]['Goats'] ), 'no aux links in main' );
422 $this->assertFalse( isset( $auxLinks[NS_MAIN]['Kittens'] ), 'no main links in aux' );
423 }
424
425 public function testGetRenderedRevision_noHtml() {
426 /** @var MockObject|Content $mockContent */
427 $mockContent = $this->getMockBuilder( WikitextContent::class )
428 ->setMethods( [ 'getParserOutput' ] )
429 ->setConstructorArgs( [ 'Whatever' ] )
430 ->getMock();
431 $mockContent->method( 'getParserOutput' )
432 ->willReturnCallback( function ( Title $title, $revId = null,
433 ParserOptions $options = null, $generateHtml = true
434 ) {
435 if ( !$generateHtml ) {
436 return new ParserOutput( null );
437 } else {
438 $this->fail( 'Should not be called with $generateHtml == true' );
439 return null; // never happens, make analyzer happy
440 }
441 } );
442
443 $renderer = $this->newRevisionRenderer();
444 $title = $this->getMockTitle( 7, 21 );
445
446 $rev = new MutableRevisionRecord( $title );
447 $rev->setContent( 'main', $mockContent );
448 $rev->setContent( 'aux', $mockContent );
449
450 // NOTE: we are testing the private combineSlotOutput() callback here.
451 $rr = $renderer->getRenderedRevision( $rev );
452
453 $output = $rr->getSlotParserOutput( 'main', [ 'generate-html' => false ] );
454 $this->assertFalse( $output->hasText(), 'hasText' );
455
456 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
457 $this->assertFalse( $output->hasText(), 'hasText' );
458 }
459
460 }