MediaWiki REL1_32
RevisionRendererTest.php
Go to the documentation of this file.
1<?php
2
4
8use LogicException;
17use PHPUnit\Framework\MockObject\MockObject;
19use User;
23
28
34 private function getMockTitle( $articleId, $revisionId ) {
36 $mock = $this->getMockBuilder( Title::class )
37 ->disableOriginalConstructor()
38 ->getMock();
39 $mock->expects( $this->any() )
40 ->method( 'getNamespace' )
41 ->will( $this->returnValue( NS_MAIN ) );
42 $mock->expects( $this->any() )
43 ->method( 'getText' )
44 ->will( $this->returnValue( __CLASS__ ) );
45 $mock->expects( $this->any() )
46 ->method( 'getPrefixedText' )
47 ->will( $this->returnValue( __CLASS__ ) );
48 $mock->expects( $this->any() )
49 ->method( 'getDBkey' )
50 ->will( $this->returnValue( __CLASS__ ) );
51 $mock->expects( $this->any() )
52 ->method( 'getArticleID' )
53 ->will( $this->returnValue( $articleId ) );
54 $mock->expects( $this->any() )
55 ->method( 'getLatestRevId' )
56 ->will( $this->returnValue( $revisionId ) );
57 $mock->expects( $this->any() )
58 ->method( 'getContentModel' )
59 ->will( $this->returnValue( CONTENT_MODEL_WIKITEXT ) );
60 $mock->expects( $this->any() )
61 ->method( 'getPageLanguage' )
62 ->will( $this->returnValue( Language::factory( 'en' ) ) );
63 $mock->expects( $this->any() )
64 ->method( 'isContentPage' )
65 ->will( $this->returnValue( true ) );
66 $mock->expects( $this->any() )
67 ->method( 'equals' )
68 ->willReturnCallback(
69 function ( Title $other ) use ( $mock ) {
70 return $mock->getArticleID() === $other->getArticleID();
71 }
72 );
73 $mock->expects( $this->any() )
74 ->method( 'userCan' )
75 ->willReturnCallback(
76 function ( $perm, User $user ) use ( $mock ) {
77 return $user->isAllowed( $perm );
78 }
79 );
80
81 return $mock;
82 }
83
90 private function getMockDatabaseConnection( $maxRev = 100, $linkCount = 0 ) {
92 $db = $this->getMock( IDatabase::class );
93 $db->method( 'selectField' )
94 ->willReturnCallback(
95 function ( $table, $fields, $cond ) use ( $maxRev, $linkCount ) {
96 return $this->selectFieldCallback(
97 $table,
98 $fields,
99 $cond,
100 $maxRev,
101 $linkCount
102 );
103 }
104 );
105
106 return $db;
107 }
108
112 private function newRevisionRenderer( $maxRev = 100, $useMaster = false ) {
113 $dbIndex = $useMaster ? DB_MASTER : DB_REPLICA;
114
115 $db = $this->getMockDatabaseConnection( $maxRev );
116
118 $lb = $this->getMock( ILoadBalancer::class );
119 $lb->method( 'getConnection' )
120 ->with( $dbIndex )
121 ->willReturn( $db );
122 $lb->method( 'getConnectionRef' )
123 ->with( $dbIndex )
124 ->willReturn( $db );
125 $lb->method( 'getLazyConnectionRef' )
126 ->with( $dbIndex )
127 ->willReturn( $db );
128
129 return new RevisionRenderer( $lb );
130 }
131
132 private function selectFieldCallback( $table, $fields, $cond, $maxRev ) {
133 if ( [ $table, $fields, $cond ] === [ 'revision', 'MAX(rev_id)', [] ] ) {
134 return $maxRev;
135 }
136
137 $this->fail( 'Unexpected call to selectField' );
138 throw new LogicException( 'Ooops' ); // Can't happen, make analyzer happy
139 }
140
141 public function testGetRenderedRevision_new() {
142 $renderer = $this->newRevisionRenderer( 100 );
143 $title = $this->getMockTitle( 7, 21 );
144
146 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
147 $rev->setTimestamp( '20180101000003' );
148 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
149
150 $text = "";
151 $text .= "* page:{{PAGENAME}}\n";
152 $text .= "* rev:{{REVISIONID}}\n";
153 $text .= "* user:{{REVISIONUSER}}\n";
154 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
155 $text .= "* [[Link It]]\n";
156
157 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
158
159 $options = ParserOptions::newCanonical( 'canonical' );
160 $rr = $renderer->getRenderedRevision( $rev, $options );
161
162 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
163
164 $this->assertSame( $rev, $rr->getRevision() );
165 $this->assertSame( $options, $rr->getOptions() );
166
167 $html = $rr->getRevisionParserOutput()->getText();
168
169 $this->assertContains( 'page:' . __CLASS__, $html );
170 $this->assertContains( 'rev:101', $html ); // from speculativeRevIdCallback
171 $this->assertContains( 'user:Frank', $html );
172 $this->assertContains( 'time:20180101000003', $html );
173
174 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
175 }
176
178 $renderer = $this->newRevisionRenderer( 100 );
179 $title = $this->getMockTitle( 7, 21 );
180
182 $rev->setId( 21 ); // current!
183 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
184 $rev->setTimestamp( '20180101000003' );
185 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
186
187 $text = "";
188 $text .= "* page:{{PAGENAME}}\n";
189 $text .= "* rev:{{REVISIONID}}\n";
190 $text .= "* user:{{REVISIONUSER}}\n";
191 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
192
193 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
194
195 $options = ParserOptions::newCanonical( 'canonical' );
196 $rr = $renderer->getRenderedRevision( $rev, $options );
197
198 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
199
200 $this->assertSame( $rev, $rr->getRevision() );
201 $this->assertSame( $options, $rr->getOptions() );
202
203 $html = $rr->getRevisionParserOutput()->getText();
204
205 $this->assertContains( 'page:' . __CLASS__, $html );
206 $this->assertContains( 'rev:21', $html );
207 $this->assertContains( 'user:Frank', $html );
208 $this->assertContains( 'time:20180101000003', $html );
209
210 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
211 }
212
214 $renderer = $this->newRevisionRenderer( 100, true ); // use master
215 $title = $this->getMockTitle( 7, 21 );
216
218 $rev->setId( 21 ); // current!
219 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
220 $rev->setTimestamp( '20180101000003' );
221 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
222
223 $text = "";
224 $text .= "* page:{{PAGENAME}}\n";
225 $text .= "* rev:{{REVISIONID}}\n";
226 $text .= "* user:{{REVISIONUSER}}\n";
227 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
228
229 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
230
231 $options = ParserOptions::newCanonical( 'canonical' );
232 $rr = $renderer->getRenderedRevision( $rev, $options, null, [ 'use-master' => true ] );
233
234 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
235
236 $html = $rr->getRevisionParserOutput()->getText();
237
238 $this->assertContains( 'rev:21', $html );
239
240 $this->assertSame( $html, $rr->getSlotParserOutput( SlotRecord::MAIN )->getText() );
241 }
242
243 public function testGetRenderedRevision_old() {
244 $renderer = $this->newRevisionRenderer( 100 );
245 $title = $this->getMockTitle( 7, 21 );
246
248 $rev->setId( 11 ); // old!
249 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
250 $rev->setTimestamp( '20180101000003' );
251 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
252
253 $text = "";
254 $text .= "* page:{{PAGENAME}}\n";
255 $text .= "* rev:{{REVISIONID}}\n";
256 $text .= "* user:{{REVISIONUSER}}\n";
257 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
258
259 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
260
261 $options = ParserOptions::newCanonical( 'canonical' );
262 $rr = $renderer->getRenderedRevision( $rev, $options );
263
264 $this->assertFalse( $rr->isContentDeleted(), 'isContentDeleted' );
265
266 $this->assertSame( $rev, $rr->getRevision() );
267 $this->assertSame( $options, $rr->getOptions() );
268
269 $html = $rr->getRevisionParserOutput()->getText();
270
271 $this->assertContains( 'page:' . __CLASS__, $html );
272 $this->assertContains( 'rev:11', $html );
273 $this->assertContains( 'user:Frank', $html );
274 $this->assertContains( 'time:20180101000003', $html );
275
276 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
277 }
278
280 $renderer = $this->newRevisionRenderer( 100 );
281 $title = $this->getMockTitle( 7, 21 );
282
284 $rev->setId( 11 ); // old!
285 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
286 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
287 $rev->setTimestamp( '20180101000003' );
288 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
289
290 $text = "";
291 $text .= "* page:{{PAGENAME}}\n";
292 $text .= "* rev:{{REVISIONID}}\n";
293 $text .= "* user:{{REVISIONUSER}}\n";
294 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
295
296 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
297
298 $options = ParserOptions::newCanonical( 'canonical' );
299 $rr = $renderer->getRenderedRevision( $rev, $options );
300
301 $this->assertNull( $rr, 'getRenderedRevision' );
302 }
303
305 $renderer = $this->newRevisionRenderer( 100 );
306 $title = $this->getMockTitle( 7, 21 );
307
309 $rev->setId( 11 ); // old!
310 $rev->setVisibility( RevisionRecord::DELETED_TEXT ); // suppressed!
311 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
312 $rev->setTimestamp( '20180101000003' );
313 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
314
315 $text = "";
316 $text .= "* page:{{PAGENAME}}\n";
317 $text .= "* rev:{{REVISIONID}}\n";
318 $text .= "* user:{{REVISIONUSER}}\n";
319 $text .= "* time:{{REVISIONTIMESTAMP}}\n";
320
321 $rev->setContent( SlotRecord::MAIN, new WikitextContent( $text ) );
322
323 $options = ParserOptions::newCanonical( 'canonical' );
324 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); // privileged!
325 $rr = $renderer->getRenderedRevision( $rev, $options, $sysop );
326
327 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
328
329 $this->assertSame( $rev, $rr->getRevision() );
330 $this->assertSame( $options, $rr->getOptions() );
331
332 $html = $rr->getRevisionParserOutput()->getText();
333
334 // Suppressed content should be visible for sysops
335 $this->assertContains( 'page:' . __CLASS__, $html );
336 $this->assertContains( 'rev:11', $html );
337 $this->assertContains( 'user:Frank', $html );
338 $this->assertContains( 'time:20180101000003', $html );
339
340 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
341 }
342
343 public function testGetRenderedRevision_raw() {
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 $rr = $renderer->getRenderedRevision(
364 $rev,
365 $options,
366 null,
367 [ 'audience' => RevisionRecord::RAW ]
368 );
369
370 $this->assertTrue( $rr->isContentDeleted(), 'isContentDeleted' );
371
372 $this->assertSame( $rev, $rr->getRevision() );
373 $this->assertSame( $options, $rr->getOptions() );
374
375 $html = $rr->getRevisionParserOutput()->getText();
376
377 // Suppressed content should be visible in raw mode
378 $this->assertContains( 'page:' . __CLASS__, $html );
379 $this->assertContains( 'rev:11', $html );
380 $this->assertContains( 'user:Frank', $html );
381 $this->assertContains( 'time:20180101000003', $html );
382
383 $this->assertSame( $html, $rr->getSlotParserOutput( 'main' )->getText() );
384 }
385
387 $renderer = $this->newRevisionRenderer();
388 $title = $this->getMockTitle( 7, 21 );
389
391 $rev->setUser( new UserIdentityValue( 9, 'Frank', 0 ) );
392 $rev->setTimestamp( '20180101000003' );
393 $rev->setComment( CommentStoreComment::newUnsavedComment( '' ) );
394
395 $rev->setContent( SlotRecord::MAIN, new WikitextContent( '[[Kittens]]' ) );
396 $rev->setContent( 'aux', new WikitextContent( '[[Goats]]' ) );
397
398 $rr = $renderer->getRenderedRevision( $rev );
399
400 $combinedOutput = $rr->getRevisionParserOutput();
401 $mainOutput = $rr->getSlotParserOutput( SlotRecord::MAIN );
402 $auxOutput = $rr->getSlotParserOutput( 'aux' );
403
404 $combinedHtml = $combinedOutput->getText();
405 $mainHtml = $mainOutput->getText();
406 $auxHtml = $auxOutput->getText();
407
408 $this->assertContains( 'Kittens', $mainHtml );
409 $this->assertContains( 'Goats', $auxHtml );
410 $this->assertNotContains( 'Goats', $mainHtml );
411 $this->assertNotContains( 'Kittens', $auxHtml );
412 $this->assertContains( 'Kittens', $combinedHtml );
413 $this->assertContains( 'Goats', $combinedHtml );
414 $this->assertContains( '>aux<', $combinedHtml, 'slot header' );
415 $this->assertNotContains( '<mw:slotheader', $combinedHtml, 'slot header placeholder' );
416
417 // make sure output wrapping works right
418 $this->assertContains( 'class="mw-parser-output"', $mainHtml );
419 $this->assertContains( 'class="mw-parser-output"', $auxHtml );
420 $this->assertContains( 'class="mw-parser-output"', $combinedHtml );
421
422 // there should be only one wrapper div
423 $this->assertSame( 1, preg_match_all( '#class="mw-parser-output"#', $combinedHtml ) );
424 $this->assertNotContains( 'class="mw-parser-output"', $combinedOutput->getRawText() );
425
426 $combinedLinks = $combinedOutput->getLinks();
427 $mainLinks = $mainOutput->getLinks();
428 $auxLinks = $auxOutput->getLinks();
429 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Kittens'] ), 'links from main slot' );
430 $this->assertTrue( isset( $combinedLinks[NS_MAIN]['Goats'] ), 'links from aux slot' );
431 $this->assertFalse( isset( $mainLinks[NS_MAIN]['Goats'] ), 'no aux links in main' );
432 $this->assertFalse( isset( $auxLinks[NS_MAIN]['Kittens'] ), 'no main links in aux' );
433 }
434
437 $mockContent = $this->getMockBuilder( WikitextContent::class )
438 ->setMethods( [ 'getParserOutput' ] )
439 ->setConstructorArgs( [ 'Whatever' ] )
440 ->getMock();
441 $mockContent->method( 'getParserOutput' )
442 ->willReturnCallback( function ( Title $title, $revId = null,
443 ParserOptions $options = null, $generateHtml = true
444 ) {
445 if ( !$generateHtml ) {
446 return new ParserOutput( null );
447 } else {
448 $this->fail( 'Should not be called with $generateHtml == true' );
449 return null; // never happens, make analyzer happy
450 }
451 } );
452
453 $renderer = $this->newRevisionRenderer();
454 $title = $this->getMockTitle( 7, 21 );
455
457 $rev->setContent( SlotRecord::MAIN, $mockContent );
458 $rev->setContent( 'aux', $mockContent );
459
460 // NOTE: we are testing the private combineSlotOutput() callback here.
461 $rr = $renderer->getRenderedRevision( $rev );
462
463 $output = $rr->getSlotParserOutput( SlotRecord::MAIN, [ 'generate-html' => false ] );
464 $this->assertFalse( $output->hasText(), 'hasText' );
465
466 $output = $rr->getRevisionParserOutput( [ 'generate-html' => false ] );
467 $this->assertFalse( $output->hasText(), 'hasText' );
468 }
469
470}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
CommentStoreComment represents a comment stored by CommentStore.
Internationalisation code.
Definition Language.php:35
Database $db
Primary database.
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
Mutable RevisionRecord implementation, for building new revision entries programmatically.
Page revision base class.
The RevisionRenderer service provides access to rendered output for revisions.
Value object representing a content slot associated with a page revision.
newRevisionRenderer( $maxRev=100, $useMaster=false)
selectFieldCallback( $table, $fields, $cond, $maxRev)
Value object representing a user's identity.
Set options of the Parser.
Represents a title within MediaWiki.
Definition Title.php:39
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition Title.php:3566
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:47
Content object for wiki text pages.
const NS_MAIN
Definition Defines.php:64
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:235
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:2050
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:994
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:2062
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:2317
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:1818
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
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:37
Base interface for content objects.
Definition Content.php:34
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26