MediaWiki  master
OutputPageTest.php
Go to the documentation of this file.
1 <?php
2 
5 
13  const SCREEN_MEDIA_QUERY = 'screen and (min-width: 982px)';
14  const SCREEN_ONLY_MEDIA_QUERY = 'only screen and (min-width: 982px)';
15 
16  // @codingStandardsIgnoreStart Generic.Files.LineLength
17  const RSS_RC_LINK = '<link rel="alternate" type="application/rss+xml" title=" RSS feed" href="/w/index.php?title=Special:RecentChanges&amp;feed=rss"/>';
18  const ATOM_RC_LINK = '<link rel="alternate" type="application/atom+xml" title=" Atom feed" href="/w/index.php?title=Special:RecentChanges&amp;feed=atom"/>';
19 
20  const RSS_TEST_LINK = '<link rel="alternate" type="application/rss+xml" title="&quot;Test&quot; RSS feed" href="fake-link"/>';
21  const ATOM_TEST_LINK = '<link rel="alternate" type="application/atom+xml" title="&quot;Test&quot; Atom feed" href="fake-link"/>';
22  // @codingStandardsIgnoreEnd
23 
24  // Ensure that we don't affect the global ResourceLoader state.
25  protected function setUp() {
26  parent::setUp();
28  }
29 
30  protected function tearDown() {
31  parent::tearDown();
33  }
34 
42  public function testRedirect( $url, $code = null ) {
43  $op = $this->newInstance();
44  if ( isset( $code ) ) {
45  $op->redirect( $url, $code );
46  } else {
47  $op->redirect( $url );
48  }
49  $expectedUrl = str_replace( "\n", '', $url );
50  $this->assertSame( $expectedUrl, $op->getRedirect() );
51  $this->assertSame( $expectedUrl, $op->mRedirect );
52  $this->assertSame( $code ?? '302', $op->mRedirectCode );
53  }
54 
55  public function provideRedirect() {
56  return [
57  [ 'http://example.com' ],
58  [ 'http://example.com', '400' ],
59  [ 'http://example.com', 'squirrels!!!' ],
60  [ "a\nb" ],
61  ];
62  }
63 
64  private function setupFeedLinks( $feed, $types ) {
65  $outputPage = $this->newInstance( [
66  'AdvertisedFeedTypes' => $types,
67  'Feed' => $feed,
68  'OverrideSiteFeed' => false,
69  'Script' => '/w',
70  'Sitename' => false,
71  ] );
72  $outputPage->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
73  $this->setMwGlobals( [
74  'wgScript' => '/w/index.php',
75  ] );
76  return $outputPage;
77  }
78 
79  private function assertFeedLinks( $outputPage, $message, $present, $non_present ) {
80  $links = $outputPage->getHeadLinksArray();
81  foreach ( $present as $link ) {
82  $this->assertContains( $link, $links, $message );
83  }
84  foreach ( $non_present as $link ) {
85  $this->assertNotContains( $link, $links, $message );
86  }
87  }
88 
89  private function assertFeedUILinks( $outputPage, $ui_links ) {
90  if ( $ui_links ) {
91  $this->assertTrue( $outputPage->isSyndicated(), 'Syndication should be offered' );
92  $this->assertGreaterThan( 0, count( $outputPage->getSyndicationLinks() ),
93  'Some syndication links should be there' );
94  } else {
95  $this->assertFalse( $outputPage->isSyndicated(), 'No syndication should be offered' );
96  $this->assertEquals( 0, count( $outputPage->getSyndicationLinks() ),
97  'No syndication links should be there' );
98  }
99  }
100 
101  public static function provideFeedLinkData() {
102  return [
103  [
104  true, [ 'rss' ], 'Only RSS RC link should be offerred',
105  [ self::RSS_RC_LINK ], [ self::ATOM_RC_LINK ]
106  ],
107  [
108  true, [ 'atom' ], 'Only Atom RC link should be offerred',
109  [ self::ATOM_RC_LINK ], [ self::RSS_RC_LINK ]
110  ],
111  [
112  true, [], 'No RC feed formats should be offerred',
113  [], [ self::ATOM_RC_LINK, self::RSS_RC_LINK ]
114  ],
115  [
116  false, [ 'atom' ], 'No RC feeds should be offerred',
117  [], [ self::ATOM_RC_LINK, self::RSS_RC_LINK ]
118  ],
119  ];
120  }
121 
126  public function testSetCopyrightUrl() {
127  $op = $this->newInstance();
128  $op->setCopyrightUrl( 'http://example.com' );
129 
130  $this->assertSame(
131  Html::element( 'link', [ 'rel' => 'license', 'href' => 'http://example.com' ] ),
132  $op->getHeadLinksArray()['copyright']
133  );
134  }
135 
140  public function testRecentChangesFeed( $feed, $advertised_feed_types,
141  $message, $present, $non_present ) {
142  $outputPage = $this->setupFeedLinks( $feed, $advertised_feed_types );
143  $this->assertFeedLinks( $outputPage, $message, $present, $non_present );
144  }
145 
146  public static function provideAdditionalFeedData() {
147  return [
148  [
149  true, [ 'atom' ], 'Additional Atom feed should be offered',
150  'atom',
151  [ self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
152  [ self::RSS_TEST_LINK, self::RSS_RC_LINK ],
153  true,
154  ],
155  [
156  true, [ 'rss' ], 'Additional RSS feed should be offered',
157  'rss',
158  [ self::RSS_TEST_LINK, self::RSS_RC_LINK ],
159  [ self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
160  true,
161  ],
162  [
163  true, [ 'rss' ], 'Additional Atom feed should NOT be offered with RSS enabled',
164  'atom',
165  [ self::RSS_RC_LINK ],
166  [ self::RSS_TEST_LINK, self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
167  false,
168  ],
169  [
170  false, [ 'atom' ], 'Additional Atom feed should NOT be offered, all feeds disabled',
171  'atom',
172  [],
173  [
174  self::RSS_TEST_LINK, self::ATOM_TEST_LINK,
175  self::ATOM_RC_LINK, self::ATOM_RC_LINK,
176  ],
177  false,
178  ],
179  ];
180  }
181 
189  public function testAdditionalFeeds( $feed, $advertised_feed_types, $message,
190  $additional_feed_type, $present, $non_present, $any_ui_links ) {
191  $outputPage = $this->setupFeedLinks( $feed, $advertised_feed_types );
192  $outputPage->addFeedLink( $additional_feed_type, 'fake-link' );
193  $this->assertFeedLinks( $outputPage, $message, $present, $non_present );
194  $this->assertFeedUILinks( $outputPage, $any_ui_links );
195  }
196 
197  // @todo How to test setStatusCode?
198 
204  public function testMetaTags() {
205  $op = $this->newInstance();
206  $op->addMeta( 'http:expires', '0' );
207  $op->addMeta( 'keywords', 'first' );
208  $op->addMeta( 'keywords', 'second' );
209  $op->addMeta( 'og:title', 'Ta-duh' );
210 
211  $expected = [
212  [ 'http:expires', '0' ],
213  [ 'keywords', 'first' ],
214  [ 'keywords', 'second' ],
215  [ 'og:title', 'Ta-duh' ],
216  ];
217  $this->assertSame( $expected, $op->getMetaTags() );
218 
219  $links = $op->getHeadLinksArray();
220  $this->assertContains( '<meta http-equiv="expires" content="0"/>', $links );
221  $this->assertContains( '<meta name="keywords" content="first"/>', $links );
222  $this->assertContains( '<meta name="keywords" content="second"/>', $links );
223  $this->assertContains( '<meta property="og:title" content="Ta-duh"/>', $links );
224  $this->assertArrayNotHasKey( 'meta-robots', $links );
225  }
226 
232  public function testAddLink() {
233  $op = $this->newInstance();
234 
235  $links = [
236  [],
237  [ 'rel' => 'foo', 'href' => 'http://example.com' ],
238  ];
239 
240  foreach ( $links as $link ) {
241  $op->addLink( $link );
242  }
243 
244  $this->assertSame( $links, $op->getLinkTags() );
245 
246  $result = $op->getHeadLinksArray();
247 
248  foreach ( $links as $link ) {
249  $this->assertContains( Html::element( 'link', $link ), $result );
250  }
251  }
252 
258  public function testSetCanonicalUrl() {
259  $op = $this->newInstance();
260  $op->setCanonicalUrl( 'http://example.comm' );
261  $op->setCanonicalUrl( 'http://example.com' );
262 
263  $this->assertSame( 'http://example.com', $op->getCanonicalUrl() );
264 
265  $headLinks = $op->getHeadLinksArray();
266 
267  $this->assertContains( Html::element( 'link', [
268  'rel' => 'canonical', 'href' => 'http://example.com'
269  ] ), $headLinks );
270 
271  $this->assertNotContains( Html::element( 'link', [
272  'rel' => 'canonical', 'href' => 'http://example.comm'
273  ] ), $headLinks );
274  }
275 
279  public function testAddScript() {
280  $op = $this->newInstance();
281  $op->addScript( 'some random string' );
282 
283  $this->assertContains( "\nsome random string\n", "\n" . $op->getBottomScripts() . "\n" );
284  }
285 
289  public function testAddScriptFile() {
290  $op = $this->newInstance();
291  $op->addScriptFile( '/somescript.js' );
292  $op->addScriptFile( '//example.com/somescript.js' );
293 
294  $this->assertContains(
295  "\n" . Html::linkedScript( '/somescript.js', $op->getCSPNonce() ) .
296  Html::linkedScript( '//example.com/somescript.js', $op->getCSPNonce() ) . "\n",
297  "\n" . $op->getBottomScripts() . "\n"
298  );
299  }
300 
307  $this->setExpectedException( PHPUnit_Framework_Error_Deprecated::class,
308  'Use of OutputPage::addScriptFile was deprecated in MediaWiki 1.24.' );
309 
310  $op = $this->newInstance();
311  $op->addScriptFile( 'ignored-script.js' );
312  }
313 
321  $this->hideDeprecated( 'OutputPage::addScriptFile' );
322  $op = $this->newInstance();
323  $op->addScriptFile( 'ignored-script.js' );
324 
325  $this->assertNotContains( 'ignored-script.js', '' . $op->getBottomScripts() );
326  }
327 
331  public function testAddInlineScript() {
332  $op = $this->newInstance();
333  $op->addInlineScript( 'let foo = "bar";' );
334  $op->addInlineScript( 'alert( foo );' );
335 
336  $this->assertContains(
337  "\n" . Html::inlineScript( "\nlet foo = \"bar\";\n", $op->getCSPNonce() ) . "\n" .
338  Html::inlineScript( "\nalert( foo );\n", $op->getCSPNonce() ) . "\n",
339  "\n" . $op->getBottomScripts() . "\n"
340  );
341  }
342 
343  // @todo How to test filterModules(), warnModuleTargetFilter(), getModules(), etc.?
344 
349  public function testSetTarget() {
350  $op = $this->newInstance();
351  $op->setTarget( 'foo' );
352 
353  $this->assertSame( 'foo', $op->getTarget() );
354  // @todo What else? Test some actual effect?
355  }
356 
357  // @todo How to test addContentOverride(Callback)?
358 
365  public function testHeadItems() {
366  $op = $this->newInstance();
367  $op->addHeadItem( 'a', 'b' );
368  $op->addHeadItems( [ 'c' => '<d>&amp;', 'e' => 'f', 'a' => 'q' ] );
369  $op->addHeadItem( 'e', 'g' );
370  $op->addHeadItems( 'x' );
371 
372  $this->assertSame( [ 'a' => 'q', 'c' => '<d>&amp;', 'e' => 'g', 'x' ],
373  $op->getHeadItemsArray() );
374 
375  $this->assertTrue( $op->hasHeadItem( 'a' ) );
376  $this->assertTrue( $op->hasHeadItem( 'c' ) );
377  $this->assertTrue( $op->hasHeadItem( 'e' ) );
378  $this->assertTrue( $op->hasHeadItem( '0' ) );
379 
380  $this->assertContains( "\nq\n<d>&amp;\ng\nx\n",
381  '' . $op->headElement( $op->getContext()->getSkin() ) );
382  }
383 
389  public function testHeadItemsParserOutput() {
390  $op = $this->newInstance();
391  $stubPO1 = $this->createParserOutputStub( 'getHeadItems', [ 'a' => 'b' ] );
392  $op->addParserOutputMetadata( $stubPO1 );
393  $stubPO2 = $this->createParserOutputStub( 'getHeadItems',
394  [ 'c' => '<d>&amp;', 'e' => 'f', 'a' => 'q' ] );
395  $op->addParserOutputMetadata( $stubPO2 );
396  $stubPO3 = $this->createParserOutputStub( 'getHeadItems', [ 'e' => 'g' ] );
397  $op->addParserOutput( $stubPO3 );
398  $stubPO4 = $this->createParserOutputStub( 'getHeadItems', [ 'x' ] );
399  $op->addParserOutputMetadata( $stubPO4 );
400 
401  $this->assertSame( [ 'a' => 'q', 'c' => '<d>&amp;', 'e' => 'g', 'x' ],
402  $op->getHeadItemsArray() );
403 
404  $this->assertTrue( $op->hasHeadItem( 'a' ) );
405  $this->assertTrue( $op->hasHeadItem( 'c' ) );
406  $this->assertTrue( $op->hasHeadItem( 'e' ) );
407  $this->assertTrue( $op->hasHeadItem( '0' ) );
408  $this->assertFalse( $op->hasHeadItem( 'b' ) );
409 
410  $this->assertContains( "\nq\n<d>&amp;\ng\nx\n",
411  '' . $op->headElement( $op->getContext()->getSkin() ) );
412  }
413 
417  public function testAddBodyClasses() {
418  $op = $this->newInstance();
419  $op->addBodyClasses( 'a' );
420  $op->addBodyClasses( 'mediawiki' );
421  $op->addBodyClasses( 'b c' );
422  $op->addBodyClasses( [ 'd', 'e' ] );
423  $op->addBodyClasses( 'a' );
424 
425  $this->assertContains( '"a mediawiki b c d e ltr',
426  '' . $op->headElement( $op->getContext()->getSkin() ) );
427  }
428 
433  public function testArticleBodyOnly() {
434  $op = $this->newInstance();
435  $this->assertFalse( $op->getArticleBodyOnly() );
436 
437  $op->setArticleBodyOnly( true );
438  $this->assertTrue( $op->getArticleBodyOnly() );
439 
440  $op->addHTML( '<b>a</b>' );
441 
442  $this->assertSame( '<b>a</b>', $op->output( true ) );
443  }
444 
449  public function testProperties() {
450  $op = $this->newInstance();
451 
452  $this->assertNull( $op->getProperty( 'foo' ) );
453 
454  $op->setProperty( 'foo', 'bar' );
455  $op->setProperty( 'baz', 'quz' );
456 
457  $this->assertSame( 'bar', $op->getProperty( 'foo' ) );
458  $this->assertSame( 'quz', $op->getProperty( 'baz' ) );
459  }
460 
467  public function testCheckLastModified(
468  $timestamp, $ifModifiedSince, $expected, $config = [], $callback = null
469  ) {
470  $request = new FauxRequest();
471  if ( $ifModifiedSince ) {
472  if ( is_numeric( $ifModifiedSince ) ) {
473  // Unix timestamp
474  $ifModifiedSince = date( 'D, d M Y H:i:s', $ifModifiedSince ) . ' GMT';
475  }
476  $request->setHeader( 'If-Modified-Since', $ifModifiedSince );
477  }
478 
479  if ( !isset( $config['CacheEpoch'] ) ) {
480  // Make sure it's not too recent
481  $config['CacheEpoch'] = '20000101000000';
482  }
483 
484  $op = $this->newInstance( $config, $request );
485 
486  if ( $callback ) {
487  $callback( $op, $this );
488  }
489 
490  // Avoid a complaint about not being able to disable compression
491  Wikimedia\suppressWarnings();
492  try {
493  $this->assertEquals( $expected, $op->checkLastModified( $timestamp ) );
494  } finally {
495  Wikimedia\restoreWarnings();
496  }
497  }
498 
499  public function provideCheckLastModified() {
500  $lastModified = time() - 3600;
501  return [
502  'Timestamp 0' =>
503  [ '0', $lastModified, false ],
504  'Timestamp Unix epoch' =>
505  [ '19700101000000', $lastModified, false ],
506  'Timestamp same as If-Modified-Since' =>
507  [ $lastModified, $lastModified, true ],
508  'Timestamp one second after If-Modified-Since' =>
509  [ $lastModified + 1, $lastModified, false ],
510  'No If-Modified-Since' =>
511  [ $lastModified + 1, null, false ],
512  'Malformed If-Modified-Since' =>
513  [ $lastModified + 1, 'GIBBERING WOMBATS !!!', false ],
514  'Non-standard IE-style If-Modified-Since' =>
515  [ $lastModified, date( 'D, d M Y H:i:s', $lastModified ) . ' GMT; length=5202',
516  true ],
517  // @todo Should we fix this behavior to match the spec? Probably no reason to.
518  'If-Modified-Since not per spec but we accept it anyway because strtotime does' =>
519  [ $lastModified, "@$lastModified", true ],
520  '$wgCachePages = false' =>
521  [ $lastModified, $lastModified, false, [ 'CachePages' => false ] ],
522  '$wgCacheEpoch' =>
523  [ $lastModified, $lastModified, false,
524  [ 'CacheEpoch' => wfTimestamp( TS_MW, $lastModified + 1 ) ] ],
525  'Recently-touched user' =>
526  [ $lastModified, $lastModified, false, [],
527  function ( $op ) {
528  $op->getContext()->setUser( $this->getTestUser()->getUser() );
529  } ],
530  'After CDN expiry' =>
531  [ $lastModified, $lastModified, false,
532  [ 'UseCdn' => true, 'CdnMaxAge' => 3599 ] ],
533  'Hook allows cache use' =>
534  [ $lastModified + 1, $lastModified, true, [],
535  function ( $op, $that ) {
536  $that->setTemporaryHook( 'OutputPageCheckLastModified',
537  function ( &$modifiedTimes ) {
538  $modifiedTimes = [ 1 ];
539  }
540  );
541  } ],
542  'Hooks prohibits cache use' =>
543  [ $lastModified, $lastModified, false, [],
544  function ( $op, $that ) {
545  $that->setTemporaryHook( 'OutputPageCheckLastModified',
546  function ( &$modifiedTimes ) {
547  $modifiedTimes = [ max( $modifiedTimes ) + 1 ];
548  }
549  );
550  } ],
551  ];
552  }
553 
559  public function testCdnCacheEpoch( $params ) {
560  $out = TestingAccessWrapper::newFromObject( $this->newInstance() );
561  $reqTime = strtotime( $params['reqTime'] );
562  $pageTime = strtotime( $params['pageTime'] );
563  $actual = max( $pageTime, $out->getCdnCacheEpoch( $reqTime, $params['maxAge'] ) );
564 
565  $this->assertEquals(
566  $params['expect'],
567  gmdate( DateTime::ATOM, $actual ),
568  'cdn epoch'
569  );
570  }
571 
572  public static function provideCdnCacheEpoch() {
573  $base = [
574  'pageTime' => '2011-04-01T12:00:00+00:00',
575  'maxAge' => 24 * 3600,
576  ];
577  return [
578  'after 1s' => [ $base + [
579  'reqTime' => '2011-04-01T12:00:01+00:00',
580  'expect' => '2011-04-01T12:00:00+00:00',
581  ] ],
582  'after 23h' => [ $base + [
583  'reqTime' => '2011-04-02T11:00:00+00:00',
584  'expect' => '2011-04-01T12:00:00+00:00',
585  ] ],
586  'after 24h and a bit' => [ $base + [
587  'reqTime' => '2011-04-02T12:34:56+00:00',
588  'expect' => '2011-04-01T12:34:56+00:00',
589  ] ],
590  'after a year' => [ $base + [
591  'reqTime' => '2012-05-06T00:12:07+00:00',
592  'expect' => '2012-05-05T00:12:07+00:00',
593  ] ],
594  ];
595  }
596 
597  // @todo How to test setLastModified?
598 
603  public function testSetRobotPolicy() {
604  $op = $this->newInstance();
605  $op->setRobotPolicy( 'noindex, nofollow' );
606 
607  $links = $op->getHeadLinksArray();
608  $this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
609  }
610 
616  public function testSetIndexFollowPolicies() {
617  $op = $this->newInstance();
618  $op->setIndexPolicy( 'noindex' );
619  $op->setFollowPolicy( 'nofollow' );
620 
621  $links = $op->getHeadLinksArray();
622  $this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
623  }
624 
625  private function extractHTMLTitle( OutputPage $op ) {
626  $html = $op->headElement( $op->getContext()->getSkin() );
627 
628  // OutputPage should always output the title in a nice format such that regexes will work
629  // fine. If it doesn't, we'll fail the tests.
630  preg_match_all( '!<title>(.*?)</title>!', $html, $matches );
631 
632  $this->assertLessThanOrEqual( 1, count( $matches[1] ), 'More than one <title>!' );
633 
634  if ( !count( $matches[1] ) ) {
635  return null;
636  }
637 
638  return $matches[1][0];
639  }
640 
644  private static function getMsgText( $op, ...$msgParams ) {
645  return $op->msg( ...$msgParams )->inContentLanguage()->text();
646  }
647 
652  public function testHTMLTitle() {
653  $op = $this->newInstance();
654 
655  // Default
656  $this->assertSame( '', $op->getHTMLTitle() );
657  $this->assertSame( '', $op->getPageTitle() );
658  $this->assertSame(
659  $this->getMsgText( $op, 'pagetitle', '' ),
660  $this->extractHTMLTitle( $op )
661  );
662 
663  // Set to string
664  $op->setHTMLTitle( 'Potatoes will eat me' );
665 
666  $this->assertSame( 'Potatoes will eat me', $op->getHTMLTitle() );
667  $this->assertSame( 'Potatoes will eat me', $this->extractHTMLTitle( $op ) );
668  // Shouldn't have changed the page title
669  $this->assertSame( '', $op->getPageTitle() );
670 
671  // Set to message
672  $msg = $op->msg( 'mainpage' );
673 
674  $op->setHTMLTitle( $msg );
675  $this->assertSame( $msg->text(), $op->getHTMLTitle() );
676  $this->assertSame( $msg->text(), $this->extractHTMLTitle( $op ) );
677  $this->assertSame( '', $op->getPageTitle() );
678  }
679 
683  public function testSetRedirectedFrom() {
684  $op = $this->newInstance();
685 
686  $op->setRedirectedFrom( Title::newFromText( 'Talk:Some page' ) );
687  $this->assertSame( 'Talk:Some_page', $op->getJSVars()['wgRedirectedFrom'] );
688  }
689 
694  public function testPageTitle() {
695  // We don't test the actual HTML output anywhere, because that's up to the skin.
696  $op = $this->newInstance();
697 
698  // Test default
699  $this->assertSame( '', $op->getPageTitle() );
700  $this->assertSame( '', $op->getHTMLTitle() );
701 
702  // Test set to plain text
703  $op->setPageTitle( 'foobar' );
704 
705  $this->assertSame( 'foobar', $op->getPageTitle() );
706  // HTML title should change as well
707  $this->assertSame( $this->getMsgText( $op, 'pagetitle', 'foobar' ), $op->getHTMLTitle() );
708 
709  // Test set to text with good and bad HTML. We don't try to be comprehensive here, that
710  // belongs in Sanitizer tests.
711  $op->setPageTitle( '<script>a</script>&amp;<i>b</i>' );
712 
713  $this->assertSame( '&lt;script&gt;a&lt;/script&gt;&amp;<i>b</i>', $op->getPageTitle() );
714  $this->assertSame(
715  $this->getMsgText( $op, 'pagetitle', '<script>a</script>&b' ),
716  $op->getHTMLTitle()
717  );
718 
719  // Test set to message
720  $text = $this->getMsgText( $op, 'mainpage' );
721 
722  $op->setPageTitle( $op->msg( 'mainpage' )->inContentLanguage() );
723  $this->assertSame( $text, $op->getPageTitle() );
724  $this->assertSame( $this->getMsgText( $op, 'pagetitle', $text ), $op->getHTMLTitle() );
725  }
726 
730  public function testSetTitle() {
731  $op = $this->newInstance();
732 
733  $this->assertSame( 'My test page', $op->getTitle()->getPrefixedText() );
734 
735  $op->setTitle( Title::newFromText( 'Another test page' ) );
736 
737  $this->assertSame( 'Another test page', $op->getTitle()->getPrefixedText() );
738  }
739 
746  public function testSubtitle() {
747  $op = $this->newInstance();
748 
749  $this->assertSame( '', $op->getSubtitle() );
750 
751  $op->addSubtitle( '<b>foo</b>' );
752 
753  $this->assertSame( '<b>foo</b>', $op->getSubtitle() );
754 
755  $op->addSubtitle( $op->msg( 'mainpage' )->inContentLanguage() );
756 
757  $this->assertSame(
758  "<b>foo</b><br />\n\t\t\t\t" . $this->getMsgText( $op, 'mainpage' ),
759  $op->getSubtitle()
760  );
761 
762  $op->setSubtitle( 'There can be only one' );
763 
764  $this->assertSame( 'There can be only one', $op->getSubtitle() );
765 
766  $op->clearSubtitle();
767 
768  $this->assertSame( '', $op->getSubtitle() );
769  }
770 
776  public function testBuildBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
777  if ( count( $titles ) > 1 ) {
778  // Not applicable
779  $this->assertTrue( true );
780  return;
781  }
782 
784  $query = $queries[0];
785 
786  $this->editPage( 'Page 1', '' );
787  $this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
788 
790 
791  foreach ( $contains as $substr ) {
792  $this->assertContains( $substr, $str );
793  }
794 
795  foreach ( $notContains as $substr ) {
796  $this->assertNotContains( $substr, $str );
797  }
798  }
799 
806  public function testAddBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
807  $this->editPage( 'Page 1', '' );
808  $this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
809 
810  $op = $this->newInstance();
811  foreach ( $titles as $i => $unused ) {
812  $op->addBacklinkSubtitle( Title::newFromText( $titles[$i] ), $queries[$i] );
813  }
814 
815  $str = $op->getSubtitle();
816 
817  foreach ( $contains as $substr ) {
818  $this->assertContains( $substr, $str );
819  }
820 
821  foreach ( $notContains as $substr ) {
822  $this->assertNotContains( $substr, $str );
823  }
824  }
825 
826  public function provideBacklinkSubtitle() {
827  return [
828  [
829  [ 'Page 1' ],
830  [ [] ],
831  [ 'Page 1' ],
832  [ 'redirect', 'Page 2' ],
833  ],
834  [
835  [ 'Page 2' ],
836  [ [] ],
837  [ 'redirect=no' ],
838  [ 'Page 1' ],
839  ],
840  [
841  [ 'Page 1' ],
842  [ [ 'action' => 'edit' ] ],
843  [ 'action=edit' ],
844  [],
845  ],
846  [
847  [ 'Page 1', 'Page 2' ],
848  [ [], [] ],
849  [ 'Page 1', 'Page 2', "<br />\n\t\t\t\t" ],
850  [],
851  ],
852  // @todo Anything else to test?
853  ];
854  }
855 
860  public function testPrintable() {
861  $op = $this->newInstance();
862 
863  $this->assertFalse( $op->isPrintable() );
864 
865  $op->setPrintable();
866 
867  $this->assertTrue( $op->isPrintable() );
868  }
869 
874  public function testDisable() {
875  $op = $this->newInstance();
876 
877  $this->assertFalse( $op->isDisabled() );
878  $this->assertNotSame( '', $op->output( true ) );
879 
880  $op->disable();
881 
882  $this->assertTrue( $op->isDisabled() );
883  $this->assertSame( '', $op->output( true ) );
884  }
885 
891  public function testShowNewSectionLink() {
892  $op = $this->newInstance();
893 
894  $this->assertFalse( $op->showNewSectionLink() );
895 
896  $pOut1 = $this->createParserOutputStub( 'getNewSection', true );
897  $op->addParserOutputMetadata( $pOut1 );
898  $this->assertTrue( $op->showNewSectionLink() );
899 
900  $pOut2 = $this->createParserOutputStub( 'getNewSection', false );
901  $op->addParserOutput( $pOut2 );
902  $this->assertFalse( $op->showNewSectionLink() );
903  }
904 
910  public function testForceHideNewSectionLink() {
911  $op = $this->newInstance();
912 
913  $this->assertFalse( $op->forceHideNewSectionLink() );
914 
915  $pOut1 = $this->createParserOutputStub( 'getHideNewSection', true );
916  $op->addParserOutputMetadata( $pOut1 );
917  $this->assertTrue( $op->forceHideNewSectionLink() );
918 
919  $pOut2 = $this->createParserOutputStub( 'getHideNewSection', false );
920  $op->addParserOutput( $pOut2 );
921  $this->assertFalse( $op->forceHideNewSectionLink() );
922  }
923 
928  public function testSetSyndicated() {
929  $op = $this->newInstance( [ 'Feed' => true ] );
930  $this->assertFalse( $op->isSyndicated() );
931 
932  $op->setSyndicated();
933  $this->assertTrue( $op->isSyndicated() );
934 
935  $op->setSyndicated( false );
936  $this->assertFalse( $op->isSyndicated() );
937 
938  $op = $this->newInstance(); // Feed => false by default
939  $this->assertFalse( $op->isSyndicated() );
940 
941  $op->setSyndicated();
942  $this->assertFalse( $op->isSyndicated() );
943  }
944 
951  public function testFeedLinks() {
952  $op = $this->newInstance( [ 'Feed' => true ] );
953  $this->assertSame( [], $op->getSyndicationLinks() );
954 
955  $op->addFeedLink( 'not a supported format', 'abc' );
956  $this->assertFalse( $op->isSyndicated() );
957  $this->assertSame( [], $op->getSyndicationLinks() );
958 
959  $feedTypes = $op->getConfig()->get( 'AdvertisedFeedTypes' );
960 
961  $op->addFeedLink( $feedTypes[0], 'def' );
962  $this->assertTrue( $op->isSyndicated() );
963  $this->assertSame( [ $feedTypes[0] => 'def' ], $op->getSyndicationLinks() );
964 
965  $op->setFeedAppendQuery( false );
966  $expected = [];
967  foreach ( $feedTypes as $type ) {
968  $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type" );
969  }
970  $this->assertSame( $expected, $op->getSyndicationLinks() );
971 
972  $op->setFeedAppendQuery( 'apples=oranges' );
973  foreach ( $feedTypes as $type ) {
974  $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type&apples=oranges" );
975  }
976  $this->assertSame( $expected, $op->getSyndicationLinks() );
977 
978  $op = $this->newInstance(); // Feed => false by default
979  $this->assertSame( [], $op->getSyndicationLinks() );
980 
981  $op->addFeedLink( $feedTypes[0], 'def' );
982  $this->assertFalse( $op->isSyndicated() );
983  $this->assertSame( [], $op->getSyndicationLinks() );
984  }
985 
992  function testArticleFlags() {
993  $op = $this->newInstance();
994  $this->assertFalse( $op->isArticle() );
995  $this->assertTrue( $op->isArticleRelated() );
996 
997  $op->setArticleRelated( false );
998  $this->assertFalse( $op->isArticle() );
999  $this->assertFalse( $op->isArticleRelated() );
1000 
1001  $op->setArticleFlag( true );
1002  $this->assertTrue( $op->isArticle() );
1003  $this->assertTrue( $op->isArticleRelated() );
1004 
1005  $op->setArticleFlag( false );
1006  $this->assertFalse( $op->isArticle() );
1007  $this->assertTrue( $op->isArticleRelated() );
1008 
1009  $op->setArticleFlag( true );
1010  $op->setArticleRelated( false );
1011  $this->assertFalse( $op->isArticle() );
1012  $this->assertFalse( $op->isArticleRelated() );
1013  }
1014 
1022  function testLanguageLinks() {
1023  $op = $this->newInstance();
1024  $this->assertSame( [], $op->getLanguageLinks() );
1025 
1026  $op->addLanguageLinks( [ 'fr:A', 'it:B' ] );
1027  $this->assertSame( [ 'fr:A', 'it:B' ], $op->getLanguageLinks() );
1028 
1029  $op->addLanguageLinks( [ 'de:C', 'es:D' ] );
1030  $this->assertSame( [ 'fr:A', 'it:B', 'de:C', 'es:D' ], $op->getLanguageLinks() );
1031 
1032  $op->setLanguageLinks( [ 'pt:E' ] );
1033  $this->assertSame( [ 'pt:E' ], $op->getLanguageLinks() );
1034 
1035  $pOut1 = $this->createParserOutputStub( 'getLanguageLinks', [ 'he:F', 'ar:G' ] );
1036  $op->addParserOutputMetadata( $pOut1 );
1037  $this->assertSame( [ 'pt:E', 'he:F', 'ar:G' ], $op->getLanguageLinks() );
1038 
1039  $pOut2 = $this->createParserOutputStub( 'getLanguageLinks', [ 'pt:H' ] );
1040  $op->addParserOutput( $pOut2 );
1041  $this->assertSame( [ 'pt:E', 'he:F', 'ar:G', 'pt:H' ], $op->getLanguageLinks() );
1042  }
1043 
1044  // @todo Are these category links tests too abstract and complicated for what they test? Would
1045  // it make sense to just write out all the tests by hand with maybe some copy-and-paste?
1046 
1061  public function testAddCategoryLinks(
1062  array $args, array $fakeResults, callable $variantLinkCallback = null,
1063  array $expectedNormal, array $expectedHidden
1064  ) {
1065  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'add' );
1066  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'add' );
1067 
1068  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1069 
1070  $op->addCategoryLinks( $args );
1071 
1072  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
1073  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1074  }
1075 
1084  array $args, array $fakeResults, callable $variantLinkCallback = null,
1085  array $expectedNormal, array $expectedHidden
1086  ) {
1087  if ( count( $args ) <= 1 ) {
1088  // @todo Should this be skipped instead of passed?
1089  $this->assertTrue( true );
1090  return;
1091  }
1092 
1093  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'onebyone' );
1094  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'onebyone' );
1095 
1096  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1097 
1098  foreach ( $args as $key => $val ) {
1099  $op->addCategoryLinks( [ $key => $val ] );
1100  }
1101 
1102  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
1103  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1104  }
1105 
1113  public function testSetCategoryLinks(
1114  array $args, array $fakeResults, callable $variantLinkCallback = null,
1115  array $expectedNormal, array $expectedHidden
1116  ) {
1117  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'set' );
1118  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'set' );
1119 
1120  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1121 
1122  $op->setCategoryLinks( [ 'Initial page' => 'Initial page' ] );
1123  $op->setCategoryLinks( $args );
1124 
1125  // We don't reset the categories, for some reason, only the links
1126  $expectedNormalCats = array_merge( [ 'Initial page' ], $expectedNormal );
1127  $expectedCats = array_merge( $expectedHidden, $expectedNormalCats );
1128 
1129  $this->doCategoryAsserts( $op, $expectedNormalCats, $expectedHidden );
1130  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1131  }
1132 
1142  array $args, array $fakeResults, callable $variantLinkCallback = null,
1143  array $expectedNormal, array $expectedHidden
1144  ) {
1145  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'pout' );
1146  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'pout' );
1147 
1148  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1149 
1150  $stubPO = $this->createParserOutputStub( 'getCategories', $args );
1151 
1152  // addParserOutput and addParserOutputMetadata should behave identically for us, so
1153  // alternate to get coverage for both without adding extra tests
1154  static $idx = 0;
1155  $idx++;
1156  $method = [ 'addParserOutputMetadata', 'addParserOutput' ][$idx % 2];
1157  $op->$method( $stubPO );
1158 
1159  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
1160  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1161  }
1162 
1168  private function extractExpectedCategories( array $expected, $key ) {
1169  if ( !$expected || isset( $expected[0] ) ) {
1170  return $expected;
1171  }
1172  return $expected[$key] ?? $expected['default'];
1173  }
1174 
1175  private function setupCategoryTests(
1176  array $fakeResults, callable $variantLinkCallback = null
1177  ) : OutputPage {
1178  $this->setMwGlobals( 'wgUsePigLatinVariant', true );
1179 
1180  $op = $this->getMockBuilder( OutputPage::class )
1181  ->setConstructorArgs( [ new RequestContext() ] )
1182  ->setMethods( [ 'addCategoryLinksToLBAndGetResult', 'getTitle' ] )
1183  ->getMock();
1184 
1185  $title = Title::newFromText( 'My test page' );
1186  $op->expects( $this->any() )
1187  ->method( 'getTitle' )
1188  ->will( $this->returnValue( $title ) );
1189 
1190  $op->expects( $this->any() )
1191  ->method( 'addCategoryLinksToLBAndGetResult' )
1192  ->will( $this->returnCallback( function ( array $categories ) use ( $fakeResults ) {
1193  $return = [];
1194  foreach ( $categories as $category => $unused ) {
1195  if ( isset( $fakeResults[$category] ) ) {
1196  $return[] = $fakeResults[$category];
1197  }
1198  }
1199  return new FakeResultWrapper( $return );
1200  } ) );
1201 
1202  if ( $variantLinkCallback ) {
1203  $mockContLang = $this->getMockBuilder( Language::class )
1204  ->setConstructorArgs( [ 'en' ] )
1205  ->setMethods( [ 'findVariantLink' ] )
1206  ->getMock();
1207  $mockContLang->expects( $this->any() )
1208  ->method( 'findVariantLink' )
1209  ->will( $this->returnCallback( $variantLinkCallback ) );
1210  $this->setContentLang( $mockContLang );
1211  }
1212 
1213  $this->assertSame( [], $op->getCategories() );
1214 
1215  return $op;
1216  }
1217 
1218  private function doCategoryAsserts( $op, $expectedNormal, $expectedHidden ) {
1219  $this->assertSame( array_merge( $expectedHidden, $expectedNormal ), $op->getCategories() );
1220  $this->assertSame( $expectedNormal, $op->getCategories( 'normal' ) );
1221  $this->assertSame( $expectedHidden, $op->getCategories( 'hidden' ) );
1222  }
1223 
1224  private function doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden ) {
1225  $catLinks = $op->getCategoryLinks();
1226  $this->assertSame( (bool)$expectedNormal + (bool)$expectedHidden, count( $catLinks ) );
1227  if ( $expectedNormal ) {
1228  $this->assertSame( count( $expectedNormal ), count( $catLinks['normal'] ) );
1229  }
1230  if ( $expectedHidden ) {
1231  $this->assertSame( count( $expectedHidden ), count( $catLinks['hidden'] ) );
1232  }
1233 
1234  foreach ( $expectedNormal as $i => $name ) {
1235  $this->assertContains( $name, $catLinks['normal'][$i] );
1236  }
1237  foreach ( $expectedHidden as $i => $name ) {
1238  $this->assertContains( $name, $catLinks['hidden'][$i] );
1239  }
1240  }
1241 
1242  public function provideGetCategories() {
1243  return [
1244  'No categories' => [ [], [], null, [], [] ],
1245  'Simple test' => [
1246  [ 'Test1' => 'Some sortkey', 'Test2' => 'A different sortkey' ],
1247  [ 'Test1' => (object)[ 'pp_value' => 1, 'page_title' => 'Test1' ],
1248  'Test2' => (object)[ 'page_title' => 'Test2' ] ],
1249  null,
1250  [ 'Test2' ],
1251  [ 'Test1' ],
1252  ],
1253  'Invalid title' => [
1254  [ '[' => '[', 'Test' => 'Test' ],
1255  [ 'Test' => (object)[ 'page_title' => 'Test' ] ],
1256  null,
1257  [ 'Test' ],
1258  [],
1259  ],
1260  'Variant link' => [
1261  [ 'Test' => 'Test', 'Estay' => 'Estay' ],
1262  [ 'Test' => (object)[ 'page_title' => 'Test' ] ],
1263  function ( &$link, &$title ) {
1264  if ( $link === 'Estay' ) {
1265  $link = 'Test';
1267  }
1268  },
1269  // For adding one by one, the variant gets added as well as the original category,
1270  // but if you add them all together the second time gets skipped.
1271  [ 'onebyone' => [ 'Test', 'Test' ], 'default' => [ 'Test' ] ],
1272  [],
1273  ],
1274  ];
1275  }
1276 
1280  public function testGetCategoriesInvalid() {
1281  $this->setExpectedException( InvalidArgumentException::class,
1282  'Invalid category type given: hiddne' );
1283 
1284  $op = $this->newInstance();
1285  $op->getCategories( 'hiddne' );
1286  }
1287 
1288  // @todo Should we test addCategoryLinksToLBAndGetResult? If so, how? Insert some test rows in
1289  // the DB?
1290 
1297  public function testIndicators() {
1298  $op = $this->newInstance();
1299  $this->assertSame( [], $op->getIndicators() );
1300 
1301  $op->setIndicators( [] );
1302  $this->assertSame( [], $op->getIndicators() );
1303 
1304  // Test sorting alphabetically
1305  $op->setIndicators( [ 'b' => 'x', 'a' => 'y' ] );
1306  $this->assertSame( [ 'a' => 'y', 'b' => 'x' ], $op->getIndicators() );
1307 
1308  // Test overwriting existing keys
1309  $op->setIndicators( [ 'c' => 'z', 'a' => 'w' ] );
1310  $this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'z' ], $op->getIndicators() );
1311 
1312  // Test with addParserOutputMetadata
1313  $pOut1 = $this->createParserOutputStub( 'getIndicators', [ 'c' => 'u', 'd' => 'v' ] );
1314  $op->addParserOutputMetadata( $pOut1 );
1315  $this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
1316  $op->getIndicators() );
1317 
1318  // Test with addParserOutput
1319  $pOut2 = $this->createParserOutputStub( 'getIndicators', [ 'a' => '!!!' ] );
1320  $op->addParserOutput( $pOut2 );
1321  $this->assertSame( [ 'a' => '!!!', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
1322  $op->getIndicators() );
1323  }
1324 
1329  public function testAddHelpLink() {
1330  $op = $this->newInstance();
1331 
1332  $op->addHelpLink( 'Manual:PHP unit testing' );
1333  $indicators = $op->getIndicators();
1334  $this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
1335  $this->assertContains( 'Manual:PHP_unit_testing', $indicators['mw-helplink'] );
1336 
1337  $op->addHelpLink( 'https://phpunit.de', true );
1338  $indicators = $op->getIndicators();
1339  $this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
1340  $this->assertContains( 'https://phpunit.de', $indicators['mw-helplink'] );
1341  $this->assertNotContains( 'mediawiki', $indicators['mw-helplink'] );
1342  $this->assertNotContains( 'Manual:PHP', $indicators['mw-helplink'] );
1343  }
1344 
1352  public function testBodyHTML() {
1353  $op = $this->newInstance();
1354  $this->assertSame( '', $op->getHTML() );
1355 
1356  $op->addHTML( 'a' );
1357  $this->assertSame( 'a', $op->getHTML() );
1358 
1359  $op->addHTML( 'b' );
1360  $this->assertSame( 'ab', $op->getHTML() );
1361 
1362  $op->prependHTML( 'c' );
1363  $this->assertSame( 'cab', $op->getHTML() );
1364 
1365  $op->addElement( 'p', [ 'id' => 'foo' ], 'd' );
1366  $this->assertSame( 'cab<p id="foo">d</p>', $op->getHTML() );
1367 
1368  $op->clearHTML();
1369  $this->assertSame( '', $op->getHTML() );
1370  }
1371 
1377  public function testRevisionId( $newVal, $expected ) {
1378  $op = $this->newInstance();
1379 
1380  $this->assertNull( $op->setRevisionId( $newVal ) );
1381  $this->assertSame( $expected, $op->getRevisionId() );
1382  $this->assertSame( $expected, $op->setRevisionId( null ) );
1383  $this->assertNull( $op->getRevisionId() );
1384  }
1385 
1386  public function provideRevisionId() {
1387  return [
1388  [ null, null ],
1389  [ 7, 7 ],
1390  [ -1, -1 ],
1391  [ 3.2, 3 ],
1392  [ '0', 0 ],
1393  [ '32% finished', 32 ],
1394  [ false, 0 ],
1395  ];
1396  }
1397 
1402  public function testRevisionTimestamp() {
1403  $op = $this->newInstance();
1404  $this->assertNull( $op->getRevisionTimestamp() );
1405 
1406  $this->assertNull( $op->setRevisionTimestamp( 'abc' ) );
1407  $this->assertSame( 'abc', $op->getRevisionTimestamp() );
1408  $this->assertSame( 'abc', $op->setRevisionTimestamp( null ) );
1409  $this->assertNull( $op->getRevisionTimestamp() );
1410  }
1411 
1416  public function testFileVersion() {
1417  $op = $this->newInstance();
1418  $this->assertNull( $op->getFileVersion() );
1419 
1420  $stubFile = $this->createMock( File::class );
1421  $stubFile->method( 'exists' )->willReturn( true );
1422  $stubFile->method( 'getTimestamp' )->willReturn( '12211221123321' );
1423  $stubFile->method( 'getSha1' )->willReturn( 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' );
1424 
1425  $op->setFileVersion( $stubFile );
1426 
1427  $this->assertEquals(
1428  [ 'time' => '12211221123321', 'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' ],
1429  $op->getFileVersion()
1430  );
1431 
1432  $stubMissingFile = $this->createMock( File::class );
1433  $stubMissingFile->method( 'exists' )->willReturn( false );
1434 
1435  $op->setFileVersion( $stubMissingFile );
1436  $this->assertNull( $op->getFileVersion() );
1437 
1438  $op->setFileVersion( $stubFile );
1439  $this->assertNotNull( $op->getFileVersion() );
1440 
1441  $op->setFileVersion( null );
1442  $this->assertNull( $op->getFileVersion() );
1443  }
1444 
1449  private function createParserOutputStub( ...$args ) {
1450  if ( count( $args ) === 0 ) {
1451  $retVals = [];
1452  } elseif ( count( $args ) === 1 ) {
1453  $retVals = $args[0];
1454  } elseif ( count( $args ) === 2 ) {
1455  $retVals = [ $args[0] => $args[1] ];
1456  }
1457  $pOut = $this->getMock( ParserOutput::class );
1458  foreach ( $retVals as $method => $retVal ) {
1459  $pOut->method( $method )->willReturn( $retVal );
1460  }
1461 
1462  $arrayReturningMethods = [
1463  'getCategories',
1464  'getFileSearchOptions',
1465  'getHeadItems',
1466  'getIndicators',
1467  'getLanguageLinks',
1468  'getOutputHooks',
1469  'getTemplateIds',
1470  ];
1471 
1472  foreach ( $arrayReturningMethods as $method ) {
1473  $pOut->method( $method )->willReturn( [] );
1474  }
1475 
1476  return $pOut;
1477  }
1478 
1484  public function testTemplateIds() {
1485  $op = $this->newInstance();
1486  $this->assertSame( [], $op->getTemplateIds() );
1487 
1488  // Test with no template id's
1489  $stubPOEmpty = $this->createParserOutputStub();
1490  $op->addParserOutputMetadata( $stubPOEmpty );
1491  $this->assertSame( [], $op->getTemplateIds() );
1492 
1493  // Test with some arbitrary template id's
1494  $ids = [
1495  NS_MAIN => [ 'A' => 3, 'B' => 17 ],
1496  NS_TALK => [ 'C' => 31 ],
1497  NS_MEDIA => [ 'D' => -1 ],
1498  ];
1499 
1500  $stubPO1 = $this->createParserOutputStub( 'getTemplateIds', $ids );
1501 
1502  $op->addParserOutputMetadata( $stubPO1 );
1503  $this->assertSame( $ids, $op->getTemplateIds() );
1504 
1505  // Test merging with a second set of id's
1506  $stubPO2 = $this->createParserOutputStub( 'getTemplateIds', [
1507  NS_MAIN => [ 'E' => 1234 ],
1508  NS_PROJECT => [ 'F' => 5678 ],
1509  ] );
1510 
1511  $finalIds = [
1512  NS_MAIN => [ 'E' => 1234, 'A' => 3, 'B' => 17 ],
1513  NS_TALK => [ 'C' => 31 ],
1514  NS_MEDIA => [ 'D' => -1 ],
1515  NS_PROJECT => [ 'F' => 5678 ],
1516  ];
1517 
1518  $op->addParserOutput( $stubPO2 );
1519  $this->assertSame( $finalIds, $op->getTemplateIds() );
1520 
1521  // Test merging with an empty set of id's
1522  $op->addParserOutputMetadata( $stubPOEmpty );
1523  $this->assertSame( $finalIds, $op->getTemplateIds() );
1524  }
1525 
1531  public function testFileSearchOptions() {
1532  $op = $this->newInstance();
1533  $this->assertSame( [], $op->getFileSearchOptions() );
1534 
1535  // Test with no files
1536  $stubPOEmpty = $this->createParserOutputStub();
1537 
1538  $op->addParserOutputMetadata( $stubPOEmpty );
1539  $this->assertSame( [], $op->getFileSearchOptions() );
1540 
1541  // Test with some arbitrary files
1542  $files1 = [
1543  'A' => [ 'time' => null, 'sha1' => '' ],
1544  'B' => [
1545  'time' => '12211221123321',
1546  'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05',
1547  ],
1548  ];
1549 
1550  $stubPO1 = $this->createParserOutputStub( 'getFileSearchOptions', $files1 );
1551 
1552  $op->addParserOutput( $stubPO1 );
1553  $this->assertSame( $files1, $op->getFileSearchOptions() );
1554 
1555  // Test merging with a second set of files
1556  $files2 = [
1557  'C' => [ 'time' => null, 'sha1' => '' ],
1558  'B' => [ 'time' => null, 'sha1' => '' ],
1559  ];
1560 
1561  $stubPO2 = $this->createParserOutputStub( 'getFileSearchOptions', $files2 );
1562 
1563  $op->addParserOutputMetadata( $stubPO2 );
1564  $this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
1565 
1566  // Test merging with an empty set of files
1567  $op->addParserOutput( $stubPOEmpty );
1568  $this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
1569  }
1570 
1583  public function testAddWikiText( $method, array $args, $expected ) {
1584  $op = $this->newInstance();
1585  $this->assertSame( '', $op->getHTML() );
1586 
1587  $this->hideDeprecated( 'OutputPage::addWikiText' );
1588  $this->hideDeprecated( 'OutputPage::addWikiTextTitle' );
1589  $this->hideDeprecated( 'OutputPage::addWikiTextWithTitle' );
1590  $this->hideDeprecated( 'OutputPage::addWikiTextTidy' );
1591  $this->hideDeprecated( 'OutputPage::addWikiTextTitleTidy' );
1592  $this->hideDeprecated( 'disabling tidy' );
1593 
1594  if ( in_array(
1595  $method,
1596  [ 'addWikiTextWithTitle', 'addWikiTextTitleTidy', 'addWikiTextTitle' ]
1597  ) && count( $args ) >= 2 && $args[1] === null ) {
1598  // Special placeholder because we can't get the actual title in the provider
1599  $args[1] = $op->getTitle();
1600  }
1601  if ( in_array(
1602  $method,
1603  [ 'addWikiTextAsInterface', 'addWikiTextAsContent' ]
1604  ) && count( $args ) >= 3 && $args[2] === null ) {
1605  // Special placeholder because we can't get the actual title in the provider
1606  $args[2] = $op->getTitle();
1607  }
1608 
1609  $op->$method( ...$args );
1610  $this->assertSame( $expected, $op->getHTML() );
1611  }
1612 
1613  public function provideAddWikiText() {
1614  $tests = [
1615  'addWikiText' => [
1616  // Not tidied; this API is deprecated.
1617  'Simple wikitext' => [
1618  [ "'''Bold'''" ],
1619  "<p><b>Bold</b>\n</p>",
1620  ], 'List at start' => [
1621  [ '* List' ],
1622  "<ul><li>List</li></ul>\n",
1623  ], 'List not at start' => [
1624  [ '* Not a list', false ],
1625  '* Not a list',
1626  ], 'Non-interface' => [
1627  [ "'''Bold'''", true, false ],
1628  "<p><b>Bold</b>\n</p>",
1629  ], 'No section edit links' => [
1630  [ '== Title ==' ],
1631  "<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>",
1632  ],
1633  ],
1634  'addWikiTextWithTitle' => [
1635  // Untidied; this API is deprecated
1636  'With title at start' => [
1637  [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ) ],
1638  "<ul><li>Some page</li></ul>\n",
1639  ], 'With title at start' => [
1640  [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ), false ],
1641  "* Some page",
1642  ],
1643  ],
1644  'addWikiTextAsInterface' => [
1645  // Preferred interface: output is tidied
1646  'Simple wikitext' => [
1647  [ "'''Bold'''" ],
1648  "<p><b>Bold</b>\n</p>",
1649  ], 'Untidy wikitext' => [
1650  [ "<b>Bold" ],
1651  "<p><b>Bold\n</b></p>",
1652  ], 'List at start' => [
1653  [ '* List' ],
1654  "<ul><li>List</li></ul>\n",
1655  ], 'List not at start' => [
1656  [ '* Not a list', false ],
1657  '<p>* Not a list</p>',
1658  ], 'No section edit links' => [
1659  [ '== Title ==' ],
1660  "<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>",
1661  ], 'With title at start' => [
1662  [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1663  "<ul><li>Some page</li></ul>\n",
1664  ], 'With title at start' => [
1665  [ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
1666  "<p>* Some page</p>",
1667  ], 'Untidy input' => [
1668  [ '<b>{{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1669  "<p><b>Some page\n</b></p>",
1670  ],
1671  ],
1672  'addWikiTextAsContent' => [
1673  // Preferred interface: output is tidied
1674  'SpecialNewimages' => [
1675  [ "<p lang='en' dir='ltr'>\nMy message" ],
1676  '<p lang="en" dir="ltr">' . "\nMy message</p>"
1677  ], 'List at start' => [
1678  [ '* List' ],
1679  "<ul><li>List</li></ul>",
1680  ], 'List not at start' => [
1681  [ '* <b>Not a list', false ],
1682  '<p>* <b>Not a list</b></p>',
1683  ], 'With title at start' => [
1684  [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1685  "<ul><li>Some page</li></ul>\n",
1686  ], 'With title at start' => [
1687  [ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
1688  "<p>* Some page</p>",
1689  ], 'EditPage' => [
1690  [ "<div class='mw-editintro'>{{PAGENAME}}", true, Title::newFromText( 'Talk:Some page' ) ],
1691  '<div class="mw-editintro">' . "Some page</div>"
1692  ],
1693  ],
1694  'wrapWikiTextAsInterface' => [
1695  'Simple' => [
1696  [ 'wrapperClass', 'text' ],
1697  "<div class=\"wrapperClass\"><p>text\n</p></div>"
1698  ], 'Spurious </div>' => [
1699  [ 'wrapperClass', 'text</div><div>more' ],
1700  "<div class=\"wrapperClass\"><p>text</p><div>more</div></div>"
1701  ], 'Extra newlines would break <p> wrappers' => [
1702  [ 'two classes', "1\n\n2\n\n3" ],
1703  "<div class=\"two classes\"><p>1\n</p><p>2\n</p><p>3\n</p></div>"
1704  ], 'Other unclosed tags' => [
1705  [ 'error', 'a<b>c<i>d' ],
1706  "<div class=\"error\"><p>a<b>c<i>d\n</i></b></p></div>"
1707  ],
1708  ],
1709  ];
1710 
1711  // Test all the others on addWikiTextTitle as well
1712  foreach ( $tests['addWikiText'] as $key => $val ) {
1713  $args = [ $val[0][0], null, $val[0][1] ?? true, false, $val[0][2] ?? true ];
1714  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1715  array_merge( [ $args ], array_slice( $val, 1 ) );
1716  }
1717  foreach ( $tests['addWikiTextWithTitle'] as $key => $val ) {
1718  $args = [ $val[0][0], $val[0][1], $val[0][2] ?? true ];
1719  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1720  array_merge( [ $args ], array_slice( $val, 1 ) );
1721  }
1722  foreach ( $tests['addWikiTextAsInterface'] as $key => $val ) {
1723  $args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, true ];
1724  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1725  array_merge( [ $args ], array_slice( $val, 1 ) );
1726  }
1727  foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
1728  $args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, false ];
1729  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1730  array_merge( [ $args ], array_slice( $val, 1 ) );
1731  }
1732  // addWikiTextTidy / addWikiTextTitleTidy were old aliases of
1733  // addWikiTextAsContent
1734  foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
1735  if ( count( $val[0] ) > 2 ) {
1736  $args = [ $val[0][0], $val[0][2], $val[0][1] ?? true ];
1737  $tests['addWikiTextTitleTidy']["$key (addWikiTextTitleTidy)"] =
1738  array_merge( [ $args ], array_slice( $val, 1 ) );
1739  } else {
1740  $args = [ $val[0][0], $val[0][1] ?? true ];
1741  $tests['addWikiTextTidy']["$key (addWikiTextTidy)"] =
1742  array_merge( [ $args ], array_slice( $val, 1 ) );
1743  }
1744  }
1745 
1746  // We have to reformat our array to match what PHPUnit wants
1747  $ret = [];
1748  foreach ( $tests as $key => $subarray ) {
1749  foreach ( $subarray as $subkey => $val ) {
1750  $val = array_merge( [ $key ], $val );
1751  $ret[$subkey] = $val;
1752  }
1753  }
1754 
1755  return $ret;
1756  }
1757 
1761  public function testAddWikiTextNoTitle() {
1762  $this->hideDeprecated( 'OutputPage::addWikiText' );
1763  $this->setExpectedException( MWException::class, 'Title is null' );
1764 
1765  $op = $this->newInstance( [], null, 'notitle' );
1766  $op->addWikiText( 'a' );
1767  }
1768 
1773  $this->setExpectedException( MWException::class, 'Title is null' );
1774 
1775  $op = $this->newInstance( [], null, 'notitle' );
1776  $op->addWikiTextAsInterface( 'a' );
1777  }
1778 
1783  $this->setExpectedException( MWException::class, 'Title is null' );
1784 
1785  $op = $this->newInstance( [], null, 'notitle' );
1786  $op->addWikiTextAsContent( 'a' );
1787  }
1788 
1792  public function testAddWikiMsg() {
1793  $msg = wfMessage( 'parentheses' );
1794  $this->assertSame( '(a)', $msg->rawParams( 'a' )->plain() );
1795 
1796  $op = $this->newInstance();
1797  $this->assertSame( '', $op->getHTML() );
1798  $op->addWikiMsg( 'parentheses', "<b>a" );
1799  // The input is bad unbalanced HTML, but the output is tidied
1800  $this->assertSame( "<p>(<b>a)\n</b></p>", $op->getHTML() );
1801  }
1802 
1806  public function testWrapWikiMsg() {
1807  $msg = wfMessage( 'parentheses' );
1808  $this->assertSame( '(a)', $msg->rawParams( 'a' )->plain() );
1809 
1810  $op = $this->newInstance();
1811  $this->assertSame( '', $op->getHTML() );
1812  $op->wrapWikiMsg( '[$1]', [ 'parentheses', "<b>a" ] );
1813  // The input is bad unbalanced HTML, but the output is tidied
1814  $this->assertSame( "<p>[(<b>a)]\n</b></p>", $op->getHTML() );
1815  }
1816 
1821  public function testNoGallery() {
1822  $op = $this->newInstance();
1823  $this->assertFalse( $op->mNoGallery );
1824 
1825  $stubPO1 = $this->createParserOutputStub( 'getNoGallery', true );
1826  $op->addParserOutputMetadata( $stubPO1 );
1827  $this->assertTrue( $op->mNoGallery );
1828 
1829  $stubPO2 = $this->createParserOutputStub( 'getNoGallery', false );
1830  $op->addParserOutput( $stubPO2 );
1831  $this->assertFalse( $op->mNoGallery );
1832  }
1833 
1834  private static $parserOutputHookCalled;
1835 
1839  public function testParserOutputHooks() {
1840  $op = $this->newInstance();
1841  $pOut = $this->createParserOutputStub( 'getOutputHooks', [
1842  [ 'myhook', 'banana' ],
1843  [ 'yourhook', 'kumquat' ],
1844  [ 'theirhook', 'hippopotamus' ],
1845  ] );
1846 
1847  self::$parserOutputHookCalled = [];
1848 
1849  $this->setMwGlobals( 'wgParserOutputHooks', [
1850  'myhook' => function ( OutputPage $innerOp, ParserOutput $innerPOut, $data )
1851  use ( $op, $pOut ) {
1852  $this->assertSame( $op, $innerOp );
1853  $this->assertSame( $pOut, $innerPOut );
1854  $this->assertSame( 'banana', $data );
1855  self::$parserOutputHookCalled[] = 'closure';
1856  },
1857  'yourhook' => [ $this, 'parserOutputHookCallback' ],
1858  'theirhook' => [ __CLASS__, 'parserOutputHookCallbackStatic' ],
1859  'uncalled' => function () {
1860  $this->assertTrue( false );
1861  },
1862  ] );
1863 
1864  $op->addParserOutputMetadata( $pOut );
1865 
1866  $this->assertSame( [ 'closure', 'callback', 'static' ], self::$parserOutputHookCalled );
1867  }
1868 
1869  public function parserOutputHookCallback(
1870  OutputPage $op, ParserOutput $pOut, $data
1871  ) {
1872  $this->assertSame( 'kumquat', $data );
1873 
1874  self::$parserOutputHookCalled[] = 'callback';
1875  }
1876 
1877  public static function parserOutputHookCallbackStatic(
1878  OutputPage $op, ParserOutput $pOut, $data
1879  ) {
1880  // All the assert methods are actually static, who knew!
1881  self::assertSame( 'hippopotamus', $data );
1882 
1883  self::$parserOutputHookCalled[] = 'static';
1884  }
1885 
1886  // @todo Make sure to test the following in addParserOutputMetadata() as well when we add tests
1887  // for them:
1888  // * addModules()
1889  // * addModuleStyles()
1890  // * addJsConfigVars()
1891  // * enableOOUI()
1892  // Otherwise those lines of addParserOutputMetadata() will be reported as covered, but we won't
1893  // be testing they actually work.
1894 
1898  public function testAddParserOutputText() {
1899  $op = $this->newInstance();
1900  $this->assertSame( '', $op->getHTML() );
1901 
1902  $pOut = $this->createParserOutputStub( 'getText', '<some text>' );
1903 
1904  $op->addParserOutputMetadata( $pOut );
1905  $this->assertSame( '', $op->getHTML() );
1906 
1907  $op->addParserOutputText( $pOut );
1908  $this->assertSame( '<some text>', $op->getHTML() );
1909  }
1910 
1914  public function testAddParserOutput() {
1915  $op = $this->newInstance();
1916  $this->assertSame( '', $op->getHTML() );
1917  $this->assertFalse( $op->showNewSectionLink() );
1918 
1919  $pOut = $this->createParserOutputStub( [
1920  'getText' => '<some text>',
1921  'getNewSection' => true,
1922  ] );
1923 
1924  $op->addParserOutput( $pOut );
1925  $this->assertSame( '<some text>', $op->getHTML() );
1926  $this->assertTrue( $op->showNewSectionLink() );
1927  }
1928 
1932  public function testAddTemplate() {
1933  $template = $this->getMock( QuickTemplate::class );
1934  $template->method( 'getHTML' )->willReturn( '<abc>&def;' );
1935 
1936  $op = $this->newInstance();
1937  $op->addTemplate( $template );
1938 
1939  $this->assertSame( '<abc>&def;', $op->getHTML() );
1940  }
1941 
1949  public function testParse( array $args, $expectedHTML ) {
1950  $this->hideDeprecated( 'OutputPage::parse' );
1951  $op = $this->newInstance();
1952  $this->assertSame( $expectedHTML, $op->parse( ...$args ) );
1953  }
1954 
1959  public function testParseInline( array $args, $expectedHTML, $expectedHTMLInline = null ) {
1960  if ( count( $args ) > 3 ) {
1961  // $language param not supported
1962  $this->assertTrue( true );
1963  return;
1964  }
1965  $this->hideDeprecated( 'OutputPage::parseInline' );
1966  $op = $this->newInstance();
1967  $this->assertSame( $expectedHTMLInline ?? $expectedHTML, $op->parseInline( ...$args ) );
1968  }
1969 
1970  public function provideParse() {
1971  return [
1972  'List at start of line (content)' => [
1973  [ '* List', true, false ],
1974  "<div class=\"mw-parser-output\"><ul><li>List</li></ul></div>",
1975  "<ul><li>List</li></ul>",
1976  ],
1977  'List at start of line (interface)' => [
1978  [ '* List', true, true ],
1979  "<ul><li>List</li></ul>",
1980  ],
1981  'List not at start (content)' => [
1982  [ "* ''Not'' list", false, false ],
1983  '<div class="mw-parser-output">* <i>Not</i> list</div>',
1984  '* <i>Not</i> list',
1985  ],
1986  'List not at start (interface)' => [
1987  [ "* ''Not'' list", false, true ],
1988  '* <i>Not</i> list',
1989  ],
1990  'Interface message' => [
1991  [ "''Italic''", true, true ],
1992  "<p><i>Italic</i>\n</p>",
1993  '<i>Italic</i>',
1994  ],
1995  'formatnum (content)' => [
1996  [ '{{formatnum:123456.789}}', true, false ],
1997  "<div class=\"mw-parser-output\"><p>123,456.789\n</p></div>",
1998  "123,456.789",
1999  ],
2000  'formatnum (interface)' => [
2001  [ '{{formatnum:123456.789}}', true, true ],
2002  "<p>123,456.789\n</p>",
2003  "123,456.789",
2004  ],
2005  'Language (content)' => [
2006  [ '{{formatnum:123456.789}}', true, false, Language::factory( 'is' ) ],
2007  "<div class=\"mw-parser-output\"><p>123.456,789\n</p></div>",
2008  ],
2009  'Language (interface)' => [
2010  [ '{{formatnum:123456.789}}', true, true, Language::factory( 'is' ) ],
2011  "<p>123.456,789\n</p>",
2012  '123.456,789',
2013  ],
2014  'No section edit links' => [
2015  [ '== Header ==' ],
2016  '<div class="mw-parser-output"><h2><span class="mw-headline" id="Header">' .
2017  "Header</span></h2></div>",
2018  '<h2><span class="mw-headline" id="Header">Header</span></h2>',
2019  ]
2020  ];
2021  }
2022 
2030  public function testParseAsContent(
2031  array $args, $expectedHTML, $expectedHTMLInline = null
2032  ) {
2033  $op = $this->newInstance();
2034  $this->assertSame( $expectedHTML, $op->parseAsContent( ...$args ) );
2035  }
2036 
2044  public function testParseAsInterface(
2045  array $args, $expectedHTML, $expectedHTMLInline = null
2046  ) {
2047  $op = $this->newInstance();
2048  $this->assertSame( $expectedHTML, $op->parseAsInterface( ...$args ) );
2049  }
2050 
2056  array $args, $expectedHTML, $expectedHTMLInline = null
2057  ) {
2058  $op = $this->newInstance();
2059  $this->assertSame(
2060  $expectedHTMLInline ?? $expectedHTML,
2061  $op->parseInlineAsInterface( ...$args )
2062  );
2063  }
2064 
2065  public function provideParseAs() {
2066  return [
2067  'List at start of line' => [
2068  [ '* List', true ],
2069  "<ul><li>List</li></ul>",
2070  ],
2071  'List not at start' => [
2072  [ "* ''Not'' list", false ],
2073  '<p>* <i>Not</i> list</p>',
2074  '* <i>Not</i> list',
2075  ],
2076  'Italics' => [
2077  [ "''Italic''", true ],
2078  "<p><i>Italic</i>\n</p>",
2079  '<i>Italic</i>',
2080  ],
2081  'formatnum' => [
2082  [ '{{formatnum:123456.789}}', true ],
2083  "<p>123,456.789\n</p>",
2084  "123,456.789",
2085  ],
2086  'No section edit links' => [
2087  [ '== Header ==' ],
2088  '<h2><span class="mw-headline" id="Header">Header</span></h2>',
2089  ]
2090  ];
2091  }
2092 
2096  public function testParseNullTitle() {
2097  $this->hideDeprecated( 'OutputPage::parse' );
2098  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2099  $op = $this->newInstance( [], null, 'notitle' );
2100  $op->parse( '' );
2101  }
2102 
2106  public function testParseInlineNullTitle() {
2107  $this->hideDeprecated( 'OutputPage::parseInline' );
2108  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2109  $op = $this->newInstance( [], null, 'notitle' );
2110  $op->parseInline( '' );
2111  }
2112 
2116  public function testParseAsContentNullTitle() {
2117  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2118  $op = $this->newInstance( [], null, 'notitle' );
2119  $op->parseAsContent( '' );
2120  }
2121 
2125  public function testParseAsInterfaceNullTitle() {
2126  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2127  $op = $this->newInstance( [], null, 'notitle' );
2128  $op->parseAsInterface( '' );
2129  }
2130 
2135  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2136  $op = $this->newInstance( [], null, 'notitle' );
2137  $op->parseInlineAsInterface( '' );
2138  }
2139 
2144  public function testCdnMaxage() {
2145  $op = $this->newInstance();
2146  $wrapper = TestingAccessWrapper::newFromObject( $op );
2147  $this->assertSame( 0, $wrapper->mCdnMaxage );
2148 
2149  $op->setCdnMaxage( -1 );
2150  $this->assertSame( -1, $wrapper->mCdnMaxage );
2151 
2152  $op->setCdnMaxage( 120 );
2153  $this->assertSame( 120, $wrapper->mCdnMaxage );
2154 
2155  $op->setCdnMaxage( 60 );
2156  $this->assertSame( 60, $wrapper->mCdnMaxage );
2157 
2158  $op->setCdnMaxage( 180 );
2159  $this->assertSame( 180, $wrapper->mCdnMaxage );
2160 
2161  $op->lowerCdnMaxage( 240 );
2162  $this->assertSame( 180, $wrapper->mCdnMaxage );
2163 
2164  $op->setCdnMaxage( 300 );
2165  $this->assertSame( 240, $wrapper->mCdnMaxage );
2166 
2167  $op->lowerCdnMaxage( 120 );
2168  $this->assertSame( 120, $wrapper->mCdnMaxage );
2169 
2170  $op->setCdnMaxage( 180 );
2171  $this->assertSame( 120, $wrapper->mCdnMaxage );
2172 
2173  $op->setCdnMaxage( 60 );
2174  $this->assertSame( 60, $wrapper->mCdnMaxage );
2175 
2176  $op->setCdnMaxage( 240 );
2177  $this->assertSame( 120, $wrapper->mCdnMaxage );
2178  }
2179 
2181  private static $fakeTime;
2182 
2191  public function testAdaptCdnTTL( array $args, $expected, array $options = [] ) {
2192  try {
2193  MWTimestamp::setFakeTime( self::$fakeTime );
2194 
2195  $op = $this->newInstance();
2196  // Set a high maxage so that it will get reduced by adaptCdnTTL(). The default maxage
2197  // is 0, so adaptCdnTTL() won't mutate the object at all.
2198  $initial = $options['initialMaxage'] ?? 86400;
2199  $op->setCdnMaxage( $initial );
2200 
2201  $op->adaptCdnTTL( ...$args );
2202  } finally {
2203  MWTimestamp::setFakeTime( false );
2204  }
2205 
2206  $wrapper = TestingAccessWrapper::newFromObject( $op );
2207 
2208  // Special rules for false/null
2209  if ( $args[0] === null || $args[0] === false ) {
2210  $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
2211  $op->setCdnMaxage( $expected + 1 );
2212  $this->assertSame( $expected + 1, $wrapper->mCdnMaxage, 'member value after new set' );
2213  return;
2214  }
2215 
2216  $this->assertSame( $expected, $wrapper->mCdnMaxageLimit, 'limit value' );
2217 
2218  if ( $initial >= $expected ) {
2219  $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value' );
2220  } else {
2221  $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
2222  }
2223 
2224  $op->setCdnMaxage( $expected + 1 );
2225  $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value after new set' );
2226  }
2227 
2228  public function provideAdaptCdnTTL() {
2229  global $wgCdnMaxAge;
2230  $now = time();
2231  self::$fakeTime = $now;
2232  return [
2233  'Five minutes ago' => [ [ $now - 300 ], 270 ],
2234  'Now' => [ [ +0 ], IExpiringStore::TTL_MINUTE ],
2235  'Five minutes from now' => [ [ $now + 300 ], IExpiringStore::TTL_MINUTE ],
2236  'Five minutes ago, initial maxage four minutes' =>
2237  [ [ $now - 300 ], 270, [ 'initialMaxage' => 240 ] ],
2238  'A very long time ago' => [ [ $now - 1000000000 ], $wgCdnMaxAge ],
2239  'Initial maxage zero' => [ [ $now - 300 ], 270, [ 'initialMaxage' => 0 ] ],
2240 
2241  'false' => [ [ false ], IExpiringStore::TTL_MINUTE ],
2242  'null' => [ [ null ], IExpiringStore::TTL_MINUTE ],
2243  "'0'" => [ [ '0' ], IExpiringStore::TTL_MINUTE ],
2244  'Empty string' => [ [ '' ], IExpiringStore::TTL_MINUTE ],
2245  // @todo These give incorrect results due to timezones, how to test?
2246  //"'now'" => [ [ 'now' ], IExpiringStore::TTL_MINUTE ],
2247  //"'parse error'" => [ [ 'parse error' ], IExpiringStore::TTL_MINUTE ],
2248 
2249  'Now, minTTL 0' => [ [ $now, 0 ], IExpiringStore::TTL_MINUTE ],
2250  'Now, minTTL 0.000001' => [ [ $now, 0.000001 ], 0 ],
2251  'A very long time ago, maxTTL even longer' =>
2252  [ [ $now - 1000000000, 0, 1000000001 ], 900000000 ],
2253  ];
2254  }
2255 
2261  public function testClientCache() {
2262  $op = $this->newInstance();
2263 
2264  // Test initial value
2265  $this->assertSame( true, $op->enableClientCache( null ) );
2266  // Test that calling with null doesn't change the value
2267  $this->assertSame( true, $op->enableClientCache( null ) );
2268 
2269  // Test setting to false
2270  $this->assertSame( true, $op->enableClientCache( false ) );
2271  $this->assertSame( false, $op->enableClientCache( null ) );
2272  // Test that calling with null doesn't change the value
2273  $this->assertSame( false, $op->enableClientCache( null ) );
2274 
2275  // Test that a cacheable ParserOutput doesn't set to true
2276  $pOutCacheable = $this->createParserOutputStub( 'isCacheable', true );
2277  $op->addParserOutputMetadata( $pOutCacheable );
2278  $this->assertSame( false, $op->enableClientCache( null ) );
2279 
2280  // Test setting back to true
2281  $this->assertSame( false, $op->enableClientCache( true ) );
2282  $this->assertSame( true, $op->enableClientCache( null ) );
2283 
2284  // Test that an uncacheable ParserOutput does set to false
2285  $pOutUncacheable = $this->createParserOutputStub( 'isCacheable', false );
2286  $op->addParserOutput( $pOutUncacheable );
2287  $this->assertSame( false, $op->enableClientCache( null ) );
2288  }
2289 
2293  public function testGetCacheVaryCookies() {
2294  global $wgCookiePrefix, $wgDBname;
2295  $op = $this->newInstance();
2296  $prefix = $wgCookiePrefix !== false ? $wgCookiePrefix : $wgDBname;
2297  $expectedCookies = [
2298  "{$prefix}Token",
2299  "{$prefix}LoggedOut",
2300  "{$prefix}_session",
2301  'forceHTTPS',
2302  'cookie1',
2303  'cookie2',
2304  ];
2305 
2306  // We have to reset the cookies because getCacheVaryCookies may have already been called
2307  TestingAccessWrapper::newFromClass( OutputPage::class )->cacheVaryCookies = null;
2308 
2309  $this->setMwGlobals( 'wgCacheVaryCookies', [ 'cookie1' ] );
2310  $this->setTemporaryHook( 'GetCacheVaryCookies',
2311  function ( $innerOP, &$cookies ) use ( $op, $expectedCookies ) {
2312  $this->assertSame( $op, $innerOP );
2313  $cookies[] = 'cookie2';
2314  $this->assertSame( $expectedCookies, $cookies );
2315  }
2316  );
2317 
2318  $this->assertSame( $expectedCookies, $op->getCacheVaryCookies() );
2319  }
2320 
2324  public function testHaveCacheVaryCookies() {
2325  $request = new FauxRequest();
2326  $op = $this->newInstance( [], $request );
2327 
2328  // No cookies are set.
2329  $this->assertFalse( $op->haveCacheVaryCookies() );
2330 
2331  // 'Token' is present but empty, so it shouldn't count.
2332  $request->setCookie( 'Token', '' );
2333  $this->assertFalse( $op->haveCacheVaryCookies() );
2334 
2335  // 'Token' present and nonempty.
2336  $request->setCookie( 'Token', '123' );
2337  $this->assertTrue( $op->haveCacheVaryCookies() );
2338  }
2339 
2352  public function testVaryHeaders( array $calls, array $cookies, $vary, $key ) {
2353  // Get rid of default Vary fields
2354  $op = $this->getMockBuilder( OutputPage::class )
2355  ->setConstructorArgs( [ new RequestContext() ] )
2356  ->setMethods( [ 'getCacheVaryCookies' ] )
2357  ->getMock();
2358  $op->expects( $this->any() )
2359  ->method( 'getCacheVaryCookies' )
2360  ->will( $this->returnValue( $cookies ) );
2361  TestingAccessWrapper::newFromObject( $op )->mVaryHeader = [];
2362 
2363  $this->hideDeprecated( '$wgUseKeyHeader' );
2364  foreach ( $calls as $call ) {
2365  $op->addVaryHeader( ...$call );
2366  }
2367  $this->assertEquals( $vary, $op->getVaryHeader(), 'Vary:' );
2368  $this->assertEquals( $key, $op->getKeyHeader(), 'Key:' );
2369  }
2370 
2371  public function provideVaryHeaders() {
2372  // note: getKeyHeader() automatically adds Vary: Cookie
2373  return [
2374  'No header' => [
2375  [],
2376  [],
2377  'Vary: ',
2378  'Key: Cookie',
2379  ],
2380  'Single header' => [
2381  [
2382  [ 'Cookie' ],
2383  ],
2384  [],
2385  'Vary: Cookie',
2386  'Key: Cookie',
2387  ],
2388  'Non-unique headers' => [
2389  [
2390  [ 'Cookie' ],
2391  [ 'Accept-Language' ],
2392  [ 'Cookie' ],
2393  ],
2394  [],
2395  'Vary: Cookie, Accept-Language',
2396  'Key: Cookie,Accept-Language',
2397  ],
2398  'Two headers with single options' => [
2399  [
2400  [ 'Cookie', [ 'param=phpsessid' ] ],
2401  [ 'Accept-Language', [ 'substr=en' ] ],
2402  ],
2403  [],
2404  'Vary: Cookie, Accept-Language',
2405  'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
2406  ],
2407  'One header with multiple options' => [
2408  [
2409  [ 'Cookie', [ 'param=phpsessid', 'param=userId' ] ],
2410  ],
2411  [],
2412  'Vary: Cookie',
2413  'Key: Cookie;param=phpsessid;param=userId',
2414  ],
2415  'Duplicate option' => [
2416  [
2417  [ 'Cookie', [ 'param=phpsessid' ] ],
2418  [ 'Cookie', [ 'param=phpsessid' ] ],
2419  [ 'Accept-Language', [ 'substr=en', 'substr=en' ] ],
2420  ],
2421  [],
2422  'Vary: Cookie, Accept-Language',
2423  'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
2424  ],
2425  'Same header, different options' => [
2426  [
2427  [ 'Cookie', [ 'param=phpsessid' ] ],
2428  [ 'Cookie', [ 'param=userId' ] ],
2429  ],
2430  [],
2431  'Vary: Cookie',
2432  'Key: Cookie;param=phpsessid;param=userId',
2433  ],
2434  'No header, vary cookies' => [
2435  [],
2436  [ 'cookie1', 'cookie2' ],
2437  'Vary: Cookie',
2438  'Key: Cookie;param=cookie1;param=cookie2',
2439  ],
2440  'Cookie header with option plus vary cookies' => [
2441  [
2442  [ 'Cookie', [ 'param=cookie1' ] ],
2443  ],
2444  [ 'cookie2', 'cookie3' ],
2445  'Vary: Cookie',
2446  'Key: Cookie;param=cookie1;param=cookie2;param=cookie3',
2447  ],
2448  'Non-cookie header plus vary cookies' => [
2449  [
2450  [ 'Accept-Language' ],
2451  ],
2452  [ 'cookie' ],
2453  'Vary: Accept-Language, Cookie',
2454  'Key: Accept-Language,Cookie;param=cookie',
2455  ],
2456  'Cookie and non-cookie headers plus vary cookies' => [
2457  [
2458  [ 'Cookie', [ 'param=cookie1' ] ],
2459  [ 'Accept-Language' ],
2460  ],
2461  [ 'cookie2' ],
2462  'Vary: Cookie, Accept-Language',
2463  'Key: Cookie;param=cookie1;param=cookie2,Accept-Language',
2464  ],
2465  ];
2466  }
2467 
2471  public function testVaryHeaderDefault() {
2472  $op = $this->newInstance();
2473  $this->assertSame( 'Vary: Accept-Encoding, Cookie', $op->getVaryHeader() );
2474  }
2475 
2482  public function testLinkHeaders( array $headers, $result ) {
2483  $op = $this->newInstance();
2484 
2485  foreach ( $headers as $header ) {
2486  $op->addLinkHeader( $header );
2487  }
2488 
2489  $this->assertEquals( $result, $op->getLinkHeader() );
2490  }
2491 
2492  public function provideLinkHeaders() {
2493  return [
2494  [
2495  [],
2496  false
2497  ],
2498  [
2499  [ '<https://foo/bar.jpg>;rel=preload;as=image' ],
2500  'Link: <https://foo/bar.jpg>;rel=preload;as=image',
2501  ],
2502  [
2503  [
2504  '<https://foo/bar.jpg>;rel=preload;as=image',
2505  '<https://foo/baz.jpg>;rel=preload;as=image'
2506  ],
2507  'Link: <https://foo/bar.jpg>;rel=preload;as=image,<https://foo/baz.jpg>;' .
2508  'rel=preload;as=image',
2509  ],
2510  ];
2511  }
2512 
2518  public function testAddAcceptLanguage(
2519  $code, array $variants, array $expected, array $options = []
2520  ) {
2521  $req = new FauxRequest( in_array( 'varianturl', $options ) ? [ 'variant' => 'x' ] : [] );
2522  $op = $this->newInstance( [], $req, in_array( 'notitle', $options ) ? 'notitle' : null );
2523 
2524  if ( !in_array( 'notitle', $options ) ) {
2525  $mockLang = $this->getMock( Language::class );
2526 
2527  if ( in_array( 'varianturl', $options ) ) {
2528  $mockLang->expects( $this->never() )->method( $this->anything() );
2529  } else {
2530  $mockLang->method( 'hasVariants' )->willReturn( count( $variants ) > 1 );
2531  $mockLang->method( 'getVariants' )->willReturn( $variants );
2532  $mockLang->method( 'getCode' )->willReturn( $code );
2533  }
2534 
2535  $mockTitle = $this->getMock( Title::class );
2536  $mockTitle->method( 'getPageLanguage' )->willReturn( $mockLang );
2537 
2538  $op->setTitle( $mockTitle );
2539  }
2540 
2541  // This will run addAcceptLanguage()
2542  $op->sendCacheControl();
2543 
2544  $this->hideDeprecated( '$wgUseKeyHeader' );
2545  $keyHeader = $op->getKeyHeader();
2546 
2547  if ( !$expected ) {
2548  $this->assertFalse( strpos( 'Accept-Language', $keyHeader ) );
2549  return;
2550  }
2551 
2552  $keyHeader = explode( ' ', $keyHeader, 2 )[1];
2553  $keyHeader = explode( ',', $keyHeader );
2554 
2555  $acceptLanguage = null;
2556  foreach ( $keyHeader as $item ) {
2557  if ( strpos( $item, 'Accept-Language;' ) === 0 ) {
2558  $acceptLanguage = $item;
2559  break;
2560  }
2561  }
2562 
2563  $expectedString = 'Accept-Language;substr=' . implode( ';substr=', $expected );
2564  $this->assertSame( $expectedString, $acceptLanguage );
2565  }
2566 
2567  public function provideAddAcceptLanguage() {
2568  return [
2569  'No variants' => [ 'en', [ 'en' ], [] ],
2570  'One simple variant' => [ 'en', [ 'en', 'en-x-piglatin' ], [ 'en-x-piglatin' ] ],
2571  'Multiple variants with BCP47 alternatives' => [
2572  'zh',
2573  [ 'zh', 'zh-hans', 'zh-cn', 'zh-tw' ],
2574  [ 'zh-hans', 'zh-Hans', 'zh-cn', 'zh-Hans-CN', 'zh-tw', 'zh-Hant-TW' ],
2575  ],
2576  'No title' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'notitle' ] ],
2577  'Variant in URL' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'varianturl' ] ],
2578  ];
2579  }
2580 
2588  public function testClickjacking() {
2589  $op = $this->newInstance();
2590  $this->assertTrue( $op->getPreventClickjacking() );
2591 
2592  $op->allowClickjacking();
2593  $this->assertFalse( $op->getPreventClickjacking() );
2594 
2595  $op->preventClickjacking();
2596  $this->assertTrue( $op->getPreventClickjacking() );
2597 
2598  $op->preventClickjacking( false );
2599  $this->assertFalse( $op->getPreventClickjacking() );
2600 
2601  $pOut1 = $this->createParserOutputStub( 'preventClickjacking', true );
2602  $op->addParserOutputMetadata( $pOut1 );
2603  $this->assertTrue( $op->getPreventClickjacking() );
2604 
2605  // The ParserOutput can't allow, only prevent
2606  $pOut2 = $this->createParserOutputStub( 'preventClickjacking', false );
2607  $op->addParserOutputMetadata( $pOut2 );
2608  $this->assertTrue( $op->getPreventClickjacking() );
2609 
2610  // Reset to test with addParserOutput()
2611  $op->allowClickjacking();
2612  $this->assertFalse( $op->getPreventClickjacking() );
2613 
2614  $op->addParserOutput( $pOut1 );
2615  $this->assertTrue( $op->getPreventClickjacking() );
2616 
2617  $op->addParserOutput( $pOut2 );
2618  $this->assertTrue( $op->getPreventClickjacking() );
2619  }
2620 
2626  public function testGetFrameOptions(
2627  $breakFrames, $preventClickjacking, $editPageFrameOptions, $expected
2628  ) {
2629  $op = $this->newInstance( [
2630  'BreakFrames' => $breakFrames,
2631  'EditPageFrameOptions' => $editPageFrameOptions,
2632  ] );
2633  $op->preventClickjacking( $preventClickjacking );
2634 
2635  $this->assertSame( $expected, $op->getFrameOptions() );
2636  }
2637 
2638  public function provideGetFrameOptions() {
2639  return [
2640  'BreakFrames true' => [ true, false, false, 'DENY' ],
2641  'Allow clickjacking locally' => [ false, false, 'DENY', false ],
2642  'Allow clickjacking globally' => [ false, true, false, false ],
2643  'DENY globally' => [ false, true, 'DENY', 'DENY' ],
2644  'SAMEORIGIN' => [ false, true, 'SAMEORIGIN', 'SAMEORIGIN' ],
2645  'BreakFrames with SAMEORIGIN' => [ true, true, 'SAMEORIGIN', 'DENY' ],
2646  ];
2647  }
2648 
2656  public function testMakeResourceLoaderLink( $args, $expectedHtml ) {
2657  $this->setMwGlobals( [
2658  'wgResourceLoaderDebug' => false,
2659  'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
2660  'wgCSPReportOnlyHeader' => true,
2661  ] );
2662  $class = new ReflectionClass( OutputPage::class );
2663  $method = $class->getMethod( 'makeResourceLoaderLink' );
2664  $method->setAccessible( true );
2665  $ctx = new RequestContext();
2666  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
2667  $ctx->setSkin( $skinFactory->makeSkin( 'fallback' ) );
2668  $ctx->setLanguage( 'en' );
2669  $out = new OutputPage( $ctx );
2670  $nonce = $class->getProperty( 'CSPNonce' );
2671  $nonce->setAccessible( true );
2672  $nonce->setValue( $out, 'secret' );
2673  $rl = $out->getResourceLoader();
2674  $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
2675  $rl->register( [
2676  'test.foo' => new ResourceLoaderTestModule( [
2677  'script' => 'mw.test.foo( { a: true } );',
2678  'styles' => '.mw-test-foo { content: "style"; }',
2679  ] ),
2680  'test.bar' => new ResourceLoaderTestModule( [
2681  'script' => 'mw.test.bar( { a: true } );',
2682  'styles' => '.mw-test-bar { content: "style"; }',
2683  ] ),
2684  'test.baz' => new ResourceLoaderTestModule( [
2685  'script' => 'mw.test.baz( { a: true } );',
2686  'styles' => '.mw-test-baz { content: "style"; }',
2687  ] ),
2688  'test.quux' => new ResourceLoaderTestModule( [
2689  'script' => 'mw.test.baz( { token: 123 } );',
2690  'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
2691  'group' => 'private',
2692  ] ),
2693  'test.noscript' => new ResourceLoaderTestModule( [
2694  'styles' => '.stuff { color: red; }',
2695  'group' => 'noscript',
2696  ] ),
2697  'test.group.foo' => new ResourceLoaderTestModule( [
2698  'script' => 'mw.doStuff( "foo" );',
2699  'group' => 'foo',
2700  ] ),
2701  'test.group.bar' => new ResourceLoaderTestModule( [
2702  'script' => 'mw.doStuff( "bar" );',
2703  'group' => 'bar',
2704  ] ),
2705  ] );
2706  $links = $method->invokeArgs( $out, $args );
2707  $actualHtml = strval( $links );
2708  $this->assertEquals( $expectedHtml, $actualHtml );
2709  }
2710 
2711  public static function provideMakeResourceLoaderLink() {
2712  // phpcs:disable Generic.Files.LineLength
2713  return [
2714  // Single only=scripts load
2715  [
2716  [ 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ],
2717  "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
2718  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.foo\u0026only=scripts");'
2719  . "});</script>"
2720  ],
2721  // Multiple only=styles load
2722  [
2723  [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
2724 
2725  '<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles"/>'
2726  ],
2727  // Private embed (only=scripts)
2728  [
2729  [ 'test.quux', ResourceLoaderModule::TYPE_SCRIPTS ],
2730  "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
2731  . "mw.test.baz({token:123});\nmw.loader.state({\"test.quux\":\"ready\"});"
2732  . "});</script>"
2733  ],
2734  // Load private module (combined)
2735  [
2736  [ 'test.quux', ResourceLoaderModule::TYPE_COMBINED ],
2737  "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
2738  . "mw.loader.implement(\"test.quux@1ev0ijv\",function($,jQuery,require,module){"
2739  . "mw.test.baz({token:123});},{\"css\":[\".mw-icon{transition:none}"
2740  . "\"]});});</script>"
2741  ],
2742  // Load no modules
2743  [
2745  '',
2746  ],
2747  // noscript group
2748  [
2749  [ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ],
2750  '<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&amp;modules=test.noscript&amp;only=styles"/></noscript>'
2751  ],
2752  // Load two modules in separate groups
2753  [
2754  [ [ 'test.group.foo', 'test.group.bar' ], ResourceLoaderModule::TYPE_COMBINED ],
2755  "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
2756  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.bar");'
2757  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.foo");'
2758  . "});</script>"
2759  ],
2760  ];
2761  // phpcs:enable
2762  }
2763 
2769  public function testBuildExemptModules( array $exemptStyleModules, $expect ) {
2770  $this->setMwGlobals( [
2771  'wgResourceLoaderDebug' => false,
2772  'wgLoadScript' => '/w/load.php',
2773  // Stub wgCacheEpoch as it influences getVersionHash used for the
2774  // urls in the expected HTML
2775  'wgCacheEpoch' => '20140101000000',
2776  ] );
2777 
2778  // Set up stubs
2779  $ctx = new RequestContext();
2780  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
2781  $ctx->setSkin( $skinFactory->makeSkin( 'fallback' ) );
2782  $ctx->setLanguage( 'en' );
2783  $op = $this->getMockBuilder( OutputPage::class )
2784  ->setConstructorArgs( [ $ctx ] )
2785  ->setMethods( [ 'buildCssLinksArray' ] )
2786  ->getMock();
2787  $op->expects( $this->any() )
2788  ->method( 'buildCssLinksArray' )
2789  ->willReturn( [] );
2790  $rl = $op->getResourceLoader();
2791  $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
2792 
2793  // Register custom modules
2794  $rl->register( [
2795  'example.site.a' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
2796  'example.site.b' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
2797  'example.user' => new ResourceLoaderTestModule( [ 'group' => 'user' ] ),
2798  ] );
2799 
2800  $op = TestingAccessWrapper::newFromObject( $op );
2801  $op->rlExemptStyleModules = $exemptStyleModules;
2802  $this->assertEquals(
2803  $expect,
2804  strval( $op->buildExemptModules() )
2805  );
2806  }
2807 
2808  public static function provideBuildExemptModules() {
2809  // phpcs:disable Generic.Files.LineLength
2810  return [
2811  'empty' => [
2812  'exemptStyleModules' => [],
2813  '<meta name="ResourceLoaderDynamicStyles" content=""/>',
2814  ],
2815  'empty sets' => [
2816  'exemptStyleModules' => [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ],
2817  '<meta name="ResourceLoaderDynamicStyles" content=""/>',
2818  ],
2819  'default logged-out' => [
2820  'exemptStyleModules' => [ 'site' => [ 'site.styles' ] ],
2821  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2822  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles"/>',
2823  ],
2824  'default logged-in' => [
2825  'exemptStyleModules' => [ 'site' => [ 'site.styles' ], 'user' => [ 'user.styles' ] ],
2826  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2827  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles"/>' . "\n" .
2828  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;version=1ai9g6t"/>',
2829  ],
2830  'custom modules' => [
2831  'exemptStyleModules' => [
2832  'site' => [ 'site.styles', 'example.site.a', 'example.site.b' ],
2833  'user' => [ 'user.styles', 'example.user' ],
2834  ],
2835  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2836  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.site.a%2Cb&amp;only=styles"/>' . "\n" .
2837  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles"/>' . "\n" .
2838  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.user&amp;only=styles&amp;version=0a56zyi"/>' . "\n" .
2839  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;version=1ai9g6t"/>',
2840  ],
2841  ];
2842  // phpcs:enable
2843  }
2844 
2850  public function testTransformResourcePath( $baseDir, $basePath, $uploadDir = null,
2851  $uploadPath = null, $path = null, $expected = null
2852  ) {
2853  if ( $path === null ) {
2854  // Skip optional $uploadDir and $uploadPath
2855  $path = $uploadDir;
2856  $expected = $uploadPath;
2857  $uploadDir = "$baseDir/images";
2858  $uploadPath = "$basePath/images";
2859  }
2860  $this->setMwGlobals( 'IP', $baseDir );
2861  $conf = new HashConfig( [
2862  'ResourceBasePath' => $basePath,
2863  'UploadDirectory' => $uploadDir,
2864  'UploadPath' => $uploadPath,
2865  ] );
2866 
2867  // Some of these paths don't exist and will cause warnings
2868  Wikimedia\suppressWarnings();
2869  $actual = OutputPage::transformResourcePath( $conf, $path );
2870  Wikimedia\restoreWarnings();
2871 
2872  $this->assertEquals( $expected ?: $path, $actual );
2873  }
2874 
2875  public static function provideTransformFilePath() {
2876  $baseDir = dirname( __DIR__ ) . '/data/media';
2877  return [
2878  // File that matches basePath, and exists. Hash found and appended.
2879  [
2880  'baseDir' => $baseDir, 'basePath' => '/w',
2881  '/w/test.jpg',
2882  '/w/test.jpg?edcf2'
2883  ],
2884  // File that matches basePath, but not found on disk. Empty query.
2885  [
2886  'baseDir' => $baseDir, 'basePath' => '/w',
2887  '/w/unknown.png',
2888  '/w/unknown.png?'
2889  ],
2890  // File not matching basePath. Ignored.
2891  [
2892  'baseDir' => $baseDir, 'basePath' => '/w',
2893  '/files/test.jpg'
2894  ],
2895  // Empty string. Ignored.
2896  [
2897  'baseDir' => $baseDir, 'basePath' => '/w',
2898  '',
2899  ''
2900  ],
2901  // Similar path, but with domain component. Ignored.
2902  [
2903  'baseDir' => $baseDir, 'basePath' => '/w',
2904  '//example.org/w/test.jpg'
2905  ],
2906  [
2907  'baseDir' => $baseDir, 'basePath' => '/w',
2908  'https://example.org/w/test.jpg'
2909  ],
2910  // Unrelated path with domain component. Ignored.
2911  [
2912  'baseDir' => $baseDir, 'basePath' => '/w',
2913  'https://example.org/files/test.jpg'
2914  ],
2915  [
2916  'baseDir' => $baseDir, 'basePath' => '/w',
2917  '//example.org/files/test.jpg'
2918  ],
2919  // Unrelated path with domain, and empty base path (root mw install). Ignored.
2920  [
2921  'baseDir' => $baseDir, 'basePath' => '',
2922  'https://example.org/files/test.jpg'
2923  ],
2924  [
2925  'baseDir' => $baseDir, 'basePath' => '',
2926  // T155310
2927  '//example.org/files/test.jpg'
2928  ],
2929  // Check UploadPath before ResourceBasePath (T155146)
2930  [
2931  'baseDir' => dirname( $baseDir ), 'basePath' => '',
2932  'uploadDir' => $baseDir, 'uploadPath' => '/images',
2933  '/images/test.jpg',
2934  '/images/test.jpg?edcf2'
2935  ],
2936  ];
2937  }
2938 
2953  protected function assertTransformCssMediaCase( $args ) {
2954  $queryData = [];
2955  if ( isset( $args['printableQuery'] ) ) {
2956  $queryData['printable'] = $args['printableQuery'];
2957  }
2958 
2959  if ( isset( $args['handheldQuery'] ) ) {
2960  $queryData['handheld'] = $args['handheldQuery'];
2961  }
2962 
2963  $fauxRequest = new FauxRequest( $queryData, false );
2964  $this->setMwGlobals( [
2965  'wgRequest' => $fauxRequest,
2966  ] );
2967 
2968  $actualReturn = OutputPage::transformCssMedia( $args['media'] );
2969  $this->assertSame( $args['expectedReturn'], $actualReturn, $args['message'] );
2970  }
2971 
2977  public function testPrintRequests() {
2978  $this->assertTransformCssMediaCase( [
2979  'printableQuery' => '1',
2980  'media' => 'screen',
2981  'expectedReturn' => null,
2982  'message' => 'On printable request, screen returns null'
2983  ] );
2984 
2985  $this->assertTransformCssMediaCase( [
2986  'printableQuery' => '1',
2987  'media' => self::SCREEN_MEDIA_QUERY,
2988  'expectedReturn' => null,
2989  'message' => 'On printable request, screen media query returns null'
2990  ] );
2991 
2992  $this->assertTransformCssMediaCase( [
2993  'printableQuery' => '1',
2994  'media' => self::SCREEN_ONLY_MEDIA_QUERY,
2995  'expectedReturn' => null,
2996  'message' => 'On printable request, screen media query with only returns null'
2997  ] );
2998 
2999  $this->assertTransformCssMediaCase( [
3000  'printableQuery' => '1',
3001  'media' => 'print',
3002  'expectedReturn' => '',
3003  'message' => 'On printable request, media print returns empty string'
3004  ] );
3005  }
3006 
3012  public function testScreenRequests() {
3013  $this->assertTransformCssMediaCase( [
3014  'media' => 'screen',
3015  'expectedReturn' => 'screen',
3016  'message' => 'On screen request, screen media type is preserved'
3017  ] );
3018 
3019  $this->assertTransformCssMediaCase( [
3020  'media' => 'handheld',
3021  'expectedReturn' => 'handheld',
3022  'message' => 'On screen request, handheld media type is preserved'
3023  ] );
3024 
3025  $this->assertTransformCssMediaCase( [
3026  'media' => self::SCREEN_MEDIA_QUERY,
3027  'expectedReturn' => self::SCREEN_MEDIA_QUERY,
3028  'message' => 'On screen request, screen media query is preserved.'
3029  ] );
3030 
3031  $this->assertTransformCssMediaCase( [
3032  'media' => self::SCREEN_ONLY_MEDIA_QUERY,
3033  'expectedReturn' => self::SCREEN_ONLY_MEDIA_QUERY,
3034  'message' => 'On screen request, screen media query with only is preserved.'
3035  ] );
3036 
3037  $this->assertTransformCssMediaCase( [
3038  'media' => 'print',
3039  'expectedReturn' => 'print',
3040  'message' => 'On screen request, print media type is preserved'
3041  ] );
3042  }
3043 
3049  public function testHandheld() {
3050  $this->assertTransformCssMediaCase( [
3051  'handheldQuery' => '1',
3052  'media' => 'handheld',
3053  'expectedReturn' => '',
3054  'message' => 'On request with handheld querystring and media is handheld, returns empty string'
3055  ] );
3056 
3057  $this->assertTransformCssMediaCase( [
3058  'handheldQuery' => '1',
3059  'media' => 'screen',
3060  'expectedReturn' => null,
3061  'message' => 'On request with handheld querystring and media is screen, returns null'
3062  ] );
3063  }
3064 
3070  public function testIsTOCEnabled() {
3071  $op = $this->newInstance();
3072  $this->assertFalse( $op->isTOCEnabled() );
3073 
3074  $pOut1 = $this->createParserOutputStub( 'getTOCHTML', false );
3075  $op->addParserOutputMetadata( $pOut1 );
3076  $this->assertFalse( $op->isTOCEnabled() );
3077 
3078  $pOut2 = $this->createParserOutputStub( 'getTOCHTML', true );
3079  $op->addParserOutput( $pOut2 );
3080  $this->assertTrue( $op->isTOCEnabled() );
3081 
3082  // The parser output doesn't disable the TOC after it was enabled
3083  $op->addParserOutputMetadata( $pOut1 );
3084  $this->assertTrue( $op->isTOCEnabled() );
3085  }
3086 
3092  public function testPreloadLinkHeaders( $config, $result ) {
3093  $this->setMwGlobals( $config );
3094  $ctx = $this->getMockBuilder( ResourceLoaderContext::class )
3095  ->disableOriginalConstructor()->getMock();
3096  $module = new ResourceLoaderSkinModule();
3097 
3098  $this->assertEquals( [ $result ], $module->getHeaders( $ctx ) );
3099  }
3100 
3101  public function providePreloadLinkHeaders() {
3102  return [
3103  [
3104  [
3105  'wgResourceBasePath' => '/w',
3106  'wgLogo' => '/img/default.png',
3107  'wgLogoHD' => [
3108  '1.5x' => '/img/one-point-five.png',
3109  '2x' => '/img/two-x.png',
3110  ],
3111  ],
3112  'Link: </img/default.png>;rel=preload;as=image;media=' .
3113  'not all and (min-resolution: 1.5dppx),' .
3114  '</img/one-point-five.png>;rel=preload;as=image;media=' .
3115  '(min-resolution: 1.5dppx) and (max-resolution: 1.999999dppx),' .
3116  '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
3117  ],
3118  [
3119  [
3120  'wgResourceBasePath' => '/w',
3121  'wgLogo' => '/img/default.png',
3122  'wgLogoHD' => false,
3123  ],
3124  'Link: </img/default.png>;rel=preload;as=image'
3125  ],
3126  [
3127  [
3128  'wgResourceBasePath' => '/w',
3129  'wgLogo' => '/img/default.png',
3130  'wgLogoHD' => [
3131  '2x' => '/img/two-x.png',
3132  ],
3133  ],
3134  'Link: </img/default.png>;rel=preload;as=image;media=' .
3135  'not all and (min-resolution: 2dppx),' .
3136  '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
3137  ],
3138  [
3139  [
3140  'wgResourceBasePath' => '/w',
3141  'wgLogo' => '/img/default.png',
3142  'wgLogoHD' => [
3143  'svg' => '/img/vector.svg',
3144  ],
3145  ],
3146  'Link: </img/vector.svg>;rel=preload;as=image'
3147 
3148  ],
3149  [
3150  [
3151  'wgResourceBasePath' => '/w',
3152  'wgLogo' => '/w/test.jpg',
3153  'wgLogoHD' => false,
3154  'wgUploadPath' => '/w/images',
3155  'IP' => dirname( __DIR__ ) . '/data/media',
3156  ],
3157  'Link: </w/test.jpg?edcf2>;rel=preload;as=image',
3158  ],
3159  ];
3160  }
3161 
3165  private function newInstance( $config = [], WebRequest $request = null, $options = [] ) {
3166  $context = new RequestContext();
3167 
3168  $context->setConfig( new MultiConfig( [
3169  new HashConfig( $config + [
3170  'AppleTouchIcon' => false,
3171  'DisableLangConversion' => true,
3172  'EnableCanonicalServerLink' => false,
3173  'Favicon' => false,
3174  'Feed' => false,
3175  'LanguageCode' => false,
3176  'ReferrerPolicy' => false,
3177  'RightsPage' => false,
3178  'RightsUrl' => false,
3179  'UniversalEditButton' => false,
3180  ] ),
3181  $context->getConfig()
3182  ] ) );
3183 
3184  if ( !in_array( 'notitle', (array)$options ) ) {
3185  $context->setTitle( Title::newFromText( 'My test page' ) );
3186  }
3187 
3188  if ( $request ) {
3189  $context->setRequest( $request );
3190  }
3191 
3192  return new OutputPage( $context );
3193  }
3194 }
testAddInlineScript()
OutputPage::addInlineScript.
testParseInline(array $args, $expectedHTML, $expectedHTMLInline=null)
provideParse OutputPage::parseInline
testProperties()
OutputPage::setProperty OutputPage::getProperty.
testRevisionTimestamp()
OutputPage::setRevisionTimestamp OutputPage::getRevisionTimestamp.
editPage( $pageName, $text, $summary='', $defaultNs=NS_MAIN, User $user=null)
Edits or creates a page/revision.
doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden)
static linkedScript( $url, $nonce=null)
Output a "<script>" tag linking to the given URL, e.g., "<script src=foo.js></script>".
Definition: Html.php:597
testAddWikiTextAsContentNoTitle()
OutputPage::addWikiTextAsContent.
assertFeedUILinks( $outputPage, $ui_links)
testAddScript()
OutputPage::addScript.
testSetTitle()
OutputPage::setTitle.
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:1982
testBodyHTML()
OutputPage::prependHTML OutputPage::addHTML OutputPage::addElement OutputPage::clearHTML OutputPage::...
setTemporaryHook( $hookName, $handler)
Create a temporary hook handler which will be reset by tearDown.
testFileVersion()
OutputPage::setFileVersion OutputPage::getFileVersion.
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1585
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:232
testAddWikiMsg()
OutputPage::addWikiMsg.
testAddWikiTextAsInterfaceNoTitle()
OutputPage::addWikiTextAsInterface.
testAddDeprecatedScriptFileWarning()
Test that addScriptFile() throws due to deprecation.
const NS_MAIN
Definition: Defines.php:60
static $parserOutputHookCalled
static provideFeedLinkData()
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
const SCREEN_ONLY_MEDIA_QUERY
testWrapWikiMsg()
OutputPage::wrapWikiMsg.
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 & $ret
Definition: hooks.txt:1982
extractExpectedCategories(array $expected, $key)
We allow different expectations for different tests as an associative array, like [ &#39;set&#39; => [ ...
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
testVaryHeaderDefault()
OutputPage::getVaryHeader.
testHeadItemsParserOutput()
OutputPage::getHeadItemsArray OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
$wgCookiePrefix
Cookies generated by MediaWiki have names starting with this prefix.
testBuildBacklinkSubtitle( $titles, $queries, $contains, $notContains)
provideBacklinkSubtitle
testAddDeprecatedScriptFileNoOp()
Test the actual behavior of the method (in the case where it doesn&#39;t throw, e.g., in production)...
testAddBacklinkSubtitle( $titles, $queries, $contains, $notContains)
provideBacklinkSubtitle
testCheckLastModified( $timestamp, $ifModifiedSince, $expected, $config=[], $callback=null)
provideCheckLastModified
globals will be eliminated from MediaWiki replaced by an application object which would be passed to constructors Whether that would be an convenient solution remains to be but certainly PHP makes such object oriented programming models easier than they were in previous versions For the time being MediaWiki programmers will have to work in an environment with some global context At the time of globals were initialised on startup by MediaWiki of these were configuration which are documented in DefaultSettings php There is no comprehensive documentation for the remaining however some of the most important ones are listed below They are typically initialised either in index php or in Setup php $wgTitle Title object created from the request URL $wgOut OutputPage object for HTTP response $wgUser User object for the user associated with the current request $wgLang Language object selected by user preferences $wgContLang Language object associated with the wiki being viewed $wgParser Parser object Parser extensions register their hooks here $wgRequest WebRequest object
Definition: globals.txt:25
$basePath
Definition: addSite.php:5
testArticleFlags()
OutputPage::setArticleFlag OutputPage::isArticle OutputPage::setArticleRelated OutputPage::isArticleR...
testMakeResourceLoaderLink( $args, $expectedHtml)
See ResourceLoaderClientHtmlTest for full coverage.
parserOutputHookCallback(OutputPage $op, ParserOutput $pOut, $data)
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 MediaWikiServices
Definition: injection.txt:23
testAddCategoryLinks(array $args, array $fakeResults, callable $variantLinkCallback=null, array $expectedNormal, array $expectedHidden)
provideGetCategories
testSetRobotPolicy()
OutputPage::setRobotPolicy OutputPage::getHeadLinksArray.
testFileSearchOptions()
OutputPage::getFileSearchOptions OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
testGetCategoriesInvalid()
OutputPage::getCategories.
static inlineScript( $contents, $nonce=null)
Output an HTML script tag with the given contents.
Definition: Html.php:573
testPrintRequests()
Tests print requests.
newInstance( $config=[], WebRequest $request=null, $options=[])
testParseNullTitle()
OutputPage::parse.
testVaryHeaders(array $calls, array $cookies, $vary, $key)
provideVaryHeaders
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of and they certainly aren t ideal for someone who s installing MediaWiki as MediaWiki does not conform to normal Unix filesystem layout Hopefully we ll offer direct support for standard layouts in the but for now *any change to the location of files is unsupported *Moving things and leaving symlinks will *probably *not break anything
testAddWikiTextNoTitle()
OutputPage::addWikiText.
extractHTMLTitle(OutputPage $op)
const SCREEN_MEDIA_QUERY
testArticleBodyOnly()
OutputPage::setArticleBodyOnly OutputPage::getArticleBodyOnly.
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1980
testParseAsContentNullTitle()
OutputPage::parseAsContent.
if( $line===false) $args
Definition: cdb.php:64
testRecentChangesFeed( $feed, $advertised_feed_types, $message, $present, $non_present)
provideFeedLinkData OutputPage::getHeadLinksArray
testHTMLTitle()
OutputPage::setHTMLTitle OutputPage::getHTMLTitle.
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition: hooks.txt:3051
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 it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:780
static provideMakeResourceLoaderLink()
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 true
Definition: hooks.txt:1982
testAddHelpLink()
OutputPage::addHelpLink OutputPage::getIndicators.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
testClickjacking()
OutputPage::preventClickjacking OutputPage::allowClickjacking OutputPage::getPreventClickjacking Outp...
static buildBacklinkSubtitle(Title $title, $query=[])
Build message object for a subtitle containing a backlink to a page.
testAdditionalFeeds( $feed, $advertised_feed_types, $message, $additional_feed_type, $present, $non_present, $any_ui_links)
provideAdditionalFeedData OutputPage::getHeadLinksArray OutputPage::addFeedLink OutputPage::getSyndic...
const NS_PROJECT
Definition: Defines.php:64
testSetCategoryLinks(array $args, array $fakeResults, callable $variantLinkCallback=null, array $expectedNormal, array $expectedHidden)
provideGetCategories
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
testPageTitle()
OutputPage::setPageTitle OutputPage::getPageTitle.
testNoGallery()
OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
headElement(Skin $sk, $includeStyle=true)
static provideAdditionalFeedData()
static transformResourcePath(Config $config, $path)
Transform path to web-accessible static resource.
testGetCacheVaryCookies()
OutputPage::getCacheVaryCookies.
testRevisionId( $newVal, $expected)
provideRevisionId OutputPage::setRevisionId OutputPage::getRevisionId
static provideBuildExemptModules()
const NS_MEDIA
Definition: Defines.php:48
testAddBodyClasses()
OutputPage::addBodyClasses.
testAddCategoryLinksOneByOne(array $args, array $fakeResults, callable $variantLinkCallback=null, array $expectedNormal, array $expectedHidden)
provideGetCategories
testSubtitle()
OutputPage::setSubtitle OutputPage::clearSubtitle OutputPage::addSubtitle OutputPage::getSubtitle.
testMetaTags()
OutputPage::addMeta OutputPage::getMetaTags OutputPage::getHeadLinksArray.
testSetCanonicalUrl()
OutputPage::setCanonicalUrl OutputPage::getCanonicalUrl OutputPage::getHeadLinksArray.
testAddAcceptLanguage( $code, array $variants, array $expected, array $options=[])
provideAddAcceptLanguage OutputPage::addAcceptLanguage OutputPage::getKeyHeader
getContext()
Get the base IContextSource object.
$params
const NS_CATEGORY
Definition: Defines.php:74
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:1982
testLanguageLinks()
OutputPage::addLanguageLinks OutputPage::setLanguageLinks OutputPage::getLanguageLinks OutputPage::ad...
assertFeedLinks( $outputPage, $message, $present, $non_present)
testBuildExemptModules(array $exemptStyleModules, $expect)
provideBuildExemptModules
testSetRedirectedFrom()
OutputPage::setRedirectedFrom.
testSetSyndicated()
OutputPage::setSyndicated OutputPage::isSyndicated.
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:780
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
static int $fakeTime
Faked time to set for tests that need it.
$wgCdnMaxAge
Cache TTL for the CDN sent as s-maxage (without ESI) or Surrogate-Control (with ESI).
testParseInlineNullTitle()
OutputPage::parseInline.
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:216
assertTransformCssMediaCase( $args)
Tests a particular case of transformCssMedia, using the given input, globals, expected return...
testShowNewSectionLink()
OutputPage::showNewSectionLink OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
static provideTransformFilePath()
$header
Provides a fallback sequence for Config objects.
Definition: MultiConfig.php:28
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown...
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 it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition: hooks.txt:780
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
hideDeprecated( $function)
Don&#39;t throw a warning if $function is deprecated and called later.
testFeedLinks()
OutputPage::isSyndicated OutputPage::setFeedAppendQuery OutputPage::addFeedLink OutputPage::getSyndic...
testAddScriptFile()
OutputPage::addScriptFile.
static getMsgText( $op,... $msgParams)
Shorthand for getting the text of a message, in content language.
testCdnMaxage()
OutputPage::setCdnMaxage OutputPage::lowerCdnMaxage.
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:617
testTemplateIds()
OutputPage::getTemplateIds OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:589
testParseAsInterface(array $args, $expectedHTML, $expectedHTMLInline=null)
provideParseAs OutputPage::parseAsInterface
testAddTemplate()
OutputPage::addTemplate.
testForceHideNewSectionLink()
OutputPage::forceHideNewSectionLink OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
testRedirect( $url, $code=null)
provideRedirect
testLinkHeaders(array $headers, $result)
provideLinkHeaders
static clearCache()
Reset static members used for caching.
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
this hook is for auditing only $req
Definition: hooks.txt:979
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 it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping $template
Definition: hooks.txt:780
testIsTOCEnabled()
OutputPage::isTOCEnabled OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
testHaveCacheVaryCookies()
OutputPage::haveCacheVaryCookies.
static transformCssMedia( $media)
Transform "media" attribute based on request parameters.
testParseInlineAsInterfaceNullTitle()
OutputPage::parseInlineAsInterface.
testAddWikiText( $method, array $args, $expected)
provideAddWikiText OutputPage::addWikiText OutputPage::addWikiTextAsInterface OutputPage::wrapWikiTex...
testPreloadLinkHeaders( $config, $result)
providePreloadLinkHeaders ResourceLoaderSkinModule::getPreloadLinks ResourceLoaderSkinModule::getLogo...
linkcache txt The LinkCache class maintains a list of article titles and the information about whether or not the article exists in the database This is used to mark up links when displaying a page If the same link appears more than once on any page then it only has to be looked up once In most cases link lookups are done in batches with the LinkBatch class or the equivalent in so the link cache is mostly useful for short snippets of parsed and for links in the navigation areas of the skin The link cache was formerly used to track links used in a document for the purposes of updating the link tables This application is now deprecated To create a you can use the following $titles
Definition: linkcache.txt:17
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
testHandheld()
Tests handheld behavior.
testAdaptCdnTTL(array $args, $expected, array $options=[])
provideAdaptCdnTTL OutputPage::adaptCdnTTL
setupFeedLinks( $feed, $types)
testPrintable()
OutputPage::setPrintable OutputPage::isPrintable.
testScreenRequests()
Tests screen requests, without either query parameter set.
testDisable()
OutputPage::disable OutputPage::isDisabled.
testParseInlineAsInterface(array $args, $expectedHTML, $expectedHTMLInline=null)
provideParseAs OutputPage::parseInlineAsInterface
testSetIndexFollowPolicies()
OutputPage::setIndexPolicy OutputPage::setFollowPolicy OutputPage::getHeadLinksArray.
doCategoryAsserts( $op, $expectedNormal, $expectedHidden)
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition: hooks.txt:2633
testSetCopyrightUrl()
OutputPage::setCopyrightUrl OutputPage::getHeadLinksArray.
testClientCache()
OutputPage::enableClientCache OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
testIndicators()
OutputPage::setIndicators OutputPage::getIndicators OutputPage::addParserOutputMetadata OutputPage::a...
testParserOutputCategoryLinks(array $args, array $fakeResults, callable $variantLinkCallback=null, array $expectedNormal, array $expectedHidden)
provideGetCategories
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
const NS_TALK
Definition: Defines.php:61
testParseAsInterfaceNullTitle()
OutputPage::parseAsInterface.
createParserOutputStub(... $args)
Call either with arguments $methodName, $returnValue; or an array [ $methodName => $returnValue...
testParse(array $args, $expectedHTML)
provideParse OutputPage::parse
testParseAsContent(array $args, $expectedHTML, $expectedHTMLInline=null)
provideParseAs OutputPage::parseAsContent
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a controlled by the following MediaWiki still creates a BagOStuff but calls it to it are no ops If the cache daemon can t be it should also disable itself fairly $wgDBname
Definition: memcached.txt:93
setupCategoryTests(array $fakeResults, callable $variantLinkCallback=null)
testAddParserOutput()
OutputPage::addParserOutput.
testAddLink()
OutputPage::addLink OutputPage::getLinkTags OutputPage::getHeadLinksArray.
$queries
testAddParserOutputText()
OutputPage::addParserOutputText.
A Config instance which stores all settings as a member variable.
Definition: HashConfig.php:28
testGetFrameOptions( $breakFrames, $preventClickjacking, $editPageFrameOptions, $expected)
provideGetFrameOptions OutputPage::getFrameOptions OutputPage::preventClickjacking ...
testSetTarget()
OutputPage::getTarget OutputPage::setTarget.
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2633
testTransformResourcePath( $baseDir, $basePath, $uploadDir=null, $uploadPath=null, $path=null, $expected=null)
provideTransformFilePath OutputPage::transformFilePath OutputPage::transformResourcePath ...
testCdnCacheEpoch( $params)
provideCdnCacheEpoch
static provideCdnCacheEpoch()
static parserOutputHookCallbackStatic(OutputPage $op, ParserOutput $pOut, $data)
testParserOutputHooks()
OutputPage::addParserOutputMetadata.
testHeadItems()
OutputPage::getHeadItemsArray OutputPage::addHeadItem OutputPage::addHeadItems OutputPage::hasHeadIte...
$matches
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:319