96658678ba57811f9215aa1ef5c6cfaa2f88f0f2
[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\MutableRevisionRecord;
8 use MediaWiki\Revision\MutableRevisionSlots;
9 use MediaWiki\Revision\RenderedRevision;
10 use MediaWiki\Revision\RevisionArchiveRecord;
11 use MediaWiki\Revision\RevisionRecord;
12 use MediaWiki\Revision\RevisionStore;
13 use MediaWiki\Revision\RevisionStoreRecord;
14 use MediaWiki\Revision\SlotRecord;
15 use MediaWiki\Revision\SuppressedDataException;
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 User;
23 use Wikimedia\TestingAccessWrapper;
24 use WikitextContent;
25
26 /**
27 * @covers \MediaWiki\Revision\RenderedRevision
28 */
29 class RenderedRevisionTest extends MediaWikiTestCase {
30
31 /** @var callable */
32 private $combinerCallback;
33
34 public function setUp() {
35 parent::setUp();
36
37 $this->combinerCallback = function ( RenderedRevision $rr, array $hints = [] ) {
38 return $this->combineOutput( $rr, $hints );
39 };
40 }
41
42 private function combineOutput( RenderedRevision $rrev, array $hints = [] ) {
43 // NOTE: the is a slightly simplified version of RevisionRenderer::combineSlotOutput
44
45 $withHtml = $hints['generate-html'] ?? true;
46
47 $revision = $rrev->getRevision();
48 $slots = $revision->getSlots()->getSlots();
49
50 $combinedOutput = new ParserOutput( null );
51 $slotOutput = [];
52 foreach ( $slots as $role => $slot ) {
53 $out = $rrev->getSlotParserOutput( $role, $hints );
54 $slotOutput[$role] = $out;
55
56 $combinedOutput->mergeInternalMetaDataFrom( $out );
57 $combinedOutput->mergeTrackingMetaDataFrom( $out );
58 }
59
60 if ( $withHtml ) {
61 $html = '';
62 /** @var ParserOutput $out */
63 foreach ( $slotOutput as $role => $out ) {
64
65 if ( $html !== '' ) {
66 // skip header for the first slot
67 $html .= "(($role))";
68 }
69
70 $html .= $out->getRawText();
71 $combinedOutput->mergeHtmlMetaDataFrom( $out );
72 }
73
74 $combinedOutput->setText( $html );
75 }
76
77 return $combinedOutput;
78 }
79
80 /**
81 * @param int $articleId
82 * @param int $revisionId
83 * @return Title
84 */
85 private function getMockTitle( $articleId, $revisionId ) {
86 /** @var Title|MockObject $mock */
87 $mock = $this->getMockBuilder( Title::class )
88 ->disableOriginalConstructor()
89 ->getMock();
90 $mock->expects( $this->any() )
91 ->method( 'getNamespace' )
92 ->will( $this->returnValue( NS_MAIN ) );
93 $mock->expects( $this->any() )
94 ->method( 'getText' )
95 ->will( $this->returnValue( 'RenderTestPage' ) );
96 $mock->expects( $this->any() )
97 ->method( 'getPrefixedText' )
98 ->will( $this->returnValue( 'RenderTestPage' ) );
99 $mock->expects( $this->any() )
100 ->method( 'getDBkey' )
101 ->will( $this->returnValue( 'RenderTestPage' ) );
102 $mock->expects( $this->any() )
103 ->method( 'getArticleID' )
104 ->will( $this->returnValue( $articleId ) );
105 $mock->expects( $this->any() )
106 ->method( 'getLatestRevId' )
107 ->will( $this->returnValue( $revisionId ) );
108 $mock->expects( $this->any() )
109 ->method( 'getContentModel' )
110 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT ) );
111 $mock->expects( $this->any() )
112 ->method( 'getPageLanguage' )
113 ->will( $this->returnValue( Language::factory( 'en' ) ) );
114 $mock->expects( $this->any() )
115 ->method( 'isContentPage' )
116 ->will( $this->returnValue( true ) );
117 $mock->expects( $this->any() )
118 ->method( 'equals' )
119 ->willReturnCallback( function ( Title $other ) use ( $mock ) {
120 return $mock->getPrefixedText() === $other->getPrefixedText();
121 } );
122 $mock->expects( $this->any() )
123 ->method( 'userCan' )
124 ->willReturnCallback( function ( $perm, User $user ) use ( $mock ) {
125 return $user->isAllowed( $perm );
126 } );
127
128 return $mock;
129 }
130
131 /**
132 * @param string $class
133 * @param Title $title
134 * @param null|int $id
135 * @param int $visibility
136 * @return RevisionRecord
137 */
138 private function getMockRevision(
139 $class,
140 $title,
141 $id = null,
142 $visibility = 0,
143 array $content = null
144 ) {
145 $frank = new UserIdentityValue( 9, 'Frank', 0 );
146
147 if ( !$content ) {
148 $text = "";
149 $text .= "* page:{{PAGENAME}}!\n";
150 $text .= "* rev:{{REVISIONID}}!\n";
151 $text .= "* user:{{REVISIONUSER}}!\n";
152 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
153 $text .= "* [[Link It]]\n";
154
155 $content = [ 'main' => new WikitextContent( $text ) ];
156 }
157
158 /** @var MockObject|RevisionRecord $mock */
159 $mock = $this->getMockBuilder( $class )
160 ->disableOriginalConstructor()
161 ->setMethods( [
162 'getId',
163 'getPageId',
164 'getPageAsLinkTarget',
165 'getUser',
166 'getVisibility',
167 'getTimestamp',
168 ] )->getMock();
169
170 $mock->method( 'getId' )->willReturn( $id );
171 $mock->method( 'getPageId' )->willReturn( $title->getArticleID() );
172 $mock->method( 'getPageAsLinkTarget' )->willReturn( $title );
173 $mock->method( 'getUser' )->willReturn( $frank );
174 $mock->method( 'getVisibility' )->willReturn( $visibility );
175 $mock->method( 'getTimestamp' )->willReturn( '20180101000003' );
176
177 /** @var object $mockAccess */
178 $mockAccess = TestingAccessWrapper::newFromObject( $mock );
179 $mockAccess->mSlots = new MutableRevisionSlots();
180
181 foreach ( $content as $role => $cnt ) {
182 $mockAccess->mSlots->setContent( $role, $cnt );
183 }
184
185 return $mock;
186 }
187
188 public function testGetRevisionParserOutput_new() {
189 $title = $this->getMockTitle( 0, 21 );
190 $rev = $this->getMockRevision( RevisionStoreRecord::class, $title );
191
192 $options = ParserOptions::newCanonical( 'canonical' );
193 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
194
195 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
196
197 $this->assertSame( $rev, $rr->getRevision() );
198 $this->assertSame( $options, $rr->getOptions() );
199
200 $html = $rr->getRevisionParserOutput()->getText();
201
202 $this->assertContains( 'page:RenderTestPage!', $html );
203 $this->assertContains( 'user:Frank!', $html );
204 $this->assertContains( 'time:20180101000003!', $html );
205 }
206
207 public function testGetRevisionParserOutput_previewWithSelfTransclusion() {
208 $title = $this->getMockTitle( 0, 21 );
209 $name = $title->getPrefixedText();
210
211 $text = "(ONE)<includeonly>(TWO)</includeonly><noinclude>#{{:$name}}#</noinclude>";
212
213 $content = [
214 'main' => new WikitextContent( $text )
215 ];
216
217 $rev = $this->getMockRevision( RevisionStoreRecord::class, $title, null, 0, $content );
218
219 $options = ParserOptions::newCanonical( 'canonical' );
220 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
221
222 $html = $rr->getRevisionParserOutput()->getText();
223 $this->assertContains( '(ONE)#(ONE)(TWO)#', $html );
224 }
225
226 public function testGetRevisionParserOutput_current() {
227 $title = $this->getMockTitle( 7, 21 );
228 $rev = $this->getMockRevision( RevisionStoreRecord::class, $title, 21 );
229
230 $options = ParserOptions::newCanonical( 'canonical' );
231 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
232
233 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
234
235 $this->assertSame( $rev, $rr->getRevision() );
236 $this->assertSame( $options, $rr->getOptions() );
237
238 $html = $rr->getRevisionParserOutput()->getText();
239
240 $this->assertContains( 'page:RenderTestPage!', $html );
241 $this->assertContains( 'rev:21!', $html );
242 $this->assertContains( 'user:Frank!', $html );
243 $this->assertContains( 'time:20180101000003!', $html );
244
245 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
246 }
247
248 public function testGetRevisionParserOutput_old() {
249 $title = $this->getMockTitle( 7, 21 );
250 $rev = $this->getMockRevision( RevisionStoreRecord::class, $title, 11 );
251
252 $options = ParserOptions::newCanonical( 'canonical' );
253 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
254
255 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
256
257 $this->assertSame( $rev, $rr->getRevision() );
258 $this->assertSame( $options, $rr->getOptions() );
259
260 $html = $rr->getRevisionParserOutput()->getText();
261
262 $this->assertContains( 'page:RenderTestPage!', $html );
263 $this->assertContains( 'rev:11!', $html );
264 $this->assertContains( 'user:Frank!', $html );
265 $this->assertContains( 'time:20180101000003!', $html );
266
267 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
268 }
269
270 public function testGetRevisionParserOutput_archive() {
271 $title = $this->getMockTitle( 7, 21 );
272 $rev = $this->getMockRevision( RevisionArchiveRecord::class, $title, 11 );
273
274 $options = ParserOptions::newCanonical( 'canonical' );
275 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
276
277 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
278
279 $this->assertSame( $rev, $rr->getRevision() );
280 $this->assertSame( $options, $rr->getOptions() );
281
282 $html = $rr->getRevisionParserOutput()->getText();
283
284 $this->assertContains( 'page:RenderTestPage!', $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( SlotRecord::MAIN )->getText() );
290 }
291
292 public function testGetRevisionParserOutput_suppressed() {
293 $title = $this->getMockTitle( 7, 21 );
294 $rev = $this->getMockRevision(
295 RevisionStoreRecord::class,
296 $title,
297 11,
298 RevisionRecord::DELETED_TEXT
299 );
300
301 $options = ParserOptions::newCanonical( 'canonical' );
302 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
303
304 $this->setExpectedException( SuppressedDataException::class );
305 $rr->getRevisionParserOutput();
306 }
307
308 public function testGetRevisionParserOutput_privileged() {
309 $title = $this->getMockTitle( 7, 21 );
310 $rev = $this->getMockRevision(
311 RevisionStoreRecord::class,
312 $title,
313 11,
314 RevisionRecord::DELETED_TEXT
315 );
316
317 $options = ParserOptions::newCanonical( 'canonical' );
318 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
319 $rr = new RenderedRevision(
320 $title,
321 $rev,
322 $options,
323 $this->combinerCallback,
324 RevisionRecord::FOR_THIS_USER,
325 $sysop
326 );
327
328 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
329
330 $this->assertSame( $rev, $rr->getRevision() );
331 $this->assertSame( $options, $rr->getOptions() );
332
333 $html = $rr->getRevisionParserOutput()->getText();
334
335 // Suppressed content should be visible for sysops
336 $this->assertContains( 'page:RenderTestPage!', $html );
337 $this->assertContains( 'rev:11!', $html );
338 $this->assertContains( 'user:Frank!', $html );
339 $this->assertContains( 'time:20180101000003!', $html );
340
341 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
342 }
343
344 public function testGetRevisionParserOutput_raw() {
345 $title = $this->getMockTitle( 7, 21 );
346 $rev = $this->getMockRevision(
347 RevisionStoreRecord::class,
348 $title,
349 11,
350 RevisionRecord::DELETED_TEXT
351 );
352
353 $options = ParserOptions::newCanonical( 'canonical' );
354 $rr = new RenderedRevision(
355 $title,
356 $rev,
357 $options,
358 $this->combinerCallback,
359 RevisionRecord::RAW
360 );
361
362 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
363
364 $this->assertSame( $rev, $rr->getRevision() );
365 $this->assertSame( $options, $rr->getOptions() );
366
367 $html = $rr->getRevisionParserOutput()->getText();
368
369 // Suppressed content should be visible for sysops
370 $this->assertContains( 'page:RenderTestPage!', $html );
371 $this->assertContains( 'rev:11!', $html );
372 $this->assertContains( 'user:Frank!', $html );
373 $this->assertContains( 'time:20180101000003!', $html );
374
375 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
376 }
377
378 public function testGetRevisionParserOutput_multi() {
379 $content = [
380 'main' => new WikitextContent( '[[Kittens]]' ),
381 'aux' => new WikitextContent( '[[Goats]]' ),
382 ];
383
384 $title = $this->getMockTitle( 7, 21 );
385 $rev = $this->getMockRevision( RevisionStoreRecord::class, $title, 11, 0, $content );
386
387 $options = ParserOptions::newCanonical( 'canonical' );
388 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
389
390 $combinedOutput = $rr->getRevisionParserOutput();
391 $mainOutput = $rr->getSlotParserOutput( SlotRecord::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 section header' );
405
406 $combinedLinks = $combinedOutput->getLinks();
407 $mainLinks = $mainOutput->getLinks();
408 $auxLinks = $auxOutput->getLinks();
409 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Kittens'] ), 'links from main slot' );
410 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Goats'] ), 'links from aux slot' );
411 $this->assertFalse( isset( $mainLinks[NS_MAIN]['Goats'] ), 'no aux links in main' );
412 $this->assertFalse( isset( $auxLinks[NS_MAIN]['Kittens'] ), 'no main links in aux' );
413 }
414
415 public function testGetRevisionParserOutput_incompleteNoId() {
416 $title = $this->getMockTitle( 7, 21 );
417
418 $rev = new MutableRevisionRecord( $title );
419
420 $text = "";
421 $text .= "* page:{{PAGENAME}}!\n";
422 $text .= "* rev:{{REVISIONID}}!\n";
423 $text .= "* user:{{REVISIONUSER}}!\n";
424 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
425
426 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
427
428 $options = ParserOptions::newCanonical( 'canonical' );
429 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
430
431 // MutableRevisionRecord without ID should be used by the parser.
432 // USeful for fake
433 $html = $rr->getRevisionParserOutput()->getText();
434
435 $this->assertContains( 'page:RenderTestPage!', $html );
436 $this->assertContains( 'rev:!', $html );
437 $this->assertContains( 'user:!', $html );
438 $this->assertContains( 'time:!', $html );
439 }
440
441 public function testGetRevisionParserOutput_incompleteWithId() {
442 $title = $this->getMockTitle( 7, 21 );
443
444 $rev = new MutableRevisionRecord( $title );
445 $rev->setId( 21 );
446
447 $text = "";
448 $text .= "* page:{{PAGENAME}}!\n";
449 $text .= "* rev:{{REVISIONID}}!\n";
450 $text .= "* user:{{REVISIONUSER}}!\n";
451 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
452
453 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
454
455 $actualRevision = $this->getMockRevision(
456 RevisionStoreRecord::class,
457 $title,
458 21,
459 RevisionRecord::DELETED_TEXT
460 );
461
462 $options = ParserOptions::newCanonical( 'canonical' );
463 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
464
465 // MutableRevisionRecord with ID should not be used by the parser,
466 // revision should be loaded instead!
467 $revisionStore = $this->getMockBuilder( RevisionStore::class )
468 ->disableOriginalConstructor()
469 ->getMock();
470
471 $revisionStore->expects( $this->once() )
472 ->method( 'getKnownCurrentRevision' )
473 ->with( $title, 0 )
474 ->willReturn( $actualRevision );
475
476 $this->setService( 'RevisionStore', $revisionStore );
477
478 $html = $rr->getRevisionParserOutput()->getText();
479
480 $this->assertContains( 'page:RenderTestPage!', $html );
481 $this->assertContains( 'rev:21!', $html );
482 $this->assertContains( 'user:Frank!', $html );
483 $this->assertContains( 'time:20180101000003!', $html );
484 }
485
486 public function testSetRevisionParserOutput() {
487 $title = $this->getMockTitle( 3, 21 );
488 $rev = $this->getMockRevision( RevisionStoreRecord::class, $title );
489
490 $options = ParserOptions::newCanonical( 'canonical' );
491 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
492
493 $output = new ParserOutput( 'Kittens' );
494 $rr->setRevisionParserOutput( $output );
495
496 $this->assertSame( $output, $rr->getRevisionParserOutput() );
497 $this->assertSame( 'Kittens', $rr->getRevisionParserOutput()->getText() );
498
499 $this->assertSame( $output, $rr->getSlotParserOutput( SlotRecord::MAIN ) );
500 $this->assertSame( 'Kittens', $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
501 }
502
503 public function testNoHtml() {
504 /** @var MockObject|Content $mockContent */
505 $mockContent = $this->getMockBuilder( WikitextContent::class )
506 ->setMethods( [ 'getParserOutput' ] )
507 ->setConstructorArgs( [ 'Whatever' ] )
508 ->getMock();
509 $mockContent->method( 'getParserOutput' )
510 ->willReturnCallback( function ( Title $title, $revId = null,
511 ParserOptions $options = null, $generateHtml = true
512 ) {
513 if ( !$generateHtml ) {
514 return new ParserOutput( null );
515 } else {
516 $this->fail( 'Should not be called with $generateHtml == true' );
517 return null; // never happens, make analyzer happy
518 }
519 } );
520
521 $title = $this->getMockTitle( 7, 21 );
522
523 $rev = new MutableRevisionRecord( $title );
524 $rev->setContent( SlotRecord::MAIN, $mockContent );
525 $rev->setContent( 'aux', $mockContent );
526
527 $options = ParserOptions::newCanonical( 'canonical' );
528 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
529
530 $output = $rr->getSlotParserOutput( SlotRecord::MAIN, [ 'generate-html' => false ] );
531 $this->assertFalse( $output->hasText(), 'hasText' );
532
533 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
534 $this->assertFalse( $output->hasText(), 'hasText' );
535 }
536
537 public function testUpdateRevision() {
538 $title = $this->getMockTitle( 7, 21 );
539
540 $rev = new MutableRevisionRecord( $title );
541
542 $text = "";
543 $text .= "* page:{{PAGENAME}}!\n";
544 $text .= "* rev:{{REVISIONID}}!\n";
545 $text .= "* user:{{REVISIONUSER}}!\n";
546 $text .= "* time:{{REVISIONTIMESTAMP}}!\n";
547
548 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
549 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
550
551 $options = ParserOptions::newCanonical( 'canonical' );
552 $rr = new RenderedRevision( $title, $rev, $options, $this->combinerCallback );
553
554 $firstOutput = $rr->getRevisionParserOutput();
555 $mainOutput = $rr->getSlotParserOutput( SlotRecord::MAIN );
556 $auxOutput = $rr->getSlotParserOutput( 'aux' );
557
558 // emulate a saved revision
559 $savedRev = new MutableRevisionRecord( $title );
560 $savedRev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
561 $savedRev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
562 $savedRev->setId( 23 ); // saved, new
563 $savedRev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
564 $savedRev->setTimestamp( '20180101000003' );
565
566 $rr->updateRevision( $savedRev );
567
568 $this->assertNotSame( $mainOutput, $rr->getSlotParserOutput( SlotRecord::MAIN ), 'Reset main' );
569 $this->assertSame( $auxOutput, $rr->getSlotParserOutput( 'aux' ), 'Keep aux' );
570
571 $updatedOutput = $rr->getRevisionParserOutput();
572 $html = $updatedOutput->getText();
573
574 $this->assertNotSame( $firstOutput, $updatedOutput, 'Reset merged' );
575 $this->assertContains( 'page:RenderTestPage!', $html );
576 $this->assertContains( 'rev:23!', $html );
577 $this->assertContains( 'user:Frank!', $html );
578 $this->assertContains( 'time:20180101000003!', $html );
579 $this->assertContains( 'Goats', $html );
580
581 $rr->updateRevision( $savedRev ); // should do nothing
582 $this->assertSame( $updatedOutput, $rr->getRevisionParserOutput(), 'no more reset needed' );
583 }
584
585 }