MediaWiki  1.32.0
OutputPageTest.php
Go to the documentation of this file.
1 <?php
2 
3 use Wikimedia\TestingAccessWrapper;
4 
11 class OutputPageTest extends MediaWikiTestCase {
12  const SCREEN_MEDIA_QUERY = 'screen and (min-width: 982px)';
13  const SCREEN_ONLY_MEDIA_QUERY = 'only screen and (min-width: 982px)';
14 
15  // Ensure that we don't affect the global ResourceLoader state.
16  protected function setUp() {
17  parent::setUp();
18  ResourceLoader::clearCache();
19  }
20  protected function tearDown() {
21  parent::tearDown();
22  ResourceLoader::clearCache();
23  }
24 
32  public function testRedirect( $url, $code = null ) {
33  $op = $this->newInstance();
34  if ( isset( $code ) ) {
35  $op->redirect( $url, $code );
36  } else {
37  $op->redirect( $url );
38  }
39  $expectedUrl = str_replace( "\n", '', $url );
40  $this->assertSame( $expectedUrl, $op->getRedirect() );
41  $this->assertSame( $expectedUrl, $op->mRedirect );
42  $this->assertSame( $code ?? '302', $op->mRedirectCode );
43  }
44 
45  public function provideRedirect() {
46  return [
47  [ 'http://example.com' ],
48  [ 'http://example.com', '400' ],
49  [ 'http://example.com', 'squirrels!!!' ],
50  [ "a\nb" ],
51  ];
52  }
53 
58  public function testSetCopyrightUrl() {
59  $op = $this->newInstance();
60  $op->setCopyrightUrl( 'http://example.com' );
61 
62  $this->assertSame(
63  Html::element( 'link', [ 'rel' => 'license', 'href' => 'http://example.com' ] ),
64  $op->getHeadLinksArray()['copyright']
65  );
66  }
67 
68  // @todo How to test setStatusCode?
69 
75  public function testMetaTags() {
76  $op = $this->newInstance();
77  $op->addMeta( 'http:expires', '0' );
78  $op->addMeta( 'keywords', 'first' );
79  $op->addMeta( 'keywords', 'second' );
80  $op->addMeta( 'og:title', 'Ta-duh' );
81 
82  $expected = [
83  [ 'http:expires', '0' ],
84  [ 'keywords', 'first' ],
85  [ 'keywords', 'second' ],
86  [ 'og:title', 'Ta-duh' ],
87  ];
88  $this->assertSame( $expected, $op->getMetaTags() );
89 
90  $links = $op->getHeadLinksArray();
91  $this->assertContains( '<meta http-equiv="expires" content="0"/>', $links );
92  $this->assertContains( '<meta name="keywords" content="first"/>', $links );
93  $this->assertContains( '<meta name="keywords" content="second"/>', $links );
94  $this->assertContains( '<meta property="og:title" content="Ta-duh"/>', $links );
95  $this->assertArrayNotHasKey( 'meta-robots', $links );
96  }
97 
103  public function testAddLink() {
104  $op = $this->newInstance();
105 
106  $links = [
107  [],
108  [ 'rel' => 'foo', 'href' => 'http://example.com' ],
109  ];
110 
111  foreach ( $links as $link ) {
112  $op->addLink( $link );
113  }
114 
115  $this->assertSame( $links, $op->getLinkTags() );
116 
117  $result = $op->getHeadLinksArray();
118 
119  foreach ( $links as $link ) {
120  $this->assertContains( Html::element( 'link', $link ), $result );
121  }
122  }
123 
129  public function testSetCanonicalUrl() {
130  $op = $this->newInstance();
131  $op->setCanonicalUrl( 'http://example.comm' );
132  $op->setCanonicalUrl( 'http://example.com' );
133 
134  $this->assertSame( 'http://example.com', $op->getCanonicalUrl() );
135 
136  $headLinks = $op->getHeadLinksArray();
137 
138  $this->assertContains( Html::element( 'link', [
139  'rel' => 'canonical', 'href' => 'http://example.com'
140  ] ), $headLinks );
141 
142  $this->assertNotContains( Html::element( 'link', [
143  'rel' => 'canonical', 'href' => 'http://example.comm'
144  ] ), $headLinks );
145  }
146 
150  public function testAddScript() {
151  $op = $this->newInstance();
152  $op->addScript( 'some random string' );
153 
154  $this->assertContains( "\nsome random string\n", "\n" . $op->getBottomScripts() . "\n" );
155  }
156 
160  public function testAddScriptFile() {
161  $op = $this->newInstance();
162  $op->addScriptFile( '/somescript.js' );
163  $op->addScriptFile( '//example.com/somescript.js' );
164 
165  $this->assertContains(
166  "\n" . Html::linkedScript( '/somescript.js', $op->getCSPNonce() ) .
167  Html::linkedScript( '//example.com/somescript.js', $op->getCSPNonce() ) . "\n",
168  "\n" . $op->getBottomScripts() . "\n"
169  );
170  }
171 
177  public function testAddDeprecatedScriptFileWarning() {
178  $this->setExpectedException( PHPUnit_Framework_Error_Deprecated::class,
179  'Use of OutputPage::addScriptFile was deprecated in MediaWiki 1.24.' );
180 
181  $op = $this->newInstance();
182  $op->addScriptFile( 'ignored-script.js' );
183  }
184 
191  public function testAddDeprecatedScriptFileNoOp() {
192  $this->hideDeprecated( 'OutputPage::addScriptFile' );
193  $op = $this->newInstance();
194  $op->addScriptFile( 'ignored-script.js' );
195 
196  $this->assertNotContains( 'ignored-script.js', '' . $op->getBottomScripts() );
197  }
198 
202  public function testAddInlineScript() {
203  $op = $this->newInstance();
204  $op->addInlineScript( 'let foo = "bar";' );
205  $op->addInlineScript( 'alert( foo );' );
206 
207  $this->assertContains(
208  "\n" . Html::inlineScript( "\nlet foo = \"bar\";\n", $op->getCSPNonce() ) . "\n" .
209  Html::inlineScript( "\nalert( foo );\n", $op->getCSPNonce() ) . "\n",
210  "\n" . $op->getBottomScripts() . "\n"
211  );
212  }
213 
214  // @todo How to test filterModules(), warnModuleTargetFilter(), getModules(), etc.?
215 
220  public function testSetTarget() {
221  $op = $this->newInstance();
222  $op->setTarget( 'foo' );
223 
224  $this->assertSame( 'foo', $op->getTarget() );
225  // @todo What else? Test some actual effect?
226  }
227 
228  // @todo How to test addContentOverride(Callback)?
229 
236  public function testHeadItems() {
237  $op = $this->newInstance();
238  $op->addHeadItem( 'a', 'b' );
239  $op->addHeadItems( [ 'c' => '<d>&amp;', 'e' => 'f', 'a' => 'q' ] );
240  $op->addHeadItem( 'e', 'g' );
241  $op->addHeadItems( 'x' );
242 
243  $this->assertSame( [ 'a' => 'q', 'c' => '<d>&amp;', 'e' => 'g', 'x' ],
244  $op->getHeadItemsArray() );
245 
246  $this->assertTrue( $op->hasHeadItem( 'a' ) );
247  $this->assertTrue( $op->hasHeadItem( 'c' ) );
248  $this->assertTrue( $op->hasHeadItem( 'e' ) );
249  $this->assertTrue( $op->hasHeadItem( '0' ) );
250 
251  $this->assertContains( "\nq\n<d>&amp;\ng\nx\n",
252  '' . $op->headElement( $op->getContext()->getSkin() ) );
253  }
254 
260  public function testHeadItemsParserOutput() {
261  $op = $this->newInstance();
262  $stubPO1 = $this->createParserOutputStub( 'getHeadItems', [ 'a' => 'b' ] );
263  $op->addParserOutputMetadata( $stubPO1 );
264  $stubPO2 = $this->createParserOutputStub( 'getHeadItems',
265  [ 'c' => '<d>&amp;', 'e' => 'f', 'a' => 'q' ] );
266  $op->addParserOutputMetadata( $stubPO2 );
267  $stubPO3 = $this->createParserOutputStub( 'getHeadItems', [ 'e' => 'g' ] );
268  $op->addParserOutput( $stubPO3 );
269  $stubPO4 = $this->createParserOutputStub( 'getHeadItems', [ 'x' ] );
270  $op->addParserOutputMetadata( $stubPO4 );
271 
272  $this->assertSame( [ 'a' => 'q', 'c' => '<d>&amp;', 'e' => 'g', 'x' ],
273  $op->getHeadItemsArray() );
274 
275  $this->assertTrue( $op->hasHeadItem( 'a' ) );
276  $this->assertTrue( $op->hasHeadItem( 'c' ) );
277  $this->assertTrue( $op->hasHeadItem( 'e' ) );
278  $this->assertTrue( $op->hasHeadItem( '0' ) );
279  $this->assertFalse( $op->hasHeadItem( 'b' ) );
280 
281  $this->assertContains( "\nq\n<d>&amp;\ng\nx\n",
282  '' . $op->headElement( $op->getContext()->getSkin() ) );
283  }
284 
288  public function testAddBodyClasses() {
289  $op = $this->newInstance();
290  $op->addBodyClasses( 'a' );
291  $op->addBodyClasses( 'mediawiki' );
292  $op->addBodyClasses( 'b c' );
293  $op->addBodyClasses( [ 'd', 'e' ] );
294  $op->addBodyClasses( 'a' );
295 
296  $this->assertContains( '"a mediawiki b c d e ltr',
297  '' . $op->headElement( $op->getContext()->getSkin() ) );
298  }
299 
304  public function testArticleBodyOnly() {
305  $op = $this->newInstance();
306  $this->assertFalse( $op->getArticleBodyOnly() );
307 
308  $op->setArticleBodyOnly( true );
309  $this->assertTrue( $op->getArticleBodyOnly() );
310 
311  $op->addHTML( '<b>a</b>' );
312 
313  $this->assertSame( '<b>a</b>', $op->output( true ) );
314  }
315 
320  public function testProperties() {
321  $op = $this->newInstance();
322 
323  $this->assertNull( $op->getProperty( 'foo' ) );
324 
325  $op->setProperty( 'foo', 'bar' );
326  $op->setProperty( 'baz', 'quz' );
327 
328  $this->assertSame( 'bar', $op->getProperty( 'foo' ) );
329  $this->assertSame( 'quz', $op->getProperty( 'baz' ) );
330  }
331 
338  public function testCheckLastModified(
339  $timestamp, $ifModifiedSince, $expected, $config = [], $callback = null
340  ) {
341  $request = new FauxRequest();
342  if ( $ifModifiedSince ) {
343  if ( is_numeric( $ifModifiedSince ) ) {
344  // Unix timestamp
345  $ifModifiedSince = date( 'D, d M Y H:i:s', $ifModifiedSince ) . ' GMT';
346  }
347  $request->setHeader( 'If-Modified-Since', $ifModifiedSince );
348  }
349 
350  if ( !isset( $config['CacheEpoch'] ) ) {
351  // Make sure it's not too recent
352  $config['CacheEpoch'] = '20000101000000';
353  }
354 
355  $op = $this->newInstance( $config, $request );
356 
357  if ( $callback ) {
358  $callback( $op, $this );
359  }
360 
361  // Avoid a complaint about not being able to disable compression
362  Wikimedia\suppressWarnings();
363  try {
364  $this->assertEquals( $expected, $op->checkLastModified( $timestamp ) );
365  } finally {
366  Wikimedia\restoreWarnings();
367  }
368  }
369 
370  public function provideCheckLastModified() {
371  $lastModified = time() - 3600;
372  return [
373  'Timestamp 0' =>
374  [ '0', $lastModified, false ],
375  'Timestamp Unix epoch' =>
376  [ '19700101000000', $lastModified, false ],
377  'Timestamp same as If-Modified-Since' =>
378  [ $lastModified, $lastModified, true ],
379  'Timestamp one second after If-Modified-Since' =>
380  [ $lastModified + 1, $lastModified, false ],
381  'No If-Modified-Since' =>
382  [ $lastModified + 1, null, false ],
383  'Malformed If-Modified-Since' =>
384  [ $lastModified + 1, 'GIBBERING WOMBATS !!!', false ],
385  'Non-standard IE-style If-Modified-Since' =>
386  [ $lastModified, date( 'D, d M Y H:i:s', $lastModified ) . ' GMT; length=5202',
387  true ],
388  // @todo Should we fix this behavior to match the spec? Probably no reason to.
389  'If-Modified-Since not per spec but we accept it anyway because strtotime does' =>
390  [ $lastModified, "@$lastModified", true ],
391  '$wgCachePages = false' =>
392  [ $lastModified, $lastModified, false, [ 'CachePages' => false ] ],
393  '$wgCacheEpoch' =>
394  [ $lastModified, $lastModified, false,
395  [ 'CacheEpoch' => wfTimestamp( TS_MW, $lastModified + 1 ) ] ],
396  'Recently-touched user' =>
397  [ $lastModified, $lastModified, false, [],
398  function ( $op ) {
399  $op->getContext()->setUser( $this->getTestUser()->getUser() );
400  } ],
401  'After Squid expiry' =>
402  [ $lastModified, $lastModified, false,
403  [ 'UseSquid' => true, 'SquidMaxage' => 3599 ] ],
404  'Hook allows cache use' =>
405  [ $lastModified + 1, $lastModified, true, [],
406  function ( $op, $that ) {
407  $that->setTemporaryHook( 'OutputPageCheckLastModified',
408  function ( &$modifiedTimes ) {
409  $modifiedTimes = [ 1 ];
410  }
411  );
412  } ],
413  'Hooks prohibits cache use' =>
414  [ $lastModified, $lastModified, false, [],
415  function ( $op, $that ) {
416  $that->setTemporaryHook( 'OutputPageCheckLastModified',
417  function ( &$modifiedTimes ) {
418  $modifiedTimes = [ max( $modifiedTimes ) + 1 ];
419  }
420  );
421  } ],
422  ];
423  }
424 
430  public function testCdnCacheEpoch( $params ) {
431  $out = TestingAccessWrapper::newFromObject( $this->newInstance() );
432  $reqTime = strtotime( $params['reqTime'] );
433  $pageTime = strtotime( $params['pageTime'] );
434  $actual = max( $pageTime, $out->getCdnCacheEpoch( $reqTime, $params['maxAge'] ) );
435 
436  $this->assertEquals(
437  $params['expect'],
438  gmdate( DateTime::ATOM, $actual ),
439  'cdn epoch'
440  );
441  }
442 
443  public static function provideCdnCacheEpoch() {
444  $base = [
445  'pageTime' => '2011-04-01T12:00:00+00:00',
446  'maxAge' => 24 * 3600,
447  ];
448  return [
449  'after 1s' => [ $base + [
450  'reqTime' => '2011-04-01T12:00:01+00:00',
451  'expect' => '2011-04-01T12:00:00+00:00',
452  ] ],
453  'after 23h' => [ $base + [
454  'reqTime' => '2011-04-02T11:00:00+00:00',
455  'expect' => '2011-04-01T12:00:00+00:00',
456  ] ],
457  'after 24h and a bit' => [ $base + [
458  'reqTime' => '2011-04-02T12:34:56+00:00',
459  'expect' => '2011-04-01T12:34:56+00:00',
460  ] ],
461  'after a year' => [ $base + [
462  'reqTime' => '2012-05-06T00:12:07+00:00',
463  'expect' => '2012-05-05T00:12:07+00:00',
464  ] ],
465  ];
466  }
467 
468  // @todo How to test setLastModified?
469 
474  public function testSetRobotPolicy() {
475  $op = $this->newInstance();
476  $op->setRobotPolicy( 'noindex, nofollow' );
477 
478  $links = $op->getHeadLinksArray();
479  $this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
480  }
481 
487  public function testSetIndexFollowPolicies() {
488  $op = $this->newInstance();
489  $op->setIndexPolicy( 'noindex' );
490  $op->setFollowPolicy( 'nofollow' );
491 
492  $links = $op->getHeadLinksArray();
493  $this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
494  }
495 
496  private function extractHTMLTitle( OutputPage $op ) {
497  $html = $op->headElement( $op->getContext()->getSkin() );
498 
499  // OutputPage should always output the title in a nice format such that regexes will work
500  // fine. If it doesn't, we'll fail the tests.
501  preg_match_all( '!<title>(.*?)</title>!', $html, $matches );
502 
503  $this->assertLessThanOrEqual( 1, count( $matches[1] ), 'More than one <title>!' );
504 
505  if ( !count( $matches[1] ) ) {
506  return null;
507  }
508 
509  return $matches[1][0];
510  }
511 
515  private static function getMsgText( $op, ...$msgParams ) {
516  return $op->msg( ...$msgParams )->inContentLanguage()->text();
517  }
518 
523  public function testHTMLTitle() {
524  $op = $this->newInstance();
525 
526  // Default
527  $this->assertSame( '', $op->getHTMLTitle() );
528  $this->assertSame( '', $op->getPageTitle() );
529  $this->assertSame(
530  $this->getMsgText( $op, 'pagetitle', '' ),
531  $this->extractHTMLTitle( $op )
532  );
533 
534  // Set to string
535  $op->setHTMLTitle( 'Potatoes will eat me' );
536 
537  $this->assertSame( 'Potatoes will eat me', $op->getHTMLTitle() );
538  $this->assertSame( 'Potatoes will eat me', $this->extractHTMLTitle( $op ) );
539  // Shouldn't have changed the page title
540  $this->assertSame( '', $op->getPageTitle() );
541 
542  // Set to message
543  $msg = $op->msg( 'mainpage' );
544 
545  $op->setHTMLTitle( $msg );
546  $this->assertSame( $msg->text(), $op->getHTMLTitle() );
547  $this->assertSame( $msg->text(), $this->extractHTMLTitle( $op ) );
548  $this->assertSame( '', $op->getPageTitle() );
549  }
550 
554  public function testSetRedirectedFrom() {
555  $op = $this->newInstance();
556 
557  $op->setRedirectedFrom( Title::newFromText( 'Talk:Some page' ) );
558  $this->assertSame( 'Talk:Some_page', $op->getJSVars()['wgRedirectedFrom'] );
559  }
560 
565  public function testPageTitle() {
566  // We don't test the actual HTML output anywhere, because that's up to the skin.
567  $op = $this->newInstance();
568 
569  // Test default
570  $this->assertSame( '', $op->getPageTitle() );
571  $this->assertSame( '', $op->getHTMLTitle() );
572 
573  // Test set to plain text
574  $op->setPageTitle( 'foobar' );
575 
576  $this->assertSame( 'foobar', $op->getPageTitle() );
577  // HTML title should change as well
578  $this->assertSame( $this->getMsgText( $op, 'pagetitle', 'foobar' ), $op->getHTMLTitle() );
579 
580  // Test set to text with good and bad HTML. We don't try to be comprehensive here, that
581  // belongs in Sanitizer tests.
582  $op->setPageTitle( '<script>a</script>&amp;<i>b</i>' );
583 
584  $this->assertSame( '&lt;script&gt;a&lt;/script&gt;&amp;<i>b</i>', $op->getPageTitle() );
585  $this->assertSame(
586  $this->getMsgText( $op, 'pagetitle', '<script>a</script>&b' ),
587  $op->getHTMLTitle()
588  );
589 
590  // Test set to message
591  $text = $this->getMsgText( $op, 'mainpage' );
592 
593  $op->setPageTitle( $op->msg( 'mainpage' )->inContentLanguage() );
594  $this->assertSame( $text, $op->getPageTitle() );
595  $this->assertSame( $this->getMsgText( $op, 'pagetitle', $text ), $op->getHTMLTitle() );
596  }
597 
601  public function testSetTitle() {
602  $op = $this->newInstance();
603 
604  $this->assertSame( 'My test page', $op->getTitle()->getPrefixedText() );
605 
606  $op->setTitle( Title::newFromText( 'Another test page' ) );
607 
608  $this->assertSame( 'Another test page', $op->getTitle()->getPrefixedText() );
609  }
610 
617  public function testSubtitle() {
618  $op = $this->newInstance();
619 
620  $this->assertSame( '', $op->getSubtitle() );
621 
622  $op->addSubtitle( '<b>foo</b>' );
623 
624  $this->assertSame( '<b>foo</b>', $op->getSubtitle() );
625 
626  $op->addSubtitle( $op->msg( 'mainpage' )->inContentLanguage() );
627 
628  $this->assertSame(
629  "<b>foo</b><br />\n\t\t\t\t" . $this->getMsgText( $op, 'mainpage' ),
630  $op->getSubtitle()
631  );
632 
633  $op->setSubtitle( 'There can be only one' );
634 
635  $this->assertSame( 'There can be only one', $op->getSubtitle() );
636 
637  $op->clearSubtitle();
638 
639  $this->assertSame( '', $op->getSubtitle() );
640  }
641 
647  public function testBuildBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
648  if ( count( $titles ) > 1 ) {
649  // Not applicable
650  $this->assertTrue( true );
651  return;
652  }
653 
655  $query = $queries[0];
656 
657  $this->editPage( 'Page 1', '' );
658  $this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
659 
660  $str = OutputPage::buildBacklinkSubtitle( $title, $query )->text();
661 
662  foreach ( $contains as $substr ) {
663  $this->assertContains( $substr, $str );
664  }
665 
666  foreach ( $notContains as $substr ) {
667  $this->assertNotContains( $substr, $str );
668  }
669  }
670 
677  public function testAddBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
678  $this->editPage( 'Page 1', '' );
679  $this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
680 
681  $op = $this->newInstance();
682  foreach ( $titles as $i => $unused ) {
683  $op->addBacklinkSubtitle( Title::newFromText( $titles[$i] ), $queries[$i] );
684  }
685 
686  $str = $op->getSubtitle();
687 
688  foreach ( $contains as $substr ) {
689  $this->assertContains( $substr, $str );
690  }
691 
692  foreach ( $notContains as $substr ) {
693  $this->assertNotContains( $substr, $str );
694  }
695  }
696 
697  public function provideBacklinkSubtitle() {
698  return [
699  [
700  [ 'Page 1' ],
701  [ [] ],
702  [ 'Page 1' ],
703  [ 'redirect', 'Page 2' ],
704  ],
705  [
706  [ 'Page 2' ],
707  [ [] ],
708  [ 'redirect=no' ],
709  [ 'Page 1' ],
710  ],
711  [
712  [ 'Page 1' ],
713  [ [ 'action' => 'edit' ] ],
714  [ 'action=edit' ],
715  [],
716  ],
717  [
718  [ 'Page 1', 'Page 2' ],
719  [ [], [] ],
720  [ 'Page 1', 'Page 2', "<br />\n\t\t\t\t" ],
721  [],
722  ],
723  // @todo Anything else to test?
724  ];
725  }
726 
731  public function testPrintable() {
732  $op = $this->newInstance();
733 
734  $this->assertFalse( $op->isPrintable() );
735 
736  $op->setPrintable();
737 
738  $this->assertTrue( $op->isPrintable() );
739  }
740 
745  public function testDisable() {
746  $op = $this->newInstance();
747 
748  $this->assertFalse( $op->isDisabled() );
749  $this->assertNotSame( '', $op->output( true ) );
750 
751  $op->disable();
752 
753  $this->assertTrue( $op->isDisabled() );
754  $this->assertSame( '', $op->output( true ) );
755  }
756 
762  public function testShowNewSectionLink() {
763  $op = $this->newInstance();
764 
765  $this->assertFalse( $op->showNewSectionLink() );
766 
767  $pOut1 = $this->createParserOutputStub( 'getNewSection', true );
768  $op->addParserOutputMetadata( $pOut1 );
769  $this->assertTrue( $op->showNewSectionLink() );
770 
771  $pOut2 = $this->createParserOutputStub( 'getNewSection', false );
772  $op->addParserOutput( $pOut2 );
773  $this->assertFalse( $op->showNewSectionLink() );
774  }
775 
781  public function testForceHideNewSectionLink() {
782  $op = $this->newInstance();
783 
784  $this->assertFalse( $op->forceHideNewSectionLink() );
785 
786  $pOut1 = $this->createParserOutputStub( 'getHideNewSection', true );
787  $op->addParserOutputMetadata( $pOut1 );
788  $this->assertTrue( $op->forceHideNewSectionLink() );
789 
790  $pOut2 = $this->createParserOutputStub( 'getHideNewSection', false );
791  $op->addParserOutput( $pOut2 );
792  $this->assertFalse( $op->forceHideNewSectionLink() );
793  }
794 
799  public function testSetSyndicated() {
800  $op = $this->newInstance();
801  $this->assertFalse( $op->isSyndicated() );
802 
803  $op->setSyndicated();
804  $this->assertTrue( $op->isSyndicated() );
805 
806  $op->setSyndicated( false );
807  $this->assertFalse( $op->isSyndicated() );
808  }
809 
816  public function testFeedLinks() {
817  $op = $this->newInstance();
818  $this->assertSame( [], $op->getSyndicationLinks() );
819 
820  $op->addFeedLink( 'not a supported format', 'abc' );
821  $this->assertFalse( $op->isSyndicated() );
822  $this->assertSame( [], $op->getSyndicationLinks() );
823 
824  $feedTypes = $op->getConfig()->get( 'AdvertisedFeedTypes' );
825 
826  $op->addFeedLink( $feedTypes[0], 'def' );
827  $this->assertTrue( $op->isSyndicated() );
828  $this->assertSame( [ $feedTypes[0] => 'def' ], $op->getSyndicationLinks() );
829 
830  $op->setFeedAppendQuery( false );
831  $expected = [];
832  foreach ( $feedTypes as $type ) {
833  $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type" );
834  }
835  $this->assertSame( $expected, $op->getSyndicationLinks() );
836 
837  $op->setFeedAppendQuery( 'apples=oranges' );
838  foreach ( $feedTypes as $type ) {
839  $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type&apples=oranges" );
840  }
841  $this->assertSame( $expected, $op->getSyndicationLinks() );
842  }
843 
850  function testArticleFlags() {
851  $op = $this->newInstance();
852  $this->assertFalse( $op->isArticle() );
853  $this->assertTrue( $op->isArticleRelated() );
854 
855  $op->setArticleRelated( false );
856  $this->assertFalse( $op->isArticle() );
857  $this->assertFalse( $op->isArticleRelated() );
858 
859  $op->setArticleFlag( true );
860  $this->assertTrue( $op->isArticle() );
861  $this->assertTrue( $op->isArticleRelated() );
862 
863  $op->setArticleFlag( false );
864  $this->assertFalse( $op->isArticle() );
865  $this->assertTrue( $op->isArticleRelated() );
866 
867  $op->setArticleFlag( true );
868  $op->setArticleRelated( false );
869  $this->assertFalse( $op->isArticle() );
870  $this->assertFalse( $op->isArticleRelated() );
871  }
872 
880  function testLanguageLinks() {
881  $op = $this->newInstance();
882  $this->assertSame( [], $op->getLanguageLinks() );
883 
884  $op->addLanguageLinks( [ 'fr:A', 'it:B' ] );
885  $this->assertSame( [ 'fr:A', 'it:B' ], $op->getLanguageLinks() );
886 
887  $op->addLanguageLinks( [ 'de:C', 'es:D' ] );
888  $this->assertSame( [ 'fr:A', 'it:B', 'de:C', 'es:D' ], $op->getLanguageLinks() );
889 
890  $op->setLanguageLinks( [ 'pt:E' ] );
891  $this->assertSame( [ 'pt:E' ], $op->getLanguageLinks() );
892 
893  $pOut1 = $this->createParserOutputStub( 'getLanguageLinks', [ 'he:F', 'ar:G' ] );
894  $op->addParserOutputMetadata( $pOut1 );
895  $this->assertSame( [ 'pt:E', 'he:F', 'ar:G' ], $op->getLanguageLinks() );
896 
897  $pOut2 = $this->createParserOutputStub( 'getLanguageLinks', [ 'pt:H' ] );
898  $op->addParserOutput( $pOut2 );
899  $this->assertSame( [ 'pt:E', 'he:F', 'ar:G', 'pt:H' ], $op->getLanguageLinks() );
900  }
901 
902  // @todo Are these category links tests too abstract and complicated for what they test? Would
903  // it make sense to just write out all the tests by hand with maybe some copy-and-paste?
904 
919  public function testAddCategoryLinks(
920  array $args, array $fakeResults, callable $variantLinkCallback = null,
921  array $expectedNormal, array $expectedHidden
922  ) {
923  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'add' );
924  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'add' );
925 
926  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
927 
928  $op->addCategoryLinks( $args );
929 
930  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
931  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
932  }
933 
941  public function testAddCategoryLinksOneByOne(
942  array $args, array $fakeResults, callable $variantLinkCallback = null,
943  array $expectedNormal, array $expectedHidden
944  ) {
945  if ( count( $args ) <= 1 ) {
946  // @todo Should this be skipped instead of passed?
947  $this->assertTrue( true );
948  return;
949  }
950 
951  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'onebyone' );
952  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'onebyone' );
953 
954  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
955 
956  foreach ( $args as $key => $val ) {
957  $op->addCategoryLinks( [ $key => $val ] );
958  }
959 
960  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
961  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
962  }
963 
971  public function testSetCategoryLinks(
972  array $args, array $fakeResults, callable $variantLinkCallback = null,
973  array $expectedNormal, array $expectedHidden
974  ) {
975  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'set' );
976  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'set' );
977 
978  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
979 
980  $op->setCategoryLinks( [ 'Initial page' => 'Initial page' ] );
981  $op->setCategoryLinks( $args );
982 
983  // We don't reset the categories, for some reason, only the links
984  $expectedNormalCats = array_merge( [ 'Initial page' ], $expectedNormal );
985  $expectedCats = array_merge( $expectedHidden, $expectedNormalCats );
986 
987  $this->doCategoryAsserts( $op, $expectedNormalCats, $expectedHidden );
988  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
989  }
990 
999  public function testParserOutputCategoryLinks(
1000  array $args, array $fakeResults, callable $variantLinkCallback = null,
1001  array $expectedNormal, array $expectedHidden
1002  ) {
1003  $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'pout' );
1004  $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'pout' );
1005 
1006  $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
1007 
1008  $stubPO = $this->createParserOutputStub( 'getCategories', $args );
1009 
1010  // addParserOutput and addParserOutputMetadata should behave identically for us, so
1011  // alternate to get coverage for both without adding extra tests
1012  static $idx = 0;
1013  $idx++;
1014  $method = [ 'addParserOutputMetadata', 'addParserOutput' ][$idx % 2];
1015  $op->$method( $stubPO );
1016 
1017  $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
1018  $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
1019  }
1020 
1026  private function extractExpectedCategories( array $expected, $key ) {
1027  if ( !$expected || isset( $expected[0] ) ) {
1028  return $expected;
1029  }
1030  return $expected[$key] ?? $expected['default'];
1031  }
1032 
1033  private function setupCategoryTests(
1034  array $fakeResults, callable $variantLinkCallback = null
1035  ) : OutputPage {
1036  $this->setMwGlobals( 'wgUsePigLatinVariant', true );
1037 
1038  $op = $this->getMockBuilder( OutputPage::class )
1039  ->setConstructorArgs( [ new RequestContext() ] )
1040  ->setMethods( [ 'addCategoryLinksToLBAndGetResult', 'getTitle' ] )
1041  ->getMock();
1042 
1043  $title = Title::newFromText( 'My test page' );
1044  $op->expects( $this->any() )
1045  ->method( 'getTitle' )
1046  ->will( $this->returnValue( $title ) );
1047 
1048  $op->expects( $this->any() )
1049  ->method( 'addCategoryLinksToLBAndGetResult' )
1050  ->will( $this->returnCallback( function ( array $categories ) use ( $fakeResults ) {
1051  $return = [];
1052  foreach ( $categories as $category => $unused ) {
1053  if ( isset( $fakeResults[$category] ) ) {
1054  $return[] = $fakeResults[$category];
1055  }
1056  }
1057  return new FakeResultWrapper( $return );
1058  } ) );
1059 
1060  if ( $variantLinkCallback ) {
1061  $mockContLang = $this->getMockBuilder( Language::class )
1062  ->setConstructorArgs( [ 'en' ] )
1063  ->setMethods( [ 'findVariantLink' ] )
1064  ->getMock();
1065  $mockContLang->expects( $this->any() )
1066  ->method( 'findVariantLink' )
1067  ->will( $this->returnCallback( $variantLinkCallback ) );
1068  $this->setContentLang( $mockContLang );
1069  }
1070 
1071  $this->assertSame( [], $op->getCategories() );
1072 
1073  return $op;
1074  }
1075 
1076  private function doCategoryAsserts( $op, $expectedNormal, $expectedHidden ) {
1077  $this->assertSame( array_merge( $expectedHidden, $expectedNormal ), $op->getCategories() );
1078  $this->assertSame( $expectedNormal, $op->getCategories( 'normal' ) );
1079  $this->assertSame( $expectedHidden, $op->getCategories( 'hidden' ) );
1080  }
1081 
1082  private function doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden ) {
1083  $catLinks = $op->getCategoryLinks();
1084  $this->assertSame( (bool)$expectedNormal + (bool)$expectedHidden, count( $catLinks ) );
1085  if ( $expectedNormal ) {
1086  $this->assertSame( count( $expectedNormal ), count( $catLinks['normal'] ) );
1087  }
1088  if ( $expectedHidden ) {
1089  $this->assertSame( count( $expectedHidden ), count( $catLinks['hidden'] ) );
1090  }
1091 
1092  foreach ( $expectedNormal as $i => $name ) {
1093  $this->assertContains( $name, $catLinks['normal'][$i] );
1094  }
1095  foreach ( $expectedHidden as $i => $name ) {
1096  $this->assertContains( $name, $catLinks['hidden'][$i] );
1097  }
1098  }
1099 
1100  public function provideGetCategories() {
1101  return [
1102  'No categories' => [ [], [], null, [], [] ],
1103  'Simple test' => [
1104  [ 'Test1' => 'Some sortkey', 'Test2' => 'A different sortkey' ],
1105  [ 'Test1' => (object)[ 'pp_value' => 1, 'page_title' => 'Test1' ],
1106  'Test2' => (object)[ 'page_title' => 'Test2' ] ],
1107  null,
1108  [ 'Test2' ],
1109  [ 'Test1' ],
1110  ],
1111  'Invalid title' => [
1112  [ '[' => '[', 'Test' => 'Test' ],
1113  [ 'Test' => (object)[ 'page_title' => 'Test' ] ],
1114  null,
1115  [ 'Test' ],
1116  [],
1117  ],
1118  'Variant link' => [
1119  [ 'Test' => 'Test', 'Estay' => 'Estay' ],
1120  [ 'Test' => (object)[ 'page_title' => 'Test' ] ],
1121  function ( &$link, &$title ) {
1122  if ( $link === 'Estay' ) {
1123  $link = 'Test';
1125  }
1126  },
1127  // For adding one by one, the variant gets added as well as the original category,
1128  // but if you add them all together the second time gets skipped.
1129  [ 'onebyone' => [ 'Test', 'Test' ], 'default' => [ 'Test' ] ],
1130  [],
1131  ],
1132  ];
1133  }
1134 
1138  public function testGetCategoriesInvalid() {
1139  $this->setExpectedException( InvalidArgumentException::class,
1140  'Invalid category type given: hiddne' );
1141 
1142  $op = $this->newInstance();
1143  $op->getCategories( 'hiddne' );
1144  }
1145 
1146  // @todo Should we test addCategoryLinksToLBAndGetResult? If so, how? Insert some test rows in
1147  // the DB?
1148 
1155  public function testIndicators() {
1156  $op = $this->newInstance();
1157  $this->assertSame( [], $op->getIndicators() );
1158 
1159  $op->setIndicators( [] );
1160  $this->assertSame( [], $op->getIndicators() );
1161 
1162  // Test sorting alphabetically
1163  $op->setIndicators( [ 'b' => 'x', 'a' => 'y' ] );
1164  $this->assertSame( [ 'a' => 'y', 'b' => 'x' ], $op->getIndicators() );
1165 
1166  // Test overwriting existing keys
1167  $op->setIndicators( [ 'c' => 'z', 'a' => 'w' ] );
1168  $this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'z' ], $op->getIndicators() );
1169 
1170  // Test with addParserOutputMetadata
1171  $pOut1 = $this->createParserOutputStub( 'getIndicators', [ 'c' => 'u', 'd' => 'v' ] );
1172  $op->addParserOutputMetadata( $pOut1 );
1173  $this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
1174  $op->getIndicators() );
1175 
1176  // Test with addParserOutput
1177  $pOut2 = $this->createParserOutputStub( 'getIndicators', [ 'a' => '!!!' ] );
1178  $op->addParserOutput( $pOut2 );
1179  $this->assertSame( [ 'a' => '!!!', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
1180  $op->getIndicators() );
1181  }
1182 
1187  public function testAddHelpLink() {
1188  $op = $this->newInstance();
1189 
1190  $op->addHelpLink( 'Manual:PHP unit testing' );
1191  $indicators = $op->getIndicators();
1192  $this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
1193  $this->assertContains( 'Manual:PHP_unit_testing', $indicators['mw-helplink'] );
1194 
1195  $op->addHelpLink( 'https://phpunit.de', true );
1196  $indicators = $op->getIndicators();
1197  $this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
1198  $this->assertContains( 'https://phpunit.de', $indicators['mw-helplink'] );
1199  $this->assertNotContains( 'mediawiki', $indicators['mw-helplink'] );
1200  $this->assertNotContains( 'Manual:PHP', $indicators['mw-helplink'] );
1201  }
1202 
1210  public function testBodyHTML() {
1211  $op = $this->newInstance();
1212  $this->assertSame( '', $op->getHTML() );
1213 
1214  $op->addHTML( 'a' );
1215  $this->assertSame( 'a', $op->getHTML() );
1216 
1217  $op->addHTML( 'b' );
1218  $this->assertSame( 'ab', $op->getHTML() );
1219 
1220  $op->prependHTML( 'c' );
1221  $this->assertSame( 'cab', $op->getHTML() );
1222 
1223  $op->addElement( 'p', [ 'id' => 'foo' ], 'd' );
1224  $this->assertSame( 'cab<p id="foo">d</p>', $op->getHTML() );
1225 
1226  $op->clearHTML();
1227  $this->assertSame( '', $op->getHTML() );
1228  }
1229 
1235  public function testRevisionId( $newVal, $expected ) {
1236  $op = $this->newInstance();
1237 
1238  $this->assertNull( $op->setRevisionId( $newVal ) );
1239  $this->assertSame( $expected, $op->getRevisionId() );
1240  $this->assertSame( $expected, $op->setRevisionId( null ) );
1241  $this->assertNull( $op->getRevisionId() );
1242  }
1243 
1244  public function provideRevisionId() {
1245  return [
1246  [ null, null ],
1247  [ 7, 7 ],
1248  [ -1, -1 ],
1249  [ 3.2, 3 ],
1250  [ '0', 0 ],
1251  [ '32% finished', 32 ],
1252  [ false, 0 ],
1253  ];
1254  }
1255 
1260  public function testRevisionTimestamp() {
1261  $op = $this->newInstance();
1262  $this->assertNull( $op->getRevisionTimestamp() );
1263 
1264  $this->assertNull( $op->setRevisionTimestamp( 'abc' ) );
1265  $this->assertSame( 'abc', $op->getRevisionTimestamp() );
1266  $this->assertSame( 'abc', $op->setRevisionTimestamp( null ) );
1267  $this->assertNull( $op->getRevisionTimestamp() );
1268  }
1269 
1274  public function testFileVersion() {
1275  $op = $this->newInstance();
1276  $this->assertNull( $op->getFileVersion() );
1277 
1278  $stubFile = $this->createMock( File::class );
1279  $stubFile->method( 'exists' )->willReturn( true );
1280  $stubFile->method( 'getTimestamp' )->willReturn( '12211221123321' );
1281  $stubFile->method( 'getSha1' )->willReturn( 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' );
1282 
1283  $op->setFileVersion( $stubFile );
1284 
1285  $this->assertEquals(
1286  [ 'time' => '12211221123321', 'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' ],
1287  $op->getFileVersion()
1288  );
1289 
1290  $stubMissingFile = $this->createMock( File::class );
1291  $stubMissingFile->method( 'exists' )->willReturn( false );
1292 
1293  $op->setFileVersion( $stubMissingFile );
1294  $this->assertNull( $op->getFileVersion() );
1295 
1296  $op->setFileVersion( $stubFile );
1297  $this->assertNotNull( $op->getFileVersion() );
1298 
1299  $op->setFileVersion( null );
1300  $this->assertNull( $op->getFileVersion() );
1301  }
1302 
1307  private function createParserOutputStub( ...$args ) {
1308  if ( count( $args ) === 0 ) {
1309  $retVals = [];
1310  } elseif ( count( $args ) === 1 ) {
1311  $retVals = $args[0];
1312  } elseif ( count( $args ) === 2 ) {
1313  $retVals = [ $args[0] => $args[1] ];
1314  }
1315  $pOut = $this->getMock( ParserOutput::class );
1316  foreach ( $retVals as $method => $retVal ) {
1317  $pOut->method( $method )->willReturn( $retVal );
1318  }
1319 
1320  $arrayReturningMethods = [
1321  'getCategories',
1322  'getFileSearchOptions',
1323  'getHeadItems',
1324  'getIndicators',
1325  'getLanguageLinks',
1326  'getOutputHooks',
1327  'getTemplateIds',
1328  ];
1329 
1330  foreach ( $arrayReturningMethods as $method ) {
1331  $pOut->method( $method )->willReturn( [] );
1332  }
1333 
1334  return $pOut;
1335  }
1336 
1342  public function testTemplateIds() {
1343  $op = $this->newInstance();
1344  $this->assertSame( [], $op->getTemplateIds() );
1345 
1346  // Test with no template id's
1347  $stubPOEmpty = $this->createParserOutputStub();
1348  $op->addParserOutputMetadata( $stubPOEmpty );
1349  $this->assertSame( [], $op->getTemplateIds() );
1350 
1351  // Test with some arbitrary template id's
1352  $ids = [
1353  NS_MAIN => [ 'A' => 3, 'B' => 17 ],
1354  NS_TALK => [ 'C' => 31 ],
1355  NS_MEDIA => [ 'D' => -1 ],
1356  ];
1357 
1358  $stubPO1 = $this->createParserOutputStub( 'getTemplateIds', $ids );
1359 
1360  $op->addParserOutputMetadata( $stubPO1 );
1361  $this->assertSame( $ids, $op->getTemplateIds() );
1362 
1363  // Test merging with a second set of id's
1364  $stubPO2 = $this->createParserOutputStub( 'getTemplateIds', [
1365  NS_MAIN => [ 'E' => 1234 ],
1366  NS_PROJECT => [ 'F' => 5678 ],
1367  ] );
1368 
1369  $finalIds = [
1370  NS_MAIN => [ 'E' => 1234, 'A' => 3, 'B' => 17 ],
1371  NS_TALK => [ 'C' => 31 ],
1372  NS_MEDIA => [ 'D' => -1 ],
1373  NS_PROJECT => [ 'F' => 5678 ],
1374  ];
1375 
1376  $op->addParserOutput( $stubPO2 );
1377  $this->assertSame( $finalIds, $op->getTemplateIds() );
1378 
1379  // Test merging with an empty set of id's
1380  $op->addParserOutputMetadata( $stubPOEmpty );
1381  $this->assertSame( $finalIds, $op->getTemplateIds() );
1382  }
1383 
1389  public function testFileSearchOptions() {
1390  $op = $this->newInstance();
1391  $this->assertSame( [], $op->getFileSearchOptions() );
1392 
1393  // Test with no files
1394  $stubPOEmpty = $this->createParserOutputStub();
1395 
1396  $op->addParserOutputMetadata( $stubPOEmpty );
1397  $this->assertSame( [], $op->getFileSearchOptions() );
1398 
1399  // Test with some arbitrary files
1400  $files1 = [
1401  'A' => [ 'time' => null, 'sha1' => '' ],
1402  'B' => [
1403  'time' => '12211221123321',
1404  'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05',
1405  ],
1406  ];
1407 
1408  $stubPO1 = $this->createParserOutputStub( 'getFileSearchOptions', $files1 );
1409 
1410  $op->addParserOutput( $stubPO1 );
1411  $this->assertSame( $files1, $op->getFileSearchOptions() );
1412 
1413  // Test merging with a second set of files
1414  $files2 = [
1415  'C' => [ 'time' => null, 'sha1' => '' ],
1416  'B' => [ 'time' => null, 'sha1' => '' ],
1417  ];
1418 
1419  $stubPO2 = $this->createParserOutputStub( 'getFileSearchOptions', $files2 );
1420 
1421  $op->addParserOutputMetadata( $stubPO2 );
1422  $this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
1423 
1424  // Test merging with an empty set of files
1425  $op->addParserOutput( $stubPOEmpty );
1426  $this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
1427  }
1428 
1441  public function testAddWikiText( $method, array $args, $expected ) {
1442  $op = $this->newInstance();
1443  $this->assertSame( '', $op->getHTML() );
1444 
1445  $this->hideDeprecated( 'OutputPage::addWikiTextTitle' );
1446  $this->hideDeprecated( 'OutputPage::addWikiTextWithTitle' );
1447  $this->hideDeprecated( 'OutputPage::addWikiTextTidy' );
1448  $this->hideDeprecated( 'OutputPage::addWikiTextTitleTidy' );
1449  if ( in_array(
1450  $method,
1451  [ 'addWikiTextWithTitle', 'addWikiTextTitleTidy', 'addWikiTextTitle' ]
1452  ) && count( $args ) >= 2 && $args[1] === null ) {
1453  // Special placeholder because we can't get the actual title in the provider
1454  $args[1] = $op->getTitle();
1455  }
1456  if ( in_array(
1457  $method,
1458  [ 'addWikiTextAsInterface', 'addWikiTextAsContent' ]
1459  ) && count( $args ) >= 3 && $args[2] === null ) {
1460  // Special placeholder because we can't get the actual title in the provider
1461  $args[2] = $op->getTitle();
1462  }
1463 
1464  $op->$method( ...$args );
1465  $this->assertSame( $expected, $op->getHTML() );
1466  }
1467 
1468  public function provideAddWikiText() {
1469  $tests = [
1470  'addWikiText' => [
1471  // Not tidied; this API is deprecated.
1472  'Simple wikitext' => [
1473  [ "'''Bold'''" ],
1474  "<p><b>Bold</b>\n</p>",
1475  ], 'List at start' => [
1476  [ '* List' ],
1477  "<ul><li>List</li></ul>\n",
1478  ], 'List not at start' => [
1479  [ '* Not a list', false ],
1480  '* Not a list',
1481  ], 'Non-interface' => [
1482  [ "'''Bold'''", true, false ],
1483  "<p><b>Bold</b>\n</p>",
1484  ], 'No section edit links' => [
1485  [ '== Title ==' ],
1486  "<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>\n",
1487  ],
1488  ],
1489  'addWikiTextWithTitle' => [
1490  // Untidied; this API is deprecated
1491  'With title at start' => [
1492  [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ) ],
1493  "<ul><li>Some page</li></ul>\n",
1494  ], 'With title at start' => [
1495  [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ), false ],
1496  "* Some page",
1497  ],
1498  ],
1499  'addWikiTextAsInterface' => [
1500  // Preferred interface: output is tidied
1501  'Simple wikitext' => [
1502  [ "'''Bold'''" ],
1503  "<p><b>Bold</b>\n</p>",
1504  ], 'Untidy wikitext' => [
1505  [ "<b>Bold" ],
1506  "<p><b>Bold\n</b></p>",
1507  ], 'List at start' => [
1508  [ '* List' ],
1509  "<ul><li>List</li></ul>\n",
1510  ], 'List not at start' => [
1511  [ '* Not a list', false ],
1512  '<p>* Not a list</p>',
1513  ], 'No section edit links' => [
1514  [ '== Title ==' ],
1515  "<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>\n",
1516  ], 'With title at start' => [
1517  [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1518  "<ul><li>Some page</li></ul>\n",
1519  ], 'With title at start' => [
1520  [ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
1521  "<p>* Some page</p>",
1522  ], 'Untidy input' => [
1523  [ '<b>{{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1524  "<p><b>Some page\n</b></p>",
1525  ],
1526  ],
1527  'addWikiTextAsContent' => [
1528  // Preferred interface: output is tidied
1529  'SpecialNewimages' => [
1530  [ "<p lang='en' dir='ltr'>\nMy message" ],
1531  '<p lang="en" dir="ltr">' . "\nMy message\n</p>"
1532  ], 'List at start' => [
1533  [ '* List' ],
1534  "<ul><li>List</li></ul>\n",
1535  ], 'List not at start' => [
1536  [ '* <b>Not a list', false ],
1537  '<p>* <b>Not a list</b></p>',
1538  ], 'With title at start' => [
1539  [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
1540  "<ul><li>Some page</li></ul>\n",
1541  ], 'With title at start' => [
1542  [ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
1543  "<p>* Some page</p>",
1544  ], 'EditPage' => [
1545  [ "<div class='mw-editintro'>{{PAGENAME}}", true, Title::newFromText( 'Talk:Some page' ) ],
1546  '<div class="mw-editintro">' . "Some page\n</div>"
1547  ],
1548  ],
1549  'wrapWikiTextAsInterface' => [
1550  'Simple' => [
1551  [ 'wrapperClass', 'text' ],
1552  "<div class=\"wrapperClass\"><p>text\n</p></div>"
1553  ], 'Spurious </div>' => [
1554  [ 'wrapperClass', 'text</div><div>more' ],
1555  "<div class=\"wrapperClass\"><p>text</p><div>more\n</div></div>"
1556  ], 'Extra newlines would break <p> wrappers' => [
1557  [ 'two classes', "1\n\n2\n\n3" ],
1558  "<div class=\"two classes\"><p>1\n</p><p>2\n</p><p>3\n</p></div>"
1559  ], 'Other unclosed tags' => [
1560  [ 'error', 'a<b>c<i>d' ],
1561  "<div class=\"error\"><p>a<b>c<i>d\n</i></b></p></div>"
1562  ],
1563  ],
1564  ];
1565 
1566  // Test all the others on addWikiTextTitle as well
1567  foreach ( $tests['addWikiText'] as $key => $val ) {
1568  $args = [ $val[0][0], null, $val[0][1] ?? true, false, $val[0][2] ?? true ];
1569  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1570  array_merge( [ $args ], array_slice( $val, 1 ) );
1571  }
1572  foreach ( $tests['addWikiTextWithTitle'] as $key => $val ) {
1573  $args = [ $val[0][0], $val[0][1], $val[0][2] ?? true ];
1574  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1575  array_merge( [ $args ], array_slice( $val, 1 ) );
1576  }
1577  foreach ( $tests['addWikiTextAsInterface'] as $key => $val ) {
1578  $args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, true ];
1579  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1580  array_merge( [ $args ], array_slice( $val, 1 ) );
1581  }
1582  foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
1583  $args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, false ];
1584  $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
1585  array_merge( [ $args ], array_slice( $val, 1 ) );
1586  }
1587  // addWikiTextTidy / addWikiTextTitleTidy were old aliases of
1588  // addWikiTextAsContent
1589  foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
1590  if ( count( $val[0] ) > 2 ) {
1591  $args = [ $val[0][0], $val[0][2], $val[0][1] ?? true ];
1592  $tests['addWikiTextTitleTidy']["$key (addWikiTextTitleTidy)"] =
1593  array_merge( [ $args ], array_slice( $val, 1 ) );
1594  } else {
1595  $args = [ $val[0][0], $val[0][1] ?? true ];
1596  $tests['addWikiTextTidy']["$key (addWikiTextTidy)"] =
1597  array_merge( [ $args ], array_slice( $val, 1 ) );
1598  }
1599  }
1600 
1601  // We have to reformat our array to match what PHPUnit wants
1602  $ret = [];
1603  foreach ( $tests as $key => $subarray ) {
1604  foreach ( $subarray as $subkey => $val ) {
1605  $val = array_merge( [ $key ], $val );
1606  $ret[$subkey] = $val;
1607  }
1608  }
1609 
1610  return $ret;
1611  }
1612 
1616  public function testAddWikiTextNoTitle() {
1617  $this->setExpectedException( MWException::class, 'Title is null' );
1618 
1619  $op = $this->newInstance( [], null, 'notitle' );
1620  $op->addWikiText( 'a' );
1621  }
1622 
1626  public function testAddWikiTextAsInterfaceNoTitle() {
1627  $this->setExpectedException( MWException::class, 'Title is null' );
1628 
1629  $op = $this->newInstance( [], null, 'notitle' );
1630  $op->addWikiTextAsInterface( 'a' );
1631  }
1632 
1636  public function testAddWikiTextAsContentNoTitle() {
1637  $this->setExpectedException( MWException::class, 'Title is null' );
1638 
1639  $op = $this->newInstance( [], null, 'notitle' );
1640  $op->addWikiTextAsContent( 'a' );
1641  }
1642 
1646  public function testAddWikiMsg() {
1647  $msg = wfMessage( 'parentheses' );
1648  $this->assertSame( '(a)', $msg->rawParams( 'a' )->plain() );
1649 
1650  $op = $this->newInstance();
1651  $this->assertSame( '', $op->getHTML() );
1652  $op->addWikiMsg( 'parentheses', "<b>a" );
1653  // This is known to be bad unbalanced HTML; this will be fixed
1654  // by I743f4185a03403f8d9b9db010ff1ee4e9342e062 (T198214)
1655  $this->assertSame( "<p>(<b>a)\n</p>", $op->getHTML() );
1656  }
1657 
1661  public function testWrapWikiMsg() {
1662  $msg = wfMessage( 'parentheses' );
1663  $this->assertSame( '(a)', $msg->rawParams( 'a' )->plain() );
1664 
1665  $op = $this->newInstance();
1666  $this->assertSame( '', $op->getHTML() );
1667  $op->wrapWikiMsg( '[$1]', [ 'parentheses', "<b>a" ] );
1668  // This is known to be bad unbalanced HTML; this will be fixed
1669  // by I743f4185a03403f8d9b9db010ff1ee4e9342e062 (T198214)
1670  $this->assertSame( "<p>[(<b>a)]\n</p>", $op->getHTML() );
1671  }
1672 
1677  public function testNoGallery() {
1678  $op = $this->newInstance();
1679  $this->assertFalse( $op->mNoGallery );
1680 
1681  $stubPO1 = $this->createParserOutputStub( 'getNoGallery', true );
1682  $op->addParserOutputMetadata( $stubPO1 );
1683  $this->assertTrue( $op->mNoGallery );
1684 
1685  $stubPO2 = $this->createParserOutputStub( 'getNoGallery', false );
1686  $op->addParserOutput( $stubPO2 );
1687  $this->assertFalse( $op->mNoGallery );
1688  }
1689 
1690  private static $parserOutputHookCalled;
1691 
1695  public function testParserOutputHooks() {
1696  $op = $this->newInstance();
1697  $pOut = $this->createParserOutputStub( 'getOutputHooks', [
1698  [ 'myhook', 'banana' ],
1699  [ 'yourhook', 'kumquat' ],
1700  [ 'theirhook', 'hippopotamus' ],
1701  ] );
1702 
1703  self::$parserOutputHookCalled = [];
1704 
1705  $this->setMwGlobals( 'wgParserOutputHooks', [
1706  'myhook' => function ( OutputPage $innerOp, ParserOutput $innerPOut, $data )
1707  use ( $op, $pOut ) {
1708  $this->assertSame( $op, $innerOp );
1709  $this->assertSame( $pOut, $innerPOut );
1710  $this->assertSame( 'banana', $data );
1711  self::$parserOutputHookCalled[] = 'closure';
1712  },
1713  'yourhook' => [ $this, 'parserOutputHookCallback' ],
1714  'theirhook' => [ __CLASS__, 'parserOutputHookCallbackStatic' ],
1715  'uncalled' => function () {
1716  $this->assertTrue( false );
1717  },
1718  ] );
1719 
1720  $op->addParserOutputMetadata( $pOut );
1721 
1722  $this->assertSame( [ 'closure', 'callback', 'static' ], self::$parserOutputHookCalled );
1723  }
1724 
1725  public function parserOutputHookCallback(
1726  OutputPage $op, ParserOutput $pOut, $data
1727  ) {
1728  $this->assertSame( 'kumquat', $data );
1729 
1730  self::$parserOutputHookCalled[] = 'callback';
1731  }
1732 
1733  public static function parserOutputHookCallbackStatic(
1734  OutputPage $op, ParserOutput $pOut, $data
1735  ) {
1736  // All the assert methods are actually static, who knew!
1737  self::assertSame( 'hippopotamus', $data );
1738 
1739  self::$parserOutputHookCalled[] = 'static';
1740  }
1741 
1742  // @todo Make sure to test the following in addParserOutputMetadata() as well when we add tests
1743  // for them:
1744  // * addModules()
1745  // * addModuleScripts()
1746  // * addModuleStyles()
1747  // * addJsConfigVars()
1748  // * enableOOUI()
1749  // Otherwise those lines of addParserOutputMetadata() will be reported as covered, but we won't
1750  // be testing they actually work.
1751 
1755  public function testAddParserOutputText() {
1756  $op = $this->newInstance();
1757  $this->assertSame( '', $op->getHTML() );
1758 
1759  $pOut = $this->createParserOutputStub( 'getText', '<some text>' );
1760 
1761  $op->addParserOutputMetadata( $pOut );
1762  $this->assertSame( '', $op->getHTML() );
1763 
1764  $op->addParserOutputText( $pOut );
1765  $this->assertSame( '<some text>', $op->getHTML() );
1766  }
1767 
1771  public function testAddParserOutput() {
1772  $op = $this->newInstance();
1773  $this->assertSame( '', $op->getHTML() );
1774  $this->assertFalse( $op->showNewSectionLink() );
1775 
1776  $pOut = $this->createParserOutputStub( [
1777  'getText' => '<some text>',
1778  'getNewSection' => true,
1779  ] );
1780 
1781  $op->addParserOutput( $pOut );
1782  $this->assertSame( '<some text>', $op->getHTML() );
1783  $this->assertTrue( $op->showNewSectionLink() );
1784  }
1785 
1789  public function testAddTemplate() {
1790  $template = $this->getMock( QuickTemplate::class );
1791  $template->method( 'getHTML' )->willReturn( '<abc>&def;' );
1792 
1793  $op = $this->newInstance();
1794  $op->addTemplate( $template );
1795 
1796  $this->assertSame( '<abc>&def;', $op->getHTML() );
1797  }
1798 
1806  public function testParse( array $args, $expectedHTML ) {
1807  $op = $this->newInstance();
1808  $this->assertSame( $expectedHTML, $op->parse( ...$args ) );
1809  }
1810 
1815  public function testParseInline( array $args, $expectedHTML, $expectedHTMLInline = null ) {
1816  if ( count( $args ) > 3 ) {
1817  // $language param not supported
1818  $this->assertTrue( true );
1819  return;
1820  }
1821  $op = $this->newInstance();
1822  $this->assertSame( $expectedHTMLInline ?? $expectedHTML, $op->parseInline( ...$args ) );
1823  }
1824 
1825  public function provideParse() {
1826  return [
1827  'List at start of line (content)' => [
1828  [ '* List', true, false ],
1829  "<div class=\"mw-parser-output\"><ul><li>List</li></ul>\n</div>",
1830  "<ul><li>List</li></ul>\n",
1831  ],
1832  'List at start of line (interface)' => [
1833  [ '* List', true, true ],
1834  "<ul><li>List</li></ul>\n",
1835  ],
1836  'List not at start (content)' => [
1837  [ "* ''Not'' list", false, false ],
1838  '<div class="mw-parser-output">* <i>Not</i> list</div>',
1839  '* <i>Not</i> list',
1840  ],
1841  'List not at start (interface)' => [
1842  [ "* ''Not'' list", false, true ],
1843  '* <i>Not</i> list',
1844  ],
1845  'Interface message' => [
1846  [ "''Italic''", true, true ],
1847  "<p><i>Italic</i>\n</p>",
1848  '<i>Italic</i>',
1849  ],
1850  'formatnum (content)' => [
1851  [ '{{formatnum:123456.789}}', true, false ],
1852  "<div class=\"mw-parser-output\"><p>123,456.789\n</p></div>",
1853  "123,456.789",
1854  ],
1855  'formatnum (interface)' => [
1856  [ '{{formatnum:123456.789}}', true, true ],
1857  "<p>123,456.789\n</p>",
1858  "123,456.789",
1859  ],
1860  'Language (content)' => [
1861  [ '{{formatnum:123456.789}}', true, false, Language::factory( 'is' ) ],
1862  "<div class=\"mw-parser-output\"><p>123.456,789\n</p></div>",
1863  ],
1864  'Language (interface)' => [
1865  [ '{{formatnum:123456.789}}', true, true, Language::factory( 'is' ) ],
1866  "<p>123.456,789\n</p>",
1867  '123.456,789',
1868  ],
1869  'No section edit links' => [
1870  [ '== Header ==' ],
1871  '<div class="mw-parser-output"><h2><span class="mw-headline" id="Header">' .
1872  "Header</span></h2>\n</div>",
1873  '<h2><span class="mw-headline" id="Header">Header</span></h2>' .
1874  "\n",
1875  ]
1876  ];
1877  }
1878 
1886  public function testParseAsContent(
1887  array $args, $expectedHTML, $expectedHTMLInline = null
1888  ) {
1889  $op = $this->newInstance();
1890  $this->assertSame( $expectedHTML, $op->parseAsContent( ...$args ) );
1891  }
1892 
1900  public function testParseAsInterface(
1901  array $args, $expectedHTML, $expectedHTMLInline = null
1902  ) {
1903  $op = $this->newInstance();
1904  $this->assertSame( $expectedHTML, $op->parseAsInterface( ...$args ) );
1905  }
1906 
1911  public function testParseInlineAsInterface(
1912  array $args, $expectedHTML, $expectedHTMLInline = null
1913  ) {
1914  $op = $this->newInstance();
1915  $this->assertSame(
1916  $expectedHTMLInline ?? $expectedHTML,
1917  $op->parseInlineAsInterface( ...$args )
1918  );
1919  }
1920 
1921  public function provideParseAs() {
1922  return [
1923  'List at start of line' => [
1924  [ '* List', true ],
1925  "<ul><li>List</li></ul>\n",
1926  ],
1927  'List not at start' => [
1928  [ "* ''Not'' list", false ],
1929  '<p>* <i>Not</i> list</p>',
1930  '* <i>Not</i> list',
1931  ],
1932  'Italics' => [
1933  [ "''Italic''", true ],
1934  "<p><i>Italic</i>\n</p>",
1935  '<i>Italic</i>',
1936  ],
1937  'formatnum' => [
1938  [ '{{formatnum:123456.789}}', true ],
1939  "<p>123,456.789\n</p>",
1940  "123,456.789",
1941  ],
1942  'No section edit links' => [
1943  [ '== Header ==' ],
1944  '<h2><span class="mw-headline" id="Header">Header</span></h2>' .
1945  "\n",
1946  ]
1947  ];
1948  }
1949 
1953  public function testParseNullTitle() {
1954  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
1955  $op = $this->newInstance( [], null, 'notitle' );
1956  $op->parse( '' );
1957  }
1958 
1962  public function testParseInlineNullTitle() {
1963  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
1964  $op = $this->newInstance( [], null, 'notitle' );
1965  $op->parseInline( '' );
1966  }
1967 
1971  public function testParseAsContentNullTitle() {
1972  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
1973  $op = $this->newInstance( [], null, 'notitle' );
1974  $op->parseAsContent( '' );
1975  }
1976 
1980  public function testParseAsInterfaceNullTitle() {
1981  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
1982  $op = $this->newInstance( [], null, 'notitle' );
1983  $op->parseAsInterface( '' );
1984  }
1985 
1989  public function testParseInlineAsInterfaceNullTitle() {
1990  $this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
1991  $op = $this->newInstance( [], null, 'notitle' );
1992  $op->parseInlineAsInterface( '' );
1993  }
1994 
1999  public function testCdnMaxage() {
2000  $op = $this->newInstance();
2001  $wrapper = TestingAccessWrapper::newFromObject( $op );
2002  $this->assertSame( 0, $wrapper->mCdnMaxage );
2003 
2004  $op->setCdnMaxage( -1 );
2005  $this->assertSame( -1, $wrapper->mCdnMaxage );
2006 
2007  $op->setCdnMaxage( 120 );
2008  $this->assertSame( 120, $wrapper->mCdnMaxage );
2009 
2010  $op->setCdnMaxage( 60 );
2011  $this->assertSame( 60, $wrapper->mCdnMaxage );
2012 
2013  $op->setCdnMaxage( 180 );
2014  $this->assertSame( 180, $wrapper->mCdnMaxage );
2015 
2016  $op->lowerCdnMaxage( 240 );
2017  $this->assertSame( 180, $wrapper->mCdnMaxage );
2018 
2019  $op->setCdnMaxage( 300 );
2020  $this->assertSame( 240, $wrapper->mCdnMaxage );
2021 
2022  $op->lowerCdnMaxage( 120 );
2023  $this->assertSame( 120, $wrapper->mCdnMaxage );
2024 
2025  $op->setCdnMaxage( 180 );
2026  $this->assertSame( 120, $wrapper->mCdnMaxage );
2027 
2028  $op->setCdnMaxage( 60 );
2029  $this->assertSame( 60, $wrapper->mCdnMaxage );
2030 
2031  $op->setCdnMaxage( 240 );
2032  $this->assertSame( 120, $wrapper->mCdnMaxage );
2033  }
2034 
2036  private static $fakeTime;
2037 
2046  public function testAdaptCdnTTL( array $args, $expected, array $options = [] ) {
2047  try {
2048  MWTimestamp::setFakeTime( self::$fakeTime );
2049 
2050  $op = $this->newInstance();
2051  // Set a high maxage so that it will get reduced by adaptCdnTTL(). The default maxage
2052  // is 0, so adaptCdnTTL() won't mutate the object at all.
2053  $initial = $options['initialMaxage'] ?? 86400;
2054  $op->setCdnMaxage( $initial );
2055 
2056  $op->adaptCdnTTL( ...$args );
2057  } finally {
2058  MWTimestamp::setFakeTime( false );
2059  }
2060 
2061  $wrapper = TestingAccessWrapper::newFromObject( $op );
2062 
2063  // Special rules for false/null
2064  if ( $args[0] === null || $args[0] === false ) {
2065  $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
2066  $op->setCdnMaxage( $expected + 1 );
2067  $this->assertSame( $expected + 1, $wrapper->mCdnMaxage, 'member value after new set' );
2068  return;
2069  }
2070 
2071  $this->assertSame( $expected, $wrapper->mCdnMaxageLimit, 'limit value' );
2072 
2073  if ( $initial >= $expected ) {
2074  $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value' );
2075  } else {
2076  $this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
2077  }
2078 
2079  $op->setCdnMaxage( $expected + 1 );
2080  $this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value after new set' );
2081  }
2082 
2083  public function provideAdaptCdnTTL() {
2084  global $wgSquidMaxage;
2085  $now = time();
2086  self::$fakeTime = $now;
2087  return [
2088  'Five minutes ago' => [ [ $now - 300 ], 270 ],
2089  'Now' => [ [ +0 ], IExpiringStore::TTL_MINUTE ],
2090  'Five minutes from now' => [ [ $now + 300 ], IExpiringStore::TTL_MINUTE ],
2091  'Five minutes ago, initial maxage four minutes' =>
2092  [ [ $now - 300 ], 270, [ 'initialMaxage' => 240 ] ],
2093  'A very long time ago' => [ [ $now - 1000000000 ], $wgSquidMaxage ],
2094  'Initial maxage zero' => [ [ $now - 300 ], 270, [ 'initialMaxage' => 0 ] ],
2095 
2096  'false' => [ [ false ], IExpiringStore::TTL_MINUTE ],
2097  'null' => [ [ null ], IExpiringStore::TTL_MINUTE ],
2098  "'0'" => [ [ '0' ], IExpiringStore::TTL_MINUTE ],
2099  'Empty string' => [ [ '' ], IExpiringStore::TTL_MINUTE ],
2100  // @todo These give incorrect results due to timezones, how to test?
2101  //"'now'" => [ [ 'now' ], IExpiringStore::TTL_MINUTE ],
2102  //"'parse error'" => [ [ 'parse error' ], IExpiringStore::TTL_MINUTE ],
2103 
2104  'Now, minTTL 0' => [ [ $now, 0 ], IExpiringStore::TTL_MINUTE ],
2105  'Now, minTTL 0.000001' => [ [ $now, 0.000001 ], 0 ],
2106  'A very long time ago, maxTTL even longer' =>
2107  [ [ $now - 1000000000, 0, 1000000001 ], 900000000 ],
2108  ];
2109  }
2110 
2116  public function testClientCache() {
2117  $op = $this->newInstance();
2118 
2119  // Test initial value
2120  $this->assertSame( true, $op->enableClientCache( null ) );
2121  // Test that calling with null doesn't change the value
2122  $this->assertSame( true, $op->enableClientCache( null ) );
2123 
2124  // Test setting to false
2125  $this->assertSame( true, $op->enableClientCache( false ) );
2126  $this->assertSame( false, $op->enableClientCache( null ) );
2127  // Test that calling with null doesn't change the value
2128  $this->assertSame( false, $op->enableClientCache( null ) );
2129 
2130  // Test that a cacheable ParserOutput doesn't set to true
2131  $pOutCacheable = $this->createParserOutputStub( 'isCacheable', true );
2132  $op->addParserOutputMetadata( $pOutCacheable );
2133  $this->assertSame( false, $op->enableClientCache( null ) );
2134 
2135  // Test setting back to true
2136  $this->assertSame( false, $op->enableClientCache( true ) );
2137  $this->assertSame( true, $op->enableClientCache( null ) );
2138 
2139  // Test that an uncacheable ParserOutput does set to false
2140  $pOutUncacheable = $this->createParserOutputStub( 'isCacheable', false );
2141  $op->addParserOutput( $pOutUncacheable );
2142  $this->assertSame( false, $op->enableClientCache( null ) );
2143  }
2144 
2148  public function testGetCacheVaryCookies() {
2149  global $wgCookiePrefix, $wgDBname;
2150  $op = $this->newInstance();
2151  $prefix = $wgCookiePrefix !== false ? $wgCookiePrefix : $wgDBname;
2152  $expectedCookies = [
2153  "{$prefix}Token",
2154  "{$prefix}LoggedOut",
2155  "{$prefix}_session",
2156  'forceHTTPS',
2157  'cookie1',
2158  'cookie2',
2159  ];
2160 
2161  // We have to reset the cookies because getCacheVaryCookies may have already been called
2162  TestingAccessWrapper::newFromClass( OutputPage::class )->cacheVaryCookies = null;
2163 
2164  $this->setMwGlobals( 'wgCacheVaryCookies', [ 'cookie1' ] );
2165  $this->setTemporaryHook( 'GetCacheVaryCookies',
2166  function ( $innerOP, &$cookies ) use ( $op, $expectedCookies ) {
2167  $this->assertSame( $op, $innerOP );
2168  $cookies[] = 'cookie2';
2169  $this->assertSame( $expectedCookies, $cookies );
2170  }
2171  );
2172 
2173  $this->assertSame( $expectedCookies, $op->getCacheVaryCookies() );
2174  }
2175 
2179  public function testHaveCacheVaryCookies() {
2180  $request = new FauxRequest();
2181  $op = $this->newInstance( [], $request );
2182 
2183  // No cookies are set.
2184  $this->assertFalse( $op->haveCacheVaryCookies() );
2185 
2186  // 'Token' is present but empty, so it shouldn't count.
2187  $request->setCookie( 'Token', '' );
2188  $this->assertFalse( $op->haveCacheVaryCookies() );
2189 
2190  // 'Token' present and nonempty.
2191  $request->setCookie( 'Token', '123' );
2192  $this->assertTrue( $op->haveCacheVaryCookies() );
2193  }
2194 
2207  public function testVaryHeaders( array $calls, array $cookies, $vary, $key ) {
2208  // Get rid of default Vary fields
2209  $op = $this->getMockBuilder( OutputPage::class )
2210  ->setConstructorArgs( [ new RequestContext() ] )
2211  ->setMethods( [ 'getCacheVaryCookies' ] )
2212  ->getMock();
2213  $op->expects( $this->any() )
2214  ->method( 'getCacheVaryCookies' )
2215  ->will( $this->returnValue( $cookies ) );
2216  TestingAccessWrapper::newFromObject( $op )->mVaryHeader = [];
2217 
2218  $this->hideDeprecated( '$wgUseKeyHeader' );
2219  foreach ( $calls as $call ) {
2220  $op->addVaryHeader( ...$call );
2221  }
2222  $this->assertEquals( $vary, $op->getVaryHeader(), 'Vary:' );
2223  $this->assertEquals( $key, $op->getKeyHeader(), 'Key:' );
2224  }
2225 
2226  public function provideVaryHeaders() {
2227  // note: getKeyHeader() automatically adds Vary: Cookie
2228  return [
2229  'No header' => [
2230  [],
2231  [],
2232  'Vary: ',
2233  'Key: Cookie',
2234  ],
2235  'Single header' => [
2236  [
2237  [ 'Cookie' ],
2238  ],
2239  [],
2240  'Vary: Cookie',
2241  'Key: Cookie',
2242  ],
2243  'Non-unique headers' => [
2244  [
2245  [ 'Cookie' ],
2246  [ 'Accept-Language' ],
2247  [ 'Cookie' ],
2248  ],
2249  [],
2250  'Vary: Cookie, Accept-Language',
2251  'Key: Cookie,Accept-Language',
2252  ],
2253  'Two headers with single options' => [
2254  [
2255  [ 'Cookie', [ 'param=phpsessid' ] ],
2256  [ 'Accept-Language', [ 'substr=en' ] ],
2257  ],
2258  [],
2259  'Vary: Cookie, Accept-Language',
2260  'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
2261  ],
2262  'One header with multiple options' => [
2263  [
2264  [ 'Cookie', [ 'param=phpsessid', 'param=userId' ] ],
2265  ],
2266  [],
2267  'Vary: Cookie',
2268  'Key: Cookie;param=phpsessid;param=userId',
2269  ],
2270  'Duplicate option' => [
2271  [
2272  [ 'Cookie', [ 'param=phpsessid' ] ],
2273  [ 'Cookie', [ 'param=phpsessid' ] ],
2274  [ 'Accept-Language', [ 'substr=en', 'substr=en' ] ],
2275  ],
2276  [],
2277  'Vary: Cookie, Accept-Language',
2278  'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
2279  ],
2280  'Same header, different options' => [
2281  [
2282  [ 'Cookie', [ 'param=phpsessid' ] ],
2283  [ 'Cookie', [ 'param=userId' ] ],
2284  ],
2285  [],
2286  'Vary: Cookie',
2287  'Key: Cookie;param=phpsessid;param=userId',
2288  ],
2289  'No header, vary cookies' => [
2290  [],
2291  [ 'cookie1', 'cookie2' ],
2292  'Vary: Cookie',
2293  'Key: Cookie;param=cookie1;param=cookie2',
2294  ],
2295  'Cookie header with option plus vary cookies' => [
2296  [
2297  [ 'Cookie', [ 'param=cookie1' ] ],
2298  ],
2299  [ 'cookie2', 'cookie3' ],
2300  'Vary: Cookie',
2301  'Key: Cookie;param=cookie1;param=cookie2;param=cookie3',
2302  ],
2303  'Non-cookie header plus vary cookies' => [
2304  [
2305  [ 'Accept-Language' ],
2306  ],
2307  [ 'cookie' ],
2308  'Vary: Accept-Language, Cookie',
2309  'Key: Accept-Language,Cookie;param=cookie',
2310  ],
2311  'Cookie and non-cookie headers plus vary cookies' => [
2312  [
2313  [ 'Cookie', [ 'param=cookie1' ] ],
2314  [ 'Accept-Language' ],
2315  ],
2316  [ 'cookie2' ],
2317  'Vary: Cookie, Accept-Language',
2318  'Key: Cookie;param=cookie1;param=cookie2,Accept-Language',
2319  ],
2320  ];
2321  }
2322 
2326  public function testVaryHeaderDefault() {
2327  $op = $this->newInstance();
2328  $this->assertSame( 'Vary: Accept-Encoding, Cookie', $op->getVaryHeader() );
2329  }
2330 
2337  public function testLinkHeaders( array $headers, $result ) {
2338  $op = $this->newInstance();
2339 
2340  foreach ( $headers as $header ) {
2341  $op->addLinkHeader( $header );
2342  }
2343 
2344  $this->assertEquals( $result, $op->getLinkHeader() );
2345  }
2346 
2347  public function provideLinkHeaders() {
2348  return [
2349  [
2350  [],
2351  false
2352  ],
2353  [
2354  [ '<https://foo/bar.jpg>;rel=preload;as=image' ],
2355  'Link: <https://foo/bar.jpg>;rel=preload;as=image',
2356  ],
2357  [
2358  [
2359  '<https://foo/bar.jpg>;rel=preload;as=image',
2360  '<https://foo/baz.jpg>;rel=preload;as=image'
2361  ],
2362  'Link: <https://foo/bar.jpg>;rel=preload;as=image,<https://foo/baz.jpg>;' .
2363  'rel=preload;as=image',
2364  ],
2365  ];
2366  }
2367 
2373  public function testAddAcceptLanguage(
2374  $code, array $variants, array $expected, array $options = []
2375  ) {
2376  $req = new FauxRequest( in_array( 'varianturl', $options ) ? [ 'variant' => 'x' ] : [] );
2377  $op = $this->newInstance( [], $req, in_array( 'notitle', $options ) ? 'notitle' : null );
2378 
2379  if ( !in_array( 'notitle', $options ) ) {
2380  $mockLang = $this->getMock( Language::class );
2381 
2382  if ( in_array( 'varianturl', $options ) ) {
2383  $mockLang->expects( $this->never() )->method( $this->anything() );
2384  } else {
2385  $mockLang->method( 'hasVariants' )->willReturn( count( $variants ) > 1 );
2386  $mockLang->method( 'getVariants' )->willReturn( $variants );
2387  $mockLang->method( 'getCode' )->willReturn( $code );
2388  }
2389 
2390  $mockTitle = $this->getMock( Title::class );
2391  $mockTitle->method( 'getPageLanguage' )->willReturn( $mockLang );
2392 
2393  $op->setTitle( $mockTitle );
2394  }
2395 
2396  // This will run addAcceptLanguage()
2397  $op->sendCacheControl();
2398 
2399  $this->hideDeprecated( '$wgUseKeyHeader' );
2400  $keyHeader = $op->getKeyHeader();
2401 
2402  if ( !$expected ) {
2403  $this->assertFalse( strpos( 'Accept-Language', $keyHeader ) );
2404  return;
2405  }
2406 
2407  $keyHeader = explode( ' ', $keyHeader, 2 )[1];
2408  $keyHeader = explode( ',', $keyHeader );
2409 
2410  $acceptLanguage = null;
2411  foreach ( $keyHeader as $item ) {
2412  if ( strpos( $item, 'Accept-Language;' ) === 0 ) {
2413  $acceptLanguage = $item;
2414  break;
2415  }
2416  }
2417 
2418  $expectedString = 'Accept-Language;substr=' . implode( ';substr=', $expected );
2419  $this->assertSame( $expectedString, $acceptLanguage );
2420  }
2421 
2422  public function provideAddAcceptLanguage() {
2423  return [
2424  'No variants' => [ 'en', [ 'en' ], [] ],
2425  'One simple variant' => [ 'en', [ 'en', 'en-x-piglatin' ], [ 'en-x-piglatin' ] ],
2426  'Multiple variants with BCP47 alternatives' => [
2427  'zh',
2428  [ 'zh', 'zh-hans', 'zh-cn', 'zh-tw' ],
2429  [ 'zh-hans', 'zh-Hans', 'zh-cn', 'zh-Hans-CN', 'zh-tw', 'zh-Hant-TW' ],
2430  ],
2431  'No title' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'notitle' ] ],
2432  'Variant in URL' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'varianturl' ] ],
2433  ];
2434  }
2435 
2443  public function testClickjacking() {
2444  $op = $this->newInstance();
2445  $this->assertTrue( $op->getPreventClickjacking() );
2446 
2447  $op->allowClickjacking();
2448  $this->assertFalse( $op->getPreventClickjacking() );
2449 
2450  $op->preventClickjacking();
2451  $this->assertTrue( $op->getPreventClickjacking() );
2452 
2453  $op->preventClickjacking( false );
2454  $this->assertFalse( $op->getPreventClickjacking() );
2455 
2456  $pOut1 = $this->createParserOutputStub( 'preventClickjacking', true );
2457  $op->addParserOutputMetadata( $pOut1 );
2458  $this->assertTrue( $op->getPreventClickjacking() );
2459 
2460  // The ParserOutput can't allow, only prevent
2461  $pOut2 = $this->createParserOutputStub( 'preventClickjacking', false );
2462  $op->addParserOutputMetadata( $pOut2 );
2463  $this->assertTrue( $op->getPreventClickjacking() );
2464 
2465  // Reset to test with addParserOutput()
2466  $op->allowClickjacking();
2467  $this->assertFalse( $op->getPreventClickjacking() );
2468 
2469  $op->addParserOutput( $pOut1 );
2470  $this->assertTrue( $op->getPreventClickjacking() );
2471 
2472  $op->addParserOutput( $pOut2 );
2473  $this->assertTrue( $op->getPreventClickjacking() );
2474  }
2475 
2481  public function testGetFrameOptions(
2482  $breakFrames, $preventClickjacking, $editPageFrameOptions, $expected
2483  ) {
2484  $op = $this->newInstance( [
2485  'BreakFrames' => $breakFrames,
2486  'EditPageFrameOptions' => $editPageFrameOptions,
2487  ] );
2488  $op->preventClickjacking( $preventClickjacking );
2489 
2490  $this->assertSame( $expected, $op->getFrameOptions() );
2491  }
2492 
2493  public function provideGetFrameOptions() {
2494  return [
2495  'BreakFrames true' => [ true, false, false, 'DENY' ],
2496  'Allow clickjacking locally' => [ false, false, 'DENY', false ],
2497  'Allow clickjacking globally' => [ false, true, false, false ],
2498  'DENY globally' => [ false, true, 'DENY', 'DENY' ],
2499  'SAMEORIGIN' => [ false, true, 'SAMEORIGIN', 'SAMEORIGIN' ],
2500  'BreakFrames with SAMEORIGIN' => [ true, true, 'SAMEORIGIN', 'DENY' ],
2501  ];
2502  }
2503 
2511  public function testMakeResourceLoaderLink( $args, $expectedHtml ) {
2512  $this->setMwGlobals( [
2513  'wgResourceLoaderDebug' => false,
2514  'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
2515  'wgCSPReportOnlyHeader' => true,
2516  ] );
2517  $class = new ReflectionClass( OutputPage::class );
2518  $method = $class->getMethod( 'makeResourceLoaderLink' );
2519  $method->setAccessible( true );
2520  $ctx = new RequestContext();
2521  $ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) );
2522  $ctx->setLanguage( 'en' );
2523  $out = new OutputPage( $ctx );
2524  $nonce = $class->getProperty( 'CSPNonce' );
2525  $nonce->setAccessible( true );
2526  $nonce->setValue( $out, 'secret' );
2527  $rl = $out->getResourceLoader();
2528  $rl->setMessageBlobStore( new NullMessageBlobStore() );
2529  $rl->register( [
2530  'test.foo' => new ResourceLoaderTestModule( [
2531  'script' => 'mw.test.foo( { a: true } );',
2532  'styles' => '.mw-test-foo { content: "style"; }',
2533  ] ),
2534  'test.bar' => new ResourceLoaderTestModule( [
2535  'script' => 'mw.test.bar( { a: true } );',
2536  'styles' => '.mw-test-bar { content: "style"; }',
2537  ] ),
2538  'test.baz' => new ResourceLoaderTestModule( [
2539  'script' => 'mw.test.baz( { a: true } );',
2540  'styles' => '.mw-test-baz { content: "style"; }',
2541  ] ),
2542  'test.quux' => new ResourceLoaderTestModule( [
2543  'script' => 'mw.test.baz( { token: 123 } );',
2544  'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
2545  'group' => 'private',
2546  ] ),
2547  'test.noscript' => new ResourceLoaderTestModule( [
2548  'styles' => '.stuff { color: red; }',
2549  'group' => 'noscript',
2550  ] ),
2551  'test.group.foo' => new ResourceLoaderTestModule( [
2552  'script' => 'mw.doStuff( "foo" );',
2553  'group' => 'foo',
2554  ] ),
2555  'test.group.bar' => new ResourceLoaderTestModule( [
2556  'script' => 'mw.doStuff( "bar" );',
2557  'group' => 'bar',
2558  ] ),
2559  ] );
2560  $links = $method->invokeArgs( $out, $args );
2561  $actualHtml = strval( $links );
2562  $this->assertEquals( $expectedHtml, $actualHtml );
2563  }
2564 
2565  public static function provideMakeResourceLoaderLink() {
2566  // phpcs:disable Generic.Files.LineLength
2567  return [
2568  // Single only=scripts load
2569  [
2570  [ 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ],
2571  "<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
2572  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.foo\u0026only=scripts\u0026skin=fallback");'
2573  . "});</script>"
2574  ],
2575  // Multiple only=styles load
2576  [
2577  [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
2578 
2579  '<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback"/>'
2580  ],
2581  // Private embed (only=scripts)
2582  [
2583  [ 'test.quux', ResourceLoaderModule::TYPE_SCRIPTS ],
2584  "<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
2585  . "mw.test.baz({token:123});\nmw.loader.state({\"test.quux\":\"ready\"});"
2586  . "});</script>"
2587  ],
2588  // Load private module (combined)
2589  [
2590  [ 'test.quux', ResourceLoaderModule::TYPE_COMBINED ],
2591  "<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
2592  . "mw.loader.implement(\"test.quux@1ev0ijv\",function($,jQuery,require,module){"
2593  . "mw.test.baz({token:123});},{\"css\":[\".mw-icon{transition:none}"
2594  . "\"]});});</script>"
2595  ],
2596  // Load no modules
2597  [
2599  '',
2600  ],
2601  // noscript group
2602  [
2603  [ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ],
2604  '<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.noscript&amp;only=styles&amp;skin=fallback"/></noscript>'
2605  ],
2606  // Load two modules in separate groups
2607  [
2608  [ [ 'test.group.foo', 'test.group.bar' ], ResourceLoaderModule::TYPE_COMBINED ],
2609  "<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
2610  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.group.bar\u0026skin=fallback");'
2611  . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.group.foo\u0026skin=fallback");'
2612  . "});</script>"
2613  ],
2614  ];
2615  // phpcs:enable
2616  }
2617 
2623  public function testBuildExemptModules( array $exemptStyleModules, $expect ) {
2624  $this->setMwGlobals( [
2625  'wgResourceLoaderDebug' => false,
2626  'wgLoadScript' => '/w/load.php',
2627  // Stub wgCacheEpoch as it influences getVersionHash used for the
2628  // urls in the expected HTML
2629  'wgCacheEpoch' => '20140101000000',
2630  ] );
2631 
2632  // Set up stubs
2633  $ctx = new RequestContext();
2634  $ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) );
2635  $ctx->setLanguage( 'en' );
2636  $op = $this->getMockBuilder( OutputPage::class )
2637  ->setConstructorArgs( [ $ctx ] )
2638  ->setMethods( [ 'buildCssLinksArray' ] )
2639  ->getMock();
2640  $op->expects( $this->any() )
2641  ->method( 'buildCssLinksArray' )
2642  ->willReturn( [] );
2643  $rl = $op->getResourceLoader();
2644  $rl->setMessageBlobStore( new NullMessageBlobStore() );
2645 
2646  // Register custom modules
2647  $rl->register( [
2648  'example.site.a' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
2649  'example.site.b' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
2650  'example.user' => new ResourceLoaderTestModule( [ 'group' => 'user' ] ),
2651  ] );
2652 
2653  $op = TestingAccessWrapper::newFromObject( $op );
2654  $op->rlExemptStyleModules = $exemptStyleModules;
2655  $this->assertEquals(
2656  $expect,
2657  strval( $op->buildExemptModules() )
2658  );
2659  }
2660 
2661  public static function provideBuildExemptModules() {
2662  // phpcs:disable Generic.Files.LineLength
2663  return [
2664  'empty' => [
2665  'exemptStyleModules' => [],
2666  '<meta name="ResourceLoaderDynamicStyles" content=""/>',
2667  ],
2668  'empty sets' => [
2669  'exemptStyleModules' => [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ],
2670  '<meta name="ResourceLoaderDynamicStyles" content=""/>',
2671  ],
2672  'default logged-out' => [
2673  'exemptStyleModules' => [ 'site' => [ 'site.styles' ] ],
2674  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2675  '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>',
2676  ],
2677  'default logged-in' => [
2678  'exemptStyleModules' => [ 'site' => [ 'site.styles' ], 'user' => [ 'user.styles' ] ],
2679  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2680  '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>' . "\n" .
2681  '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=user.styles&amp;only=styles&amp;skin=fallback&amp;version=1ai9g6t"/>',
2682  ],
2683  'custom modules' => [
2684  'exemptStyleModules' => [
2685  'site' => [ 'site.styles', 'example.site.a', 'example.site.b' ],
2686  'user' => [ 'user.styles', 'example.user' ],
2687  ],
2688  '<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
2689  '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=example.site.a%2Cb&amp;only=styles&amp;skin=fallback"/>' . "\n" .
2690  '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=site.styles&amp;only=styles&amp;skin=fallback"/>' . "\n" .
2691  '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=example.user&amp;only=styles&amp;skin=fallback&amp;version=0a56zyi"/>' . "\n" .
2692  '<link rel="stylesheet" href="/w/load.php?debug=false&amp;lang=en&amp;modules=user.styles&amp;only=styles&amp;skin=fallback&amp;version=1ai9g6t"/>',
2693  ],
2694  ];
2695  // phpcs:enable
2696  }
2697 
2703  public function testTransformResourcePath( $baseDir, $basePath, $uploadDir = null,
2704  $uploadPath = null, $path = null, $expected = null
2705  ) {
2706  if ( $path === null ) {
2707  // Skip optional $uploadDir and $uploadPath
2708  $path = $uploadDir;
2709  $expected = $uploadPath;
2710  $uploadDir = "$baseDir/images";
2711  $uploadPath = "$basePath/images";
2712  }
2713  $this->setMwGlobals( 'IP', $baseDir );
2714  $conf = new HashConfig( [
2715  'ResourceBasePath' => $basePath,
2716  'UploadDirectory' => $uploadDir,
2717  'UploadPath' => $uploadPath,
2718  ] );
2719 
2720  // Some of these paths don't exist and will cause warnings
2721  Wikimedia\suppressWarnings();
2722  $actual = OutputPage::transformResourcePath( $conf, $path );
2723  Wikimedia\restoreWarnings();
2724 
2725  $this->assertEquals( $expected ?: $path, $actual );
2726  }
2727 
2728  public static function provideTransformFilePath() {
2729  $baseDir = dirname( __DIR__ ) . '/data/media';
2730  return [
2731  // File that matches basePath, and exists. Hash found and appended.
2732  [
2733  'baseDir' => $baseDir, 'basePath' => '/w',
2734  '/w/test.jpg',
2735  '/w/test.jpg?edcf2'
2736  ],
2737  // File that matches basePath, but not found on disk. Empty query.
2738  [
2739  'baseDir' => $baseDir, 'basePath' => '/w',
2740  '/w/unknown.png',
2741  '/w/unknown.png?'
2742  ],
2743  // File not matching basePath. Ignored.
2744  [
2745  'baseDir' => $baseDir, 'basePath' => '/w',
2746  '/files/test.jpg'
2747  ],
2748  // Empty string. Ignored.
2749  [
2750  'baseDir' => $baseDir, 'basePath' => '/w',
2751  '',
2752  ''
2753  ],
2754  // Similar path, but with domain component. Ignored.
2755  [
2756  'baseDir' => $baseDir, 'basePath' => '/w',
2757  '//example.org/w/test.jpg'
2758  ],
2759  [
2760  'baseDir' => $baseDir, 'basePath' => '/w',
2761  'https://example.org/w/test.jpg'
2762  ],
2763  // Unrelated path with domain component. Ignored.
2764  [
2765  'baseDir' => $baseDir, 'basePath' => '/w',
2766  'https://example.org/files/test.jpg'
2767  ],
2768  [
2769  'baseDir' => $baseDir, 'basePath' => '/w',
2770  '//example.org/files/test.jpg'
2771  ],
2772  // Unrelated path with domain, and empty base path (root mw install). Ignored.
2773  [
2774  'baseDir' => $baseDir, 'basePath' => '',
2775  'https://example.org/files/test.jpg'
2776  ],
2777  [
2778  'baseDir' => $baseDir, 'basePath' => '',
2779  // T155310
2780  '//example.org/files/test.jpg'
2781  ],
2782  // Check UploadPath before ResourceBasePath (T155146)
2783  [
2784  'baseDir' => dirname( $baseDir ), 'basePath' => '',
2785  'uploadDir' => $baseDir, 'uploadPath' => '/images',
2786  '/images/test.jpg',
2787  '/images/test.jpg?edcf2'
2788  ],
2789  ];
2790  }
2791 
2806  protected function assertTransformCssMediaCase( $args ) {
2807  $queryData = [];
2808  if ( isset( $args['printableQuery'] ) ) {
2809  $queryData['printable'] = $args['printableQuery'];
2810  }
2811 
2812  if ( isset( $args['handheldQuery'] ) ) {
2813  $queryData['handheld'] = $args['handheldQuery'];
2814  }
2815 
2816  $fauxRequest = new FauxRequest( $queryData, false );
2817  $this->setMwGlobals( [
2818  'wgRequest' => $fauxRequest,
2819  ] );
2820 
2821  $actualReturn = OutputPage::transformCssMedia( $args['media'] );
2822  $this->assertSame( $args['expectedReturn'], $actualReturn, $args['message'] );
2823  }
2824 
2830  public function testPrintRequests() {
2831  $this->assertTransformCssMediaCase( [
2832  'printableQuery' => '1',
2833  'media' => 'screen',
2834  'expectedReturn' => null,
2835  'message' => 'On printable request, screen returns null'
2836  ] );
2837 
2838  $this->assertTransformCssMediaCase( [
2839  'printableQuery' => '1',
2840  'media' => self::SCREEN_MEDIA_QUERY,
2841  'expectedReturn' => null,
2842  'message' => 'On printable request, screen media query returns null'
2843  ] );
2844 
2845  $this->assertTransformCssMediaCase( [
2846  'printableQuery' => '1',
2847  'media' => self::SCREEN_ONLY_MEDIA_QUERY,
2848  'expectedReturn' => null,
2849  'message' => 'On printable request, screen media query with only returns null'
2850  ] );
2851 
2852  $this->assertTransformCssMediaCase( [
2853  'printableQuery' => '1',
2854  'media' => 'print',
2855  'expectedReturn' => '',
2856  'message' => 'On printable request, media print returns empty string'
2857  ] );
2858  }
2859 
2865  public function testScreenRequests() {
2866  $this->assertTransformCssMediaCase( [
2867  'media' => 'screen',
2868  'expectedReturn' => 'screen',
2869  'message' => 'On screen request, screen media type is preserved'
2870  ] );
2871 
2872  $this->assertTransformCssMediaCase( [
2873  'media' => 'handheld',
2874  'expectedReturn' => 'handheld',
2875  'message' => 'On screen request, handheld media type is preserved'
2876  ] );
2877 
2878  $this->assertTransformCssMediaCase( [
2879  'media' => self::SCREEN_MEDIA_QUERY,
2880  'expectedReturn' => self::SCREEN_MEDIA_QUERY,
2881  'message' => 'On screen request, screen media query is preserved.'
2882  ] );
2883 
2884  $this->assertTransformCssMediaCase( [
2885  'media' => self::SCREEN_ONLY_MEDIA_QUERY,
2886  'expectedReturn' => self::SCREEN_ONLY_MEDIA_QUERY,
2887  'message' => 'On screen request, screen media query with only is preserved.'
2888  ] );
2889 
2890  $this->assertTransformCssMediaCase( [
2891  'media' => 'print',
2892  'expectedReturn' => 'print',
2893  'message' => 'On screen request, print media type is preserved'
2894  ] );
2895  }
2896 
2902  public function testHandheld() {
2903  $this->assertTransformCssMediaCase( [
2904  'handheldQuery' => '1',
2905  'media' => 'handheld',
2906  'expectedReturn' => '',
2907  'message' => 'On request with handheld querystring and media is handheld, returns empty string'
2908  ] );
2909 
2910  $this->assertTransformCssMediaCase( [
2911  'handheldQuery' => '1',
2912  'media' => 'screen',
2913  'expectedReturn' => null,
2914  'message' => 'On request with handheld querystring and media is screen, returns null'
2915  ] );
2916  }
2917 
2923  public function testIsTOCEnabled() {
2924  $op = $this->newInstance();
2925  $this->assertFalse( $op->isTOCEnabled() );
2926 
2927  $pOut1 = $this->createParserOutputStub( 'getTOCHTML', false );
2928  $op->addParserOutputMetadata( $pOut1 );
2929  $this->assertFalse( $op->isTOCEnabled() );
2930 
2931  $pOut2 = $this->createParserOutputStub( 'getTOCHTML', true );
2932  $op->addParserOutput( $pOut2 );
2933  $this->assertTrue( $op->isTOCEnabled() );
2934 
2935  // The parser output doesn't disable the TOC after it was enabled
2936  $op->addParserOutputMetadata( $pOut1 );
2937  $this->assertTrue( $op->isTOCEnabled() );
2938  }
2939 
2945  public function testPreloadLinkHeaders( $config, $result ) {
2946  $this->setMwGlobals( $config );
2947  $ctx = $this->getMockBuilder( ResourceLoaderContext::class )
2948  ->disableOriginalConstructor()->getMock();
2949  $module = new ResourceLoaderSkinModule();
2950 
2951  $this->assertEquals( [ $result ], $module->getHeaders( $ctx ) );
2952  }
2953 
2954  public function providePreloadLinkHeaders() {
2955  return [
2956  [
2957  [
2958  'wgResourceBasePath' => '/w',
2959  'wgLogo' => '/img/default.png',
2960  'wgLogoHD' => [
2961  '1.5x' => '/img/one-point-five.png',
2962  '2x' => '/img/two-x.png',
2963  ],
2964  ],
2965  'Link: </img/default.png>;rel=preload;as=image;media=' .
2966  'not all and (min-resolution: 1.5dppx),' .
2967  '</img/one-point-five.png>;rel=preload;as=image;media=' .
2968  '(min-resolution: 1.5dppx) and (max-resolution: 1.999999dppx),' .
2969  '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
2970  ],
2971  [
2972  [
2973  'wgResourceBasePath' => '/w',
2974  'wgLogo' => '/img/default.png',
2975  'wgLogoHD' => false,
2976  ],
2977  'Link: </img/default.png>;rel=preload;as=image'
2978  ],
2979  [
2980  [
2981  'wgResourceBasePath' => '/w',
2982  'wgLogo' => '/img/default.png',
2983  'wgLogoHD' => [
2984  '2x' => '/img/two-x.png',
2985  ],
2986  ],
2987  'Link: </img/default.png>;rel=preload;as=image;media=' .
2988  'not all and (min-resolution: 2dppx),' .
2989  '</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
2990  ],
2991  [
2992  [
2993  'wgResourceBasePath' => '/w',
2994  'wgLogo' => '/img/default.png',
2995  'wgLogoHD' => [
2996  'svg' => '/img/vector.svg',
2997  ],
2998  ],
2999  'Link: </img/vector.svg>;rel=preload;as=image'
3000 
3001  ],
3002  [
3003  [
3004  'wgResourceBasePath' => '/w',
3005  'wgLogo' => '/w/test.jpg',
3006  'wgLogoHD' => false,
3007  'wgUploadPath' => '/w/images',
3008  'IP' => dirname( __DIR__ ) . '/data/media',
3009  ],
3010  'Link: </w/test.jpg?edcf2>;rel=preload;as=image',
3011  ],
3012  ];
3013  }
3014 
3018  private function newInstance( $config = [], WebRequest $request = null, $options = [] ) {
3019  $context = new RequestContext();
3020 
3021  $context->setConfig( new MultiConfig( [
3022  new HashConfig( $config + [
3023  'AppleTouchIcon' => false,
3024  'DisableLangConversion' => true,
3025  'EnableCanonicalServerLink' => false,
3026  'Favicon' => false,
3027  'Feed' => false,
3028  'LanguageCode' => false,
3029  'ReferrerPolicy' => false,
3030  'RightsPage' => false,
3031  'RightsUrl' => false,
3032  'UniversalEditButton' => false,
3033  ] ),
3034  $context->getConfig()
3035  ] ) );
3036 
3037  if ( !in_array( 'notitle', (array)$options ) ) {
3038  $context->setTitle( Title::newFromText( 'My test page' ) );
3039  }
3040 
3041  if ( $request ) {
3042  $context->setRequest( $request );
3043  }
3044 
3045  return new OutputPage( $context );
3046  }
3047 }
3048 
3052 class NullMessageBlobStore extends MessageBlobStore {
3053  public function get( ResourceLoader $resourceLoader, $modules, $lang ) {
3054  return [];
3055  }
3056 
3057  public function updateModule( $name, ResourceLoaderModule $module, $lang ) {
3058  }
3059 
3060  public function updateMessage( $key ) {
3061  }
3062 
3063  public function clear() {
3064  }
3065 }
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:33
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:280
$template
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub 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:813
ParserOutput
Definition: ParserOutput.php:25
false
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
$wgDBname
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
$context
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:2675
ResourceLoaderModule\TYPE_COMBINED
const TYPE_COMBINED
Definition: ResourceLoaderModule.php:39
MediaWikiTestCase\getTestUser
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
Definition: MediaWikiTestCase.php:179
MultiConfig
Provides a fallback sequence for Config objects.
Definition: MultiConfig.php:28
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:33
HashConfig
A Config instance which stores all settings as a member variable.
Definition: HashConfig.php:28
captcha-old.count
count
Definition: captcha-old.py:249
$result
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED since 1.16! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) '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 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) '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:2034
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1954
MessageBlobStore\clear
clear()
Invalidate cache keys for all known modules.
Definition: MessageBlobStore.php:181
anything
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
Definition: distributors.txt:39
$req
this hook is for auditing only $req
Definition: hooks.txt:1018
$params
$params
Definition: styleTest.css.php:44
$resourceLoader
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 such as when responding to a resource loader request or generating HTML output & $resourceLoader
Definition: hooks.txt:2675
IExpiringStore\TTL_MINUTE
const TTL_MINUTE
Definition: IExpiringStore.php:34
$base
$base
Definition: generateLocalAutoload.php:11
php
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
NS_MAIN
const NS_MAIN
Definition: Defines.php:64
$query
null for the 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:1627
Html\linkedScript
static linkedScript( $url, $nonce=null)
Output a "<script>" tag linking to the given URL, e.g., "<script src=foo.js></script>".
Definition: Html.php:593
MessageBlobStore\updateMessage
updateMessage( $key)
Invalidate cache keys for modules using this message key.
Definition: MessageBlobStore.php:169
$html
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:2036
$title
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:964
NS_PROJECT
const NS_PROJECT
Definition: Defines.php:68
$titles
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
MediaWikiTestCase\setMwGlobals
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown.
Definition: MediaWikiTestCase.php:706
$matches
$matches
Definition: NoLocalSettings.php:24
MediaWikiTestCase
Definition: MediaWikiTestCase.php:16
$modules
$modules
Definition: HTMLFormElement.php:12
MediaWikiTestCase\hideDeprecated
hideDeprecated( $function)
Don't throw a warning if $function is deprecated and called later.
Definition: MediaWikiTestCase.php:1940
ResourceLoaderModule\TYPE_SCRIPTS
const TYPE_SCRIPTS
Definition: ResourceLoaderModule.php:37
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
$code
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub 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:813
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:78
RequestContext
Group all the pieces relevant to the context of a request into one instance.
Definition: RequestContext.php:32
array
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
ResourceLoaderTestModule
Definition: ResourceLoaderTestCase.php:86
$request
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:2675
MediaWikiTestCase\setContentLang
setContentLang( $lang)
Definition: MediaWikiTestCase.php:1063
$name
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:302
any
they could even be mouse clicks or menu items whatever suits your program You should also get your if any
Definition: COPYING.txt:326
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:573
MediaWikiTestCase\editPage
editPage( $pageName, $text, $summary='', $defaultNs=NS_MAIN)
Edits or creates a page/revision.
Definition: MediaWikiTestCase.php:2333
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:52
Html\inlineScript
static inlineScript( $contents, $nonce=null)
Output an HTML script tag with the given contents.
Definition: Html.php:567
$header
$header
Definition: updateCredits.php:35
$ret
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:2036
MediaWikiTestCase\setUp
setUp()
Definition: MediaWikiTestCase.php:501
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:41
$args
if( $line===false) $args
Definition: cdb.php:64
ResourceLoaderModule
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: ResourceLoaderModule.php:35
$options
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:2036
$path
$path
Definition: NoLocalSettings.php:25
as
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
MessageBlobStore
This class generates message blobs for use by ResourceLoader modules.
Definition: MessageBlobStore.php:37
$basePath
$basePath
Definition: addSite.php:5
$link
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition: hooks.txt:3090
ResourceLoaderModule\TYPE_STYLES
const TYPE_STYLES
Definition: ResourceLoaderModule.php:38
true
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:2036
NS_TALK
const NS_TALK
Definition: Defines.php:65
Language\factory
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:214
class
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
object
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
MediaWikiTestCase\setTemporaryHook
setTemporaryHook( $hookName, $handler)
Create a temporary hook handler which will be reset by tearDown.
Definition: MediaWikiTestCase.php:2291
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:232
wfMessage
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
ResourceLoaderSkinModule
Definition: ResourceLoaderSkinModule.php:24
$wgCookiePrefix
$wgCookiePrefix
Cookies generated by MediaWiki have names starting with this prefix.
Definition: DefaultSettings.php:6047
$queries
$queries
Definition: profileinfo.php:416
SkinFactory\getDefaultInstance
static getDefaultInstance()
Definition: SkinFactory.php:50
$wgSquidMaxage
$wgSquidMaxage
Cache TTL for the CDN sent as s-maxage (without ESI) or Surrogate-Control (with ESI).
Definition: DefaultSettings.php:2777
MediaWikiTestCase\tearDown
tearDown()
Definition: MediaWikiTestCase.php:544
$out
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub 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:813
$type
$type
Definition: testCompression.php:48