MediaWiki  master
OutputPageTest.php
Go to the documentation of this file.
1 <?php
2 
5 
13  const SCREEN_MEDIA_QUERY = 'screen and (min-width: 982px)';
14  const SCREEN_ONLY_MEDIA_QUERY = 'only screen and (min-width: 982px)';
15 
16  // @codingStandardsIgnoreStart Generic.Files.LineLength
17  const RSS_RC_LINK = '<link rel="alternate" type="application/rss+xml" title=" RSS feed" href="/w/index.php?title=Special:RecentChanges&amp;feed=rss"/>';
18  const ATOM_RC_LINK = '<link rel="alternate" type="application/atom+xml" title=" Atom feed" href="/w/index.php?title=Special:RecentChanges&amp;feed=atom"/>';
19 
20  const RSS_TEST_LINK = '<link rel="alternate" type="application/rss+xml" title="&quot;Test&quot; RSS feed" href="fake-link"/>';
21  const ATOM_TEST_LINK = '<link rel="alternate" type="application/atom+xml" title="&quot;Test&quot; Atom feed" href="fake-link"/>';
22  // @codingStandardsIgnoreEnd
23 
24  // Ensure that we don't affect the global ResourceLoader state.
25  protected function setUp() {
26  parent::setUp();
28  }
29 
30  protected function tearDown() {
31  parent::tearDown();
33  }
34 
42  public function testRedirect( $url, $code = null ) {
43  $op = $this->newInstance();
44  if ( isset( $code ) ) {
45  $op->redirect( $url, $code );
46  } else {
47  $op->redirect( $url );
48  }
49  $expectedUrl = str_replace( "\n", '', $url );
50  $this->assertSame( $expectedUrl, $op->getRedirect() );
51  $this->assertSame( $expectedUrl, $op->mRedirect );
52  $this->assertSame( $code ?? '302', $op->mRedirectCode );
53  }
54 
55  public function provideRedirect() {
56  return [
57  [ 'http://example.com' ],
58  [ 'http://example.com', '400' ],
59  [ 'http://example.com', 'squirrels!!!' ],
60  [ "a\nb" ],
61  ];
62  }
63 
64  private function setupFeedLinks( $feed, $types ) {
65  $outputPage = $this->newInstance( [
66  'AdvertisedFeedTypes' => $types,
67  'Feed' => $feed,
68  'OverrideSiteFeed' => false,
69  'Script' => '/w',
70  'Sitename' => false,
71  ] );
72  $outputPage->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
73  $this->setMwGlobals( [
74  'wgScript' => '/w/index.php',
75  ] );
76  return $outputPage;
77  }
78 
79  private function assertFeedLinks( $outputPage, $message, $present, $non_present ) {
80  $links = $outputPage->getHeadLinksArray();
81  foreach ( $present as $link ) {
82  $this->assertContains( $link, $links, $message );
83  }
84  foreach ( $non_present as $link ) {
85  $this->assertNotContains( $link, $links, $message );
86  }
87  }
88 
89  private function assertFeedUILinks( $outputPage, $ui_links ) {
90  if ( $ui_links ) {
91  $this->assertTrue( $outputPage->isSyndicated(), 'Syndication should be offered' );
92  $this->assertGreaterThan( 0, count( $outputPage->getSyndicationLinks() ),
93  'Some syndication links should be there' );
94  } else {
95  $this->assertFalse( $outputPage->isSyndicated(), 'No syndication should be offered' );
96  $this->assertEquals( 0, count( $outputPage->getSyndicationLinks() ),
97  'No syndication links should be there' );
98  }
99  }
100 
101  public static function provideFeedLinkData() {
102  return [
103  [
104  true, [ 'rss' ], 'Only RSS RC link should be offerred',
105  [ self::RSS_RC_LINK ], [ self::ATOM_RC_LINK ]
106  ],
107  [
108  true, [ 'atom' ], 'Only Atom RC link should be offerred',
109  [ self::ATOM_RC_LINK ], [ self::RSS_RC_LINK ]
110  ],
111  [
112  true, [], 'No RC feed formats should be offerred',
113  [], [ self::ATOM_RC_LINK, self::RSS_RC_LINK ]
114  ],
115  [
116  false, [ 'atom' ], 'No RC feeds should be offerred',
117  [], [ self::ATOM_RC_LINK, self::RSS_RC_LINK ]
118  ],
119  ];
120  }
121 
126  public function testSetCopyrightUrl() {
127  $op = $this->newInstance();
128  $op->setCopyrightUrl( 'http://example.com' );
129 
130  $this->assertSame(
131  Html::element( 'link', [ 'rel' => 'license', 'href' => 'http://example.com' ] ),
132  $op->getHeadLinksArray()['copyright']
133  );
134  }
135 
140  public function testRecentChangesFeed( $feed, $advertised_feed_types,
141  $message, $present, $non_present ) {
142  $outputPage = $this->setupFeedLinks( $feed, $advertised_feed_types );
143  $this->assertFeedLinks( $outputPage, $message, $present, $non_present );
144  }
145 
146  public static function provideAdditionalFeedData() {
147  return [
148  [
149  true, [ 'atom' ], 'Additional Atom feed should be offered',
150  'atom',
151  [ self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
152  [ self::RSS_TEST_LINK, self::RSS_RC_LINK ],
153  true,
154  ],
155  [
156  true, [ 'rss' ], 'Additional RSS feed should be offered',
157  'rss',
158  [ self::RSS_TEST_LINK, self::RSS_RC_LINK ],
159  [ self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
160  true,
161  ],
162  [
163  true, [ 'rss' ], 'Additional Atom feed should NOT be offered with RSS enabled',
164  'atom',
165  [ self::RSS_RC_LINK ],
166  [ self::RSS_TEST_LINK, self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
167  false,
168  ],
169  [
170  false, [ 'atom' ], 'Additional Atom feed should NOT be offered, all feeds disabled',
171  'atom',
172  [],
173  [
174  self::RSS_TEST_LINK, self::ATOM_TEST_LINK,
175  self::ATOM_RC_LINK, self::ATOM_RC_LINK,
176  ],
177  false,
178  ],
179  ];
180  }
181 
189  public function testAdditionalFeeds( $feed, $advertised_feed_types, $message,
190  $additional_feed_type, $present, $non_present, $any_ui_links ) {
191  $outputPage = $this->setupFeedLinks( $feed, $advertised_feed_types );
192  $outputPage->addFeedLink( $additional_feed_type, 'fake-link' );
193  $this->assertFeedLinks( $outputPage, $message, $present, $non_present );
194  $this->assertFeedUILinks( $outputPage, $any_ui_links );
195  }
196 
197  // @todo How to test setStatusCode?
198 
204  public function testMetaTags() {
205  $op = $this->newInstance();
206  $op->addMeta( 'http:expires', '0' );
207  $op->addMeta( 'keywords', 'first' );
208  $op->addMeta( 'keywords', 'second' );
209  $op->addMeta( 'og:title', 'Ta-duh' );
210 
211  $expected = [
212  [ 'http:expires', '0' ],
213  [ 'keywords', 'first' ],
214  [ 'keywords', 'second' ],
215  [ 'og:title', 'Ta-duh' ],
216  ];
217  $this->assertSame( $expected, $op->getMetaTags() );
218 
219  $links = $op->getHeadLinksArray();
220  $this->assertContains( '<meta http-equiv="expires" content="0"/>', $links );
221  $this->assertContains( '<meta name="keywords" content="first"/>', $links );
222  $this->assertContains( '<meta name="keywords" content="second"/>', $links );
223  $this->assertContains( '<meta property="og:title" content="Ta-duh"/>', $links );
224  $this->assertArrayNotHasKey( 'meta-robots', $links );
225  }
226 
232  public function testAddLink() {
233  $op = $this->newInstance();
234 
235  $links = [
236  [],
237  [ 'rel' => 'foo', 'href' => 'http://example.com' ],
238  ];
239 
240  foreach ( $links as $link ) {
241  $op->addLink( $link );
242  }
243 
244  $this->assertSame( $links, $op->getLinkTags() );
245 
246  $result = $op->getHeadLinksArray();
247 
248  foreach ( $links as $link ) {
249  $this->assertContains( Html::element( 'link', $link ), $result );
250  }
251  }
252 
258  public function testSetCanonicalUrl() {
259  $op = $this->newInstance();
260  $op->setCanonicalUrl( 'http://example.comm' );
261  $op->setCanonicalUrl( 'http://example.com' );
262 
263  $this->assertSame( 'http://example.com', $op->getCanonicalUrl() );
264 
265  $headLinks = $op->getHeadLinksArray();
266 
267  $this->assertContains( Html::element( 'link', [
268  'rel' => 'canonical', 'href' => 'http://example.com'
269  ] ), $headLinks );
270 
271  $this->assertNotContains( Html::element( 'link', [
272  'rel' => 'canonical', 'href' => 'http://example.comm'
273  ] ), $headLinks );
274  }
275 
279  public function testAddScript() {
280  $op = $this->newInstance();
281  $op->addScript( 'some random string' );
282 
283  $this->assertContains( "\nsome random string\n", "\n" . $op->getBottomScripts() . "\n" );
284  }
285 
289  public function testAddScriptFile() {
290  $op = $this->newInstance();
291  $op->addScriptFile( '/somescript.js' );
292  $op->addScriptFile( '//example.com/somescript.js' );
293 
294  $this->assertContains(
295  "\n" . Html::linkedScript( '/somescript.js', $op->getCSPNonce() ) .
296  Html::linkedScript( '//example.com/somescript.js', $op->getCSPNonce() ) . "\n",
297  "\n" . $op->getBottomScripts() . "\n"
298  );
299  }
300 
304  public function testAddInlineScript() {
305  $op = $this->newInstance();
306  $op->addInlineScript( 'let foo = "bar";' );
307  $op->addInlineScript( 'alert( foo );' );
308 
309  $this->assertContains(
310  "\n" . Html::inlineScript( "\nlet foo = \"bar\";\n", $op->getCSPNonce() ) . "\n" .
311  Html::inlineScript( "\nalert( foo );\n", $op->getCSPNonce() ) . "\n",
312  "\n" . $op->getBottomScripts() . "\n"
313  );
314  }
315 
316  // @todo How to test filterModules(), warnModuleTargetFilter(), getModules(), etc.?
317 
322  public function testSetTarget() {
323  $op = $this->newInstance();
324  $op->setTarget( 'foo' );
325 
326  $this->assertSame( 'foo', $op->getTarget() );
327  // @todo What else? Test some actual effect?
328  }
329 
330  // @todo How to test addContentOverride(Callback)?
331 
338  public function testHeadItems() {
339  $op = $this->newInstance();
340  $op->addHeadItem( 'a', 'b' );
341  $op->addHeadItems( [ 'c' => '<d>&amp;', 'e' => 'f', 'a' => 'q' ] );
342  $op->addHeadItem( 'e', 'g' );
343  $op->addHeadItems( 'x' );
344 
345  $this->assertSame( [ 'a' => 'q', 'c' => '<d>&amp;', 'e' => 'g', 'x' ],
346  $op->getHeadItemsArray() );
347 
348  $this->assertTrue( $op->hasHeadItem( 'a' ) );
349  $this->assertTrue( $op->hasHeadItem( 'c' ) );
350  $this->assertTrue( $op->hasHeadItem( 'e' ) );
351  $this->assertTrue( $op->hasHeadItem( '0' ) );
352 
353  $this->assertContains( "\nq\n<d>&amp;\ng\nx\n",
354  '' . $op->headElement( $op->getContext()->getSkin() ) );
355  }
356 
362  public function testHeadItemsParserOutput() {
363  $op = $this->newInstance();
364  $stubPO1 = $this->createParserOutputStub( 'getHeadItems', [ 'a' => 'b' ] );
365  $op->addParserOutputMetadata( $stubPO1 );
366  $stubPO2 = $this->createParserOutputStub( 'getHeadItems',
367  [ 'c' => '<d>&amp;', 'e' => 'f', 'a' => 'q' ] );
368  $op->addParserOutputMetadata( $stubPO2 );
369  $stubPO3 = $this->createParserOutputStub( 'getHeadItems', [ 'e' => 'g' ] );
370  $op->addParserOutput( $stubPO3 );
371  $stubPO4 = $this->createParserOutputStub( 'getHeadItems', [ 'x' ] );
372  $op->addParserOutputMetadata( $stubPO4 );
373 
374  $this->assertSame( [ 'a' => 'q', 'c' => '<d>&amp;', 'e' => 'g', 'x' ],
375  $op->getHeadItemsArray() );
376 
377  $this->assertTrue( $op->hasHeadItem( 'a' ) );
378  $this->assertTrue( $op->hasHeadItem( 'c' ) );
379  $this->assertTrue( $op->hasHeadItem( 'e' ) );
380  $this->assertTrue( $op->hasHeadItem( '0' ) );
381  $this->assertFalse( $op->hasHeadItem( 'b' ) );
382 
383  $this->assertContains( "\nq\n<d>&amp;\ng\nx\n",
384  '' . $op->headElement( $op->getContext()->getSkin() ) );
385  }
386 
390  public function testAddBodyClasses() {
391  $op = $this->newInstance();
392  $op->addBodyClasses( 'a' );
393  $op->addBodyClasses( 'mediawiki' );
394  $op->addBodyClasses( 'b c' );
395  $op->addBodyClasses( [ 'd', 'e' ] );
396  $op->addBodyClasses( 'a' );
397 
398  $this->assertContains( '"a mediawiki b c d e ltr',
399  '' . $op->headElement( $op->getContext()->getSkin() ) );
400  }
401 
406  public function testArticleBodyOnly() {
407  $op = $this->newInstance();
408  $this->assertFalse( $op->getArticleBodyOnly() );
409 
410  $op->setArticleBodyOnly( true );
411  $this->assertTrue( $op->getArticleBodyOnly() );
412 
413  $op->addHTML( '<b>a</b>' );
414 
415  $this->assertSame( '<b>a</b>', $op->output( true ) );
416  }
417 
422  public function testProperties() {
423  $op = $this->newInstance();
424 
425  $this->assertNull( $op->getProperty( 'foo' ) );
426 
427  $op->setProperty( 'foo', 'bar' );
428  $op->setProperty( 'baz', 'quz' );
429 
430  $this->assertSame( 'bar', $op->getProperty( 'foo' ) );
431  $this->assertSame( 'quz', $op->getProperty( 'baz' ) );
432  }
433 
440  public function testCheckLastModified(
441  $timestamp, $ifModifiedSince, $expected, $config = [], $callback = null
442  ) {
443  $request = new FauxRequest();
444  if ( $ifModifiedSince ) {
445  if ( is_numeric( $ifModifiedSince ) ) {
446  // Unix timestamp
447  $ifModifiedSince = date( 'D, d M Y H:i:s', $ifModifiedSince ) . ' GMT';
448  }
449  $request->setHeader( 'If-Modified-Since', $ifModifiedSince );
450  }
451 
452  if ( !isset( $config['CacheEpoch'] ) ) {
453  // Make sure it's not too recent
454  $config['CacheEpoch'] = '20000101000000';
455  }
456 
457  $op = $this->newInstance( $config, $request );
458 
459  if ( $callback ) {
460  $callback( $op, $this );
461  }
462 
463  // Avoid a complaint about not being able to disable compression
464  Wikimedia\suppressWarnings();
465  try {
466  $this->assertEquals( $expected, $op->checkLastModified( $timestamp ) );
467  } finally {
468  Wikimedia\restoreWarnings();
469  }
470  }
471 
472  public function provideCheckLastModified() {
473  $lastModified = time() - 3600;
474  return [
475  'Timestamp 0' =>
476  [ '0', $lastModified, false ],
477  'Timestamp Unix epoch' =>
478  [ '19700101000000', $lastModified, false ],
479  'Timestamp same as If-Modified-Since' =>
480  [ $lastModified, $lastModified, true ],
481  'Timestamp one second after If-Modified-Since' =>
482  [ $lastModified + 1, $lastModified, false ],
483  'No If-Modified-Since' =>
484  [ $lastModified + 1, null, false ],
485  'Malformed If-Modified-Since' =>
486  [ $lastModified + 1, 'GIBBERING WOMBATS !!!', false ],
487  'Non-standard IE-style If-Modified-Since' =>
488  [ $lastModified, date( 'D, d M Y H:i:s', $lastModified ) . ' GMT; length=5202',
489  true ],
490  // @todo Should we fix this behavior to match the spec? Probably no reason to.
491  'If-Modified-Since not per spec but we accept it anyway because strtotime does' =>
492  [ $lastModified, "@$lastModified", true ],
493  '$wgCachePages = false' =>
494  [ $lastModified, $lastModified, false, [ 'CachePages' => false ] ],
495  '$wgCacheEpoch' =>
496  [ $lastModified, $lastModified, false,
497  [ 'CacheEpoch' => wfTimestamp( TS_MW, $lastModified + 1 ) ] ],
498  'Recently-touched user' =>
499  [ $lastModified, $lastModified, false, [],
500  function ( $op ) {
501  $op->getContext()->setUser( $this->getTestUser()->getUser() );
502  } ],
503  'After CDN expiry' =>
504  [ $lastModified, $lastModified, false,
505  [ 'UseCdn' => true, 'CdnMaxAge' => 3599 ] ],
506  'Hook allows cache use' =>
507  [ $lastModified + 1, $lastModified, true, [],
508  function ( $op, $that ) {
509  $that->setTemporaryHook( 'OutputPageCheckLastModified',
510  function ( &$modifiedTimes ) {
511  $modifiedTimes = [ 1 ];
512  }
513  );
514  } ],
515  'Hooks prohibits cache use' =>
516  [ $lastModified, $lastModified, false, [],
517  function ( $op, $that ) {
518  $that->setTemporaryHook( 'OutputPageCheckLastModified',
519  function ( &$modifiedTimes ) {
520  $modifiedTimes = [ max( $modifiedTimes ) + 1 ];
521  }
522  );
523  } ],
524  ];
525  }
526 
532  public function testCdnCacheEpoch( $params ) {
533  $out = TestingAccessWrapper::newFromObject( $this->newInstance() );
534  $reqTime = strtotime( $params['reqTime'] );
535  $pageTime = strtotime( $params['pageTime'] );
536  $actual = max( $pageTime, $out->getCdnCacheEpoch( $reqTime, $params['maxAge'] ) );
537 
538  $this->assertEquals(
539  $params['expect'],
540  gmdate( DateTime::ATOM, $actual ),
541  'cdn epoch'
542  );
543  }
544 
545  public static function provideCdnCacheEpoch() {
546  $base = [
547  'pageTime' => '2011-04-01T12:00:00+00:00',
548  'maxAge' => 24 * 3600,
549  ];
550  return [
551  'after 1s' => [ $base + [
552  'reqTime' => '2011-04-01T12:00:01+00:00',
553  'expect' => '2011-04-01T12:00:00+00:00',
554  ] ],
555  'after 23h' => [ $base + [
556  'reqTime' => '2011-04-02T11:00:00+00:00',
557  'expect' => '2011-04-01T12:00:00+00:00',
558  ] ],
559  'after 24h and a bit' => [ $base + [
560  'reqTime' => '2011-04-02T12:34:56+00:00',
561  'expect' => '2011-04-01T12:34:56+00:00',
562  ] ],
563  'after a year' => [ $base + [
564  'reqTime' => '2012-05-06T00:12:07+00:00',
565  'expect' => '2012-05-05T00:12:07+00:00',
566  ] ],
567  ];
568  }
569 
570  // @todo How to test setLastModified?
571 
576  public function testSetRobotPolicy() {
577  $op = $this->newInstance();
578  $op->setRobotPolicy( 'noindex, nofollow' );
579 
580  $links = $op->getHeadLinksArray();
581  $this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
582  }
583 
589  public function testSetIndexFollowPolicies() {
590  $op = $this->newInstance();
591  $op->setIndexPolicy( 'noindex' );
592  $op->setFollowPolicy( 'nofollow' );
593 
594  $links = $op->getHeadLinksArray();
595  $this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
596  }
597 
598  private function extractHTMLTitle( OutputPage $op ) {
599  $html = $op->headElement( $op->getContext()->getSkin() );
600 
601  // OutputPage should always output the title in a nice format such that regexes will work
602  // fine. If it doesn't, we'll fail the tests.
603  preg_match_all( '!<title>(.*?)</title>!', $html, $matches );
604 
605  $this->assertLessThanOrEqual( 1, count( $matches[1] ), 'More than one <title>!' );
606 
607  if ( !count( $matches[1] ) ) {
608  return null;
609  }
610 
611  return $matches[1][0];
612  }
613 
617  private static function getMsgText( $op, ...$msgParams ) {
618  return $op->msg( ...$msgParams )->inContentLanguage()->text();
619  }
620 
625  public function testHTMLTitle() {
626  $op = $this->newInstance();
627 
628  // Default
629  $this->assertSame( '', $op->getHTMLTitle() );
630  $this->assertSame( '', $op->getPageTitle() );
631  $this->assertSame(
632  $this->getMsgText( $op, 'pagetitle', '' ),
633  $this->extractHTMLTitle( $op )
634  );
635 
636  // Set to string
637  $op->setHTMLTitle( 'Potatoes will eat me' );
638 
639  $this->assertSame( 'Potatoes will eat me', $op->getHTMLTitle() );
640  $this->assertSame( 'Potatoes will eat me', $this->extractHTMLTitle( $op ) );
641  // Shouldn't have changed the page title
642  $this->assertSame( '', $op->getPageTitle() );
643 
644  // Set to message
645  $msg = $op->msg( 'mainpage' );
646 
647  $op->setHTMLTitle( $msg );
648  $this->assertSame( $msg->text(), $op->getHTMLTitle() );
649  $this->assertSame( $msg->text(), $this->extractHTMLTitle( $op ) );
650  $this->assertSame( '', $op->getPageTitle() );
651  }
652 
656  public function testSetRedirectedFrom() {
657  $op = $this->newInstance();
658 
659  $op->setRedirectedFrom( Title::newFromText( 'Talk:Some page' ) );
660  $this->assertSame( 'Talk:Some_page', $op->getJSVars()['wgRedirectedFrom'] );
661  }
662 
667  public function testPageTitle() {
668  // We don't test the actual HTML output anywhere, because that's up to the skin.
669  $op = $this->newInstance();
670 
671  // Test default
672  $this->assertSame( '', $op->getPageTitle() );
673  $this->assertSame( '', $op->getHTMLTitle() );
674 
675  // Test set to plain text
676  $op->setPageTitle( 'foobar' );
677 
678  $this->assertSame( 'foobar', $op->getPageTitle() );
679  // HTML title should change as well
680  $this->assertSame( $this->getMsgText( $op, 'pagetitle', 'foobar' ), $op->getHTMLTitle() );
681 
682  // Test set to text with good and bad HTML. We don't try to be comprehensive here, that
683  // belongs in Sanitizer tests.
684  $op->setPageTitle( '<script>a</script>&amp;<i>b</i>' );
685 
686  $this->assertSame( '&lt;script&gt;a&lt;/script&gt;&amp;<i>b</i>', $op->getPageTitle() );
687  $this->assertSame(
688  $this->getMsgText( $op, 'pagetitle', '<script>a</script>&b' ),
689  $op->getHTMLTitle()
690  );
691 
692  // Test set to message
693  $text = $this->getMsgText( $op, 'mainpage' );
694 
695  $op->setPageTitle( $op->msg( 'mainpage' )->inContentLanguage() );
696  $this->assertSame( $text, $op->getPageTitle() );
697  $this->assertSame( $this->getMsgText( $op, 'pagetitle', $text ), $op->getHTMLTitle() );
698  }
699 
703  public function testSetTitle() {
704  $op = $this->newInstance();
705 
706  $this->assertSame( 'My test page', $op->getTitle()->getPrefixedText() );
707 
708  $op->setTitle( Title::newFromText( 'Another test page' ) );
709 
710  $this->assertSame( 'Another test page', $op->getTitle()->getPrefixedText() );
711  }
712 
719  public function testSubtitle() {
720  $op = $this->newInstance();
721 
722  $this->assertSame( '', $op->getSubtitle() );
723 
724  $op->addSubtitle( '<b>foo</b>' );
725 
726  $this->assertSame( '<b>foo</b>', $op->getSubtitle() );
727 
728  $op->addSubtitle( $op->msg( 'mainpage' )->inContentLanguage() );
729 
730  $this->assertSame(
731  "<b>foo</b><br />\n\t\t\t\t" . $this->getMsgText( $op, 'mainpage' ),
732  $op->getSubtitle()
733  );
734 
735  $op->setSubtitle( 'There can be only one' );
736 
737  $this->assertSame( 'There can be only one', $op->getSubtitle() );
738 
739  $op->clearSubtitle();
740 
741  $this->assertSame( '', $op->getSubtitle() );
742  }
743 
749  public function testBuildBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
750  if ( count( $titles ) > 1 ) {
751  // Not applicable
752  $this->assertTrue( true );
753  return;
754  }
755 
757  $query = $queries[0];
758 
759  $this->editPage( 'Page 1', '' );
760  $this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
761 
763 
764  foreach ( $contains as $substr ) {
765  $this->assertContains( $substr, $str );
766  }
767 
768  foreach ( $notContains as $substr ) {
769  $this->assertNotContains( $substr, $str );
770  }
771  }
772 
779  public function testAddBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
780  $this->editPage( 'Page 1', '' );
781  $this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
782 
783  $op = $this->newInstance();
784  foreach ( $titles as $i => $unused ) {
785  $op->addBacklinkSubtitle( Title::newFromText( $titles[$i] ), $queries[$i] );
786  }
787 
788  $str = $op->getSubtitle();
789 
790  foreach ( $contains as $substr ) {
791  $this->assertContains( $substr, $str );
792  }
793 
794  foreach ( $notContains as $substr ) {
795  $this->assertNotContains( $substr, $str );
796  }
797  }
798 
799  public function provideBacklinkSubtitle() {
800  return [
801  [
802  [ 'Page 1' ],
803  [ [] ],
804  [ 'Page 1' ],
805  [ 'redirect', 'Page 2' ],
806  ],
807  [
808  [ 'Page 2' ],
809  [ [] ],
810  [ 'redirect=no' ],
811  [ 'Page 1' ],
812  ],
813  [
814  [ 'Page 1' ],
815  [ [ 'action' => 'edit' ] ],
816  [ 'action=edit' ],
817  [],
818  ],
819  [
820  [ 'Page 1', 'Page 2' ],
821  [ [], [] ],
822  [ 'Page 1', 'Page 2', "<br />\n\t\t\t\t" ],
823  [],
824  ],
825  // @todo Anything else to test?
826  ];
827  }
828 
833  public function testPrintable() {
834  $op = $this->newInstance();
835 
836  $this->assertFalse( $op->isPrintable() );
837 
838  $op->setPrintable();
839 
840  $this->assertTrue( $op->isPrintable() );
841  }
842 
847  public function testDisable() {
848  $op = $this->newInstance();
849 
850  $this->assertFalse( $op->isDisabled() );
851  $this->assertNotSame( '', $op->output( true ) );
852 
853  $op->disable();
854 
855  $this->assertTrue( $op->isDisabled() );
856  $this->assertSame( '', $op->output( true ) );
857  }
858 
864  public function testShowNewSectionLink() {
865  $op = $this->newInstance();
866 
867  $this->assertFalse( $op->showNewSectionLink() );
868 
869  $pOut1 = $this->createParserOutputStub( 'getNewSection', true );
870  $op->addParserOutputMetadata( $pOut1 );
871  $this->assertTrue( $op->showNewSectionLink() );
872 
873  $pOut2 = $this->createParserOutputStub( 'getNewSection', false );
874  $op->addParserOutput( $pOut2 );
875  $this->assertFalse( $op->showNewSectionLink() );
876  }
877 
883  public function testForceHideNewSectionLink() {
884  $op = $this->newInstance();
885 
886  $this->assertFalse( $op->forceHideNewSectionLink() );
887 
888  $pOut1 = $this->createParserOutputStub( 'getHideNewSection', true );
889  $op->addParserOutputMetadata( $pOut1 );
890  $this->assertTrue( $op->forceHideNewSectionLink() );
891 
892  $pOut2 = $this->createParserOutputStub( 'getHideNewSection', false );
893  $op->addParserOutput( $pOut2 );
894  $this->assertFalse( $op->forceHideNewSectionLink() );
895  }
896 
901  public function testSetSyndicated() {
902  $op = $this->newInstance( [ 'Feed' => true ] );
903  $this->assertFalse( $op->isSyndicated() );
904 
905  $op->setSyndicated();
906  $this->assertTrue( $op->isSyndicated() );
907 
908  $op->setSyndicated( false );
909  $this->assertFalse( $op->isSyndicated() );
910 
911  $op = $this->newInstance(); // Feed => false by default
912  $this->assertFalse( $op->isSyndicated() );
913 
914  $op->setSyndicated();
915  $this->assertFalse( $op->isSyndicated() );
916  }
917 
924  public function testFeedLinks() {
925  $op = $this->newInstance( [ 'Feed' => true ] );
926  $this->assertSame( [], $op->getSyndicationLinks() );
927 
928  $op->addFeedLink( 'not a supported format', 'abc' );
929  $this->assertFalse( $op->isSyndicated() );
930  $this->assertSame( [], $op->getSyndicationLinks() );
931 
932  $feedTypes = $op->getConfig()->get( 'AdvertisedFeedTypes' );
933 
934  $op->addFeedLink( $feedTypes[0], 'def' );
935  $this->assertTrue( $op->isSyndicated() );
936  $this->assertSame( [ $feedTypes[0] => 'def' ], $op->getSyndicationLinks() );
937 
938  $op->setFeedAppendQuery( false );
939  $expected = [];
940  foreach ( $feedTypes as $type ) {
941  $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type" );
942  }
943  $this->assertSame( $expected, $op->getSyndicationLinks() );
944 
945  $op->setFeedAppendQuery( 'apples=oranges' );
946  foreach ( $feedTypes as $type ) {
947  $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type&apples=oranges" );
948  }
949  $this->assertSame( $expected, $op->getSyndicationLinks() );
950 
951  $op = $this->newInstance(); // Feed => false by default
952  $this->assertSame( [], $op->getSyndicationLinks() );
953 
954  $op->addFeedLink( $feedTypes[0], 'def' );
955  $this->assertFalse( $op->isSyndicated() );
956  $this->assertSame( [], $op->getSyndicationLinks() );
957  }
958 
965  function testArticleFlags() {
966  $op = $this->newInstance();
967  $this->assertFalse( $op->isArticle() );
968  $this->assertTrue( $op->isArticleRelated() );
969 
970  $op->setArticleRelated( false );
971  $this->assertFalse( $op->isArticle() );
972  $this->assertFalse( $op->isArticleRelated() );
973 
974  $op->setArticleFlag( true );
975  $this->assertTrue( $op->isArticle() );
976  $this->assertTrue( $op->isArticleRelated() );
977 
978  $op->setArticleFlag( false );
979  $this->assertFalse( $op->isArticle() );
980  $this->assertTrue( $op->isArticleRelated() );
981 
982  $op->setArticleFlag( true );
983  $op->setArticleRelated( false );
984  $this->assertFalse( $op->isArticle() );
985  $this->assertFalse( $op->isArticleRelated() );
986  }
987 
995  function testLanguageLinks() {
996  $op = $this->newInstance();
997  $this->assertSame( [], $op->getLanguageLinks() );
998 
999  $op->addLanguageLinks( [ 'fr:A', 'it:B' ] );
1000  $this->assertSame( [ 'fr:A', 'it:B' ], $op->getLanguageLinks() );
1001 
1002  $op->addLanguageLinks( [ 'de:C', 'es:D' ] );
1003  $this->assertSame( [ 'fr:A', 'it:B', 'de:C', 'es:D' ], $op->getLanguageLinks() );
1004 
1005  $op->setLanguageLinks( [ 'pt:E' ] );
1006  $this->assertSame( [ 'pt:E' ], $op->getLanguageLinks() );
1007 
1008  $pOut1 = $this->createParserOutputStub( 'getLanguageLinks', [ 'he:F', 'ar:G' ] );
1009  $op->addParserOutputMetadata( $pOut1 );
1010  $this->assertSame( [ 'pt:E', 'he:F', 'ar:G' ], $op->getLanguageLinks() );
1011 
1012  $pOut2 = $this->createParserOutputStub( 'getLanguageLinks', [ 'pt:H' ] );
1013  $op->addParserOutput( $pOut2 );
1014  $this->assertSame( [ 'pt:E', 'he:F', 'ar:G', 'pt:H' ], $op->getLanguageLinks() );
1015  }
1016 
1017  // @todo Are these category links tests too abstract and complicated for what they test? Would
1018  // it make sense to just write out all the tests by hand with maybe some copy-and-paste?
1019 
1034  public function testAddCategoryLinks(
1035  array $args, array $fakeResults, callable $variantLinkCallback = null,
1036  array $expectedNormal, array $expectedHidden
1037  ) {
1038  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'add' );
1039  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'add' );
1040 
1041  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1042 
1043  $op->addCategoryLinks( $args );
1044 
1045  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
1046  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1047  }
1048 
1057  array $args, array $fakeResults, callable $variantLinkCallback = null,
1058  array $expectedNormal, array $expectedHidden
1059  ) {
1060  if ( count( $args ) <= 1 ) {
1061  // @todo Should this be skipped instead of passed?
1062  $this->assertTrue( true );
1063  return;
1064  }
1065 
1066  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'onebyone' );
1067  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'onebyone' );
1068 
1069  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1070 
1071  foreach ( $args as $key => $val ) {
1072  $op->addCategoryLinks( [ $key => $val ] );
1073  }
1074 
1075  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
1076  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1077  }
1078 
1086  public function testSetCategoryLinks(
1087  array $args, array $fakeResults, callable $variantLinkCallback = null,
1088  array $expectedNormal, array $expectedHidden
1089  ) {
1090  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'set' );
1091  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'set' );
1092 
1093  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1094 
1095  $op->setCategoryLinks( [ 'Initial page' => 'Initial page' ] );
1096  $op->setCategoryLinks( $args );
1097 
1098  // We don't reset the categories, for some reason, only the links
1099  $expectedNormalCats = array_merge( [ 'Initial page' ], $expectedNormal );
1100  $expectedCats = array_merge( $expectedHidden, $expectedNormalCats );
1101 
1102  $this->doCategoryAsserts( $op, $expectedNormalCats, $expectedHidden );
1103  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1104  }
1105 
1115  array $args, array $fakeResults, callable $variantLinkCallback = null,
1116  array $expectedNormal, array $expectedHidden
1117  ) {
1118  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'pout' );
1119  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'pout' );
1120 
1121  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1122 
1123  $stubPO = $this->createParserOutputStub( 'getCategories', $args );
1124 
1125  // addParserOutput and addParserOutputMetadata should behave identically for us, so
1126  // alternate to get coverage for both without adding extra tests
1127  static $idx = 0;
1128  $idx++;
1129  $method = [ 'addParserOutputMetadata', 'addParserOutput' ][$idx % 2];
1130  $op->$method( $stubPO );
1131 
1132  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
1133  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1134  }
1135 
1141  private function extractExpectedCategories( array $expected, $key ) {
1142  if ( !$expected || isset( $expected[0] ) ) {
1143  return $expected;
1144  }
1145  return $expected[$key] ?? $expected['default'];
1146  }
1147 
1148  private function setupCategoryTests(
1149  array $fakeResults, callable $variantLinkCallback = null
1150  ) : OutputPage {
1151  $this->setMwGlobals( 'wgUsePigLatinVariant', true );
1152 
1153  $op = $this->getMockBuilder( OutputPage::class )
1154  ->setConstructorArgs( [ new RequestContext() ] )
1155  ->setMethods( [ 'addCategoryLinksToLBAndGetResult', 'getTitle' ] )
1156  ->getMock();
1157 
1158  $title = Title::newFromText( 'My test page' );
1159  $op->expects( $this->any() )
1160  ->method( 'getTitle' )
1161  ->will( $this->returnValue( $title ) );
1162 
1163  $op->expects( $this->any() )
1164  ->method( 'addCategoryLinksToLBAndGetResult' )
1165  ->will( $this->returnCallback( function ( array $categories ) use ( $fakeResults ) {
1166  $return = [];
1167  foreach ( $categories as $category => $unused ) {
1168  if ( isset( $fakeResults[$category] ) ) {
1169  $return[] = $fakeResults[$category];
1170  }
1171  }
1172  return new FakeResultWrapper( $return );
1173  } ) );
1174 
1175  if ( $variantLinkCallback ) {
1176  $mockContLang = $this->getMockBuilder( Language::class )
1177  ->setConstructorArgs( [ 'en' ] )
1178  ->setMethods( [ 'findVariantLink' ] )
1179  ->getMock();
1180  $mockContLang->expects( $this->any() )
1181  ->method( 'findVariantLink' )
1182  ->will( $this->returnCallback( $variantLinkCallback ) );
1183  $this->setContentLang( $mockContLang );
1184  }
1185 
1186  $this->assertSame( [], $op->getCategories() );
1187 
1188  return $op;
1189  }
1190 
1191  private function doCategoryAsserts( $op, $expectedNormal, $expectedHidden ) {
1192  $this->assertSame( array_merge( $expectedHidden, $expectedNormal ), $op->getCategories() );
1193  $this->assertSame( $expectedNormal, $op->getCategories( 'normal' ) );
1194  $this->assertSame( $expectedHidden, $op->getCategories( 'hidden' ) );
1195  }
1196 
1197  private function doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden ) {
1198  $catLinks = $op->getCategoryLinks();
1199  $this->assertSame( (bool)$expectedNormal + (bool)$expectedHidden, count( $catLinks ) );
1200  if ( $expectedNormal ) {
1201  $this->assertSame( count( $expectedNormal ), count( $catLinks['normal'] ) );
1202  }
1203  if ( $expectedHidden ) {
1204  $this->assertSame( count( $expectedHidden ), count( $catLinks['hidden'] ) );
1205  }
1206 
1207  foreach ( $expectedNormal as $i => $name ) {
1208  $this->assertContains( $name, $catLinks['normal'][$i] );
1209  }
1210  foreach ( $expectedHidden as $i => $name ) {
1211  $this->assertContains( $name, $catLinks['hidden'][$i] );
1212  }
1213  }
1214 
1215  public function provideGetCategories() {
1216  return [
1217  'No categories' => [ [], [], null, [], [] ],
1218  'Simple test' => [
1219  [ 'Test1' => 'Some sortkey', 'Test2' => 'A different sortkey' ],
1220  [ 'Test1' => (object)[ 'pp_value' => 1, 'page_title' => 'Test1' ],
1221  'Test2' => (object)[ 'page_title' => 'Test2' ] ],
1222  null,
1223  [ 'Test2' ],
1224  [ 'Test1' ],
1225  ],
1226  'Invalid title' => [
1227  [ '[' => '[', 'Test' => 'Test' ],
1228  [ 'Test' => (object)[ 'page_title' => 'Test' ] ],
1229  null,
1230  [ 'Test' ],
1231  [],
1232  ],
1233  'Variant link' => [
1234  [ 'Test' => 'Test', 'Estay' => 'Estay' ],
1235  [ 'Test' => (object)[ 'page_title' => 'Test' ] ],
1236  function ( &$link, &$title ) {
1237  if ( $link === 'Estay' ) {
1238  $link = 'Test';
1240  }
1241  },
1242  // For adding one by one, the variant gets added as well as the original category,
1243  // but if you add them all together the second time gets skipped.
1244  [ 'onebyone' => [ 'Test', 'Test' ], 'default' => [ 'Test' ] ],
1245  [],
1246  ],
1247  ];
1248  }
1249 
1253  public function testGetCategoriesInvalid() {
1254  $this->setExpectedException( InvalidArgumentException::class,
1255  'Invalid category type given: hiddne' );
1256 
1257  $op = $this->newInstance();
1258  $op->getCategories( 'hiddne' );
1259  }
1260 
1261  // @todo Should we test addCategoryLinksToLBAndGetResult? If so, how? Insert some test rows in
1262  // the DB?
1263 
1270  public function testIndicators() {
1271  $op = $this->newInstance();
1272  $this->assertSame( [], $op->getIndicators() );
1273 
1274  $op->setIndicators( [] );
1275  $this->assertSame( [], $op->getIndicators() );
1276 
1277  // Test sorting alphabetically
1278  $op->setIndicators( [ 'b' => 'x', 'a' => 'y' ] );
1279  $this->assertSame( [ 'a' => 'y', 'b' => 'x' ], $op->getIndicators() );
1280 
1281  // Test overwriting existing keys
1282  $op->setIndicators( [ 'c' => 'z', 'a' => 'w' ] );
1283  $this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'z' ], $op->getIndicators() );
1284 
1285  // Test with addParserOutputMetadata
1286  $pOut1 = $this->createParserOutputStub( 'getIndicators', [ 'c' => 'u', 'd' => 'v' ] );
1287  $op->addParserOutputMetadata( $pOut1 );
1288  $this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
1289  $op->getIndicators() );
1290 
1291  // Test with addParserOutput
1292  $pOut2 = $this->createParserOutputStub( 'getIndicators', [ 'a' => '!!!' ] );
1293  $op->addParserOutput( $pOut2 );
1294  $this->assertSame( [ 'a' => '!!!', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
1295  $op->getIndicators() );
1296  }
1297 
1302  public function testAddHelpLink() {
1303  $op = $this->newInstance();
1304 
1305  $op->addHelpLink( 'Manual:PHP unit testing' );
1306  $indicators = $op->getIndicators();
1307  $this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
1308  $this->assertContains( 'Manual:PHP_unit_testing', $indicators['mw-helplink'] );
1309 
1310  $op->addHelpLink( 'https://phpunit.de', true );
1311  $indicators = $op->getIndicators();
1312  $this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
1313  $this->assertContains( 'https://phpunit.de', $indicators['mw-helplink'] );
1314  $this->assertNotContains( 'mediawiki', $indicators['mw-helplink'] );
1315  $this->assertNotContains( 'Manual:PHP', $indicators['mw-helplink'] );
1316  }
1317 
1325  public function testBodyHTML() {
1326  $op = $this->newInstance();
1327  $this->assertSame( '', $op->getHTML() );
1328 
1329  $op->addHTML( 'a' );
1330  $this->assertSame( 'a', $op->getHTML() );
1331 
1332  $op->addHTML( 'b' );
1333  $this->assertSame( 'ab', $op->getHTML() );
1334 
1335  $op->prependHTML( 'c' );
1336  $this->assertSame( 'cab', $op->getHTML() );
1337 
1338  $op->addElement( 'p', [ 'id' => 'foo' ], 'd' );
1339  $this->assertSame( 'cab<p id="foo">d</p>', $op->getHTML() );
1340 
1341  $op->clearHTML();
1342  $this->assertSame( '', $op->getHTML() );
1343  }
1344 
1350  public function testRevisionId( $newVal, $expected ) {
1351  $op = $this->newInstance();
1352 
1353  $this->assertNull( $op->setRevisionId( $newVal ) );
1354  $this->assertSame( $expected, $op->getRevisionId() );
1355  $this->assertSame( $expected, $op->setRevisionId( null ) );
1356  $this->assertNull( $op->getRevisionId() );
1357  }
1358 
1359  public function provideRevisionId() {
1360  return [
1361  [ null, null ],
1362  [ 7, 7 ],
1363  [ -1, -1 ],
1364  [ 3.2, 3 ],
1365  [ '0', 0 ],
1366  [ '32% finished', 32 ],
1367  [ false, 0 ],
1368  ];
1369  }
1370 
1375  public function testRevisionTimestamp() {
1376  $op = $this->newInstance();
1377  $this->assertNull( $op->getRevisionTimestamp() );
1378 
1379  $this->assertNull( $op->setRevisionTimestamp( 'abc' ) );
1380  $this->assertSame( 'abc', $op->getRevisionTimestamp() );
1381  $this->assertSame( 'abc', $op->setRevisionTimestamp( null ) );
1382  $this->assertNull( $op->getRevisionTimestamp() );
1383  }
1384 
1389  public function testFileVersion() {
1390  $op = $this->newInstance();
1391  $this->assertNull( $op->getFileVersion() );
1392 
1393  $stubFile = $this->createMock( File::class );
1394  $stubFile->method( 'exists' )->willReturn( true );
1395  $stubFile->method( 'getTimestamp' )->willReturn( '12211221123321' );
1396  $stubFile->method( 'getSha1' )->willReturn( 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' );
1397 
1398  $op->setFileVersion( $stubFile );
1399 
1400  $this->assertEquals(
1401  [ 'time' => '12211221123321', 'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' ],
1402  $op->getFileVersion()
1403  );
1404 
1405  $stubMissingFile = $this->createMock( File::class );
1406  $stubMissingFile->method( 'exists' )->willReturn( false );
1407 
1408  $op->setFileVersion( $stubMissingFile );
1409  $this->assertNull( $op->getFileVersion() );
1410 
1411  $op->setFileVersion( $stubFile );
1412  $this->assertNotNull( $op->getFileVersion() );
1413 
1414  $op->setFileVersion( null );
1415  $this->assertNull( $op->getFileVersion() );
1416  }
1417 
1422  private function createParserOutputStub( ...$args ) {
1423  if ( count( $args ) === 0 ) {
1424  $retVals = [];
1425  } elseif ( count( $args ) === 1 ) {
1426  $retVals = $args[0];
1427  } elseif ( count( $args ) === 2 ) {
1428  $retVals = [ $args[0] => $args[1] ];
1429  }
1430  $pOut = $this->getMock( ParserOutput::class );
1431  foreach ( $retVals as $method => $retVal ) {
1432  $pOut->method( $method )->willReturn( $retVal );
1433  }
1434 
1435  $arrayReturningMethods = [
1436  'getCategories',
1437  'getFileSearchOptions',
1438  'getHeadItems',
1439  'getIndicators',
1440  'getLanguageLinks',
1441  'getOutputHooks',
1442  'getTemplateIds',
1443  ];
1444 
1445  foreach ( $arrayReturningMethods as $method ) {
1446  $pOut->method( $method )->willReturn( [] );
1447  }
1448 
1449  return $pOut;
1450  }
1451 
1457  public function testTemplateIds() {
1458  $op = $this->newInstance();
1459  $this->assertSame( [], $op->getTemplateIds() );
1460 
1461  // Test with no template id's
1462  $stubPOEmpty = $this->createParserOutputStub();
1463  $op->addParserOutputMetadata( $stubPOEmpty );
1464  $this->assertSame( [], $op->getTemplateIds() );
1465 
1466  // Test with some arbitrary template id's
1467  $ids = [
1468  NS_MAIN => [ 'A' => 3, 'B' => 17 ],
1469  NS_TALK => [ 'C' => 31 ],
1470  NS_MEDIA => [ 'D' => -1 ],
1471  ];
1472 
1473  $stubPO1 = $this->createParserOutputStub( 'getTemplateIds', $ids );
1474 
1475  $op->addParserOutputMetadata( $stubPO1 );
1476  $this->assertSame( $ids, $op->getTemplateIds() );
1477 
1478  // Test merging with a second set of id's
1479  $stubPO2 = $this->createParserOutputStub( 'getTemplateIds', [
1480  NS_MAIN => [ 'E' => 1234 ],
1481  NS_PROJECT => [ 'F' => 5678 ],
1482  ] );
1483 
1484  $finalIds = [
1485  NS_MAIN => [ 'E' => 1234, 'A' => 3, 'B' => 17 ],
1486  NS_TALK => [ 'C' => 31 ],
1487  NS_MEDIA => [ 'D' => -1 ],
1488  NS_PROJECT => [ 'F' => 5678 ],
1489  ];
1490 
1491  $op->addParserOutput( $stubPO2 );
1492  $this->assertSame( $finalIds, $op->getTemplateIds() );
1493 
1494  // Test merging with an empty set of id's
1495  $op->addParserOutputMetadata( $stubPOEmpty );
1496  $this->assertSame( $finalIds, $op->getTemplateIds() );
1497  }
1498 
1504  public function testFileSearchOptions() {
1505  $op = $this->newInstance();
1506  $this->assertSame( [], $op->getFileSearchOptions() );
1507 
1508  // Test with no files
1509  $stubPOEmpty = $this->createParserOutputStub();
1510 
1511  $op->addParserOutputMetadata( $stubPOEmpty );
1512  $this->assertSame( [], $op->getFileSearchOptions() );
1513 
1514  // Test with some arbitrary files
1515  $files1 = [
1516  'A' => [ 'time' => null, 'sha1' => '' ],
1517  'B' => [
1518  'time' => '12211221123321',
1519  'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05',
1520  ],
1521  ];
1522 
1523  $stubPO1 = $this->createParserOutputStub( 'getFileSearchOptions', $files1 );
1524 
1525  $op->addParserOutput( $stubPO1 );
1526  $this->assertSame( $files1, $op->getFileSearchOptions() );
1527 
1528  // Test merging with a second set of files
1529  $files2 = [
1530  'C' => [ 'time' => null, 'sha1' => '' ],
1531  'B' => [ 'time' => null, 'sha1' => '' ],
1532  ];
1533 
1534  $stubPO2 = $this->createParserOutputStub( 'getFileSearchOptions', $files2 );
1535 
1536  $op->addParserOutputMetadata( $stubPO2 );
1537  $this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
1538 
1539  // Test merging with an empty set of files
1540  $op->addParserOutput( $stubPOEmpty );
1541  $this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
1542  }
1543 
1551  public function testAddWikiText( $method, array $args, $expected ) {
1552  $op = $this->newInstance();
1553  $this->assertSame( '', $op->getHTML() );
1554 
1555  if ( in_array(
1556  $method,
1557  [ 'addWikiTextAsInterface', 'addWikiTextAsContent' ]
1558  ) && count( $args ) >= 3 && $args[2] === null ) {
1559  // Special placeholder because we can't get the actual title in the provider
1560  $args[2] = $op->getTitle();
1561  }
1562 
1563  $op->$method( ...$args );
1564  $this->assertSame( $expected, $op->getHTML() );
1565  }
1566 
1567  public function provideAddWikiText() {
1568  $tests = [
1569  'addWikiTextAsInterface' => [
1570  'Simple wikitext' => [
1571  [ "'''Bold'''" ],
1572  "<p><b>Bold</b>\n</p>",
1573  ], 'Untidy wikitext' => [
1574  [ "<b>Bold" ],
1575  "<p><b>Bold\n</b></p>",
1576  ], 'List at start' => [
1577  [ '* List' ],
1578  "<ul><li>List</li></ul>\n",
1579  ], 'List not at start' => [
1580  [ '* Not a list', false ],
1581  '<p>* Not a list</p>',
1582  ], 'No section edit links' => [
1583  [ '== Title ==' ],
1584  "<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>",
1585  ], 'With title at start' => [
1586  [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1587  "<ul><li>Some page</li></ul>\n",
1588  ], 'With title at start' => [
1589  [ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
1590  "<p>* Some page</p>",
1591  ], 'Untidy input' => [
1592  [ '<b>{{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1593  "<p><b>Some page\n</b></p>",
1594  ],
1595  ],
1596  'addWikiTextAsContent' => [
1597  'SpecialNewimages' => [
1598  [ "<p lang='en' dir='ltr'>\nMy message" ],
1599  '<p lang="en" dir="ltr">' . "\nMy message</p>"
1600  ], 'List at start' => [
1601  [ '* List' ],
1602  "<ul><li>List</li></ul>",
1603  ], 'List not at start' => [
1604  [ '* <b>Not a list', false ],
1605  '<p>* <b>Not a list</b></p>',
1606  ], 'With title at start' => [
1607  [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1608  "<ul><li>Some page</li></ul>\n",
1609  ], 'With title at start' => [
1610  [ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
1611  "<p>* Some page</p>",
1612  ], 'EditPage' => [
1613  [ "<div class='mw-editintro'>{{PAGENAME}}", true, Title::newFromText( 'Talk:Some page' ) ],
1614  '<div class="mw-editintro">' . "Some page</div>"
1615  ],
1616  ],
1617  'wrapWikiTextAsInterface' => [
1618  'Simple' => [
1619  [ 'wrapperClass', 'text' ],
1620  "<div class=\"wrapperClass\"><p>text\n</p></div>"
1621  ], 'Spurious </div>' => [
1622  [ 'wrapperClass', 'text</div><div>more' ],
1623  "<div class=\"wrapperClass\"><p>text</p><div>more</div></div>"
1624  ], 'Extra newlines would break <p> wrappers' => [
1625  [ 'two classes', "1\n\n2\n\n3" ],
1626  "<div class=\"two classes\"><p>1\n</p><p>2\n</p><p>3\n</p></div>"
1627  ], 'Other unclosed tags' => [
1628  [ 'error', 'a<b>c<i>d' ],
1629  "<div class=\"error\"><p>a<b>c<i>d\n</i></b></p></div>"
1630  ],
1631  ],
1632  ];
1633 
1634  // We have to reformat our array to match what PHPUnit wants
1635  $ret = [];
1636  foreach ( $tests as $key => $subarray ) {
1637  foreach ( $subarray as $subkey => $val ) {
1638  $val = array_merge( [ $key ], $val );
1639  $ret[$subkey] = $val;
1640  }
1641  }
1642 
1643  return $ret;
1644  }
1645 
1650  $this->setExpectedException( MWException::class, 'Title is null' );
1651 
1652  $op = $this->newInstance( [], null, 'notitle' );
1653  $op->addWikiTextAsInterface( 'a' );
1654  }
1655 
1660  $this->setExpectedException( MWException::class, 'Title is null' );
1661 
1662  $op = $this->newInstance( [], null, 'notitle' );
1663  $op->addWikiTextAsContent( 'a' );
1664  }
1665 
1669  public function testAddWikiMsg() {
1670  $msg = wfMessage( 'parentheses' );
1671  $this->assertSame( '(a)', $msg->rawParams( 'a' )->plain() );
1672 
1673  $op = $this->newInstance();
1674  $this->assertSame( '', $op->getHTML() );
1675  $op->addWikiMsg( 'parentheses', "<b>a" );
1676  // The input is bad unbalanced HTML, but the output is tidied
1677  $this->assertSame( "<p>(<b>a)\n</b></p>", $op->getHTML() );
1678  }
1679 
1683  public function testWrapWikiMsg() {
1684  $msg = wfMessage( 'parentheses' );
1685  $this->assertSame( '(a)', $msg->rawParams( 'a' )->plain() );
1686 
1687  $op = $this->newInstance();
1688  $this->assertSame( '', $op->getHTML() );
1689  $op->wrapWikiMsg( '[$1]', [ 'parentheses', "<b>a" ] );
1690  // The input is bad unbalanced HTML, but the output is tidied
1691  $this->assertSame( "<p>[(<b>a)]\n</b></p>", $op->getHTML() );
1692  }
1693 
1698  public function testNoGallery() {
1699  $op = $this->newInstance();
1700  $this->assertFalse( $op->mNoGallery );
1701 
1702  $stubPO1 = $this->createParserOutputStub( 'getNoGallery', true );
1703  $op->addParserOutputMetadata( $stubPO1 );
1704  $this->assertTrue( $op->mNoGallery );
1705 
1706  $stubPO2 = $this->createParserOutputStub( 'getNoGallery', false );
1707  $op->addParserOutput( $stubPO2 );
1708  $this->assertFalse( $op->mNoGallery );
1709  }
1710 
1711  private static $parserOutputHookCalled;
1712 
1716  public function testParserOutputHooks() {
1717  $op = $this->newInstance();
1718  $pOut = $this->createParserOutputStub( 'getOutputHooks', [
1719  [ 'myhook', 'banana' ],
1720  [ 'yourhook', 'kumquat' ],
1721  [ 'theirhook', 'hippopotamus' ],
1722  ] );
1723 
1724  self::$parserOutputHookCalled = [];
1725 
1726  $this->setMwGlobals( 'wgParserOutputHooks', [
1727  'myhook' => function ( OutputPage $innerOp, ParserOutput $innerPOut, $data )
1728  use ( $op, $pOut ) {
1729  $this->assertSame( $op, $innerOp );
1730  $this->assertSame( $pOut, $innerPOut );
1731  $this->assertSame( 'banana', $data );
1732  self::$parserOutputHookCalled[] = 'closure';
1733  },
1734  'yourhook' => [ $this, 'parserOutputHookCallback' ],
1735  'theirhook' => [ __CLASS__, 'parserOutputHookCallbackStatic' ],
1736  'uncalled' => function () {
1737  $this->assertTrue( false );
1738  },
1739  ] );
1740 
1741  $op->addParserOutputMetadata( $pOut );
1742 
1743  $this->assertSame( [ 'closure', 'callback', 'static' ], self::$parserOutputHookCalled );
1744  }
1745 
1746  public function parserOutputHookCallback(
1747  OutputPage $op, ParserOutput $pOut, $data
1748  ) {
1749  $this->assertSame( 'kumquat', $data );
1750 
1751  self::$parserOutputHookCalled[] = 'callback';
1752  }
1753 
1754  public static function parserOutputHookCallbackStatic(
1755  OutputPage $op, ParserOutput $pOut, $data
1756  ) {
1757  // All the assert methods are actually static, who knew!
1758  self::assertSame( 'hippopotamus', $data );
1759 
1760  self::$parserOutputHookCalled[] = 'static';
1761  }
1762 
1763  // @todo Make sure to test the following in addParserOutputMetadata() as well when we add tests
1764  // for them:
1765  // * addModules()
1766  // * addModuleStyles()
1767  // * addJsConfigVars()
1768  // * enableOOUI()
1769  // Otherwise those lines of addParserOutputMetadata() will be reported as covered, but we won't
1770  // be testing they actually work.
1771 
1775  public function testAddParserOutputText() {
1776  $op = $this->newInstance();
1777  $this->assertSame( '', $op->getHTML() );
1778 
1779  $pOut = $this->createParserOutputStub( 'getText', '<some text>' );
1780 
1781  $op->addParserOutputMetadata( $pOut );
1782  $this->assertSame( '', $op->getHTML() );
1783 
1784  $op->addParserOutputText( $pOut );
1785  $this->assertSame( '<some text>', $op->getHTML() );
1786  }
1787 
1791  public function testAddParserOutput() {
1792  $op = $this->newInstance();
1793  $this->assertSame( '', $op->getHTML() );
1794  $this->assertFalse( $op->showNewSectionLink() );
1795 
1796  $pOut = $this->createParserOutputStub( [
1797  'getText' => '<some text>',
1798  'getNewSection' => true,
1799  ] );
1800 
1801  $op->addParserOutput( $pOut );
1802  $this->assertSame( '<some text>', $op->getHTML() );
1803  $this->assertTrue( $op->showNewSectionLink() );
1804  }
1805 
1809  public function testAddTemplate() {
1810  $template = $this->getMock( QuickTemplate::class );
1811  $template->method( 'getHTML' )->willReturn( '<abc>&def;' );
1812 
1813  $op = $this->newInstance();
1814  $op->addTemplate( $template );
1815 
1816  $this->assertSame( '<abc>&def;', $op->getHTML() );
1817  }
1818 
1826  public function testParse( array $args, $expectedHTML ) {
1827  $this->hideDeprecated( 'OutputPage::parse' );
1828  $op = $this->newInstance();
1829  $this->assertSame( $expectedHTML, $op->parse( ...$args ) );
1830  }
1831 
1836  public function testParseInline( array $args, $expectedHTML, $expectedHTMLInline = null ) {
1837  if ( count( $args ) > 3 ) {
1838  // $language param not supported
1839  $this->assertTrue( true );
1840  return;
1841  }
1842  $this->hideDeprecated( 'OutputPage::parseInline' );
1843  $op = $this->newInstance();
1844  $this->assertSame( $expectedHTMLInline ?? $expectedHTML, $op->parseInline( ...$args ) );
1845  }
1846 
1847  public function provideParse() {
1848  return [
1849  'List at start of line (content)' => [
1850  [ '* List', true, false ],
1851  "<div class=\"mw-parser-output\"><ul><li>List</li></ul></div>",
1852  "<ul><li>List</li></ul>",
1853  ],
1854  'List at start of line (interface)' => [
1855  [ '* List', true, true ],
1856  "<ul><li>List</li></ul>",
1857  ],
1858  'List not at start (content)' => [
1859  [ "* ''Not'' list", false, false ],
1860  '<div class="mw-parser-output">* <i>Not</i> list</div>',
1861  '* <i>Not</i> list',
1862  ],
1863  'List not at start (interface)' => [
1864  [ "* ''Not'' list", false, true ],
1865  '* <i>Not</i> list',
1866  ],
1867  'Interface message' => [
1868  [ "''Italic''", true, true ],
1869  "<p><i>Italic</i>\n</p>",
1870  '<i>Italic</i>',
1871  ],
1872  'formatnum (content)' => [
1873  [ '{{formatnum:123456.789}}', true, false ],
1874  "<div class=\"mw-parser-output\"><p>123,456.789\n</p></div>",
1875  "123,456.789",
1876  ],
1877  'formatnum (interface)' => [
1878  [ '{{formatnum:123456.789}}', true, true ],
1879  "<p>123,456.789\n</p>",
1880  "123,456.789",
1881  ],
1882  'Language (content)' => [
1883  [ '{{formatnum:123456.789}}', true, false, Language::factory( 'is' ) ],
1884  "<div class=\"mw-parser-output\"><p>123.456,789\n</p></div>",
1885  ],
1886  'Language (interface)' => [
1887  [ '{{formatnum:123456.789}}', true, true, Language::factory( 'is' ) ],
1888  "<p>123.456,789\n</p>",
1889  '123.456,789',
1890  ],
1891  'No section edit links' => [
1892  [ '== Header ==' ],
1893  '<div class="mw-parser-output"><h2><span class="mw-headline" id="Header">' .
1894  "Header</span></h2></div>",
1895  '<h2><span class="mw-headline" id="Header">Header</span></h2>',
1896  ]
1897  ];
1898  }
1899 
1907  public function testParseAsContent(
1908  array $args, $expectedHTML, $expectedHTMLInline = null
1909  ) {
1910  $op = $this->newInstance();
1911  $this->assertSame( $expectedHTML, $op->parseAsContent( ...$args ) );
1912  }
1913 
1921  public function testParseAsInterface(
1922  array $args, $expectedHTML, $expectedHTMLInline = null
1923  ) {
1924  $op = $this->newInstance();
1925  $this->assertSame( $expectedHTML, $op->parseAsInterface( ...$args ) );
1926  }
1927 
1933  array $args, $expectedHTML, $expectedHTMLInline = null
1934  ) {
1935  $op = $this->newInstance();
1936  $this->assertSame(
1937  $expectedHTMLInline ?? $expectedHTML,
1938  $op->parseInlineAsInterface( ...$args )
1939  );
1940  }
1941 
1942  public function provideParseAs() {
1943  return [
1944  'List at start of line' => [
1945  [ '* List', true ],
1946  "<ul><li>List</li></ul>",
1947  ],
1948  'List not at start' => [
1949  [ "* ''Not'' list", false ],
1950  '<p>* <i>Not</i> list</p>',
1951  '* <i>Not</i> list',
1952  ],
1953  'Italics' => [
1954  [ "''Italic''", true ],
1955  "<p><i>Italic</i>\n</p>",
1956  '<i>Italic</i>',
1957  ],
1958  'formatnum' => [
1959  [ '{{formatnum:123456.789}}', true ],
1960  "<p>123,456.789\n</p>",
1961  "123,456.789",
1962  ],
1963  'No section edit links' => [
1964  [ '== Header ==' ],
1965  '<h2><span class="mw-headline" id="Header">Header</span></h2>',
1966  ]
1967  ];
1968  }
1969 
1973  public function testParseNullTitle() {
1974  $this->hideDeprecated( 'OutputPage::parse' );
1975  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
1976  $op = $this->newInstance( [], null, 'notitle' );
1977  $op->parse( '' );
1978  }
1979 
1983  public function testParseInlineNullTitle() {
1984  $this->hideDeprecated( 'OutputPage::parseInline' );
1985  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
1986  $op = $this->newInstance( [], null, 'notitle' );
1987  $op->parseInline( '' );
1988  }
1989 
1993  public function testParseAsContentNullTitle() {
1994  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
1995  $op = $this->newInstance( [], null, 'notitle' );
1996  $op->parseAsContent( '' );
1997  }
1998 
2002  public function testParseAsInterfaceNullTitle() {
2003  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2004  $op = $this->newInstance( [], null, 'notitle' );
2005  $op->parseAsInterface( '' );
2006  }
2007 
2012  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
2013  $op = $this->newInstance( [], null, 'notitle' );
2014  $op->parseInlineAsInterface( '' );
2015  }
2016 
2021  public function testCdnMaxage() {
2022  $op = $this->newInstance();
2023  $wrapper = TestingAccessWrapper::newFromObject( $op );
2024  $this->assertSame( 0, $wrapper->mCdnMaxage );
2025 
2026  $op->setCdnMaxage( -1 );
2027  $this->assertSame( -1, $wrapper->mCdnMaxage );
2028 
2029  $op->setCdnMaxage( 120 );
2030  $this->assertSame( 120, $wrapper->mCdnMaxage );
2031 
2032  $op->setCdnMaxage( 60 );
2033  $this->assertSame( 60, $wrapper->mCdnMaxage );
2034 
2035  $op->setCdnMaxage( 180 );
2036  $this->assertSame( 180, $wrapper->mCdnMaxage );
2037 
2038  $op->lowerCdnMaxage( 240 );
2039  $this->assertSame( 180, $wrapper->mCdnMaxage );
2040 
2041  $op->setCdnMaxage( 300 );
2042  $this->assertSame( 240, $wrapper->mCdnMaxage );
2043 
2044  $op->lowerCdnMaxage( 120 );
2045  $this->assertSame( 120, $wrapper->mCdnMaxage );
2046 
2047  $op->setCdnMaxage( 180 );
2048  $this->assertSame( 120, $wrapper->mCdnMaxage );
2049 
2050  $op->setCdnMaxage( 60 );
2051  $this->assertSame( 60, $wrapper->mCdnMaxage );
2052 
2053  $op->setCdnMaxage( 240 );
2054  $this->assertSame( 120, $wrapper->mCdnMaxage );
2055  }
2056 
2058  private static $fakeTime;
2059 
2068  public function testAdaptCdnTTL( array $args, $expected, array $options = [] ) {
2069  try {
2070  MWTimestamp::setFakeTime( self::$fakeTime );
2071 
2072  $op = $this->newInstance();
2073  // Set a high maxage so that it will get reduced by adaptCdnTTL(). The default maxage
2074  // is 0, so adaptCdnTTL() won't mutate the object at all.
2075  $initial = $options['initialMaxage'] ?? 86400;
2076  $op->setCdnMaxage( $initial );
2077 
2078  $op->adaptCdnTTL( ...$args );
2079  } finally {
2080  MWTimestamp::setFakeTime( false );
2081  }
2082 
2083  $wrapper = TestingAccessWrapper::newFromObject( $op );
2084 
2085  // Special rules for false/null
2086  if ( $args[0] === null || $args[0] === false ) {
2087  $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
2088  $op->setCdnMaxage( $expected + 1 );
2089  $this->assertSame( $expected + 1, $wrapper->mCdnMaxage, 'member value after new set' );
2090  return;
2091  }
2092 
2093  $this->assertSame( $expected, $wrapper->mCdnMaxageLimit, 'limit value' );
2094 
2095  if ( $initial >= $expected ) {
2096  $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value' );
2097  } else {
2098  $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
2099  }
2100 
2101  $op->setCdnMaxage( $expected + 1 );
2102  $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value after new set' );
2103  }
2104 
2105  public function provideAdaptCdnTTL() {
2106  global $wgCdnMaxAge;
2107  $now = time();
2108  self::$fakeTime = $now;
2109  return [
2110  'Five minutes ago' => [ [ $now - 300 ], 270 ],
2111  'Now' => [ [ +0 ], IExpiringStore::TTL_MINUTE ],
2112  'Five minutes from now' => [ [ $now + 300 ], IExpiringStore::TTL_MINUTE ],
2113  'Five minutes ago, initial maxage four minutes' =>
2114  [ [ $now - 300 ], 270, [ 'initialMaxage' => 240 ] ],
2115  'A very long time ago' => [ [ $now - 1000000000 ], $wgCdnMaxAge ],
2116  'Initial maxage zero' => [ [ $now - 300 ], 270, [ 'initialMaxage' => 0 ] ],
2117 
2118  'false' => [ [ false ], IExpiringStore::TTL_MINUTE ],
2119  'null' => [ [ null ], IExpiringStore::TTL_MINUTE ],
2120  "'0'" => [ [ '0' ], IExpiringStore::TTL_MINUTE ],
2121  'Empty string' => [ [ '' ], IExpiringStore::TTL_MINUTE ],
2122  // @todo These give incorrect results due to timezones, how to test?
2123  //"'now'" => [ [ 'now' ], IExpiringStore::TTL_MINUTE ],
2124  //"'parse error'" => [ [ 'parse error' ], IExpiringStore::TTL_MINUTE ],
2125 
2126  'Now, minTTL 0' => [ [ $now, 0 ], IExpiringStore::TTL_MINUTE ],
2127  'Now, minTTL 0.000001' => [ [ $now, 0.000001 ], 0 ],
2128  'A very long time ago, maxTTL even longer' =>
2129  [ [ $now - 1000000000, 0, 1000000001 ], 900000000 ],
2130  ];
2131  }
2132 
2138  public function testClientCache() {
2139  $op = $this->newInstance();
2140 
2141  // Test initial value
2142  $this->assertSame( true, $op->enableClientCache( null ) );
2143  // Test that calling with null doesn't change the value
2144  $this->assertSame( true, $op->enableClientCache( null ) );
2145 
2146  // Test setting to false
2147  $this->assertSame( true, $op->enableClientCache( false ) );
2148  $this->assertSame( false, $op->enableClientCache( null ) );
2149  // Test that calling with null doesn't change the value
2150  $this->assertSame( false, $op->enableClientCache( null ) );
2151 
2152  // Test that a cacheable ParserOutput doesn't set to true
2153  $pOutCacheable = $this->createParserOutputStub( 'isCacheable', true );
2154  $op->addParserOutputMetadata( $pOutCacheable );
2155  $this->assertSame( false, $op->enableClientCache( null ) );
2156 
2157  // Test setting back to true
2158  $this->assertSame( false, $op->enableClientCache( true ) );
2159  $this->assertSame( true, $op->enableClientCache( null ) );
2160 
2161  // Test that an uncacheable ParserOutput does set to false
2162  $pOutUncacheable = $this->createParserOutputStub( 'isCacheable', false );
2163  $op->addParserOutput( $pOutUncacheable );
2164  $this->assertSame( false, $op->enableClientCache( null ) );
2165  }
2166 
2170  public function testGetCacheVaryCookies() {
2171  global $wgCookiePrefix, $wgDBname;
2172  $op = $this->newInstance();
2173  $prefix = $wgCookiePrefix !== false ? $wgCookiePrefix : $wgDBname;
2174  $expectedCookies = [
2175  "{$prefix}Token",
2176  "{$prefix}LoggedOut",
2177  "{$prefix}_session",
2178  'forceHTTPS',
2179  'cookie1',
2180  'cookie2',
2181  ];
2182 
2183  // We have to reset the cookies because getCacheVaryCookies may have already been called
2184  TestingAccessWrapper::newFromClass( OutputPage::class )->cacheVaryCookies = null;
2185 
2186  $this->setMwGlobals( 'wgCacheVaryCookies', [ 'cookie1' ] );
2187  $this->setTemporaryHook( 'GetCacheVaryCookies',
2188  function ( $innerOP, &$cookies ) use ( $op, $expectedCookies ) {
2189  $this->assertSame( $op, $innerOP );
2190  $cookies[] = 'cookie2';
2191  $this->assertSame( $expectedCookies, $cookies );
2192  }
2193  );
2194 
2195  $this->assertSame( $expectedCookies, $op->getCacheVaryCookies() );
2196  }
2197 
2201  public function testHaveCacheVaryCookies() {
2202  $request = new FauxRequest();
2203  $op = $this->newInstance( [], $request );
2204 
2205  // No cookies are set.
2206  $this->assertFalse( $op->haveCacheVaryCookies() );
2207 
2208  // 'Token' is present but empty, so it shouldn't count.
2209  $request->setCookie( 'Token', '' );
2210  $this->assertFalse( $op->haveCacheVaryCookies() );
2211 
2212  // 'Token' present and nonempty.
2213  $request->setCookie( 'Token', '123' );
2214  $this->assertTrue( $op->haveCacheVaryCookies() );
2215  }
2216 
2227  public function testVaryHeaders( array $calls, array $cookies, $vary ) {
2228  // Get rid of default Vary fields
2229  $op = $this->getMockBuilder( OutputPage::class )
2230  ->setConstructorArgs( [ new RequestContext() ] )
2231  ->setMethods( [ 'getCacheVaryCookies' ] )
2232  ->getMock();
2233  $op->expects( $this->any() )
2234  ->method( 'getCacheVaryCookies' )
2235  ->will( $this->returnValue( $cookies ) );
2236  TestingAccessWrapper::newFromObject( $op )->mVaryHeader = [];
2237 
2238  $this->hideDeprecated( 'addVaryHeader $option is ignored' );
2239  foreach ( $calls as $call ) {
2240  $op->addVaryHeader( ...$call );
2241  }
2242  $this->assertEquals( $vary, $op->getVaryHeader(), 'Vary:' );
2243  }
2244 
2245  public function provideVaryHeaders() {
2246  return [
2247  'No header' => [
2248  [],
2249  [],
2250  'Vary: ',
2251  ],
2252  'Single header' => [
2253  [
2254  [ 'Cookie' ],
2255  ],
2256  [],
2257  'Vary: Cookie',
2258  ],
2259  'Non-unique headers' => [
2260  [
2261  [ 'Cookie' ],
2262  [ 'Accept-Language' ],
2263  [ 'Cookie' ],
2264  ],
2265  [],
2266  'Vary: Cookie, Accept-Language',
2267  ],
2268  'Two headers with single options' => [
2269  // Options are deprecated since 1.34
2270  [
2271  [ 'Cookie', [ 'param=phpsessid' ] ],
2272  [ 'Accept-Language', [ 'substr=en' ] ],
2273  ],
2274  [],
2275  'Vary: Cookie, Accept-Language',
2276  ],
2277  'One header with multiple options' => [
2278  // Options are deprecated since 1.34
2279  [
2280  [ 'Cookie', [ 'param=phpsessid', 'param=userId' ] ],
2281  ],
2282  [],
2283  'Vary: Cookie',
2284  ],
2285  'Duplicate option' => [
2286  // Options are deprecated since 1.34
2287  [
2288  [ 'Cookie', [ 'param=phpsessid' ] ],
2289  [ 'Cookie', [ 'param=phpsessid' ] ],
2290  [ 'Accept-Language', [ 'substr=en', 'substr=en' ] ],
2291  ],
2292  [],
2293  'Vary: Cookie, Accept-Language',
2294  ],
2295  'Same header, different options' => [
2296  // Options are deprecated since 1.34
2297  [
2298  [ 'Cookie', [ 'param=phpsessid' ] ],
2299  [ 'Cookie', [ 'param=userId' ] ],
2300  ],
2301  [],
2302  'Vary: Cookie',
2303  ],
2304  'No header, vary cookies' => [
2305  [],
2306  [ 'cookie1', 'cookie2' ],
2307  'Vary: Cookie',
2308  ],
2309  'Cookie header with option plus vary cookies' => [
2310  // Options are deprecated since 1.34
2311  [
2312  [ 'Cookie', [ 'param=cookie1' ] ],
2313  ],
2314  [ 'cookie2', 'cookie3' ],
2315  'Vary: Cookie',
2316  ],
2317  'Non-cookie header plus vary cookies' => [
2318  [
2319  [ 'Accept-Language' ],
2320  ],
2321  [ 'cookie' ],
2322  'Vary: Accept-Language, Cookie',
2323  ],
2324  'Cookie and non-cookie headers plus vary cookies' => [
2325  // Options are deprecated since 1.34
2326  [
2327  [ 'Cookie', [ 'param=cookie1' ] ],
2328  [ 'Accept-Language' ],
2329  ],
2330  [ 'cookie2' ],
2331  'Vary: Cookie, Accept-Language',
2332  ],
2333  ];
2334  }
2335 
2339  public function testVaryHeaderDefault() {
2340  $op = $this->newInstance();
2341  $this->assertSame( 'Vary: Accept-Encoding, Cookie', $op->getVaryHeader() );
2342  }
2343 
2350  public function testLinkHeaders( array $headers, $result ) {
2351  $op = $this->newInstance();
2352 
2353  foreach ( $headers as $header ) {
2354  $op->addLinkHeader( $header );
2355  }
2356 
2357  $this->assertEquals( $result, $op->getLinkHeader() );
2358  }
2359 
2360  public function provideLinkHeaders() {
2361  return [
2362  [
2363  [],
2364  false
2365  ],
2366  [
2367  [ '<https://foo/bar.jpg>;rel=preload;as=image' ],
2368  'Link: <https://foo/bar.jpg>;rel=preload;as=image',
2369  ],
2370  [
2371  [
2372  '<https://foo/bar.jpg>;rel=preload;as=image',
2373  '<https://foo/baz.jpg>;rel=preload;as=image'
2374  ],
2375  'Link: <https://foo/bar.jpg>;rel=preload;as=image,<https://foo/baz.jpg>;' .
2376  'rel=preload;as=image',
2377  ],
2378  ];
2379  }
2380 
2385  public function testAddAcceptLanguage(
2386  $code, array $variants, $expected, array $options = []
2387  ) {
2388  $req = new FauxRequest( in_array( 'varianturl', $options ) ? [ 'variant' => 'x' ] : [] );
2389  $op = $this->newInstance( [], $req, in_array( 'notitle', $options ) ? 'notitle' : null );
2390 
2391  if ( !in_array( 'notitle', $options ) ) {
2392  $mockLang = $this->getMock( Language::class );
2393 
2394  if ( in_array( 'varianturl', $options ) ) {
2395  $mockLang->expects( $this->never() )->method( $this->anything() );
2396  } else {
2397  $mockLang->method( 'hasVariants' )->willReturn( count( $variants ) > 1 );
2398  $mockLang->method( 'getVariants' )->willReturn( $variants );
2399  $mockLang->method( 'getCode' )->willReturn( $code );
2400  }
2401 
2402  $mockTitle = $this->getMock( Title::class );
2403  $mockTitle->method( 'getPageLanguage' )->willReturn( $mockLang );
2404 
2405  $op->setTitle( $mockTitle );
2406  }
2407 
2408  // This will run addAcceptLanguage()
2409  $op->sendCacheControl();
2410  $this->assertSame( "Vary: $expected", $op->getVaryHeader() );
2411  }
2412 
2413  public function provideAddAcceptLanguage() {
2414  return [
2415  'No variants' => [
2416  'en',
2417  [ 'en' ],
2418  'Accept-Encoding, Cookie',
2419  ],
2420  'One simple variant' => [
2421  'en',
2422  [ 'en', 'en-x-piglatin' ],
2423  'Accept-Encoding, Cookie, Accept-Language',
2424  ],
2425  'Multiple variants with BCP47 alternatives' => [
2426  'zh',
2427  [ 'zh', 'zh-hans', 'zh-cn', 'zh-tw' ],
2428  'Accept-Encoding, Cookie, Accept-Language',
2429  ],
2430  'No title' => [
2431  'en',
2432  [ 'en', 'en-x-piglatin' ],
2433  'Accept-Encoding, Cookie',
2434  [ 'notitle' ]
2435  ],
2436  'Variant in URL' => [
2437  'en',
2438  [ 'en', 'en-x-piglatin' ],
2439  'Accept-Encoding, Cookie',
2440  [ 'varianturl' ]
2441  ],
2442  ];
2443  }
2444 
2452  public function testClickjacking() {
2453  $op = $this->newInstance();
2454  $this->assertTrue( $op->getPreventClickjacking() );
2455 
2456  $op->allowClickjacking();
2457  $this->assertFalse( $op->getPreventClickjacking() );
2458 
2459  $op->preventClickjacking();
2460  $this->assertTrue( $op->getPreventClickjacking() );
2461 
2462  $op->preventClickjacking( false );
2463  $this->assertFalse( $op->getPreventClickjacking() );
2464 
2465  $pOut1 = $this->createParserOutputStub( 'preventClickjacking', true );
2466  $op->addParserOutputMetadata( $pOut1 );
2467  $this->assertTrue( $op->getPreventClickjacking() );
2468 
2469  // The ParserOutput can't allow, only prevent
2470  $pOut2 = $this->createParserOutputStub( 'preventClickjacking', false );
2471  $op->addParserOutputMetadata( $pOut2 );
2472  $this->assertTrue( $op->getPreventClickjacking() );
2473 
2474  // Reset to test with addParserOutput()
2475  $op->allowClickjacking();
2476  $this->assertFalse( $op->getPreventClickjacking() );
2477 
2478  $op->addParserOutput( $pOut1 );
2479  $this->assertTrue( $op->getPreventClickjacking() );
2480 
2481  $op->addParserOutput( $pOut2 );
2482  $this->assertTrue( $op->getPreventClickjacking() );
2483  }
2484 
2490  public function testGetFrameOptions(
2491  $breakFrames, $preventClickjacking, $editPageFrameOptions, $expected
2492  ) {
2493  $op = $this->newInstance( [
2494  'BreakFrames' => $breakFrames,
2495  'EditPageFrameOptions' => $editPageFrameOptions,
2496  ] );
2497  $op->preventClickjacking( $preventClickjacking );
2498 
2499  $this->assertSame( $expected, $op->getFrameOptions() );
2500  }
2501 
2502  public function provideGetFrameOptions() {
2503  return [
2504  'BreakFrames true' => [ true, false, false, 'DENY' ],
2505  'Allow clickjacking locally' => [ false, false, 'DENY', false ],
2506  'Allow clickjacking globally' => [ false, true, false, false ],
2507  'DENY globally' => [ false, true, 'DENY', 'DENY' ],
2508  'SAMEORIGIN' => [ false, true, 'SAMEORIGIN', 'SAMEORIGIN' ],
2509  'BreakFrames with SAMEORIGIN' => [ true, true, 'SAMEORIGIN', 'DENY' ],
2510  ];
2511  }
2512 
2520  public function testMakeResourceLoaderLink( $args, $expectedHtml ) {
2521  $this->setMwGlobals( [
2522  'wgResourceLoaderDebug' => false,
2523  'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
2524  'wgCSPReportOnlyHeader' => true,
2525  ] );
2526  $class = new ReflectionClass( OutputPage::class );
2527  $method = $class->getMethod( 'makeResourceLoaderLink' );
2528  $method->setAccessible( true );
2529  $ctx = new RequestContext();
2530  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
2531  $ctx->setSkin( $skinFactory->makeSkin( 'fallback' ) );
2532  $ctx->setLanguage( 'en' );
2533  $out = new OutputPage( $ctx );
2534  $nonce = $class->getProperty( 'CSPNonce' );
2535  $nonce->setAccessible( true );
2536  $nonce->setValue( $out, 'secret' );
2537  $rl = $out->getResourceLoader();
2538  $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
2539  $rl->register( [
2540  'test.foo' => [
2542  'script' => 'mw.test.foo( { a: true } );',
2543  'styles' => '.mw-test-foo { content: "style"; }',
2544  ],
2545  'test.bar' => [
2547  'script' => 'mw.test.bar( { a: true } );',
2548  'styles' => '.mw-test-bar { content: "style"; }',
2549  ],
2550  'test.baz' => [
2552  'script' => 'mw.test.baz( { a: true } );',
2553  'styles' => '.mw-test-baz { content: "style"; }',
2554  ],
2555  'test.quux' => [
2557  'script' => 'mw.test.baz( { token: 123 } );',
2558  'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
2559  'group' => 'private',
2560  ],
2561  'test.noscript' => [
2563  'styles' => '.stuff { color: red; }',
2564  'group' => 'noscript',
2565  ],
2566  'test.group.foo' => [
2568  'script' => 'mw.doStuff( "foo" );',
2569  'group' => 'foo',
2570  ],
2571  'test.group.bar' => [
2573  'script' => 'mw.doStuff( "bar" );',
2574  'group' => 'bar',
2575  ],
2576  ] );
2577  $links = $method->invokeArgs( $out, $args );
2578  $actualHtml = strval( $links );
2579  $this->assertEquals( $expectedHtml, $actualHtml );
2580  }
2581 
2582  public static function provideMakeResourceLoaderLink() {
2583  // phpcs:disable Generic.Files.LineLength
2584  return [
2585  // Single only=scripts load
2586  [
2587  [ 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ],
2588  "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
2589  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.foo\u0026only=scripts");'
2590  . "});</script>"
2591  ],
2592  // Multiple only=styles load
2593  [
2594  [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
2595 
2596  '<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles"/>'
2597  ],
2598  // Private embed (only=scripts)
2599  [
2600  [ 'test.quux', ResourceLoaderModule::TYPE_SCRIPTS ],
2601  "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
2602  . "mw.test.baz({token:123});\nmw.loader.state({\"test.quux\":\"ready\"});"
2603  . "});</script>"
2604  ],
2605  // Load private module (combined)
2606  [
2607  [ 'test.quux', ResourceLoaderModule::TYPE_COMBINED ],
2608  "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
2609  . "mw.loader.implement(\"test.quux@1ev0ijv\",function($,jQuery,require,module){"
2610  . "mw.test.baz({token:123});},{\"css\":[\".mw-icon{transition:none}"
2611  . "\"]});});</script>"
2612  ],
2613  // Load no modules
2614  [
2615  [ [], ResourceLoaderModule::TYPE_COMBINED ],
2616  '',
2617  ],
2618  // noscript group
2619  [
2620  [ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ],
2621  '<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&amp;modules=test.noscript&amp;only=styles"/></noscript>'
2622  ],
2623  // Load two modules in separate groups
2624  [
2625  [ [ 'test.group.foo', 'test.group.bar' ], ResourceLoaderModule::TYPE_COMBINED ],
2626  "<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
2627  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.bar");'
2628  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.foo");'
2629  . "});</script>"
2630  ],
2631  ];
2632  // phpcs:enable
2633  }
2634 
2640  public function testBuildExemptModules( array $exemptStyleModules, $expect ) {
2641  $this->setMwGlobals( [
2642  'wgResourceLoaderDebug' => false,
2643  'wgLoadScript' => '/w/load.php',
2644  // Stub wgCacheEpoch as it influences getVersionHash used for the
2645  // urls in the expected HTML
2646  'wgCacheEpoch' => '20140101000000',
2647  ] );
2648 
2649  // Set up stubs
2650  $ctx = new RequestContext();
2651  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
2652  $ctx->setSkin( $skinFactory->makeSkin( 'fallback' ) );
2653  $ctx->setLanguage( 'en' );
2654  $op = $this->getMockBuilder( OutputPage::class )
2655  ->setConstructorArgs( [ $ctx ] )
2656  ->setMethods( [ 'buildCssLinksArray' ] )
2657  ->getMock();
2658  $op->method( 'buildCssLinksArray' )
2659  ->willReturn( [] );
2660  $rl = $op->getResourceLoader();
2661  $rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
2662 
2663  // Register custom modules
2664  $rl->register( [
2665  'example.site.a' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'site' ],
2666  'example.site.b' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'site' ],
2667  'example.user' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'user' ],
2668  ] );
2669 
2670  $op = TestingAccessWrapper::newFromObject( $op );
2671  $op->rlExemptStyleModules = $exemptStyleModules;
2672  $this->assertEquals(
2673  $expect,
2674  strval( $op->buildExemptModules() )
2675  );
2676  }
2677 
2678  public static function provideBuildExemptModules() {
2679  // phpcs:disable Generic.Files.LineLength
2680  return [
2681  'empty' => [
2682  'exemptStyleModules' => [],
2683  '',
2684  ],
2685  'empty sets' => [
2686  'exemptStyleModules' => [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ],
2687  '',
2688  ],
2689  'default logged-out' => [
2690  'exemptStyleModules' => [ 'site' => [ 'site.styles' ] ],
2691  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2692  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles"/>',
2693  ],
2694  'default logged-in' => [
2695  'exemptStyleModules' => [ 'site' => [ 'site.styles' ], 'user' => [ 'user.styles' ] ],
2696  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2697  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles"/>' . "\n" .
2698  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;version=1ai9g6t"/>',
2699  ],
2700  'custom modules' => [
2701  'exemptStyleModules' => [
2702  'site' => [ 'site.styles', 'example.site.a', 'example.site.b' ],
2703  'user' => [ 'user.styles', 'example.user' ],
2704  ],
2705  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2706  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.site.a%2Cb&amp;only=styles"/>' . "\n" .
2707  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=site.styles&amp;only=styles"/>' . "\n" .
2708  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=example.user&amp;only=styles&amp;version=0a56zyi"/>' . "\n" .
2709  '<link rel="stylesheet" href="/w/load.php?lang=en&amp;modules=user.styles&amp;only=styles&amp;version=1ai9g6t"/>',
2710  ],
2711  ];
2712  // phpcs:enable
2713  }
2714 
2720  public function testTransformResourcePath( $baseDir, $basePath, $uploadDir = null,
2721  $uploadPath = null, $path = null, $expected = null
2722  ) {
2723  if ( $path === null ) {
2724  // Skip optional $uploadDir and $uploadPath
2725  $path = $uploadDir;
2726  $expected = $uploadPath;
2727  $uploadDir = "$baseDir/images";
2728  $uploadPath = "$basePath/images";
2729  }
2730  $this->setMwGlobals( 'IP', $baseDir );
2731  $conf = new HashConfig( [
2732  'ResourceBasePath' => $basePath,
2733  'UploadDirectory' => $uploadDir,
2734  'UploadPath' => $uploadPath,
2735  ] );
2736 
2737  // Some of these paths don't exist and will cause warnings
2738  Wikimedia\suppressWarnings();
2739  $actual = OutputPage::transformResourcePath( $conf, $path );
2740  Wikimedia\restoreWarnings();
2741 
2742  $this->assertEquals( $expected ?: $path, $actual );
2743  }
2744 
2745  public static function provideTransformFilePath() {
2746  $baseDir = dirname( __DIR__ ) . '/data/media';
2747  return [
2748  // File that matches basePath, and exists. Hash found and appended.
2749  [
2750  'baseDir' => $baseDir, 'basePath' => '/w',
2751  '/w/test.jpg',
2752  '/w/test.jpg?edcf2'
2753  ],
2754  // File that matches basePath, but not found on disk. Empty query.
2755  [
2756  'baseDir' => $baseDir, 'basePath' => '/w',
2757  '/w/unknown.png',
2758  '/w/unknown.png?'
2759  ],
2760  // File not matching basePath. Ignored.
2761  [
2762  'baseDir' => $baseDir, 'basePath' => '/w',
2763  '/files/test.jpg'
2764  ],
2765  // Empty string. Ignored.
2766  [
2767  'baseDir' => $baseDir, 'basePath' => '/w',
2768  '',
2769  ''
2770  ],
2771  // Similar path, but with domain component. Ignored.
2772  [
2773  'baseDir' => $baseDir, 'basePath' => '/w',
2774  '//example.org/w/test.jpg'
2775  ],
2776  [
2777  'baseDir' => $baseDir, 'basePath' => '/w',
2778  'https://example.org/w/test.jpg'
2779  ],
2780  // Unrelated path with domain component. Ignored.
2781  [
2782  'baseDir' => $baseDir, 'basePath' => '/w',
2783  'https://example.org/files/test.jpg'
2784  ],
2785  [
2786  'baseDir' => $baseDir, 'basePath' => '/w',
2787  '//example.org/files/test.jpg'
2788  ],
2789  // Unrelated path with domain, and empty base path (root mw install). Ignored.
2790  [
2791  'baseDir' => $baseDir, 'basePath' => '',
2792  'https://example.org/files/test.jpg'
2793  ],
2794  [
2795  'baseDir' => $baseDir, 'basePath' => '',
2796  // T155310
2797  '//example.org/files/test.jpg'
2798  ],
2799  // Check UploadPath before ResourceBasePath (T155146)
2800  [
2801  'baseDir' => dirname( $baseDir ), 'basePath' => '',
2802  'uploadDir' => $baseDir, 'uploadPath' => '/images',
2803  '/images/test.jpg',
2804  '/images/test.jpg?edcf2'
2805  ],
2806  ];
2807  }
2808 
2823  protected function assertTransformCssMediaCase( $args ) {
2824  $queryData = [];
2825  if ( isset( $args['printableQuery'] ) ) {
2826  $queryData['printable'] = $args['printableQuery'];
2827  }
2828 
2829  if ( isset( $args['handheldQuery'] ) ) {
2830  $queryData['handheld'] = $args['handheldQuery'];
2831  }
2832 
2833  $fauxRequest = new FauxRequest( $queryData, false );
2834  $this->setMwGlobals( [
2835  'wgRequest' => $fauxRequest,
2836  ] );
2837 
2838  $actualReturn = OutputPage::transformCssMedia( $args['media'] );
2839  $this->assertSame( $args['expectedReturn'], $actualReturn, $args['message'] );
2840  }
2841 
2847  public function testPrintRequests() {
2848  $this->assertTransformCssMediaCase( [
2849  'printableQuery' => '1',
2850  'media' => 'screen',
2851  'expectedReturn' => null,
2852  'message' => 'On printable request, screen returns null'
2853  ] );
2854 
2855  $this->assertTransformCssMediaCase( [
2856  'printableQuery' => '1',
2857  'media' => self::SCREEN_MEDIA_QUERY,
2858  'expectedReturn' => null,
2859  'message' => 'On printable request, screen media query returns null'
2860  ] );
2861 
2862  $this->assertTransformCssMediaCase( [
2863  'printableQuery' => '1',
2864  'media' => self::SCREEN_ONLY_MEDIA_QUERY,
2865  'expectedReturn' => null,
2866  'message' => 'On printable request, screen media query with only returns null'
2867  ] );
2868 
2869  $this->assertTransformCssMediaCase( [
2870  'printableQuery' => '1',
2871  'media' => 'print',
2872  'expectedReturn' => '',
2873  'message' => 'On printable request, media print returns empty string'
2874  ] );
2875  }
2876 
2882  public function testScreenRequests() {
2883  $this->assertTransformCssMediaCase( [
2884  'media' => 'screen',
2885  'expectedReturn' => 'screen',
2886  'message' => 'On screen request, screen media type is preserved'
2887  ] );
2888 
2889  $this->assertTransformCssMediaCase( [
2890  'media' => 'handheld',
2891  'expectedReturn' => 'handheld',
2892  'message' => 'On screen request, handheld media type is preserved'
2893  ] );
2894 
2895  $this->assertTransformCssMediaCase( [
2896  'media' => self::SCREEN_MEDIA_QUERY,
2897  'expectedReturn' => self::SCREEN_MEDIA_QUERY,
2898  'message' => 'On screen request, screen media query is preserved.'
2899  ] );
2900 
2901  $this->assertTransformCssMediaCase( [
2902  'media' => self::SCREEN_ONLY_MEDIA_QUERY,
2903  'expectedReturn' => self::SCREEN_ONLY_MEDIA_QUERY,
2904  'message' => 'On screen request, screen media query with only is preserved.'
2905  ] );
2906 
2907  $this->assertTransformCssMediaCase( [
2908  'media' => 'print',
2909  'expectedReturn' => 'print',
2910  'message' => 'On screen request, print media type is preserved'
2911  ] );
2912  }
2913 
2919  public function testHandheld() {
2920  $this->assertTransformCssMediaCase( [
2921  'handheldQuery' => '1',
2922  'media' => 'handheld',
2923  'expectedReturn' => '',
2924  'message' => 'On request with handheld querystring and media is handheld, returns empty string'
2925  ] );
2926 
2927  $this->assertTransformCssMediaCase( [
2928  'handheldQuery' => '1',
2929  'media' => 'screen',
2930  'expectedReturn' => null,
2931  'message' => 'On request with handheld querystring and media is screen, returns null'
2932  ] );
2933  }
2934 
2940  public function testIsTOCEnabled() {
2941  $op = $this->newInstance();
2942  $this->assertFalse( $op->isTOCEnabled() );
2943 
2944  $pOut1 = $this->createParserOutputStub( 'getTOCHTML', false );
2945  $op->addParserOutputMetadata( $pOut1 );
2946  $this->assertFalse( $op->isTOCEnabled() );
2947 
2948  $pOut2 = $this->createParserOutputStub( 'getTOCHTML', true );
2949  $op->addParserOutput( $pOut2 );
2950  $this->assertTrue( $op->isTOCEnabled() );
2951 
2952  // The parser output doesn't disable the TOC after it was enabled
2953  $op->addParserOutputMetadata( $pOut1 );
2954  $this->assertTrue( $op->isTOCEnabled() );
2955  }
2956 
2962  public function testPreloadLinkHeaders( $config, $result ) {
2963  $this->setMwGlobals( $config );
2964  $ctx = $this->getMockBuilder( ResourceLoaderContext::class )
2965  ->disableOriginalConstructor()->getMock();
2966  $module = new ResourceLoaderSkinModule();
2967 
2968  $this->assertEquals( [ $result ], $module->getHeaders( $ctx ) );
2969  }
2970 
2971  public function providePreloadLinkHeaders() {
2972  return [
2973  [
2974  [
2975  'wgResourceBasePath' => '/w',
2976  'wgLogo' => '/img/default.png',
2977  'wgLogoHD' => [
2978  '1.5x' => '/img/one-point-five.png',
2979  '2x' => '/img/two-x.png',
2980  ],
2981  ],
2982  'Link: </img/default.png>;rel=preload;as=image;media=' .
2983  'not all and (min-resolution: 1.5dppx),' .
2984  '</img/one-point-five.png>;rel=preload;as=image;media=' .
2985  '(min-resolution: 1.5dppx) and (max-resolution: 1.999999dppx),' .
2986  '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
2987  ],
2988  [
2989  [
2990  'wgResourceBasePath' => '/w',
2991  'wgLogo' => '/img/default.png',
2992  'wgLogoHD' => false,
2993  ],
2994  'Link: </img/default.png>;rel=preload;as=image'
2995  ],
2996  [
2997  [
2998  'wgResourceBasePath' => '/w',
2999  'wgLogo' => '/img/default.png',
3000  'wgLogoHD' => [
3001  '2x' => '/img/two-x.png',
3002  ],
3003  ],
3004  'Link: </img/default.png>;rel=preload;as=image;media=' .
3005  'not all and (min-resolution: 2dppx),' .
3006  '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
3007  ],
3008  [
3009  [
3010  'wgResourceBasePath' => '/w',
3011  'wgLogo' => '/img/default.png',
3012  'wgLogoHD' => [
3013  'svg' => '/img/vector.svg',
3014  ],
3015  ],
3016  'Link: </img/vector.svg>;rel=preload;as=image'
3017 
3018  ],
3019  [
3020  [
3021  'wgResourceBasePath' => '/w',
3022  'wgLogo' => '/w/test.jpg',
3023  'wgLogoHD' => false,
3024  'wgUploadPath' => '/w/images',
3025  'IP' => dirname( __DIR__ ) . '/data/media',
3026  ],
3027  'Link: </w/test.jpg?edcf2>;rel=preload;as=image',
3028  ],
3029  ];
3030  }
3031 
3035  private function newInstance( $config = [], WebRequest $request = null, $options = [] ) {
3036  $context = new RequestContext();
3037 
3038  $context->setConfig( new MultiConfig( [
3039  new HashConfig( $config + [
3040  'AppleTouchIcon' => false,
3041  'DisableLangConversion' => true,
3042  'EnableCanonicalServerLink' => false,
3043  'Favicon' => false,
3044  'Feed' => false,
3045  'LanguageCode' => false,
3046  'ReferrerPolicy' => false,
3047  'RightsPage' => false,
3048  'RightsUrl' => false,
3049  'UniversalEditButton' => false,
3050  ] ),
3051  $context->getConfig()
3052  ] ) );
3053 
3054  if ( !in_array( 'notitle', (array)$options ) ) {
3055  $context->setTitle( Title::newFromText( 'My test page' ) );
3056  }
3057 
3058  if ( $request ) {
3059  $context->setRequest( $request );
3060  }
3061 
3062  return new OutputPage( $context );
3063  }
3064 }
testAddInlineScript()
OutputPage::addInlineScript.
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:596
testAddWikiTextAsContentNoTitle()
OutputPage::addWikiTextAsContent.
assertFeedUILinks( $outputPage, $ui_links)
testAddScript()
OutputPage::addScript.
testSetTitle()
OutputPage::setTitle.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:1972
testBodyHTML()
OutputPage::prependHTML OutputPage::addHTML OutputPage::addElement OutputPage::clearHTML OutputPage::...
testFileVersion()
OutputPage::setFileVersion OutputPage::getFileVersion.
return true to allow those checks to and false if checking is done remove or add to the links of a group of changes in EnhancedChangesList Hook subscribers can return false to omit this line from recentchanges use this to change the tables headers change it to an object instance and return false override the list derivative used $groups Array of ChangesListFilterGroup objects(added in 1.34) 'FileDeleteComplete' 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:1529
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
testAddWikiMsg()
OutputPage::addWikiMsg.
testAddWikiTextAsInterfaceNoTitle()
OutputPage::addWikiTextAsInterface.
const NS_MAIN
Definition: Defines.php:60
static $parserOutputHookCalled
static provideFeedLinkData()
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
const SCREEN_ONLY_MEDIA_QUERY
testWrapWikiMsg()
OutputPage::wrapWikiMsg.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1972
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
testAddBacklinkSubtitle( $titles, $queries, $contains, $notContains)
provideBacklinkSubtitle
testCheckLastModified( $timestamp, $ifModifiedSince, $expected, $config=[], $callback=null)
provideCheckLastModified
globals will be eliminated from MediaWiki replaced by an application object which would be passed to constructors Whether that would be an convenient solution remains to be but certainly PHP makes such object oriented programming models easier than they were in previous versions For the time being MediaWiki programmers will have to work in an environment with some global context At the time of globals were initialised on startup by MediaWiki of these were configuration which are documented in DefaultSettings php There is no comprehensive documentation for the remaining however some of the most important ones are listed below They are typically initialised either in index php or in Setup php $wgTitle Title object created from the request URL $wgOut OutputPage object for HTTP response $wgUser User object for the user associated with the current request $wgLang Language object selected by user preferences $wgContLang Language object associated with the wiki being viewed $wgParser Parser object Parser extensions register their hooks here $wgRequest WebRequest object
Definition: globals.txt:25
$basePath
Definition: addSite.php:5
testArticleFlags()
OutputPage::setArticleFlag OutputPage::isArticle OutputPage::setArticleRelated OutputPage::isArticleR...
testMakeResourceLoaderLink( $args, $expectedHtml)
See ResourceLoaderClientHtmlTest for full coverage.
parserOutputHookCallback(OutputPage $op, ParserOutput $pOut, $data)
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
testAddAcceptLanguage( $code, array $variants, $expected, array $options=[])
provideAddAcceptLanguage OutputPage::addAcceptLanguage
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:572
testPrintRequests()
Tests print requests.
newInstance( $config=[], WebRequest $request=null, $options=[])
testParseNullTitle()
OutputPage::parse.
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
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. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\*-\*)") will be honored when streaming the file. '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:1970
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:3039
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:767
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:1972
testAddHelpLink()
OutputPage::addHelpLink OutputPage::getIndicators.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
testClickjacking()
OutputPage::preventClickjacking OutputPage::allowClickjacking OutputPage::getPreventClickjacking Outp...
static buildBacklinkSubtitle(Title $title, $query=[])
Build message object for a subtitle containing a backlink to a page.
testAdditionalFeeds( $feed, $advertised_feed_types, $message, $additional_feed_type, $present, $non_present, $any_ui_links)
provideAdditionalFeedData OutputPage::getHeadLinksArray OutputPage::addFeedLink OutputPage::getSyndic...
const NS_PROJECT
Definition: Defines.php:64
testSetCategoryLinks(array $args, array $fakeResults, callable $variantLinkCallback=null, array $expectedNormal, array $expectedHidden)
provideGetCategories
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
testPageTitle()
OutputPage::setPageTitle OutputPage::getPageTitle.
testVaryHeaders(array $calls, array $cookies, $vary)
provideVaryHeaders
testNoGallery()
OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
headElement(Skin $sk, $includeStyle=true)
static provideAdditionalFeedData()
static transformResourcePath(Config $config, $path)
Transform path to web-accessible static resource.
testGetCacheVaryCookies()
OutputPage::getCacheVaryCookies.
testRevisionId( $newVal, $expected)
provideRevisionId OutputPage::setRevisionId OutputPage::getRevisionId
static provideBuildExemptModules()
const NS_MEDIA
Definition: Defines.php:48
testAddBodyClasses()
OutputPage::addBodyClasses.
testAddCategoryLinksOneByOne(array $args, array $fakeResults, callable $variantLinkCallback=null, array $expectedNormal, array $expectedHidden)
provideGetCategories
testSubtitle()
OutputPage::setSubtitle OutputPage::clearSubtitle OutputPage::addSubtitle OutputPage::getSubtitle.
testMetaTags()
OutputPage::addMeta OutputPage::getMetaTags OutputPage::getHeadLinksArray.
testSetCanonicalUrl()
OutputPage::setCanonicalUrl OutputPage::getCanonicalUrl OutputPage::getHeadLinksArray.
getContext()
Get the base IContextSource object.
$params
const NS_CATEGORY
Definition: Defines.php:74
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1972
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:767
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:912
static int $fakeTime
Faked time to set for tests that need it.
$wgCdnMaxAge
Cache TTL for the CDN sent as s-maxage (without ESI) or Surrogate-Control (with ESI).
testParseInlineNullTitle()
OutputPage::parseInline.
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:216
assertTransformCssMediaCase( $args)
Tests a particular case of transformCssMedia, using the given input, globals, expected return...
testShowNewSectionLink()
OutputPage::showNewSectionLink OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
static provideTransformFilePath()
$header
Provides a fallback sequence for Config objects.
Definition: MultiConfig.php:28
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:767
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
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:620
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:592
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:960
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:767
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::addWikiTextAsInterface OutputPage::wrapWikiTextAsInterface OutputPage:...
testPreloadLinkHeaders( $config, $result)
providePreloadLinkHeaders ResourceLoaderSkinModule::getPreloadLinks ResourceLoaderSkinModule::getLogo...
linkcache txt The LinkCache class maintains a list of article titles and the information about whether or not the article exists in the database This is used to mark up links when displaying a page If the same link appears more than once on any page then it only has to be looked up once In most cases link lookups are done in batches with the LinkBatch class or the equivalent in so the link cache is mostly useful for short snippets of parsed and for links in the navigation areas of the skin The link cache was formerly used to track links used in a document for the purposes of updating the link tables This application is now deprecated To create a you can use the following $titles
Definition: linkcache.txt:17
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
testHandheld()
Tests handheld behavior.
testAdaptCdnTTL(array $args, $expected, array $options=[])
provideAdaptCdnTTL OutputPage::adaptCdnTTL
setupFeedLinks( $feed, $types)
testPrintable()
OutputPage::setPrintable OutputPage::isPrintable.
testScreenRequests()
Tests screen requests, without either query parameter set.
testDisable()
OutputPage::disable OutputPage::isDisabled.
testParseInlineAsInterface(array $args, $expectedHTML, $expectedHTMLInline=null)
provideParseAs OutputPage::parseInlineAsInterface
testSetIndexFollowPolicies()
OutputPage::setIndexPolicy OutputPage::setFollowPolicy OutputPage::getHeadLinksArray.
doCategoryAsserts( $op, $expectedNormal, $expectedHidden)
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition: hooks.txt:2621
testSetCopyrightUrl()
OutputPage::setCopyrightUrl OutputPage::getHeadLinksArray.
testClientCache()
OutputPage::enableClientCache OutputPage::addParserOutputMetadata OutputPage::addParserOutput.
testIndicators()
OutputPage::setIndicators OutputPage::getIndicators OutputPage::addParserOutputMetadata OutputPage::a...
testParserOutputCategoryLinks(array $args, array $fakeResults, callable $variantLinkCallback=null, array $expectedNormal, array $expectedHidden)
provideGetCategories
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
const NS_TALK
Definition: Defines.php:61
testParseAsInterfaceNullTitle()
OutputPage::parseAsInterface.
createParserOutputStub(... $args)
Call either with arguments $methodName, $returnValue; or an array [ $methodName => $returnValue...
testParse(array $args, $expectedHTML)
provideParse OutputPage::parse
testParseAsContent(array $args, $expectedHTML, $expectedHTMLInline=null)
provideParseAs OutputPage::parseAsContent
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a controlled by the following MediaWiki still creates a BagOStuff but calls it to it are no ops If the cache daemon can t be it should also disable itself fairly $wgDBname
Definition: memcached.txt:93
setupCategoryTests(array $fakeResults, callable $variantLinkCallback=null)
testAddParserOutput()
OutputPage::addParserOutput.
testAddLink()
OutputPage::addLink OutputPage::getLinkTags OutputPage::getHeadLinksArray.
$queries
ResourceLoader module for skin stylesheets.
testAddParserOutputText()
OutputPage::addParserOutputText.
A Config instance which stores all settings as a member variable.
Definition: HashConfig.php:28
testGetFrameOptions( $breakFrames, $preventClickjacking, $editPageFrameOptions, $expected)
provideGetFrameOptions OutputPage::getFrameOptions OutputPage::preventClickjacking ...
testSetTarget()
OutputPage::getTarget OutputPage::setTarget.
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:2621
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:322