2013-01-14 03:26:15 +00:00
|
|
|
<?php
|
|
|
|
|
|
2023-09-20 07:54:42 +00:00
|
|
|
use MediaWiki\Config\HashConfig;
|
|
|
|
|
use MediaWiki\Config\MultiConfig;
|
2024-02-08 14:56:54 +00:00
|
|
|
use MediaWiki\Context\RequestContext;
|
2023-02-16 19:27:21 +00:00
|
|
|
use MediaWiki\Html\Html;
|
2023-08-10 16:06:35 +00:00
|
|
|
use MediaWiki\Language\RawMessage;
|
2020-05-06 00:26:09 +00:00
|
|
|
use MediaWiki\Languages\LanguageConverterFactory;
|
2022-07-15 00:07:38 +00:00
|
|
|
use MediaWiki\MainConfigNames;
|
2023-09-05 17:31:53 +00:00
|
|
|
use MediaWiki\Output\OutputPage;
|
2021-03-04 15:55:20 +00:00
|
|
|
use MediaWiki\Page\PageIdentity;
|
2021-04-08 19:11:06 +00:00
|
|
|
use MediaWiki\Page\PageReference;
|
|
|
|
|
use MediaWiki\Page\PageReferenceValue;
|
|
|
|
|
use MediaWiki\Page\PageStoreRecord;
|
2022-06-09 12:59:50 +00:00
|
|
|
use MediaWiki\Parser\ParserOutputFlags;
|
2021-03-04 15:55:20 +00:00
|
|
|
use MediaWiki\Permissions\Authority;
|
2023-02-16 12:36:41 +00:00
|
|
|
use MediaWiki\Request\ContentSecurityPolicy;
|
2022-10-28 10:04:25 +00:00
|
|
|
use MediaWiki\Request\FauxRequest;
|
2023-09-07 11:46:15 +00:00
|
|
|
use MediaWiki\Request\WebRequest;
|
2022-05-06 09:09:56 +00:00
|
|
|
use MediaWiki\ResourceLoader as RL;
|
2022-05-28 06:56:01 +00:00
|
|
|
use MediaWiki\ResourceLoader\ResourceLoader;
|
2021-03-04 15:55:20 +00:00
|
|
|
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
|
2023-03-01 20:33:26 +00:00
|
|
|
use MediaWiki\Title\Title;
|
2023-09-19 12:13:45 +00:00
|
|
|
use MediaWiki\User\User;
|
2023-08-19 03:35:06 +00:00
|
|
|
use MediaWiki\Utils\MWTimestamp;
|
2021-08-05 06:54:11 +00:00
|
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
2019-06-29 04:50:31 +00:00
|
|
|
use Wikimedia\DependencyStore\KeyValueDependencyStore;
|
2023-08-06 21:41:29 +00:00
|
|
|
use Wikimedia\LightweightObjectStore\ExpirationAwareness;
|
2022-05-27 20:38:32 +00:00
|
|
|
use Wikimedia\Rdbms\FakeResultWrapper;
|
2017-04-19 19:37:35 +00:00
|
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
|
|
2013-01-14 03:26:15 +00:00
|
|
|
/**
|
|
|
|
|
* @author Matthew Flaschen
|
|
|
|
|
*
|
2017-03-18 23:06:09 +00:00
|
|
|
* @group Database
|
2013-01-14 03:26:15 +00:00
|
|
|
* @group Output
|
2024-01-27 00:11:07 +00:00
|
|
|
* @covers \MediaWiki\Output\OutputPage
|
2013-01-14 03:26:15 +00:00
|
|
|
*/
|
2020-06-30 15:09:24 +00:00
|
|
|
class OutputPageTest extends MediaWikiIntegrationTestCase {
|
2021-03-04 15:55:20 +00:00
|
|
|
use MockAuthorityTrait;
|
2021-04-08 19:11:06 +00:00
|
|
|
use MockTitleTrait;
|
2021-03-04 15:55:20 +00:00
|
|
|
|
2020-05-16 00:27:13 +00:00
|
|
|
private const SCREEN_MEDIA_QUERY = 'screen and (min-width: 982px)';
|
|
|
|
|
private const SCREEN_ONLY_MEDIA_QUERY = 'only screen and (min-width: 982px)';
|
2022-05-24 22:15:00 +00:00
|
|
|
private const RSS_RC_LINK = '<link rel="alternate" type="application/rss+xml" title=" RSS feed" href="/w/index.php?title=Special:RecentChanges&feed=rss">';
|
|
|
|
|
private const ATOM_RC_LINK = '<link rel="alternate" type="application/atom+xml" title=" Atom feed" href="/w/index.php?title=Special:RecentChanges&feed=atom">';
|
2018-01-28 02:21:51 +00:00
|
|
|
|
2022-05-24 22:15:00 +00:00
|
|
|
private const RSS_TEST_LINK = '<link rel="alternate" type="application/rss+xml" title=""Test" RSS feed" href="fake-link">';
|
|
|
|
|
private const ATOM_TEST_LINK = '<link rel="alternate" type="application/atom+xml" title=""Test" Atom feed" href="fake-link">';
|
2021-04-04 19:18:22 +00:00
|
|
|
// phpcs:enable
|
2018-01-28 02:21:51 +00:00
|
|
|
|
2018-09-25 14:31:57 +00:00
|
|
|
// Ensure that we don't affect the global ResourceLoader state.
|
2021-07-22 03:11:47 +00:00
|
|
|
protected function setUp(): void {
|
2018-09-25 14:31:57 +00:00
|
|
|
parent::setUp();
|
|
|
|
|
ResourceLoader::clearCache();
|
2023-03-07 20:06:23 +00:00
|
|
|
|
|
|
|
|
$this->overrideConfigValues( [
|
|
|
|
|
MainConfigNames::ScriptPath => '/mw',
|
|
|
|
|
MainConfigNames::Script => '/mw/index.php',
|
|
|
|
|
MainConfigNames::ArticlePath => '/wikipage/$1',
|
|
|
|
|
MainConfigNames::Server => 'http://example.org',
|
|
|
|
|
MainConfigNames::CanonicalServer => 'https://www.example.org',
|
|
|
|
|
] );
|
2023-08-19 16:44:32 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::LanguageCode, 'en' );
|
2018-09-25 14:31:57 +00:00
|
|
|
}
|
2019-05-11 01:17:43 +00:00
|
|
|
|
2021-07-22 03:11:47 +00:00
|
|
|
protected function tearDown(): void {
|
2018-09-25 14:31:57 +00:00
|
|
|
ResourceLoader::clearCache();
|
2020-12-23 10:03:34 +00:00
|
|
|
parent::tearDown();
|
2018-09-25 14:31:57 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideRedirect
|
|
|
|
|
*/
|
|
|
|
|
public function testRedirect( $url, $code = null ) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
if ( isset( $code ) ) {
|
|
|
|
|
$op->redirect( $url, $code );
|
|
|
|
|
} else {
|
|
|
|
|
$op->redirect( $url );
|
|
|
|
|
}
|
|
|
|
|
$expectedUrl = str_replace( "\n", '', $url );
|
|
|
|
|
$this->assertSame( $expectedUrl, $op->getRedirect() );
|
|
|
|
|
$this->assertSame( $expectedUrl, $op->mRedirect );
|
|
|
|
|
$this->assertSame( $code ?? '302', $op->mRedirectCode );
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideRedirect() {
|
2018-07-23 18:26:32 +00:00
|
|
|
return [
|
|
|
|
|
[ 'http://example.com' ],
|
|
|
|
|
[ 'http://example.com', '400' ],
|
|
|
|
|
[ 'http://example.com', 'squirrels!!!' ],
|
|
|
|
|
[ "a\nb" ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-22 03:11:47 +00:00
|
|
|
private function setupFeedLinks( $feed, $types ): OutputPage {
|
2018-01-28 02:21:51 +00:00
|
|
|
$outputPage = $this->newInstance( [
|
|
|
|
|
'AdvertisedFeedTypes' => $types,
|
|
|
|
|
'Feed' => $feed,
|
|
|
|
|
'OverrideSiteFeed' => false,
|
|
|
|
|
'Script' => '/w',
|
|
|
|
|
'Sitename' => false,
|
|
|
|
|
] );
|
|
|
|
|
$outputPage->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
|
2022-07-15 00:07:38 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::Script, '/w/index.php' );
|
2018-01-28 02:21:51 +00:00
|
|
|
return $outputPage;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-30 14:25:17 +00:00
|
|
|
private function assertFeedLinks( OutputPage $outputPage, $message, $present, $non_present ) {
|
2018-01-28 02:21:51 +00:00
|
|
|
$links = $outputPage->getHeadLinksArray();
|
|
|
|
|
foreach ( $present as $link ) {
|
|
|
|
|
$this->assertContains( $link, $links, $message );
|
|
|
|
|
}
|
|
|
|
|
foreach ( $non_present as $link ) {
|
|
|
|
|
$this->assertNotContains( $link, $links, $message );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-30 14:25:17 +00:00
|
|
|
private function assertFeedUILinks( OutputPage $outputPage, $ui_links ) {
|
2018-01-28 02:21:51 +00:00
|
|
|
if ( $ui_links ) {
|
|
|
|
|
$this->assertTrue( $outputPage->isSyndicated(), 'Syndication should be offered' );
|
|
|
|
|
$this->assertGreaterThan( 0, count( $outputPage->getSyndicationLinks() ),
|
|
|
|
|
'Some syndication links should be there' );
|
|
|
|
|
} else {
|
|
|
|
|
$this->assertFalse( $outputPage->isSyndicated(), 'No syndication should be offered' );
|
2020-02-28 15:45:22 +00:00
|
|
|
$this->assertSame( [], $outputPage->getSyndicationLinks(),
|
2018-01-28 02:21:51 +00:00
|
|
|
'No syndication links should be there' );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function provideFeedLinkData() {
|
|
|
|
|
return [
|
|
|
|
|
[
|
|
|
|
|
true, [ 'rss' ], 'Only RSS RC link should be offerred',
|
|
|
|
|
[ self::RSS_RC_LINK ], [ self::ATOM_RC_LINK ]
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
true, [ 'atom' ], 'Only Atom RC link should be offerred',
|
|
|
|
|
[ self::ATOM_RC_LINK ], [ self::RSS_RC_LINK ]
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
true, [], 'No RC feed formats should be offerred',
|
|
|
|
|
[], [ self::ATOM_RC_LINK, self::RSS_RC_LINK ]
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
false, [ 'atom' ], 'No RC feeds should be offerred',
|
|
|
|
|
[], [ self::ATOM_RC_LINK, self::RSS_RC_LINK ]
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-08 16:52:57 +00:00
|
|
|
public function testSetCanonicalUrl() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->setCanonicalUrl( 'http://example.comm' );
|
|
|
|
|
$op->setCanonicalUrl( 'http://example.com' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( 'http://example.com', $op->getCanonicalUrl() );
|
|
|
|
|
|
|
|
|
|
$headLinks = $op->getHeadLinksArray();
|
|
|
|
|
|
|
|
|
|
$this->assertContains( Html::element( 'link', [
|
|
|
|
|
'rel' => 'canonical', 'href' => 'http://example.com'
|
|
|
|
|
] ), $headLinks );
|
|
|
|
|
|
|
|
|
|
$this->assertNotContains( Html::element( 'link', [
|
|
|
|
|
'rel' => 'canonical', 'href' => 'http://example.comm'
|
|
|
|
|
] ), $headLinks );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function provideGetHeadLinksArray() {
|
|
|
|
|
return [
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
'EnableCanonicalServerLink' => true,
|
|
|
|
|
],
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/xyzzy/Hello',
|
2023-02-08 16:52:57 +00:00
|
|
|
true,
|
2023-03-07 20:06:23 +00:00
|
|
|
'/xyzzy/Hello'
|
2023-02-08 16:52:57 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
'EnableCanonicalServerLink' => true,
|
|
|
|
|
],
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/wikipage/My_test_page',
|
2023-02-08 16:52:57 +00:00
|
|
|
true,
|
|
|
|
|
null
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
'EnableCanonicalServerLink' => true,
|
|
|
|
|
],
|
|
|
|
|
'https://www.mediawiki.org/wiki/Manual:FauxRequest.php',
|
|
|
|
|
false,
|
|
|
|
|
null
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideGetHeadLinksArray
|
|
|
|
|
*/
|
|
|
|
|
public function testGetHeadLinksArray( $config, $canonicalUrl, $isArticleRelated, $canonicalUrlToSet = null ) {
|
|
|
|
|
$request = new FauxRequest();
|
|
|
|
|
$request->setRequestURL( 'https://www.mediawiki.org/wiki/Manual:FauxRequest.php' );
|
|
|
|
|
$op = $this->newInstance( $config, $request );
|
|
|
|
|
if ( $canonicalUrlToSet ) {
|
|
|
|
|
$op->setCanonicalUrl( $canonicalUrlToSet );
|
|
|
|
|
}
|
|
|
|
|
$op->setArticleRelated( $isArticleRelated );
|
|
|
|
|
$headLinks = $op->getHeadLinksArray();
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
Html::element( 'link',
|
|
|
|
|
[ 'rel' => 'canonical', 'href' => $canonicalUrl ]
|
|
|
|
|
),
|
|
|
|
|
$headLinks['link-canonical']
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-01 01:35:13 +00:00
|
|
|
/**
|
|
|
|
|
* Test the generation of hreflang Tags when site language has variants
|
|
|
|
|
*/
|
|
|
|
|
public function testGetLanguageVariantUrl() {
|
2023-03-07 20:06:23 +00:00
|
|
|
$this->overrideConfigValue( 'LanguageCode', 'zh' );
|
2021-11-01 01:35:13 +00:00
|
|
|
|
|
|
|
|
$op = $this->newInstance();
|
2023-02-08 16:52:57 +00:00
|
|
|
$headLinks = $op->getHeadLinksArray();
|
2021-11-01 01:35:13 +00:00
|
|
|
|
2023-02-08 16:52:57 +00:00
|
|
|
# T123901, T305540, T108443: Don't use language variant link for mixed-variant variant
|
|
|
|
|
# (the language code with converter / the main code)
|
2021-11-01 01:35:13 +00:00
|
|
|
$this->assertSame(
|
|
|
|
|
Html::element( 'link', [ 'rel' => 'alternate', 'hreflang' => 'zh',
|
2023-03-07 20:06:23 +00:00
|
|
|
'href' => 'http://example.org/wikipage/My_test_page' ] ),
|
2023-02-08 16:52:57 +00:00
|
|
|
$headLinks['link-alternate-language-zh']
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
# Make sure alternate URLs use BCP 47 codes in hreflang
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
Html::element( 'link', [ 'rel' => 'alternate', 'hreflang' => 'zh-Hant-TW',
|
2023-03-07 20:06:23 +00:00
|
|
|
'href' => 'http://example.org/mw/index.php?title=My_test_page&variant=zh-tw' ] ),
|
2023-02-08 16:52:57 +00:00
|
|
|
$headLinks['link-alternate-language-zh-hant-tw']
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
# Make sure $wgVariantArticlePath work
|
|
|
|
|
# We currently use MediaWiki internal language code as the primary variant URL parameter
|
2023-03-07 20:06:23 +00:00
|
|
|
$this->overrideConfigValues( [
|
|
|
|
|
'LanguageCode' => 'zh',
|
|
|
|
|
'VariantArticlePath' => '/$2/$1',
|
2023-02-08 16:52:57 +00:00
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$headLinks = $op->getHeadLinksArray();
|
|
|
|
|
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
Html::element( 'link', [ 'rel' => 'alternate', 'hreflang' => 'zh-Hant-TW',
|
|
|
|
|
'href' => 'http://example.org/zh-tw/My_test_page' ] ),
|
|
|
|
|
$headLinks['link-alternate-language-zh-hant-tw']
|
2021-11-01 01:35:13 +00:00
|
|
|
);
|
|
|
|
|
}
|
2018-01-28 02:21:51 +00:00
|
|
|
|
2023-02-09 10:43:55 +00:00
|
|
|
public static function provideCanonicalUrlAndAlternateUrlData() {
|
|
|
|
|
# $messsage, $action, $urlVariant, $canonicalUrl, $altUrlLangCode, $present, $nonpresent
|
|
|
|
|
return [
|
|
|
|
|
[
|
|
|
|
|
'Non-specified variant with view action - '
|
|
|
|
|
. 'We currently use MediaWiki internal codes as the primary URL parameter',
|
|
|
|
|
null,
|
|
|
|
|
null,
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/wikipage/My_test_page',
|
2023-02-09 10:43:55 +00:00
|
|
|
'zh-tw',
|
2023-03-07 20:06:23 +00:00
|
|
|
'http://example.org/mw/index.php?title=My_test_page&variant=zh-tw',
|
|
|
|
|
'http://example.org/mw/index.php?title=My_test_page&variant=zh-hant-tw',
|
2023-02-09 10:43:55 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'Specified zh-tw variant with view action - '
|
|
|
|
|
. 'Canonical URL and alternate URL should be the same; '
|
|
|
|
|
. 'Alternate URL should be kept even when it is the current page view language',
|
|
|
|
|
null,
|
|
|
|
|
'zh-tw',
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/mw/index.php?title=My_test_page&variant=zh-tw',
|
2023-02-09 10:43:55 +00:00
|
|
|
'zh-tw',
|
2023-03-07 20:06:23 +00:00
|
|
|
'http://example.org/mw/index.php?title=My_test_page&variant=zh-tw',
|
|
|
|
|
'http://example.org/mw/index.php?title=My_test_page&variant=zh-hant-tw',
|
2023-02-09 10:43:55 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'Non-specified variant with history action - '
|
|
|
|
|
. 'There should be no alternate URLs for language variants'
|
|
|
|
|
. 'There should be no alternate URLs for language variants',
|
|
|
|
|
'history',
|
|
|
|
|
null,
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/mw/index.php?title=My_test_page&action=history',
|
2023-02-09 10:43:55 +00:00
|
|
|
'zh-tw',
|
|
|
|
|
null,
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/mw/index.php?title=My_test_page&action=history&variant=zh-tw',
|
2023-02-09 10:43:55 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'Specified zh-tw variant with history action - '
|
|
|
|
|
. 'There should be no alternate URLs for language variants',
|
|
|
|
|
'history',
|
|
|
|
|
'zh-tw',
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/mw/index.php?title=My_test_page&action=history',
|
2023-02-09 10:43:55 +00:00
|
|
|
'zh-tw',
|
|
|
|
|
null,
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/mw/index.php?title=My_test_page&action=history&variant=zh-tw',
|
2023-02-09 10:43:55 +00:00
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideCanonicalUrlAndAlternateUrlData
|
|
|
|
|
*/
|
|
|
|
|
public function testCanonicalUrlAndAlternateUrls(
|
|
|
|
|
$messsage, $action, $urlVariant, $canonicalUrl, $altUrlLangCode, $present, $nonpresent
|
|
|
|
|
) {
|
|
|
|
|
$req = new FauxRequest( [
|
|
|
|
|
'title' => 'My_test_page',
|
|
|
|
|
'action' => $action,
|
|
|
|
|
'variant' => $urlVariant,
|
|
|
|
|
] );
|
2023-03-07 20:06:23 +00:00
|
|
|
$this->overrideConfigValues( [
|
|
|
|
|
'LanguageCode' => 'zh',
|
|
|
|
|
'Request' => $req, # LanguageConverter is using global state...
|
2023-02-09 10:43:55 +00:00
|
|
|
] );
|
|
|
|
|
$op = $this->newInstance( [ MainConfigNames::EnableCanonicalServerLink => true ], $req );
|
|
|
|
|
$bcp47 = LanguageCode::bcp47( $altUrlLangCode );
|
|
|
|
|
$bcp47Lowercase = strtolower( $bcp47 );
|
|
|
|
|
$headLinks = $op->getHeadLinksArray();
|
|
|
|
|
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
Html::element( 'link', [ 'rel' => 'canonical', 'href' => $canonicalUrl ] ),
|
|
|
|
|
$headLinks['link-canonical'],
|
|
|
|
|
$messsage
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ( isset( $present ) ) {
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
Html::element(
|
|
|
|
|
'link',
|
|
|
|
|
[
|
|
|
|
|
'rel' => 'alternate',
|
|
|
|
|
'hreflang' => $bcp47,
|
|
|
|
|
'href' => $present,
|
|
|
|
|
]
|
|
|
|
|
),
|
|
|
|
|
$headLinks['link-alternate-language-' . $bcp47Lowercase],
|
|
|
|
|
$messsage
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->assertNotContains(
|
|
|
|
|
Html::element(
|
|
|
|
|
'link',
|
|
|
|
|
[ 'rel' => 'alternate', 'hreflang' => $bcp47, 'href' => $nonpresent, ]
|
|
|
|
|
),
|
|
|
|
|
$headLinks,
|
|
|
|
|
$messsage
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testSetCopyrightUrl() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->setCopyrightUrl( 'http://example.com' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
Html::element( 'link', [ 'rel' => 'license', 'href' => 'http://example.com' ] ),
|
|
|
|
|
$op->getHeadLinksArray()['copyright']
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-28 02:21:51 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideFeedLinkData
|
|
|
|
|
*/
|
|
|
|
|
public function testRecentChangesFeed( $feed, $advertised_feed_types,
|
|
|
|
|
$message, $present, $non_present ) {
|
|
|
|
|
$outputPage = $this->setupFeedLinks( $feed, $advertised_feed_types );
|
|
|
|
|
$this->assertFeedLinks( $outputPage, $message, $present, $non_present );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function provideAdditionalFeedData() {
|
|
|
|
|
return [
|
|
|
|
|
[
|
|
|
|
|
true, [ 'atom' ], 'Additional Atom feed should be offered',
|
|
|
|
|
'atom',
|
|
|
|
|
[ self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
|
|
|
|
|
[ self::RSS_TEST_LINK, self::RSS_RC_LINK ],
|
|
|
|
|
true,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
true, [ 'rss' ], 'Additional RSS feed should be offered',
|
|
|
|
|
'rss',
|
|
|
|
|
[ self::RSS_TEST_LINK, self::RSS_RC_LINK ],
|
|
|
|
|
[ self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
|
|
|
|
|
true,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
true, [ 'rss' ], 'Additional Atom feed should NOT be offered with RSS enabled',
|
|
|
|
|
'atom',
|
|
|
|
|
[ self::RSS_RC_LINK ],
|
|
|
|
|
[ self::RSS_TEST_LINK, self::ATOM_TEST_LINK, self::ATOM_RC_LINK ],
|
|
|
|
|
false,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
false, [ 'atom' ], 'Additional Atom feed should NOT be offered, all feeds disabled',
|
|
|
|
|
'atom',
|
|
|
|
|
[],
|
|
|
|
|
[
|
|
|
|
|
self::RSS_TEST_LINK, self::ATOM_TEST_LINK,
|
|
|
|
|
self::ATOM_RC_LINK, self::ATOM_RC_LINK,
|
|
|
|
|
],
|
|
|
|
|
false,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideAdditionalFeedData
|
|
|
|
|
*/
|
|
|
|
|
public function testAdditionalFeeds( $feed, $advertised_feed_types, $message,
|
|
|
|
|
$additional_feed_type, $present, $non_present, $any_ui_links ) {
|
|
|
|
|
$outputPage = $this->setupFeedLinks( $feed, $advertised_feed_types );
|
|
|
|
|
$outputPage->addFeedLink( $additional_feed_type, 'fake-link' );
|
|
|
|
|
$this->assertFeedLinks( $outputPage, $message, $present, $non_present );
|
|
|
|
|
$this->assertFeedUILinks( $outputPage, $any_ui_links );
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
// @todo How to test setStatusCode?
|
|
|
|
|
|
2017-01-24 17:30:33 +00:00
|
|
|
public function testMetaTags() {
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addMeta( 'http:expires', '0' );
|
|
|
|
|
$op->addMeta( 'keywords', 'first' );
|
|
|
|
|
$op->addMeta( 'keywords', 'second' );
|
|
|
|
|
$op->addMeta( 'og:title', 'Ta-duh' );
|
2017-01-24 17:30:33 +00:00
|
|
|
|
|
|
|
|
$expected = [
|
|
|
|
|
[ 'http:expires', '0' ],
|
|
|
|
|
[ 'keywords', 'first' ],
|
|
|
|
|
[ 'keywords', 'second' ],
|
2017-01-24 12:01:47 +00:00
|
|
|
[ 'og:title', 'Ta-duh' ],
|
2017-01-24 17:30:33 +00:00
|
|
|
];
|
2018-07-23 18:26:32 +00:00
|
|
|
$this->assertSame( $expected, $op->getMetaTags() );
|
2017-01-24 17:30:33 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$links = $op->getHeadLinksArray();
|
2022-05-24 22:15:00 +00:00
|
|
|
$this->assertContains( '<meta http-equiv="expires" content="0">', $links );
|
|
|
|
|
$this->assertContains( '<meta name="keywords" content="first">', $links );
|
|
|
|
|
$this->assertContains( '<meta name="keywords" content="second">', $links );
|
|
|
|
|
$this->assertContains( '<meta property="og:title" content="Ta-duh">', $links );
|
2022-10-05 13:22:15 +00:00
|
|
|
$this->assertArrayHasKey( 'meta-robots', $links );
|
2017-01-24 17:30:33 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testAddLink() {
|
|
|
|
|
$op = $this->newInstance();
|
2017-01-24 17:30:33 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$links = [
|
|
|
|
|
[],
|
|
|
|
|
[ 'rel' => 'foo', 'href' => 'http://example.com' ],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ( $links as $link ) {
|
|
|
|
|
$op->addLink( $link );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $links, $op->getLinkTags() );
|
|
|
|
|
|
|
|
|
|
$result = $op->getHeadLinksArray();
|
|
|
|
|
|
|
|
|
|
foreach ( $links as $link ) {
|
|
|
|
|
$this->assertContains( Html::element( 'link', $link ), $result );
|
|
|
|
|
}
|
2017-01-24 17:30:33 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testAddScript() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addScript( 'some random string' );
|
|
|
|
|
|
2019-12-14 10:27:56 +00:00
|
|
|
$this->assertStringContainsString(
|
|
|
|
|
"\nsome random string\n",
|
|
|
|
|
"\n" . $op->getBottomScripts() . "\n"
|
|
|
|
|
);
|
2018-07-23 18:26:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddScriptFile() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addScriptFile( '/somescript.js' );
|
|
|
|
|
$op->addScriptFile( '//example.com/somescript.js' );
|
|
|
|
|
|
2019-12-14 10:27:56 +00:00
|
|
|
$this->assertStringContainsString(
|
2023-08-06 21:57:55 +00:00
|
|
|
"\n" . Html::linkedScript( '/somescript.js' ) .
|
|
|
|
|
Html::linkedScript( '//example.com/somescript.js' ) . "\n",
|
2018-07-23 18:26:32 +00:00
|
|
|
"\n" . $op->getBottomScripts() . "\n"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddInlineScript() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addInlineScript( 'let foo = "bar";' );
|
|
|
|
|
$op->addInlineScript( 'alert( foo );' );
|
|
|
|
|
|
2019-12-14 10:27:56 +00:00
|
|
|
$this->assertStringContainsString(
|
2023-08-06 21:57:55 +00:00
|
|
|
"\n" . Html::inlineScript( "\nlet foo = \"bar\";\n" ) . "\n" .
|
|
|
|
|
Html::inlineScript( "\nalert( foo );\n" ) . "\n",
|
2018-07-23 18:26:32 +00:00
|
|
|
"\n" . $op->getBottomScripts() . "\n"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @todo How to test addContentOverride(Callback)?
|
|
|
|
|
|
|
|
|
|
public function testHeadItems() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addHeadItem( 'a', 'b' );
|
|
|
|
|
$op->addHeadItems( [ 'c' => '<d>&', 'e' => 'f', 'a' => 'q' ] );
|
|
|
|
|
$op->addHeadItem( 'e', 'g' );
|
|
|
|
|
$op->addHeadItems( 'x' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( [ 'a' => 'q', 'c' => '<d>&', 'e' => 'g', 'x' ],
|
|
|
|
|
$op->getHeadItemsArray() );
|
|
|
|
|
|
|
|
|
|
$this->assertTrue( $op->hasHeadItem( 'a' ) );
|
|
|
|
|
$this->assertTrue( $op->hasHeadItem( 'c' ) );
|
|
|
|
|
$this->assertTrue( $op->hasHeadItem( 'e' ) );
|
|
|
|
|
$this->assertTrue( $op->hasHeadItem( '0' ) );
|
|
|
|
|
|
2019-12-14 10:27:56 +00:00
|
|
|
$this->assertStringContainsString( "\nq\n<d>&\ng\nx\n",
|
2018-07-23 18:26:32 +00:00
|
|
|
'' . $op->headElement( $op->getContext()->getSkin() ) );
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-25 18:41:42 +00:00
|
|
|
public function testHeadItemsParserOutput() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$stubPO1 = $this->createParserOutputStub( 'getHeadItems', [ 'a' => 'b' ] );
|
|
|
|
|
$op->addParserOutputMetadata( $stubPO1 );
|
|
|
|
|
$stubPO2 = $this->createParserOutputStub( 'getHeadItems',
|
|
|
|
|
[ 'c' => '<d>&', 'e' => 'f', 'a' => 'q' ] );
|
|
|
|
|
$op->addParserOutputMetadata( $stubPO2 );
|
|
|
|
|
$stubPO3 = $this->createParserOutputStub( 'getHeadItems', [ 'e' => 'g' ] );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutput( $stubPO3 );
|
2018-07-25 18:41:42 +00:00
|
|
|
$stubPO4 = $this->createParserOutputStub( 'getHeadItems', [ 'x' ] );
|
|
|
|
|
$op->addParserOutputMetadata( $stubPO4 );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( [ 'a' => 'q', 'c' => '<d>&', 'e' => 'g', 'x' ],
|
|
|
|
|
$op->getHeadItemsArray() );
|
|
|
|
|
|
|
|
|
|
$this->assertTrue( $op->hasHeadItem( 'a' ) );
|
|
|
|
|
$this->assertTrue( $op->hasHeadItem( 'c' ) );
|
|
|
|
|
$this->assertTrue( $op->hasHeadItem( 'e' ) );
|
|
|
|
|
$this->assertTrue( $op->hasHeadItem( '0' ) );
|
|
|
|
|
$this->assertFalse( $op->hasHeadItem( 'b' ) );
|
|
|
|
|
|
2019-12-14 10:27:56 +00:00
|
|
|
$this->assertStringContainsString( "\nq\n<d>&\ng\nx\n",
|
2018-07-25 18:41:42 +00:00
|
|
|
'' . $op->headElement( $op->getContext()->getSkin() ) );
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-03 09:50:14 +00:00
|
|
|
public function testCSPParserOutput() {
|
2022-07-15 00:07:38 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::CSPHeader, [] );
|
2020-02-03 09:50:14 +00:00
|
|
|
foreach ( [ 'Default', 'Script', 'Style' ] as $type ) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$ltype = strtolower( $type );
|
|
|
|
|
$stubPO1 = $this->createParserOutputStub( "getExtraCSP{$type}Srcs", [ "{$ltype}src.com" ] );
|
|
|
|
|
$op->addParserOutputMetadata( $stubPO1 );
|
|
|
|
|
$csp = TestingAccessWrapper::newFromObject( $op->getCSP() );
|
|
|
|
|
$actual = $csp->makeCSPDirectives( [ 'default-src' => [] ], false );
|
|
|
|
|
$regex = '/(^|;)\s*' . $ltype . '-src\s[^;]*' . $ltype . 'src\.com[\s;]/';
|
2022-10-07 17:03:35 +00:00
|
|
|
$this->assertMatchesRegularExpression( $regex, $actual, $type );
|
2020-02-03 09:50:14 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testAddBodyClasses() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addBodyClasses( 'a' );
|
|
|
|
|
$op->addBodyClasses( 'mediawiki' );
|
|
|
|
|
$op->addBodyClasses( 'b c' );
|
|
|
|
|
$op->addBodyClasses( [ 'd', 'e' ] );
|
|
|
|
|
$op->addBodyClasses( 'a' );
|
|
|
|
|
|
2022-10-18 01:35:55 +00:00
|
|
|
$this->assertStringContainsString( '<body class="a mediawiki b c d e ',
|
2018-07-23 18:26:32 +00:00
|
|
|
'' . $op->headElement( $op->getContext()->getSkin() ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testArticleBodyOnly() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertFalse( $op->getArticleBodyOnly() );
|
|
|
|
|
|
|
|
|
|
$op->setArticleBodyOnly( true );
|
|
|
|
|
$this->assertTrue( $op->getArticleBodyOnly() );
|
|
|
|
|
|
|
|
|
|
$op->addHTML( '<b>a</b>' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( '<b>a</b>', $op->output( true ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testProperties() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertNull( $op->getProperty( 'foo' ) );
|
|
|
|
|
|
|
|
|
|
$op->setProperty( 'foo', 'bar' );
|
|
|
|
|
$op->setProperty( 'baz', 'quz' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( 'bar', $op->getProperty( 'foo' ) );
|
|
|
|
|
$this->assertSame( 'quz', $op->getProperty( 'baz' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideCheckLastModified
|
2013-01-14 03:26:15 +00:00
|
|
|
*/
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testCheckLastModified(
|
|
|
|
|
$timestamp, $ifModifiedSince, $expected, $config = [], $callback = null
|
|
|
|
|
) {
|
|
|
|
|
$request = new FauxRequest();
|
|
|
|
|
if ( $ifModifiedSince ) {
|
|
|
|
|
if ( is_numeric( $ifModifiedSince ) ) {
|
|
|
|
|
// Unix timestamp
|
|
|
|
|
$ifModifiedSince = date( 'D, d M Y H:i:s', $ifModifiedSince ) . ' GMT';
|
|
|
|
|
}
|
|
|
|
|
$request->setHeader( 'If-Modified-Since', $ifModifiedSince );
|
2013-01-14 03:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
2023-07-16 13:19:58 +00:00
|
|
|
// Make sure it's not too recent
|
|
|
|
|
$config['CacheEpoch'] ??= '20000101000000';
|
|
|
|
|
$config['CachePages'] ??= true;
|
2013-01-14 03:26:15 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = $this->newInstance( $config, $request );
|
2013-01-14 03:26:15 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
if ( $callback ) {
|
|
|
|
|
$callback( $op, $this );
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-24 19:57:59 +00:00
|
|
|
// Ignore complaint about not being able to disable compression
|
|
|
|
|
$this->assertEquals( $expected, @$op->checkLastModified( $timestamp ) );
|
2018-07-23 18:26:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideCheckLastModified() {
|
|
|
|
|
$lastModified = time() - 3600;
|
|
|
|
|
return [
|
|
|
|
|
'Timestamp 0' =>
|
|
|
|
|
[ '0', $lastModified, false ],
|
|
|
|
|
'Timestamp Unix epoch' =>
|
|
|
|
|
[ '19700101000000', $lastModified, false ],
|
|
|
|
|
'Timestamp same as If-Modified-Since' =>
|
|
|
|
|
[ $lastModified, $lastModified, true ],
|
|
|
|
|
'Timestamp one second after If-Modified-Since' =>
|
|
|
|
|
[ $lastModified + 1, $lastModified, false ],
|
|
|
|
|
'No If-Modified-Since' =>
|
|
|
|
|
[ $lastModified + 1, null, false ],
|
|
|
|
|
'Malformed If-Modified-Since' =>
|
|
|
|
|
[ $lastModified + 1, 'GIBBERING WOMBATS !!!', false ],
|
|
|
|
|
'Non-standard IE-style If-Modified-Since' =>
|
|
|
|
|
[ $lastModified, date( 'D, d M Y H:i:s', $lastModified ) . ' GMT; length=5202',
|
|
|
|
|
true ],
|
|
|
|
|
// @todo Should we fix this behavior to match the spec? Probably no reason to.
|
|
|
|
|
'If-Modified-Since not per spec but we accept it anyway because strtotime does' =>
|
|
|
|
|
[ $lastModified, "@$lastModified", true ],
|
|
|
|
|
'$wgCachePages = false' =>
|
|
|
|
|
[ $lastModified, $lastModified, false, [ 'CachePages' => false ] ],
|
|
|
|
|
'$wgCacheEpoch' =>
|
|
|
|
|
[ $lastModified, $lastModified, false,
|
|
|
|
|
[ 'CacheEpoch' => wfTimestamp( TS_MW, $lastModified + 1 ) ] ],
|
|
|
|
|
'Recently-touched user' =>
|
|
|
|
|
[ $lastModified, $lastModified, false, [],
|
2019-09-30 14:25:17 +00:00
|
|
|
function ( OutputPage $op ) {
|
2018-07-23 18:26:32 +00:00
|
|
|
$op->getContext()->setUser( $this->getTestUser()->getUser() );
|
|
|
|
|
} ],
|
2017-11-01 20:55:24 +00:00
|
|
|
'After CDN expiry' =>
|
2018-07-23 18:26:32 +00:00
|
|
|
[ $lastModified, $lastModified, false,
|
2017-11-01 20:55:24 +00:00
|
|
|
[ 'UseCdn' => true, 'CdnMaxAge' => 3599 ] ],
|
2018-07-23 18:26:32 +00:00
|
|
|
'Hook allows cache use' =>
|
|
|
|
|
[ $lastModified + 1, $lastModified, true, [],
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( $op, $that ) {
|
2018-07-23 18:26:32 +00:00
|
|
|
$that->setTemporaryHook( 'OutputPageCheckLastModified',
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( &$modifiedTimes ) {
|
2018-07-23 18:26:32 +00:00
|
|
|
$modifiedTimes = [ 1 ];
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
} ],
|
|
|
|
|
'Hooks prohibits cache use' =>
|
|
|
|
|
[ $lastModified, $lastModified, false, [],
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( $op, $that ) {
|
2018-07-23 18:26:32 +00:00
|
|
|
$that->setTemporaryHook( 'OutputPageCheckLastModified',
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( &$modifiedTimes ) {
|
2018-07-23 18:26:32 +00:00
|
|
|
$modifiedTimes = [ max( $modifiedTimes ) + 1 ];
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
} ],
|
|
|
|
|
];
|
2013-01-14 03:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* @dataProvider provideCdnCacheEpoch
|
2013-01-14 03:26:15 +00:00
|
|
|
*/
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testCdnCacheEpoch( $params ) {
|
|
|
|
|
$out = TestingAccessWrapper::newFromObject( $this->newInstance() );
|
|
|
|
|
$reqTime = strtotime( $params['reqTime'] );
|
|
|
|
|
$pageTime = strtotime( $params['pageTime'] );
|
|
|
|
|
$actual = max( $pageTime, $out->getCdnCacheEpoch( $reqTime, $params['maxAge'] ) );
|
2013-01-14 03:26:15 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$this->assertEquals(
|
|
|
|
|
$params['expect'],
|
|
|
|
|
gmdate( DateTime::ATOM, $actual ),
|
|
|
|
|
'cdn epoch'
|
|
|
|
|
);
|
2013-01-14 03:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
2018-04-20 15:00:32 +00:00
|
|
|
public static function provideCdnCacheEpoch() {
|
|
|
|
|
$base = [
|
|
|
|
|
'pageTime' => '2011-04-01T12:00:00+00:00',
|
|
|
|
|
'maxAge' => 24 * 3600,
|
|
|
|
|
];
|
|
|
|
|
return [
|
|
|
|
|
'after 1s' => [ $base + [
|
|
|
|
|
'reqTime' => '2011-04-01T12:00:01+00:00',
|
|
|
|
|
'expect' => '2011-04-01T12:00:00+00:00',
|
|
|
|
|
] ],
|
|
|
|
|
'after 23h' => [ $base + [
|
|
|
|
|
'reqTime' => '2011-04-02T11:00:00+00:00',
|
|
|
|
|
'expect' => '2011-04-01T12:00:00+00:00',
|
|
|
|
|
] ],
|
|
|
|
|
'after 24h and a bit' => [ $base + [
|
|
|
|
|
'reqTime' => '2011-04-02T12:34:56+00:00',
|
|
|
|
|
'expect' => '2011-04-01T12:34:56+00:00',
|
|
|
|
|
] ],
|
|
|
|
|
'after a year' => [ $base + [
|
|
|
|
|
'reqTime' => '2012-05-06T00:12:07+00:00',
|
|
|
|
|
'expect' => '2012-05-05T00:12:07+00:00',
|
|
|
|
|
] ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
// @todo How to test setLastModified?
|
|
|
|
|
|
|
|
|
|
public function testSetRobotPolicy() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->setRobotPolicy( 'noindex, nofollow' );
|
2018-04-20 15:00:32 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$links = $op->getHeadLinksArray();
|
2022-05-24 22:15:00 +00:00
|
|
|
$this->assertContains( '<meta name="robots" content="noindex,nofollow,max-image-preview:standard">', $links );
|
2018-07-23 18:26:32 +00:00
|
|
|
}
|
|
|
|
|
|
2022-03-10 21:27:21 +00:00
|
|
|
public function testSetRobotsOptions() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->setRobotPolicy( 'noindex, nofollow' );
|
|
|
|
|
$op->setRobotsOptions( [ 'max-snippet' => '500' ] );
|
|
|
|
|
$op->setIndexPolicy( 'index' );
|
|
|
|
|
|
|
|
|
|
$links = $op->getHeadLinksArray();
|
2022-05-24 22:15:00 +00:00
|
|
|
$this->assertContains( '<meta name="robots" content="index,nofollow,max-image-preview:standard,max-snippet:500">', $links );
|
2022-03-10 21:27:21 +00:00
|
|
|
|
|
|
|
|
$op->setFollowPolicy( 'follow' );
|
|
|
|
|
$links = $op->getHeadLinksArray();
|
|
|
|
|
$this->assertContains(
|
2022-05-24 22:15:00 +00:00
|
|
|
'<meta name="robots" content="max-image-preview:standard,max-snippet:500">',
|
2022-03-10 21:27:21 +00:00
|
|
|
$links,
|
|
|
|
|
'When index,follow (browser default) omit'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-11 12:23:51 +00:00
|
|
|
public function testGetRobotPolicy() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->setRobotPolicy( 'noindex, follow' );
|
|
|
|
|
|
|
|
|
|
$policy = $op->getRobotPolicy();
|
|
|
|
|
$this->assertSame( 'noindex,follow', $policy );
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testSetIndexFollowPolicies() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->setIndexPolicy( 'noindex' );
|
|
|
|
|
$op->setFollowPolicy( 'nofollow' );
|
|
|
|
|
|
|
|
|
|
$links = $op->getHeadLinksArray();
|
2022-05-24 22:15:00 +00:00
|
|
|
$this->assertContains( '<meta name="robots" content="noindex,nofollow,max-image-preview:standard">', $links );
|
2018-07-23 18:26:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function extractHTMLTitle( OutputPage $op ) {
|
|
|
|
|
$html = $op->headElement( $op->getContext()->getSkin() );
|
|
|
|
|
|
|
|
|
|
// OutputPage should always output the title in a nice format such that regexes will work
|
|
|
|
|
// fine. If it doesn't, we'll fail the tests.
|
|
|
|
|
preg_match_all( '!<title>(.*?)</title>!', $html, $matches );
|
|
|
|
|
|
|
|
|
|
$this->assertLessThanOrEqual( 1, count( $matches[1] ), 'More than one <title>!' );
|
|
|
|
|
|
|
|
|
|
if ( !count( $matches[1] ) ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $matches[1][0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Shorthand for getting the text of a message, in content language.
|
2021-01-16 19:44:17 +00:00
|
|
|
* @param MessageLocalizer $op
|
|
|
|
|
* @param mixed ...$msgParams
|
|
|
|
|
* @return string
|
2018-07-23 18:26:32 +00:00
|
|
|
*/
|
2019-09-30 14:25:17 +00:00
|
|
|
private static function getMsgText( MessageLocalizer $op, ...$msgParams ) {
|
2018-07-23 18:26:32 +00:00
|
|
|
return $op->msg( ...$msgParams )->inContentLanguage()->text();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testHTMLTitle() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
// Default
|
|
|
|
|
$this->assertSame( '', $op->getHTMLTitle() );
|
|
|
|
|
$this->assertSame( '', $op->getPageTitle() );
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
$this->getMsgText( $op, 'pagetitle', '' ),
|
|
|
|
|
$this->extractHTMLTitle( $op )
|
2018-04-20 15:00:32 +00:00
|
|
|
);
|
2018-07-23 18:26:32 +00:00
|
|
|
|
|
|
|
|
// Set to string
|
|
|
|
|
$op->setHTMLTitle( 'Potatoes will eat me' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( 'Potatoes will eat me', $op->getHTMLTitle() );
|
|
|
|
|
$this->assertSame( 'Potatoes will eat me', $this->extractHTMLTitle( $op ) );
|
|
|
|
|
// Shouldn't have changed the page title
|
|
|
|
|
$this->assertSame( '', $op->getPageTitle() );
|
|
|
|
|
|
|
|
|
|
// Set to message
|
|
|
|
|
$msg = $op->msg( 'mainpage' );
|
|
|
|
|
|
|
|
|
|
$op->setHTMLTitle( $msg );
|
|
|
|
|
$this->assertSame( $msg->text(), $op->getHTMLTitle() );
|
|
|
|
|
$this->assertSame( $msg->text(), $this->extractHTMLTitle( $op ) );
|
|
|
|
|
$this->assertSame( '', $op->getPageTitle() );
|
2018-04-20 15:00:32 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testSetRedirectedFrom() {
|
|
|
|
|
$op = $this->newInstance();
|
2013-05-11 19:05:43 +00:00
|
|
|
|
2021-04-08 19:11:06 +00:00
|
|
|
$op->setRedirectedFrom( new PageReferenceValue( NS_TALK, 'Some page', PageReference::LOCAL ) );
|
2018-07-23 18:26:32 +00:00
|
|
|
$this->assertSame( 'Talk:Some_page', $op->getJSVars()['wgRedirectedFrom'] );
|
|
|
|
|
}
|
2013-01-14 03:26:15 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testPageTitle() {
|
|
|
|
|
// We don't test the actual HTML output anywhere, because that's up to the skin.
|
|
|
|
|
$op = $this->newInstance();
|
2013-01-14 03:26:15 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
// Test default
|
|
|
|
|
$this->assertSame( '', $op->getPageTitle() );
|
|
|
|
|
$this->assertSame( '', $op->getHTMLTitle() );
|
2013-01-14 03:26:15 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
// Test set to plain text
|
|
|
|
|
$op->setPageTitle( 'foobar' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( 'foobar', $op->getPageTitle() );
|
|
|
|
|
// HTML title should change as well
|
|
|
|
|
$this->assertSame( $this->getMsgText( $op, 'pagetitle', 'foobar' ), $op->getHTMLTitle() );
|
|
|
|
|
|
2022-01-03 11:07:10 +00:00
|
|
|
// Test set to text with good and bad HTML. We don't try to be *too*
|
|
|
|
|
// comprehensive here, that belongs in Sanitizer tests, but we'll
|
|
|
|
|
// address the issues specifically noted in T298401/T67747 at least...
|
|
|
|
|
$sanitizerTests = [
|
|
|
|
|
[
|
|
|
|
|
'input' => '<script>a</script>&<i>b</i>',
|
|
|
|
|
'getPageTitle' => '<script>a</script>&<i>b</i>',
|
|
|
|
|
'getHTMLTitle' => '<script>a</script>&b',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'input' => '<code style="display:none">', # T298401
|
|
|
|
|
'getPageTitle' => '<code style="display:none"></code>',
|
|
|
|
|
'getHTMLTitle' => '',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'input' => '<b>Foo bar<b>', # T67747
|
|
|
|
|
'getPageTitle' => '<b>Foo bar<b></b></b>',
|
|
|
|
|
'getHTMLTitle' => 'Foo bar',
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
foreach ( $sanitizerTests as $case ) {
|
|
|
|
|
$op->setPageTitle( $case['input'] );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $case['getPageTitle'], $op->getPageTitle() );
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
$this->getMsgText( $op, 'pagetitle', $case['getHTMLTitle'] ),
|
|
|
|
|
$op->getHTMLTitle()
|
|
|
|
|
);
|
|
|
|
|
}
|
2018-07-23 18:26:32 +00:00
|
|
|
|
2023-08-10 16:06:35 +00:00
|
|
|
// Test set to message (deprecated unescaped)
|
2018-07-23 18:26:32 +00:00
|
|
|
$text = $this->getMsgText( $op, 'mainpage' );
|
|
|
|
|
|
|
|
|
|
$op->setPageTitle( $op->msg( 'mainpage' )->inContentLanguage() );
|
|
|
|
|
$this->assertSame( $text, $op->getPageTitle() );
|
|
|
|
|
$this->assertSame( $this->getMsgText( $op, 'pagetitle', $text ), $op->getHTMLTitle() );
|
2023-08-10 16:06:35 +00:00
|
|
|
|
|
|
|
|
// Test set to message (::setPageTitleMsg(), escaped)
|
|
|
|
|
$msg = ( new RawMessage( 'nope:<span>$1 yes:$2' ) )
|
|
|
|
|
->plaintextParams( '</span>' )
|
|
|
|
|
->rawParams( '<span>!</span>' );
|
|
|
|
|
// deprecated ::setPageTitle(Message), doesn't escape either
|
|
|
|
|
// the localized message or the plaintext parameters
|
|
|
|
|
$op->setPageTitle( $msg );
|
|
|
|
|
$this->assertSame( "nope:<span></span> yes:<span>!</span>", $op->getPageTitle() );
|
|
|
|
|
// preferred ::setPageTitleMsg(Msg)
|
|
|
|
|
$op->setPageTitleMsg( $msg );
|
|
|
|
|
$this->assertSame( 'nope:<span></span> yes:<span>!</span>', $op->getPageTitle() );
|
|
|
|
|
// Note that HTML title is unescaped plaintext, it is expected to be
|
|
|
|
|
// HTML escaped before becoming the <title> element.
|
|
|
|
|
$this->assertSame( $this->getMsgText( $op, 'pagetitle', 'nope:<span></span> yes:!' ), $op->getHTMLTitle() );
|
2013-01-14 03:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testSetTitle() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertSame( 'My test page', $op->getTitle()->getPrefixedText() );
|
|
|
|
|
|
2022-09-23 19:53:11 +00:00
|
|
|
$op->setTitle( Title::makeTitle( NS_MAIN, 'Another test page' ) );
|
2018-07-23 18:26:32 +00:00
|
|
|
|
|
|
|
|
$this->assertSame( 'Another test page', $op->getTitle()->getPrefixedText() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSubtitle() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertSame( '', $op->getSubtitle() );
|
|
|
|
|
|
|
|
|
|
$op->addSubtitle( '<b>foo</b>' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( '<b>foo</b>', $op->getSubtitle() );
|
|
|
|
|
|
|
|
|
|
$op->addSubtitle( $op->msg( 'mainpage' )->inContentLanguage() );
|
|
|
|
|
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
"<b>foo</b><br />\n\t\t\t\t" . $this->getMsgText( $op, 'mainpage' ),
|
|
|
|
|
$op->getSubtitle()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$op->setSubtitle( 'There can be only one' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( 'There can be only one', $op->getSubtitle() );
|
|
|
|
|
|
|
|
|
|
$op->clearSubtitle();
|
|
|
|
|
|
|
|
|
|
$this->assertSame( '', $op->getSubtitle() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideBacklinkSubtitle
|
|
|
|
|
*/
|
2018-07-25 18:41:42 +00:00
|
|
|
public function testBuildBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
|
|
|
|
|
if ( count( $titles ) > 1 ) {
|
|
|
|
|
// Not applicable
|
|
|
|
|
$this->assertTrue( true );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-08 19:11:06 +00:00
|
|
|
$title = $titles[0];
|
2018-07-25 18:41:42 +00:00
|
|
|
$query = $queries[0];
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$str = OutputPage::buildBacklinkSubtitle( $title, $query )->text();
|
|
|
|
|
|
|
|
|
|
foreach ( $contains as $substr ) {
|
2019-12-14 12:45:35 +00:00
|
|
|
$this->assertStringContainsString( $substr, $str );
|
2018-07-23 18:26:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $notContains as $substr ) {
|
2019-12-14 12:45:35 +00:00
|
|
|
$this->assertStringNotContainsString( $substr, $str );
|
2018-07-23 18:26:32 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideBacklinkSubtitle
|
|
|
|
|
*/
|
2018-07-25 18:41:42 +00:00
|
|
|
public function testAddBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = $this->newInstance();
|
2018-07-25 18:41:42 +00:00
|
|
|
foreach ( $titles as $i => $unused ) {
|
2021-04-08 19:11:06 +00:00
|
|
|
$op->addBacklinkSubtitle( $titles[$i], $queries[$i] );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
2018-07-23 18:26:32 +00:00
|
|
|
|
|
|
|
|
$str = $op->getSubtitle();
|
|
|
|
|
|
|
|
|
|
foreach ( $contains as $substr ) {
|
2019-12-14 12:45:35 +00:00
|
|
|
$this->assertStringContainsString( $substr, $str );
|
2018-07-23 18:26:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $notContains as $substr ) {
|
2019-12-14 12:45:35 +00:00
|
|
|
$this->assertStringNotContainsString( $substr, $str );
|
2018-07-23 18:26:32 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideBacklinkSubtitle() {
|
2021-04-08 19:11:06 +00:00
|
|
|
$page1title = $this->makeMockTitle( 'Page 1', [ 'redirect' => true ] );
|
|
|
|
|
$page1ref = new PageReferenceValue( NS_MAIN, 'Page 1', PageReference::LOCAL );
|
|
|
|
|
|
|
|
|
|
$row = [
|
|
|
|
|
'page_id' => 28,
|
|
|
|
|
'page_namespace' => NS_MAIN,
|
|
|
|
|
'page_title' => 'Page 2',
|
|
|
|
|
'page_latest' => 75,
|
|
|
|
|
'page_is_redirect' => true,
|
|
|
|
|
'page_is_new' => true,
|
|
|
|
|
'page_touched' => '20200101221133',
|
|
|
|
|
'page_lang' => 'en',
|
|
|
|
|
];
|
|
|
|
|
$page2rec = new PageStoreRecord( (object)$row, PageReference::LOCAL );
|
|
|
|
|
|
|
|
|
|
$special = new PageReferenceValue( NS_SPECIAL, 'BlankPage', PageReference::LOCAL );
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
return [
|
2018-07-25 18:41:42 +00:00
|
|
|
[
|
2021-04-08 19:11:06 +00:00
|
|
|
[ $page1title ],
|
2018-07-25 18:41:42 +00:00
|
|
|
[ [] ],
|
|
|
|
|
[ 'Page 1' ],
|
|
|
|
|
[ 'redirect', 'Page 2' ],
|
|
|
|
|
],
|
|
|
|
|
[
|
2021-04-08 19:11:06 +00:00
|
|
|
[ $page2rec ],
|
2018-07-25 18:41:42 +00:00
|
|
|
[ [] ],
|
|
|
|
|
[ 'redirect=no' ],
|
|
|
|
|
[ 'Page 1' ],
|
|
|
|
|
],
|
|
|
|
|
[
|
2021-04-08 19:11:06 +00:00
|
|
|
[ $special ],
|
|
|
|
|
[ [] ],
|
|
|
|
|
[ 'Special:BlankPage' ],
|
|
|
|
|
[ 'redirect=no' ],
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ $page1ref ],
|
2018-07-25 18:41:42 +00:00
|
|
|
[ [ 'action' => 'edit' ] ],
|
|
|
|
|
[ 'action=edit' ],
|
|
|
|
|
[],
|
|
|
|
|
],
|
|
|
|
|
[
|
2021-04-08 19:11:06 +00:00
|
|
|
[ $page1ref, $page2rec ],
|
2018-07-25 18:41:42 +00:00
|
|
|
[ [], [] ],
|
|
|
|
|
[ 'Page 1', 'Page 2', "<br />\n\t\t\t\t" ],
|
|
|
|
|
[],
|
|
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// @todo Anything else to test?
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-25 18:41:42 +00:00
|
|
|
public function testPrintable() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertFalse( $op->isPrintable() );
|
|
|
|
|
|
|
|
|
|
$op->setPrintable();
|
|
|
|
|
|
|
|
|
|
$this->assertTrue( $op->isPrintable() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDisable() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertFalse( $op->isDisabled() );
|
|
|
|
|
$this->assertNotSame( '', $op->output( true ) );
|
|
|
|
|
|
|
|
|
|
$op->disable();
|
|
|
|
|
|
|
|
|
|
$this->assertTrue( $op->isDisabled() );
|
|
|
|
|
$this->assertSame( '', $op->output( true ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testShowNewSectionLink() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertFalse( $op->showNewSectionLink() );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertFalse( $op->getOutputFlag( ParserOutputFlags::NEW_SECTION ) );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
2023-03-21 18:26:05 +00:00
|
|
|
$pOut1 = $this->createParserOutputStubWithFlags(
|
|
|
|
|
[ 'getNewSection' => true ], [ ParserOutputFlags::NEW_SECTION ]
|
|
|
|
|
);
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertTrue( $op->showNewSectionLink() );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::NEW_SECTION ) );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
$pOut2 = $this->createParserOutputStub( 'getNewSection', false );
|
|
|
|
|
$op->addParserOutput( $pOut2 );
|
|
|
|
|
$this->assertFalse( $op->showNewSectionLink() );
|
2023-03-21 18:26:05 +00:00
|
|
|
// Note that flags are OR'ed together, and not reset.
|
|
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::NEW_SECTION ) );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testForceHideNewSectionLink() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertFalse( $op->forceHideNewSectionLink() );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertFalse( $op->getOutputFlag( ParserOutputFlags::HIDE_NEW_SECTION ) );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
2023-03-21 18:26:05 +00:00
|
|
|
$pOut1 = $this->createParserOutputStubWithFlags(
|
|
|
|
|
[ 'getHideNewSection' => true ], [ ParserOutputFlags::HIDE_NEW_SECTION ]
|
|
|
|
|
);
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertTrue( $op->forceHideNewSectionLink() );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::HIDE_NEW_SECTION ) );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
$pOut2 = $this->createParserOutputStub( 'getHideNewSection', false );
|
|
|
|
|
$op->addParserOutput( $pOut2 );
|
|
|
|
|
$this->assertFalse( $op->forceHideNewSectionLink() );
|
2023-03-21 18:26:05 +00:00
|
|
|
// Note that flags are OR'ed together, and not reset.
|
|
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::HIDE_NEW_SECTION ) );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testSetSyndicated() {
|
2018-01-28 02:21:51 +00:00
|
|
|
$op = $this->newInstance( [ 'Feed' => true ] );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertFalse( $op->isSyndicated() );
|
|
|
|
|
|
|
|
|
|
$op->setSyndicated();
|
|
|
|
|
$this->assertTrue( $op->isSyndicated() );
|
|
|
|
|
|
|
|
|
|
$op->setSyndicated( false );
|
|
|
|
|
$this->assertFalse( $op->isSyndicated() );
|
2018-01-28 02:21:51 +00:00
|
|
|
|
|
|
|
|
$op = $this->newInstance(); // Feed => false by default
|
|
|
|
|
$this->assertFalse( $op->isSyndicated() );
|
|
|
|
|
|
|
|
|
|
$op->setSyndicated();
|
|
|
|
|
$this->assertFalse( $op->isSyndicated() );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testFeedLinks() {
|
2018-01-28 02:21:51 +00:00
|
|
|
$op = $this->newInstance( [ 'Feed' => true ] );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertSame( [], $op->getSyndicationLinks() );
|
|
|
|
|
|
|
|
|
|
$op->addFeedLink( 'not a supported format', 'abc' );
|
|
|
|
|
$this->assertFalse( $op->isSyndicated() );
|
|
|
|
|
$this->assertSame( [], $op->getSyndicationLinks() );
|
|
|
|
|
|
|
|
|
|
$feedTypes = $op->getConfig()->get( 'AdvertisedFeedTypes' );
|
|
|
|
|
|
|
|
|
|
$op->addFeedLink( $feedTypes[0], 'def' );
|
|
|
|
|
$this->assertTrue( $op->isSyndicated() );
|
|
|
|
|
$this->assertSame( [ $feedTypes[0] => 'def' ], $op->getSyndicationLinks() );
|
|
|
|
|
|
|
|
|
|
$op->setFeedAppendQuery( false );
|
|
|
|
|
$expected = [];
|
|
|
|
|
foreach ( $feedTypes as $type ) {
|
|
|
|
|
$expected[$type] = $op->getTitle()->getLocalURL( "feed=$type" );
|
|
|
|
|
}
|
|
|
|
|
$this->assertSame( $expected, $op->getSyndicationLinks() );
|
|
|
|
|
|
|
|
|
|
$op->setFeedAppendQuery( 'apples=oranges' );
|
|
|
|
|
foreach ( $feedTypes as $type ) {
|
|
|
|
|
$expected[$type] = $op->getTitle()->getLocalURL( "feed=$type&apples=oranges" );
|
|
|
|
|
}
|
|
|
|
|
$this->assertSame( $expected, $op->getSyndicationLinks() );
|
2018-01-28 02:21:51 +00:00
|
|
|
|
|
|
|
|
$op = $this->newInstance(); // Feed => false by default
|
|
|
|
|
$this->assertSame( [], $op->getSyndicationLinks() );
|
|
|
|
|
|
|
|
|
|
$op->addFeedLink( $feedTypes[0], 'def' );
|
|
|
|
|
$this->assertFalse( $op->isSyndicated() );
|
|
|
|
|
$this->assertSame( [], $op->getSyndicationLinks() );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
2019-10-09 18:24:07 +00:00
|
|
|
public function testArticleFlags() {
|
2018-07-25 18:41:42 +00:00
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertFalse( $op->isArticle() );
|
|
|
|
|
$this->assertTrue( $op->isArticleRelated() );
|
|
|
|
|
|
|
|
|
|
$op->setArticleRelated( false );
|
|
|
|
|
$this->assertFalse( $op->isArticle() );
|
|
|
|
|
$this->assertFalse( $op->isArticleRelated() );
|
|
|
|
|
|
|
|
|
|
$op->setArticleFlag( true );
|
|
|
|
|
$this->assertTrue( $op->isArticle() );
|
|
|
|
|
$this->assertTrue( $op->isArticleRelated() );
|
|
|
|
|
|
|
|
|
|
$op->setArticleFlag( false );
|
|
|
|
|
$this->assertFalse( $op->isArticle() );
|
|
|
|
|
$this->assertTrue( $op->isArticleRelated() );
|
|
|
|
|
|
|
|
|
|
$op->setArticleFlag( true );
|
|
|
|
|
$op->setArticleRelated( false );
|
|
|
|
|
$this->assertFalse( $op->isArticle() );
|
|
|
|
|
$this->assertFalse( $op->isArticleRelated() );
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-09 18:24:07 +00:00
|
|
|
public function testLanguageLinks() {
|
2018-07-25 18:41:42 +00:00
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( [], $op->getLanguageLinks() );
|
|
|
|
|
|
|
|
|
|
$op->addLanguageLinks( [ 'fr:A', 'it:B' ] );
|
|
|
|
|
$this->assertSame( [ 'fr:A', 'it:B' ], $op->getLanguageLinks() );
|
|
|
|
|
|
|
|
|
|
$op->addLanguageLinks( [ 'de:C', 'es:D' ] );
|
|
|
|
|
$this->assertSame( [ 'fr:A', 'it:B', 'de:C', 'es:D' ], $op->getLanguageLinks() );
|
|
|
|
|
|
|
|
|
|
$op->setLanguageLinks( [ 'pt:E' ] );
|
|
|
|
|
$this->assertSame( [ 'pt:E' ], $op->getLanguageLinks() );
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
$pOut1 = $this->createParserOutputStub( 'getLanguageLinks', [ 'he:F', 'ar:G' ] );
|
|
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertSame( [ 'pt:E', 'he:F', 'ar:G' ], $op->getLanguageLinks() );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
$pOut2 = $this->createParserOutputStub( 'getLanguageLinks', [ 'pt:H' ] );
|
|
|
|
|
$op->addParserOutput( $pOut2 );
|
|
|
|
|
$this->assertSame( [ 'pt:E', 'he:F', 'ar:G', 'pt:H' ], $op->getLanguageLinks() );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @todo Are these category links tests too abstract and complicated for what they test? Would
|
|
|
|
|
// it make sense to just write out all the tests by hand with maybe some copy-and-paste?
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideGetCategories
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* @param array $args Array of form [ category name => sort key ]
|
|
|
|
|
* @param array $fakeResults Array of form [ category name => value to return from mocked
|
|
|
|
|
* LinkBatch ]
|
2019-11-18 18:58:07 +00:00
|
|
|
* @param callable|null $variantLinkCallback Callback to replace findVariantLink() call
|
2018-07-25 18:41:42 +00:00
|
|
|
* @param array $expectedNormal Expected return value of getCategoryLinks['normal']
|
|
|
|
|
* @param array $expectedHidden Expected return value of getCategoryLinks['hidden']
|
2018-07-23 18:26:32 +00:00
|
|
|
*/
|
2018-07-25 18:41:42 +00:00
|
|
|
public function testAddCategoryLinks(
|
2019-11-18 18:58:07 +00:00
|
|
|
array $args, array $fakeResults, ?callable $variantLinkCallback,
|
2018-07-25 18:41:42 +00:00
|
|
|
array $expectedNormal, array $expectedHidden
|
|
|
|
|
) {
|
|
|
|
|
$expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'add' );
|
|
|
|
|
$expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'add' );
|
|
|
|
|
|
|
|
|
|
$op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
|
|
|
|
|
|
|
|
|
|
$op->addCategoryLinks( $args );
|
|
|
|
|
|
|
|
|
|
$this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
|
|
|
|
|
$this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideGetCategories
|
|
|
|
|
*/
|
|
|
|
|
public function testAddCategoryLinksOneByOne(
|
2019-11-18 18:58:07 +00:00
|
|
|
array $args, array $fakeResults, ?callable $variantLinkCallback,
|
2018-07-25 18:41:42 +00:00
|
|
|
array $expectedNormal, array $expectedHidden
|
|
|
|
|
) {
|
|
|
|
|
if ( count( $args ) <= 1 ) {
|
|
|
|
|
// @todo Should this be skipped instead of passed?
|
|
|
|
|
$this->assertTrue( true );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'onebyone' );
|
|
|
|
|
$expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'onebyone' );
|
|
|
|
|
|
|
|
|
|
$op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
|
|
|
|
|
|
|
|
|
|
foreach ( $args as $key => $val ) {
|
|
|
|
|
$op->addCategoryLinks( [ $key => $val ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
|
|
|
|
|
$this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideGetCategories
|
|
|
|
|
*/
|
|
|
|
|
public function testSetCategoryLinks(
|
2019-11-18 18:58:07 +00:00
|
|
|
array $args, array $fakeResults, ?callable $variantLinkCallback,
|
2018-07-25 18:41:42 +00:00
|
|
|
array $expectedNormal, array $expectedHidden
|
|
|
|
|
) {
|
|
|
|
|
$expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'set' );
|
|
|
|
|
$expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'set' );
|
|
|
|
|
|
|
|
|
|
$op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
|
|
|
|
|
|
|
|
|
|
$op->setCategoryLinks( [ 'Initial page' => 'Initial page' ] );
|
|
|
|
|
$op->setCategoryLinks( $args );
|
|
|
|
|
|
|
|
|
|
// We don't reset the categories, for some reason, only the links
|
|
|
|
|
$expectedNormalCats = array_merge( [ 'Initial page' ], $expectedNormal );
|
|
|
|
|
|
|
|
|
|
$this->doCategoryAsserts( $op, $expectedNormalCats, $expectedHidden );
|
|
|
|
|
$this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideGetCategories
|
|
|
|
|
*/
|
|
|
|
|
public function testParserOutputCategoryLinks(
|
2019-11-18 18:58:07 +00:00
|
|
|
array $args, array $fakeResults, ?callable $variantLinkCallback,
|
2018-07-25 18:41:42 +00:00
|
|
|
array $expectedNormal, array $expectedHidden
|
|
|
|
|
) {
|
|
|
|
|
$expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'pout' );
|
|
|
|
|
$expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'pout' );
|
|
|
|
|
|
|
|
|
|
$op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
|
|
|
|
|
|
2023-09-21 17:06:50 +00:00
|
|
|
$stubPO = $this->createParserOutputStub( [
|
|
|
|
|
'getCategories' => $args,
|
|
|
|
|
'getCategoryMap' => $args,
|
|
|
|
|
] );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
// addParserOutput and addParserOutputMetadata should behave identically for us, so
|
|
|
|
|
// alternate to get coverage for both without adding extra tests
|
|
|
|
|
static $idx = 0;
|
|
|
|
|
$idx++;
|
|
|
|
|
$method = [ 'addParserOutputMetadata', 'addParserOutput' ][$idx % 2];
|
|
|
|
|
$op->$method( $stubPO );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
|
|
|
|
$this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
|
|
|
|
|
$this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* We allow different expectations for different tests as an associative array, like
|
|
|
|
|
* [ 'set' => [ ... ], 'default' => [ ... ] ] if setCategoryLinks() will give a different
|
|
|
|
|
* result.
|
2021-01-16 19:44:17 +00:00
|
|
|
* @param array $expected
|
|
|
|
|
* @param string $key
|
|
|
|
|
* @return array
|
2018-07-25 18:41:42 +00:00
|
|
|
*/
|
|
|
|
|
private function extractExpectedCategories( array $expected, $key ) {
|
|
|
|
|
if ( !$expected || isset( $expected[0] ) ) {
|
|
|
|
|
return $expected;
|
|
|
|
|
}
|
|
|
|
|
return $expected[$key] ?? $expected['default'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function setupCategoryTests(
|
|
|
|
|
array $fakeResults, callable $variantLinkCallback = null
|
2021-07-22 03:11:47 +00:00
|
|
|
): OutputPage {
|
2022-07-15 00:07:38 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::UsePigLatinVariant, true );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
2020-05-06 00:26:09 +00:00
|
|
|
if ( $variantLinkCallback ) {
|
|
|
|
|
$mockLanguageConverter = $this
|
|
|
|
|
->createMock( ILanguageConverter::class );
|
|
|
|
|
$mockLanguageConverter
|
|
|
|
|
->method( 'findVariantLink' )
|
2022-06-05 23:39:02 +00:00
|
|
|
->willReturnCallback( $variantLinkCallback );
|
2020-05-06 00:26:09 +00:00
|
|
|
|
|
|
|
|
$languageConverterFactory = $this
|
|
|
|
|
->createMock( LanguageConverterFactory::class );
|
|
|
|
|
$languageConverterFactory
|
|
|
|
|
->method( 'getLanguageConverter' )
|
|
|
|
|
->willReturn( $mockLanguageConverter );
|
|
|
|
|
$this->setService(
|
|
|
|
|
'LanguageConverterFactory',
|
|
|
|
|
$languageConverterFactory
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = $this->getMockBuilder( OutputPage::class )
|
|
|
|
|
->setConstructorArgs( [ new RequestContext() ] )
|
2021-03-20 15:18:58 +00:00
|
|
|
->onlyMethods( [ 'addCategoryLinksToLBAndGetResult', 'getTitle' ] )
|
2018-07-23 18:26:32 +00:00
|
|
|
->getMock();
|
2018-07-25 18:41:42 +00:00
|
|
|
|
2022-09-23 19:53:11 +00:00
|
|
|
$title = Title::makeTitle( NS_MAIN, 'My test page' );
|
2021-04-22 08:28:11 +00:00
|
|
|
$op->method( 'getTitle' )
|
|
|
|
|
->willReturn( $title );
|
2018-09-27 21:36:19 +00:00
|
|
|
|
2021-04-22 08:40:46 +00:00
|
|
|
$op->method( 'addCategoryLinksToLBAndGetResult' )
|
2022-06-05 23:39:02 +00:00
|
|
|
->willReturnCallback( static function ( array $categories ) use ( $fakeResults ) {
|
2018-07-25 18:41:42 +00:00
|
|
|
$return = [];
|
|
|
|
|
foreach ( $categories as $category => $unused ) {
|
|
|
|
|
if ( isset( $fakeResults[$category] ) ) {
|
|
|
|
|
$return[] = $fakeResults[$category];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return new FakeResultWrapper( $return );
|
2022-06-05 23:39:02 +00:00
|
|
|
} );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
|
|
|
|
$this->assertSame( [], $op->getCategories() );
|
|
|
|
|
|
|
|
|
|
return $op;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-30 14:25:17 +00:00
|
|
|
private function doCategoryAsserts( OutputPage $op, $expectedNormal, $expectedHidden ) {
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertSame( array_merge( $expectedHidden, $expectedNormal ), $op->getCategories() );
|
|
|
|
|
$this->assertSame( $expectedNormal, $op->getCategories( 'normal' ) );
|
|
|
|
|
$this->assertSame( $expectedHidden, $op->getCategories( 'hidden' ) );
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-30 14:25:17 +00:00
|
|
|
private function doCategoryLinkAsserts( OutputPage $op, $expectedNormal, $expectedHidden ) {
|
2018-07-25 18:41:42 +00:00
|
|
|
$catLinks = $op->getCategoryLinks();
|
2021-01-30 12:51:38 +00:00
|
|
|
$this->assertCount( (bool)$expectedNormal + (bool)$expectedHidden, $catLinks );
|
2018-07-25 18:41:42 +00:00
|
|
|
if ( $expectedNormal ) {
|
2023-09-27 13:22:44 +00:00
|
|
|
$this->assertSameSize( $expectedNormal, $catLinks['normal'] );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
if ( $expectedHidden ) {
|
2023-09-27 13:22:44 +00:00
|
|
|
$this->assertSameSize( $expectedHidden, $catLinks['hidden'] );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $expectedNormal as $i => $name ) {
|
2019-12-14 12:45:35 +00:00
|
|
|
$this->assertStringContainsString( $name, $catLinks['normal'][$i] );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
foreach ( $expectedHidden as $i => $name ) {
|
2019-12-14 12:45:35 +00:00
|
|
|
$this->assertStringContainsString( $name, $catLinks['hidden'][$i] );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideGetCategories() {
|
2018-07-25 18:41:42 +00:00
|
|
|
return [
|
|
|
|
|
'No categories' => [ [], [], null, [], [] ],
|
|
|
|
|
'Simple test' => [
|
|
|
|
|
[ 'Test1' => 'Some sortkey', 'Test2' => 'A different sortkey' ],
|
|
|
|
|
[ 'Test1' => (object)[ 'pp_value' => 1, 'page_title' => 'Test1' ],
|
|
|
|
|
'Test2' => (object)[ 'page_title' => 'Test2' ] ],
|
|
|
|
|
null,
|
|
|
|
|
[ 'Test2' ],
|
|
|
|
|
[ 'Test1' ],
|
|
|
|
|
],
|
|
|
|
|
'Invalid title' => [
|
|
|
|
|
[ '[' => '[', 'Test' => 'Test' ],
|
|
|
|
|
[ 'Test' => (object)[ 'page_title' => 'Test' ] ],
|
|
|
|
|
null,
|
|
|
|
|
[ 'Test' ],
|
|
|
|
|
[],
|
|
|
|
|
],
|
|
|
|
|
'Variant link' => [
|
|
|
|
|
[ 'Test' => 'Test', 'Estay' => 'Estay' ],
|
|
|
|
|
[ 'Test' => (object)[ 'page_title' => 'Test' ] ],
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( &$link, &$title ) {
|
2018-07-25 18:41:42 +00:00
|
|
|
if ( $link === 'Estay' ) {
|
|
|
|
|
$link = 'Test';
|
|
|
|
|
$title = Title::makeTitleSafe( NS_CATEGORY, $link );
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// For adding one by one, the variant gets added as well as the original category,
|
|
|
|
|
// but if you add them all together the second time gets skipped.
|
|
|
|
|
[ 'onebyone' => [ 'Test', 'Test' ], 'default' => [ 'Test' ] ],
|
|
|
|
|
[],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testGetCategoriesInvalid() {
|
2019-10-05 15:42:53 +00:00
|
|
|
$this->expectException( InvalidArgumentException::class );
|
|
|
|
|
$this->expectExceptionMessage( 'Invalid category type given: hiddne' );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->getCategories( 'hiddne' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @todo Should we test addCategoryLinksToLBAndGetResult? If so, how? Insert some test rows in
|
|
|
|
|
// the DB?
|
|
|
|
|
|
|
|
|
|
public function testIndicators() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( [], $op->getIndicators() );
|
|
|
|
|
|
|
|
|
|
$op->setIndicators( [] );
|
|
|
|
|
$this->assertSame( [], $op->getIndicators() );
|
|
|
|
|
|
|
|
|
|
// Test sorting alphabetically
|
|
|
|
|
$op->setIndicators( [ 'b' => 'x', 'a' => 'y' ] );
|
|
|
|
|
$this->assertSame( [ 'a' => 'y', 'b' => 'x' ], $op->getIndicators() );
|
|
|
|
|
|
|
|
|
|
// Test overwriting existing keys
|
|
|
|
|
$op->setIndicators( [ 'c' => 'z', 'a' => 'w' ] );
|
|
|
|
|
$this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'z' ], $op->getIndicators() );
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
// Test with addParserOutputMetadata
|
2021-12-22 00:19:02 +00:00
|
|
|
// Note that the indicators are wrapped.
|
|
|
|
|
$pOut1 = $this->createParserOutputStub( [
|
|
|
|
|
'getIndicators' => [ 'c' => 'u', 'd' => 'v' ],
|
|
|
|
|
'getWrapperDivClass' => 'wrapper1',
|
|
|
|
|
] );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
2021-12-22 00:19:02 +00:00
|
|
|
$this->assertSame( [
|
|
|
|
|
'a' => 'w',
|
|
|
|
|
'b' => 'x',
|
|
|
|
|
'c' => '<div class="wrapper1">u</div>',
|
|
|
|
|
'd' => '<div class="wrapper1">v</div>',
|
|
|
|
|
], $op->getIndicators() );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
// Test with addParserOutput
|
2021-12-22 00:19:02 +00:00
|
|
|
$pOut2 = $this->createParserOutputStub( [
|
|
|
|
|
'getIndicators' => [ 'a' => '!!!' ],
|
|
|
|
|
'getWrapperDivClass' => 'wrapper2',
|
|
|
|
|
] );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutput( $pOut2 );
|
2021-12-22 00:19:02 +00:00
|
|
|
$this->assertSame( [
|
|
|
|
|
'a' => '<div class="wrapper2">!!!</div>',
|
|
|
|
|
'b' => 'x',
|
|
|
|
|
'c' => '<div class="wrapper1">u</div>',
|
|
|
|
|
'd' => '<div class="wrapper1">v</div>',
|
|
|
|
|
], $op->getIndicators() );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddHelpLink() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$op->addHelpLink( 'Manual:PHP unit testing' );
|
|
|
|
|
$indicators = $op->getIndicators();
|
|
|
|
|
$this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
|
2019-12-14 10:27:56 +00:00
|
|
|
$this->assertStringContainsString( 'Manual:PHP_unit_testing', $indicators['mw-helplink'] );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
|
|
|
|
$op->addHelpLink( 'https://phpunit.de', true );
|
|
|
|
|
$indicators = $op->getIndicators();
|
|
|
|
|
$this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
|
2019-12-14 10:27:56 +00:00
|
|
|
$this->assertStringContainsString( 'https://phpunit.de', $indicators['mw-helplink'] );
|
|
|
|
|
$this->assertStringNotContainsString( 'mediawiki', $indicators['mw-helplink'] );
|
|
|
|
|
$this->assertStringNotContainsString( 'Manual:PHP', $indicators['mw-helplink'] );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBodyHTML() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
|
|
|
|
|
$op->addHTML( 'a' );
|
|
|
|
|
$this->assertSame( 'a', $op->getHTML() );
|
|
|
|
|
|
|
|
|
|
$op->addHTML( 'b' );
|
|
|
|
|
$this->assertSame( 'ab', $op->getHTML() );
|
|
|
|
|
|
|
|
|
|
$op->prependHTML( 'c' );
|
|
|
|
|
$this->assertSame( 'cab', $op->getHTML() );
|
|
|
|
|
|
|
|
|
|
$op->addElement( 'p', [ 'id' => 'foo' ], 'd' );
|
|
|
|
|
$this->assertSame( 'cab<p id="foo">d</p>', $op->getHTML() );
|
|
|
|
|
|
|
|
|
|
$op->clearHTML();
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideRevisionId
|
|
|
|
|
*/
|
|
|
|
|
public function testRevisionId( $newVal, $expected ) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertNull( $op->setRevisionId( $newVal ) );
|
|
|
|
|
$this->assertSame( $expected, $op->getRevisionId() );
|
|
|
|
|
$this->assertSame( $expected, $op->setRevisionId( null ) );
|
|
|
|
|
$this->assertNull( $op->getRevisionId() );
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideRevisionId() {
|
2018-07-25 18:41:42 +00:00
|
|
|
return [
|
|
|
|
|
[ null, null ],
|
|
|
|
|
[ 7, 7 ],
|
|
|
|
|
[ -1, -1 ],
|
|
|
|
|
[ 3.2, 3 ],
|
|
|
|
|
[ '0', 0 ],
|
|
|
|
|
[ '32% finished', 32 ],
|
|
|
|
|
[ false, 0 ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testRevisionTimestamp() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertNull( $op->getRevisionTimestamp() );
|
|
|
|
|
|
|
|
|
|
$this->assertNull( $op->setRevisionTimestamp( 'abc' ) );
|
|
|
|
|
$this->assertSame( 'abc', $op->getRevisionTimestamp() );
|
|
|
|
|
$this->assertSame( 'abc', $op->setRevisionTimestamp( null ) );
|
|
|
|
|
$this->assertNull( $op->getRevisionTimestamp() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testFileVersion() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertNull( $op->getFileVersion() );
|
|
|
|
|
|
|
|
|
|
$stubFile = $this->createMock( File::class );
|
|
|
|
|
$stubFile->method( 'exists' )->willReturn( true );
|
|
|
|
|
$stubFile->method( 'getTimestamp' )->willReturn( '12211221123321' );
|
|
|
|
|
$stubFile->method( 'getSha1' )->willReturn( 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' );
|
|
|
|
|
|
2019-09-30 14:25:17 +00:00
|
|
|
/** @var File $stubFile */
|
2018-07-25 18:41:42 +00:00
|
|
|
$op->setFileVersion( $stubFile );
|
|
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
|
[ 'time' => '12211221123321', 'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' ],
|
|
|
|
|
$op->getFileVersion()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$stubMissingFile = $this->createMock( File::class );
|
|
|
|
|
$stubMissingFile->method( 'exists' )->willReturn( false );
|
|
|
|
|
|
2019-09-30 14:25:17 +00:00
|
|
|
/** @var File $stubMissingFile */
|
2018-07-25 18:41:42 +00:00
|
|
|
$op->setFileVersion( $stubMissingFile );
|
|
|
|
|
$this->assertNull( $op->getFileVersion() );
|
|
|
|
|
|
|
|
|
|
$op->setFileVersion( $stubFile );
|
|
|
|
|
$this->assertNotNull( $op->getFileVersion() );
|
|
|
|
|
|
|
|
|
|
$op->setFileVersion( null );
|
|
|
|
|
$this->assertNull( $op->getFileVersion() );
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
/**
|
|
|
|
|
* Call either with arguments $methodName, $returnValue; or an array
|
|
|
|
|
* [ $methodName => $returnValue, $methodName => $returnValue, ... ]
|
2021-01-16 19:44:17 +00:00
|
|
|
* @param mixed ...$args
|
|
|
|
|
* @return ParserOutput
|
2018-08-02 18:42:17 +00:00
|
|
|
*/
|
2021-07-22 03:11:47 +00:00
|
|
|
private function createParserOutputStub( ...$args ): ParserOutput {
|
2018-08-02 18:42:17 +00:00
|
|
|
if ( count( $args ) === 0 ) {
|
|
|
|
|
$retVals = [];
|
|
|
|
|
} elseif ( count( $args ) === 1 ) {
|
|
|
|
|
$retVals = $args[0];
|
|
|
|
|
} elseif ( count( $args ) === 2 ) {
|
|
|
|
|
$retVals = [ $args[0] => $args[1] ];
|
|
|
|
|
}
|
2023-03-21 18:26:05 +00:00
|
|
|
return $this->createParserOutputStubWithFlags( $retVals, [] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* First argument is an array
|
|
|
|
|
* [ $methodName => $returnValue, $methodName => $returnValue, ... ]
|
|
|
|
|
* Second argument is an array of parser flags for which ::getOutputFlag()
|
|
|
|
|
* should return 'TRUE'.
|
|
|
|
|
* @param array $retVals
|
|
|
|
|
* @param array $flags
|
|
|
|
|
* @return ParserOutput
|
|
|
|
|
*/
|
|
|
|
|
private function createParserOutputStubWithFlags( array $retVals, array $flags ): ParserOutput {
|
2019-10-05 22:14:35 +00:00
|
|
|
$pOut = $this->createMock( ParserOutput::class );
|
2023-11-02 03:21:07 +00:00
|
|
|
|
|
|
|
|
$mockedGetText = false;
|
2018-08-02 18:42:17 +00:00
|
|
|
foreach ( $retVals as $method => $retVal ) {
|
2018-07-25 18:41:42 +00:00
|
|
|
$pOut->method( $method )->willReturn( $retVal );
|
2023-11-02 03:21:07 +00:00
|
|
|
if ( $method === 'getText' ) {
|
|
|
|
|
$mockedGetText = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Needed to ensure OutputPage::getParserOutputText doesn't return null
|
|
|
|
|
if ( !$mockedGetText ) {
|
|
|
|
|
$pOut->method( 'getText' )->willReturn( '' );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$arrayReturningMethods = [
|
|
|
|
|
'getCategories',
|
2023-09-21 17:06:50 +00:00
|
|
|
'getCategoryMap',
|
2018-07-25 18:41:42 +00:00
|
|
|
'getFileSearchOptions',
|
|
|
|
|
'getHeadItems',
|
2020-02-07 05:31:00 +00:00
|
|
|
'getImages',
|
2018-07-25 18:41:42 +00:00
|
|
|
'getIndicators',
|
2022-12-19 22:43:43 +00:00
|
|
|
'getSections',
|
2018-07-25 18:41:42 +00:00
|
|
|
'getLanguageLinks',
|
|
|
|
|
'getTemplateIds',
|
2020-02-03 09:50:14 +00:00
|
|
|
'getExtraCSPDefaultSrcs',
|
|
|
|
|
'getExtraCSPStyleSrcs',
|
|
|
|
|
'getExtraCSPScriptSrcs',
|
2018-07-25 18:41:42 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ( $arrayReturningMethods as $method ) {
|
|
|
|
|
$pOut->method( $method )->willReturn( [] );
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-21 18:26:05 +00:00
|
|
|
$pOut->method( 'getOutputFlag' )->willReturnCallback( static function ( $name ) use ( $flags ) {
|
|
|
|
|
return in_array( $name, $flags, true );
|
|
|
|
|
} );
|
|
|
|
|
|
2018-07-25 18:41:42 +00:00
|
|
|
return $pOut;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testTemplateIds() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( [], $op->getTemplateIds() );
|
|
|
|
|
|
|
|
|
|
// Test with no template id's
|
|
|
|
|
$stubPOEmpty = $this->createParserOutputStub();
|
|
|
|
|
$op->addParserOutputMetadata( $stubPOEmpty );
|
|
|
|
|
$this->assertSame( [], $op->getTemplateIds() );
|
|
|
|
|
|
|
|
|
|
// Test with some arbitrary template id's
|
|
|
|
|
$ids = [
|
|
|
|
|
NS_MAIN => [ 'A' => 3, 'B' => 17 ],
|
|
|
|
|
NS_TALK => [ 'C' => 31 ],
|
|
|
|
|
NS_MEDIA => [ 'D' => -1 ],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$stubPO1 = $this->createParserOutputStub( 'getTemplateIds', $ids );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutputMetadata( $stubPO1 );
|
|
|
|
|
$this->assertSame( $ids, $op->getTemplateIds() );
|
|
|
|
|
|
|
|
|
|
// Test merging with a second set of id's
|
|
|
|
|
$stubPO2 = $this->createParserOutputStub( 'getTemplateIds', [
|
|
|
|
|
NS_MAIN => [ 'E' => 1234 ],
|
|
|
|
|
NS_PROJECT => [ 'F' => 5678 ],
|
2016-02-17 09:09:32 +00:00
|
|
|
] );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
|
|
|
|
$finalIds = [
|
|
|
|
|
NS_MAIN => [ 'E' => 1234, 'A' => 3, 'B' => 17 ],
|
|
|
|
|
NS_TALK => [ 'C' => 31 ],
|
|
|
|
|
NS_MEDIA => [ 'D' => -1 ],
|
|
|
|
|
NS_PROJECT => [ 'F' => 5678 ],
|
|
|
|
|
];
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutput( $stubPO2 );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertSame( $finalIds, $op->getTemplateIds() );
|
|
|
|
|
|
|
|
|
|
// Test merging with an empty set of id's
|
|
|
|
|
$op->addParserOutputMetadata( $stubPOEmpty );
|
|
|
|
|
$this->assertSame( $finalIds, $op->getTemplateIds() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testFileSearchOptions() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( [], $op->getFileSearchOptions() );
|
|
|
|
|
|
|
|
|
|
// Test with no files
|
|
|
|
|
$stubPOEmpty = $this->createParserOutputStub();
|
|
|
|
|
|
|
|
|
|
$op->addParserOutputMetadata( $stubPOEmpty );
|
|
|
|
|
$this->assertSame( [], $op->getFileSearchOptions() );
|
|
|
|
|
|
|
|
|
|
// Test with some arbitrary files
|
|
|
|
|
$files1 = [
|
|
|
|
|
'A' => [ 'time' => null, 'sha1' => '' ],
|
|
|
|
|
'B' => [
|
|
|
|
|
'time' => '12211221123321',
|
|
|
|
|
'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05',
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$stubPO1 = $this->createParserOutputStub( 'getFileSearchOptions', $files1 );
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutput( $stubPO1 );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertSame( $files1, $op->getFileSearchOptions() );
|
|
|
|
|
|
|
|
|
|
// Test merging with a second set of files
|
|
|
|
|
$files2 = [
|
|
|
|
|
'C' => [ 'time' => null, 'sha1' => '' ],
|
|
|
|
|
'B' => [ 'time' => null, 'sha1' => '' ],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$stubPO2 = $this->createParserOutputStub( 'getFileSearchOptions', $files2 );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutputMetadata( $stubPO2 );
|
|
|
|
|
$this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
|
|
|
|
|
|
|
|
|
|
// Test merging with an empty set of files
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutput( $stubPOEmpty );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
|
2013-01-14 03:26:15 +00:00
|
|
|
}
|
2014-06-28 20:40:22 +00:00
|
|
|
|
2018-07-25 18:41:42 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideAddWikiText
|
|
|
|
|
*/
|
|
|
|
|
public function testAddWikiText( $method, array $args, $expected ) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
|
2018-09-21 16:24:57 +00:00
|
|
|
if ( in_array(
|
|
|
|
|
$method,
|
|
|
|
|
[ 'addWikiTextAsInterface', 'addWikiTextAsContent' ]
|
|
|
|
|
) && count( $args ) >= 3 && $args[2] === null ) {
|
|
|
|
|
// Special placeholder because we can't get the actual title in the provider
|
|
|
|
|
$args[2] = $op->getTitle();
|
|
|
|
|
}
|
2018-07-25 18:41:42 +00:00
|
|
|
|
|
|
|
|
$op->$method( ...$args );
|
|
|
|
|
$this->assertSame( $expected, $op->getHTML() );
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideAddWikiText() {
|
2021-04-08 19:11:06 +00:00
|
|
|
$somePageRef = new PageReferenceValue( NS_TALK, 'Some page', PageReference::LOCAL );
|
|
|
|
|
|
2018-07-25 18:41:42 +00:00
|
|
|
$tests = [
|
2018-09-21 16:24:57 +00:00
|
|
|
'addWikiTextAsInterface' => [
|
|
|
|
|
'Simple wikitext' => [
|
|
|
|
|
[ "'''Bold'''" ],
|
|
|
|
|
"<p><b>Bold</b>\n</p>",
|
|
|
|
|
], 'Untidy wikitext' => [
|
|
|
|
|
[ "<b>Bold" ],
|
|
|
|
|
"<p><b>Bold\n</b></p>",
|
|
|
|
|
], 'List at start' => [
|
|
|
|
|
[ '* List' ],
|
|
|
|
|
"<ul><li>List</li></ul>\n",
|
|
|
|
|
], 'List not at start' => [
|
|
|
|
|
[ '* Not a list', false ],
|
|
|
|
|
'<p>* Not a list</p>',
|
|
|
|
|
], 'No section edit links' => [
|
|
|
|
|
[ '== Title ==' ],
|
2018-10-23 23:26:51 +00:00
|
|
|
"<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>",
|
2018-09-21 16:24:57 +00:00
|
|
|
], 'With title at start' => [
|
2022-09-23 19:53:11 +00:00
|
|
|
[ '* {{PAGENAME}}', true, Title::makeTitle( NS_TALK, 'Some page' ) ],
|
2018-09-21 16:24:57 +00:00
|
|
|
"<ul><li>Some page</li></ul>\n",
|
2021-04-08 19:11:06 +00:00
|
|
|
], 'With title not at start' => [
|
2022-09-23 19:53:11 +00:00
|
|
|
[ '* {{PAGENAME}}', false, Title::makeTitle( NS_TALK, 'Some page' ) ],
|
2018-09-21 16:24:57 +00:00
|
|
|
"<p>* Some page</p>",
|
|
|
|
|
], 'Untidy input' => [
|
2021-04-08 19:11:06 +00:00
|
|
|
[ '<b>{{PAGENAME}}', true, $somePageRef ],
|
2018-09-21 16:24:57 +00:00
|
|
|
"<p><b>Some page\n</b></p>",
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'addWikiTextAsContent' => [
|
2018-09-25 13:06:12 +00:00
|
|
|
'SpecialNewimages' => [
|
|
|
|
|
[ "<p lang='en' dir='ltr'>\nMy message" ],
|
2018-10-23 23:26:51 +00:00
|
|
|
'<p lang="en" dir="ltr">' . "\nMy message</p>"
|
2018-09-25 13:06:12 +00:00
|
|
|
], 'List at start' => [
|
|
|
|
|
[ '* List' ],
|
2018-10-23 22:47:48 +00:00
|
|
|
"<ul><li>List</li></ul>",
|
2018-09-25 13:06:12 +00:00
|
|
|
], 'List not at start' => [
|
|
|
|
|
[ '* <b>Not a list', false ],
|
|
|
|
|
'<p>* <b>Not a list</b></p>',
|
2018-09-21 16:24:57 +00:00
|
|
|
], 'With title at start' => [
|
2022-09-23 19:53:11 +00:00
|
|
|
[ '* {{PAGENAME}}', true, Title::makeTitle( NS_TALK, 'Some page' ) ],
|
2021-04-08 19:11:06 +00:00
|
|
|
"<ul><li>Some page</li></ul>",
|
|
|
|
|
], 'With title not at start' => [
|
2022-09-23 19:53:11 +00:00
|
|
|
[ '* {{PAGENAME}}', false, Title::makeTitle( NS_TALK, 'Some page' ) ],
|
2018-09-25 13:06:12 +00:00
|
|
|
"<p>* Some page</p>",
|
|
|
|
|
], 'EditPage' => [
|
2021-04-08 19:11:06 +00:00
|
|
|
[ "<div class='mw-editintro'>{{PAGENAME}}", true, $somePageRef ],
|
2018-10-23 23:26:51 +00:00
|
|
|
'<div class="mw-editintro">' . "Some page</div>"
|
2018-07-25 18:41:42 +00:00
|
|
|
],
|
|
|
|
|
],
|
2018-09-27 15:04:45 +00:00
|
|
|
'wrapWikiTextAsInterface' => [
|
|
|
|
|
'Simple' => [
|
|
|
|
|
[ 'wrapperClass', 'text' ],
|
parser: Move lang/dir and mw-content-ltr to ParserOutput::getText
== Skin::wrapHTML ==
Skin::wrapHTML no longer has to perform any guessing of the
ParserOutput language. Nor does it have to special wiki pages vs
special pages in this regard. Yay, code removal.
== ImagePage ==
On URLs like /wiki/File:Example.jpg, the main output handler is
ImagePage::view. This calls the parent Article::view to handle most of
its output. Article::view obtains the ParserOptions, and then fetches
ParserOutput, and then adds `<div class=mw-parser-output>` and its
metadata to OutputPage.
Before this change, ImagePage::view was creating a wrapper based
on "predicting" what language the ParserOutput will contain. It
couldn't call the new OutputPage::getContentLanguage or some
equivalent as Article::view wouldn't have populated that yet.
This leaky abstraction is fixed by this change as now the `<div>`
from ParserOutput no longer comes with a "please wrap it properly"
contract that Article subclasses couldn't possibly implement correctly
(it coudln't wrap it after the fact because Article::view writes to
OutputPage directly).
RECENT (T310445):
A special case was recently added for file pages about translated SVGs.
For those, we decide which language to use for the "fullMedia" thumb
atop the page. This was recently changed as part of T310445 from a
hardcoded $wgLanguageCode (site content lang) to new problematic
Title::getPageViewLanguage, which tries to guestimate the page
language of the rendered ParserOutput and then gets the preferred
variant for the current user. The motivation for this was to support
language variants but used Title::getPageViewLanguage as a kitchen
sink to achieve that minor side-effect. The only part of this
now-deprecated method that we actually need is
LanguageConverter::getPreferredVariant().
Test plan: Covered by ImagePageTest.
== Skin mainpage-title ==
RECENT (T331095, T298715):
A special case was added to Skin::getTemplateData that powers the
mainpage-title interface message feature. This is empty by default,
but when created via MediaWiki:mainpage-title allows interface admins
to replace the H1 with a custom and localised page heading.
A few months ago, in Ifc9f0a7174, Title::getPageViewLanguage was
applied here to support language variants. Replace with the same
fix as for ImagePage. Revert back to Message::inContentLanguage()
but refactor to inLanguage() via MediaWikiServices::getContentLanguage
so that LanguageConverter::getPreferredVariant can be applied.
== EditPage ==
This was doing similar "predicting" of the ParserOutput language to
create an empty preview placeholder for use by preview.js. Now that
ApiParse (via ParserOutput::getText) returns a usable element without
any secret "you magically know the right class, lang, and dir" contract,
this placeholder is no longer needed.
Test Plan:
* EditPage: Default preview
1. index.php?title=Main_Page&action=edit
2. Show preview
3. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
* EditPage: JS preview
1. Preferences > Editing > Show preview without reload
2. index.php?title=Main_Page&action=edit
3. Show preview
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
5. Type something and 'Show preview' again
6. Assert old element gone, new text is shown, and new element
attributes are the same as the above.
== McrUndoAction ==
Same as EditPage basically, but without the JS preview use case.
== DifferenceEngine ==
Test:
1. Open /w/index.php?title=Main_Page&diff=0
(this shows the latest diff, can do manually by viewing
/wiki/Main_Page, click "View history", click "Compare selected revisions")
2. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
3. Open /w/index.php?title=Main_Page&diff=0&action=render
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
== Special:ExpandTemplates ==
Test:
1. /wiki/Special:ExpandTemplates
2. Write "Hello".
3. "OK"
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
Bug: T341244
Depends-On: Icd9c079f5896ee83d86b9c2699636dc81d25a14c
Depends-On: I4e7484b3b94f1cb6062e7cef9f20626b650bb4b1
Depends-On: I90b88f3b3a3bbeba4f48d118f92f54864997e105
Change-Id: Ib130a055e46764544af0f1a46d2bc2b3a7ee85b7
2023-10-04 04:45:07 +00:00
|
|
|
"<div class=\"mw-content-ltr wrapperClass\" lang=\"en\" dir=\"ltr\"><p>text\n</p></div>"
|
2018-09-27 15:04:45 +00:00
|
|
|
], 'Spurious </div>' => [
|
|
|
|
|
[ 'wrapperClass', 'text</div><div>more' ],
|
parser: Move lang/dir and mw-content-ltr to ParserOutput::getText
== Skin::wrapHTML ==
Skin::wrapHTML no longer has to perform any guessing of the
ParserOutput language. Nor does it have to special wiki pages vs
special pages in this regard. Yay, code removal.
== ImagePage ==
On URLs like /wiki/File:Example.jpg, the main output handler is
ImagePage::view. This calls the parent Article::view to handle most of
its output. Article::view obtains the ParserOptions, and then fetches
ParserOutput, and then adds `<div class=mw-parser-output>` and its
metadata to OutputPage.
Before this change, ImagePage::view was creating a wrapper based
on "predicting" what language the ParserOutput will contain. It
couldn't call the new OutputPage::getContentLanguage or some
equivalent as Article::view wouldn't have populated that yet.
This leaky abstraction is fixed by this change as now the `<div>`
from ParserOutput no longer comes with a "please wrap it properly"
contract that Article subclasses couldn't possibly implement correctly
(it coudln't wrap it after the fact because Article::view writes to
OutputPage directly).
RECENT (T310445):
A special case was recently added for file pages about translated SVGs.
For those, we decide which language to use for the "fullMedia" thumb
atop the page. This was recently changed as part of T310445 from a
hardcoded $wgLanguageCode (site content lang) to new problematic
Title::getPageViewLanguage, which tries to guestimate the page
language of the rendered ParserOutput and then gets the preferred
variant for the current user. The motivation for this was to support
language variants but used Title::getPageViewLanguage as a kitchen
sink to achieve that minor side-effect. The only part of this
now-deprecated method that we actually need is
LanguageConverter::getPreferredVariant().
Test plan: Covered by ImagePageTest.
== Skin mainpage-title ==
RECENT (T331095, T298715):
A special case was added to Skin::getTemplateData that powers the
mainpage-title interface message feature. This is empty by default,
but when created via MediaWiki:mainpage-title allows interface admins
to replace the H1 with a custom and localised page heading.
A few months ago, in Ifc9f0a7174, Title::getPageViewLanguage was
applied here to support language variants. Replace with the same
fix as for ImagePage. Revert back to Message::inContentLanguage()
but refactor to inLanguage() via MediaWikiServices::getContentLanguage
so that LanguageConverter::getPreferredVariant can be applied.
== EditPage ==
This was doing similar "predicting" of the ParserOutput language to
create an empty preview placeholder for use by preview.js. Now that
ApiParse (via ParserOutput::getText) returns a usable element without
any secret "you magically know the right class, lang, and dir" contract,
this placeholder is no longer needed.
Test Plan:
* EditPage: Default preview
1. index.php?title=Main_Page&action=edit
2. Show preview
3. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
* EditPage: JS preview
1. Preferences > Editing > Show preview without reload
2. index.php?title=Main_Page&action=edit
3. Show preview
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
5. Type something and 'Show preview' again
6. Assert old element gone, new text is shown, and new element
attributes are the same as the above.
== McrUndoAction ==
Same as EditPage basically, but without the JS preview use case.
== DifferenceEngine ==
Test:
1. Open /w/index.php?title=Main_Page&diff=0
(this shows the latest diff, can do manually by viewing
/wiki/Main_Page, click "View history", click "Compare selected revisions")
2. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
3. Open /w/index.php?title=Main_Page&diff=0&action=render
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
== Special:ExpandTemplates ==
Test:
1. /wiki/Special:ExpandTemplates
2. Write "Hello".
3. "OK"
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
Bug: T341244
Depends-On: Icd9c079f5896ee83d86b9c2699636dc81d25a14c
Depends-On: I4e7484b3b94f1cb6062e7cef9f20626b650bb4b1
Depends-On: I90b88f3b3a3bbeba4f48d118f92f54864997e105
Change-Id: Ib130a055e46764544af0f1a46d2bc2b3a7ee85b7
2023-10-04 04:45:07 +00:00
|
|
|
"<div class=\"mw-content-ltr wrapperClass\" lang=\"en\" dir=\"ltr\"><p>text</p><div>more</div></div>"
|
2018-09-27 15:04:45 +00:00
|
|
|
], 'Extra newlines would break <p> wrappers' => [
|
|
|
|
|
[ 'two classes', "1\n\n2\n\n3" ],
|
parser: Move lang/dir and mw-content-ltr to ParserOutput::getText
== Skin::wrapHTML ==
Skin::wrapHTML no longer has to perform any guessing of the
ParserOutput language. Nor does it have to special wiki pages vs
special pages in this regard. Yay, code removal.
== ImagePage ==
On URLs like /wiki/File:Example.jpg, the main output handler is
ImagePage::view. This calls the parent Article::view to handle most of
its output. Article::view obtains the ParserOptions, and then fetches
ParserOutput, and then adds `<div class=mw-parser-output>` and its
metadata to OutputPage.
Before this change, ImagePage::view was creating a wrapper based
on "predicting" what language the ParserOutput will contain. It
couldn't call the new OutputPage::getContentLanguage or some
equivalent as Article::view wouldn't have populated that yet.
This leaky abstraction is fixed by this change as now the `<div>`
from ParserOutput no longer comes with a "please wrap it properly"
contract that Article subclasses couldn't possibly implement correctly
(it coudln't wrap it after the fact because Article::view writes to
OutputPage directly).
RECENT (T310445):
A special case was recently added for file pages about translated SVGs.
For those, we decide which language to use for the "fullMedia" thumb
atop the page. This was recently changed as part of T310445 from a
hardcoded $wgLanguageCode (site content lang) to new problematic
Title::getPageViewLanguage, which tries to guestimate the page
language of the rendered ParserOutput and then gets the preferred
variant for the current user. The motivation for this was to support
language variants but used Title::getPageViewLanguage as a kitchen
sink to achieve that minor side-effect. The only part of this
now-deprecated method that we actually need is
LanguageConverter::getPreferredVariant().
Test plan: Covered by ImagePageTest.
== Skin mainpage-title ==
RECENT (T331095, T298715):
A special case was added to Skin::getTemplateData that powers the
mainpage-title interface message feature. This is empty by default,
but when created via MediaWiki:mainpage-title allows interface admins
to replace the H1 with a custom and localised page heading.
A few months ago, in Ifc9f0a7174, Title::getPageViewLanguage was
applied here to support language variants. Replace with the same
fix as for ImagePage. Revert back to Message::inContentLanguage()
but refactor to inLanguage() via MediaWikiServices::getContentLanguage
so that LanguageConverter::getPreferredVariant can be applied.
== EditPage ==
This was doing similar "predicting" of the ParserOutput language to
create an empty preview placeholder for use by preview.js. Now that
ApiParse (via ParserOutput::getText) returns a usable element without
any secret "you magically know the right class, lang, and dir" contract,
this placeholder is no longer needed.
Test Plan:
* EditPage: Default preview
1. index.php?title=Main_Page&action=edit
2. Show preview
3. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
* EditPage: JS preview
1. Preferences > Editing > Show preview without reload
2. index.php?title=Main_Page&action=edit
3. Show preview
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
5. Type something and 'Show preview' again
6. Assert old element gone, new text is shown, and new element
attributes are the same as the above.
== McrUndoAction ==
Same as EditPage basically, but without the JS preview use case.
== DifferenceEngine ==
Test:
1. Open /w/index.php?title=Main_Page&diff=0
(this shows the latest diff, can do manually by viewing
/wiki/Main_Page, click "View history", click "Compare selected revisions")
2. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
3. Open /w/index.php?title=Main_Page&diff=0&action=render
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
== Special:ExpandTemplates ==
Test:
1. /wiki/Special:ExpandTemplates
2. Write "Hello".
3. "OK"
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
Bug: T341244
Depends-On: Icd9c079f5896ee83d86b9c2699636dc81d25a14c
Depends-On: I4e7484b3b94f1cb6062e7cef9f20626b650bb4b1
Depends-On: I90b88f3b3a3bbeba4f48d118f92f54864997e105
Change-Id: Ib130a055e46764544af0f1a46d2bc2b3a7ee85b7
2023-10-04 04:45:07 +00:00
|
|
|
"<div class=\"mw-content-ltr two classes\" lang=\"en\" dir=\"ltr\"><p>1\n</p><p>2\n</p><p>3\n</p></div>"
|
2018-09-27 15:04:45 +00:00
|
|
|
], 'Other unclosed tags' => [
|
|
|
|
|
[ 'error', 'a<b>c<i>d' ],
|
parser: Move lang/dir and mw-content-ltr to ParserOutput::getText
== Skin::wrapHTML ==
Skin::wrapHTML no longer has to perform any guessing of the
ParserOutput language. Nor does it have to special wiki pages vs
special pages in this regard. Yay, code removal.
== ImagePage ==
On URLs like /wiki/File:Example.jpg, the main output handler is
ImagePage::view. This calls the parent Article::view to handle most of
its output. Article::view obtains the ParserOptions, and then fetches
ParserOutput, and then adds `<div class=mw-parser-output>` and its
metadata to OutputPage.
Before this change, ImagePage::view was creating a wrapper based
on "predicting" what language the ParserOutput will contain. It
couldn't call the new OutputPage::getContentLanguage or some
equivalent as Article::view wouldn't have populated that yet.
This leaky abstraction is fixed by this change as now the `<div>`
from ParserOutput no longer comes with a "please wrap it properly"
contract that Article subclasses couldn't possibly implement correctly
(it coudln't wrap it after the fact because Article::view writes to
OutputPage directly).
RECENT (T310445):
A special case was recently added for file pages about translated SVGs.
For those, we decide which language to use for the "fullMedia" thumb
atop the page. This was recently changed as part of T310445 from a
hardcoded $wgLanguageCode (site content lang) to new problematic
Title::getPageViewLanguage, which tries to guestimate the page
language of the rendered ParserOutput and then gets the preferred
variant for the current user. The motivation for this was to support
language variants but used Title::getPageViewLanguage as a kitchen
sink to achieve that minor side-effect. The only part of this
now-deprecated method that we actually need is
LanguageConverter::getPreferredVariant().
Test plan: Covered by ImagePageTest.
== Skin mainpage-title ==
RECENT (T331095, T298715):
A special case was added to Skin::getTemplateData that powers the
mainpage-title interface message feature. This is empty by default,
but when created via MediaWiki:mainpage-title allows interface admins
to replace the H1 with a custom and localised page heading.
A few months ago, in Ifc9f0a7174, Title::getPageViewLanguage was
applied here to support language variants. Replace with the same
fix as for ImagePage. Revert back to Message::inContentLanguage()
but refactor to inLanguage() via MediaWikiServices::getContentLanguage
so that LanguageConverter::getPreferredVariant can be applied.
== EditPage ==
This was doing similar "predicting" of the ParserOutput language to
create an empty preview placeholder for use by preview.js. Now that
ApiParse (via ParserOutput::getText) returns a usable element without
any secret "you magically know the right class, lang, and dir" contract,
this placeholder is no longer needed.
Test Plan:
* EditPage: Default preview
1. index.php?title=Main_Page&action=edit
2. Show preview
3. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
* EditPage: JS preview
1. Preferences > Editing > Show preview without reload
2. index.php?title=Main_Page&action=edit
3. Show preview
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
5. Type something and 'Show preview' again
6. Assert old element gone, new text is shown, and new element
attributes are the same as the above.
== McrUndoAction ==
Same as EditPage basically, but without the JS preview use case.
== DifferenceEngine ==
Test:
1. Open /w/index.php?title=Main_Page&diff=0
(this shows the latest diff, can do manually by viewing
/wiki/Main_Page, click "View history", click "Compare selected revisions")
2. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
3. Open /w/index.php?title=Main_Page&diff=0&action=render
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
== Special:ExpandTemplates ==
Test:
1. /wiki/Special:ExpandTemplates
2. Write "Hello".
3. "OK"
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
Bug: T341244
Depends-On: Icd9c079f5896ee83d86b9c2699636dc81d25a14c
Depends-On: I4e7484b3b94f1cb6062e7cef9f20626b650bb4b1
Depends-On: I90b88f3b3a3bbeba4f48d118f92f54864997e105
Change-Id: Ib130a055e46764544af0f1a46d2bc2b3a7ee85b7
2023-10-04 04:45:07 +00:00
|
|
|
"<div class=\"mw-content-ltr error\" lang=\"en\" dir=\"ltr\"><p>a<b>c<i>d\n</i></b></p></div>"
|
2018-09-27 15:04:45 +00:00
|
|
|
],
|
|
|
|
|
],
|
2018-07-25 18:41:42 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// We have to reformat our array to match what PHPUnit wants
|
|
|
|
|
$ret = [];
|
|
|
|
|
foreach ( $tests as $key => $subarray ) {
|
|
|
|
|
foreach ( $subarray as $subkey => $val ) {
|
|
|
|
|
$val = array_merge( [ $key ], $val );
|
|
|
|
|
$ret[$subkey] = $val;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-21 16:24:57 +00:00
|
|
|
public function testAddWikiTextAsInterfaceNoTitle() {
|
2023-06-10 00:29:51 +00:00
|
|
|
$this->expectException( RuntimeException::class );
|
2019-10-05 15:42:53 +00:00
|
|
|
$this->expectExceptionMessage( 'Title is null' );
|
2018-09-21 16:24:57 +00:00
|
|
|
|
|
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->addWikiTextAsInterface( 'a' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddWikiTextAsContentNoTitle() {
|
2023-06-10 00:29:51 +00:00
|
|
|
$this->expectException( RuntimeException::class );
|
2019-10-05 15:42:53 +00:00
|
|
|
$this->expectExceptionMessage( 'Title is null' );
|
2018-09-21 16:24:57 +00:00
|
|
|
|
|
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->addWikiTextAsContent( 'a' );
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-27 15:05:47 +00:00
|
|
|
public function testAddWikiMsg() {
|
|
|
|
|
$msg = wfMessage( 'parentheses' );
|
|
|
|
|
$this->assertSame( '(a)', $msg->rawParams( 'a' )->plain() );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
$op->addWikiMsg( 'parentheses', "<b>a" );
|
2018-09-27 21:45:04 +00:00
|
|
|
// The input is bad unbalanced HTML, but the output is tidied
|
|
|
|
|
$this->assertSame( "<p>(<b>a)\n</b></p>", $op->getHTML() );
|
2018-09-27 15:05:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testWrapWikiMsg() {
|
|
|
|
|
$msg = wfMessage( 'parentheses' );
|
|
|
|
|
$this->assertSame( '(a)', $msg->rawParams( 'a' )->plain() );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
$op->wrapWikiMsg( '[$1]', [ 'parentheses', "<b>a" ] );
|
2018-09-25 15:02:07 +00:00
|
|
|
// The input is bad unbalanced HTML, but the output is tidied
|
|
|
|
|
$this->assertSame( "<p>[(<b>a)]\n</b></p>", $op->getHTML() );
|
2018-09-27 15:05:47 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-25 18:41:42 +00:00
|
|
|
public function testNoGallery() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertFalse( $op->mNoGallery );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertFalse( $op->getOutputFlag( ParserOutputFlags::NO_GALLERY ) );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
2023-03-21 18:26:05 +00:00
|
|
|
$stubPO1 = $this->createParserOutputStubWithFlags(
|
|
|
|
|
[ 'getNoGallery' => true ], [ ParserOutputFlags::NO_GALLERY ]
|
|
|
|
|
);
|
2018-07-25 18:41:42 +00:00
|
|
|
$op->addParserOutputMetadata( $stubPO1 );
|
|
|
|
|
$this->assertTrue( $op->mNoGallery );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::NO_GALLERY ) );
|
2018-07-25 18:41:42 +00:00
|
|
|
|
|
|
|
|
$stubPO2 = $this->createParserOutputStub( 'getNoGallery', false );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutput( $stubPO2 );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertFalse( $op->mNoGallery );
|
2023-03-21 18:26:05 +00:00
|
|
|
// Note that flags are OR'ed together, and not reset.
|
|
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::NO_GALLERY ) );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @todo Make sure to test the following in addParserOutputMetadata() as well when we add tests
|
|
|
|
|
// for them:
|
|
|
|
|
// * addModules()
|
|
|
|
|
// * addModuleStyles()
|
|
|
|
|
// * addJsConfigVars()
|
2018-08-02 18:42:17 +00:00
|
|
|
// * enableOOUI()
|
2018-07-25 18:41:42 +00:00
|
|
|
// Otherwise those lines of addParserOutputMetadata() will be reported as covered, but we won't
|
|
|
|
|
// be testing they actually work.
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
public function testAddParserOutputText() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
|
2023-11-02 03:21:07 +00:00
|
|
|
$text = '<some text>';
|
|
|
|
|
$pOut = $this->createParserOutputStub( 'getText', $text );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
$op->addParserOutputMetadata( $pOut );
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
|
2023-11-02 03:21:07 +00:00
|
|
|
$op->addParserOutputText( $text );
|
2018-08-02 18:42:17 +00:00
|
|
|
$this->assertSame( '<some text>', $op->getHTML() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddParserOutput() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
$this->assertFalse( $op->showNewSectionLink() );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertFalse( $op->getOutputFlag( ParserOutputFlags::NEW_SECTION ) );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
2023-03-21 18:26:05 +00:00
|
|
|
$pOut = $this->createParserOutputStubWithFlags( [
|
2018-08-02 18:42:17 +00:00
|
|
|
'getText' => '<some text>',
|
|
|
|
|
'getNewSection' => true,
|
2023-03-21 18:26:05 +00:00
|
|
|
], [
|
|
|
|
|
ParserOutputFlags::NEW_SECTION,
|
2018-08-02 18:42:17 +00:00
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutput( $pOut );
|
|
|
|
|
$this->assertSame( '<some text>', $op->getHTML() );
|
|
|
|
|
$this->assertTrue( $op->showNewSectionLink() );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::NEW_SECTION ) );
|
2018-08-02 18:42:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAddTemplate() {
|
2019-10-05 22:14:35 +00:00
|
|
|
$template = $this->createMock( QuickTemplate::class );
|
2018-08-02 18:42:17 +00:00
|
|
|
$template->method( 'getHTML' )->willReturn( '<abc>&def;' );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addTemplate( $template );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( '<abc>&def;', $op->getHTML() );
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-26 15:14:01 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideParseAs
|
|
|
|
|
*/
|
|
|
|
|
public function testParseAsContent(
|
|
|
|
|
array $args, $expectedHTML, $expectedHTMLInline = null
|
|
|
|
|
) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( $expectedHTML, $op->parseAsContent( ...$args ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideParseAs
|
|
|
|
|
*/
|
|
|
|
|
public function testParseAsInterface(
|
|
|
|
|
array $args, $expectedHTML, $expectedHTMLInline = null
|
|
|
|
|
) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( $expectedHTML, $op->parseAsInterface( ...$args ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideParseAs
|
|
|
|
|
*/
|
|
|
|
|
public function testParseInlineAsInterface(
|
|
|
|
|
array $args, $expectedHTML, $expectedHTMLInline = null
|
|
|
|
|
) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
$expectedHTMLInline ?? $expectedHTML,
|
|
|
|
|
$op->parseInlineAsInterface( ...$args )
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideParseAs() {
|
2018-10-26 15:14:01 +00:00
|
|
|
return [
|
|
|
|
|
'List at start of line' => [
|
|
|
|
|
[ '* List', true ],
|
2018-10-23 22:47:48 +00:00
|
|
|
"<ul><li>List</li></ul>",
|
2018-10-26 15:14:01 +00:00
|
|
|
],
|
|
|
|
|
'List not at start' => [
|
|
|
|
|
[ "* ''Not'' list", false ],
|
|
|
|
|
'<p>* <i>Not</i> list</p>',
|
|
|
|
|
'* <i>Not</i> list',
|
|
|
|
|
],
|
|
|
|
|
'Italics' => [
|
|
|
|
|
[ "''Italic''", true ],
|
|
|
|
|
"<p><i>Italic</i>\n</p>",
|
|
|
|
|
'<i>Italic</i>',
|
|
|
|
|
],
|
|
|
|
|
'formatnum' => [
|
|
|
|
|
[ '{{formatnum:123456.789}}', true ],
|
|
|
|
|
"<p>123,456.789\n</p>",
|
|
|
|
|
"123,456.789",
|
|
|
|
|
],
|
|
|
|
|
'No section edit links' => [
|
|
|
|
|
[ '== Header ==' ],
|
2018-10-23 23:26:51 +00:00
|
|
|
'<h2><span class="mw-headline" id="Header">Header</span></h2>',
|
2018-10-26 15:14:01 +00:00
|
|
|
]
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testParseAsContentNullTitle() {
|
2023-06-10 00:29:51 +00:00
|
|
|
$this->expectException( RuntimeException::class );
|
2023-09-05 17:31:53 +00:00
|
|
|
$this->expectExceptionMessage( 'Empty $mTitle in MediaWiki\Output\OutputPage::parseInternal' );
|
2018-10-26 15:14:01 +00:00
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->parseAsContent( '' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testParseAsInterfaceNullTitle() {
|
2023-06-10 00:29:51 +00:00
|
|
|
$this->expectException( RuntimeException::class );
|
2023-09-05 17:31:53 +00:00
|
|
|
$this->expectExceptionMessage( 'Empty $mTitle in MediaWiki\Output\OutputPage::parseInternal' );
|
2018-10-26 15:14:01 +00:00
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->parseAsInterface( '' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testParseInlineAsInterfaceNullTitle() {
|
2023-06-10 00:29:51 +00:00
|
|
|
$this->expectException( RuntimeException::class );
|
2023-09-05 17:31:53 +00:00
|
|
|
$this->expectExceptionMessage( 'Empty $mTitle in MediaWiki\Output\OutputPage::parseInternal' );
|
2018-10-26 15:14:01 +00:00
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->parseInlineAsInterface( '' );
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
public function testCdnMaxage() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$wrapper = TestingAccessWrapper::newFromObject( $op );
|
|
|
|
|
$this->assertSame( 0, $wrapper->mCdnMaxage );
|
|
|
|
|
|
|
|
|
|
$op->setCdnMaxage( -1 );
|
|
|
|
|
$this->assertSame( -1, $wrapper->mCdnMaxage );
|
|
|
|
|
|
|
|
|
|
$op->setCdnMaxage( 120 );
|
|
|
|
|
$this->assertSame( 120, $wrapper->mCdnMaxage );
|
|
|
|
|
|
|
|
|
|
$op->setCdnMaxage( 60 );
|
|
|
|
|
$this->assertSame( 60, $wrapper->mCdnMaxage );
|
|
|
|
|
|
|
|
|
|
$op->setCdnMaxage( 180 );
|
|
|
|
|
$this->assertSame( 180, $wrapper->mCdnMaxage );
|
|
|
|
|
|
|
|
|
|
$op->lowerCdnMaxage( 240 );
|
|
|
|
|
$this->assertSame( 180, $wrapper->mCdnMaxage );
|
|
|
|
|
|
|
|
|
|
$op->setCdnMaxage( 300 );
|
|
|
|
|
$this->assertSame( 240, $wrapper->mCdnMaxage );
|
|
|
|
|
|
|
|
|
|
$op->lowerCdnMaxage( 120 );
|
|
|
|
|
$this->assertSame( 120, $wrapper->mCdnMaxage );
|
|
|
|
|
|
|
|
|
|
$op->setCdnMaxage( 180 );
|
|
|
|
|
$this->assertSame( 120, $wrapper->mCdnMaxage );
|
|
|
|
|
|
|
|
|
|
$op->setCdnMaxage( 60 );
|
|
|
|
|
$this->assertSame( 60, $wrapper->mCdnMaxage );
|
|
|
|
|
|
|
|
|
|
$op->setCdnMaxage( 240 );
|
|
|
|
|
$this->assertSame( 120, $wrapper->mCdnMaxage );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @var int Faked time to set for tests that need it */
|
|
|
|
|
private static $fakeTime;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideAdaptCdnTTL
|
|
|
|
|
* @param array $args To pass to adaptCdnTTL()
|
|
|
|
|
* @param int $expected Expected new value of mCdnMaxageLimit
|
|
|
|
|
* @param array $options Associative array:
|
|
|
|
|
* initialMaxage => Maxage to set before calling adaptCdnTTL() (default 86400)
|
|
|
|
|
*/
|
|
|
|
|
public function testAdaptCdnTTL( array $args, $expected, array $options = [] ) {
|
2022-03-01 17:09:17 +00:00
|
|
|
MWTimestamp::setFakeTime( self::$fakeTime );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
2022-03-01 17:09:17 +00:00
|
|
|
$op = $this->newInstance();
|
|
|
|
|
// Set a high maxage so that it will get reduced by adaptCdnTTL(). The default maxage
|
|
|
|
|
// is 0, so adaptCdnTTL() won't mutate the object at all.
|
|
|
|
|
$initial = $options['initialMaxage'] ?? 86400;
|
|
|
|
|
$op->setCdnMaxage( $initial );
|
|
|
|
|
$op->adaptCdnTTL( ...$args );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
$wrapper = TestingAccessWrapper::newFromObject( $op );
|
|
|
|
|
|
|
|
|
|
// Special rules for false/null
|
|
|
|
|
if ( $args[0] === null || $args[0] === false ) {
|
|
|
|
|
$this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
|
|
|
|
|
$op->setCdnMaxage( $expected + 1 );
|
|
|
|
|
$this->assertSame( $expected + 1, $wrapper->mCdnMaxage, 'member value after new set' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $expected, $wrapper->mCdnMaxageLimit, 'limit value' );
|
|
|
|
|
|
|
|
|
|
if ( $initial >= $expected ) {
|
|
|
|
|
$this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value' );
|
|
|
|
|
} else {
|
|
|
|
|
$this->assertSame( $initial, $wrapper->mCdnMaxage, 'member value' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$op->setCdnMaxage( $expected + 1 );
|
|
|
|
|
$this->assertSame( $expected, $wrapper->mCdnMaxage, 'member value after new set' );
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideAdaptCdnTTL() {
|
2017-11-01 20:55:24 +00:00
|
|
|
global $wgCdnMaxAge;
|
2018-08-02 18:42:17 +00:00
|
|
|
$now = time();
|
|
|
|
|
self::$fakeTime = $now;
|
|
|
|
|
return [
|
|
|
|
|
'Five minutes ago' => [ [ $now - 300 ], 270 ],
|
2023-08-06 21:41:29 +00:00
|
|
|
'Now' => [ [ +0 ], ExpirationAwareness::TTL_MINUTE ],
|
|
|
|
|
'Five minutes from now' => [ [ $now + 300 ], ExpirationAwareness::TTL_MINUTE ],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Five minutes ago, initial maxage four minutes' =>
|
|
|
|
|
[ [ $now - 300 ], 270, [ 'initialMaxage' => 240 ] ],
|
2017-11-01 20:55:24 +00:00
|
|
|
'A very long time ago' => [ [ $now - 1000000000 ], $wgCdnMaxAge ],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Initial maxage zero' => [ [ $now - 300 ], 270, [ 'initialMaxage' => 0 ] ],
|
|
|
|
|
|
2023-08-06 21:41:29 +00:00
|
|
|
'false' => [ [ false ], ExpirationAwareness::TTL_MINUTE ],
|
|
|
|
|
'null' => [ [ null ], ExpirationAwareness::TTL_MINUTE ],
|
|
|
|
|
"'0'" => [ [ '0' ], ExpirationAwareness::TTL_MINUTE ],
|
|
|
|
|
'Empty string' => [ [ '' ], ExpirationAwareness::TTL_MINUTE ],
|
2018-08-02 18:42:17 +00:00
|
|
|
// @todo These give incorrect results due to timezones, how to test?
|
2023-08-06 21:41:29 +00:00
|
|
|
//"'now'" => [ [ 'now' ], ExpirationAwareness::TTL_MINUTE ],
|
|
|
|
|
//"'parse error'" => [ [ 'parse error' ], ExpirationAwareness::TTL_MINUTE ],
|
2018-08-02 18:42:17 +00:00
|
|
|
|
2023-08-06 21:41:29 +00:00
|
|
|
'Now, minTTL 0' => [ [ $now, 0 ], ExpirationAwareness::TTL_MINUTE ],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Now, minTTL 0.000001' => [ [ $now, 0.000001 ], 0 ],
|
|
|
|
|
'A very long time ago, maxTTL even longer' =>
|
|
|
|
|
[ [ $now - 1000000000, 0, 1000000001 ], 900000000 ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testClientCache() {
|
|
|
|
|
$op = $this->newInstance();
|
2022-02-04 19:12:11 +00:00
|
|
|
$op->considerCacheSettingsFinal();
|
|
|
|
|
|
|
|
|
|
// Test initial value
|
|
|
|
|
$this->assertSame( true, $op->couldBePublicCached() );
|
|
|
|
|
|
|
|
|
|
// Test setting to false
|
|
|
|
|
$op->disableClientCache();
|
|
|
|
|
$this->assertSame( false, $op->couldBePublicCached() );
|
|
|
|
|
|
2022-09-02 16:13:30 +00:00
|
|
|
// Test setting to true
|
|
|
|
|
$op->enableClientCache();
|
|
|
|
|
$this->assertSame( true, $op->couldBePublicCached() );
|
|
|
|
|
|
|
|
|
|
// set back to false
|
|
|
|
|
$op->disableClientCache();
|
|
|
|
|
|
2022-02-04 19:12:11 +00:00
|
|
|
// Test that a cacheable ParserOutput doesn't set to true
|
|
|
|
|
$pOutCacheable = $this->createParserOutputStub( 'isCacheable', true );
|
|
|
|
|
$op->addParserOutputMetadata( $pOutCacheable );
|
|
|
|
|
$this->assertSame( false, $op->couldBePublicCached() );
|
|
|
|
|
|
|
|
|
|
// Reset to true
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->considerCacheSettingsFinal();
|
|
|
|
|
$this->assertSame( true, $op->couldBePublicCached() );
|
|
|
|
|
|
|
|
|
|
// Test that an uncacheable ParserOutput does set to false
|
|
|
|
|
$pOutUncacheable = $this->createParserOutputStub( 'isCacheable', false );
|
|
|
|
|
$op->addParserOutput( $pOutUncacheable );
|
|
|
|
|
$this->assertSame( false, $op->couldBePublicCached() );
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
public function testGetCacheVaryCookies() {
|
|
|
|
|
global $wgCookiePrefix, $wgDBname;
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$prefix = $wgCookiePrefix !== false ? $wgCookiePrefix : $wgDBname;
|
|
|
|
|
$expectedCookies = [
|
|
|
|
|
"{$prefix}Token",
|
|
|
|
|
"{$prefix}LoggedOut",
|
|
|
|
|
"{$prefix}_session",
|
|
|
|
|
'forceHTTPS',
|
|
|
|
|
'cookie1',
|
|
|
|
|
'cookie2',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// We have to reset the cookies because getCacheVaryCookies may have already been called
|
|
|
|
|
TestingAccessWrapper::newFromClass( OutputPage::class )->cacheVaryCookies = null;
|
|
|
|
|
|
2022-07-15 00:07:38 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::CacheVaryCookies, [ 'cookie1' ] );
|
2018-08-02 18:42:17 +00:00
|
|
|
$this->setTemporaryHook( 'GetCacheVaryCookies',
|
|
|
|
|
function ( $innerOP, &$cookies ) use ( $op, $expectedCookies ) {
|
|
|
|
|
$this->assertSame( $op, $innerOP );
|
|
|
|
|
$cookies[] = 'cookie2';
|
|
|
|
|
$this->assertSame( $expectedCookies, $cookies );
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $expectedCookies, $op->getCacheVaryCookies() );
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testHaveCacheVaryCookies() {
|
|
|
|
|
$request = new FauxRequest();
|
2018-08-02 18:42:17 +00:00
|
|
|
$op = $this->newInstance( [], $request );
|
2018-07-23 18:26:32 +00:00
|
|
|
|
|
|
|
|
// No cookies are set.
|
|
|
|
|
$this->assertFalse( $op->haveCacheVaryCookies() );
|
|
|
|
|
|
|
|
|
|
// 'Token' is present but empty, so it shouldn't count.
|
|
|
|
|
$request->setCookie( 'Token', '' );
|
|
|
|
|
$this->assertFalse( $op->haveCacheVaryCookies() );
|
|
|
|
|
|
|
|
|
|
// 'Token' present and nonempty.
|
|
|
|
|
$request->setCookie( 'Token', '123' );
|
|
|
|
|
$this->assertTrue( $op->haveCacheVaryCookies() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideVaryHeaders
|
|
|
|
|
*
|
2018-08-02 18:42:17 +00:00
|
|
|
*
|
|
|
|
|
* @param array[] $calls For each array, call addVaryHeader() with those arguments
|
|
|
|
|
* @param string[] $cookies Array of cookie names to vary on
|
|
|
|
|
* @param string $vary Text of expected Vary header (including the 'Vary: ')
|
2018-07-23 18:26:32 +00:00
|
|
|
*/
|
2019-06-19 18:22:42 +00:00
|
|
|
public function testVaryHeaders( array $calls, array $cookies, $vary ) {
|
2018-08-02 18:42:17 +00:00
|
|
|
// Get rid of default Vary fields
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = $this->getMockBuilder( OutputPage::class )
|
|
|
|
|
->setConstructorArgs( [ new RequestContext() ] )
|
2021-03-20 15:18:58 +00:00
|
|
|
->onlyMethods( [ 'getCacheVaryCookies' ] )
|
2018-07-23 18:26:32 +00:00
|
|
|
->getMock();
|
2021-04-22 08:28:11 +00:00
|
|
|
$op->method( 'getCacheVaryCookies' )
|
|
|
|
|
->willReturn( $cookies );
|
2018-07-23 18:26:32 +00:00
|
|
|
TestingAccessWrapper::newFromObject( $op )->mVaryHeader = [];
|
|
|
|
|
|
|
|
|
|
foreach ( $calls as $call ) {
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addVaryHeader( ...$call );
|
2018-07-23 18:26:32 +00:00
|
|
|
}
|
|
|
|
|
$this->assertEquals( $vary, $op->getVaryHeader(), 'Vary:' );
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideVaryHeaders() {
|
2017-01-18 05:58:46 +00:00
|
|
|
return [
|
2018-08-02 18:42:17 +00:00
|
|
|
'No header' => [
|
|
|
|
|
[],
|
|
|
|
|
[],
|
|
|
|
|
'Vary: ',
|
|
|
|
|
],
|
|
|
|
|
'Single header' => [
|
2018-07-23 18:26:32 +00:00
|
|
|
[
|
|
|
|
|
[ 'Cookie' ],
|
|
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
[],
|
2018-07-23 18:26:32 +00:00
|
|
|
'Vary: Cookie',
|
2017-02-10 00:03:06 +00:00
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Non-unique headers' => [
|
2018-07-23 18:26:32 +00:00
|
|
|
[
|
|
|
|
|
[ 'Cookie' ],
|
|
|
|
|
[ 'Accept-Language' ],
|
|
|
|
|
[ 'Cookie' ],
|
|
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
[],
|
2018-07-23 18:26:32 +00:00
|
|
|
'Vary: Cookie, Accept-Language',
|
2017-02-10 00:03:06 +00:00
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Two headers with single options' => [
|
2019-06-19 18:22:42 +00:00
|
|
|
// Options are deprecated since 1.34
|
2018-07-23 18:26:32 +00:00
|
|
|
[
|
|
|
|
|
[ 'Cookie', [ 'param=phpsessid' ] ],
|
|
|
|
|
[ 'Accept-Language', [ 'substr=en' ] ],
|
|
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
[],
|
2018-07-23 18:26:32 +00:00
|
|
|
'Vary: Cookie, Accept-Language',
|
2017-02-10 00:03:06 +00:00
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'One header with multiple options' => [
|
2019-06-19 18:22:42 +00:00
|
|
|
// Options are deprecated since 1.34
|
2018-07-23 18:26:32 +00:00
|
|
|
[
|
|
|
|
|
[ 'Cookie', [ 'param=phpsessid', 'param=userId' ] ],
|
|
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
[],
|
2018-07-23 18:26:32 +00:00
|
|
|
'Vary: Cookie',
|
2017-02-10 00:03:06 +00:00
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Duplicate option' => [
|
2019-06-19 18:22:42 +00:00
|
|
|
// Options are deprecated since 1.34
|
2018-07-23 18:26:32 +00:00
|
|
|
[
|
|
|
|
|
[ 'Cookie', [ 'param=phpsessid' ] ],
|
|
|
|
|
[ 'Cookie', [ 'param=phpsessid' ] ],
|
|
|
|
|
[ 'Accept-Language', [ 'substr=en', 'substr=en' ] ],
|
|
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
[],
|
2018-07-23 18:26:32 +00:00
|
|
|
'Vary: Cookie, Accept-Language',
|
|
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Same header, different options' => [
|
2019-06-19 18:22:42 +00:00
|
|
|
// Options are deprecated since 1.34
|
2018-07-23 18:26:32 +00:00
|
|
|
[
|
|
|
|
|
[ 'Cookie', [ 'param=phpsessid' ] ],
|
|
|
|
|
[ 'Cookie', [ 'param=userId' ] ],
|
|
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
[],
|
2018-07-23 18:26:32 +00:00
|
|
|
'Vary: Cookie',
|
2017-02-10 00:03:06 +00:00
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'No header, vary cookies' => [
|
|
|
|
|
[],
|
|
|
|
|
[ 'cookie1', 'cookie2' ],
|
|
|
|
|
'Vary: Cookie',
|
|
|
|
|
],
|
|
|
|
|
'Cookie header with option plus vary cookies' => [
|
2019-06-19 18:22:42 +00:00
|
|
|
// Options are deprecated since 1.34
|
2018-08-02 18:42:17 +00:00
|
|
|
[
|
|
|
|
|
[ 'Cookie', [ 'param=cookie1' ] ],
|
|
|
|
|
],
|
|
|
|
|
[ 'cookie2', 'cookie3' ],
|
|
|
|
|
'Vary: Cookie',
|
|
|
|
|
],
|
|
|
|
|
'Non-cookie header plus vary cookies' => [
|
|
|
|
|
[
|
|
|
|
|
[ 'Accept-Language' ],
|
|
|
|
|
],
|
|
|
|
|
[ 'cookie' ],
|
|
|
|
|
'Vary: Accept-Language, Cookie',
|
|
|
|
|
],
|
|
|
|
|
'Cookie and non-cookie headers plus vary cookies' => [
|
2019-06-19 18:22:42 +00:00
|
|
|
// Options are deprecated since 1.34
|
2018-08-02 18:42:17 +00:00
|
|
|
[
|
|
|
|
|
[ 'Cookie', [ 'param=cookie1' ] ],
|
|
|
|
|
[ 'Accept-Language' ],
|
|
|
|
|
],
|
|
|
|
|
[ 'cookie2' ],
|
|
|
|
|
'Vary: Cookie, Accept-Language',
|
|
|
|
|
],
|
2017-01-18 05:58:46 +00:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
public function testVaryHeaderDefault() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( 'Vary: Accept-Encoding, Cookie', $op->getVaryHeader() );
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-18 05:58:46 +00:00
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* @dataProvider provideLinkHeaders
|
2017-01-18 05:58:46 +00:00
|
|
|
*/
|
2018-08-02 18:42:17 +00:00
|
|
|
public function testLinkHeaders( array $headers, $result ) {
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = $this->newInstance();
|
2017-01-18 05:58:46 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
foreach ( $headers as $header ) {
|
|
|
|
|
$op->addLinkHeader( $header );
|
|
|
|
|
}
|
2017-01-18 05:58:46 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$this->assertEquals( $result, $op->getLinkHeader() );
|
2017-01-18 05:58:46 +00:00
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideLinkHeaders() {
|
2016-03-19 01:05:19 +00:00
|
|
|
return [
|
|
|
|
|
[
|
2018-07-23 18:26:32 +00:00
|
|
|
[],
|
|
|
|
|
false
|
Initial support for Content Security Policy, disabled by default
The primary goal here is a defense in depth measure to
stop an attacker who found a bug in the parser allowing
them to insert malicious attributes.
This wouldn't stop someone who could insert a full
script tag (since at current it can't distinguish between
malicious and legit user js). It also would not prevent
DOM-based or reflected XSS for anons, as the nonce value
is guessable for anons when receiving a response cached
by varnish. However, the limited protection of just stopping
stored XSS where the attacker only has control of attributes,
is still a big win in my opinion. (But it wouldn't prevent
someone who has that type of xss from abusing things like
data-ooui attribute).
This will likely break many gadgets. Its expected that any
sort of rollout on Wikimedia will be done very slowly, with
lots of testing and the report-only option to begin with.
This is behind feature flags that are off by default, so
merging this patch should not cause any change in default
behaviour.
This may break some extensions (The most obvious one
is charinsert (See fe648d41005), but will probably need
some testing in report-only mode to see if anything else breaks)
This uses the unsafe-eval option of CSP, in order to
support RL's local storage thingy. For better security,
we may want to remove some of the sillier uses of eval
(e.g. jquery.ui.datepicker.js).
For more info, see spec: https://www.w3.org/TR/CSP2/
Additionally see:
https://www.mediawiki.org/wiki/Requests_for_comment/Content-Security-Policy
Bug: T135963
Change-Id: I80f6f469ba4c0b608385483457df96ccb7429ae5
2016-02-29 04:13:10 +00:00
|
|
|
],
|
|
|
|
|
[
|
2018-07-23 18:26:32 +00:00
|
|
|
[ '<https://foo/bar.jpg>;rel=preload;as=image' ],
|
|
|
|
|
'Link: <https://foo/bar.jpg>;rel=preload;as=image',
|
Initial support for Content Security Policy, disabled by default
The primary goal here is a defense in depth measure to
stop an attacker who found a bug in the parser allowing
them to insert malicious attributes.
This wouldn't stop someone who could insert a full
script tag (since at current it can't distinguish between
malicious and legit user js). It also would not prevent
DOM-based or reflected XSS for anons, as the nonce value
is guessable for anons when receiving a response cached
by varnish. However, the limited protection of just stopping
stored XSS where the attacker only has control of attributes,
is still a big win in my opinion. (But it wouldn't prevent
someone who has that type of xss from abusing things like
data-ooui attribute).
This will likely break many gadgets. Its expected that any
sort of rollout on Wikimedia will be done very slowly, with
lots of testing and the report-only option to begin with.
This is behind feature flags that are off by default, so
merging this patch should not cause any change in default
behaviour.
This may break some extensions (The most obvious one
is charinsert (See fe648d41005), but will probably need
some testing in report-only mode to see if anything else breaks)
This uses the unsafe-eval option of CSP, in order to
support RL's local storage thingy. For better security,
we may want to remove some of the sillier uses of eval
(e.g. jquery.ui.datepicker.js).
For more info, see spec: https://www.w3.org/TR/CSP2/
Additionally see:
https://www.mediawiki.org/wiki/Requests_for_comment/Content-Security-Policy
Bug: T135963
Change-Id: I80f6f469ba4c0b608385483457df96ccb7429ae5
2016-02-29 04:13:10 +00:00
|
|
|
],
|
|
|
|
|
[
|
2018-08-02 18:42:17 +00:00
|
|
|
[
|
|
|
|
|
'<https://foo/bar.jpg>;rel=preload;as=image',
|
|
|
|
|
'<https://foo/baz.jpg>;rel=preload;as=image'
|
|
|
|
|
],
|
|
|
|
|
'Link: <https://foo/bar.jpg>;rel=preload;as=image,<https://foo/baz.jpg>;' .
|
|
|
|
|
'rel=preload;as=image',
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideAddAcceptLanguage
|
|
|
|
|
*/
|
|
|
|
|
public function testAddAcceptLanguage(
|
2019-06-19 18:22:42 +00:00
|
|
|
$code, array $variants, $expected, array $options = []
|
2018-08-02 18:42:17 +00:00
|
|
|
) {
|
|
|
|
|
$req = new FauxRequest( in_array( 'varianturl', $options ) ? [ 'variant' => 'x' ] : [] );
|
|
|
|
|
$op = $this->newInstance( [], $req, in_array( 'notitle', $options ) ? 'notitle' : null );
|
|
|
|
|
|
|
|
|
|
if ( !in_array( 'notitle', $options ) ) {
|
2019-10-05 22:14:35 +00:00
|
|
|
$mockLang = $this->createMock( Language::class );
|
2020-11-03 17:18:15 +00:00
|
|
|
$mockLang->method( 'getCode' )->willReturn( $code );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
2020-11-03 17:18:15 +00:00
|
|
|
$mockLanguageConverter = $this
|
|
|
|
|
->createMock( ILanguageConverter::class );
|
2018-08-02 18:42:17 +00:00
|
|
|
if ( in_array( 'varianturl', $options ) ) {
|
2020-11-03 17:18:15 +00:00
|
|
|
$mockLanguageConverter->expects( $this->never() )->method( $this->anything() );
|
2018-08-02 18:42:17 +00:00
|
|
|
} else {
|
2020-11-03 17:18:15 +00:00
|
|
|
$mockLanguageConverter->method( 'hasVariants' )->willReturn( count( $variants ) > 1 );
|
|
|
|
|
$mockLanguageConverter->method( 'getVariants' )->willReturn( $variants );
|
2018-08-02 18:42:17 +00:00
|
|
|
}
|
|
|
|
|
|
2020-11-03 17:18:15 +00:00
|
|
|
$languageConverterFactory = $this
|
|
|
|
|
->createMock( LanguageConverterFactory::class );
|
|
|
|
|
$languageConverterFactory
|
|
|
|
|
->method( 'getLanguageConverter' )
|
|
|
|
|
->willReturn( $mockLanguageConverter );
|
|
|
|
|
$this->setService(
|
|
|
|
|
'LanguageConverterFactory',
|
|
|
|
|
$languageConverterFactory
|
|
|
|
|
);
|
|
|
|
|
|
2019-10-05 22:14:35 +00:00
|
|
|
$mockTitle = $this->createMock( Title::class );
|
2018-08-02 18:42:17 +00:00
|
|
|
$mockTitle->method( 'getPageLanguage' )->willReturn( $mockLang );
|
|
|
|
|
|
|
|
|
|
$op->setTitle( $mockTitle );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This will run addAcceptLanguage()
|
|
|
|
|
$op->sendCacheControl();
|
2019-06-19 18:22:42 +00:00
|
|
|
$this->assertSame( "Vary: $expected", $op->getVaryHeader() );
|
2018-08-02 18:42:17 +00:00
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideAddAcceptLanguage() {
|
2018-08-02 18:42:17 +00:00
|
|
|
return [
|
2019-06-19 18:22:42 +00:00
|
|
|
'No variants' => [
|
|
|
|
|
'en',
|
|
|
|
|
[ 'en' ],
|
|
|
|
|
'Accept-Encoding, Cookie',
|
|
|
|
|
],
|
|
|
|
|
'One simple variant' => [
|
|
|
|
|
'en',
|
|
|
|
|
[ 'en', 'en-x-piglatin' ],
|
|
|
|
|
'Accept-Encoding, Cookie, Accept-Language',
|
|
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Multiple variants with BCP47 alternatives' => [
|
|
|
|
|
'zh',
|
|
|
|
|
[ 'zh', 'zh-hans', 'zh-cn', 'zh-tw' ],
|
2019-06-19 18:22:42 +00:00
|
|
|
'Accept-Encoding, Cookie, Accept-Language',
|
|
|
|
|
],
|
|
|
|
|
'No title' => [
|
|
|
|
|
'en',
|
|
|
|
|
[ 'en', 'en-x-piglatin' ],
|
|
|
|
|
'Accept-Encoding, Cookie',
|
|
|
|
|
[ 'notitle' ]
|
|
|
|
|
],
|
|
|
|
|
'Variant in URL' => [
|
|
|
|
|
'en',
|
|
|
|
|
[ 'en', 'en-x-piglatin' ],
|
|
|
|
|
'Accept-Encoding, Cookie',
|
|
|
|
|
[ 'varianturl' ]
|
Initial support for Content Security Policy, disabled by default
The primary goal here is a defense in depth measure to
stop an attacker who found a bug in the parser allowing
them to insert malicious attributes.
This wouldn't stop someone who could insert a full
script tag (since at current it can't distinguish between
malicious and legit user js). It also would not prevent
DOM-based or reflected XSS for anons, as the nonce value
is guessable for anons when receiving a response cached
by varnish. However, the limited protection of just stopping
stored XSS where the attacker only has control of attributes,
is still a big win in my opinion. (But it wouldn't prevent
someone who has that type of xss from abusing things like
data-ooui attribute).
This will likely break many gadgets. Its expected that any
sort of rollout on Wikimedia will be done very slowly, with
lots of testing and the report-only option to begin with.
This is behind feature flags that are off by default, so
merging this patch should not cause any change in default
behaviour.
This may break some extensions (The most obvious one
is charinsert (See fe648d41005), but will probably need
some testing in report-only mode to see if anything else breaks)
This uses the unsafe-eval option of CSP, in order to
support RL's local storage thingy. For better security,
we may want to remove some of the sillier uses of eval
(e.g. jquery.ui.datepicker.js).
For more info, see spec: https://www.w3.org/TR/CSP2/
Additionally see:
https://www.mediawiki.org/wiki/Requests_for_comment/Content-Security-Policy
Bug: T135963
Change-Id: I80f6f469ba4c0b608385483457df96ccb7429ae5
2016-02-29 04:13:10 +00:00
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testClickjacking() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
|
2021-09-29 20:58:59 +00:00
|
|
|
$op->setPreventClickjacking( false );
|
2018-08-02 18:42:17 +00:00
|
|
|
$this->assertFalse( $op->getPreventClickjacking() );
|
|
|
|
|
|
2021-09-29 20:58:59 +00:00
|
|
|
$op->setPreventClickjacking( true );
|
2018-08-02 18:42:17 +00:00
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
|
2021-09-29 20:58:59 +00:00
|
|
|
$op->setPreventClickjacking( false );
|
2018-08-02 18:42:17 +00:00
|
|
|
$this->assertFalse( $op->getPreventClickjacking() );
|
|
|
|
|
|
2021-09-29 20:58:59 +00:00
|
|
|
$pOut1 = $this->createParserOutputStub( 'getPreventClickjacking', true );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
// The ParserOutput can't allow, only prevent
|
2021-09-29 20:58:59 +00:00
|
|
|
$pOut2 = $this->createParserOutputStub( 'getPreventClickjacking', false );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutputMetadata( $pOut2 );
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
// Reset to test with addParserOutput()
|
2021-09-29 20:58:59 +00:00
|
|
|
$op->setPreventClickjacking( false );
|
2018-08-02 18:42:17 +00:00
|
|
|
$this->assertFalse( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutput( $pOut1 );
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutput( $pOut2 );
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideGetFrameOptions
|
|
|
|
|
*/
|
|
|
|
|
public function testGetFrameOptions(
|
|
|
|
|
$breakFrames, $preventClickjacking, $editPageFrameOptions, $expected
|
|
|
|
|
) {
|
|
|
|
|
$op = $this->newInstance( [
|
|
|
|
|
'BreakFrames' => $breakFrames,
|
|
|
|
|
'EditPageFrameOptions' => $editPageFrameOptions,
|
|
|
|
|
] );
|
2021-09-29 20:58:59 +00:00
|
|
|
$op->setPreventClickjacking( $preventClickjacking );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
$this->assertSame( $expected, $op->getFrameOptions() );
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideGetFrameOptions() {
|
2018-08-02 18:42:17 +00:00
|
|
|
return [
|
|
|
|
|
'BreakFrames true' => [ true, false, false, 'DENY' ],
|
|
|
|
|
'Allow clickjacking locally' => [ false, false, 'DENY', false ],
|
|
|
|
|
'Allow clickjacking globally' => [ false, true, false, false ],
|
|
|
|
|
'DENY globally' => [ false, true, 'DENY', 'DENY' ],
|
|
|
|
|
'SAMEORIGIN' => [ false, true, 'SAMEORIGIN', 'SAMEORIGIN' ],
|
|
|
|
|
'BreakFrames with SAMEORIGIN' => [ true, true, 'SAMEORIGIN', 'DENY' ],
|
2016-03-19 01:05:19 +00:00
|
|
|
];
|
2014-06-28 20:40:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2022-05-06 09:09:56 +00:00
|
|
|
* See ClientHtmlTest for full coverage.
|
resourceloader: Move queue formatting out of OutputPage
HTML formatting of the queue was distributed over several OutputPage methods.
Each method demanding a snippet of HTML by calling makeResourceLoaderLink()
with a limited amount of information. As such, makeResourceLoaderLink() was
unable to provide the client with the proper state information.
Centralising it also allows it to better reduce duplication in HTML output
and maintain a more accurate state.
Problems fixed by centralising:
1. The 'user' module is special (due to per-user 'version' and 'user' params).
It is manually requested via script-src. To avoid a separate (and wrong)
request from something that requires it, we set state=loading directly.
However, because the module is in the bottom, the old HTML formatter could
only put state=loading in the bottom also. This sometimes caused a wrong
request to be fired for modules=user if something in the top queue
triggered a requirement for it.
2. Since a464d1d4 (T87871) we track states of page-style modules, with purpose
of allowing dependencies on style modules without risking duplicate loading
on pages where the styles are loaded already. This didn't work, because the
state information about page-style modules is output near the stylesheet,
which is after the script tag with mw.loader.load(). That runs first, and
mw.loader would still make a duplicate request before it learns the state.
Changes:
* Document reasons for style/script tag order in getHeadHtml (per 09537e83).
* Pass $type from getModuleStyles() to getAllowedModules(). This wasn't needed
before since a duplicate check in makeResourceLoaderLink() verified the
origin a second time.
* Declare explicit position 'top' on 'user.options' and 'user.tokens' module.
Previously, OutputPage hardcoded them in the top. The new formatter doesn't.
* Remove getHeadScripts().
* Remove getInlineHeadScripts().
* Remove getExternalHeadScripts().
* Remove buildCssLinks().
* Remove getScriptsForBottomQueue().
* Change where Skin::setupSkinUserCss() is called. This methods lets the skin
add modules to the queue. Previously it was called from buildCssLinks(),
via headElement(), via prepareQuickTemplate(), via OutputPage::output().
It's now in OutputPage::output() directly (slightly earlier). This is needed
because prepareQuickTemplate() calls bottomScripts() before headElement().
And bottomScript() would lazy-initialise the queue and lock it before
setupSkinUserCss() is called from headElement().
This makes execution order more predictable instead of being dependent on
the arbitrary order of data extraction in prepareQuickTemplate (which varies
from one skin to another).
* Compute isUserModulePreview() and isKnownEmpty() for the 'user' module early
on so. This avoids wrongful loading and fixes problem 1.
Effective changes in output:
* mw.loader.state() is now before mw.loader.load(). This fixes problem 2.
* mw.loader.state() now sets 'user.options' and 'user.tokens' to "loading".
* mw.loader.state() now sets 'user' (as "loading" or "ready"). Fixes problem 1.
* The <script async src> tag for 'startup' changed position (slightly).
Previously it was after all inline scripts and stylesheets. It's still after
all inline scripts and after most stylesheets, but before any user styles.
Since the queue is now formatted outside OutputPage, it can't inject the
meta-ResourceLoaderDynamicStyles tag and user-stylesheet hack in the middle
of existing output. This shouldn't have any noticable impact.
Bug: T87871
Change-Id: I605b8cd1e1fc009b4662a0edbc54d09dd65ee1df
2016-07-15 14:13:09 +00:00
|
|
|
*
|
2014-06-28 20:40:22 +00:00
|
|
|
* @dataProvider provideMakeResourceLoaderLink
|
|
|
|
|
*/
|
2014-07-19 21:12:10 +00:00
|
|
|
public function testMakeResourceLoaderLink( $args, $expectedHtml ) {
|
2022-07-15 00:07:38 +00:00
|
|
|
$this->overrideConfigValues( [
|
|
|
|
|
MainConfigNames::ResourceLoaderDebug => false,
|
|
|
|
|
MainConfigNames::LoadScript => 'http://127.0.0.1:8080/w/load.php',
|
|
|
|
|
MainConfigNames::CSPReportOnlyHeader => true,
|
2016-02-17 09:09:32 +00:00
|
|
|
] );
|
2018-01-13 00:02:09 +00:00
|
|
|
$class = new ReflectionClass( OutputPage::class );
|
2014-06-28 20:40:22 +00:00
|
|
|
$method = $class->getMethod( 'makeResourceLoaderLink' );
|
|
|
|
|
$method->setAccessible( true );
|
|
|
|
|
$ctx = new RequestContext();
|
2022-01-12 20:13:39 +00:00
|
|
|
$skinFactory = $this->getServiceContainer()->getSkinFactory();
|
2019-06-03 00:55:00 +00:00
|
|
|
$ctx->setSkin( $skinFactory->makeSkin( 'fallback' ) );
|
2014-07-11 14:35:14 +00:00
|
|
|
$ctx->setLanguage( 'en' );
|
2014-06-28 20:40:22 +00:00
|
|
|
$out = new OutputPage( $ctx );
|
2019-10-28 05:01:17 +00:00
|
|
|
$reflectCSP = new ReflectionClass( ContentSecurityPolicy::class );
|
2014-06-28 20:40:22 +00:00
|
|
|
$rl = $out->getResourceLoader();
|
2022-05-06 09:09:56 +00:00
|
|
|
$rl->setMessageBlobStore( $this->createMock( RL\MessageBlobStore::class ) );
|
2019-06-29 04:50:31 +00:00
|
|
|
$rl->setDependencyStore( $this->createMock( KeyValueDependencyStore::class ) );
|
2016-02-17 09:09:32 +00:00
|
|
|
$rl->register( [
|
2019-07-12 17:30:06 +00:00
|
|
|
'test.foo' => [
|
|
|
|
|
'class' => ResourceLoaderTestModule::class,
|
2014-06-28 20:40:22 +00:00
|
|
|
'script' => 'mw.test.foo( { a: true } );',
|
|
|
|
|
'styles' => '.mw-test-foo { content: "style"; }',
|
2019-07-12 17:30:06 +00:00
|
|
|
],
|
|
|
|
|
'test.bar' => [
|
|
|
|
|
'class' => ResourceLoaderTestModule::class,
|
2014-06-28 20:40:22 +00:00
|
|
|
'script' => 'mw.test.bar( { a: true } );',
|
|
|
|
|
'styles' => '.mw-test-bar { content: "style"; }',
|
2019-07-12 17:30:06 +00:00
|
|
|
],
|
|
|
|
|
'test.baz' => [
|
|
|
|
|
'class' => ResourceLoaderTestModule::class,
|
2014-06-28 20:40:22 +00:00
|
|
|
'script' => 'mw.test.baz( { a: true } );',
|
|
|
|
|
'styles' => '.mw-test-baz { content: "style"; }',
|
2019-07-12 17:30:06 +00:00
|
|
|
],
|
|
|
|
|
'test.quux' => [
|
|
|
|
|
'class' => ResourceLoaderTestModule::class,
|
2014-06-28 20:40:22 +00:00
|
|
|
'script' => 'mw.test.baz( { token: 123 } );',
|
|
|
|
|
'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
|
|
|
|
|
'group' => 'private',
|
2019-07-12 17:30:06 +00:00
|
|
|
],
|
|
|
|
|
'test.noscript' => [
|
|
|
|
|
'class' => ResourceLoaderTestModule::class,
|
Initial support for Content Security Policy, disabled by default
The primary goal here is a defense in depth measure to
stop an attacker who found a bug in the parser allowing
them to insert malicious attributes.
This wouldn't stop someone who could insert a full
script tag (since at current it can't distinguish between
malicious and legit user js). It also would not prevent
DOM-based or reflected XSS for anons, as the nonce value
is guessable for anons when receiving a response cached
by varnish. However, the limited protection of just stopping
stored XSS where the attacker only has control of attributes,
is still a big win in my opinion. (But it wouldn't prevent
someone who has that type of xss from abusing things like
data-ooui attribute).
This will likely break many gadgets. Its expected that any
sort of rollout on Wikimedia will be done very slowly, with
lots of testing and the report-only option to begin with.
This is behind feature flags that are off by default, so
merging this patch should not cause any change in default
behaviour.
This may break some extensions (The most obvious one
is charinsert (See fe648d41005), but will probably need
some testing in report-only mode to see if anything else breaks)
This uses the unsafe-eval option of CSP, in order to
support RL's local storage thingy. For better security,
we may want to remove some of the sillier uses of eval
(e.g. jquery.ui.datepicker.js).
For more info, see spec: https://www.w3.org/TR/CSP2/
Additionally see:
https://www.mediawiki.org/wiki/Requests_for_comment/Content-Security-Policy
Bug: T135963
Change-Id: I80f6f469ba4c0b608385483457df96ccb7429ae5
2016-02-29 04:13:10 +00:00
|
|
|
'styles' => '.stuff { color: red; }',
|
|
|
|
|
'group' => 'noscript',
|
2019-07-12 17:30:06 +00:00
|
|
|
],
|
|
|
|
|
'test.group.foo' => [
|
|
|
|
|
'class' => ResourceLoaderTestModule::class,
|
Initial support for Content Security Policy, disabled by default
The primary goal here is a defense in depth measure to
stop an attacker who found a bug in the parser allowing
them to insert malicious attributes.
This wouldn't stop someone who could insert a full
script tag (since at current it can't distinguish between
malicious and legit user js). It also would not prevent
DOM-based or reflected XSS for anons, as the nonce value
is guessable for anons when receiving a response cached
by varnish. However, the limited protection of just stopping
stored XSS where the attacker only has control of attributes,
is still a big win in my opinion. (But it wouldn't prevent
someone who has that type of xss from abusing things like
data-ooui attribute).
This will likely break many gadgets. Its expected that any
sort of rollout on Wikimedia will be done very slowly, with
lots of testing and the report-only option to begin with.
This is behind feature flags that are off by default, so
merging this patch should not cause any change in default
behaviour.
This may break some extensions (The most obvious one
is charinsert (See fe648d41005), but will probably need
some testing in report-only mode to see if anything else breaks)
This uses the unsafe-eval option of CSP, in order to
support RL's local storage thingy. For better security,
we may want to remove some of the sillier uses of eval
(e.g. jquery.ui.datepicker.js).
For more info, see spec: https://www.w3.org/TR/CSP2/
Additionally see:
https://www.mediawiki.org/wiki/Requests_for_comment/Content-Security-Policy
Bug: T135963
Change-Id: I80f6f469ba4c0b608385483457df96ccb7429ae5
2016-02-29 04:13:10 +00:00
|
|
|
'script' => 'mw.doStuff( "foo" );',
|
|
|
|
|
'group' => 'foo',
|
2019-07-12 17:30:06 +00:00
|
|
|
],
|
|
|
|
|
'test.group.bar' => [
|
|
|
|
|
'class' => ResourceLoaderTestModule::class,
|
Initial support for Content Security Policy, disabled by default
The primary goal here is a defense in depth measure to
stop an attacker who found a bug in the parser allowing
them to insert malicious attributes.
This wouldn't stop someone who could insert a full
script tag (since at current it can't distinguish between
malicious and legit user js). It also would not prevent
DOM-based or reflected XSS for anons, as the nonce value
is guessable for anons when receiving a response cached
by varnish. However, the limited protection of just stopping
stored XSS where the attacker only has control of attributes,
is still a big win in my opinion. (But it wouldn't prevent
someone who has that type of xss from abusing things like
data-ooui attribute).
This will likely break many gadgets. Its expected that any
sort of rollout on Wikimedia will be done very slowly, with
lots of testing and the report-only option to begin with.
This is behind feature flags that are off by default, so
merging this patch should not cause any change in default
behaviour.
This may break some extensions (The most obvious one
is charinsert (See fe648d41005), but will probably need
some testing in report-only mode to see if anything else breaks)
This uses the unsafe-eval option of CSP, in order to
support RL's local storage thingy. For better security,
we may want to remove some of the sillier uses of eval
(e.g. jquery.ui.datepicker.js).
For more info, see spec: https://www.w3.org/TR/CSP2/
Additionally see:
https://www.mediawiki.org/wiki/Requests_for_comment/Content-Security-Policy
Bug: T135963
Change-Id: I80f6f469ba4c0b608385483457df96ccb7429ae5
2016-02-29 04:13:10 +00:00
|
|
|
'script' => 'mw.doStuff( "bar" );',
|
|
|
|
|
'group' => 'bar',
|
2019-07-12 17:30:06 +00:00
|
|
|
],
|
2016-02-17 09:09:32 +00:00
|
|
|
] );
|
2014-06-28 20:40:22 +00:00
|
|
|
$links = $method->invokeArgs( $out, $args );
|
resourceloader: Move queue formatting out of OutputPage
HTML formatting of the queue was distributed over several OutputPage methods.
Each method demanding a snippet of HTML by calling makeResourceLoaderLink()
with a limited amount of information. As such, makeResourceLoaderLink() was
unable to provide the client with the proper state information.
Centralising it also allows it to better reduce duplication in HTML output
and maintain a more accurate state.
Problems fixed by centralising:
1. The 'user' module is special (due to per-user 'version' and 'user' params).
It is manually requested via script-src. To avoid a separate (and wrong)
request from something that requires it, we set state=loading directly.
However, because the module is in the bottom, the old HTML formatter could
only put state=loading in the bottom also. This sometimes caused a wrong
request to be fired for modules=user if something in the top queue
triggered a requirement for it.
2. Since a464d1d4 (T87871) we track states of page-style modules, with purpose
of allowing dependencies on style modules without risking duplicate loading
on pages where the styles are loaded already. This didn't work, because the
state information about page-style modules is output near the stylesheet,
which is after the script tag with mw.loader.load(). That runs first, and
mw.loader would still make a duplicate request before it learns the state.
Changes:
* Document reasons for style/script tag order in getHeadHtml (per 09537e83).
* Pass $type from getModuleStyles() to getAllowedModules(). This wasn't needed
before since a duplicate check in makeResourceLoaderLink() verified the
origin a second time.
* Declare explicit position 'top' on 'user.options' and 'user.tokens' module.
Previously, OutputPage hardcoded them in the top. The new formatter doesn't.
* Remove getHeadScripts().
* Remove getInlineHeadScripts().
* Remove getExternalHeadScripts().
* Remove buildCssLinks().
* Remove getScriptsForBottomQueue().
* Change where Skin::setupSkinUserCss() is called. This methods lets the skin
add modules to the queue. Previously it was called from buildCssLinks(),
via headElement(), via prepareQuickTemplate(), via OutputPage::output().
It's now in OutputPage::output() directly (slightly earlier). This is needed
because prepareQuickTemplate() calls bottomScripts() before headElement().
And bottomScript() would lazy-initialise the queue and lock it before
setupSkinUserCss() is called from headElement().
This makes execution order more predictable instead of being dependent on
the arbitrary order of data extraction in prepareQuickTemplate (which varies
from one skin to another).
* Compute isUserModulePreview() and isKnownEmpty() for the 'user' module early
on so. This avoids wrongful loading and fixes problem 1.
Effective changes in output:
* mw.loader.state() is now before mw.loader.load(). This fixes problem 2.
* mw.loader.state() now sets 'user.options' and 'user.tokens' to "loading".
* mw.loader.state() now sets 'user' (as "loading" or "ready"). Fixes problem 1.
* The <script async src> tag for 'startup' changed position (slightly).
Previously it was after all inline scripts and stylesheets. It's still after
all inline scripts and after most stylesheets, but before any user styles.
Since the queue is now formatted outside OutputPage, it can't inject the
meta-ResourceLoaderDynamicStyles tag and user-stylesheet hack in the middle
of existing output. This shouldn't have any noticable impact.
Bug: T87871
Change-Id: I605b8cd1e1fc009b4662a0edbc54d09dd65ee1df
2016-07-15 14:13:09 +00:00
|
|
|
$actualHtml = strval( $links );
|
2014-08-06 15:54:22 +00:00
|
|
|
$this->assertEquals( $expectedHtml, $actualHtml );
|
2014-06-28 20:40:22 +00:00
|
|
|
}
|
2015-09-08 21:59:45 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public static function provideMakeResourceLoaderLink() {
|
2017-05-12 22:20:02 +00:00
|
|
|
return [
|
2018-07-23 18:26:32 +00:00
|
|
|
// Single only=scripts load
|
|
|
|
|
[
|
2022-05-06 09:09:56 +00:00
|
|
|
[ 'test.foo', RL\Module::TYPE_SCRIPTS ],
|
2023-08-06 21:57:55 +00:00
|
|
|
"<script>(RLQ=window.RLQ||[]).push(function(){"
|
2019-06-10 17:38:05 +00:00
|
|
|
. 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.foo\u0026only=scripts");'
|
2018-07-23 18:26:32 +00:00
|
|
|
. "});</script>"
|
2017-05-12 22:20:02 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// Multiple only=styles load
|
|
|
|
|
[
|
2022-05-06 09:09:56 +00:00
|
|
|
[ [ 'test.baz', 'test.foo', 'test.bar' ], RL\Module::TYPE_STYLES ],
|
2018-07-23 18:26:32 +00:00
|
|
|
|
2022-05-24 22:15:00 +00:00
|
|
|
'<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&modules=test.bar%2Cbaz%2Cfoo&only=styles">'
|
2017-05-12 22:20:02 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// Private embed (only=scripts)
|
|
|
|
|
[
|
2022-05-06 09:09:56 +00:00
|
|
|
[ 'test.quux', RL\Module::TYPE_SCRIPTS ],
|
2023-08-06 21:57:55 +00:00
|
|
|
"<script>(RLQ=window.RLQ||[]).push(function(){"
|
2018-07-23 18:26:32 +00:00
|
|
|
. "mw.test.baz({token:123});\nmw.loader.state({\"test.quux\":\"ready\"});"
|
|
|
|
|
. "});</script>"
|
2017-05-12 22:20:02 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// Load private module (combined)
|
|
|
|
|
[
|
2022-05-06 09:09:56 +00:00
|
|
|
[ 'test.quux', RL\Module::TYPE_COMBINED ],
|
2023-08-06 21:57:55 +00:00
|
|
|
"<script>(RLQ=window.RLQ||[]).push(function(){"
|
2023-07-24 05:37:04 +00:00
|
|
|
. "mw.loader.impl(function(){return[\"test.quux@1b4i1\",function($,jQuery,require,module){"
|
ResourceLoader: Implement JavaScript source map support
In the debugger of Firefox and Chrome, without any special debug mode,
you will be able to see the original unminified JavaScript source, and
to set breakpoints in it and step through it.
Main visible changes:
* Add a config variable controlling the generation of source map links,
off by default for now.
* For script responses, move errors to the bottom of the response. This
avoids disturbing the source map.
* mw.loader.impl() calls will have less whitespace in debug mode,
because minification is no longer done as a post-processing step on
these calls.
Details:
* Use an index map when multiple responses are requested. This requires
an update to the minify library.
* Add a boolean "sourcemap" query parameter which causes load.php to
deliver source map output instead of regular minified content.
* Bundle sources into the source map and use two kinds of fake URL if a
real debug URL is not available. "Open in new tab" on a fake URL is
not functional.
* In the source map mode, respond with 404 if the version is mismatched
or if the content type is unimplemented.
* Fix createLoaderURL() so that $extraQuery is not ignored when there
are conflicting context parameters, so that we can successfully
override the version. The source map version should match the
delivered content, not the requested version.
* Since minification with source map tracking can't use filter(),
add a new cache for module source maps and minification. Add hit rate
stats.
Also:
* Fix unnecessary array_map() in getCombinedVersion()
Bug: T47514
Change-Id: I086e275148fdcac89f67a2fa0466d0dc063a17af
2023-08-08 07:17:29 +00:00
|
|
|
. "mw.test.baz({token:123});\n"
|
|
|
|
|
. "},{\"css\":[\".mw-icon{transition:none}"
|
2023-08-03 04:09:01 +00:00
|
|
|
. "\"]}];});});</script>"
|
2017-05-12 22:20:02 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// Load no modules
|
|
|
|
|
[
|
2022-05-06 09:09:56 +00:00
|
|
|
[ [], RL\Module::TYPE_COMBINED ],
|
2018-07-23 18:26:32 +00:00
|
|
|
'',
|
|
|
|
|
],
|
|
|
|
|
// noscript group
|
|
|
|
|
[
|
2022-05-06 09:09:56 +00:00
|
|
|
[ 'test.noscript', RL\Module::TYPE_STYLES ],
|
2022-05-24 22:15:00 +00:00
|
|
|
'<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&modules=test.noscript&only=styles"></noscript>'
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
// Load two modules in separate groups
|
|
|
|
|
[
|
2022-05-06 09:09:56 +00:00
|
|
|
[ [ 'test.group.foo', 'test.group.bar' ], RL\Module::TYPE_COMBINED ],
|
2023-08-06 21:57:55 +00:00
|
|
|
"<script>(RLQ=window.RLQ||[]).push(function(){"
|
2019-06-10 17:38:05 +00:00
|
|
|
. 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.bar");'
|
|
|
|
|
. 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.foo");'
|
2018-07-23 18:26:32 +00:00
|
|
|
. "});</script>"
|
2017-05-12 22:20:02 +00:00
|
|
|
],
|
|
|
|
|
];
|
2018-01-01 13:10:16 +00:00
|
|
|
// phpcs:enable
|
2017-05-12 22:20:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideBuildExemptModules
|
|
|
|
|
*/
|
|
|
|
|
public function testBuildExemptModules( array $exemptStyleModules, $expect ) {
|
2022-07-15 00:07:38 +00:00
|
|
|
$this->overrideConfigValues( [
|
|
|
|
|
MainConfigNames::ResourceLoaderDebug => false,
|
|
|
|
|
MainConfigNames::LoadScript => '/w/load.php',
|
2017-05-12 22:20:02 +00:00
|
|
|
// Stub wgCacheEpoch as it influences getVersionHash used for the
|
|
|
|
|
// urls in the expected HTML
|
2022-07-15 00:07:38 +00:00
|
|
|
MainConfigNames::CacheEpoch => '20140101000000',
|
2017-05-12 22:20:02 +00:00
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
// Set up stubs
|
|
|
|
|
$ctx = new RequestContext();
|
2022-01-12 20:13:39 +00:00
|
|
|
$skinFactory = $this->getServiceContainer()->getSkinFactory();
|
2019-06-03 00:55:00 +00:00
|
|
|
$ctx->setSkin( $skinFactory->makeSkin( 'fallback' ) );
|
2017-05-12 22:20:02 +00:00
|
|
|
$ctx->setLanguage( 'en' );
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = $this->getMockBuilder( OutputPage::class )
|
2017-05-12 22:20:02 +00:00
|
|
|
->setConstructorArgs( [ $ctx ] )
|
2021-03-20 15:18:58 +00:00
|
|
|
->onlyMethods( [ 'buildCssLinksArray' ] )
|
2017-05-12 22:20:02 +00:00
|
|
|
->getMock();
|
2019-07-12 17:30:06 +00:00
|
|
|
$op->method( 'buildCssLinksArray' )
|
2017-05-12 22:20:02 +00:00
|
|
|
->willReturn( [] );
|
2019-09-30 14:25:17 +00:00
|
|
|
/** @var OutputPage $op */
|
2018-07-23 18:26:32 +00:00
|
|
|
$rl = $op->getResourceLoader();
|
2022-05-06 09:09:56 +00:00
|
|
|
$rl->setMessageBlobStore( $this->createMock( RL\MessageBlobStore::class ) );
|
2017-05-12 22:20:02 +00:00
|
|
|
|
|
|
|
|
// Register custom modules
|
|
|
|
|
$rl->register( [
|
2019-07-12 17:30:06 +00:00
|
|
|
'example.site.a' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'site' ],
|
|
|
|
|
'example.site.b' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'site' ],
|
|
|
|
|
'example.user' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'user' ],
|
2017-05-12 22:20:02 +00:00
|
|
|
] );
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = TestingAccessWrapper::newFromObject( $op );
|
|
|
|
|
$op->rlExemptStyleModules = $exemptStyleModules;
|
2019-07-29 16:15:23 +00:00
|
|
|
$expect = strtr( $expect, [
|
|
|
|
|
'{blankCombi}' => ResourceLoaderTestCase::BLANK_COMBI,
|
|
|
|
|
] );
|
2017-05-12 22:20:02 +00:00
|
|
|
$this->assertEquals(
|
|
|
|
|
$expect,
|
2018-07-23 18:26:32 +00:00
|
|
|
strval( $op->buildExemptModules() )
|
2017-05-12 22:20:02 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public static function provideBuildExemptModules() {
|
|
|
|
|
return [
|
|
|
|
|
'empty' => [
|
|
|
|
|
'exemptStyleModules' => [],
|
resourceloader: Only output ResourceLoaderDynamicStyles when needed
In mediawiki.js, this marker has always been optional, falling back to
appending to <head>. When no stylesheets need to be after the marker
(e.g. no site styles on the wiki, and user is not logged-in), then
there is no need for the marker to exist.
In a previous refactor, I was going to do this and created an
"$append" variable in the function to do what this commit does,
but I forgot to actually use it for anything.
Test Plan:
* Local wiki, with no MediaWiki:Common/{Skinname}.css pages existing.
* When logged-out, before this change, there is a marker, now there is not.
* When creating "MediaWiki:Group-user.css" and logging in, there is still
a marker, and it is still above the <link> for that user styles request
in the <head>.
Bug: T219342
Change-Id: I2e9657f318088860916823efeb96ae4f1532974c
2019-06-26 23:20:33 +00:00
|
|
|
'',
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
'empty sets' => [
|
|
|
|
|
'exemptStyleModules' => [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ],
|
resourceloader: Only output ResourceLoaderDynamicStyles when needed
In mediawiki.js, this marker has always been optional, falling back to
appending to <head>. When no stylesheets need to be after the marker
(e.g. no site styles on the wiki, and user is not logged-in), then
there is no need for the marker to exist.
In a previous refactor, I was going to do this and created an
"$append" variable in the function to do what this commit does,
but I forgot to actually use it for anything.
Test Plan:
* Local wiki, with no MediaWiki:Common/{Skinname}.css pages existing.
* When logged-out, before this change, there is a marker, now there is not.
* When creating "MediaWiki:Group-user.css" and logging in, there is still
a marker, and it is still above the <link> for that user styles request
in the <head>.
Bug: T219342
Change-Id: I2e9657f318088860916823efeb96ae4f1532974c
2019-06-26 23:20:33 +00:00
|
|
|
'',
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
'default logged-out' => [
|
|
|
|
|
'exemptStyleModules' => [ 'site' => [ 'site.styles' ] ],
|
2022-05-24 22:15:00 +00:00
|
|
|
'<meta name="ResourceLoaderDynamicStyles" content="">' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=site.styles&only=styles">',
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
'default logged-in' => [
|
|
|
|
|
'exemptStyleModules' => [ 'site' => [ 'site.styles' ], 'user' => [ 'user.styles' ] ],
|
2022-05-24 22:15:00 +00:00
|
|
|
'<meta name="ResourceLoaderDynamicStyles" content="">' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=site.styles&only=styles">' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=user.styles&only=styles&version=94mvi">',
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
'custom modules' => [
|
|
|
|
|
'exemptStyleModules' => [
|
|
|
|
|
'site' => [ 'site.styles', 'example.site.a', 'example.site.b' ],
|
|
|
|
|
'user' => [ 'user.styles', 'example.user' ],
|
|
|
|
|
],
|
2022-05-24 22:15:00 +00:00
|
|
|
'<meta name="ResourceLoaderDynamicStyles" content="">' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=example.site.a%2Cb&only=styles">' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=site.styles&only=styles">' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=example.user&only=styles&version={blankCombi}">' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=user.styles&only=styles&version=94mvi">',
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
// phpcs:enable
|
|
|
|
|
}
|
|
|
|
|
|
2015-09-08 21:59:45 +00:00
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* @dataProvider provideTransformFilePath
|
2015-09-08 21:59:45 +00:00
|
|
|
*/
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testTransformResourcePath( $baseDir, $basePath, $uploadDir = null,
|
|
|
|
|
$uploadPath = null, $path = null, $expected = null
|
|
|
|
|
) {
|
|
|
|
|
if ( $path === null ) {
|
|
|
|
|
// Skip optional $uploadDir and $uploadPath
|
|
|
|
|
$path = $uploadDir;
|
|
|
|
|
$expected = $uploadPath;
|
|
|
|
|
$uploadDir = "$baseDir/images";
|
|
|
|
|
$uploadPath = "$basePath/images";
|
2015-09-08 21:59:45 +00:00
|
|
|
}
|
2018-07-23 18:26:32 +00:00
|
|
|
$conf = new HashConfig( [
|
2022-08-18 16:44:16 +00:00
|
|
|
MainConfigNames::ResourceBasePath => $basePath,
|
|
|
|
|
MainConfigNames::UploadDirectory => $uploadDir,
|
|
|
|
|
MainConfigNames::UploadPath => $uploadPath,
|
|
|
|
|
MainConfigNames::BaseDirectory => $baseDir
|
2018-07-23 18:26:32 +00:00
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
// Some of these paths don't exist and will cause warnings
|
2022-02-24 19:57:59 +00:00
|
|
|
$actual = @OutputPage::transformResourcePath( $conf, $path );
|
2018-07-23 18:26:32 +00:00
|
|
|
|
|
|
|
|
$this->assertEquals( $expected ?: $path, $actual );
|
2015-09-08 21:59:45 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public static function provideTransformFilePath() {
|
2023-09-12 18:28:21 +00:00
|
|
|
$baseDir = dirname( __DIR__ ) . '/../data/media';
|
2016-02-17 09:09:32 +00:00
|
|
|
return [
|
2018-07-23 18:26:32 +00:00
|
|
|
// File that matches basePath, and exists. Hash found and appended.
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '/w',
|
|
|
|
|
'/w/test.jpg',
|
|
|
|
|
'/w/test.jpg?edcf2'
|
2016-02-17 09:09:32 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// File that matches basePath, but not found on disk. Empty query.
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '/w',
|
|
|
|
|
'/w/unknown.png',
|
2021-05-21 18:15:51 +00:00
|
|
|
'/w/unknown.png'
|
2016-02-17 09:09:32 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// File not matching basePath. Ignored.
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '/w',
|
|
|
|
|
'/files/test.jpg'
|
2016-02-17 09:09:32 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// Empty string. Ignored.
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '/w',
|
|
|
|
|
'',
|
|
|
|
|
''
|
2016-02-17 09:09:32 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// Similar path, but with domain component. Ignored.
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '/w',
|
|
|
|
|
'//example.org/w/test.jpg'
|
2016-02-17 09:09:32 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '/w',
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/w/test.jpg'
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
// Unrelated path with domain component. Ignored.
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '/w',
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/files/test.jpg'
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '/w',
|
|
|
|
|
'//example.org/files/test.jpg'
|
|
|
|
|
],
|
|
|
|
|
// Unrelated path with domain, and empty base path (root mw install). Ignored.
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '',
|
2023-03-07 20:06:23 +00:00
|
|
|
'https://www.example.org/files/test.jpg'
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '',
|
|
|
|
|
// T155310
|
|
|
|
|
'//example.org/files/test.jpg'
|
|
|
|
|
],
|
|
|
|
|
// Check UploadPath before ResourceBasePath (T155146)
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => dirname( $baseDir ), 'basePath' => '',
|
|
|
|
|
'uploadDir' => $baseDir, 'uploadPath' => '/images',
|
|
|
|
|
'/images/test.jpg',
|
|
|
|
|
'/images/test.jpg?edcf2'
|
2016-02-17 09:09:32 +00:00
|
|
|
],
|
|
|
|
|
];
|
2015-09-08 21:59:45 +00:00
|
|
|
}
|
2015-10-08 04:45:26 +00:00
|
|
|
|
|
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* Tests a particular case of transformCssMedia, using the given input, globals,
|
|
|
|
|
* expected return, and message
|
|
|
|
|
*
|
|
|
|
|
* Asserts that $expectedReturn is returned.
|
|
|
|
|
*
|
2021-10-09 01:41:15 +00:00
|
|
|
* options['queryData'] - value of query string
|
2018-07-23 18:26:32 +00:00
|
|
|
* options['media'] - passed into the method under the same name
|
|
|
|
|
* options['expectedReturn'] - expected return value
|
|
|
|
|
* options['message'] - PHPUnit message for assertion
|
|
|
|
|
*
|
|
|
|
|
* @param array $args Key-value array of arguments as shown above
|
2015-10-08 04:45:26 +00:00
|
|
|
*/
|
2018-07-23 18:26:32 +00:00
|
|
|
protected function assertTransformCssMediaCase( $args ) {
|
2021-10-09 01:41:15 +00:00
|
|
|
$queryData = $args['queryData'] ?? [];
|
2015-10-08 04:45:26 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$fauxRequest = new FauxRequest( $queryData, false );
|
2020-12-15 09:47:15 +00:00
|
|
|
$this->setRequest( $fauxRequest );
|
2015-10-08 04:45:26 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$actualReturn = OutputPage::transformCssMedia( $args['media'] );
|
|
|
|
|
$this->assertSame( $args['expectedReturn'], $actualReturn, $args['message'] );
|
2015-10-08 04:45:26 +00:00
|
|
|
}
|
2016-07-08 19:11:53 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testPrintRequests() {
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
2021-10-09 01:41:15 +00:00
|
|
|
'queryData' => [ 'printable' => '1' ],
|
2018-07-23 18:26:32 +00:00
|
|
|
'media' => 'screen',
|
|
|
|
|
'expectedReturn' => null,
|
|
|
|
|
'message' => 'On printable request, screen returns null'
|
2016-07-08 19:11:53 +00:00
|
|
|
] );
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$this->assertTransformCssMediaCase( [
|
2021-10-09 01:41:15 +00:00
|
|
|
'queryData' => [ 'printable' => '1' ],
|
2018-07-23 18:26:32 +00:00
|
|
|
'media' => self::SCREEN_MEDIA_QUERY,
|
|
|
|
|
'expectedReturn' => null,
|
|
|
|
|
'message' => 'On printable request, screen media query returns null'
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
2021-10-09 01:41:15 +00:00
|
|
|
'queryData' => [ 'printable' => '1' ],
|
2018-07-23 18:26:32 +00:00
|
|
|
'media' => self::SCREEN_ONLY_MEDIA_QUERY,
|
|
|
|
|
'expectedReturn' => null,
|
|
|
|
|
'message' => 'On printable request, screen media query with only returns null'
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
2021-10-09 01:41:15 +00:00
|
|
|
'queryData' => [ 'printable' => '1' ],
|
2018-07-23 18:26:32 +00:00
|
|
|
'media' => 'print',
|
|
|
|
|
'expectedReturn' => '',
|
|
|
|
|
'message' => 'On printable request, media print returns empty string'
|
2016-07-08 19:11:53 +00:00
|
|
|
] );
|
|
|
|
|
}
|
2017-01-24 17:30:33 +00:00
|
|
|
|
2015-06-01 16:58:42 +00:00
|
|
|
/**
|
OutputPage,Html,Xml: Widen `@covers` annotations in unit tests
Follows-up I6d845bdfbb80, I69b5385868, I4c7d826c7e, I1287f3979ab, which
widened the `@covers` annotations of other test suites:
> We lose useful coverage and spend valuable time keeping these tags
> accurate through refactors (or worse, forget to do so).
>
> I've audited each test to confirm it is a general test of the
> subject class, where tagging missing methods would be an accepted
> change, thus widening it is merely a no-op that clarifies intent
> and reduces maintenance. I am not disabling the "only track coverage
> of specified subject" mechanism, nor am I claiming coverage in
> in classes outside the subject under test.
>
> Tracking this narrow detail wastes time to keep methods in sync during
> refactors, time to realize (and fix) when other people inevitably
> dodn't keep them in sync, time lost in finding uncovered code to
> write tests for only to realize it is already covered but not tagged.
Change-Id: I9d63105fd64b0a863d5bfaa67ae069c14531a4cf
2023-06-02 17:42:49 +00:00
|
|
|
* Test screen requests, without either query parameter set
|
2015-06-01 16:58:42 +00:00
|
|
|
*/
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testScreenRequests() {
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
|
|
|
|
'media' => 'screen',
|
|
|
|
|
'expectedReturn' => 'screen',
|
|
|
|
|
'message' => 'On screen request, screen media type is preserved'
|
|
|
|
|
] );
|
2015-06-01 16:58:42 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$this->assertTransformCssMediaCase( [
|
|
|
|
|
'media' => 'handheld',
|
|
|
|
|
'expectedReturn' => 'handheld',
|
|
|
|
|
'message' => 'On screen request, handheld media type is preserved'
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
|
|
|
|
'media' => self::SCREEN_MEDIA_QUERY,
|
|
|
|
|
'expectedReturn' => self::SCREEN_MEDIA_QUERY,
|
|
|
|
|
'message' => 'On screen request, screen media query is preserved.'
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
|
|
|
|
'media' => self::SCREEN_ONLY_MEDIA_QUERY,
|
|
|
|
|
'expectedReturn' => self::SCREEN_ONLY_MEDIA_QUERY,
|
|
|
|
|
'message' => 'On screen request, screen media query with only is preserved.'
|
|
|
|
|
] );
|
2015-06-01 16:58:42 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$this->assertTransformCssMediaCase( [
|
|
|
|
|
'media' => 'print',
|
|
|
|
|
'expectedReturn' => 'print',
|
|
|
|
|
'message' => 'On screen request, print media type is preserved'
|
|
|
|
|
] );
|
2015-06-01 16:58:42 +00:00
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
public function testIsTOCEnabled() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertFalse( $op->isTOCEnabled() );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertFalse( $op->getOutputFlag( ParserOutputFlags::SHOW_TOC ) );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
2022-06-09 12:59:50 +00:00
|
|
|
$pOut1 = $this->createParserOutputStub();
|
|
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
|
|
|
|
$this->assertFalse( $op->isTOCEnabled() );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertFalse( $op->getOutputFlag( ParserOutputFlags::SHOW_TOC ) );
|
2022-06-09 12:59:50 +00:00
|
|
|
|
2023-03-21 18:26:05 +00:00
|
|
|
$pOut2 = $this->createParserOutputStubWithFlags(
|
|
|
|
|
[], [ ParserOutputFlags::SHOW_TOC ]
|
|
|
|
|
);
|
2022-06-09 12:59:50 +00:00
|
|
|
$op->addParserOutput( $pOut2 );
|
|
|
|
|
$this->assertTrue( $op->isTOCEnabled() );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::SHOW_TOC ) );
|
2022-06-09 12:59:50 +00:00
|
|
|
|
|
|
|
|
// The parser output doesn't disable the TOC after it was enabled
|
|
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
|
|
|
|
$this->assertTrue( $op->isTOCEnabled() );
|
2023-03-21 18:26:05 +00:00
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::SHOW_TOC ) );
|
2022-06-09 12:59:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testIsTOCEnabledBackCompat() {
|
|
|
|
|
// This tests backward compatibility: OutputPage *used* to use
|
|
|
|
|
// ParserOutput::getTOCHTML() to determine whether the TOC should
|
|
|
|
|
// be enabled, before ParserOutputFlags::SHOW_TOC was added in 1.39.
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertFalse( $op->isTOCEnabled() );
|
|
|
|
|
|
2023-01-26 22:12:42 +00:00
|
|
|
$pOut1 = $this->createParserOutputStub( [
|
|
|
|
|
'getTOCHTML' => '',
|
|
|
|
|
] );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
|
|
|
|
$this->assertFalse( $op->isTOCEnabled() );
|
|
|
|
|
|
2023-10-12 14:26:17 +00:00
|
|
|
// Transitional: This is now a no-op and will be deleted in the next commit.
|
2023-01-26 22:12:42 +00:00
|
|
|
$pOut2 = $this->createParserOutputStub( [
|
|
|
|
|
'getTOCHTML' => 'stuff',
|
|
|
|
|
] );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutput( $pOut2 );
|
2023-10-12 14:26:17 +00:00
|
|
|
$this->assertFalse( $op->isTOCEnabled() );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
2023-10-12 14:26:17 +00:00
|
|
|
// The parser output doesn't somehow enable the TOC
|
2018-08-02 18:42:17 +00:00
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
2023-10-12 14:26:17 +00:00
|
|
|
$this->assertFalse( $op->isTOCEnabled() );
|
2018-08-02 18:42:17 +00:00
|
|
|
}
|
|
|
|
|
|
2023-03-21 18:26:05 +00:00
|
|
|
public function testNoTOC() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertFalse( $op->getOutputFlag( ParserOutputFlags::NO_TOC ) );
|
|
|
|
|
|
|
|
|
|
$stubPO1 = $this->createParserOutputStubWithFlags(
|
|
|
|
|
[], [ ParserOutputFlags::NO_TOC ]
|
|
|
|
|
);
|
|
|
|
|
$op->addParserOutputMetadata( $stubPO1 );
|
|
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::NO_TOC ) );
|
|
|
|
|
|
|
|
|
|
$stubPO2 = $this->createParserOutputStub();
|
|
|
|
|
$this->assertFalse( $stubPO2->getOutputFlag( ParserOutputFlags::NO_TOC ) );
|
|
|
|
|
$op->addParserOutput( $stubPO2 );
|
|
|
|
|
// Note that flags are OR'ed together, and not reset.
|
|
|
|
|
$this->assertTrue( $op->getOutputFlag( ParserOutputFlags::NO_TOC ) );
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider providePreloadLinkHeaders
|
2023-02-21 16:44:38 +00:00
|
|
|
* @covers \MediaWiki\ResourceLoader\SkinModule
|
2018-08-02 18:42:17 +00:00
|
|
|
*/
|
2022-01-27 20:24:12 +00:00
|
|
|
public function testPreloadLinkHeaders( $config, $result ) {
|
2022-05-06 09:09:56 +00:00
|
|
|
$ctx = $this->createMock( RL\Context::class );
|
|
|
|
|
$module = new RL\SkinModule();
|
2021-06-11 15:11:37 +00:00
|
|
|
$module->setConfig( new HashConfig( $config + ResourceLoaderTestCase::getSettings() ) );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
$this->assertEquals( [ $result ], $module->getHeaders( $ctx ) );
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function providePreloadLinkHeaders() {
|
2018-08-02 18:42:17 +00:00
|
|
|
return [
|
|
|
|
|
[
|
|
|
|
|
[
|
2023-06-19 19:21:47 +00:00
|
|
|
MainConfigNames::ResourceBasePath => '/w',
|
|
|
|
|
MainConfigNames::Logo => '/img/default.png',
|
|
|
|
|
MainConfigNames::Logos => [
|
2018-08-02 18:42:17 +00:00
|
|
|
'1.5x' => '/img/one-point-five.png',
|
|
|
|
|
'2x' => '/img/two-x.png',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'Link: </img/default.png>;rel=preload;as=image;media=' .
|
|
|
|
|
'not all and (min-resolution: 1.5dppx),' .
|
|
|
|
|
'</img/one-point-five.png>;rel=preload;as=image;media=' .
|
|
|
|
|
'(min-resolution: 1.5dppx) and (max-resolution: 1.999999dppx),' .
|
|
|
|
|
'</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[
|
2023-06-19 19:21:47 +00:00
|
|
|
MainConfigNames::ResourceBasePath => '/w',
|
|
|
|
|
MainConfigNames::Logos => [
|
2020-01-07 19:18:51 +00:00
|
|
|
'1x' => '/img/default.png',
|
|
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
],
|
|
|
|
|
'Link: </img/default.png>;rel=preload;as=image'
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[
|
2023-06-19 19:21:47 +00:00
|
|
|
MainConfigNames::ResourceBasePath => '/w',
|
|
|
|
|
MainConfigNames::Logos => [
|
2020-01-07 19:18:51 +00:00
|
|
|
'1x' => '/img/default.png',
|
2018-08-02 18:42:17 +00:00
|
|
|
'2x' => '/img/two-x.png',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'Link: </img/default.png>;rel=preload;as=image;media=' .
|
|
|
|
|
'not all and (min-resolution: 2dppx),' .
|
|
|
|
|
'</img/two-x.png>;rel=preload;as=image;media=(min-resolution: 2dppx)'
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[
|
2023-06-19 19:21:47 +00:00
|
|
|
MainConfigNames::ResourceBasePath => '/w',
|
|
|
|
|
MainConfigNames::Logos => [
|
2020-01-07 19:18:51 +00:00
|
|
|
'1x' => '/img/default.png',
|
2018-08-02 18:42:17 +00:00
|
|
|
'svg' => '/img/vector.svg',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'Link: </img/vector.svg>;rel=preload;as=image'
|
|
|
|
|
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[
|
2023-06-19 19:21:47 +00:00
|
|
|
MainConfigNames::ResourceBasePath => '/w',
|
|
|
|
|
MainConfigNames::Logos => [
|
2020-01-07 19:18:51 +00:00
|
|
|
'1x' => '/w/test.jpg',
|
|
|
|
|
],
|
2023-06-19 19:21:47 +00:00
|
|
|
MainConfigNames::UploadPath => '/w/images',
|
2023-09-12 18:28:21 +00:00
|
|
|
MainConfigNames::BaseDirectory => dirname( __DIR__ ) . '/../data/media'
|
2018-08-02 18:42:17 +00:00
|
|
|
],
|
|
|
|
|
'Link: </w/test.jpg?edcf2>;rel=preload;as=image',
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-21 12:10:09 +00:00
|
|
|
/**
|
|
|
|
|
* @param int $titleLastRevision Last Title revision to set
|
|
|
|
|
* @param int $outputRevision Revision stored in OutputPage
|
|
|
|
|
* @param bool $expectedResult Expected result of $output->isRevisionCurrent call
|
|
|
|
|
* @dataProvider provideIsRevisionCurrent
|
|
|
|
|
*/
|
|
|
|
|
public function testIsRevisionCurrent( $titleLastRevision, $outputRevision, $expectedResult ) {
|
2019-10-06 09:52:39 +00:00
|
|
|
$titleMock = $this->createMock( Title::class );
|
2021-04-22 08:40:46 +00:00
|
|
|
$titleMock->method( 'getLatestRevID' )
|
2019-08-21 12:10:09 +00:00
|
|
|
->willReturn( $titleLastRevision );
|
|
|
|
|
|
2019-09-30 14:25:17 +00:00
|
|
|
$output = $this->newInstance( [], null );
|
2019-08-21 12:10:09 +00:00
|
|
|
$output->setTitle( $titleMock );
|
|
|
|
|
$output->setRevisionId( $outputRevision );
|
|
|
|
|
$this->assertEquals( $expectedResult, $output->isRevisionCurrent() );
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideIsRevisionCurrent() {
|
2019-08-21 12:10:09 +00:00
|
|
|
return [
|
|
|
|
|
[ 10, null, true ],
|
|
|
|
|
[ 42, 42, true ],
|
|
|
|
|
[ null, 0, true ],
|
|
|
|
|
[ 42, 47, false ],
|
|
|
|
|
[ 47, 42, false ]
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-30 01:13:03 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideSendCacheControl
|
|
|
|
|
*/
|
2021-01-14 08:20:36 +00:00
|
|
|
public function testSendCacheControl( array $options = [], array $expectations = [] ) {
|
2022-10-19 17:58:11 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::UsePigLatinVariant, $options['variant'] ?? false );
|
|
|
|
|
|
2019-10-30 01:13:03 +00:00
|
|
|
$output = $this->newInstance( [
|
|
|
|
|
'UseCdn' => $options['useCdn'] ?? false,
|
|
|
|
|
] );
|
2022-02-04 19:12:11 +00:00
|
|
|
$output->considerCacheSettingsFinal();
|
|
|
|
|
|
|
|
|
|
$cacheable = $options['enableClientCache'] ?? true;
|
|
|
|
|
if ( !$cacheable ) {
|
|
|
|
|
$output->disableClientCache();
|
|
|
|
|
}
|
|
|
|
|
$this->assertEquals( $cacheable, $output->couldBePublicCached() );
|
2019-10-30 01:13:03 +00:00
|
|
|
|
2020-05-26 13:14:46 +00:00
|
|
|
$output->setCdnMaxage( $options['cdnMaxAge'] ?? 0 );
|
2019-10-30 01:13:03 +00:00
|
|
|
|
|
|
|
|
if ( isset( $options['lastModified'] ) ) {
|
|
|
|
|
$output->setLastModified( $options['lastModified'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$response = $output->getRequest()->response();
|
|
|
|
|
if ( isset( $options['cookie'] ) ) {
|
|
|
|
|
$response->setCookie( 'test', 1234 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$output->sendCacheControl();
|
|
|
|
|
|
|
|
|
|
$headers = [
|
|
|
|
|
'Vary' => 'Accept-Encoding, Cookie',
|
|
|
|
|
'Cache-Control' => 'private, must-revalidate, max-age=0',
|
|
|
|
|
'Expires' => true,
|
|
|
|
|
'Last-Modified' => false,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ( $headers as $header => $default ) {
|
2021-01-14 08:20:36 +00:00
|
|
|
$value = $expectations[$header] ?? $default;
|
2019-10-30 01:13:03 +00:00
|
|
|
if ( $value === true ) {
|
2022-10-19 17:58:11 +00:00
|
|
|
$this->assertNotEmpty( $response->getHeader( $header ), "$header header" );
|
2019-10-30 01:13:03 +00:00
|
|
|
} elseif ( $value === false ) {
|
2022-10-19 17:58:11 +00:00
|
|
|
$this->assertNull( $response->getHeader( $header ), "$header header" );
|
2019-10-30 01:13:03 +00:00
|
|
|
} else {
|
2022-10-19 17:58:11 +00:00
|
|
|
$this->assertEquals( $value, $response->getHeader( $header ), "$header header" );
|
2019-10-30 01:13:03 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideSendCacheControl() {
|
2019-10-30 01:13:03 +00:00
|
|
|
return [
|
2022-10-19 17:58:11 +00:00
|
|
|
'Vary on variant' => [
|
|
|
|
|
[
|
|
|
|
|
'variant' => true,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'Vary' => 'Accept-Encoding, Cookie, Accept-Language',
|
|
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'Private per default' => [
|
|
|
|
|
[],
|
2019-10-30 01:13:03 +00:00
|
|
|
[
|
2021-10-20 16:16:03 +00:00
|
|
|
'Cache-Control' => 'private, must-revalidate, max-age=0',
|
2019-10-30 01:13:03 +00:00
|
|
|
],
|
|
|
|
|
],
|
2022-10-19 17:58:11 +00:00
|
|
|
'Cookies force private' => [
|
2019-10-30 01:13:03 +00:00
|
|
|
[
|
|
|
|
|
'cookie' => true,
|
2022-10-19 17:58:11 +00:00
|
|
|
'useCdn' => true,
|
|
|
|
|
'cdnMaxAge' => 300,
|
2019-10-30 01:13:03 +00:00
|
|
|
],
|
2022-10-19 17:58:11 +00:00
|
|
|
[
|
|
|
|
|
'Cache-Control' => 'private, must-revalidate, max-age=0',
|
|
|
|
|
]
|
2019-10-30 01:13:03 +00:00
|
|
|
],
|
|
|
|
|
'Disable client cache' => [
|
|
|
|
|
[
|
|
|
|
|
'enableClientCache' => false,
|
2022-10-19 17:58:11 +00:00
|
|
|
'useCdn' => true,
|
|
|
|
|
'cdnMaxAge' => 300,
|
2019-10-30 01:13:03 +00:00
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'Cache-Control' => 'no-cache, no-store, max-age=0, must-revalidate',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'Set last modified' => [
|
|
|
|
|
[
|
|
|
|
|
// 0 is the current time, so we'll use 1 instead.
|
|
|
|
|
'lastModified' => 1,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'Last-Modified' => 'Thu, 01 Jan 1970 00:00:01 GMT',
|
|
|
|
|
]
|
|
|
|
|
],
|
|
|
|
|
'Public' => [
|
|
|
|
|
[
|
|
|
|
|
'useCdn' => true,
|
|
|
|
|
'cdnMaxAge' => 300,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'Cache-Control' => 's-maxage=300, must-revalidate, max-age=0',
|
|
|
|
|
'Expires' => false,
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-04 15:55:20 +00:00
|
|
|
public function provideGetJsVarsEditable() {
|
|
|
|
|
yield 'can edit and create' => [
|
|
|
|
|
'performer' => $this->mockAnonAuthorityWithPermissions( [ 'edit', 'create' ] ),
|
|
|
|
|
'expectedEditableConfig' => [
|
|
|
|
|
'wgIsProbablyEditable' => true,
|
|
|
|
|
'wgRelevantPageIsProbablyEditable' => true,
|
|
|
|
|
]
|
|
|
|
|
];
|
|
|
|
|
yield 'cannot edit or create' => [
|
|
|
|
|
'performer' => $this->mockAnonAuthorityWithoutPermissions( [ 'edit', 'create' ] ),
|
|
|
|
|
'expectedEditableConfig' => [
|
|
|
|
|
'wgIsProbablyEditable' => false,
|
|
|
|
|
'wgRelevantPageIsProbablyEditable' => false,
|
|
|
|
|
]
|
|
|
|
|
];
|
|
|
|
|
yield 'only can edit relevant title' => [
|
2021-04-29 16:24:12 +00:00
|
|
|
'performer' => $this->mockAnonAuthority( static function (
|
2021-03-04 15:55:20 +00:00
|
|
|
string $permission,
|
|
|
|
|
PageIdentity $page
|
|
|
|
|
) {
|
2023-08-22 18:54:10 +00:00
|
|
|
return ( $permission === 'edit' || $permission === 'create' ) && $page->getDBkey() === 'RelevantTitle';
|
2021-03-04 15:55:20 +00:00
|
|
|
} ),
|
|
|
|
|
'expectedEditableConfig' => [
|
|
|
|
|
'wgIsProbablyEditable' => false,
|
|
|
|
|
'wgRelevantPageIsProbablyEditable' => true,
|
|
|
|
|
]
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideGetJsVarsEditable
|
|
|
|
|
*/
|
|
|
|
|
public function testGetJsVarsEditable( Authority $performer, array $expectedEditableConfig ) {
|
|
|
|
|
$op = $this->newInstance( [], null, null, $performer );
|
2022-09-23 19:53:11 +00:00
|
|
|
$op->getContext()->getSkin()->setRelevantTitle( Title::makeTitle( NS_MAIN, 'RelevantTitle' ) );
|
2021-03-04 15:55:20 +00:00
|
|
|
$this->assertArraySubmapSame( $expectedEditableConfig, $op->getJSVars() );
|
|
|
|
|
}
|
|
|
|
|
|
parser: Move lang/dir and mw-content-ltr to ParserOutput::getText
== Skin::wrapHTML ==
Skin::wrapHTML no longer has to perform any guessing of the
ParserOutput language. Nor does it have to special wiki pages vs
special pages in this regard. Yay, code removal.
== ImagePage ==
On URLs like /wiki/File:Example.jpg, the main output handler is
ImagePage::view. This calls the parent Article::view to handle most of
its output. Article::view obtains the ParserOptions, and then fetches
ParserOutput, and then adds `<div class=mw-parser-output>` and its
metadata to OutputPage.
Before this change, ImagePage::view was creating a wrapper based
on "predicting" what language the ParserOutput will contain. It
couldn't call the new OutputPage::getContentLanguage or some
equivalent as Article::view wouldn't have populated that yet.
This leaky abstraction is fixed by this change as now the `<div>`
from ParserOutput no longer comes with a "please wrap it properly"
contract that Article subclasses couldn't possibly implement correctly
(it coudln't wrap it after the fact because Article::view writes to
OutputPage directly).
RECENT (T310445):
A special case was recently added for file pages about translated SVGs.
For those, we decide which language to use for the "fullMedia" thumb
atop the page. This was recently changed as part of T310445 from a
hardcoded $wgLanguageCode (site content lang) to new problematic
Title::getPageViewLanguage, which tries to guestimate the page
language of the rendered ParserOutput and then gets the preferred
variant for the current user. The motivation for this was to support
language variants but used Title::getPageViewLanguage as a kitchen
sink to achieve that minor side-effect. The only part of this
now-deprecated method that we actually need is
LanguageConverter::getPreferredVariant().
Test plan: Covered by ImagePageTest.
== Skin mainpage-title ==
RECENT (T331095, T298715):
A special case was added to Skin::getTemplateData that powers the
mainpage-title interface message feature. This is empty by default,
but when created via MediaWiki:mainpage-title allows interface admins
to replace the H1 with a custom and localised page heading.
A few months ago, in Ifc9f0a7174, Title::getPageViewLanguage was
applied here to support language variants. Replace with the same
fix as for ImagePage. Revert back to Message::inContentLanguage()
but refactor to inLanguage() via MediaWikiServices::getContentLanguage
so that LanguageConverter::getPreferredVariant can be applied.
== EditPage ==
This was doing similar "predicting" of the ParserOutput language to
create an empty preview placeholder for use by preview.js. Now that
ApiParse (via ParserOutput::getText) returns a usable element without
any secret "you magically know the right class, lang, and dir" contract,
this placeholder is no longer needed.
Test Plan:
* EditPage: Default preview
1. index.php?title=Main_Page&action=edit
2. Show preview
3. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
* EditPage: JS preview
1. Preferences > Editing > Show preview without reload
2. index.php?title=Main_Page&action=edit
3. Show preview
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
5. Type something and 'Show preview' again
6. Assert old element gone, new text is shown, and new element
attributes are the same as the above.
== McrUndoAction ==
Same as EditPage basically, but without the JS preview use case.
== DifferenceEngine ==
Test:
1. Open /w/index.php?title=Main_Page&diff=0
(this shows the latest diff, can do manually by viewing
/wiki/Main_Page, click "View history", click "Compare selected revisions")
2. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
3. Open /w/index.php?title=Main_Page&diff=0&action=render
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
== Special:ExpandTemplates ==
Test:
1. /wiki/Special:ExpandTemplates
2. Write "Hello".
3. "OK"
4. Assert <div class="mw-content-ltr mw-parser-output" lang=en dir=ltr>
Bug: T341244
Depends-On: Icd9c079f5896ee83d86b9c2699636dc81d25a14c
Depends-On: I4e7484b3b94f1cb6062e7cef9f20626b650bb4b1
Depends-On: I90b88f3b3a3bbeba4f48d118f92f54864997e105
Change-Id: Ib130a055e46764544af0f1a46d2bc2b3a7ee85b7
2023-10-04 04:45:07 +00:00
|
|
|
public function provideJsVarsAboutPageLang() {
|
|
|
|
|
// Format:
|
|
|
|
|
// - expected
|
|
|
|
|
// - title
|
|
|
|
|
// - site content language
|
|
|
|
|
// - user language
|
|
|
|
|
// - wgDefaultLanguageVariant
|
|
|
|
|
return [
|
|
|
|
|
[ 'fr', [ NS_HELP, 'I_need_somebody' ], 'fr', 'fr', false ],
|
|
|
|
|
[ 'es', [ NS_HELP, 'I_need_somebody' ], 'es', 'zh-tw', false ],
|
|
|
|
|
[ 'zh', [ NS_HELP, 'I_need_somebody' ], 'zh', 'zh-tw', false ],
|
|
|
|
|
[ 'es', [ NS_HELP, 'I_need_somebody' ], 'es', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'es', [ NS_MEDIAWIKI, 'About' ], 'es', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'es', [ NS_MEDIAWIKI, 'About/' ], 'es', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'de', [ NS_MEDIAWIKI, 'About/de' ], 'es', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'en', [ NS_MEDIAWIKI, 'Common.js' ], 'es', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'en', [ NS_MEDIAWIKI, 'Common.css' ], 'es', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'en', [ NS_USER, 'JohnDoe/Common.js' ], 'es', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'en', [ NS_USER, 'JohnDoe/Monobook.css' ], 'es', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
|
|
|
|
|
[ 'zh-cn', [ NS_HELP, 'I_need_somebody' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'zh', [ NS_MEDIAWIKI, 'About' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'zh', [ NS_MEDIAWIKI, 'About/' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'de', [ NS_MEDIAWIKI, 'About/de' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'zh-cn', [ NS_MEDIAWIKI, 'About/zh-cn' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'zh-tw', [ NS_MEDIAWIKI, 'About/zh-tw' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'en', [ NS_MEDIAWIKI, 'Common.js' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'en', [ NS_MEDIAWIKI, 'Common.css' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'en', [ NS_USER, 'JohnDoe/Common.js' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'en', [ NS_USER, 'JohnDoe/Monobook.css' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
|
|
|
|
|
[ 'nl', [ NS_SPECIAL, 'BlankPage' ], 'en', 'nl', false ],
|
|
|
|
|
[ 'zh-tw', [ NS_SPECIAL, 'NewPages' ], 'es', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
[ 'zh-tw', [ NS_SPECIAL, 'NewPages' ], 'zh', 'zh-tw', 'zh-cn' ],
|
|
|
|
|
|
|
|
|
|
[ 'sr-ec', [ NS_FILE, 'Example' ], 'sr', 'sr', 'sr-ec' ],
|
|
|
|
|
[ 'sr', [ NS_FILE, 'Example' ], 'sr', 'sr', 'sr' ],
|
|
|
|
|
[ 'sr-ec', [ NS_MEDIAWIKI, 'Example' ], 'sr-ec', 'sr-ec', 'sr' ],
|
|
|
|
|
[ 'sr' , [ NS_MEDIAWIKI, 'Example' ], 'sr', 'sr', 'sr-ec' ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideJsVarsAboutPageLang
|
|
|
|
|
*/
|
|
|
|
|
public function testGetJsVarsAboutPageLang( $expected, $title, $contLang, $userLang, $variant ) {
|
|
|
|
|
$this->overrideConfigValues( [
|
|
|
|
|
MainConfigNames::DefaultLanguageVariant => $variant,
|
|
|
|
|
] );
|
|
|
|
|
$this->setContentLang( $contLang );
|
|
|
|
|
$output = $this->newInstance(
|
|
|
|
|
[ 'LanguageCode' => $contLang ],
|
|
|
|
|
new FauxRequest( [ 'uselang' => $userLang ] ),
|
|
|
|
|
'notitle'
|
|
|
|
|
);
|
|
|
|
|
$output->setTitle( Title::makeTitle( $title[0], $title[1] ) );
|
|
|
|
|
|
|
|
|
|
$this->assertArraySubmapSame( [
|
|
|
|
|
'wgPageViewLanguage' => $expected,
|
|
|
|
|
'wgPageContentLanguage' => $expected,
|
|
|
|
|
], $output->getJSVars() );
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-05 06:54:11 +00:00
|
|
|
/**
|
|
|
|
|
* @param bool $registered
|
|
|
|
|
* @param bool $matchToken
|
|
|
|
|
* @return MockObject|User
|
|
|
|
|
*/
|
|
|
|
|
private function mockUser( bool $registered, bool $matchToken ) {
|
|
|
|
|
$user = $this->createNoOpMock( User::class, [ 'isRegistered', 'matchEditToken' ] );
|
|
|
|
|
$user->method( 'isRegistered' )->willReturn( $registered );
|
|
|
|
|
$user->method( 'matchEditToken' )->willReturn( $matchToken );
|
|
|
|
|
return $user;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-04 15:55:20 +00:00
|
|
|
public function provideUserCanPreview() {
|
|
|
|
|
yield 'all good' => [
|
2021-08-05 06:54:11 +00:00
|
|
|
'performer' => $this->mockUserAuthorityWithPermissions(
|
|
|
|
|
$this->mockUser( true, true ),
|
|
|
|
|
[ 'edit' ]
|
|
|
|
|
),
|
2021-03-04 15:55:20 +00:00
|
|
|
'request' => new FauxRequest( [ 'action' => 'submit' ], true ),
|
|
|
|
|
true
|
|
|
|
|
];
|
|
|
|
|
yield 'get request' => [
|
2021-08-05 06:54:11 +00:00
|
|
|
'performer' => $this->mockUserAuthorityWithPermissions(
|
|
|
|
|
$this->mockUser( true, true ),
|
|
|
|
|
[ 'edit' ]
|
|
|
|
|
),
|
2021-03-04 15:55:20 +00:00
|
|
|
'request' => new FauxRequest( [ 'action' => 'submit' ], false ),
|
|
|
|
|
false
|
|
|
|
|
];
|
|
|
|
|
yield 'not a submit action' => [
|
2021-08-05 06:54:11 +00:00
|
|
|
'performer' => $this->mockUserAuthorityWithPermissions(
|
|
|
|
|
$this->mockUser( true, true ),
|
|
|
|
|
[ 'edit' ]
|
|
|
|
|
),
|
2021-03-04 15:55:20 +00:00
|
|
|
'request' => new FauxRequest( [ 'action' => 'something' ], true ),
|
|
|
|
|
false
|
|
|
|
|
];
|
|
|
|
|
yield 'anon can not' => [
|
2021-08-05 06:54:11 +00:00
|
|
|
'performer' => $this->mockUserAuthorityWithPermissions(
|
|
|
|
|
$this->mockUser( false, true ),
|
|
|
|
|
[ 'edit' ]
|
|
|
|
|
),
|
2021-03-04 15:55:20 +00:00
|
|
|
'request' => new FauxRequest( [ 'action' => 'submit' ], true ),
|
|
|
|
|
false
|
|
|
|
|
];
|
|
|
|
|
yield 'token not match' => [
|
2021-08-05 06:54:11 +00:00
|
|
|
'performer' => $this->mockUserAuthorityWithPermissions(
|
|
|
|
|
$this->mockUser( true, false ),
|
|
|
|
|
[ 'edit' ]
|
|
|
|
|
),
|
2021-03-04 15:55:20 +00:00
|
|
|
'request' => new FauxRequest( [ 'action' => 'submit' ], true ),
|
|
|
|
|
false
|
|
|
|
|
];
|
|
|
|
|
yield 'no permission' => [
|
2021-08-05 06:54:11 +00:00
|
|
|
'performer' => $this->mockUserAuthorityWithoutPermissions(
|
|
|
|
|
$this->mockUser( true, true ),
|
|
|
|
|
[ 'edit' ]
|
|
|
|
|
),
|
2021-03-04 15:55:20 +00:00
|
|
|
'request' => new FauxRequest( [ 'action' => 'submit' ], true ),
|
|
|
|
|
false
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideUserCanPreview
|
|
|
|
|
*/
|
2021-08-05 06:54:11 +00:00
|
|
|
public function testUserCanPreview( Authority $performer, WebRequest $request, bool $expected ) {
|
2021-03-04 15:55:20 +00:00
|
|
|
$op = $this->newInstance( [], $request, null, $performer );
|
|
|
|
|
$this->assertSame( $expected, $op->userCanPreview() );
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-30 14:25:17 +00:00
|
|
|
private function newInstance(
|
|
|
|
|
array $config = [],
|
|
|
|
|
WebRequest $request = null,
|
2021-03-04 15:55:20 +00:00
|
|
|
$option = null,
|
|
|
|
|
Authority $performer = null
|
2021-07-22 03:11:47 +00:00
|
|
|
): OutputPage {
|
2017-01-24 17:30:33 +00:00
|
|
|
$context = new RequestContext();
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$context->setConfig( new MultiConfig( [
|
|
|
|
|
new HashConfig( $config + [
|
2023-06-19 19:21:47 +00:00
|
|
|
MainConfigNames::AppleTouchIcon => false,
|
|
|
|
|
MainConfigNames::EnableCanonicalServerLink => false,
|
|
|
|
|
MainConfigNames::Favicon => false,
|
|
|
|
|
MainConfigNames::Feed => false,
|
|
|
|
|
MainConfigNames::LanguageCode => false,
|
|
|
|
|
MainConfigNames::ReferrerPolicy => false,
|
|
|
|
|
MainConfigNames::RightsPage => false,
|
|
|
|
|
MainConfigNames::RightsUrl => false,
|
|
|
|
|
MainConfigNames::UniversalEditButton => false,
|
2018-07-23 18:26:32 +00:00
|
|
|
] ),
|
2023-03-07 20:06:23 +00:00
|
|
|
$this->getServiceContainer()->getMainConfig(),
|
2017-01-24 17:30:33 +00:00
|
|
|
] ) );
|
|
|
|
|
|
2019-09-30 14:25:17 +00:00
|
|
|
if ( $option !== 'notitle' ) {
|
2022-09-23 19:53:11 +00:00
|
|
|
$context->setTitle( Title::makeTitle( NS_MAIN, 'My test page' ) );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
2018-07-23 18:26:32 +00:00
|
|
|
|
|
|
|
|
if ( $request ) {
|
|
|
|
|
$context->setRequest( $request );
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-04 15:55:20 +00:00
|
|
|
if ( $performer ) {
|
|
|
|
|
$context->setAuthority( $performer );
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-24 17:30:33 +00:00
|
|
|
return new OutputPage( $context );
|
|
|
|
|
}
|
2013-01-14 03:26:15 +00:00
|
|
|
}
|