MediaWiki  master
RevisionRendererTest.php
Go to the documentation of this file.
1 <?php
2 
4 
21 use Title;
25 
30 
36  private function getMockTitle( $articleId, $revisionId ) {
38  $mock = $this->getMockBuilder( Title::class )
39  ->disableOriginalConstructor()
40  ->getMock();
41  $mock->expects( $this->any() )
42  ->method( 'getNamespace' )
43  ->will( $this->returnValue( NS_MAIN ) );
44  $mock->expects( $this->any() )
45  ->method( 'getText' )
46  ->will( $this->returnValue( __CLASS__ ) );
47  $mock->expects( $this->any() )
48  ->method( 'getPrefixedText' )
49  ->will( $this->returnValue( __CLASS__ ) );
50  $mock->expects( $this->any() )
51  ->method( 'getDBkey' )
52  ->will( $this->returnValue( __CLASS__ ) );
53  $mock->expects( $this->any() )
54  ->method( 'getArticleID' )
55  ->will( $this->returnValue( $articleId ) );
56  $mock->expects( $this->any() )
57  ->method( 'getLatestRevId' )
58  ->will( $this->returnValue( $revisionId ) );
59  $mock->expects( $this->any() )
60  ->method( 'getContentModel' )
61  ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT ) );
62  $mock->expects( $this->any() )
63  ->method( 'getPageLanguage' )
64  ->will( $this->returnValue( Language::factory( 'en' ) ) );
65  $mock->expects( $this->any() )
66  ->method( 'isContentPage' )
67  ->will( $this->returnValue( true ) );
68  $mock->expects( $this->any() )
69  ->method( 'equals' )
70  ->willReturnCallback(
71  function ( Title $other ) use ( $mock ) {
72  return $mock->getArticleID() === $other->getArticleID();
73  }
74  );
75  $mock->expects( $this->any() )
76  ->method( 'getRestrictions' )
77  ->willReturn( [] );
78 
79  return $mock;
80  }
81 
88  private function getMockDatabaseConnection( $maxRev = 100, $linkCount = 0 ) {
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 
110  private function newRevisionRenderer( $maxRev = 100, $useMaster = false ) {
111  $dbIndex = $useMaster ? DB_MASTER : DB_REPLICA;
112 
113  $db = $this->getMockDatabaseConnection( $maxRev );
114 
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 
128  $slotRoles = $this->getMockBuilder( NameTableStore::class )
129  ->disableOriginalConstructor()
130  ->getMock();
131  $slotRoles->method( 'getMap' )
132  ->willReturn( [] );
133 
134  $roleReg = new SlotRoleRegistry( $slotRoles );
135  $roleReg->defineRole( 'main', function () {
136  return new MainSlotRoleHandler( [] );
137  } );
138  $roleReg->defineRoleWithModel( 'aux', CONTENT_MODEL_WIKITEXT );
139 
140  return new RevisionRenderer( $lb, $roleReg );
141  }
142 
143  private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
144  if ( [ $table, $fields, $cond ] === [ 'revision', 'MAX(rev_id)', [] ] ) {
145  return $maxRev;
146  }
147 
148  $this->fail( 'Unexpected call to selectField' );
149  throw new LogicException( 'Ooops' ); // Can't happen, make analyzer happy
150  }
151 
152  public function testGetRenderedRevision_new() {
153  $renderer = $this->newRevisionRenderer( 100 );
154  $title = $this->getMockTitle( 7, 21 );
155 
157  $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
158  $rev->setTimestamp( '20180101000003' );
159  $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
160 
161  $text = "";
162  $text .= "* page:{{PAGENAME}}\n";
163  $text .= "* rev:{{REVISIONID}}\n";
164  $text .= "* user:{{REVISIONUSER}}\n";
165  $text .= "* time:{{REVISIONTIMESTAMP}}\n";
166  $text .= "* [[Link It]]\n";
167 
168  $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
169 
170  $options = ParserOptions::newCanonical( 'canonical' );
171  $rr = $renderer->getRenderedRevision( $rev, $options );
172 
173  $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
174 
175  $this->assertSame( $rev, $rr->getRevision() );
176  $this->assertSame( $options, $rr->getOptions() );
177 
178  $html = $rr->getRevisionParserOutput()->getText();
179 
180  $this->assertContains( 'page:' . __CLASS__, $html );
181  $this->assertContains( 'rev:101', $html ); // from speculativeRevIdCallback
182  $this->assertContains( 'user:Frank', $html );
183  $this->assertContains( 'time:20180101000003', $html );
184 
185  $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
186  }
187 
189  $renderer = $this->newRevisionRenderer( 100 );
190  $title = $this->getMockTitle( 7, 21 );
191 
193  $rev->setId( 21 ); // current!
194  $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
195  $rev->setTimestamp( '20180101000003' );
196  $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
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( SlotRecord::MAIN, new WikitextContent( $text ) );
205 
206  $options = ParserOptions::newCanonical( 'canonical' );
207  $rr = $renderer->getRenderedRevision( $rev, $options );
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:21', $html );
218  $this->assertContains( 'user:Frank', $html );
219  $this->assertContains( 'time:20180101000003', $html );
220 
221  $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
222  }
223 
224  public function testGetRenderedRevision_master() {
225  $renderer = $this->newRevisionRenderer( 100, true ); // use master
226  $title = $this->getMockTitle( 7, 21 );
227 
229  $rev->setId( 21 ); // current!
230  $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
231  $rev->setTimestamp( '20180101000003' );
232  $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
233 
234  $text = "";
235  $text .= "* page:{{PAGENAME}}\n";
236  $text .= "* rev:{{REVISIONID}}\n";
237  $text .= "* user:{{REVISIONUSER}}\n";
238  $text .= "* time:{{REVISIONTIMESTAMP}}\n";
239 
240  $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
241 
242  $options = ParserOptions::newCanonical( 'canonical' );
243  $rr = $renderer->getRenderedRevision( $rev, $options, null, [ 'use-master' => true ] );
244 
245  $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
246 
247  $html = $rr->getRevisionParserOutput()->getText();
248 
249  $this->assertContains( 'rev:21', $html );
250 
251  $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
252  }
253 
254  public function testGetRenderedRevision_known() {
255  $renderer = $this->newRevisionRenderer( 100, true ); // use master
256  $title = $this->getMockTitle( 7, 21 );
257 
259  $rev->setId( 21 ); // current!
260  $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
261  $rev->setTimestamp( '20180101000003' );
262  $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
263 
264  $text = "uncached text";
265  $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
266 
267  $output = new ParserOutput( 'cached text' );
268 
269  $options = ParserOptions::newCanonical( 'canonical' );
270  $rr = $renderer->getRenderedRevision(
271  $rev,
272  $options,
273  null,
274  [ 'known-revision-output' => $output ]
275  );
276 
277  $this->assertSame( $output, $rr->getRevisionParserOutput() );
278  $this->assertSame( 'cached text', $rr->getRevisionParserOutput()->getText() );
279  $this->assertSame( 'cached text', $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
280  }
281 
282  public function testGetRenderedRevision_old() {
283  $renderer = $this->newRevisionRenderer( 100 );
284  $title = $this->getMockTitle( 7, 21 );
285 
287  $rev->setId( 11 ); // old!
288  $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
289  $rev->setTimestamp( '20180101000003' );
290  $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
291 
292  $text = "";
293  $text .= "* page:{{PAGENAME}}\n";
294  $text .= "* rev:{{REVISIONID}}\n";
295  $text .= "* user:{{REVISIONUSER}}\n";
296  $text .= "* time:{{REVISIONTIMESTAMP}}\n";
297 
298  $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
299 
300  $options = ParserOptions::newCanonical( 'canonical' );
301  $rr = $renderer->getRenderedRevision( $rev, $options );
302 
303  $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
304 
305  $this->assertSame( $rev, $rr->getRevision() );
306  $this->assertSame( $options, $rr->getOptions() );
307 
308  $html = $rr->getRevisionParserOutput()->getText();
309 
310  $this->assertContains( 'page:' . __CLASS__, $html );
311  $this->assertContains( 'rev:11', $html );
312  $this->assertContains( 'user:Frank', $html );
313  $this->assertContains( 'time:20180101000003', $html );
314 
315  $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
316  }
317 
319  $renderer = $this->newRevisionRenderer( 100 );
320  $title = $this->getMockTitle( 7, 21 );
321 
323  $rev->setId( 11 ); // old!
324  $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
325  $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
326  $rev->setTimestamp( '20180101000003' );
327  $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
328 
329  $text = "";
330  $text .= "* page:{{PAGENAME}}\n";
331  $text .= "* rev:{{REVISIONID}}\n";
332  $text .= "* user:{{REVISIONUSER}}\n";
333  $text .= "* time:{{REVISIONTIMESTAMP}}\n";
334 
335  $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
336 
337  $options = ParserOptions::newCanonical( 'canonical' );
338  $rr = $renderer->getRenderedRevision( $rev, $options );
339 
340  $this->assertNull( $rr, 'getRenderedRevision' );
341  }
342 
344  $renderer = $this->newRevisionRenderer( 100 );
345  $title = $this->getMockTitle( 7, 21 );
346 
348  $rev->setId( 11 ); // old!
349  $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
350  $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
351  $rev->setTimestamp( '20180101000003' );
352  $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
353 
354  $text = "";
355  $text .= "* page:{{PAGENAME}}\n";
356  $text .= "* rev:{{REVISIONID}}\n";
357  $text .= "* user:{{REVISIONUSER}}\n";
358  $text .= "* time:{{REVISIONTIMESTAMP}}\n";
359 
360  $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
361 
362  $options = ParserOptions::newCanonical( 'canonical' );
363  $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
364  $rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
365 
366  $this->assertNotNull( $rr, 'getRenderedRevision' );
367  $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
368 
369  $this->assertSame( $rev, $rr->getRevision() );
370  $this->assertSame( $options, $rr->getOptions() );
371 
372  $html = $rr->getRevisionParserOutput()->getText();
373 
374  // Suppressed content should be visible for sysops
375  $this->assertContains( 'page:' . __CLASS__, $html );
376  $this->assertContains( 'rev:11', $html );
377  $this->assertContains( 'user:Frank', $html );
378  $this->assertContains( 'time:20180101000003', $html );
379 
380  $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
381  }
382 
383  public function testGetRenderedRevision_raw() {
384  $renderer = $this->newRevisionRenderer( 100 );
385  $title = $this->getMockTitle( 7, 21 );
386 
388  $rev->setId( 11 ); // old!
389  $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
390  $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
391  $rev->setTimestamp( '20180101000003' );
392  $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
393 
394  $text = "";
395  $text .= "* page:{{PAGENAME}}\n";
396  $text .= "* rev:{{REVISIONID}}\n";
397  $text .= "* user:{{REVISIONUSER}}\n";
398  $text .= "* time:{{REVISIONTIMESTAMP}}\n";
399 
400  $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
401 
402  $options = ParserOptions::newCanonical( 'canonical' );
403  $rr = $renderer->getRenderedRevision(
404  $rev,
405  $options,
406  null,
407  [ 'audience' => RevisionRecord::RAW ]
408  );
409 
410  $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
411 
412  $this->assertSame( $rev, $rr->getRevision() );
413  $this->assertSame( $options, $rr->getOptions() );
414 
415  $html = $rr->getRevisionParserOutput()->getText();
416 
417  // Suppressed content should be visible in raw mode
418  $this->assertContains( 'page:' . __CLASS__, $html );
419  $this->assertContains( 'rev:11', $html );
420  $this->assertContains( 'user:Frank', $html );
421  $this->assertContains( 'time:20180101000003', $html );
422 
423  $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
424  }
425 
426  public function testGetRenderedRevision_multi() {
427  $renderer = $this->newRevisionRenderer();
428  $title = $this->getMockTitle( 7, 21 );
429 
431  $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
432  $rev->setTimestamp( '20180101000003' );
433  $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
434 
435  $rev->setContent( SlotRecord::MAIN, new WikitextContent( '[[Kittens]]' ) );
436  $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
437 
438  $rr = $renderer->getRenderedRevision( $rev );
439 
440  $combinedOutput = $rr->getRevisionParserOutput();
441  $mainOutput = $rr->getSlotParserOutput( SlotRecord::MAIN );
442  $auxOutput = $rr->getSlotParserOutput( 'aux' );
443 
444  $combinedHtml = $combinedOutput->getText();
445  $mainHtml = $mainOutput->getText();
446  $auxHtml = $auxOutput->getText();
447 
448  $this->assertContains( 'Kittens', $mainHtml );
449  $this->assertContains( 'Goats', $auxHtml );
450  $this->assertNotContains( 'Goats', $mainHtml );
451  $this->assertNotContains( 'Kittens', $auxHtml );
452  $this->assertContains( 'Kittens', $combinedHtml );
453  $this->assertContains( 'Goats', $combinedHtml );
454  $this->assertContains( '>aux<', $combinedHtml, 'slot header' );
455  $this->assertNotContains( '<mw:slotheader', $combinedHtml, 'slot header placeholder' );
456 
457  // make sure output wrapping works right
458  $this->assertContains( 'class="mw-parser-output"', $mainHtml );
459  $this->assertContains( 'class="mw-parser-output"', $auxHtml );
460  $this->assertContains( 'class="mw-parser-output"', $combinedHtml );
461 
462  // there should be only one wrapper div
463  $this->assertSame( 1, preg_match_all( '#class="mw-parser-output"#', $combinedHtml ) );
464  $this->assertNotContains( 'class="mw-parser-output"', $combinedOutput->getRawText() );
465 
466  $combinedLinks = $combinedOutput->getLinks();
467  $mainLinks = $mainOutput->getLinks();
468  $auxLinks = $auxOutput->getLinks();
469  $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Kittens'] ), 'links from main slot' );
470  $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Goats'] ), 'links from aux slot' );
471  $this->assertFalse( isset( $mainLinks[NS_MAIN]['Goats'] ), 'no aux links in main' );
472  $this->assertFalse( isset( $auxLinks[NS_MAIN]['Kittens'] ), 'no main links in aux' );
473  }
474 
475  public function testGetRenderedRevision_noHtml() {
477  $mockContent = $this->getMockBuilder( WikitextContent::class )
478  ->setMethods( [ 'getParserOutput' ] )
479  ->setConstructorArgs( [ 'Whatever' ] )
480  ->getMock();
481  $mockContent->method( 'getParserOutput' )
482  ->willReturnCallback( function ( Title $title, $revId = null,
483  ParserOptions $options = null, $generateHtml = true
484  ) {
485  if ( !$generateHtml ) {
486  return new ParserOutput( null );
487  } else {
488  $this->fail( 'Should not be called with $generateHtml == true' );
489  return null; // never happens, make analyzer happy
490  }
491  } );
492 
493  $renderer = $this->newRevisionRenderer();
494  $title = $this->getMockTitle( 7, 21 );
495 
496  $rev = new MutableRevisionRecord( $title );
497  $rev->setContent( SlotRecord::MAIN, $mockContent );
498  $rev->setContent( 'aux', $mockContent );
499 
500  // NOTE: we are testing the private combineSlotOutput() callback here.
501  $rr = $renderer->getRenderedRevision( $rev );
502 
503  $output = $rr->getSlotParserOutput( SlotRecord::MAIN, [ 'generate-html' => false ] );
504  $this->assertFalse( $output->hasText(), 'hasText' );
505 
506  $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
507  $this->assertFalse( $output->hasText(), 'hasText' );
508  }
509 
510 }
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:1971
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3025
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:215
const NS_MAIN
Definition: Defines.php:60
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
The RevisionRenderer service provides access to rendered output for revisions.
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page...
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
const DB_MASTER
Definition: defines.php:26
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place $output
Definition: hooks.txt:2204
Mutable RevisionRecord implementation, for building new revision entries programmatically.
static newCanonical( $context=null, $userLang=null)
Creates a "canonical" ParserOptions object.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1971
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:767
Value object representing a user&#39;s identity.
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:912
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:216
selectFieldCallback( $table, $fields, $cond, $maxRev)
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1747
A SlotRoleHandler for the main slot.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
newRevisionRenderer( $maxRev=100, $useMaster=false)
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
\MediaWiki\Revision\RevisionRenderer
const DB_REPLICA
Definition: defines.php:25