MediaWiki  master
OutputPageTest.php
Go to the documentation of this file.
1 <?php
2 
4 
12  const SCREEN_MEDIA_QUERY = 'screen and (min-width: 982px)';
13  const SCREEN_ONLY_MEDIA_QUERY = 'only screen and (min-width: 982px)';
14 
15  // @codingStandardsIgnoreStart Generic.Files.LineLength
16  const RSS_RC_LINK = '<link rel="alternate" type="application/rss+xml" title=" RSS feed" href="/w/index.php?title=Special:RecentChanges&amp;feed=rss"/>';
17  const ATOM_RC_LINK = '<link rel="alternate" type="application/atom+xml" title=" Atom feed" href="/w/index.php?title=Special:RecentChanges&amp;feed=atom"/>';
18 
19  const RSS_TEST_LINK = '<link rel="alternate" type="application/rss+xml" title="&quot;Test&quot; RSS feed" href="fake-link"/>';
20  const ATOM_TEST_LINK = '<link rel="alternate" type="application/atom+xml" title="&quot;Test&quot; Atom feed" href="fake-link"/>';
21  // @codingStandardsIgnoreEnd
22 
23  // Ensure that we don't affect the global ResourceLoader state.
24  protected function setUp() {
25  parent::setUp();
27  }
28  protected function tearDown() {
29  parent::tearDown();
31  }
32 
40  public function testRedirect( $url, $code = null ) {
41  $op = $this->newInstance();
42  if ( isset( $code ) ) {
43  $op->redirect( $url, $code );
44  } else {
45  $op->redirect( $url );
46  }
47  $expectedUrl = str_replace( "\n", '', $url );
48  $this->assertSame( $expectedUrl, $op->getRedirect() );
49  $this->assertSame( $expectedUrl, $op->mRedirect );
50  $this->assertSame( $code ?? '302', $op->mRedirectCode );
51  }
52 
53  public function provideRedirect() {
54  return [
55  [ 'http://example.com' ],
56  [ 'http://example.com', '400' ],
57  [ 'http://example.com', 'squirrels!!!' ],
58  [ "a\nb" ],
59  ];
60  }
61 
62  private function setupFeedLinks( $feed, $types ) {
63  $outputPage = $this->newInstance( [
64  'AdvertisedFeedTypes' => $types,
65  'Feed' => $feed,
66  'OverrideSiteFeed' => false,
67  'Script' => '/w',
68  'Sitename' => false,
69  ] );
70  $outputPage->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
71  $this->setMwGlobals( [
72  'wgScript' => '/w/index.php',
73  ] );
74  return $outputPage;
75  }
76 
77  private function assertFeedLinks( $outputPage, $message, $present, $non_present ) {
78  $links = $outputPage->getHeadLinksArray();
79  foreach ( $present as $link ) {
80  $this->assertContains( $link, $links, $message );
81  }
82  foreach ( $non_present as $link ) {
83  $this->assertNotContains( $link, $links, $message );
84  }
85  }
86 
87  private function assertFeedUILinks( $outputPage, $ui_links ) {
88  if ( $ui_links ) {
89  $this->assertTrue( $outputPage->isSyndicated(), 'Syndication should be offered' );
90  $this->assertGreaterThan( 0, count( $outputPage->getSyndicationLinks() ),
91  'Some syndication links should be there' );
92  } else {
93  $this->assertFalse( $outputPage->isSyndicated(), 'No syndication should be offered' );
94  $this->assertEquals( 0, count( $outputPage->getSyndicationLinks() ),
95  'No syndication links should be there' );
96  }
97  }
98 
99  public static function provideFeedLinkData() {
100  return [
101  [
102  true, [ 'rss' ], 'Only RSS RC link should be offerred',
103  [ self::RSS_RC_LINK ], [ self::ATOM_RC_LINK ]
104  ],
105  [
106  true, [ 'atom' ], 'Only Atom RC link should be offerred',
107  [ self::ATOM_RC_LINK ], [ self::RSS_RC_LINK ]
108  ],
109  [
110  true, [], 'No RC feed formats should be offerred',
111  [], [ self::ATOM_RC_LINK, self::RSS_RC_LINK ]
112  ],
113  [
114  false, [ 'atom' ], 'No RC feeds should be offerred',
115  [], [ self::ATOM_RC_LINK, self::RSS_RC_LINK ]
116  ],
117  ];
118  }
119 
124  public function testSetCopyrightUrl() {
125  $op = $this->newInstance();
126  $op->setCopyrightUrl( 'http://example.com' );
127 
128  $this->assertSame(
129  Html::element( 'link', [ 'rel' => 'license', 'href' => 'http://example.com' ] ),
130  $op->getHeadLinksArray()['copyright']
131  );
132  }
133 
138  public function testRecentChangesFeed( $feed, $advertised_feed_types,
139  $message, $present, $non_present ) {
140  $outputPage = $this->setupFeedLinks( $feed, $advertised_feed_types );
141  $this->assertFeedLinks( $outputPage, $message, $present, $non_present );
142  }
143 
144  public static function provideAdditionalFeedData() {
145  return [
146  [
147  true, [ 'atom' ], 'Additional Atom feed should be offered',
148  'atom',
149  [ self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
150  [ self::RSS_TEST_LINK, self::RSS_RC_LINK ],
151  true,
152  ],
153  [
154  true, [ 'rss' ], 'Additional RSS feed should be offered',
155  'rss',
156  [ self::RSS_TEST_LINK, self::RSS_RC_LINK ],
157  [ self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
158  true,
159  ],
160  [
161  true, [ 'rss' ], 'Additional Atom feed should NOT be offered with RSS enabled',
162  'atom',
163  [ self::RSS_RC_LINK ],
164  [ self::RSS_TEST_LINK, self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
165  false,
166  ],
167  [
168  false, [ 'atom' ], 'Additional Atom feed should NOT be offered, all feeds disabled',
169  'atom',
170  [],
171  [
172  self::RSS_TEST_LINK, self::ATOM_TEST_LINK,
173  self::ATOM_RC_LINK, self::ATOM_RC_LINK,
174  ],
175  false,
176  ],
177  ];
178  }
179 
187  public function testAdditionalFeeds( $feed, $advertised_feed_types, $message,
188  $additional_feed_type, $present, $non_present, $any_ui_links ) {
189  $outputPage = $this->setupFeedLinks( $feed, $advertised_feed_types );
190  $outputPage->addFeedLink( $additional_feed_type, 'fake-link' );
191  $this->assertFeedLinks( $outputPage, $message, $present, $non_present );
192  $this->assertFeedUILinks( $outputPage, $any_ui_links );
193  }
194 
195  // @todo How to test setStatusCode?
196 
202  public function testMetaTags() {
203  $op = $this->newInstance();
204  $op->addMeta( 'http:expires', '0' );
205  $op->addMeta( 'keywords', 'first' );
206  $op->addMeta( 'keywords', 'second' );
207  $op->addMeta( 'og:title', 'Ta-duh' );
208 
209  $expected = [
210  [ 'http:expires', '0' ],
211  [ 'keywords', 'first' ],
212  [ 'keywords', 'second' ],
213  [ 'og:title', 'Ta-duh' ],
214  ];
215  $this->assertSame( $expected, $op->getMetaTags() );
216 
217  $links = $op->getHeadLinksArray();
218  $this->assertContains( '<meta http-equiv="expires" content="0"/>', $links );
219  $this->assertContains( '<meta name="keywords" content="first"/>', $links );
220  $this->assertContains( '<meta name="keywords" content="second"/>', $links );
221  $this->assertContains( '<meta property="og:title" content="Ta-duh"/>', $links );
222  $this->assertArrayNotHasKey( 'meta-robots', $links );
223  }
224 
230  public function testAddLink() {
231  $op = $this->newInstance();
232 
233  $links = [
234  [],
235  [ 'rel' => 'foo', 'href' => 'http://example.com' ],
236  ];
237 
238  foreach ( $links as $link ) {
239  $op->addLink( $link );
240  }
241 
242  $this->assertSame( $links, $op->getLinkTags() );
243 
244  $result = $op->getHeadLinksArray();
245 
246  foreach ( $links as $link ) {
247  $this->assertContains( Html::element( 'link', $link ), $result );
248  }
249  }
250 
256  public function testSetCanonicalUrl() {
257  $op = $this->newInstance();
258  $op->setCanonicalUrl( 'http://example.comm' );
259  $op->setCanonicalUrl( 'http://example.com' );
260 
261  $this->assertSame( 'http://example.com', $op->getCanonicalUrl() );
262 
263  $headLinks = $op->getHeadLinksArray();
264 
265  $this->assertContains( Html::element( 'link', [
266  'rel' => 'canonical', 'href' => 'http://example.com'
267  ] ), $headLinks );
268 
269  $this->assertNotContains( Html::element( 'link', [
270  'rel' => 'canonical', 'href' => 'http://example.comm'
271  ] ), $headLinks );
272  }
273 
277  public function testAddScript() {
278  $op = $this->newInstance();
279  $op->addScript( 'some random string' );
280 
281  $this->assertContains( "\nsome random string\n", "\n" . $op->getBottomScripts() . "\n" );
282  }
283 
287  public function testAddScriptFile() {
288  $op = $this->newInstance();
289  $op->addScriptFile( '/somescript.js' );
290  $op->addScriptFile( '//example.com/somescript.js' );
291 
292  $this->assertContains(
293  "\n" . Html::linkedScript( '/somescript.js', $op->getCSPNonce() ) .
294  Html::linkedScript( '//example.com/somescript.js', $op->getCSPNonce() ) . "\n",
295  "\n" . $op->getBottomScripts() . "\n"
296  );
297  }
298 
305  $this->setExpectedException( PHPUnit_Framework_Error_Deprecated::class,
306  'Use of OutputPage::addScriptFile was deprecated in MediaWiki 1.24.' );
307 
308  $op = $this->newInstance();
309  $op->addScriptFile( 'ignored-script.js' );
310  }
311 
319  $this->hideDeprecated( 'OutputPage::addScriptFile' );
320  $op = $this->newInstance();
321  $op->addScriptFile( 'ignored-script.js' );
322 
323  $this->assertNotContains( 'ignored-script.js', '' . $op->getBottomScripts() );
324  }
325 
329  public function testAddInlineScript() {
330  $op = $this->newInstance();
331  $op->addInlineScript( 'let foo = "bar";' );
332  $op->addInlineScript( 'alert( foo );' );
333 
334  $this->assertContains(
335  "\n" . Html::inlineScript( "\nlet foo = \"bar\";\n", $op->getCSPNonce() ) . "\n" .
336  Html::inlineScript( "\nalert( foo );\n", $op->getCSPNonce() ) . "\n",
337  "\n" . $op->getBottomScripts() . "\n"
338  );
339  }
340 
341  // @todo How to test filterModules(), warnModuleTargetFilter(), getModules(), etc.?
342 
347  public function testSetTarget() {
348  $op = $this->newInstance();
349  $op->setTarget( 'foo' );
350 
351  $this->assertSame( 'foo', $op->getTarget() );
352  // @todo What else? Test some actual effect?
353  }
354 
355  // @todo How to test addContentOverride(Callback)?
356 
363  public function testHeadItems() {
364  $op = $this->newInstance();
365  $op->addHeadItem( 'a', 'b' );
366  $op->addHeadItems( [ 'c' => '<d>&amp;', 'e' => 'f', 'a' => 'q' ] );
367  $op->addHeadItem( 'e', 'g' );
368  $op->addHeadItems( 'x' );
369 
370  $this->assertSame( [ 'a' => 'q', 'c' => '<d>&amp;', 'e' => 'g', 'x' ],
371  $op->getHeadItemsArray() );
372 
373  $this->assertTrue( $op->hasHeadItem( 'a' ) );
374  $this->assertTrue( $op->hasHeadItem( 'c' ) );
375  $this->assertTrue( $op->hasHeadItem( 'e' ) );
376  $this->assertTrue( $op->hasHeadItem( '0' ) );
377 
378  $this->assertContains( "\nq\n<d>&amp;\ng\nx\n",
379  '' . $op->headElement( $op->getContext()->getSkin() ) );
380  }
381 
387  public function testHeadItemsParserOutput() {
388  $op = $this->newInstance();
389  $stubPO1 = $this->createParserOutputStub( 'getHeadItems', [ 'a' => 'b' ] );
390  $op->addParserOutputMetadata( $stubPO1 );
391  $stubPO2 = $this->createParserOutputStub( 'getHeadItems',
392  [ 'c' => '<d>&amp;', 'e' => 'f', 'a' => 'q' ] );
393  $op->addParserOutputMetadata( $stubPO2 );
394  $stubPO3 = $this->createParserOutputStub( 'getHeadItems', [ 'e' => 'g' ] );
395  $op->addParserOutput( $stubPO3 );
396  $stubPO4 = $this->createParserOutputStub( 'getHeadItems', [ 'x' ] );
397  $op->addParserOutputMetadata( $stubPO4 );
398 
399  $this->assertSame( [ 'a' => 'q', 'c' => '<d>&amp;', 'e' => 'g', 'x' ],
400  $op->getHeadItemsArray() );
401 
402  $this->assertTrue( $op->hasHeadItem( 'a' ) );
403  $this->assertTrue( $op->hasHeadItem( 'c' ) );
404  $this->assertTrue( $op->hasHeadItem( 'e' ) );
405  $this->assertTrue( $op->hasHeadItem( '0' ) );
406  $this->assertFalse( $op->hasHeadItem( 'b' ) );
407 
408  $this->assertContains( "\nq\n<d>&amp;\ng\nx\n",
409  '' . $op->headElement( $op->getContext()->getSkin() ) );
410  }
411 
415  public function testAddBodyClasses() {
416  $op = $this->newInstance();
417  $op->addBodyClasses( 'a' );
418  $op->addBodyClasses( 'mediawiki' );
419  $op->addBodyClasses( 'b c' );
420  $op->addBodyClasses( [ 'd', 'e' ] );
421  $op->addBodyClasses( 'a' );
422 
423  $this->assertContains( '"a mediawiki b c d e ltr',
424  '' . $op->headElement( $op->getContext()->getSkin() ) );
425  }
426 
431  public function testArticleBodyOnly() {
432  $op = $this->newInstance();
433  $this->assertFalse( $op->getArticleBodyOnly() );
434 
435  $op->setArticleBodyOnly( true );
436  $this->assertTrue( $op->getArticleBodyOnly() );
437 
438  $op->addHTML( '<b>a</b>' );
439 
440  $this->assertSame( '<b>a</b>', $op->output( true ) );
441  }
442 
447  public function testProperties() {
448  $op = $this->newInstance();
449 
450  $this->assertNull( $op->getProperty( 'foo' ) );
451 
452  $op->setProperty( 'foo', 'bar' );
453  $op->setProperty( 'baz', 'quz' );
454 
455  $this->assertSame( 'bar', $op->getProperty( 'foo' ) );
456  $this->assertSame( 'quz', $op->getProperty( 'baz' ) );
457  }
458 
465  public function testCheckLastModified(
466  $timestamp, $ifModifiedSince, $expected, $config = [], $callback = null
467  ) {
468  $request = new FauxRequest();
469  if ( $ifModifiedSince ) {
470  if ( is_numeric( $ifModifiedSince ) ) {
471  // Unix timestamp
472  $ifModifiedSince = date( 'D, d M Y H:i:s', $ifModifiedSince ) . ' GMT';
473  }
474  $request->setHeader( 'If-Modified-Since', $ifModifiedSince );
475  }
476 
477  if ( !isset( $config['CacheEpoch'] ) ) {
478  // Make sure it's not too recent
479  $config['CacheEpoch'] = '20000101000000';
480  }
481 
482  $op = $this->newInstance( $config, $request );
483 
484  if ( $callback ) {
485  $callback( $op, $this );
486  }
487 
488  // Avoid a complaint about not being able to disable compression
489  Wikimedia\suppressWarnings();
490  try {
491  $this->assertEquals( $expected, $op->checkLastModified( $timestamp ) );
492  } finally {
493  Wikimedia\restoreWarnings();
494  }
495  }
496 
497  public function provideCheckLastModified() {
498  $lastModified = time() - 3600;
499  return [
500  'Timestamp 0' =>
501  [ '0', $lastModified, false ],
502  'Timestamp Unix epoch' =>
503  [ '19700101000000', $lastModified, false ],
504  'Timestamp same as If-Modified-Since' =>
505  [ $lastModified, $lastModified, true ],
506  'Timestamp one second after If-Modified-Since' =>
507  [ $lastModified + 1, $lastModified, false ],
508  'No If-Modified-Since' =>
509  [ $lastModified + 1, null, false ],
510  'Malformed If-Modified-Since' =>
511  [ $lastModified + 1, 'GIBBERING WOMBATS !!!', false ],
512  'Non-standard IE-style If-Modified-Since' =>
513  [ $lastModified, date( 'D, d M Y H:i:s', $lastModified ) . ' GMT; length=5202',
514  true ],
515  // @todo Should we fix this behavior to match the spec? Probably no reason to.
516  'If-Modified-Since not per spec but we accept it anyway because strtotime does' =>
517  [ $lastModified, "@$lastModified", true ],
518  '$wgCachePages = false' =>
519  [ $lastModified, $lastModified, false, [ 'CachePages' => false ] ],
520  '$wgCacheEpoch' =>
521  [ $lastModified, $lastModified, false,
522  [ 'CacheEpoch' => wfTimestamp( TS_MW, $lastModified + 1 ) ] ],
523  'Recently-touched user' =>
524  [ $lastModified, $lastModified, false, [],
525  function ( $op ) {
526  $op->getContext()->setUser( $this->getTestUser()->getUser() );
527  } ],
528  'After Squid expiry' =>
529  [ $lastModified, $lastModified, false,
530  [ 'UseSquid' => true, 'SquidMaxage' => 3599 ] ],
531  'Hook allows cache use' =>
532  [ $lastModified + 1, $lastModified, true, [],
533  function ( $op, $that ) {
534  $that->setTemporaryHook( 'OutputPageCheckLastModified',
535  function ( &$modifiedTimes ) {
536  $modifiedTimes = [ 1 ];
537  }
538  );
539  } ],
540  'Hooks prohibits cache use' =>
541  [ $lastModified, $lastModified, false, [],
542  function ( $op, $that ) {
543  $that->setTemporaryHook( 'OutputPageCheckLastModified',
544  function ( &$modifiedTimes ) {
545  $modifiedTimes = [ max( $modifiedTimes ) + 1 ];
546  }
547  );
548  } ],
549  ];
550  }
551 
557  public function testCdnCacheEpoch( $params ) {
558  $out = TestingAccessWrapper::newFromObject( $this->newInstance() );
559  $reqTime = strtotime( $params['reqTime'] );
560  $pageTime = strtotime( $params['pageTime'] );
561  $actual = max( $pageTime, $out->getCdnCacheEpoch( $reqTime, $params['maxAge'] ) );
562 
563  $this->assertEquals(
564  $params['expect'],
565  gmdate( DateTime::ATOM, $actual ),
566  'cdn epoch'
567  );
568  }
569 
570  public static function provideCdnCacheEpoch() {
571  $base = [
572  'pageTime' => '2011-04-01T12:00:00+00:00',
573  'maxAge' => 24 * 3600,
574  ];
575  return [
576  'after 1s' => [ $base + [
577  'reqTime' => '2011-04-01T12:00:01+00:00',
578  'expect' => '2011-04-01T12:00:00+00:00',
579  ] ],
580  'after 23h' => [ $base + [
581  'reqTime' => '2011-04-02T11:00:00+00:00',
582  'expect' => '2011-04-01T12:00:00+00:00',
583  ] ],
584  'after 24h and a bit' => [ $base + [
585  'reqTime' => '2011-04-02T12:34:56+00:00',
586  'expect' => '2011-04-01T12:34:56+00:00',
587  ] ],
588  'after a year' => [ $base + [
589  'reqTime' => '2012-05-06T00:12:07+00:00',
590  'expect' => '2012-05-05T00:12:07+00:00',
591  ] ],
592  ];
593  }
594 
595  // @todo How to test setLastModified?
596 
601  public function testSetRobotPolicy() {
602  $op = $this->newInstance();
603  $op->setRobotPolicy( 'noindex, nofollow' );
604 
605  $links = $op->getHeadLinksArray();
606  $this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
607  }
608 
614  public function testSetIndexFollowPolicies() {
615  $op = $this->newInstance();
616  $op->setIndexPolicy( 'noindex' );
617  $op->setFollowPolicy( 'nofollow' );
618 
619  $links = $op->getHeadLinksArray();
620  $this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
621  }
622 
623  private function extractHTMLTitle( OutputPage $op ) {
624  $html = $op->headElement( $op->getContext()->getSkin() );
625 
626  // OutputPage should always output the title in a nice format such that regexes will work
627  // fine. If it doesn't, we'll fail the tests.
628  preg_match_all( '!<title>(.*?)</title>!', $html, $matches );
629 
630  $this->assertLessThanOrEqual( 1, count( $matches[1] ), 'More than one <title>!' );
631 
632  if ( !count( $matches[1] ) ) {
633  return null;
634  }
635 
636  return $matches[1][0];
637  }
638 
642  private static function getMsgText( $op, ...$msgParams ) {
643  return $op->msg( ...$msgParams )->inContentLanguage()->text();
644  }
645 
650  public function testHTMLTitle() {
651  $op = $this->newInstance();
652 
653  // Default
654  $this->assertSame( '', $op->getHTMLTitle() );
655  $this->assertSame( '', $op->getPageTitle() );
656  $this->assertSame(
657  $this->getMsgText( $op, 'pagetitle', '' ),
658  $this->extractHTMLTitle( $op )
659  );
660 
661  // Set to string
662  $op->setHTMLTitle( 'Potatoes will eat me' );
663 
664  $this->assertSame( 'Potatoes will eat me', $op->getHTMLTitle() );
665  $this->assertSame( 'Potatoes will eat me', $this->extractHTMLTitle( $op ) );
666  // Shouldn't have changed the page title
667  $this->assertSame( '', $op->getPageTitle() );
668 
669  // Set to message
670  $msg = $op->msg( 'mainpage' );
671 
672  $op->setHTMLTitle( $msg );
673  $this->assertSame( $msg->text(), $op->getHTMLTitle() );
674  $this->assertSame( $msg->text(), $this->extractHTMLTitle( $op ) );
675  $this->assertSame( '', $op->getPageTitle() );
676  }
677 
681  public function testSetRedirectedFrom() {
682  $op = $this->newInstance();
683 
684  $op->setRedirectedFrom( Title::newFromText( 'Talk:Some page' ) );
685  $this->assertSame( 'Talk:Some_page', $op->getJSVars()['wgRedirectedFrom'] );
686  }
687 
692  public function testPageTitle() {
693  // We don't test the actual HTML output anywhere, because that's up to the skin.
694  $op = $this->newInstance();
695 
696  // Test default
697  $this->assertSame( '', $op->getPageTitle() );
698  $this->assertSame( '', $op->getHTMLTitle() );
699 
700  // Test set to plain text
701  $op->setPageTitle( 'foobar' );
702 
703  $this->assertSame( 'foobar', $op->getPageTitle() );
704  // HTML title should change as well
705  $this->assertSame( $this->getMsgText( $op, 'pagetitle', 'foobar' ), $op->getHTMLTitle() );
706 
707  // Test set to text with good and bad HTML. We don't try to be comprehensive here, that
708  // belongs in Sanitizer tests.
709  $op->setPageTitle( '<script>a</script>&amp;<i>b</i>' );
710 
711  $this->assertSame( '&lt;script&gt;a&lt;/script&gt;&amp;<i>b</i>', $op->getPageTitle() );
712  $this->assertSame(
713  $this->getMsgText( $op, 'pagetitle', '<script>a</script>&b' ),
714  $op->getHTMLTitle()
715  );
716 
717  // Test set to message
718  $text = $this->getMsgText( $op, 'mainpage' );
719 
720  $op->setPageTitle( $op->msg( 'mainpage' )->inContentLanguage() );
721  $this->assertSame( $text, $op->getPageTitle() );
722  $this->assertSame( $this->getMsgText( $op, 'pagetitle', $text ), $op->getHTMLTitle() );
723  }
724 
728  public function testSetTitle() {
729  $op = $this->newInstance();
730 
731  $this->assertSame( 'My test page', $op->getTitle()->getPrefixedText() );
732 
733  $op->setTitle( Title::newFromText( 'Another test page' ) );
734 
735  $this->assertSame( 'Another test page', $op->getTitle()->getPrefixedText() );
736  }
737 
744  public function testSubtitle() {
745  $op = $this->newInstance();
746 
747  $this->assertSame( '', $op->getSubtitle() );
748 
749  $op->addSubtitle( '<b>foo</b>' );
750 
751  $this->assertSame( '<b>foo</b>', $op->getSubtitle() );
752 
753  $op->addSubtitle( $op->msg( 'mainpage' )->inContentLanguage() );
754 
755  $this->assertSame(
756  "<b>foo</b><br />\n\t\t\t\t" . $this->getMsgText( $op, 'mainpage' ),
757  $op->getSubtitle()
758  );
759 
760  $op->setSubtitle( 'There can be only one' );
761 
762  $this->assertSame( 'There can be only one', $op->getSubtitle() );
763 
764  $op->clearSubtitle();
765 
766  $this->assertSame( '', $op->getSubtitle() );
767  }
768 
774  public function testBuildBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
775  if ( count( $titles ) > 1 ) {
776  // Not applicable
777  $this->assertTrue( true );
778  return;
779  }
780 
782  $query = $queries[0];
783 
784  $this->editPage( 'Page 1', '' );
785  $this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
786 
788 
789  foreach ( $contains as $substr ) {
790  $this->assertContains( $substr, $str );
791  }
792 
793  foreach ( $notContains as $substr ) {
794  $this->assertNotContains( $substr, $str );
795  }
796  }
797 
804  public function testAddBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
805  $this->editPage( 'Page 1', '' );
806  $this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
807 
808  $op = $this->newInstance();
809  foreach ( $titles as $i => $unused ) {
810  $op->addBacklinkSubtitle( Title::newFromText( $titles[$i] ), $queries[$i] );
811  }
812 
813  $str = $op->getSubtitle();
814 
815  foreach ( $contains as $substr ) {
816  $this->assertContains( $substr, $str );
817  }
818 
819  foreach ( $notContains as $substr ) {
820  $this->assertNotContains( $substr, $str );
821  }
822  }
823 
824  public function provideBacklinkSubtitle() {
825  return [
826  [
827  [ 'Page 1' ],
828  [ [] ],
829  [ 'Page 1' ],
830  [ 'redirect', 'Page 2' ],
831  ],
832  [
833  [ 'Page 2' ],
834  [ [] ],
835  [ 'redirect=no' ],
836  [ 'Page 1' ],
837  ],
838  [
839  [ 'Page 1' ],
840  [ [ 'action' => 'edit' ] ],
841  [ 'action=edit' ],
842  [],
843  ],
844  [
845  [ 'Page 1', 'Page 2' ],
846  [ [], [] ],
847  [ 'Page 1', 'Page 2', "<br />\n\t\t\t\t" ],
848  [],
849  ],
850  // @todo Anything else to test?
851  ];
852  }
853 
858  public function testPrintable() {
859  $op = $this->newInstance();
860 
861  $this->assertFalse( $op->isPrintable() );
862 
863  $op->setPrintable();
864 
865  $this->assertTrue( $op->isPrintable() );
866  }
867 
872  public function testDisable() {
873  $op = $this->newInstance();
874 
875  $this->assertFalse( $op->isDisabled() );
876  $this->assertNotSame( '', $op->output( true ) );
877 
878  $op->disable();
879 
880  $this->assertTrue( $op->isDisabled() );
881  $this->assertSame( '', $op->output( true ) );
882  }
883 
889  public function testShowNewSectionLink() {
890  $op = $this->newInstance();
891 
892  $this->assertFalse( $op->showNewSectionLink() );
893 
894  $pOut1 = $this->createParserOutputStub( 'getNewSection', true );
895  $op->addParserOutputMetadata( $pOut1 );
896  $this->assertTrue( $op->showNewSectionLink() );
897 
898  $pOut2 = $this->createParserOutputStub( 'getNewSection', false );
899  $op->addParserOutput( $pOut2 );
900  $this->assertFalse( $op->showNewSectionLink() );
901  }
902 
908  public function testForceHideNewSectionLink() {
909  $op = $this->newInstance();
910 
911  $this->assertFalse( $op->forceHideNewSectionLink() );
912 
913  $pOut1 = $this->createParserOutputStub( 'getHideNewSection', true );
914  $op->addParserOutputMetadata( $pOut1 );
915  $this->assertTrue( $op->forceHideNewSectionLink() );
916 
917  $pOut2 = $this->createParserOutputStub( 'getHideNewSection', false );
918  $op->addParserOutput( $pOut2 );
919  $this->assertFalse( $op->forceHideNewSectionLink() );
920  }
921 
926  public function testSetSyndicated() {
927  $op = $this->newInstance( [ 'Feed' => true ] );
928  $this->assertFalse( $op->isSyndicated() );
929 
930  $op->setSyndicated();
931  $this->assertTrue( $op->isSyndicated() );
932 
933  $op->setSyndicated( false );
934  $this->assertFalse( $op->isSyndicated() );
935 
936  $op = $this->newInstance(); // Feed => false by default
937  $this->assertFalse( $op->isSyndicated() );
938 
939  $op->setSyndicated();
940  $this->assertFalse( $op->isSyndicated() );
941  }
942 
949  public function testFeedLinks() {
950  $op = $this->newInstance( [ 'Feed' => true ] );
951  $this->assertSame( [], $op->getSyndicationLinks() );
952 
953  $op->addFeedLink( 'not a supported format', 'abc' );
954  $this->assertFalse( $op->isSyndicated() );
955  $this->assertSame( [], $op->getSyndicationLinks() );
956 
957  $feedTypes = $op->getConfig()->get( 'AdvertisedFeedTypes' );
958 
959  $op->addFeedLink( $feedTypes[0], 'def' );
960  $this->assertTrue( $op->isSyndicated() );
961  $this->assertSame( [ $feedTypes[0] => 'def' ], $op->getSyndicationLinks() );
962 
963  $op->setFeedAppendQuery( false );
964  $expected = [];
965  foreach ( $feedTypes as $type ) {
966  $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type" );
967  }
968  $this->assertSame( $expected, $op->getSyndicationLinks() );
969 
970  $op->setFeedAppendQuery( 'apples=oranges' );
971  foreach ( $feedTypes as $type ) {
972  $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type&apples=oranges" );
973  }
974  $this->assertSame( $expected, $op->getSyndicationLinks() );
975 
976  $op = $this->newInstance(); // Feed => false by default
977  $this->assertSame( [], $op->getSyndicationLinks() );
978 
979  $op->addFeedLink( $feedTypes[0], 'def' );
980  $this->assertFalse( $op->isSyndicated() );
981  $this->assertSame( [], $op->getSyndicationLinks() );
982  }
983 
990  function testArticleFlags() {
991  $op = $this->newInstance();
992  $this->assertFalse( $op->isArticle() );
993  $this->assertTrue( $op->isArticleRelated() );
994 
995  $op->setArticleRelated( false );
996  $this->assertFalse( $op->isArticle() );
997  $this->assertFalse( $op->isArticleRelated() );
998 
999  $op->setArticleFlag( true );
1000  $this->assertTrue( $op->isArticle() );
1001  $this->assertTrue( $op->isArticleRelated() );
1002 
1003  $op->setArticleFlag( false );
1004  $this->assertFalse( $op->isArticle() );
1005  $this->assertTrue( $op->isArticleRelated() );
1006 
1007  $op->setArticleFlag( true );
1008  $op->setArticleRelated( false );
1009  $this->assertFalse( $op->isArticle() );
1010  $this->assertFalse( $op->isArticleRelated() );
1011  }
1012 
1020  function testLanguageLinks() {
1021  $op = $this->newInstance();
1022  $this->assertSame( [], $op->getLanguageLinks() );
1023 
1024  $op->addLanguageLinks( [ 'fr:A', 'it:B' ] );
1025  $this->assertSame( [ 'fr:A', 'it:B' ], $op->getLanguageLinks() );
1026 
1027  $op->addLanguageLinks( [ 'de:C', 'es:D' ] );
1028  $this->assertSame( [ 'fr:A', 'it:B', 'de:C', 'es:D' ], $op->getLanguageLinks() );
1029 
1030  $op->setLanguageLinks( [ 'pt:E' ] );
1031  $this->assertSame( [ 'pt:E' ], $op->getLanguageLinks() );
1032 
1033  $pOut1 = $this->createParserOutputStub( 'getLanguageLinks', [ 'he:F', 'ar:G' ] );
1034  $op->addParserOutputMetadata( $pOut1 );
1035  $this->assertSame( [ 'pt:E', 'he:F', 'ar:G' ], $op->getLanguageLinks() );
1036 
1037  $pOut2 = $this->createParserOutputStub( 'getLanguageLinks', [ 'pt:H' ] );
1038  $op->addParserOutput( $pOut2 );
1039  $this->assertSame( [ 'pt:E', 'he:F', 'ar:G', 'pt:H' ], $op->getLanguageLinks() );
1040  }
1041 
1042  // @todo Are these category links tests too abstract and complicated for what they test? Would
1043  // it make sense to just write out all the tests by hand with maybe some copy-and-paste?
1044 
1059  public function testAddCategoryLinks(
1060  array $args, array $fakeResults, callable $variantLinkCallback = null,
1061  array $expectedNormal, array $expectedHidden
1062  ) {
1063  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'add' );
1064  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'add' );
1065 
1066  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1067 
1068  $op->addCategoryLinks( $args );
1069 
1070  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
1071  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1072  }
1073 
1082  array $args, array $fakeResults, callable $variantLinkCallback = null,
1083  array $expectedNormal, array $expectedHidden
1084  ) {
1085  if ( count( $args ) <= 1 ) {
1086  // @todo Should this be skipped instead of passed?
1087  $this->assertTrue( true );
1088  return;
1089  }
1090 
1091  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'onebyone' );
1092  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'onebyone' );
1093 
1094  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1095 
1096  foreach ( $args as $key => $val ) {
1097  $op->addCategoryLinks( [ $key => $val ] );
1098  }
1099 
1100  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
1101  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1102  }
1103 
1111  public function testSetCategoryLinks(
1112  array $args, array $fakeResults, callable $variantLinkCallback = null,
1113  array $expectedNormal, array $expectedHidden
1114  ) {
1115  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'set' );
1116  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'set' );
1117 
1118  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1119 
1120  $op->setCategoryLinks( [ 'Initial page' => 'Initial page' ] );
1121  $op->setCategoryLinks( $args );
1122 
1123  // We don't reset the categories, for some reason, only the links
1124  $expectedNormalCats = array_merge( [ 'Initial page' ], $expectedNormal );
1125  $expectedCats = array_merge( $expectedHidden, $expectedNormalCats );
1126 
1127  $this->doCategoryAsserts( $op, $expectedNormalCats, $expectedHidden );
1128  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1129  }
1130 
1140  array $args, array $fakeResults, callable $variantLinkCallback = null,
1141  array $expectedNormal, array $expectedHidden
1142  ) {
1143  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'pout' );
1144  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'pout' );
1145 
1146  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1147 
1148  $stubPO = $this->createParserOutputStub( 'getCategories', $args );
1149 
1150  // addParserOutput and addParserOutputMetadata should behave identically for us, so
1151  // alternate to get coverage for both without adding extra tests
1152  static $idx = 0;
1153  $idx++;
1154  $method = [ 'addParserOutputMetadata', 'addParserOutput' ][$idx % 2];
1155  $op->$method( $stubPO );
1156 
1157  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
1158  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1159  }
1160 
1166  private function extractExpectedCategories( array $expected, $key ) {
1167  if ( !$expected || isset( $expected[0] ) ) {
1168  return $expected;
1169  }
1170  return $expected[$key] ?? $expected['default'];
1171  }
1172 
1173  private function setupCategoryTests(
1174  array $fakeResults, callable $variantLinkCallback = null
1175  ) : OutputPage {
1176  $this->setMwGlobals( 'wgUsePigLatinVariant', true );
1177 
1178  $op = $this->getMockBuilder( OutputPage::class )
1179  ->setConstructorArgs( [ new RequestContext() ] )
1180  ->setMethods( [ 'addCategoryLinksToLBAndGetResult', 'getTitle' ] )
1181  ->getMock();
1182 
1183  $title = Title::newFromText( 'My test page' );
1184  $op->expects( $this->any() )
1185  ->method( 'getTitle' )
1186  ->will( $this->returnValue( $title ) );
1187 
1188  $op->expects( $this->any() )
1189  ->method( 'addCategoryLinksToLBAndGetResult' )
1190  ->will( $this->returnCallback( function ( array $categories ) use ( $fakeResults ) {
1191  $return = [];
1192  foreach ( $categories as $category => $unused ) {
1193  if ( isset( $fakeResults[$category] ) ) {
1194  $return[] = $fakeResults[$category];
1195  }
1196  }
1197  return new FakeResultWrapper( $return );
1198  } ) );
1199 
1200  if ( $variantLinkCallback ) {
1201  $mockContLang = $this->getMockBuilder( Language::class )
1202  ->setConstructorArgs( [ 'en' ] )
1203  ->setMethods( [ 'findVariantLink' ] )
1204  ->getMock();
1205  $mockContLang->expects( $this->any() )
1206  ->method( 'findVariantLink' )
1207  ->will( $this->returnCallback( $variantLinkCallback ) );
1208  $this->setContentLang( $mockContLang );
1209  }
1210 
1211  $this->assertSame( [], $op->getCategories() );
1212 
1213  return $op;
1214  }
1215 
1216  private function doCategoryAsserts( $op, $expectedNormal, $expectedHidden ) {
1217  $this->assertSame( array_merge( $expectedHidden, $expectedNormal ), $op->getCategories() );
1218  $this->assertSame( $expectedNormal, $op->getCategories( 'normal' ) );
1219  $this->assertSame( $expectedHidden, $op->getCategories( 'hidden' ) );
1220  }
1221 
1222  private function doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden ) {
1223  $catLinks = $op->getCategoryLinks();
1224  $this->assertSame( (bool)$expectedNormal + (bool)$expectedHidden, count( $catLinks ) );
1225  if ( $expectedNormal ) {
1226  $this->assertSame( count( $expectedNormal ), count( $catLinks['normal'] ) );
1227  }
1228  if ( $expectedHidden ) {
1229  $this->assertSame( count( $expectedHidden ), count( $catLinks['hidden'] ) );
1230  }
1231 
1232  foreach ( $expectedNormal as $i => $name ) {
1233  $this->assertContains( $name, $catLinks['normal'][$i] );
1234  }
1235  foreach ( $expectedHidden as $i => $name ) {
1236  $this->assertContains( $name, $catLinks['hidden'][$i] );
1237  }
1238  }
1239 
1240  public function provideGetCategories() {
1241  return [
1242  'No categories' => [ [], [], null, [], [] ],
1243  'Simple test' => [
1244  [ 'Test1' => 'Some sortkey', 'Test2' => 'A different sortkey' ],
1245  [ 'Test1' => (object)[ 'pp_value' => 1, 'page_title' => 'Test1' ],
1246  'Test2' => (object)[ 'page_title' => 'Test2' ] ],
1247  null,
1248  [ 'Test2' ],
1249  [ 'Test1' ],
1250  ],
1251  'Invalid title' => [
1252  [ '[' => '[', 'Test' => 'Test' ],
1253  [ 'Test' => (object)[ 'page_title' => 'Test' ] ],
1254  null,
1255  [ 'Test' ],
1256  [],
1257  ],
1258  'Variant link' => [
1259  [ 'Test' => 'Test', 'Estay' => 'Estay' ],
1260  [ 'Test' => (object)[ 'page_title' => 'Test' ] ],
1261  function ( &$link, &$title ) {
1262  if ( $link === 'Estay' ) {
1263  $link = 'Test';
1265  }
1266  },
1267  // For adding one by one, the variant gets added as well as the original category,
1268  // but if you add them all together the second time gets skipped.
1269  [ 'onebyone' => [ 'Test', 'Test' ], 'default' => [ 'Test' ] ],
1270  [],
1271  ],
1272  ];
1273  }
1274 
1278  public function testGetCategoriesInvalid() {
1279  $this->setExpectedException( InvalidArgumentException::class,
1280  'Invalid category type given: hiddne' );
1281 
1282  $op = $this->newInstance();
1283  $op->getCategories( 'hiddne' );
1284  }
1285 
1286  // @todo Should we test addCategoryLinksToLBAndGetResult? If so, how? Insert some test rows in
1287  // the DB?
1288 
1295  public function testIndicators() {
1296  $op = $this->newInstance();
1297  $this->assertSame( [], $op->getIndicators() );
1298 
1299  $op->setIndicators( [] );
1300  $this->assertSame( [], $op->getIndicators() );
1301 
1302  // Test sorting alphabetically
1303  $op->setIndicators( [ 'b' => 'x', 'a' => 'y' ] );
1304  $this->assertSame( [ 'a' => 'y', 'b' => 'x' ], $op->getIndicators() );
1305 
1306  // Test overwriting existing keys
1307  $op->setIndicators( [ 'c' => 'z', 'a' => 'w' ] );
1308  $this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'z' ], $op->getIndicators() );
1309 
1310  // Test with addParserOutputMetadata
1311  $pOut1 = $this->createParserOutputStub( 'getIndicators', [ 'c' => 'u', 'd' => 'v' ] );
1312  $op->addParserOutputMetadata( $pOut1 );
1313  $this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
1314  $op->getIndicators() );
1315 
1316  // Test with addParserOutput
1317  $pOut2 = $this->createParserOutputStub( 'getIndicators', [ 'a' => '!!!' ] );
1318  $op->addParserOutput( $pOut2 );
1319  $this->assertSame( [ 'a' => '!!!', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
1320  $op->getIndicators() );
1321  }
1322 
1327  public function testAddHelpLink() {
1328  $op = $this->newInstance();
1329 
1330  $op->addHelpLink( 'Manual:PHP unit testing' );
1331  $indicators = $op->getIndicators();
1332  $this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
1333  $this->assertContains( 'Manual:PHP_unit_testing', $indicators['mw-helplink'] );
1334 
1335  $op->addHelpLink( 'https://phpunit.de', true );
1336  $indicators = $op->getIndicators();
1337  $this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
1338  $this->assertContains( 'https://phpunit.de', $indicators['mw-helplink'] );
1339  $this->assertNotContains( 'mediawiki', $indicators['mw-helplink'] );
1340  $this->assertNotContains( 'Manual:PHP', $indicators['mw-helplink'] );
1341  }
1342 
1350  public function testBodyHTML() {
1351  $op = $this->newInstance();
1352  $this->assertSame( '', $op->getHTML() );
1353 
1354  $op->addHTML( 'a' );
1355  $this->assertSame( 'a', $op->getHTML() );
1356 
1357  $op->addHTML( 'b' );
1358  $this->assertSame( 'ab', $op->getHTML() );
1359 
1360  $op->prependHTML( 'c' );
1361  $this->assertSame( 'cab', $op->getHTML() );
1362 
1363  $op->addElement( 'p', [ 'id' => 'foo' ], 'd' );
1364  $this->assertSame( 'cab<p id="foo">d</p>', $op->getHTML() );
1365 
1366  $op->clearHTML();
1367  $this->assertSame( '', $op->getHTML() );
1368  }
1369 
1375  public function testRevisionId( $newVal, $expected ) {
1376  $op = $this->newInstance();
1377 
1378  $this->assertNull( $op->setRevisionId( $newVal ) );
1379  $this->assertSame( $expected, $op->getRevisionId() );
1380  $this->assertSame( $expected, $op->setRevisionId( null ) );
1381  $this->assertNull( $op->getRevisionId() );
1382  }
1383 
1384  public function provideRevisionId() {
1385  return [
1386  [ null, null ],
1387  [ 7, 7 ],
1388  [ -1, -1 ],
1389  [ 3.2, 3 ],
1390  [ '0', 0 ],
1391  [ '32% finished', 32 ],
1392  [ false, 0 ],
1393  ];
1394  }
1395 
1400  public function testRevisionTimestamp() {
1401  $op = $this->newInstance();
1402  $this->assertNull( $op->getRevisionTimestamp() );
1403 
1404  $this->assertNull( $op->setRevisionTimestamp( 'abc' ) );
1405  $this->assertSame( 'abc', $op->getRevisionTimestamp() );
1406  $this->assertSame( 'abc', $op->setRevisionTimestamp( null ) );
1407  $this->assertNull( $op->getRevisionTimestamp() );
1408  }
1409 
1414  public function testFileVersion() {
1415  $op = $this->newInstance();
1416  $this->assertNull( $op->getFileVersion() );
1417 
1418  $stubFile = $this->createMock( File::class );
1419  $stubFile->method( 'exists' )->willReturn( true );
1420  $stubFile->method( 'getTimestamp' )->willReturn( '12211221123321' );
1421  $stubFile->method( 'getSha1' )->willReturn( 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' );
1422 
1423  $op->setFileVersion( $stubFile );
1424 
1425  $this->assertEquals(
1426  [ 'time' => '12211221123321', 'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' ],
1427  $op->getFileVersion()
1428  );
1429 
1430  $stubMissingFile = $this->createMock( File::class );
1431  $stubMissingFile->method( 'exists' )->willReturn( false );
1432 
1433  $op->setFileVersion( $stubMissingFile );
1434  $this->assertNull( $op->getFileVersion() );
1435 
1436  $op->setFileVersion( $stubFile );
1437  $this->assertNotNull( $op->getFileVersion() );
1438 
1439  $op->setFileVersion( null );
1440  $this->assertNull( $op->getFileVersion() );
1441  }
1442 
1447  private function createParserOutputStub( ...$args ) {
1448  if ( count( $args ) === 0 ) {
1449  $retVals = [];
1450  } elseif ( count( $args ) === 1 ) {
1451  $retVals = $args[0];
1452  } elseif ( count( $args ) === 2 ) {
1453  $retVals = [ $args[0] => $args[1] ];
1454  }
1455  $pOut = $this->getMock( ParserOutput::class );
1456  foreach ( $retVals as $method => $retVal ) {
1457  $pOut->method( $method )->willReturn( $retVal );
1458  }
1459 
1460  $arrayReturningMethods = [
1461  'getCategories',
1462  'getFileSearchOptions',
1463  'getHeadItems',
1464  'getIndicators',
1465  'getLanguageLinks',
1466  'getOutputHooks',
1467  'getTemplateIds',
1468  ];
1469 
1470  foreach ( $arrayReturningMethods as $method ) {
1471  $pOut->method( $method )->willReturn( [] );
1472  }
1473 
1474  return $pOut;
1475  }
1476 
1482  public function testTemplateIds() {
1483  $op = $this->newInstance();
1484  $this->assertSame( [], $op->getTemplateIds() );
1485 
1486  // Test with no template id's
1487  $stubPOEmpty = $this->createParserOutputStub();
1488  $op->addParserOutputMetadata( $stubPOEmpty );
1489  $this->assertSame( [], $op->getTemplateIds() );
1490 
1491  // Test with some arbitrary template id's
1492  $ids = [
1493  NS_MAIN => [ 'A' => 3, 'B' => 17 ],
1494  NS_TALK => [ 'C' => 31 ],
1495  NS_MEDIA => [ 'D' => -1 ],
1496  ];
1497 
1498  $stubPO1 = $this->createParserOutputStub( 'getTemplateIds', $ids );
1499 
1500  $op->addParserOutputMetadata( $stubPO1 );
1501  $this->assertSame( $ids, $op->getTemplateIds() );
1502 
1503  // Test merging with a second set of id's
1504  $stubPO2 = $this->createParserOutputStub( 'getTemplateIds', [
1505  NS_MAIN => [ 'E' => 1234 ],
1506  NS_PROJECT => [ 'F' => 5678 ],
1507  ] );
1508 
1509  $finalIds = [
1510  NS_MAIN => [ 'E' => 1234, 'A' => 3, 'B' => 17 ],
1511  NS_TALK => [ 'C' => 31 ],
1512  NS_MEDIA => [ 'D' => -1 ],
1513  NS_PROJECT => [ 'F' => 5678 ],
1514  ];
1515 
1516  $op->addParserOutput( $stubPO2 );
1517  $this->assertSame( $finalIds, $op->getTemplateIds() );
1518 
1519  // Test merging with an empty set of id's
1520  $op->addParserOutputMetadata( $stubPOEmpty );
1521  $this->assertSame( $finalIds, $op->getTemplateIds() );
1522  }
1523 
1529  public function testFileSearchOptions() {
1530  $op = $this->newInstance();
1531  $this->assertSame( [], $op->getFileSearchOptions() );
1532 
1533  // Test with no files
1534  $stubPOEmpty = $this->createParserOutputStub();
1535 
1536  $op->addParserOutputMetadata( $stubPOEmpty );
1537  $this->assertSame( [], $op->getFileSearchOptions() );
1538 
1539  // Test with some arbitrary files
1540  $files1 = [
1541  'A' => [ 'time' => null, 'sha1' => '' ],
1542  'B' => [
1543  'time' => '12211221123321',
1544  'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05',
1545  ],
1546  ];
1547 
1548  $stubPO1 = $this->createParserOutputStub( 'getFileSearchOptions', $files1 );
1549 
1550  $op->addParserOutput( $stubPO1 );
1551  $this->assertSame( $files1, $op->getFileSearchOptions() );
1552 
1553  // Test merging with a second set of files
1554  $files2 = [
1555  'C' => [ 'time' => null, 'sha1' => '' ],
1556  'B' => [ 'time' => null, 'sha1' => '' ],
1557  ];
1558 
1559  $stubPO2 = $this->createParserOutputStub( 'getFileSearchOptions', $files2 );
1560 
1561  $op->addParserOutputMetadata( $stubPO2 );
1562  $this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
1563 
1564  // Test merging with an empty set of files
1565  $op->addParserOutput( $stubPOEmpty );
1566  $this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
1567  }
1568 
1581  public function testAddWikiText( $method, array $args, $expected ) {
1582  $op = $this->newInstance();
1583  $this->assertSame( '', $op->getHTML() );
1584 
1585  $this->hideDeprecated( 'OutputPage::addWikiText' );
1586  $this->hideDeprecated( 'OutputPage::addWikiTextTitle' );
1587  $this->hideDeprecated( 'OutputPage::addWikiTextWithTitle' );
1588  $this->hideDeprecated( 'OutputPage::addWikiTextTidy' );
1589  $this->hideDeprecated( 'OutputPage::addWikiTextTitleTidy' );
1590  $this->hideDeprecated( 'disabling tidy' );
1591 
1592  if ( in_array(
1593  $method,
1594  [ 'addWikiTextWithTitle', 'addWikiTextTitleTidy', 'addWikiTextTitle' ]
1595  ) && count( $args ) >= 2 && $args[1] === null ) {
1596  // Special placeholder because we can't get the actual title in the provider
1597  $args[1] = $op->getTitle();
1598  }
1599  if ( in_array(
1600  $method,
1601  [ 'addWikiTextAsInterface', 'addWikiTextAsContent' ]
1602  ) && count( $args ) >= 3 && $args[2] === null ) {
1603  // Special placeholder because we can't get the actual title in the provider
1604  $args[2] = $op->getTitle();
1605  }
1606 
1607  $op->$method( ...$args );
1608  $this->assertSame( $expected, $op->getHTML() );
1609  }
1610 
1611  public function provideAddWikiText() {
1612  $tests = [
1613  'addWikiText' => [
1614  // Not tidied; this API is deprecated.
1615  'Simple wikitext' => [
1616  [ "'''Bold'''" ],
1617  "<p><b>Bold</b>\n</p>",
1618  ], 'List at start' => [
1619  [ '* List' ],
1620  "<ul><li>List</li></ul>\n",
1621  ], 'List not at start' => [
1622  [ '* Not a list', false ],
1623  '* Not a list',
1624  ], 'Non-interface' => [
1625  [ "'''Bold'''", true, false ],
1626  "<p><b>Bold</b>\n</p>",
1627  ], 'No section edit links' => [
1628  [ '== Title ==' ],
1629  "<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>",
1630  ],
1631  ],
1632  'addWikiTextWithTitle' => [
1633  // Untidied; this API is deprecated
1634  'With title at start' => [
1635  [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ) ],
1636  "<ul><li>Some page</li></ul>\n",
1637  ], 'With title at start' => [
1638  [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ), false ],
1639  "* Some page",
1640  ],
1641  ],
1642  'addWikiTextAsInterface' => [
1643  // Preferred interface: output is tidied
1644  'Simple wikitext' => [
1645  [ "'''Bold'''" ],
1646  "<p><b>Bold</b>\n</p>",
1647  ], 'Untidy wikitext' => [
1648  [ "<b>Bold" ],
1649  "<p><b>Bold\n</b></p>",
1650  ], 'List at start' => [
1651  [ '* List' ],
1652  "<ul><li>List</li></ul>\n",
1653  ], 'List not at start' => [
1654  [ '* Not a list', false ],
1655  '<p>* Not a list</p>',
1656  ], 'No section edit links' => [
1657  [ '== Title ==' ],
1658  "<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>",
1659  ], 'With title at start' => [
1660  [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1661  "<ul><li>Some page</li></ul>\n",
1662  ], 'With title at start' => [
1663  [ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
1664  "<p>* Some page</p>",
1665  ], 'Untidy input' => [
1666  [ '<b>{{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1667  "<p><b>Some page\n</b></p>",
1668  ],
1669  ],
1670  'addWikiTextAsContent' => [
1671  // Preferred interface: output is tidied
1672  'SpecialNewimages' => [
1673  [ "<p lang='en' dir='ltr'>\nMy message" ],
1674  '<p lang="en" dir="ltr">' . "\nMy message</p>"
1675  ], 'List at start' => [
1676  [ '* List' ],
1677  "<ul><li>List</li></ul>",
1678  ], 'List not at start' => [
1679  [ '* <b>Not a list', false ],
1680  '<p>* <b>Not a list</b></p>',
1681  ], 'With title at start' => [
1682  [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1683  "<ul><li>Some page</li></ul>\n",
1684  ], 'With title at start' => [
1685  [ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
1686  "<p>* Some page</p>",
1687  ], 'EditPage' => [
1688  [ "<div class='mw-editintro'>{{PAGENAME}}", true, Title::newFromText( 'Talk:Some page' ) ],
1689  '<div class="mw-editintro">' . "Some page</div>"
1690  ],
1691  ],
1692  'wrapWikiTextAsInterface' => [
1693  'Simple' => [
1694  [ 'wrapperClass', 'text' ],
1695  "<div class=\"wrapperClass\"><p>text\n</p></div>"
1696  ], 'Spurious </div>' => [
1697  [ 'wrapperClass', 'text</div><div>more' ],
1698  "<div class=\"wrapperClass\"><p>text</p><div>more</div></div>"
1699  ], 'Extra newlines would break <p> wrappers' => [
1700  [ 'two classes', "1\n\n2\n\n3" ],
1701  "<div class=\"two classes\"><p>1\n</p><p>2\n</p><p>3\n</p></div>"
1702  ], 'Other unclosed tags' => [
1703  [ 'error', 'a<b>c<i>d' ],
1704  "<div class=\"error\"><p>a<b>c<i>d\n</i></b></p></div>"
1705  ],
1706  ],
1707  ];
1708 
1709  // Test all the others on addWikiTextTitle as well
1710  foreach ( $tests['addWikiText'] as $key => $val ) {
1711  $args = [ $val[0][0], null, $val[0][1] ?? true, false, $val[0][2] ?? true ];
1712  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1713  array_merge( [ $args ], array_slice( $val, 1 ) );
1714  }
1715  foreach ( $tests['addWikiTextWithTitle'] as $key => $val ) {
1716  $args = [ $val[0][0], $val[0][1], $val[0][2] ?? true ];
1717  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1718  array_merge( [ $args ], array_slice( $val, 1 ) );
1719  }
1720  foreach ( $tests['addWikiTextAsInterface'] as $key => $val ) {
1721  $args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, true ];
1722  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1723  array_merge( [ $args ], array_slice( $val, 1 ) );
1724  }
1725  foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
1726  $args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, false ];
1727  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1728  array_merge( [ $args ], array_slice( $val, 1 ) );
1729  }
1730  // addWikiTextTidy / addWikiTextTitleTidy were old aliases of
1731  // addWikiTextAsContent
1732  foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
1733  if ( count( $val[0] ) > 2 ) {
1734  $args = [ $val[0][0], $val[0][2], $val[0][1] ?? true ];
1735  $tests['addWikiTextTitleTidy']["$key (addWikiTextTitleTidy)"] =
1736  array_merge( [ $args ], array_slice( $val, 1 ) );
1737  } else {
1738  $args = [ $val[0][0], $val[0][1] ?? true ];
1739  $tests['addWikiTextTidy']["$key (addWikiTextTidy)"] =
1740  array_merge( [ $args ], array_slice( $val, 1 ) );
1741  }
1742  }
1743 
1744  // We have to reformat our array to match what PHPUnit wants
1745  $ret = [];
1746  foreach ( $tests as $key => $subarray ) {
1747  foreach ( $subarray as $subkey => $val ) {
1748  $val = array_merge( [ $key ], $val );
1749  $ret[$subkey] = $val;
1750  }
1751  }
1752 
1753  return $ret;
1754  }
1755 
1759  public function testAddWikiTextNoTitle() {
1760  $this->hideDeprecated( 'OutputPage::addWikiText' );
1761  $this->setExpectedException( MWException::class, 'Title is null' );
1762 
1763  $op = $this->newInstance( [], null, 'notitle' );
1764  $op->addWikiText( 'a' );
1765  }
1766 
1771  $this->setExpectedException( MWException::class, 'Title is null' );
1772 
1773  $op = $this->newInstance( [], null, 'notitle' );
1774  $op->addWikiTextAsInterface( 'a' );
1775  }
1776 
1781  $this->setExpectedException( MWException::class, 'Title is null' );
1782 
1783  $op = $this->newInstance( [], null, 'notitle' );
1784  $op->addWikiTextAsContent( 'a' );
1785  }
1786 
1790  public function testAddWikiMsg() {
1791  $msg = wfMessage( 'parentheses' );
1792  $this->assertSame( '(a)', $msg->rawParams( 'a' )->plain() );
1793 
1794  $op = $this->newInstance();
1795  $this->assertSame( '', $op->getHTML() );
1796  $op->addWikiMsg( 'parentheses', "<b>a" );
1797  // The input is bad unbalanced HTML, but the output is tidied
1798  $this->assertSame( "<p>(<b>a)\n</b></p>", $op->getHTML() );
1799  }
1800 
1804  public function testWrapWikiMsg() {
1805  $msg = wfMessage( 'parentheses' );
1806  $this->assertSame( '(a)', $msg->rawParams( 'a' )->plain() );
1807 
1808  $op = $this->newInstance();
1809  $this->assertSame( '', $op->getHTML() );
1810  $op->wrapWikiMsg( '[$1]', [ 'parentheses', "<b>a" ] );
1811  // The input is bad unbalanced HTML, but the output is tidied
1812  $this->assertSame( "<p>[(<b>a)]\n</b></p>", $op->getHTML() );
1813  }
1814 
1819  public function testNoGallery() {
1820  $op = $this->newInstance();
1821  $this->assertFalse( $op->mNoGallery );
1822 
1823  $stubPO1 = $this->createParserOutputStub( 'getNoGallery', true );
1824  $op->addParserOutputMetadata( $stubPO1 );
1825  $this->assertTrue( $op->mNoGallery );
1826 
1827  $stubPO2 = $this->createParserOutputStub( 'getNoGallery', false );
1828  $op->addParserOutput( $stubPO2 );
1829  $this->assertFalse( $op->mNoGallery );
1830  }
1831 
1832  private static $parserOutputHookCalled;
1833 
1837  public function testParserOutputHooks() {
1838  $op = $this->newInstance();
1839  $pOut = $this->createParserOutputStub( 'getOutputHooks', [
1840  [ 'myhook', 'banana' ],
1841  [ 'yourhook', 'kumquat' ],
1842  [ 'theirhook', 'hippopotamus' ],
1843  ] );
1844 
1845  self::$parserOutputHookCalled = [];
1846 
1847  $this->setMwGlobals( 'wgParserOutputHooks', [
1848  'myhook' => function ( OutputPage $innerOp, ParserOutput $innerPOut, $data )
1849  use ( $op, $pOut ) {
1850  $this->assertSame( $op, $innerOp );
1851  $this->assertSame( $pOut, $innerPOut );
1852  $this->assertSame( 'banana', $data );
1853  self::$parserOutputHookCalled[] = 'closure';
1854  },
1855  'yourhook' => [ $this, 'parserOutputHookCallback' ],
1856  'theirhook' => [ __CLASS__, 'parserOutputHookCallbackStatic' ],
1857  'uncalled' => function () {
1858  $this->assertTrue( false );
1859  },
1860  ] );
1861 
1862  $op->addParserOutputMetadata( $pOut );
1863 
1864  $this->assertSame( [ 'closure', 'callback', 'static' ], self::$parserOutputHookCalled );
1865  }
1866 
1867  public function parserOutputHookCallback(
1868  OutputPage $op, ParserOutput $pOut, $data
1869  ) {
1870  $this->assertSame( 'kumquat', $data );
1871 
1872  self::$parserOutputHookCalled[] = 'callback';
1873  }
1874 
1875  public static function parserOutputHookCallbackStatic(
1876  OutputPage $op, ParserOutput $pOut, $data
1877  ) {
1878  // All the assert methods are actually static, who knew!
1879  self::assertSame( 'hippopotamus', $data );
1880 
1881  self::$parserOutputHookCalled[] = 'static';
1882  }
1883 
1884  // @todo Make sure to test the following in addParserOutputMetadata() as well when we add tests
1885  // for them:
1886  // * addModules()
1887  // * addModuleStyles()
1888  // * addJsConfigVars()
1889  // * enableOOUI()
1890  // Otherwise those lines of addParserOutputMetadata() will be reported as covered, but we won't
1891  // be testing they actually work.
1892 
1896  public function testAddParserOutputText() {
1897  $op = $this->newInstance();
1898  $this->assertSame( '', $op->getHTML() );
1899 
1900  $pOut = $this->createParserOutputStub( 'getText', '<some text>' );
1901 
1902  $op->addParserOutputMetadata( $pOut );
1903  $this->assertSame( '', $op->getHTML() );
1904 
1905  $op->addParserOutputText( $pOut );
1906  $this->assertSame( '<some text>', $op->getHTML() );
1907  }
1908 
1912  public function testAddParserOutput() {
1913  $op = $this->newInstance();
1914  $this->assertSame( '', $op->getHTML() );
1915  $this->assertFalse( $op->showNewSectionLink() );
1916 
1917  $pOut = $this->createParserOutputStub( [
1918  'getText' => '<some text>',
1919  'getNewSection' => true,
1920  ] );
1921 
1922  $op->addParserOutput( $pOut );
1923  $this->assertSame( '<some text>', $op->getHTML() );
1924  $this->assertTrue( $op->showNewSectionLink() );
1925  }
1926 
1930  public function testAddTemplate() {
1931  $template = $this->getMock( QuickTemplate::class );
1932  $template->method( 'getHTML' )->willReturn( '<abc>&def;' );
1933 
1934  $op = $this->newInstance();
1935  $op->addTemplate( $template );
1936 
1937  $this->assertSame( '<abc>&def;', $op->getHTML() );
1938  }
1939 
1947  public function testParse( array $args, $expectedHTML ) {
1948  $this->hideDeprecated( 'OutputPage::parse' );
1949  $op = $this->newInstance();
1950  $this->assertSame( $expectedHTML, $op->parse( ...$args ) );
1951  }
1952 
1957  public function testParseInline( array $args, $expectedHTML, $expectedHTMLInline = null ) {
1958  if ( count( $args ) > 3 ) {
1959  // $language param not supported
1960  $this->assertTrue( true );
1961  return;
1962  }
1963  $this->hideDeprecated( 'OutputPage::parseInline' );
1964  $op = $this->newInstance();
1965  $this->assertSame( $expectedHTMLInline ?? $expectedHTML, $op->parseInline( ...$args ) );
1966  }
1967 
1968  public function provideParse() {
1969  return [
1970  'List at start of line (content)' => [
1971  [ '* List', true, false ],
1972  "<div class=\"mw-parser-output\"><ul><li>List</li></ul></div>",
1973  "<ul><li>List</li></ul>",
1974  ],
1975  'List at start of line (interface)' => [
1976  [ '* List', true, true ],
1977  "<ul><li>List</li></ul>",
1978  ],
1979  'List not at start (content)' => [
1980  [ "* ''Not'' list", false, false ],
1981  '<div class="mw-parser-output">* <i>Not</i> list</div>',
1982  '* <i>Not</i> list',
1983  ],
1984  'List not at start (interface)' => [
1985  [ "* ''Not'' list", false, true ],
1986  '* <i>Not</i> list',
1987  ],
1988  'Interface message' => [
1989  [ "''Italic''", true, true ],
1990  "<p><i>Italic</i>\n</p>",
1991  '<i>Italic</i>',
1992  ],
1993  'formatnum (content)' => [
1994  [ '{{formatnum:123456.789}}', true, false ],
1995  "<div class=\"mw-parser-output\"><p>123,456.789\n</p></div>",
1996  "123,456.789",
1997  ],
1998  'formatnum (interface)' => [
1999  [ '{{formatnum:123456.789}}', true, true ],
2000  "<p>123,456.789\n</p>",
2001  "123,456.789",
2002  ],
2003  'Language (content)' => [
2004  [ '{{formatnum:123456.789}}', true, false, Language::factory( 'is' ) ],
2005  "<div class=\"mw-parser-output\"><p>123.456,789\n</p></div>",
2006  ],
2007  'Language (interface)' => [
2008  [ '{{formatnum:123456.789}}', true, true, Language::factory( 'is' ) ],
2009  "<p>123.456,789\n</p>",
2010  '123.456,789',
2011  ],
2012  'No section edit links' => [
2013  [ '== Header ==' ],
2014  '<div class="mw-parser-output"><h2><span class="mw-headline" id="Header">' .
2015  "Header</span></h2></div>",
2016  '<h2><span class="mw-headline" id="Header">Header</span></h2>',
2017  ]
2018  ];
2019  }
2020 
2028  public function testParseAsContent(
2029  array $args, $expectedHTML, $expectedHTMLInline = null
2030  ) {
2031  $op = $this->newInstance();
2032  $this->assertSame( $expectedHTML, $op->parseAsContent( ...$args ) );
2033  }
2034 
2042  public function testParseAsInterface(
2043  array $args, $expectedHTML, $expectedHTMLInline = null
2044  ) {
2045  $op = $this->newInstance();
2046  $this->assertSame( $expectedHTML, $op->parseAsInterface( ...$args ) );
2047  }
2048 
2054  array $args, $expectedHTML, $expectedHTMLInline = null
2055  ) {
2056  $op = $this->newInstance();
2057  $this->assertSame(
2058  $expectedHTMLInline ?? $expectedHTML,
2059  $op->parseInlineAsInterface( ...$args )
2060  );
2061  }
2062 
2063  public function provideParseAs() {
2064  return [
2065  'List at start of line' => [
2066  [ '* List', true ],
2067  "<ul><li>List</li></ul>",
2068  ],
2069  'List not at start' => [
2070  [ "* ''Not'' list", false ],
2071  '<p>* <i>Not</i> list</p>',
2072  '* <i>Not</i> list',
2073  ],
2074  'Italics' => [
2075  [ "''Italic''", true ],
2076  "<p><i>Italic</i>\n</p>",
2077  '<i>Italic</i>',
2078  ],
2079  'formatnum' => [
2080  [ '{{formatnum:123456.789}}', true ],
2081  "<p>123,456.789\n</p>",
2082  "123,456.789",
2083  ],
2084  'No section edit links' => [
2085  [ '== Header ==' ],
2086  '<h2><span class="mw-headline" id="Header">Header</span></h2>',
2087  ]
2088  ];
2089  }
2090 
2094  public function testParseNullTitle() {
2095  $this->hideDeprecated( 'OutputPage::parse' );
2096  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2097  $op = $this->newInstance( [], null, 'notitle' );
2098  $op->parse( '' );
2099  }
2100 
2104  public function testParseInlineNullTitle() {
2105  $this->hideDeprecated( 'OutputPage::parseInline' );
2106  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2107  $op = $this->newInstance( [], null, 'notitle' );
2108  $op->parseInline( '' );
2109  }
2110 
2114  public function testParseAsContentNullTitle() {
2115  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2116  $op = $this->newInstance( [], null, 'notitle' );
2117  $op->parseAsContent( '' );
2118  }
2119 
2123  public function testParseAsInterfaceNullTitle() {
2124  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2125  $op = $this->newInstance( [], null, 'notitle' );
2126  $op->parseAsInterface( '' );
2127  }
2128 
2133  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2134  $op = $this->newInstance( [], null, 'notitle' );
2135  $op->parseInlineAsInterface( '' );
2136  }
2137 
2142  public function testCdnMaxage() {
2143  $op = $this->newInstance();
2144  $wrapper = TestingAccessWrapper::newFromObject( $op );
2145  $this->assertSame( 0, $wrapper->mCdnMaxage );
2146 
2147  $op->setCdnMaxage( -1 );
2148  $this->assertSame( -1, $wrapper->mCdnMaxage );
2149 
2150  $op->setCdnMaxage( 120 );
2151  $this->assertSame( 120, $wrapper->mCdnMaxage );
2152 
2153  $op->setCdnMaxage( 60 );
2154  $this->assertSame( 60, $wrapper->mCdnMaxage );
2155 
2156  $op->setCdnMaxage( 180 );
2157  $this->assertSame( 180, $wrapper->mCdnMaxage );
2158 
2159  $op->lowerCdnMaxage( 240 );
2160  $this->assertSame( 180, $wrapper->mCdnMaxage );
2161 
2162  $op->setCdnMaxage( 300 );
2163  $this->assertSame( 240, $wrapper->mCdnMaxage );
2164 
2165  $op->lowerCdnMaxage( 120 );
2166  $this->assertSame( 120, $wrapper->mCdnMaxage );
2167 
2168  $op->setCdnMaxage( 180 );
2169  $this->assertSame( 120, $wrapper->mCdnMaxage );
2170 
2171  $op->setCdnMaxage( 60 );
2172  $this->assertSame( 60, $wrapper->mCdnMaxage );
2173 
2174  $op->setCdnMaxage( 240 );
2175  $this->assertSame( 120, $wrapper->mCdnMaxage );
2176  }
2177 
2179  private static $fakeTime;
2180 
2189  public function testAdaptCdnTTL( array $args, $expected, array $options = [] ) {
2190  try {
2191  MWTimestamp::setFakeTime( self::$fakeTime );
2192 
2193  $op = $this->newInstance();
2194  // Set a high maxage so that it will get reduced by adaptCdnTTL(). The default maxage
2195  // is 0, so adaptCdnTTL() won't mutate the object at all.
2196  $initial = $options['initialMaxage'] ?? 86400;
2197  $op->setCdnMaxage( $initial );
2198 
2199  $op->adaptCdnTTL( ...$args );
2200  } finally {
2201  MWTimestamp::setFakeTime( false );
2202  }
2203 
2204  $wrapper = TestingAccessWrapper::newFromObject( $op );
2205 
2206  // Special rules for false/null
2207  if ( $args[0] === null || $args[0] === false ) {
2208  $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
2209  $op->setCdnMaxage( $expected + 1 );
2210  $this->assertSame( $expected + 1, $wrapper->mCdnMaxage, 'member value after new set' );
2211  return;
2212  }
2213 
2214  $this->assertSame( $expected, $wrapper->mCdnMaxageLimit, 'limit value' );
2215 
2216  if ( $initial >= $expected ) {
2217  $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value' );
2218  } else {
2219  $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
2220  }
2221 
2222  $op->setCdnMaxage( $expected + 1 );
2223  $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value after new set' );
2224  }
2225 
2226  public function provideAdaptCdnTTL() {
2227  global $wgSquidMaxage;
2228  $now = time();
2229  self::$fakeTime = $now;
2230  return [
2231  'Five minutes ago' => [ [ $now - 300 ], 270 ],
2232  'Now' => [ [ +0 ], IExpiringStore::TTL_MINUTE ],
2233  'Five minutes from now' => [ [ $now + 300 ], IExpiringStore::TTL_MINUTE ],
2234  'Five minutes ago, initial maxage four minutes' =>
2235  [ [ $now - 300 ], 270, [ 'initialMaxage' => 240 ] ],
2236  'A very long time ago' => [ [ $now - 1000000000 ], $wgSquidMaxage ],
2237  'Initial maxage zero' => [ [ $now - 300 ], 270, [ 'initialMaxage' => 0 ] ],
2238 
2239  'false' => [ [ false ], IExpiringStore::TTL_MINUTE ],
2240  'null' => [ [ null ], IExpiringStore::TTL_MINUTE ],
2241  "'0'" => [ [ '0' ], IExpiringStore::TTL_MINUTE ],
2242  'Empty string' => [ [ '' ], IExpiringStore::TTL_MINUTE ],
2243  // @todo These give incorrect results due to timezones, how to test?
2244  //"'now'" => [ [ 'now' ], IExpiringStore::TTL_MINUTE ],
2245  //"'parse error'" => [ [ 'parse error' ], IExpiringStore::TTL_MINUTE ],
2246 
2247  'Now, minTTL 0' => [ [ $now, 0 ], IExpiringStore::TTL_MINUTE ],
2248  'Now, minTTL 0.000001' => [ [ $now, 0.000001 ], 0 ],
2249  'A very long time ago, maxTTL even longer' =>
2250  [ [ $now - 1000000000, 0, 1000000001 ], 900000000 ],
2251  ];
2252  }
2253 
2259  public function testClientCache() {
2260  $op = $this->newInstance();
2261 
2262  // Test initial value
2263  $this->assertSame( true, $op->enableClientCache( null ) );
2264  // Test that calling with null doesn't change the value
2265  $this->assertSame( true, $op->enableClientCache( null ) );
2266 
2267  // Test setting to false
2268  $this->assertSame( true, $op->enableClientCache( false ) );
2269  $this->assertSame( false, $op->enableClientCache( null ) );
2270  // Test that calling with null doesn't change the value
2271  $this->assertSame( false, $op->enableClientCache( null ) );
2272 
2273  // Test that a cacheable ParserOutput doesn't set to true
2274  $pOutCacheable = $this->createParserOutputStub( 'isCacheable', true );
2275  $op->addParserOutputMetadata( $pOutCacheable );
2276  $this->assertSame( false, $op->enableClientCache( null ) );
2277 
2278  // Test setting back to true
2279  $this->assertSame( false, $op->enableClientCache( true ) );
2280  $this->assertSame( true, $op->enableClientCache( null ) );
2281 
2282  // Test that an uncacheable ParserOutput does set to false
2283  $pOutUncacheable = $this->createParserOutputStub( 'isCacheable', false );
2284  $op->addParserOutput( $pOutUncacheable );
2285  $this->assertSame( false, $op->enableClientCache( null ) );
2286  }
2287 
2291  public function testGetCacheVaryCookies() {
2292  global $wgCookiePrefix, $wgDBname;
2293  $op = $this->newInstance();
2294  $prefix = $wgCookiePrefix !== false ? $wgCookiePrefix : $wgDBname;
2295  $expectedCookies = [
2296  "{$prefix}Token",
2297  "{$prefix}LoggedOut",
2298  "{$prefix}_session",
2299  'forceHTTPS',
2300  'cookie1',
2301  'cookie2',
2302  ];
2303 
2304  // We have to reset the cookies because getCacheVaryCookies may have already been called
2305  TestingAccessWrapper::newFromClass( OutputPage::class )->cacheVaryCookies = null;
2306 
2307  $this->setMwGlobals( 'wgCacheVaryCookies', [ 'cookie1' ] );
2308  $this->setTemporaryHook( 'GetCacheVaryCookies',
2309  function ( $innerOP, &$cookies ) use ( $op, $expectedCookies ) {
2310  $this->assertSame( $op, $innerOP );
2311  $cookies[] = 'cookie2';
2312  $this->assertSame( $expectedCookies, $cookies );
2313  }
2314  );
2315 
2316  $this->assertSame( $expectedCookies, $op->getCacheVaryCookies() );
2317  }
2318 
2322  public function testHaveCacheVaryCookies() {
2323  $request = new FauxRequest();
2324  $op = $this->newInstance( [], $request );
2325 
2326  // No cookies are set.
2327  $this->assertFalse( $op->haveCacheVaryCookies() );
2328 
2329  // 'Token' is present but empty, so it shouldn't count.
2330  $request->setCookie( 'Token', '' );
2331  $this->assertFalse( $op->haveCacheVaryCookies() );
2332 
2333  // 'Token' present and nonempty.
2334  $request->setCookie( 'Token', '123' );
2335  $this->assertTrue( $op->haveCacheVaryCookies() );
2336  }
2337 
2350  public function testVaryHeaders( array $calls, array $cookies, $vary, $key ) {
2351  // Get rid of default Vary fields
2352  $op = $this->getMockBuilder( OutputPage::class )
2353  ->setConstructorArgs( [ new RequestContext() ] )
2354  ->setMethods( [ 'getCacheVaryCookies' ] )
2355  ->getMock();
2356  $op->expects( $this->any() )
2357  ->method( 'getCacheVaryCookies' )
2358  ->will( $this->returnValue( $cookies ) );
2359  TestingAccessWrapper::newFromObject( $op )->mVaryHeader = [];
2360 
2361  $this->hideDeprecated( '$wgUseKeyHeader' );
2362  foreach ( $calls as $call ) {
2363  $op->addVaryHeader( ...$call );
2364  }
2365  $this->assertEquals( $vary, $op->getVaryHeader(), 'Vary:' );
2366  $this->assertEquals( $key, $op->getKeyHeader(), 'Key:' );
2367  }
2368 
2369  public function provideVaryHeaders() {
2370  // note: getKeyHeader() automatically adds Vary: Cookie
2371  return [
2372  'No header' => [
2373  [],
2374  [],
2375  'Vary: ',
2376  'Key: Cookie',
2377  ],
2378  'Single header' => [
2379  [
2380  [ 'Cookie' ],
2381  ],
2382  [],
2383  'Vary: Cookie',
2384  'Key: Cookie',
2385  ],
2386  'Non-unique headers' => [
2387  [
2388  [ 'Cookie' ],
2389  [ 'Accept-Language' ],
2390  [ 'Cookie' ],
2391  ],
2392  [],
2393  'Vary: Cookie, Accept-Language',
2394  'Key: Cookie,Accept-Language',
2395  ],
2396  'Two headers with single options' => [
2397  [
2398  [ 'Cookie', [ 'param=phpsessid' ] ],
2399  [ 'Accept-Language', [ 'substr=en' ] ],
2400  ],
2401  [],
2402  'Vary: Cookie, Accept-Language',
2403  'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
2404  ],
2405  'One header with multiple options' => [
2406  [
2407  [ 'Cookie', [ 'param=phpsessid', 'param=userId' ] ],
2408  ],
2409  [],
2410  'Vary: Cookie',
2411  'Key: Cookie;param=phpsessid;param=userId',
2412  ],
2413  'Duplicate option' => [
2414  [
2415  [ 'Cookie', [ 'param=phpsessid' ] ],
2416  [ 'Cookie', [ 'param=phpsessid' ] ],
2417  [ 'Accept-Language', [ 'substr=en', 'substr=en' ] ],
2418  ],
2419  [],
2420  'Vary: Cookie, Accept-Language',
2421  'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
2422  ],
2423  'Same header, different options' => [
2424  [
2425  [ 'Cookie', [ 'param=phpsessid' ] ],
2426  [ 'Cookie', [ 'param=userId' ] ],
2427  ],
2428  [],
2429  'Vary: Cookie',
2430  'Key: Cookie;param=phpsessid;param=userId',
2431  ],
2432  'No header, vary cookies' => [
2433  [],
2434  [ 'cookie1', 'cookie2' ],
2435  'Vary: Cookie',
2436  'Key: Cookie;param=cookie1;param=cookie2',
2437  ],
2438  'Cookie header with option plus vary cookies' => [
2439  [
2440  [ 'Cookie', [ 'param=cookie1' ] ],
2441  ],
2442  [ 'cookie2', 'cookie3' ],
2443  'Vary: Cookie',
2444  'Key: Cookie;param=cookie1;param=cookie2;param=cookie3',
2445  ],
2446  'Non-cookie header plus vary cookies' => [
2447  [
2448  [ 'Accept-Language' ],
2449  ],
2450  [ 'cookie' ],
2451  'Vary: Accept-Language, Cookie',
2452  'Key: Accept-Language,Cookie;param=cookie',
2453  ],
2454  'Cookie and non-cookie headers plus vary cookies' => [
2455  [
2456  [ 'Cookie', [ 'param=cookie1' ] ],
2457  [ 'Accept-Language' ],
2458  ],
2459  [ 'cookie2' ],
2460  'Vary: Cookie, Accept-Language',
2461  'Key: Cookie;param=cookie1;param=cookie2,Accept-Language',
2462  ],
2463  ];
2464  }
2465 
2469  public function testVaryHeaderDefault() {
2470  $op = $this->newInstance();
2471  $this->assertSame( 'Vary: Accept-Encoding, Cookie', $op->getVaryHeader() );
2472  }
2473 
2480  public function testLinkHeaders( array $headers, $result ) {
2481  $op = $this->newInstance();
2482 
2483  foreach ( $headers as $header ) {
2484  $op->addLinkHeader( $header );
2485  }
2486 
2487  $this->assertEquals( $result, $op->getLinkHeader() );
2488  }
2489 
2490  public function provideLinkHeaders() {
2491  return [
2492  [
2493  [],
2494  false
2495  ],
2496  [
2497  [ '<https://foo/bar.jpg>;rel=preload;as=image' ],
2498  'Link: <https://foo/bar.jpg>;rel=preload;as=image',
2499  ],
2500  [
2501  [
2502  '<https://foo/bar.jpg>;rel=preload;as=image',
2503  '<https://foo/baz.jpg>;rel=preload;as=image'
2504  ],
2505  'Link: <https://foo/bar.jpg>;rel=preload;as=image,<https://foo/baz.jpg>;' .
2506  'rel=preload;as=image',
2507  ],
2508  ];
2509  }
2510 
2516  public function testAddAcceptLanguage(
2517  $code, array $variants, array $expected, array $options = []
2518  ) {
2519  $req = new FauxRequest( in_array( 'varianturl', $options ) ? [ 'variant' => 'x' ] : [] );
2520  $op = $this->newInstance( [], $req, in_array( 'notitle', $options ) ? 'notitle' : null );
2521 
2522  if ( !in_array( 'notitle', $options ) ) {
2523  $mockLang = $this->getMock( Language::class );
2524 
2525  if ( in_array( 'varianturl', $options ) ) {
2526  $mockLang->expects( $this->never() )->method( $this->anything() );
2527  } else {
2528  $mockLang->method( 'hasVariants' )->willReturn( count( $variants ) > 1 );
2529  $mockLang->method( 'getVariants' )->willReturn( $variants );
2530  $mockLang->method( 'getCode' )->willReturn( $code );
2531  }
2532 
2533  $mockTitle = $this->getMock( Title::class );
2534  $mockTitle->method( 'getPageLanguage' )->willReturn( $mockLang );
2535 
2536  $op->setTitle( $mockTitle );
2537  }
2538 
2539  // This will run addAcceptLanguage()
2540  $op->sendCacheControl();
2541 
2542  $this->hideDeprecated( '$wgUseKeyHeader' );
2543  $keyHeader = $op->getKeyHeader();
2544 
2545  if ( !$expected ) {
2546  $this->assertFalse( strpos( 'Accept-Language', $keyHeader ) );
2547  return;
2548  }
2549 
2550  $keyHeader = explode( ' ', $keyHeader, 2 )[1];
2551  $keyHeader = explode( ',', $keyHeader );
2552 
2553  $acceptLanguage = null;
2554  foreach ( $keyHeader as $item ) {
2555  if ( strpos( $item, 'Accept-Language;' ) === 0 ) {
2556  $acceptLanguage = $item;
2557  break;
2558  }
2559  }
2560 
2561  $expectedString = 'Accept-Language;substr=' . implode( ';substr=', $expected );
2562  $this->assertSame( $expectedString, $acceptLanguage );
2563  }
2564 
2565  public function provideAddAcceptLanguage() {
2566  return [
2567  'No variants' => [ 'en', [ 'en' ], [] ],
2568  'One simple variant' => [ 'en', [ 'en', 'en-x-piglatin' ], [ 'en-x-piglatin' ] ],
2569  'Multiple variants with BCP47 alternatives' => [
2570  'zh',
2571  [ 'zh', 'zh-hans', 'zh-cn', 'zh-tw' ],
2572  [ 'zh-hans', 'zh-Hans', 'zh-cn', 'zh-Hans-CN', 'zh-tw', 'zh-Hant-TW' ],
2573  ],
2574  'No title' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'notitle' ] ],
2575  'Variant in URL' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'varianturl' ] ],
2576  ];
2577  }
2578 
2586  public function testClickjacking() {
2587  $op = $this->newInstance();
2588  $this->assertTrue( $op->getPreventClickjacking() );
2589 
2590  $op->allowClickjacking();
2591  $this->assertFalse( $op->getPreventClickjacking() );
2592 
2593  $op->preventClickjacking();
2594  $this->assertTrue( $op->getPreventClickjacking() );
2595 
2596  $op->preventClickjacking( false );
2597  $this->assertFalse( $op->getPreventClickjacking() );
2598 
2599  $pOut1 = $this->createParserOutputStub( 'preventClickjacking', true );
2600  $op->addParserOutputMetadata( $pOut1 );
2601  $this->assertTrue( $op->getPreventClickjacking() );
2602 
2603  // The ParserOutput can't allow, only prevent
2604  $pOut2 = $this->createParserOutputStub( 'preventClickjacking', false );
2605  $op->addParserOutputMetadata( $pOut2 );
2606  $this->assertTrue( $op->getPreventClickjacking() );
2607 
2608  // Reset to test with addParserOutput()
2609  $op->allowClickjacking();
2610  $this->assertFalse( $op->getPreventClickjacking() );
2611 
2612  $op->addParserOutput( $pOut1 );
2613  $this->assertTrue( $op->getPreventClickjacking() );
2614 
2615  $op->addParserOutput( $pOut2 );
2616  $this->assertTrue( $op->getPreventClickjacking() );
2617  }
2618 
2624  public function testGetFrameOptions(
2625  $breakFrames, $preventClickjacking, $editPageFrameOptions, $expected
2626  ) {
2627  $op = $this->newInstance( [
2628  'BreakFrames' => $breakFrames,
2629  'EditPageFrameOptions' => $editPageFrameOptions,
2630  ] );
2631  $op->preventClickjacking( $preventClickjacking );
2632 
2633  $this->assertSame( $expected, $op->getFrameOptions() );
2634  }
2635 
2636  public function provideGetFrameOptions() {
2637  return [
2638  'BreakFrames true' => [ true, false, false, 'DENY' ],
2639  'Allow clickjacking locally' => [ false, false, 'DENY', false ],
2640  'Allow clickjacking globally' => [ false, true, false, false ],
2641  'DENY globally' => [ false, true, 'DENY', 'DENY' ],
2642  'SAMEORIGIN' => [ false, true, 'SAMEORIGIN', 'SAMEORIGIN' ],
2643  'BreakFrames with SAMEORIGIN' => [ true, true, 'SAMEORIGIN', 'DENY' ],
2644  ];
2645  }
2646 
2654  public function testMakeResourceLoaderLink( $args, $expectedHtml ) {
2655  $this->setMwGlobals( [
2656  'wgResourceLoaderDebug' => false,
2657  'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
2658  'wgCSPReportOnlyHeader' => true,
2659  ] );
2660  $class = new ReflectionClass( OutputPage::class );
2661  $method = $class->getMethod( 'makeResourceLoaderLink' );
2662  $method->setAccessible( true );
2663  $ctx = new RequestContext();
2664  $ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) );
2665  $ctx->setLanguage( 'en' );
2666  $out = new OutputPage( $ctx );
2667  $nonce = $class->getProperty( 'CSPNonce' );
2668  $nonce->setAccessible( true );
2669  $nonce->setValue( $out, 'secret' );
2670  $rl = $out->getResourceLoader();
2671  $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
2672  $rl->register( [
2673  'test.foo' => new ResourceLoaderTestModule( [
2674  'script' => 'mw.test.foo( { a: true } );',
2675  'styles' => '.mw-test-foo { content: "style"; }',
2676  ] ),
2677  'test.bar' => new ResourceLoaderTestModule( [
2678  'script' => 'mw.test.bar( { a: true } );',
2679  'styles' => '.mw-test-bar { content: "style"; }',
2680  ] ),
2681  'test.baz' => new ResourceLoaderTestModule( [
2682  'script' => 'mw.test.baz( { a: true } );',
2683  'styles' => '.mw-test-baz { content: "style"; }',
2684  ] ),
2685  'test.quux' => new ResourceLoaderTestModule( [
2686  'script' => 'mw.test.baz( { token: 123 } );',
2687  'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
2688  'group' => 'private',
2689  ] ),
2690  'test.noscript' => new ResourceLoaderTestModule( [
2691  'styles' => '.stuff { color: red; }',
2692  'group' => 'noscript',
2693  ] ),
2694  'test.group.foo' => new ResourceLoaderTestModule( [
2695  'script' => 'mw.doStuff( "foo" );',
2696  'group' => 'foo',
2697  ] ),
2698  'test.group.bar' => new ResourceLoaderTestModule( [
2699  'script' => 'mw.doStuff( "bar" );',
2700  'group' => 'bar',
2701  ] ),
2702  ] );
2703  $links = $method->invokeArgs( $out, $args );
2704  $actualHtml = strval( $links );
2705  $this->assertEquals( $expectedHtml, $actualHtml );
2706  }
2707 
2708  public static function provideMakeResourceLoaderLink() {
2709  // phpcs:disable Generic.Files.LineLength
2710  return [
2711  // Single only=scripts load
2712  [
2713  [ 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ],
2714  "<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
2715  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.foo\u0026only=scripts\u0026skin=fallback");'
2716  . "});</script>"
2717  ],
2718  // Multiple only=styles load
2719  [
2720  [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
2721 
2722  '<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback"/>'
2723  ],
2724  // Private embed (only=scripts)
2725  [
2726  [ 'test.quux', ResourceLoaderModule::TYPE_SCRIPTS ],
2727  "<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
2728  . "mw.test.baz({token:123});\nmw.loader.state({\"test.quux\":\"ready\"});"
2729  . "});</script>"
2730  ],
2731  // Load private module (combined)
2732  [
2733  [ 'test.quux', ResourceLoaderModule::TYPE_COMBINED ],
2734  "<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
2735  . "mw.loader.implement(\"test.quux@1ev0ijv\",function($,jQuery,require,module){"
2736  . "mw.test.baz({token:123});},{\"css\":[\".mw-icon{transition:none}"
2737  . "\"]});});</script>"
2738  ],
2739  // Load no modules
2740  [
2742  '',
2743  ],
2744  // noscript group
2745  [
2746  [ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ],
2747  '<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback"/></noscript>'
2748  ],
2749  // Load two modules in separate groups
2750  [
2751  [ [ 'test.group.foo', 'test.group.bar' ], ResourceLoaderModule::TYPE_COMBINED ],
2752  "<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
2753  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.bar\u0026skin=fallback");'
2754  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.foo\u0026skin=fallback");'
2755  . "});</script>"
2756  ],
2757  ];
2758  // phpcs:enable
2759  }
2760 
2766  public function testBuildExemptModules( array $exemptStyleModules, $expect ) {
2767  $this->setMwGlobals( [
2768  'wgResourceLoaderDebug' => false,
2769  'wgLoadScript' => '/w/load.php',
2770  // Stub wgCacheEpoch as it influences getVersionHash used for the
2771  // urls in the expected HTML
2772  'wgCacheEpoch' => '20140101000000',
2773  ] );
2774 
2775  // Set up stubs
2776  $ctx = new RequestContext();
2777  $ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) );
2778  $ctx->setLanguage( 'en' );
2779  $op = $this->getMockBuilder( OutputPage::class )
2780  ->setConstructorArgs( [ $ctx ] )
2781  ->setMethods( [ 'buildCssLinksArray' ] )
2782  ->getMock();
2783  $op->expects( $this->any() )
2784  ->method( 'buildCssLinksArray' )
2785  ->willReturn( [] );
2786  $rl = $op->getResourceLoader();
2787  $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
2788 
2789  // Register custom modules
2790  $rl->register( [
2791  'example.site.a' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
2792  'example.site.b' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
2793  'example.user' => new ResourceLoaderTestModule( [ 'group' => 'user' ] ),
2794  ] );
2795 
2796  $op = TestingAccessWrapper::newFromObject( $op );
2797  $op->rlExemptStyleModules = $exemptStyleModules;
2798  $this->assertEquals(
2799  $expect,
2800  strval( $op->buildExemptModules() )
2801  );
2802  }
2803 
2804  public static function provideBuildExemptModules() {
2805  // phpcs:disable Generic.Files.LineLength
2806  return [
2807  'empty' => [
2808  'exemptStyleModules' => [],
2809  '<meta name="ResourceLoaderDynamicStyles" content=""/>',
2810  ],
2811  'empty sets' => [
2812  'exemptStyleModules' => [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ],
2813  '<meta name="ResourceLoaderDynamicStyles" content=""/>',
2814  ],
2815  'default logged-out' => [
2816  'exemptStyleModules' => [ 'site' => [ 'site.styles' ] ],
2817  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2818  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>',
2819  ],
2820  'default logged-in' => [
2821  'exemptStyleModules' => [ 'site' => [ 'site.styles' ], 'user' => [ 'user.styles' ] ],
2822  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2823  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>' . "\n" .
2824  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;skin=fallback&amp;version=1ai9g6t"/>',
2825  ],
2826  'custom modules' => [
2827  'exemptStyleModules' => [
2828  'site' => [ 'site.styles', 'example.site.a', 'example.site.b' ],
2829  'user' => [ 'user.styles', 'example.user' ],
2830  ],
2831  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2832  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.site.a%2Cb&amp;only=styles&amp;skin=fallback"/>' . "\n" .
2833  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>' . "\n" .
2834  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.user&amp;only=styles&amp;skin=fallback&amp;version=0a56zyi"/>' . "\n" .
2835  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;skin=fallback&amp;version=1ai9g6t"/>',
2836  ],
2837  ];
2838  // phpcs:enable
2839  }
2840 
2846  public function testTransformResourcePath( $baseDir, $basePath, $uploadDir = null,
2847  $uploadPath = null, $path = null, $expected = null
2848  ) {
2849  if ( $path === null ) {
2850  // Skip optional $uploadDir and $uploadPath
2851  $path = $uploadDir;
2852  $expected = $uploadPath;
2853  $uploadDir = "$baseDir/images";
2854  $uploadPath = "$basePath/images";
2855  }
2856  $this->setMwGlobals( 'IP', $baseDir );
2857  $conf = new HashConfig( [
2858  'ResourceBasePath' => $basePath,
2859  'UploadDirectory' => $uploadDir,
2860  'UploadPath' => $uploadPath,
2861  ] );
2862 
2863  // Some of these paths don't exist and will cause warnings
2864  Wikimedia\suppressWarnings();
2865  $actual = OutputPage::transformResourcePath( $conf, $path );
2866  Wikimedia\restoreWarnings();
2867 
2868  $this->assertEquals( $expected ?: $path, $actual );
2869  }
2870 
2871  public static function provideTransformFilePath() {
2872  $baseDir = dirname( __DIR__ ) . '/data/media';
2873  return [
2874  // File that matches basePath, and exists. Hash found and appended.
2875  [
2876  'baseDir' => $baseDir, 'basePath' => '/w',
2877  '/w/test.jpg',
2878  '/w/test.jpg?edcf2'
2879  ],
2880  // File that matches basePath, but not found on disk. Empty query.
2881  [
2882  'baseDir' => $baseDir, 'basePath' => '/w',
2883  '/w/unknown.png',
2884  '/w/unknown.png?'
2885  ],
2886  // File not matching basePath. Ignored.
2887  [
2888  'baseDir' => $baseDir, 'basePath' => '/w',
2889  '/files/test.jpg'
2890  ],
2891  // Empty string. Ignored.
2892  [
2893  'baseDir' => $baseDir, 'basePath' => '/w',
2894  '',
2895  ''
2896  ],
2897  // Similar path, but with domain component. Ignored.
2898  [
2899  'baseDir' => $baseDir, 'basePath' => '/w',
2900  '//example.org/w/test.jpg'
2901  ],
2902  [
2903  'baseDir' => $baseDir, 'basePath' => '/w',
2904  'https://example.org/w/test.jpg'
2905  ],
2906  // Unrelated path with domain component. Ignored.
2907  [
2908  'baseDir' => $baseDir, 'basePath' => '/w',
2909  'https://example.org/files/test.jpg'
2910  ],
2911  [
2912  'baseDir' => $baseDir, 'basePath' => '/w',
2913  '//example.org/files/test.jpg'
2914  ],
2915  // Unrelated path with domain, and empty base path (root mw install). Ignored.
2916  [
2917  'baseDir' => $baseDir, 'basePath' => '',
2918  'https://example.org/files/test.jpg'
2919  ],
2920  [
2921  'baseDir' => $baseDir, 'basePath' => '',
2922  // T155310
2923  '//example.org/files/test.jpg'
2924  ],
2925  // Check UploadPath before ResourceBasePath (T155146)
2926  [
2927  'baseDir' => dirname( $baseDir ), 'basePath' => '',
2928  'uploadDir' => $baseDir, 'uploadPath' => '/images',
2929  '/images/test.jpg',
2930  '/images/test.jpg?edcf2'
2931  ],
2932  ];
2933  }
2934 
2949  protected function assertTransformCssMediaCase( $args ) {
2950  $queryData = [];
2951  if ( isset( $args['printableQuery'] ) ) {
2952  $queryData['printable'] = $args['printableQuery'];
2953  }
2954 
2955  if ( isset( $args['handheldQuery'] ) ) {
2956  $queryData['handheld'] = $args['handheldQuery'];
2957  }
2958 
2959  $fauxRequest = new FauxRequest( $queryData, false );
2960  $this->setMwGlobals( [
2961  'wgRequest' => $fauxRequest,
2962  ] );
2963 
2964  $actualReturn = OutputPage::transformCssMedia( $args['media'] );
2965  $this->assertSame( $args['expectedReturn'], $actualReturn, $args['message'] );
2966  }
2967 
2973  public function testPrintRequests() {
2974  $this->assertTransformCssMediaCase( [
2975  'printableQuery' => '1',
2976  'media' => 'screen',
2977  'expectedReturn' => null,
2978  'message' => 'On printable request, screen returns null'
2979  ] );
2980 
2981  $this->assertTransformCssMediaCase( [
2982  'printableQuery' => '1',
2983  'media' => self::SCREEN_MEDIA_QUERY,
2984  'expectedReturn' => null,
2985  'message' => 'On printable request, screen media query returns null'
2986  ] );
2987 
2988  $this->assertTransformCssMediaCase( [
2989  'printableQuery' => '1',
2990  'media' => self::SCREEN_ONLY_MEDIA_QUERY,
2991  'expectedReturn' => null,
2992  'message' => 'On printable request, screen media query with only returns null'
2993  ] );
2994 
2995  $this->assertTransformCssMediaCase( [
2996  'printableQuery' => '1',
2997  'media' => 'print',
2998  'expectedReturn' => '',
2999  'message' => 'On printable request, media print returns empty string'
3000  ] );
3001  }
3002 
3008  public function testScreenRequests() {
3009  $this->assertTransformCssMediaCase( [
3010  'media' => 'screen',
3011  'expectedReturn' => 'screen',
3012  'message' => 'On screen request, screen media type is preserved'
3013  ] );
3014 
3015  $this->assertTransformCssMediaCase( [
3016  'media' => 'handheld',
3017  'expectedReturn' => 'handheld',
3018  'message' => 'On screen request, handheld media type is preserved'
3019  ] );
3020 
3021  $this->assertTransformCssMediaCase( [
3022  'media' => self::SCREEN_MEDIA_QUERY,
3023  'expectedReturn' => self::SCREEN_MEDIA_QUERY,
3024  'message' => 'On screen request, screen media query is preserved.'
3025  ] );
3026 
3027  $this->assertTransformCssMediaCase( [
3028  'media' => self::SCREEN_ONLY_MEDIA_QUERY,
3029  'expectedReturn' => self::SCREEN_ONLY_MEDIA_QUERY,
3030  'message' => 'On screen request, screen media query with only is preserved.'
3031  ] );
3032 
3033  $this->assertTransformCssMediaCase( [
3034  'media' => 'print',
3035  'expectedReturn' => 'print',
3036  'message' => 'On screen request, print media type is preserved'
3037  ] );
3038  }
3039 
3045  public function testHandheld() {
3046  $this->assertTransformCssMediaCase( [
3047  'handheldQuery' => '1',
3048  'media' => 'handheld',
3049  'expectedReturn' => '',
3050  'message' => 'On request with handheld querystring and media is handheld, returns empty string'
3051  ] );
3052 
3053  $this->assertTransformCssMediaCase( [
3054  'handheldQuery' => '1',
3055  'media' => 'screen',
3056  'expectedReturn' => null,
3057  'message' => 'On request with handheld querystring and media is screen, returns null'
3058  ] );
3059  }
3060 
3066  public function testIsTOCEnabled() {
3067  $op = $this->newInstance();
3068  $this->assertFalse( $op->isTOCEnabled() );
3069 
3070  $pOut1 = $this->createParserOutputStub( 'getTOCHTML', false );
3071  $op->addParserOutputMetadata( $pOut1 );
3072  $this->assertFalse( $op->isTOCEnabled() );
3073 
3074  $pOut2 = $this->createParserOutputStub( 'getTOCHTML', true );
3075  $op->addParserOutput( $pOut2 );
3076  $this->assertTrue( $op->isTOCEnabled() );
3077 
3078  // The parser output doesn't disable the TOC after it was enabled
3079  $op->addParserOutputMetadata( $pOut1 );
3080  $this->assertTrue( $op->isTOCEnabled() );
3081  }
3082 
3088  public function testPreloadLinkHeaders( $config, $result ) {
3089  $this->setMwGlobals( $config );
3090  $ctx = $this->getMockBuilder( ResourceLoaderContext::class )
3091  ->disableOriginalConstructor()->getMock();
3092  $module = new ResourceLoaderSkinModule();
3093 
3094  $this->assertEquals( [ $result ], $module->getHeaders( $ctx ) );
3095  }
3096 
3097  public function providePreloadLinkHeaders() {
3098  return [
3099  [
3100  [
3101  'wgResourceBasePath' => '/w',
3102  'wgLogo' => '/img/default.png',
3103  'wgLogoHD' => [
3104  '1.5x' => '/img/one-point-five.png',
3105  '2x' => '/img/two-x.png',
3106  ],
3107  ],
3108  'Link: </img/default.png>;rel=preload;as=image;media=' .
3109  'not all and (min-resolution: 1.5dppx),' .
3110  '</img/one-point-five.png>;rel=preload;as=image;media=' .
3111  '(min-resolution: 1.5dppx) and (max-resolution: 1.999999dppx),' .
3112  '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
3113  ],
3114  [
3115  [
3116  'wgResourceBasePath' => '/w',
3117  'wgLogo' => '/img/default.png',
3118  'wgLogoHD' => false,
3119  ],
3120  'Link: </img/default.png>;rel=preload;as=image'
3121  ],
3122  [
3123  [
3124  'wgResourceBasePath' => '/w',
3125  'wgLogo' => '/img/default.png',
3126  'wgLogoHD' => [
3127  '2x' => '/img/two-x.png',
3128  ],
3129  ],
3130  'Link: </img/default.png>;rel=preload;as=image;media=' .
3131  'not all and (min-resolution: 2dppx),' .
3132  '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
3133  ],
3134  [
3135  [
3136  'wgResourceBasePath' => '/w',
3137  'wgLogo' => '/img/default.png',
3138  'wgLogoHD' => [
3139  'svg' => '/img/vector.svg',
3140  ],
3141  ],
3142  'Link: </img/vector.svg>;rel=preload;as=image'
3143 
3144  ],
3145  [
3146  [
3147  'wgResourceBasePath' => '/w',
3148  'wgLogo' => '/w/test.jpg',
3149  'wgLogoHD' => false,
3150  'wgUploadPath' => '/w/images',
3151  'IP' => dirname( __DIR__ ) . '/data/media',
3152  ],
3153  'Link: </w/test.jpg?edcf2>;rel=preload;as=image',
3154  ],
3155  ];
3156  }
3157 
3161  private function newInstance( $config = [], WebRequest $request = null, $options = [] ) {
3162  $context = new RequestContext();
3163 
3164  $context->setConfig( new MultiConfig( [
3165  new HashConfig( $config + [
3166  'AppleTouchIcon' => false,
3167  'DisableLangConversion' => true,
3168  'EnableCanonicalServerLink' => false,
3169  'Favicon' => false,
3170  'Feed' => false,
3171  'LanguageCode' => false,
3172  'ReferrerPolicy' => false,
3173  'RightsPage' => false,
3174  'RightsUrl' => false,
3175  'UniversalEditButton' => false,
3176  ] ),
3177  $context->getConfig()
3178  ] ) );
3179 
3180  if ( !in_array( 'notitle', (array)$options ) ) {
3181  $context->setTitle( Title::newFromText( 'My test page' ) );
3182  }
3183 
3184  if ( $request ) {
3185  $context->setRequest( $request );
3186  }
3187 
3188  return new OutputPage( $context );
3189  }
3190 }
testAddInlineScript()
OutputPage::addInlineScript.
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 array("192.168.0.1:11211", 2))
testParseInline(array $args, $expectedHTML, $expectedHTMLInline=null)
provideParse OutputPage::parseInline
testProperties()
OutputPage::setProperty OutputPage::getProperty.
testRevisionTimestamp()
OutputPage::setRevisionTimestamp OutputPage::getRevisionTimestamp.
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.
$wgSquidMaxage
Cache TTL for the CDN sent as s-maxage (without ESI) or Surrogate-Control (with ESI).
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
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
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:64
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)
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:3050
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:68
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.
editPage( $pageName, $text, $summary='', $defaultNs=NS_MAIN)
Edits or creates a page/revision.
testRevisionId( $newVal, $expected)
provideRevisionId OutputPage::setRevisionId OutputPage::getRevisionId
static provideBuildExemptModules()
const NS_MEDIA
Definition: Defines.php:52
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:78
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.
testParseInlineNullTitle()
OutputPage::parseInline.
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:215
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.
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
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:65
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
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 ...
static getDefaultInstance()
Definition: SkinFactory.php:50
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