Introduce ContentHandler::getSecondaryDataUpdates.
[lhc/web/wiklou.git] / tests / phpunit / includes / Revision / RenderedRevisionTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Revision;
4
5 use Content;
6 use Language;
7 use MediaWiki\Revision\RenderedRevision;
8 use MediaWiki\Storage\MutableRevisionRecord;
9 use MediaWiki\Storage\RevisionRecord;
10 use MediaWiki\Storage\SuppressedDataException;
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 WikitextContent;
19
20 /**
21 * @covers \MediaWiki\Revision\RenderedRevision
22 */
23 class RenderedRevisionTest extends MediaWikiTestCase {
24
25 /** @var callable */
26 private $combinerCallback;
27
28 public function setUp() {
29 parent::setUp();
30
31 $this->combinerCallback = function ( RenderedRevision $rr, array $hints = [] ) {
32 return $this->combineOutput( $rr, $hints );
33 };
34 }
35
36 private function combineOutput( RenderedRevision $rrev, array $hints = [] ) {
37 // NOTE: the is a slightly simplified version of RevisionRenderer::combineSlotOutput
38
39 $withHtml = $hints['generate-html'] ?? true;
40
41 $revision = $rrev->getRevision();
42 $slots = $revision->getSlots()->getSlots();
43
44 $combinedOutput = new ParserOutput( null );
45 $slotOutput = [];
46 foreach ( $slots as $role => $slot ) {
47 $out = $rrev->getSlotParserOutput( $role, $hints );
48 $slotOutput[$role] = $out;
49
50 $combinedOutput->mergeInternalMetaDataFrom( $out );
51 $combinedOutput->mergeTrackingMetaDataFrom( $out );
52 }
53
54 if ( $withHtml ) {
55 $html = '';
56 /** @var ParserOutput $out */
57 foreach ( $slotOutput as $role => $out ) {
58
59 if ( $html !== '' ) {
60 // skip header for the first slot
61 $html .= "(($role))";
62 }
63
64 $html .= $out->getRawText();
65 $combinedOutput->mergeHtmlMetaDataFrom( $out );
66 }
67
68 $combinedOutput->setText( $html );
69 }
70
71 return $combinedOutput;
72 }
73
74 /**
75 * @param $articleId
76 * @param $revisionId
77 * @return Title
78 */
79 private function getMockTitle( $articleId, $revisionId ) {
80 /** @var Title|MockObject $mock */
81 $mock = $this->getMockBuilder( Title::class )
82 ->disableOriginalConstructor()
83 ->getMock();
84 $mock->expects( $this->any() )
85 ->method( 'getNamespace' )
86 ->will( $this->returnValue( NS_MAIN ) );
87 $mock->expects( $this->any() )
88 ->method( 'getText' )
89 ->will( $this->returnValue( __CLASS__ ) );
90 $mock->expects( $this->any() )
91 ->method( 'getPrefixedText' )
92 ->will( $this->returnValue( __CLASS__ ) );
93 $mock->expects( $this->any() )
94 ->method( 'getDBkey' )
95 ->will( $this->returnValue( __CLASS__ ) );
96 $mock->expects( $this->any() )
97 ->method( 'getArticleID' )
98 ->will( $this->returnValue( $articleId ) );
99 $mock->expects( $this->any() )
100 ->method( 'getLatestRevId' )
101 ->will( $this->returnValue( $revisionId ) );
102 $mock->expects( $this->any() )
103 ->method( 'getContentModel' )
104 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT ) );
105 $mock->expects( $this->any() )
106 ->method( 'getPageLanguage' )
107 ->will( $this->returnValue( Language::factory( 'en' ) ) );
108 $mock->expects( $this->any() )
109 ->method( 'isContentPage' )
110 ->will( $this->returnValue( true ) );
111 $mock->expects( $this->any() )
112 ->method( 'equals' )
113 ->willReturnCallback( function ( Title $other ) use ( $mock ) {
114 return $mock->getArticleID() === $other->getArticleID();
115 } );
116 $mock->expects( $this->any() )
117 ->method( 'userCan' )
118 ->willReturnCallback( function ( $perm, User $user ) use ( $mock ) {
119 return $user->isAllowed( $perm );
120 } );
121
122 return $mock;
123 }
124
125 public function testGetRevisionParserOutput_new() {
126 $title = $this->getMockTitle( 7, 21 );
127
128 $rev = new MutableRevisionRecord( $title );
129 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
130 $rev->setTimestamp( '20180101000003' );
131
132 $text = "";
133 $text .= "* page:{{PAGENAME}}\n";
134 $text .= "* rev:{{REVISIONID}}\n";
135 $text .= "* user:{{REVISIONUSER}}\n";
136 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
137 $text .= "* [[Link It]]\n";
138
139 $rev->setContent( 'main', new WikitextContent( $text ) );
140
141 $options = ParserOptions::newCanonical( 'canonical' );
142 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
143
144 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
145
146 $this->assertSame( $rev, $rr->getRevision() );
147 $this->assertSame( $options, $rr->getOptions() );
148
149 $html = $rr->getRevisionParserOutput()->getText();
150
151 $this->assertContains( 'page:' . __CLASS__, $html );
152 $this->assertContains( 'user:Frank', $html );
153 $this->assertContains( 'time:20180101000003', $html );
154 }
155
156 public function testGetRevisionParserOutput_current() {
157 $title = $this->getMockTitle( 7, 21 );
158
159 $rev = new MutableRevisionRecord( $title );
160 $rev->setId( 21 ); // current!
161 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
162 $rev->setTimestamp( '20180101000003' );
163
164 $text = "";
165 $text .= "* page:{{PAGENAME}}\n";
166 $text .= "* rev:{{REVISIONID}}\n";
167 $text .= "* user:{{REVISIONUSER}}\n";
168 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
169
170 $rev->setContent( 'main', new WikitextContent( $text ) );
171
172 $options = ParserOptions::newCanonical( 'canonical' );
173 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
174
175 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
176
177 $this->assertSame( $rev, $rr->getRevision() );
178 $this->assertSame( $options, $rr->getOptions() );
179
180 $html = $rr->getRevisionParserOutput()->getText();
181
182 $this->assertContains( 'page:' . __CLASS__, $html );
183 $this->assertContains( 'rev:21', $html );
184 $this->assertContains( 'user:Frank', $html );
185 $this->assertContains( 'time:20180101000003', $html );
186
187 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
188 }
189
190 public function testGetRevisionParserOutput_old() {
191 $title = $this->getMockTitle( 7, 21 );
192
193 $rev = new MutableRevisionRecord( $title );
194 $rev->setId( 11 ); // old!
195 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
196 $rev->setTimestamp( '20180101000003' );
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( 'main', new WikitextContent( $text ) );
205
206 $options = ParserOptions::newCanonical( 'canonical' );
207 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
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:11', $html );
218 $this->assertContains( 'user:Frank', $html );
219 $this->assertContains( 'time:20180101000003', $html );
220
221 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
222 }
223
224 public function testGetRevisionParserOutput_suppressed() {
225 $title = $this->getMockTitle( 7, 21 );
226
227 $rev = new MutableRevisionRecord( $title );
228 $rev->setId( 11 ); // old!
229 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
230 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
231 $rev->setTimestamp( '20180101000003' );
232
233 $text = "";
234 $text .= "* page:{{PAGENAME}}\n";
235 $text .= "* rev:{{REVISIONID}}\n";
236 $text .= "* user:{{REVISIONUSER}}\n";
237 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
238
239 $rev->setContent( 'main', new WikitextContent( $text ) );
240
241 $options = ParserOptions::newCanonical( 'canonical' );
242 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
243
244 $this->setExpectedException( SuppressedDataException::class );
245 $rr->getRevisionParserOutput();
246 }
247
248 public function testGetRevisionParserOutput_privileged() {
249 $title = $this->getMockTitle( 7, 21 );
250
251 $rev = new MutableRevisionRecord( $title );
252 $rev->setId( 11 ); // old!
253 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
254 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
255 $rev->setTimestamp( '20180101000003' );
256
257 $text = "";
258 $text .= "* page:{{PAGENAME}}\n";
259 $text .= "* rev:{{REVISIONID}}\n";
260 $text .= "* user:{{REVISIONUSER}}\n";
261 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
262
263 $rev->setContent( 'main', new WikitextContent( $text ) );
264
265 $options = ParserOptions::newCanonical( 'canonical' );
266 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
267 $rr = new RenderedRevision(
268 $title,
269 $rev,
270 $options,
271 $this->combinerCallback,
272 RevisionRecord::FOR_THIS_USER,
273 $sysop
274 );
275
276 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
277
278 $this->assertSame( $rev, $rr->getRevision() );
279 $this->assertSame( $options, $rr->getOptions() );
280
281 $html = $rr->getRevisionParserOutput()->getText();
282
283 // Suppressed content should be visible for sysops
284 $this->assertContains( 'page:' . __CLASS__, $html );
285 $this->assertContains( 'rev:11', $html );
286 $this->assertContains( 'user:Frank', $html );
287 $this->assertContains( 'time:20180101000003', $html );
288
289 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
290 }
291
292 public function testGetRevisionParserOutput_raw() {
293 $title = $this->getMockTitle( 7, 21 );
294
295 $rev = new MutableRevisionRecord( $title );
296 $rev->setId( 11 ); // old!
297 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
298 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
299 $rev->setTimestamp( '20180101000003' );
300
301 $text = "";
302 $text .= "* page:{{PAGENAME}}\n";
303 $text .= "* rev:{{REVISIONID}}\n";
304 $text .= "* user:{{REVISIONUSER}}\n";
305 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
306
307 $rev->setContent( 'main', new WikitextContent( $text ) );
308
309 $options = ParserOptions::newCanonical( 'canonical' );
310 $rr = new RenderedRevision(
311 $title,
312 $rev,
313 $options,
314 $this->combinerCallback,
315 RevisionRecord::RAW
316 );
317
318 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
319
320 $this->assertSame( $rev, $rr->getRevision() );
321 $this->assertSame( $options, $rr->getOptions() );
322
323 $html = $rr->getRevisionParserOutput()->getText();
324
325 // Suppressed content should be visible for sysops
326 $this->assertContains( 'page:' . __CLASS__, $html );
327 $this->assertContains( 'rev:11', $html );
328 $this->assertContains( 'user:Frank', $html );
329 $this->assertContains( 'time:20180101000003', $html );
330
331 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
332 }
333
334 public function testGetRevisionParserOutput_multi() {
335 $title = $this->getMockTitle( 7, 21 );
336
337 $rev = new MutableRevisionRecord( $title );
338 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
339 $rev->setTimestamp( '20180101000003' );
340
341 $rev->setContent( 'main', new WikitextContent( '[[Kittens]]' ) );
342 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
343
344 $options = ParserOptions::newCanonical( 'canonical' );
345 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
346
347 $combinedOutput = $rr->getRevisionParserOutput();
348 $mainOutput = $rr->getSlotParserOutput( 'main' );
349 $auxOutput = $rr->getSlotParserOutput( 'aux' );
350
351 $combinedHtml = $combinedOutput->getText();
352 $mainHtml = $mainOutput->getText();
353 $auxHtml = $auxOutput->getText();
354
355 $this->assertContains( 'Kittens', $mainHtml );
356 $this->assertContains( 'Goats', $auxHtml );
357 $this->assertNotContains( 'Goats', $mainHtml );
358 $this->assertNotContains( 'Kittens', $auxHtml );
359 $this->assertContains( 'Kittens', $combinedHtml );
360 $this->assertContains( 'Goats', $combinedHtml );
361 $this->assertContains( 'aux', $combinedHtml, 'slot section header' );
362
363 $combinedLinks = $combinedOutput->getLinks();
364 $mainLinks = $mainOutput->getLinks();
365 $auxLinks = $auxOutput->getLinks();
366 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Kittens'] ), 'links from main slot' );
367 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Goats'] ), 'links from aux slot' );
368 $this->assertFalse( isset( $mainLinks[NS_MAIN]['Goats'] ), 'no aux links in main' );
369 $this->assertFalse( isset( $auxLinks[NS_MAIN]['Kittens'] ), 'no main links in aux' );
370 }
371
372 public function testNoHtml() {
373 /** @var MockObject|Content $mockContent */
374 $mockContent = $this->getMockBuilder( WikitextContent::class )
375 ->setMethods( [ 'getParserOutput' ] )
376 ->setConstructorArgs( [ 'Whatever' ] )
377 ->getMock();
378 $mockContent->method( 'getParserOutput' )
379 ->willReturnCallback( function ( Title $title, $revId = null,
380 ParserOptions $options = null, $generateHtml = true
381 ) {
382 if ( !$generateHtml ) {
383 return new ParserOutput( null );
384 } else {
385 $this->fail( 'Should not be called with $generateHtml == true' );
386 return null; // never happens, make analyzer happy
387 }
388 } );
389
390 $title = $this->getMockTitle( 7, 21 );
391
392 $rev = new MutableRevisionRecord( $title );
393 $rev->setContent( 'main', $mockContent );
394 $rev->setContent( 'aux', $mockContent );
395
396 $options = ParserOptions::newCanonical( 'canonical' );
397 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
398
399 $output = $rr->getSlotParserOutput( 'main', [ 'generate-html' => false ] );
400 $this->assertFalse( $output->hasText(), 'hasText' );
401
402 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
403 $this->assertFalse( $output->hasText(), 'hasText' );
404 }
405
406 public function testUpdateRevision() {
407 $title = $this->getMockTitle( 7, 21 );
408
409 $rev = new MutableRevisionRecord( $title );
410
411 $text = "";
412 $text .= "* page:{{PAGENAME}}\n";
413 $text .= "* rev:{{REVISIONID}}\n";
414 $text .= "* user:{{REVISIONUSER}}\n";
415 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
416
417 $rev->setContent( 'main', new WikitextContent( $text ) );
418 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
419
420 $options = ParserOptions::newCanonical( 'canonical' );
421 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
422
423 $firstOutput = $rr->getRevisionParserOutput();
424 $mainOutput = $rr->getSlotParserOutput( 'main' );
425 $auxOutput = $rr->getSlotParserOutput( 'aux' );
426
427 // emulate a saved revision
428 $savedRev = new MutableRevisionRecord( $title );
429 $savedRev->setContent( 'main', new WikitextContent( $text ) );
430 $savedRev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
431 $savedRev->setId( 23 ); // saved, new
432 $savedRev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
433 $savedRev->setTimestamp( '20180101000003' );
434
435 $rr->updateRevision( $savedRev );
436
437 $this->assertNotSame( $mainOutput, $rr->getSlotParserOutput( 'main' ), 'Reset main' );
438 $this->assertSame( $auxOutput, $rr->getSlotParserOutput( 'aux' ), 'Keep aux' );
439
440 $updatedOutput = $rr->getRevisionParserOutput();
441 $html = $updatedOutput->getText();
442
443 $this->assertNotSame( $firstOutput, $updatedOutput, 'Reset merged' );
444 $this->assertContains( 'page:' . __CLASS__, $html );
445 $this->assertContains( 'rev:23', $html );
446 $this->assertContains( 'user:Frank', $html );
447 $this->assertContains( 'time:20180101000003', $html );
448 $this->assertContains( 'Goats', $html );
449
450 $rr->updateRevision( $savedRev ); // should do nothing
451 $this->assertSame( $updatedOutput, $rr->getRevisionParserOutput(), 'no more reset needed' );
452 }
453
454 }