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