2013-01-14 03:26:15 +00:00
|
|
|
<?php
|
|
|
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
class OutputPageTest extends MediaWikiTestCase {
|
|
|
|
|
const SCREEN_MEDIA_QUERY = 'screen and (min-width: 982px)';
|
|
|
|
|
const SCREEN_ONLY_MEDIA_QUERY = 'only screen and (min-width: 982px)';
|
|
|
|
|
|
2018-09-25 14:31:57 +00:00
|
|
|
// Ensure that we don't affect the global ResourceLoader state.
|
|
|
|
|
protected function setUp() {
|
|
|
|
|
parent::setUp();
|
|
|
|
|
ResourceLoader::clearCache();
|
|
|
|
|
}
|
|
|
|
|
protected function tearDown() {
|
|
|
|
|
parent::tearDown();
|
|
|
|
|
ResourceLoader::clearCache();
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideRedirect
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::__construct
|
|
|
|
|
* @covers OutputPage::redirect
|
|
|
|
|
* @covers OutputPage::getRedirect
|
|
|
|
|
*/
|
|
|
|
|
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 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideRedirect() {
|
|
|
|
|
return [
|
|
|
|
|
[ 'http://example.com' ],
|
|
|
|
|
[ 'http://example.com', '400' ],
|
|
|
|
|
[ 'http://example.com', 'squirrels!!!' ],
|
|
|
|
|
[ "a\nb" ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setCopyrightUrl
|
|
|
|
|
* @covers OutputPage::getHeadLinksArray
|
|
|
|
|
*/
|
|
|
|
|
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']
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @todo How to test setStatusCode?
|
|
|
|
|
|
2017-01-24 17:30:33 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addMeta
|
|
|
|
|
* @covers OutputPage::getMetaTags
|
|
|
|
|
* @covers OutputPage::getHeadLinksArray
|
|
|
|
|
*/
|
|
|
|
|
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();
|
2017-01-24 17:30:33 +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 );
|
2017-01-24 12:01:47 +00:00
|
|
|
$this->assertContains( '<meta property="og:title" content="Ta-duh"/>', $links );
|
2017-01-24 17:30:33 +00:00
|
|
|
$this->assertArrayNotHasKey( 'meta-robots', $links );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* @covers OutputPage::addLink
|
|
|
|
|
* @covers OutputPage::getLinkTags
|
2017-01-24 17:30:33 +00:00
|
|
|
* @covers OutputPage::getHeadLinksArray
|
|
|
|
|
*/
|
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
|
|
|
}
|
|
|
|
|
|
2013-01-14 03:26:15 +00:00
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* @covers OutputPage::setCanonicalUrl
|
|
|
|
|
* @covers OutputPage::getCanonicalUrl
|
|
|
|
|
* @covers OutputPage::getHeadLinksArray
|
|
|
|
|
*/
|
|
|
|
|
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 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addScript
|
|
|
|
|
*/
|
|
|
|
|
public function testAddScript() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addScript( 'some random string' );
|
|
|
|
|
|
|
|
|
|
$this->assertContains( "\nsome random string\n", "\n" . $op->getBottomScripts() . "\n" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addScriptFile
|
|
|
|
|
*/
|
|
|
|
|
public function testAddScriptFile() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addScriptFile( '/somescript.js' );
|
|
|
|
|
$op->addScriptFile( '//example.com/somescript.js' );
|
|
|
|
|
|
|
|
|
|
$this->assertContains(
|
|
|
|
|
"\n" . Html::linkedScript( '/somescript.js', $op->getCSPNonce() ) .
|
|
|
|
|
Html::linkedScript( '//example.com/somescript.js', $op->getCSPNonce() ) . "\n",
|
|
|
|
|
"\n" . $op->getBottomScripts() . "\n"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test that addScriptFile() throws due to deprecation.
|
2013-01-14 03:26:15 +00:00
|
|
|
*
|
2018-07-23 18:26:32 +00:00
|
|
|
* @covers OutputPage::addScriptFile
|
|
|
|
|
*/
|
|
|
|
|
public function testAddDeprecatedScriptFileWarning() {
|
|
|
|
|
$this->setExpectedException( PHPUnit_Framework_Error_Deprecated::class,
|
|
|
|
|
'Use of OutputPage::addScriptFile was deprecated in MediaWiki 1.24.' );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addScriptFile( 'ignored-script.js' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test the actual behavior of the method (in the case where it doesn't throw, e.g., in
|
2018-09-25 14:21:24 +00:00
|
|
|
* production).
|
2013-01-14 03:26:15 +00:00
|
|
|
*
|
2018-07-23 18:26:32 +00:00
|
|
|
* @covers OutputPage::addScriptFile
|
|
|
|
|
*/
|
|
|
|
|
public function testAddDeprecatedScriptFileNoOp() {
|
2018-09-25 14:21:24 +00:00
|
|
|
$this->hideDeprecated( 'OutputPage::addScriptFile' );
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addScriptFile( 'ignored-script.js' );
|
|
|
|
|
|
|
|
|
|
$this->assertNotContains( 'ignored-script.js', '' . $op->getBottomScripts() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addInlineScript
|
|
|
|
|
*/
|
|
|
|
|
public function testAddInlineScript() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addInlineScript( 'let foo = "bar";' );
|
|
|
|
|
$op->addInlineScript( 'alert( foo );' );
|
|
|
|
|
|
|
|
|
|
$this->assertContains(
|
|
|
|
|
"\n" . Html::inlineScript( "\nlet foo = \"bar\";\n", $op->getCSPNonce() ) . "\n" .
|
|
|
|
|
Html::inlineScript( "\nalert( foo );\n", $op->getCSPNonce() ) . "\n",
|
|
|
|
|
"\n" . $op->getBottomScripts() . "\n"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @todo How to test filterModules(), warnModuleTargetFilter(), getModules(), etc.?
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::getTarget
|
|
|
|
|
* @covers OutputPage::setTarget
|
|
|
|
|
*/
|
|
|
|
|
public function testSetTarget() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->setTarget( 'foo' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( 'foo', $op->getTarget() );
|
|
|
|
|
// @todo What else? Test some actual effect?
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @todo How to test addContentOverride(Callback)?
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::getHeadItemsArray
|
|
|
|
|
* @covers OutputPage::addHeadItem
|
|
|
|
|
* @covers OutputPage::addHeadItems
|
|
|
|
|
* @covers OutputPage::hasHeadItem
|
|
|
|
|
*/
|
|
|
|
|
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' ) );
|
|
|
|
|
|
|
|
|
|
$this->assertContains( "\nq\n<d>&\ng\nx\n",
|
|
|
|
|
'' . $op->headElement( $op->getContext()->getSkin() ) );
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-25 18:41:42 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::getHeadItemsArray
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
2018-08-02 18:42:17 +00:00
|
|
|
* @covers OutputPage::addParserOutput
|
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' ) );
|
|
|
|
|
|
|
|
|
|
$this->assertContains( "\nq\n<d>&\ng\nx\n",
|
|
|
|
|
'' . $op->headElement( $op->getContext()->getSkin() ) );
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addBodyClasses
|
|
|
|
|
*/
|
|
|
|
|
public function testAddBodyClasses() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addBodyClasses( 'a' );
|
|
|
|
|
$op->addBodyClasses( 'mediawiki' );
|
|
|
|
|
$op->addBodyClasses( 'b c' );
|
|
|
|
|
$op->addBodyClasses( [ 'd', 'e' ] );
|
|
|
|
|
$op->addBodyClasses( 'a' );
|
|
|
|
|
|
|
|
|
|
$this->assertContains( '"a mediawiki b c d e ltr',
|
|
|
|
|
'' . $op->headElement( $op->getContext()->getSkin() ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setArticleBodyOnly
|
|
|
|
|
* @covers OutputPage::getArticleBodyOnly
|
|
|
|
|
*/
|
|
|
|
|
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 ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setProperty
|
|
|
|
|
* @covers OutputPage::getProperty
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
* @covers OutputPage::checkLastModified
|
|
|
|
|
* @covers OutputPage::getCdnCacheEpoch
|
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
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
if ( !isset( $config['CacheEpoch'] ) ) {
|
|
|
|
|
// Make sure it's not too recent
|
|
|
|
|
$config['CacheEpoch'] = '20000101000000';
|
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 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Avoid a complaint about not being able to disable compression
|
|
|
|
|
Wikimedia\suppressWarnings();
|
|
|
|
|
try {
|
|
|
|
|
$this->assertEquals( $expected, $op->checkLastModified( $timestamp ) );
|
|
|
|
|
} finally {
|
|
|
|
|
Wikimedia\restoreWarnings();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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, [],
|
|
|
|
|
function ( $op ) {
|
|
|
|
|
$op->getContext()->setUser( $this->getTestUser()->getUser() );
|
|
|
|
|
} ],
|
|
|
|
|
'After Squid expiry' =>
|
|
|
|
|
[ $lastModified, $lastModified, false,
|
|
|
|
|
[ 'UseSquid' => true, 'SquidMaxage' => 3599 ] ],
|
|
|
|
|
'Hook allows cache use' =>
|
|
|
|
|
[ $lastModified + 1, $lastModified, true, [],
|
|
|
|
|
function ( $op, $that ) {
|
|
|
|
|
$that->setTemporaryHook( 'OutputPageCheckLastModified',
|
|
|
|
|
function ( &$modifiedTimes ) {
|
|
|
|
|
$modifiedTimes = [ 1 ];
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
} ],
|
|
|
|
|
'Hooks prohibits cache use' =>
|
|
|
|
|
[ $lastModified, $lastModified, false, [],
|
|
|
|
|
function ( $op, $that ) {
|
|
|
|
|
$that->setTemporaryHook( 'OutputPageCheckLastModified',
|
|
|
|
|
function ( &$modifiedTimes ) {
|
|
|
|
|
$modifiedTimes = [ max( $modifiedTimes ) + 1 ];
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
} ],
|
|
|
|
|
];
|
2013-01-14 03:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* @dataProvider provideCdnCacheEpoch
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::getCdnCacheEpoch
|
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?
|
|
|
|
|
|
2018-04-20 15:00:32 +00:00
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* @covers OutputPage::setRobotPolicy
|
|
|
|
|
* @covers OutputPage::getHeadLinksArray
|
2018-04-20 15:00:32 +00:00
|
|
|
*/
|
2018-07-23 18:26:32 +00:00
|
|
|
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();
|
|
|
|
|
$this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setIndexPolicy
|
|
|
|
|
* @covers OutputPage::setFollowPolicy
|
|
|
|
|
* @covers OutputPage::getHeadLinksArray
|
|
|
|
|
*/
|
|
|
|
|
public function testSetIndexFollowPolicies() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->setIndexPolicy( 'noindex' );
|
|
|
|
|
$op->setFollowPolicy( 'nofollow' );
|
|
|
|
|
|
|
|
|
|
$links = $op->getHeadLinksArray();
|
|
|
|
|
$this->assertContains( '<meta name="robots" content="noindex,nofollow"/>', $links );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
private static function getMsgText( $op, ...$msgParams ) {
|
|
|
|
|
return $op->msg( ...$msgParams )->inContentLanguage()->text();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setHTMLTitle
|
|
|
|
|
* @covers OutputPage::getHTMLTitle
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2013-01-14 03:26:15 +00:00
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* @covers OutputPage::setRedirectedFrom
|
2013-01-14 03:26:15 +00:00
|
|
|
*/
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testSetRedirectedFrom() {
|
|
|
|
|
$op = $this->newInstance();
|
2013-05-11 19:05:43 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$op->setRedirectedFrom( Title::newFromText( 'Talk:Some page' ) );
|
|
|
|
|
$this->assertSame( 'Talk:Some_page', $op->getJSVars()['wgRedirectedFrom'] );
|
|
|
|
|
}
|
2013-01-14 03:26:15 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setPageTitle
|
|
|
|
|
* @covers OutputPage::getPageTitle
|
|
|
|
|
*/
|
|
|
|
|
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() );
|
|
|
|
|
|
|
|
|
|
// Test set to text with good and bad HTML. We don't try to be comprehensive here, that
|
|
|
|
|
// belongs in Sanitizer tests.
|
|
|
|
|
$op->setPageTitle( '<script>a</script>&<i>b</i>' );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( '<script>a</script>&<i>b</i>', $op->getPageTitle() );
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
$this->getMsgText( $op, 'pagetitle', '<script>a</script>&b' ),
|
|
|
|
|
$op->getHTMLTitle()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Test set to message
|
|
|
|
|
$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() );
|
2013-01-14 03:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* @covers OutputPage::setTitle
|
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() );
|
|
|
|
|
|
|
|
|
|
$op->setTitle( Title::newFromText( 'Another test page' ) );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( 'Another test page', $op->getTitle()->getPrefixedText() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setSubtitle
|
|
|
|
|
* @covers OutputPage::clearSubtitle
|
|
|
|
|
* @covers OutputPage::addSubtitle
|
|
|
|
|
* @covers OutputPage::getSubtitle
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::buildBacklinkSubtitle
|
|
|
|
|
*/
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$title = Title::newFromText( $titles[0] );
|
|
|
|
|
$query = $queries[0];
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$this->editPage( 'Page 1', '' );
|
|
|
|
|
$this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
|
|
|
|
|
|
|
|
|
|
$str = OutputPage::buildBacklinkSubtitle( $title, $query )->text();
|
|
|
|
|
|
|
|
|
|
foreach ( $contains as $substr ) {
|
|
|
|
|
$this->assertContains( $substr, $str );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $notContains as $substr ) {
|
|
|
|
|
$this->assertNotContains( $substr, $str );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideBacklinkSubtitle
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::addBacklinkSubtitle
|
|
|
|
|
* @covers OutputPage::getSubtitle
|
|
|
|
|
*/
|
2018-07-25 18:41:42 +00:00
|
|
|
public function testAddBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
|
2018-07-23 18:26:32 +00:00
|
|
|
$this->editPage( 'Page 1', '' );
|
|
|
|
|
$this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance();
|
2018-07-25 18:41:42 +00:00
|
|
|
foreach ( $titles as $i => $unused ) {
|
|
|
|
|
$op->addBacklinkSubtitle( Title::newFromText( $titles[$i] ), $queries[$i] );
|
|
|
|
|
}
|
2018-07-23 18:26:32 +00:00
|
|
|
|
|
|
|
|
$str = $op->getSubtitle();
|
|
|
|
|
|
|
|
|
|
foreach ( $contains as $substr ) {
|
|
|
|
|
$this->assertContains( $substr, $str );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $notContains as $substr ) {
|
|
|
|
|
$this->assertNotContains( $substr, $str );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideBacklinkSubtitle() {
|
|
|
|
|
return [
|
2018-07-25 18:41:42 +00:00
|
|
|
[
|
|
|
|
|
[ 'Page 1' ],
|
|
|
|
|
[ [] ],
|
|
|
|
|
[ 'Page 1' ],
|
|
|
|
|
[ 'redirect', 'Page 2' ],
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ 'Page 2' ],
|
|
|
|
|
[ [] ],
|
|
|
|
|
[ 'redirect=no' ],
|
|
|
|
|
[ 'Page 1' ],
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ 'Page 1' ],
|
|
|
|
|
[ [ 'action' => 'edit' ] ],
|
|
|
|
|
[ 'action=edit' ],
|
|
|
|
|
[],
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[ 'Page 1', 'Page 2' ],
|
|
|
|
|
[ [], [] ],
|
|
|
|
|
[ '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
|
|
|
* @covers OutputPage::setPrintable
|
|
|
|
|
* @covers OutputPage::isPrintable
|
|
|
|
|
*/
|
|
|
|
|
public function testPrintable() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertFalse( $op->isPrintable() );
|
|
|
|
|
|
|
|
|
|
$op->setPrintable();
|
|
|
|
|
|
|
|
|
|
$this->assertTrue( $op->isPrintable() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::disable
|
|
|
|
|
* @covers OutputPage::isDisabled
|
|
|
|
|
*/
|
|
|
|
|
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 ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::showNewSectionLink
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
2018-08-02 18:42:17 +00:00
|
|
|
* @covers OutputPage::addParserOutput
|
2018-07-25 18:41:42 +00:00
|
|
|
*/
|
|
|
|
|
public function testShowNewSectionLink() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertFalse( $op->showNewSectionLink() );
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
$pOut1 = $this->createParserOutputStub( 'getNewSection', true );
|
|
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertTrue( $op->showNewSectionLink() );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
$pOut2 = $this->createParserOutputStub( 'getNewSection', false );
|
|
|
|
|
$op->addParserOutput( $pOut2 );
|
|
|
|
|
$this->assertFalse( $op->showNewSectionLink() );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::forceHideNewSectionLink
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
2018-08-02 18:42:17 +00:00
|
|
|
* @covers OutputPage::addParserOutput
|
2018-07-25 18:41:42 +00:00
|
|
|
*/
|
|
|
|
|
public function testForceHideNewSectionLink() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$this->assertFalse( $op->forceHideNewSectionLink() );
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
$pOut1 = $this->createParserOutputStub( 'getHideNewSection', true );
|
|
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertTrue( $op->forceHideNewSectionLink() );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
$pOut2 = $this->createParserOutputStub( 'getHideNewSection', false );
|
|
|
|
|
$op->addParserOutput( $pOut2 );
|
|
|
|
|
$this->assertFalse( $op->forceHideNewSectionLink() );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setSyndicated
|
|
|
|
|
* @covers OutputPage::isSyndicated
|
|
|
|
|
*/
|
|
|
|
|
public function testSetSyndicated() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertFalse( $op->isSyndicated() );
|
|
|
|
|
|
|
|
|
|
$op->setSyndicated();
|
|
|
|
|
$this->assertTrue( $op->isSyndicated() );
|
|
|
|
|
|
|
|
|
|
$op->setSyndicated( false );
|
|
|
|
|
$this->assertFalse( $op->isSyndicated() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::isSyndicated
|
|
|
|
|
* @covers OutputPage::setFeedAppendQuery
|
|
|
|
|
* @covers OutputPage::addFeedLink
|
|
|
|
|
* @covers OutputPage::getSyndicationLinks()
|
|
|
|
|
*/
|
|
|
|
|
public function testFeedLinks() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$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() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setArticleFlag
|
|
|
|
|
* @covers OutputPage::isArticle
|
|
|
|
|
* @covers OutputPage::setArticleRelated
|
|
|
|
|
* @covers OutputPage::isArticleRelated
|
|
|
|
|
*/
|
|
|
|
|
function testArticleFlags() {
|
|
|
|
|
$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() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addLanguageLinks
|
|
|
|
|
* @covers OutputPage::setLanguageLinks
|
|
|
|
|
* @covers OutputPage::getLanguageLinks
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
2018-08-02 18:42:17 +00:00
|
|
|
* @covers OutputPage::addParserOutput
|
2018-07-25 18:41:42 +00:00
|
|
|
*/
|
|
|
|
|
function testLanguageLinks() {
|
|
|
|
|
$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
|
|
|
|
|
*
|
2018-07-23 18:26:32 +00:00
|
|
|
* @covers OutputPage::addCategoryLinks
|
|
|
|
|
* @covers OutputPage::getCategories
|
2018-07-25 18:41:42 +00:00
|
|
|
* @covers OutputPage::getCategoryLinks
|
|
|
|
|
*
|
|
|
|
|
* @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-01-30 20:24:06 +00:00
|
|
|
* @param callable $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(
|
|
|
|
|
array $args, array $fakeResults, callable $variantLinkCallback = null,
|
|
|
|
|
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
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::addCategoryLinks
|
|
|
|
|
* @covers OutputPage::getCategories
|
|
|
|
|
* @covers OutputPage::getCategoryLinks
|
|
|
|
|
*/
|
|
|
|
|
public function testAddCategoryLinksOneByOne(
|
|
|
|
|
array $args, array $fakeResults, callable $variantLinkCallback = null,
|
|
|
|
|
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
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::setCategoryLinks
|
|
|
|
|
* @covers OutputPage::getCategories
|
|
|
|
|
* @covers OutputPage::getCategoryLinks
|
|
|
|
|
*/
|
|
|
|
|
public function testSetCategoryLinks(
|
|
|
|
|
array $args, array $fakeResults, callable $variantLinkCallback = null,
|
|
|
|
|
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 );
|
|
|
|
|
$expectedCats = array_merge( $expectedHidden, $expectedNormalCats );
|
|
|
|
|
|
|
|
|
|
$this->doCategoryAsserts( $op, $expectedNormalCats, $expectedHidden );
|
|
|
|
|
$this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideGetCategories
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
2018-08-02 18:42:17 +00:00
|
|
|
* @covers OutputPage::addParserOutput
|
2018-07-25 18:41:42 +00:00
|
|
|
* @covers OutputPage::getCategories
|
|
|
|
|
* @covers OutputPage::getCategoryLinks
|
|
|
|
|
*/
|
|
|
|
|
public function testParserOutputCategoryLinks(
|
|
|
|
|
array $args, array $fakeResults, callable $variantLinkCallback = null,
|
|
|
|
|
array $expectedNormal, array $expectedHidden
|
|
|
|
|
) {
|
|
|
|
|
$expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'pout' );
|
|
|
|
|
$expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'pout' );
|
|
|
|
|
|
|
|
|
|
$op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
|
|
|
|
|
|
|
|
|
|
$stubPO = $this->createParserOutputStub( 'getCategories', $args );
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
) : OutputPage {
|
|
|
|
|
$this->setMwGlobals( 'wgUsePigLatinVariant', true );
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = $this->getMockBuilder( OutputPage::class )
|
|
|
|
|
->setConstructorArgs( [ new RequestContext() ] )
|
2018-09-27 21:36:19 +00:00
|
|
|
->setMethods( [ 'addCategoryLinksToLBAndGetResult', 'getTitle' ] )
|
2018-07-23 18:26:32 +00:00
|
|
|
->getMock();
|
2018-07-25 18:41:42 +00:00
|
|
|
|
2018-09-27 21:36:19 +00:00
|
|
|
$title = Title::newFromText( 'My test page' );
|
|
|
|
|
$op->expects( $this->any() )
|
|
|
|
|
->method( 'getTitle' )
|
|
|
|
|
->will( $this->returnValue( $title ) );
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$op->expects( $this->any() )
|
|
|
|
|
->method( 'addCategoryLinksToLBAndGetResult' )
|
2018-07-25 18:41:42 +00:00
|
|
|
->will( $this->returnCallback( function ( array $categories ) use ( $fakeResults ) {
|
|
|
|
|
$return = [];
|
|
|
|
|
foreach ( $categories as $category => $unused ) {
|
|
|
|
|
if ( isset( $fakeResults[$category] ) ) {
|
|
|
|
|
$return[] = $fakeResults[$category];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return new FakeResultWrapper( $return );
|
|
|
|
|
} ) );
|
|
|
|
|
|
|
|
|
|
if ( $variantLinkCallback ) {
|
|
|
|
|
$mockContLang = $this->getMockBuilder( Language::class )
|
|
|
|
|
->setConstructorArgs( [ 'en' ] )
|
|
|
|
|
->setMethods( [ 'findVariantLink' ] )
|
|
|
|
|
->getMock();
|
|
|
|
|
$mockContLang->expects( $this->any() )
|
|
|
|
|
->method( 'findVariantLink' )
|
|
|
|
|
->will( $this->returnCallback( $variantLinkCallback ) );
|
|
|
|
|
$this->setContentLang( $mockContLang );
|
|
|
|
|
}
|
2013-01-14 03:26:15 +00:00
|
|
|
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertSame( [], $op->getCategories() );
|
|
|
|
|
|
|
|
|
|
return $op;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function doCategoryAsserts( $op, $expectedNormal, $expectedHidden ) {
|
|
|
|
|
$this->assertSame( array_merge( $expectedHidden, $expectedNormal ), $op->getCategories() );
|
|
|
|
|
$this->assertSame( $expectedNormal, $op->getCategories( 'normal' ) );
|
|
|
|
|
$this->assertSame( $expectedHidden, $op->getCategories( 'hidden' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden ) {
|
|
|
|
|
$catLinks = $op->getCategoryLinks();
|
|
|
|
|
$this->assertSame( (bool)$expectedNormal + (bool)$expectedHidden, count( $catLinks ) );
|
|
|
|
|
if ( $expectedNormal ) {
|
|
|
|
|
$this->assertSame( count( $expectedNormal ), count( $catLinks['normal'] ) );
|
|
|
|
|
}
|
|
|
|
|
if ( $expectedHidden ) {
|
|
|
|
|
$this->assertSame( count( $expectedHidden ), count( $catLinks['hidden'] ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $expectedNormal as $i => $name ) {
|
|
|
|
|
$this->assertContains( $name, $catLinks['normal'][$i] );
|
|
|
|
|
}
|
|
|
|
|
foreach ( $expectedHidden as $i => $name ) {
|
|
|
|
|
$this->assertContains( $name, $catLinks['hidden'][$i] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideGetCategories() {
|
|
|
|
|
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' ] ],
|
|
|
|
|
function ( &$link, &$title ) {
|
|
|
|
|
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' ] ],
|
|
|
|
|
[],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::getCategories
|
|
|
|
|
*/
|
|
|
|
|
public function testGetCategoriesInvalid() {
|
|
|
|
|
$this->setExpectedException( InvalidArgumentException::class,
|
|
|
|
|
'Invalid category type given: hiddne' );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->getCategories( 'hiddne' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @todo Should we test addCategoryLinksToLBAndGetResult? If so, how? Insert some test rows in
|
|
|
|
|
// the DB?
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setIndicators
|
|
|
|
|
* @covers OutputPage::getIndicators
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
2018-08-02 18:42:17 +00:00
|
|
|
* @covers OutputPage::addParserOutput
|
2018-07-25 18:41:42 +00:00
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
$pOut1 = $this->createParserOutputStub( 'getIndicators', [ 'c' => 'u', 'd' => 'v' ] );
|
|
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
|
|
|
|
|
$op->getIndicators() );
|
2018-08-02 18:42:17 +00:00
|
|
|
|
|
|
|
|
// Test with addParserOutput
|
|
|
|
|
$pOut2 = $this->createParserOutputStub( 'getIndicators', [ 'a' => '!!!' ] );
|
|
|
|
|
$op->addParserOutput( $pOut2 );
|
|
|
|
|
$this->assertSame( [ 'a' => '!!!', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
|
|
|
|
|
$op->getIndicators() );
|
2018-07-25 18:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addHelpLink
|
|
|
|
|
* @covers OutputPage::getIndicators
|
|
|
|
|
*/
|
|
|
|
|
public function testAddHelpLink() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
$op->addHelpLink( 'Manual:PHP unit testing' );
|
|
|
|
|
$indicators = $op->getIndicators();
|
|
|
|
|
$this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
|
|
|
|
|
$this->assertContains( 'Manual:PHP_unit_testing', $indicators['mw-helplink'] );
|
|
|
|
|
|
|
|
|
|
$op->addHelpLink( 'https://phpunit.de', true );
|
|
|
|
|
$indicators = $op->getIndicators();
|
|
|
|
|
$this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
|
|
|
|
|
$this->assertContains( 'https://phpunit.de', $indicators['mw-helplink'] );
|
|
|
|
|
$this->assertNotContains( 'mediawiki', $indicators['mw-helplink'] );
|
|
|
|
|
$this->assertNotContains( 'Manual:PHP', $indicators['mw-helplink'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::prependHTML
|
|
|
|
|
* @covers OutputPage::addHTML
|
|
|
|
|
* @covers OutputPage::addElement
|
|
|
|
|
* @covers OutputPage::clearHTML
|
|
|
|
|
* @covers OutputPage::getHTML
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
* @covers OutputPage::setRevisionId
|
|
|
|
|
* @covers OutputPage::getRevisionId
|
|
|
|
|
*/
|
|
|
|
|
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() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideRevisionId() {
|
|
|
|
|
return [
|
|
|
|
|
[ null, null ],
|
|
|
|
|
[ 7, 7 ],
|
|
|
|
|
[ -1, -1 ],
|
|
|
|
|
[ 3.2, 3 ],
|
|
|
|
|
[ '0', 0 ],
|
|
|
|
|
[ '32% finished', 32 ],
|
|
|
|
|
[ false, 0 ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setRevisionTimestamp
|
|
|
|
|
* @covers OutputPage::getRevisionTimestamp
|
|
|
|
|
*/
|
|
|
|
|
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() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setFileVersion
|
|
|
|
|
* @covers OutputPage::getFileVersion
|
|
|
|
|
*/
|
|
|
|
|
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' );
|
|
|
|
|
|
|
|
|
|
$op->setFileVersion( $stubFile );
|
|
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
|
[ 'time' => '12211221123321', 'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' ],
|
|
|
|
|
$op->getFileVersion()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$stubMissingFile = $this->createMock( File::class );
|
|
|
|
|
$stubMissingFile->method( 'exists' )->willReturn( false );
|
|
|
|
|
|
|
|
|
|
$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, ... ]
|
|
|
|
|
*/
|
|
|
|
|
private function createParserOutputStub( ...$args ) {
|
|
|
|
|
if ( count( $args ) === 0 ) {
|
|
|
|
|
$retVals = [];
|
|
|
|
|
} elseif ( count( $args ) === 1 ) {
|
|
|
|
|
$retVals = $args[0];
|
|
|
|
|
} elseif ( count( $args ) === 2 ) {
|
|
|
|
|
$retVals = [ $args[0] => $args[1] ];
|
|
|
|
|
}
|
2018-07-25 18:41:42 +00:00
|
|
|
$pOut = $this->getMock( ParserOutput::class );
|
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 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$arrayReturningMethods = [
|
|
|
|
|
'getCategories',
|
|
|
|
|
'getFileSearchOptions',
|
|
|
|
|
'getHeadItems',
|
|
|
|
|
'getIndicators',
|
|
|
|
|
'getLanguageLinks',
|
|
|
|
|
'getOutputHooks',
|
|
|
|
|
'getTemplateIds',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ( $arrayReturningMethods as $method ) {
|
|
|
|
|
$pOut->method( $method )->willReturn( [] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $pOut;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::getTemplateIds
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
2018-08-02 18:42:17 +00:00
|
|
|
* @covers OutputPage::addParserOutput
|
2018-07-25 18:41:42 +00:00
|
|
|
*/
|
|
|
|
|
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() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::getFileSearchOptions
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
2018-08-02 18:42:17 +00:00
|
|
|
* @covers OutputPage::addParserOutput
|
2018-07-25 18:41:42 +00:00
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
* @covers OutputPage::addWikiText
|
2018-09-21 16:24:57 +00:00
|
|
|
* @covers OutputPage::addWikiTextAsInterface
|
2018-09-27 15:04:45 +00:00
|
|
|
* @covers OutputPage::wrapWikiTextAsInterface
|
2018-09-21 16:24:57 +00:00
|
|
|
* @covers OutputPage::addWikiTextAsContent
|
2018-07-25 18:41:42 +00:00
|
|
|
* @covers OutputPage::addWikiTextWithTitle
|
|
|
|
|
* @covers OutputPage::addWikiTextTitle
|
2018-09-25 13:06:12 +00:00
|
|
|
* @covers OutputPage::addWikiTextTidy
|
|
|
|
|
* @covers OutputPage::addWikiTextTitleTidy
|
2018-07-25 18:41:42 +00:00
|
|
|
* @covers OutputPage::getHTML
|
|
|
|
|
*/
|
|
|
|
|
public function testAddWikiText( $method, array $args, $expected ) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
|
2018-09-25 14:05:55 +00:00
|
|
|
$this->hideDeprecated( 'OutputPage::addWikiText' );
|
2018-09-21 17:16:42 +00:00
|
|
|
$this->hideDeprecated( 'OutputPage::addWikiTextTitle' );
|
|
|
|
|
$this->hideDeprecated( 'OutputPage::addWikiTextWithTitle' );
|
2018-09-26 17:22:38 +00:00
|
|
|
$this->hideDeprecated( 'OutputPage::addWikiTextTidy' );
|
|
|
|
|
$this->hideDeprecated( 'OutputPage::addWikiTextTitleTidy' );
|
2018-09-25 14:05:55 +00:00
|
|
|
$this->hideDeprecated( 'disabling tidy' );
|
|
|
|
|
|
2018-07-25 18:41:42 +00:00
|
|
|
if ( in_array(
|
|
|
|
|
$method,
|
|
|
|
|
[ 'addWikiTextWithTitle', 'addWikiTextTitleTidy', 'addWikiTextTitle' ]
|
|
|
|
|
) && count( $args ) >= 2 && $args[1] === null ) {
|
|
|
|
|
// Special placeholder because we can't get the actual title in the provider
|
|
|
|
|
$args[1] = $op->getTitle();
|
|
|
|
|
}
|
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() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideAddWikiText() {
|
|
|
|
|
$tests = [
|
|
|
|
|
'addWikiText' => [
|
2018-09-21 16:24:57 +00:00
|
|
|
// Not tidied; this API is deprecated.
|
2018-07-25 18:41:42 +00:00
|
|
|
'Simple wikitext' => [
|
|
|
|
|
[ "'''Bold'''" ],
|
|
|
|
|
"<p><b>Bold</b>\n</p>",
|
|
|
|
|
], 'List at start' => [
|
|
|
|
|
[ '* List' ],
|
|
|
|
|
"<ul><li>List</li></ul>\n",
|
|
|
|
|
], 'List not at start' => [
|
|
|
|
|
[ '* Not a list', false ],
|
|
|
|
|
'* Not a list',
|
|
|
|
|
], 'Non-interface' => [
|
|
|
|
|
[ "'''Bold'''", true, false ],
|
2018-09-25 13:06:12 +00:00
|
|
|
"<p><b>Bold</b>\n</p>",
|
2018-07-25 18:41:42 +00:00
|
|
|
], 'No section edit links' => [
|
|
|
|
|
[ '== Title ==' ],
|
|
|
|
|
"<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>\n",
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'addWikiTextWithTitle' => [
|
2018-09-21 16:24:57 +00:00
|
|
|
// Untidied; this API is deprecated
|
2018-07-25 18:41:42 +00:00
|
|
|
'With title at start' => [
|
|
|
|
|
[ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ) ],
|
2018-09-25 13:06:12 +00:00
|
|
|
"<ul><li>Some page</li></ul>\n",
|
|
|
|
|
], 'With title at start' => [
|
|
|
|
|
[ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ), false ],
|
|
|
|
|
"* Some page",
|
|
|
|
|
],
|
|
|
|
|
],
|
2018-09-21 16:24:57 +00:00
|
|
|
'addWikiTextAsInterface' => [
|
|
|
|
|
// Preferred interface: output is tidied
|
|
|
|
|
'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 ==' ],
|
|
|
|
|
"<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>\n",
|
|
|
|
|
], 'With title at start' => [
|
|
|
|
|
[ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
|
|
|
|
|
"<ul><li>Some page</li></ul>\n",
|
|
|
|
|
], 'With title at start' => [
|
|
|
|
|
[ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
|
|
|
|
|
"<p>* Some page</p>",
|
|
|
|
|
], 'Untidy input' => [
|
|
|
|
|
[ '<b>{{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
|
|
|
|
|
"<p><b>Some page\n</b></p>",
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'addWikiTextAsContent' => [
|
|
|
|
|
// Preferred interface: output is tidied
|
2018-09-25 13:06:12 +00:00
|
|
|
'SpecialNewimages' => [
|
|
|
|
|
[ "<p lang='en' dir='ltr'>\nMy message" ],
|
|
|
|
|
'<p lang="en" dir="ltr">' . "\nMy message\n</p>"
|
|
|
|
|
], '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' => [
|
|
|
|
|
[ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
|
2018-09-25 13:06:12 +00:00
|
|
|
"<ul><li>Some page</li></ul>\n",
|
2018-07-25 18:41:42 +00:00
|
|
|
], 'With title at start' => [
|
2018-09-21 16:24:57 +00:00
|
|
|
[ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
|
2018-09-25 13:06:12 +00:00
|
|
|
"<p>* Some page</p>",
|
|
|
|
|
], 'EditPage' => [
|
2018-09-21 16:24:57 +00:00
|
|
|
[ "<div class='mw-editintro'>{{PAGENAME}}", true, Title::newFromText( 'Talk:Some page' ) ],
|
2018-09-25 13:06:12 +00:00
|
|
|
'<div class="mw-editintro">' . "Some page\n</div>"
|
2018-07-25 18:41:42 +00:00
|
|
|
],
|
|
|
|
|
],
|
2018-09-27 15:04:45 +00:00
|
|
|
'wrapWikiTextAsInterface' => [
|
|
|
|
|
'Simple' => [
|
|
|
|
|
[ 'wrapperClass', 'text' ],
|
|
|
|
|
"<div class=\"wrapperClass\"><p>text\n</p></div>"
|
|
|
|
|
], 'Spurious </div>' => [
|
|
|
|
|
[ 'wrapperClass', 'text</div><div>more' ],
|
|
|
|
|
"<div class=\"wrapperClass\"><p>text</p><div>more\n</div></div>"
|
|
|
|
|
], 'Extra newlines would break <p> wrappers' => [
|
|
|
|
|
[ 'two classes', "1\n\n2\n\n3" ],
|
|
|
|
|
"<div class=\"two classes\"><p>1\n</p><p>2\n</p><p>3\n</p></div>"
|
|
|
|
|
], 'Other unclosed tags' => [
|
|
|
|
|
[ 'error', 'a<b>c<i>d' ],
|
|
|
|
|
"<div class=\"error\"><p>a<b>c<i>d\n</i></b></p></div>"
|
|
|
|
|
],
|
|
|
|
|
],
|
2018-07-25 18:41:42 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Test all the others on addWikiTextTitle as well
|
|
|
|
|
foreach ( $tests['addWikiText'] as $key => $val ) {
|
|
|
|
|
$args = [ $val[0][0], null, $val[0][1] ?? true, false, $val[0][2] ?? true ];
|
|
|
|
|
$tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
|
|
|
|
|
array_merge( [ $args ], array_slice( $val, 1 ) );
|
|
|
|
|
}
|
|
|
|
|
foreach ( $tests['addWikiTextWithTitle'] as $key => $val ) {
|
|
|
|
|
$args = [ $val[0][0], $val[0][1], $val[0][2] ?? true ];
|
|
|
|
|
$tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
|
|
|
|
|
array_merge( [ $args ], array_slice( $val, 1 ) );
|
|
|
|
|
}
|
2018-09-21 16:24:57 +00:00
|
|
|
foreach ( $tests['addWikiTextAsInterface'] as $key => $val ) {
|
|
|
|
|
$args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, true ];
|
2018-09-25 13:06:12 +00:00
|
|
|
$tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
|
|
|
|
|
array_merge( [ $args ], array_slice( $val, 1 ) );
|
|
|
|
|
}
|
2018-09-21 16:24:57 +00:00
|
|
|
foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
|
|
|
|
|
$args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, false ];
|
2018-09-25 13:06:12 +00:00
|
|
|
$tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
|
|
|
|
|
array_merge( [ $args ], array_slice( $val, 1 ) );
|
|
|
|
|
}
|
2018-09-21 16:24:57 +00:00
|
|
|
// addWikiTextTidy / addWikiTextTitleTidy were old aliases of
|
|
|
|
|
// addWikiTextAsContent
|
|
|
|
|
foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
|
|
|
|
|
if ( count( $val[0] ) > 2 ) {
|
|
|
|
|
$args = [ $val[0][0], $val[0][2], $val[0][1] ?? true ];
|
|
|
|
|
$tests['addWikiTextTitleTidy']["$key (addWikiTextTitleTidy)"] =
|
|
|
|
|
array_merge( [ $args ], array_slice( $val, 1 ) );
|
|
|
|
|
} else {
|
|
|
|
|
$args = [ $val[0][0], $val[0][1] ?? true ];
|
|
|
|
|
$tests['addWikiTextTidy']["$key (addWikiTextTidy)"] =
|
|
|
|
|
array_merge( [ $args ], array_slice( $val, 1 ) );
|
|
|
|
|
}
|
|
|
|
|
}
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addWikiText
|
|
|
|
|
*/
|
|
|
|
|
public function testAddWikiTextNoTitle() {
|
2018-09-25 14:05:55 +00:00
|
|
|
$this->hideDeprecated( 'OutputPage::addWikiText' );
|
2018-07-25 18:41:42 +00:00
|
|
|
$this->setExpectedException( MWException::class, 'Title is null' );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->addWikiText( 'a' );
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-21 16:24:57 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addWikiTextAsInterface
|
|
|
|
|
*/
|
|
|
|
|
public function testAddWikiTextAsInterfaceNoTitle() {
|
|
|
|
|
$this->setExpectedException( MWException::class, 'Title is null' );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->addWikiTextAsInterface( 'a' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addWikiTextAsContent
|
|
|
|
|
*/
|
|
|
|
|
public function testAddWikiTextAsContentNoTitle() {
|
|
|
|
|
$this->setExpectedException( MWException::class, 'Title is null' );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->addWikiTextAsContent( 'a' );
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-27 15:05:47 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addWikiMsg
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::wrapWikiMsg
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
2018-08-02 18:42:17 +00:00
|
|
|
* @covers OutputPage::addParserOutput
|
2018-07-25 18:41:42 +00:00
|
|
|
*/
|
|
|
|
|
public function testNoGallery() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertFalse( $op->mNoGallery );
|
|
|
|
|
|
|
|
|
|
$stubPO1 = $this->createParserOutputStub( 'getNoGallery', true );
|
|
|
|
|
$op->addParserOutputMetadata( $stubPO1 );
|
|
|
|
|
$this->assertTrue( $op->mNoGallery );
|
|
|
|
|
|
|
|
|
|
$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 );
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
private static $parserOutputHookCalled;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
|
|
|
|
*/
|
|
|
|
|
public function testParserOutputHooks() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$pOut = $this->createParserOutputStub( 'getOutputHooks', [
|
|
|
|
|
[ 'myhook', 'banana' ],
|
|
|
|
|
[ 'yourhook', 'kumquat' ],
|
|
|
|
|
[ 'theirhook', 'hippopotamus' ],
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
self::$parserOutputHookCalled = [];
|
|
|
|
|
|
|
|
|
|
$this->setMwGlobals( 'wgParserOutputHooks', [
|
|
|
|
|
'myhook' => function ( OutputPage $innerOp, ParserOutput $innerPOut, $data )
|
|
|
|
|
use ( $op, $pOut ) {
|
|
|
|
|
$this->assertSame( $op, $innerOp );
|
|
|
|
|
$this->assertSame( $pOut, $innerPOut );
|
|
|
|
|
$this->assertSame( 'banana', $data );
|
|
|
|
|
self::$parserOutputHookCalled[] = 'closure';
|
|
|
|
|
},
|
|
|
|
|
'yourhook' => [ $this, 'parserOutputHookCallback' ],
|
|
|
|
|
'theirhook' => [ __CLASS__, 'parserOutputHookCallbackStatic' ],
|
|
|
|
|
'uncalled' => function () {
|
|
|
|
|
$this->assertTrue( false );
|
|
|
|
|
},
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutputMetadata( $pOut );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( [ 'closure', 'callback', 'static' ], self::$parserOutputHookCalled );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function parserOutputHookCallback(
|
|
|
|
|
OutputPage $op, ParserOutput $pOut, $data
|
|
|
|
|
) {
|
|
|
|
|
$this->assertSame( 'kumquat', $data );
|
|
|
|
|
|
|
|
|
|
self::$parserOutputHookCalled[] = 'callback';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function parserOutputHookCallbackStatic(
|
|
|
|
|
OutputPage $op, ParserOutput $pOut, $data
|
|
|
|
|
) {
|
|
|
|
|
// All the assert methods are actually static, who knew!
|
|
|
|
|
self::assertSame( 'hippopotamus', $data );
|
|
|
|
|
|
|
|
|
|
self::$parserOutputHookCalled[] = 'static';
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addParserOutputText
|
|
|
|
|
*/
|
|
|
|
|
public function testAddParserOutputText() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
|
|
|
|
|
$pOut = $this->createParserOutputStub( 'getText', '<some text>' );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutputMetadata( $pOut );
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutputText( $pOut );
|
|
|
|
|
$this->assertSame( '<some text>', $op->getHTML() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addParserOutput
|
|
|
|
|
*/
|
|
|
|
|
public function testAddParserOutput() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( '', $op->getHTML() );
|
|
|
|
|
$this->assertFalse( $op->showNewSectionLink() );
|
|
|
|
|
|
|
|
|
|
$pOut = $this->createParserOutputStub( [
|
|
|
|
|
'getText' => '<some text>',
|
|
|
|
|
'getNewSection' => true,
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutput( $pOut );
|
|
|
|
|
$this->assertSame( '<some text>', $op->getHTML() );
|
|
|
|
|
$this->assertTrue( $op->showNewSectionLink() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::addTemplate
|
|
|
|
|
*/
|
|
|
|
|
public function testAddTemplate() {
|
|
|
|
|
$template = $this->getMock( QuickTemplate::class );
|
|
|
|
|
$template->method( 'getHTML' )->willReturn( '<abc>&def;' );
|
|
|
|
|
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$op->addTemplate( $template );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( '<abc>&def;', $op->getHTML() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideParse
|
|
|
|
|
* @covers OutputPage::parse
|
|
|
|
|
* @param array $args To pass to parse()
|
|
|
|
|
* @param string $expectedHTML Expected return value for parse()
|
|
|
|
|
* @param string $expectedHTML Expected return value for parseInline(), if different
|
|
|
|
|
*/
|
|
|
|
|
public function testParse( array $args, $expectedHTML ) {
|
2018-10-26 15:28:12 +00:00
|
|
|
$this->hideDeprecated( 'OutputPage::parse' );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( $expectedHTML, $op->parse( ...$args ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideParse
|
|
|
|
|
* @covers OutputPage::parseInline
|
|
|
|
|
*/
|
|
|
|
|
public function testParseInline( array $args, $expectedHTML, $expectedHTMLInline = null ) {
|
|
|
|
|
if ( count( $args ) > 3 ) {
|
|
|
|
|
// $language param not supported
|
|
|
|
|
$this->assertTrue( true );
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-10-26 15:28:12 +00:00
|
|
|
$this->hideDeprecated( 'OutputPage::parseInline' );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( $expectedHTMLInline ?? $expectedHTML, $op->parseInline( ...$args ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideParse() {
|
|
|
|
|
return [
|
2018-10-26 14:05:34 +00:00
|
|
|
'List at start of line (content)' => [
|
|
|
|
|
[ '* List', true, false ],
|
2018-10-23 22:47:48 +00:00
|
|
|
"<div class=\"mw-parser-output\"><ul><li>List</li></ul></div>",
|
|
|
|
|
"<ul><li>List</li></ul>",
|
2018-08-02 18:42:17 +00:00
|
|
|
],
|
2018-10-26 14:05:34 +00:00
|
|
|
'List at start of line (interface)' => [
|
|
|
|
|
[ '* List', true, true ],
|
2018-10-23 22:47:48 +00:00
|
|
|
"<ul><li>List</li></ul>",
|
2018-10-26 14:05:34 +00:00
|
|
|
],
|
|
|
|
|
'List not at start (content)' => [
|
|
|
|
|
[ "* ''Not'' list", false, false ],
|
2018-08-02 18:42:17 +00:00
|
|
|
'<div class="mw-parser-output">* <i>Not</i> list</div>',
|
2018-10-26 14:05:34 +00:00
|
|
|
'* <i>Not</i> list',
|
|
|
|
|
],
|
|
|
|
|
'List not at start (interface)' => [
|
|
|
|
|
[ "* ''Not'' list", false, true ],
|
|
|
|
|
'* <i>Not</i> list',
|
2018-08-02 18:42:17 +00:00
|
|
|
],
|
2018-10-26 14:05:34 +00:00
|
|
|
'Interface message' => [
|
2018-08-02 18:42:17 +00:00
|
|
|
[ "''Italic''", true, true ],
|
|
|
|
|
"<p><i>Italic</i>\n</p>",
|
|
|
|
|
'<i>Italic</i>',
|
|
|
|
|
],
|
2018-10-26 14:05:34 +00:00
|
|
|
'formatnum (content)' => [
|
|
|
|
|
[ '{{formatnum:123456.789}}', true, false ],
|
2018-08-02 18:42:17 +00:00
|
|
|
"<div class=\"mw-parser-output\"><p>123,456.789\n</p></div>",
|
2018-10-26 14:05:34 +00:00
|
|
|
"123,456.789",
|
|
|
|
|
],
|
|
|
|
|
'formatnum (interface)' => [
|
|
|
|
|
[ '{{formatnum:123456.789}}', true, true ],
|
|
|
|
|
"<p>123,456.789\n</p>",
|
|
|
|
|
"123,456.789",
|
2018-08-02 18:42:17 +00:00
|
|
|
],
|
2018-10-26 14:05:34 +00:00
|
|
|
'Language (content)' => [
|
2018-08-02 18:42:17 +00:00
|
|
|
[ '{{formatnum:123456.789}}', true, false, Language::factory( 'is' ) ],
|
|
|
|
|
"<div class=\"mw-parser-output\"><p>123.456,789\n</p></div>",
|
|
|
|
|
],
|
2018-10-26 14:05:34 +00:00
|
|
|
'Language (interface)' => [
|
2018-08-02 18:42:17 +00:00
|
|
|
[ '{{formatnum:123456.789}}', true, true, Language::factory( 'is' ) ],
|
|
|
|
|
"<p>123.456,789\n</p>",
|
|
|
|
|
'123.456,789',
|
|
|
|
|
],
|
|
|
|
|
'No section edit links' => [
|
|
|
|
|
[ '== Header ==' ],
|
|
|
|
|
'<div class="mw-parser-output"><h2><span class="mw-headline" id="Header">' .
|
|
|
|
|
"Header</span></h2>\n</div>",
|
2018-10-26 14:05:34 +00:00
|
|
|
'<h2><span class="mw-headline" id="Header">Header</span></h2>' .
|
|
|
|
|
"\n",
|
2018-08-02 18:42:17 +00:00
|
|
|
]
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-26 15:14:01 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideParseAs
|
|
|
|
|
* @covers OutputPage::parseAsContent
|
|
|
|
|
* @param array $args To pass to parse()
|
|
|
|
|
* @param string $expectedHTML Expected return value for parseAsContent()
|
|
|
|
|
* @param string $expectedHTML Expected return value for parseInlineAsInterface(), if different
|
|
|
|
|
*/
|
|
|
|
|
public function testParseAsContent(
|
|
|
|
|
array $args, $expectedHTML, $expectedHTMLInline = null
|
|
|
|
|
) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( $expectedHTML, $op->parseAsContent( ...$args ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideParseAs
|
|
|
|
|
* @covers OutputPage::parseAsInterface
|
|
|
|
|
* @param array $args To pass to parse()
|
|
|
|
|
* @param string $expectedHTML Expected return value for parseAsInterface()
|
|
|
|
|
* @param string $expectedHTML Expected return value for parseInlineAsInterface(), if different
|
|
|
|
|
*/
|
|
|
|
|
public function testParseAsInterface(
|
|
|
|
|
array $args, $expectedHTML, $expectedHTMLInline = null
|
|
|
|
|
) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame( $expectedHTML, $op->parseAsInterface( ...$args ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideParseAs
|
|
|
|
|
* @covers OutputPage::parseInlineAsInterface
|
|
|
|
|
*/
|
|
|
|
|
public function testParseInlineAsInterface(
|
|
|
|
|
array $args, $expectedHTML, $expectedHTMLInline = null
|
|
|
|
|
) {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
$expectedHTMLInline ?? $expectedHTML,
|
|
|
|
|
$op->parseInlineAsInterface( ...$args )
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideParseAs() {
|
|
|
|
|
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 ==' ],
|
|
|
|
|
'<h2><span class="mw-headline" id="Header">Header</span></h2>' .
|
|
|
|
|
"\n",
|
|
|
|
|
]
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::parse
|
|
|
|
|
*/
|
|
|
|
|
public function testParseNullTitle() {
|
2018-10-26 15:28:12 +00:00
|
|
|
$this->hideDeprecated( 'OutputPage::parse' );
|
2018-10-26 15:14:01 +00:00
|
|
|
$this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->parse( '' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-10-26 15:14:01 +00:00
|
|
|
* @covers OutputPage::parseInline
|
2018-08-02 18:42:17 +00:00
|
|
|
*/
|
|
|
|
|
public function testParseInlineNullTitle() {
|
2018-10-26 15:28:12 +00:00
|
|
|
$this->hideDeprecated( 'OutputPage::parseInline' );
|
2018-10-26 15:14:01 +00:00
|
|
|
$this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
|
2018-08-02 18:42:17 +00:00
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->parseInline( '' );
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-26 15:14:01 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::parseAsContent
|
|
|
|
|
*/
|
|
|
|
|
public function testParseAsContentNullTitle() {
|
|
|
|
|
$this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
|
|
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->parseAsContent( '' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::parseAsInterface
|
|
|
|
|
*/
|
|
|
|
|
public function testParseAsInterfaceNullTitle() {
|
|
|
|
|
$this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
|
|
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->parseAsInterface( '' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::parseInlineAsInterface
|
|
|
|
|
*/
|
|
|
|
|
public function testParseInlineAsInterfaceNullTitle() {
|
|
|
|
|
$this->setExpectedException( MWException::class, 'Empty $mTitle in OutputPage::parseInternal' );
|
|
|
|
|
$op = $this->newInstance( [], null, 'notitle' );
|
|
|
|
|
$op->parseInlineAsInterface( '' );
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::setCdnMaxage
|
|
|
|
|
* @covers OutputPage::lowerCdnMaxage
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
* @covers OutputPage::adaptCdnTTL
|
|
|
|
|
* @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 = [] ) {
|
|
|
|
|
try {
|
|
|
|
|
MWTimestamp::setFakeTime( self::$fakeTime );
|
|
|
|
|
|
|
|
|
|
$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 );
|
|
|
|
|
} finally {
|
|
|
|
|
MWTimestamp::setFakeTime( false );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$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' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideAdaptCdnTTL() {
|
|
|
|
|
global $wgSquidMaxage;
|
|
|
|
|
$now = time();
|
|
|
|
|
self::$fakeTime = $now;
|
|
|
|
|
return [
|
|
|
|
|
'Five minutes ago' => [ [ $now - 300 ], 270 ],
|
|
|
|
|
'Now' => [ [ +0 ], IExpiringStore::TTL_MINUTE ],
|
|
|
|
|
'Five minutes from now' => [ [ $now + 300 ], IExpiringStore::TTL_MINUTE ],
|
|
|
|
|
'Five minutes ago, initial maxage four minutes' =>
|
|
|
|
|
[ [ $now - 300 ], 270, [ 'initialMaxage' => 240 ] ],
|
|
|
|
|
'A very long time ago' => [ [ $now - 1000000000 ], $wgSquidMaxage ],
|
|
|
|
|
'Initial maxage zero' => [ [ $now - 300 ], 270, [ 'initialMaxage' => 0 ] ],
|
|
|
|
|
|
|
|
|
|
'false' => [ [ false ], IExpiringStore::TTL_MINUTE ],
|
|
|
|
|
'null' => [ [ null ], IExpiringStore::TTL_MINUTE ],
|
|
|
|
|
"'0'" => [ [ '0' ], IExpiringStore::TTL_MINUTE ],
|
|
|
|
|
'Empty string' => [ [ '' ], IExpiringStore::TTL_MINUTE ],
|
|
|
|
|
// @todo These give incorrect results due to timezones, how to test?
|
|
|
|
|
//"'now'" => [ [ 'now' ], IExpiringStore::TTL_MINUTE ],
|
|
|
|
|
//"'parse error'" => [ [ 'parse error' ], IExpiringStore::TTL_MINUTE ],
|
|
|
|
|
|
|
|
|
|
'Now, minTTL 0' => [ [ $now, 0 ], IExpiringStore::TTL_MINUTE ],
|
|
|
|
|
'Now, minTTL 0.000001' => [ [ $now, 0.000001 ], 0 ],
|
|
|
|
|
'A very long time ago, maxTTL even longer' =>
|
|
|
|
|
[ [ $now - 1000000000, 0, 1000000001 ], 900000000 ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::enableClientCache
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
|
|
|
|
* @covers OutputPage::addParserOutput
|
|
|
|
|
*/
|
|
|
|
|
public function testClientCache() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
|
|
|
|
|
// Test initial value
|
|
|
|
|
$this->assertSame( true, $op->enableClientCache( null ) );
|
|
|
|
|
// Test that calling with null doesn't change the value
|
|
|
|
|
$this->assertSame( true, $op->enableClientCache( null ) );
|
|
|
|
|
|
|
|
|
|
// Test setting to false
|
|
|
|
|
$this->assertSame( true, $op->enableClientCache( false ) );
|
|
|
|
|
$this->assertSame( false, $op->enableClientCache( null ) );
|
|
|
|
|
// Test that calling with null doesn't change the value
|
|
|
|
|
$this->assertSame( false, $op->enableClientCache( null ) );
|
|
|
|
|
|
|
|
|
|
// Test that a cacheable ParserOutput doesn't set to true
|
|
|
|
|
$pOutCacheable = $this->createParserOutputStub( 'isCacheable', true );
|
|
|
|
|
$op->addParserOutputMetadata( $pOutCacheable );
|
|
|
|
|
$this->assertSame( false, $op->enableClientCache( null ) );
|
|
|
|
|
|
|
|
|
|
// Test setting back to true
|
|
|
|
|
$this->assertSame( false, $op->enableClientCache( true ) );
|
|
|
|
|
$this->assertSame( true, $op->enableClientCache( null ) );
|
|
|
|
|
|
|
|
|
|
// Test that an uncacheable ParserOutput does set to false
|
|
|
|
|
$pOutUncacheable = $this->createParserOutputStub( 'isCacheable', false );
|
|
|
|
|
$op->addParserOutput( $pOutUncacheable );
|
|
|
|
|
$this->assertSame( false, $op->enableClientCache( null ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::getCacheVaryCookies
|
|
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
$this->setMwGlobals( 'wgCacheVaryCookies', [ 'cookie1' ] );
|
|
|
|
|
$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
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::haveCacheVaryCookies
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::addVaryHeader
|
|
|
|
|
* @covers OutputPage::getVaryHeader
|
|
|
|
|
* @covers OutputPage::getKeyHeader
|
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: ')
|
|
|
|
|
* @param string $key Text of expected Key header (including the 'Key: ')
|
2018-07-23 18:26:32 +00:00
|
|
|
*/
|
2018-08-02 18:42:17 +00:00
|
|
|
public function testVaryHeaders( array $calls, array $cookies, $vary, $key ) {
|
|
|
|
|
// Get rid of default Vary fields
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = $this->getMockBuilder( OutputPage::class )
|
|
|
|
|
->setConstructorArgs( [ new RequestContext() ] )
|
|
|
|
|
->setMethods( [ 'getCacheVaryCookies' ] )
|
|
|
|
|
->getMock();
|
|
|
|
|
$op->expects( $this->any() )
|
|
|
|
|
->method( 'getCacheVaryCookies' )
|
2018-08-02 18:42:17 +00:00
|
|
|
->will( $this->returnValue( $cookies ) );
|
2018-07-23 18:26:32 +00:00
|
|
|
TestingAccessWrapper::newFromObject( $op )->mVaryHeader = [];
|
|
|
|
|
|
2018-10-12 15:50:14 +00:00
|
|
|
$this->hideDeprecated( '$wgUseKeyHeader' );
|
2018-07-23 18:26:32 +00:00
|
|
|
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:' );
|
|
|
|
|
$this->assertEquals( $key, $op->getKeyHeader(), 'Key:' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideVaryHeaders() {
|
|
|
|
|
// note: getKeyHeader() automatically adds Vary: Cookie
|
2017-01-18 05:58:46 +00:00
|
|
|
return [
|
2018-08-02 18:42:17 +00:00
|
|
|
'No header' => [
|
|
|
|
|
[],
|
|
|
|
|
[],
|
|
|
|
|
'Vary: ',
|
|
|
|
|
'Key: Cookie',
|
|
|
|
|
],
|
|
|
|
|
'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',
|
|
|
|
|
'Key: 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',
|
|
|
|
|
'Key: Cookie,Accept-Language',
|
2017-02-10 00:03:06 +00:00
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Two headers with single options' => [
|
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',
|
|
|
|
|
'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
|
2017-02-10 00:03:06 +00:00
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'One header with multiple options' => [
|
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',
|
|
|
|
|
'Key: Cookie;param=phpsessid;param=userId',
|
2017-02-10 00:03:06 +00:00
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Duplicate option' => [
|
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',
|
|
|
|
|
'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
|
|
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'Same header, different options' => [
|
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',
|
|
|
|
|
'Key: Cookie;param=phpsessid;param=userId',
|
2017-02-10 00:03:06 +00:00
|
|
|
],
|
2018-08-02 18:42:17 +00:00
|
|
|
'No header, vary cookies' => [
|
|
|
|
|
[],
|
|
|
|
|
[ 'cookie1', 'cookie2' ],
|
|
|
|
|
'Vary: Cookie',
|
|
|
|
|
'Key: Cookie;param=cookie1;param=cookie2',
|
|
|
|
|
],
|
|
|
|
|
'Cookie header with option plus vary cookies' => [
|
|
|
|
|
[
|
|
|
|
|
[ 'Cookie', [ 'param=cookie1' ] ],
|
|
|
|
|
],
|
|
|
|
|
[ 'cookie2', 'cookie3' ],
|
|
|
|
|
'Vary: Cookie',
|
|
|
|
|
'Key: Cookie;param=cookie1;param=cookie2;param=cookie3',
|
|
|
|
|
],
|
|
|
|
|
'Non-cookie header plus vary cookies' => [
|
|
|
|
|
[
|
|
|
|
|
[ 'Accept-Language' ],
|
|
|
|
|
],
|
|
|
|
|
[ 'cookie' ],
|
|
|
|
|
'Vary: Accept-Language, Cookie',
|
|
|
|
|
'Key: Accept-Language,Cookie;param=cookie',
|
|
|
|
|
],
|
|
|
|
|
'Cookie and non-cookie headers plus vary cookies' => [
|
|
|
|
|
[
|
|
|
|
|
[ 'Cookie', [ 'param=cookie1' ] ],
|
|
|
|
|
[ 'Accept-Language' ],
|
|
|
|
|
],
|
|
|
|
|
[ 'cookie2' ],
|
|
|
|
|
'Vary: Cookie, Accept-Language',
|
|
|
|
|
'Key: Cookie;param=cookie1;param=cookie2,Accept-Language',
|
|
|
|
|
],
|
2017-01-18 05:58:46 +00:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::getVaryHeader
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::addLinkHeader
|
|
|
|
|
* @covers OutputPage::getLinkHeader
|
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
|
|
|
}
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
public 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
|
|
|
|
|
* @covers OutputPage::addAcceptLanguage
|
2018-10-12 15:50:14 +00:00
|
|
|
* @covers OutputPage::getKeyHeader
|
2018-08-02 18:42:17 +00:00
|
|
|
*/
|
|
|
|
|
public function testAddAcceptLanguage(
|
|
|
|
|
$code, array $variants, array $expected, array $options = []
|
|
|
|
|
) {
|
|
|
|
|
$req = new FauxRequest( in_array( 'varianturl', $options ) ? [ 'variant' => 'x' ] : [] );
|
|
|
|
|
$op = $this->newInstance( [], $req, in_array( 'notitle', $options ) ? 'notitle' : null );
|
|
|
|
|
|
|
|
|
|
if ( !in_array( 'notitle', $options ) ) {
|
|
|
|
|
$mockLang = $this->getMock( Language::class );
|
|
|
|
|
|
|
|
|
|
if ( in_array( 'varianturl', $options ) ) {
|
|
|
|
|
$mockLang->expects( $this->never() )->method( $this->anything() );
|
|
|
|
|
} else {
|
|
|
|
|
$mockLang->method( 'hasVariants' )->willReturn( count( $variants ) > 1 );
|
|
|
|
|
$mockLang->method( 'getVariants' )->willReturn( $variants );
|
|
|
|
|
$mockLang->method( 'getCode' )->willReturn( $code );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$mockTitle = $this->getMock( Title::class );
|
|
|
|
|
$mockTitle->method( 'getPageLanguage' )->willReturn( $mockLang );
|
|
|
|
|
|
|
|
|
|
$op->setTitle( $mockTitle );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This will run addAcceptLanguage()
|
|
|
|
|
$op->sendCacheControl();
|
|
|
|
|
|
2018-10-12 15:50:14 +00:00
|
|
|
$this->hideDeprecated( '$wgUseKeyHeader' );
|
2018-08-02 18:42:17 +00:00
|
|
|
$keyHeader = $op->getKeyHeader();
|
|
|
|
|
|
|
|
|
|
if ( !$expected ) {
|
|
|
|
|
$this->assertFalse( strpos( 'Accept-Language', $keyHeader ) );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$keyHeader = explode( ' ', $keyHeader, 2 )[1];
|
|
|
|
|
$keyHeader = explode( ',', $keyHeader );
|
|
|
|
|
|
|
|
|
|
$acceptLanguage = null;
|
|
|
|
|
foreach ( $keyHeader as $item ) {
|
|
|
|
|
if ( strpos( $item, 'Accept-Language;' ) === 0 ) {
|
|
|
|
|
$acceptLanguage = $item;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$expectedString = 'Accept-Language;substr=' . implode( ';substr=', $expected );
|
|
|
|
|
$this->assertSame( $expectedString, $acceptLanguage );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideAddAcceptLanguage() {
|
|
|
|
|
return [
|
|
|
|
|
'No variants' => [ 'en', [ 'en' ], [] ],
|
|
|
|
|
'One simple variant' => [ 'en', [ 'en', 'en-x-piglatin' ], [ 'en-x-piglatin' ] ],
|
|
|
|
|
'Multiple variants with BCP47 alternatives' => [
|
|
|
|
|
'zh',
|
|
|
|
|
[ 'zh', 'zh-hans', 'zh-cn', 'zh-tw' ],
|
|
|
|
|
[ 'zh-hans', 'zh-Hans', 'zh-cn', 'zh-Hans-CN', 'zh-tw', 'zh-Hant-TW' ],
|
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
|
|
|
'No title' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'notitle' ] ],
|
|
|
|
|
'Variant in URL' => [ 'en', [ 'en', 'en-x-piglatin' ], [], [ 'varianturl' ] ],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::preventClickjacking
|
|
|
|
|
* @covers OutputPage::allowClickjacking
|
|
|
|
|
* @covers OutputPage::getPreventClickjacking
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
|
|
|
|
* @covers OutputPage::addParserOutput
|
|
|
|
|
*/
|
|
|
|
|
public function testClickjacking() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
$op->allowClickjacking();
|
|
|
|
|
$this->assertFalse( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
$op->preventClickjacking();
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
$op->preventClickjacking( false );
|
|
|
|
|
$this->assertFalse( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
$pOut1 = $this->createParserOutputStub( 'preventClickjacking', true );
|
|
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
// The ParserOutput can't allow, only prevent
|
|
|
|
|
$pOut2 = $this->createParserOutputStub( 'preventClickjacking', false );
|
|
|
|
|
$op->addParserOutputMetadata( $pOut2 );
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
// Reset to test with addParserOutput()
|
|
|
|
|
$op->allowClickjacking();
|
|
|
|
|
$this->assertFalse( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutput( $pOut1 );
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
|
|
|
|
|
$op->addParserOutput( $pOut2 );
|
|
|
|
|
$this->assertTrue( $op->getPreventClickjacking() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideGetFrameOptions
|
|
|
|
|
* @covers OutputPage::getFrameOptions
|
|
|
|
|
* @covers OutputPage::preventClickjacking
|
|
|
|
|
*/
|
|
|
|
|
public function testGetFrameOptions(
|
|
|
|
|
$breakFrames, $preventClickjacking, $editPageFrameOptions, $expected
|
|
|
|
|
) {
|
|
|
|
|
$op = $this->newInstance( [
|
|
|
|
|
'BreakFrames' => $breakFrames,
|
|
|
|
|
'EditPageFrameOptions' => $editPageFrameOptions,
|
|
|
|
|
] );
|
|
|
|
|
$op->preventClickjacking( $preventClickjacking );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $expected, $op->getFrameOptions() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function provideGetFrameOptions() {
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
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
|
|
|
* See ResourceLoaderClientHtmlTest for full coverage.
|
|
|
|
|
*
|
2014-06-28 20:40:22 +00:00
|
|
|
* @dataProvider provideMakeResourceLoaderLink
|
2018-07-23 18:26:32 +00:00
|
|
|
*
|
2014-06-28 20:40:22 +00:00
|
|
|
* @covers OutputPage::makeResourceLoaderLink
|
|
|
|
|
*/
|
2014-07-19 21:12:10 +00:00
|
|
|
public function testMakeResourceLoaderLink( $args, $expectedHtml ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->setMwGlobals( [
|
2014-07-11 14:35:14 +00:00
|
|
|
'wgResourceLoaderDebug' => false,
|
2014-06-28 20:40:22 +00:00
|
|
|
'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
|
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
|
|
|
'wgCSPReportOnlyHeader' => 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();
|
2014-08-09 12:36:35 +00:00
|
|
|
$ctx->setSkin( SkinFactory::getDefaultInstance()->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 );
|
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
|
|
|
$nonce = $class->getProperty( 'CSPNonce' );
|
|
|
|
|
$nonce->setAccessible( true );
|
|
|
|
|
$nonce->setValue( $out, 'secret' );
|
2014-06-28 20:40:22 +00:00
|
|
|
$rl = $out->getResourceLoader();
|
2019-02-16 23:46:30 +00:00
|
|
|
$rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
|
2016-02-17 09:09:32 +00:00
|
|
|
$rl->register( [
|
|
|
|
|
'test.foo' => new ResourceLoaderTestModule( [
|
2014-06-28 20:40:22 +00:00
|
|
|
'script' => 'mw.test.foo( { a: true } );',
|
|
|
|
|
'styles' => '.mw-test-foo { content: "style"; }',
|
2016-02-17 09:09:32 +00:00
|
|
|
] ),
|
|
|
|
|
'test.bar' => new ResourceLoaderTestModule( [
|
2014-06-28 20:40:22 +00:00
|
|
|
'script' => 'mw.test.bar( { a: true } );',
|
|
|
|
|
'styles' => '.mw-test-bar { content: "style"; }',
|
2016-02-17 09:09:32 +00:00
|
|
|
] ),
|
|
|
|
|
'test.baz' => new ResourceLoaderTestModule( [
|
2014-06-28 20:40:22 +00:00
|
|
|
'script' => 'mw.test.baz( { a: true } );',
|
|
|
|
|
'styles' => '.mw-test-baz { content: "style"; }',
|
2016-02-17 09:09:32 +00:00
|
|
|
] ),
|
|
|
|
|
'test.quux' => new ResourceLoaderTestModule( [
|
2014-06-28 20:40:22 +00:00
|
|
|
'script' => 'mw.test.baz( { token: 123 } );',
|
|
|
|
|
'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
|
|
|
|
|
'group' => 'private',
|
2016-02-17 09:09:32 +00:00
|
|
|
] ),
|
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
|
|
|
'test.noscript' => new ResourceLoaderTestModule( [
|
|
|
|
|
'styles' => '.stuff { color: red; }',
|
|
|
|
|
'group' => 'noscript',
|
|
|
|
|
] ),
|
|
|
|
|
'test.group.foo' => new ResourceLoaderTestModule( [
|
|
|
|
|
'script' => 'mw.doStuff( "foo" );',
|
|
|
|
|
'group' => 'foo',
|
|
|
|
|
] ),
|
|
|
|
|
'test.group.bar' => new ResourceLoaderTestModule( [
|
|
|
|
|
'script' => 'mw.doStuff( "bar" );',
|
|
|
|
|
'group' => 'bar',
|
|
|
|
|
] ),
|
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() {
|
2018-01-01 13:10:16 +00:00
|
|
|
// phpcs:disable Generic.Files.LineLength
|
2017-05-12 22:20:02 +00:00
|
|
|
return [
|
2018-07-23 18:26:32 +00:00
|
|
|
// Single only=scripts load
|
|
|
|
|
[
|
|
|
|
|
[ 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ],
|
|
|
|
|
"<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
|
2019-03-08 20:37:12 +00:00
|
|
|
. 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.foo\u0026only=scripts\u0026skin=fallback");'
|
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
|
|
|
|
|
[
|
|
|
|
|
[ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
|
|
|
|
|
|
2019-03-08 20:37:12 +00:00
|
|
|
'<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&modules=test.bar%2Cbaz%2Cfoo&only=styles&skin=fallback"/>'
|
2017-05-12 22:20:02 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// Private embed (only=scripts)
|
|
|
|
|
[
|
|
|
|
|
[ 'test.quux', ResourceLoaderModule::TYPE_SCRIPTS ],
|
|
|
|
|
"<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
|
|
|
|
|
. "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)
|
|
|
|
|
[
|
|
|
|
|
[ 'test.quux', ResourceLoaderModule::TYPE_COMBINED ],
|
|
|
|
|
"<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
|
|
|
|
|
. "mw.loader.implement(\"test.quux@1ev0ijv\",function($,jQuery,require,module){"
|
|
|
|
|
. "mw.test.baz({token:123});},{\"css\":[\".mw-icon{transition:none}"
|
|
|
|
|
. "\"]});});</script>"
|
2017-05-12 22:20:02 +00:00
|
|
|
],
|
2018-07-23 18:26:32 +00:00
|
|
|
// Load no modules
|
|
|
|
|
[
|
|
|
|
|
[ [], ResourceLoaderModule::TYPE_COMBINED ],
|
|
|
|
|
'',
|
|
|
|
|
],
|
|
|
|
|
// noscript group
|
|
|
|
|
[
|
|
|
|
|
[ 'test.noscript', ResourceLoaderModule::TYPE_STYLES ],
|
2019-03-08 20:37:12 +00:00
|
|
|
'<noscript><link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?lang=en&modules=test.noscript&only=styles&skin=fallback"/></noscript>'
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
// Load two modules in separate groups
|
|
|
|
|
[
|
|
|
|
|
[ [ 'test.group.foo', 'test.group.bar' ], ResourceLoaderModule::TYPE_COMBINED ],
|
|
|
|
|
"<script nonce=\"secret\">(window.RLQ=window.RLQ||[]).push(function(){"
|
2019-03-08 20:37:12 +00:00
|
|
|
. 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.bar\u0026skin=fallback");'
|
|
|
|
|
. 'mw.loader.load("http://127.0.0.1:8080/w/load.php?lang=en\u0026modules=test.group.foo\u0026skin=fallback");'
|
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
|
2018-07-23 18:26:32 +00:00
|
|
|
*
|
2017-05-12 22:20:02 +00:00
|
|
|
* @covers OutputPage::buildExemptModules
|
|
|
|
|
*/
|
|
|
|
|
public function testBuildExemptModules( array $exemptStyleModules, $expect ) {
|
|
|
|
|
$this->setMwGlobals( [
|
|
|
|
|
'wgResourceLoaderDebug' => false,
|
|
|
|
|
'wgLoadScript' => '/w/load.php',
|
|
|
|
|
// Stub wgCacheEpoch as it influences getVersionHash used for the
|
|
|
|
|
// urls in the expected HTML
|
|
|
|
|
'wgCacheEpoch' => '20140101000000',
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
// Set up stubs
|
|
|
|
|
$ctx = new RequestContext();
|
|
|
|
|
$ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) );
|
|
|
|
|
$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 ] )
|
2017-02-28 20:52:17 +00:00
|
|
|
->setMethods( [ 'buildCssLinksArray' ] )
|
2017-05-12 22:20:02 +00:00
|
|
|
->getMock();
|
2018-07-23 18:26:32 +00:00
|
|
|
$op->expects( $this->any() )
|
2017-05-12 22:20:02 +00:00
|
|
|
->method( 'buildCssLinksArray' )
|
|
|
|
|
->willReturn( [] );
|
2018-07-23 18:26:32 +00:00
|
|
|
$rl = $op->getResourceLoader();
|
2019-02-16 23:46:30 +00:00
|
|
|
$rl->setMessageBlobStore( $this->createMock( MessageBlobStore::class ) );
|
2017-05-12 22:20:02 +00:00
|
|
|
|
|
|
|
|
// Register custom modules
|
|
|
|
|
$rl->register( [
|
|
|
|
|
'example.site.a' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
|
|
|
|
|
'example.site.b' => new ResourceLoaderTestModule( [ 'group' => 'site' ] ),
|
|
|
|
|
'example.user' => new ResourceLoaderTestModule( [ 'group' => 'user' ] ),
|
|
|
|
|
] );
|
|
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$op = TestingAccessWrapper::newFromObject( $op );
|
|
|
|
|
$op->rlExemptStyleModules = $exemptStyleModules;
|
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() {
|
|
|
|
|
// phpcs:disable Generic.Files.LineLength
|
|
|
|
|
return [
|
|
|
|
|
'empty' => [
|
|
|
|
|
'exemptStyleModules' => [],
|
|
|
|
|
'<meta name="ResourceLoaderDynamicStyles" content=""/>',
|
|
|
|
|
],
|
|
|
|
|
'empty sets' => [
|
|
|
|
|
'exemptStyleModules' => [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ],
|
|
|
|
|
'<meta name="ResourceLoaderDynamicStyles" content=""/>',
|
|
|
|
|
],
|
|
|
|
|
'default logged-out' => [
|
|
|
|
|
'exemptStyleModules' => [ 'site' => [ 'site.styles' ] ],
|
|
|
|
|
'<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
|
2019-03-08 20:37:12 +00:00
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=site.styles&only=styles&skin=fallback"/>',
|
2018-07-23 18:26:32 +00:00
|
|
|
],
|
|
|
|
|
'default logged-in' => [
|
|
|
|
|
'exemptStyleModules' => [ 'site' => [ 'site.styles' ], 'user' => [ 'user.styles' ] ],
|
|
|
|
|
'<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
|
2019-03-08 20:37:12 +00:00
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=site.styles&only=styles&skin=fallback"/>' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=user.styles&only=styles&skin=fallback&version=1ai9g6t"/>',
|
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' ],
|
|
|
|
|
],
|
|
|
|
|
'<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
|
2019-03-08 20:37:12 +00:00
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=example.site.a%2Cb&only=styles&skin=fallback"/>' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=site.styles&only=styles&skin=fallback"/>' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=example.user&only=styles&skin=fallback&version=0a56zyi"/>' . "\n" .
|
|
|
|
|
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=user.styles&only=styles&skin=fallback&version=1ai9g6t"/>',
|
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
|
|
|
|
|
* @covers OutputPage::transformFilePath
|
|
|
|
|
* @covers OutputPage::transformResourcePath
|
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
|
|
|
$this->setMwGlobals( 'IP', $baseDir );
|
|
|
|
|
$conf = new HashConfig( [
|
|
|
|
|
'ResourceBasePath' => $basePath,
|
|
|
|
|
'UploadDirectory' => $uploadDir,
|
|
|
|
|
'UploadPath' => $uploadPath,
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
// Some of these paths don't exist and will cause warnings
|
|
|
|
|
Wikimedia\suppressWarnings();
|
|
|
|
|
$actual = OutputPage::transformResourcePath( $conf, $path );
|
|
|
|
|
Wikimedia\restoreWarnings();
|
|
|
|
|
|
|
|
|
|
$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() {
|
|
|
|
|
$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',
|
|
|
|
|
'/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',
|
|
|
|
|
'https://example.org/w/test.jpg'
|
|
|
|
|
],
|
|
|
|
|
// Unrelated path with domain component. Ignored.
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '/w',
|
|
|
|
|
'https://example.org/files/test.jpg'
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '/w',
|
|
|
|
|
'//example.org/files/test.jpg'
|
|
|
|
|
],
|
|
|
|
|
// Unrelated path with domain, and empty base path (root mw install). Ignored.
|
|
|
|
|
[
|
|
|
|
|
'baseDir' => $baseDir, 'basePath' => '',
|
|
|
|
|
'https://example.org/files/test.jpg'
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'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.
|
|
|
|
|
*
|
|
|
|
|
* options['printableQuery'] - value of query string for printable, or omitted for none
|
|
|
|
|
* options['handheldQuery'] - value of query string for handheld, or omitted for none
|
|
|
|
|
* 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 ) {
|
|
|
|
|
$queryData = [];
|
|
|
|
|
if ( isset( $args['printableQuery'] ) ) {
|
|
|
|
|
$queryData['printable'] = $args['printableQuery'];
|
|
|
|
|
}
|
2015-10-08 04:45:26 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
if ( isset( $args['handheldQuery'] ) ) {
|
|
|
|
|
$queryData['handheld'] = $args['handheldQuery'];
|
|
|
|
|
}
|
2015-10-08 04:45:26 +00:00
|
|
|
|
2018-07-23 18:26:32 +00:00
|
|
|
$fauxRequest = new FauxRequest( $queryData, false );
|
|
|
|
|
$this->setMwGlobals( [
|
|
|
|
|
'wgRequest' => $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
|
|
|
|
2017-12-25 07:28:03 +00:00
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* Tests print requests
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::transformCssMedia
|
2016-07-08 19:11:53 +00:00
|
|
|
*/
|
2018-07-23 18:26:32 +00:00
|
|
|
public function testPrintRequests() {
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
|
|
|
|
'printableQuery' => '1',
|
|
|
|
|
'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( [
|
|
|
|
|
'printableQuery' => '1',
|
|
|
|
|
'media' => self::SCREEN_MEDIA_QUERY,
|
|
|
|
|
'expectedReturn' => null,
|
|
|
|
|
'message' => 'On printable request, screen media query returns null'
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
|
|
|
|
'printableQuery' => '1',
|
|
|
|
|
'media' => self::SCREEN_ONLY_MEDIA_QUERY,
|
|
|
|
|
'expectedReturn' => null,
|
|
|
|
|
'message' => 'On printable request, screen media query with only returns null'
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
|
|
|
|
'printableQuery' => '1',
|
|
|
|
|
'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
|
|
|
/**
|
2018-07-23 18:26:32 +00:00
|
|
|
* Tests screen requests, without either query parameter set
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::transformCssMedia
|
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-07-23 18:26:32 +00:00
|
|
|
/**
|
|
|
|
|
* Tests handheld behavior
|
|
|
|
|
*
|
|
|
|
|
* @covers OutputPage::transformCssMedia
|
|
|
|
|
*/
|
|
|
|
|
public function testHandheld() {
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
|
|
|
|
'handheldQuery' => '1',
|
|
|
|
|
'media' => 'handheld',
|
|
|
|
|
'expectedReturn' => '',
|
|
|
|
|
'message' => 'On request with handheld querystring and media is handheld, returns empty string'
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$this->assertTransformCssMediaCase( [
|
|
|
|
|
'handheldQuery' => '1',
|
|
|
|
|
'media' => 'screen',
|
|
|
|
|
'expectedReturn' => null,
|
|
|
|
|
'message' => 'On request with handheld querystring and media is screen, returns null'
|
|
|
|
|
] );
|
2015-06-01 16:58:42 +00:00
|
|
|
}
|
|
|
|
|
|
2018-08-02 18:42:17 +00:00
|
|
|
/**
|
|
|
|
|
* @covers OutputPage::isTOCEnabled
|
|
|
|
|
* @covers OutputPage::addParserOutputMetadata
|
|
|
|
|
* @covers OutputPage::addParserOutput
|
|
|
|
|
*/
|
|
|
|
|
public function testIsTOCEnabled() {
|
|
|
|
|
$op = $this->newInstance();
|
|
|
|
|
$this->assertFalse( $op->isTOCEnabled() );
|
|
|
|
|
|
|
|
|
|
$pOut1 = $this->createParserOutputStub( 'getTOCHTML', false );
|
|
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
|
|
|
|
$this->assertFalse( $op->isTOCEnabled() );
|
|
|
|
|
|
|
|
|
|
$pOut2 = $this->createParserOutputStub( 'getTOCHTML', true );
|
|
|
|
|
$op->addParserOutput( $pOut2 );
|
|
|
|
|
$this->assertTrue( $op->isTOCEnabled() );
|
|
|
|
|
|
|
|
|
|
// The parser output doesn't disable the TOC after it was enabled
|
|
|
|
|
$op->addParserOutputMetadata( $pOut1 );
|
|
|
|
|
$this->assertTrue( $op->isTOCEnabled() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider providePreloadLinkHeaders
|
|
|
|
|
* @covers ResourceLoaderSkinModule::getPreloadLinks
|
|
|
|
|
* @covers ResourceLoaderSkinModule::getLogoPreloadlinks
|
|
|
|
|
*/
|
|
|
|
|
public function testPreloadLinkHeaders( $config, $result ) {
|
|
|
|
|
$this->setMwGlobals( $config );
|
|
|
|
|
$ctx = $this->getMockBuilder( ResourceLoaderContext::class )
|
|
|
|
|
->disableOriginalConstructor()->getMock();
|
|
|
|
|
$module = new ResourceLoaderSkinModule();
|
|
|
|
|
|
|
|
|
|
$this->assertEquals( [ $result ], $module->getHeaders( $ctx ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function providePreloadLinkHeaders() {
|
|
|
|
|
return [
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
'wgResourceBasePath' => '/w',
|
|
|
|
|
'wgLogo' => '/img/default.png',
|
|
|
|
|
'wgLogoHD' => [
|
|
|
|
|
'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)'
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
'wgResourceBasePath' => '/w',
|
|
|
|
|
'wgLogo' => '/img/default.png',
|
|
|
|
|
'wgLogoHD' => false,
|
|
|
|
|
],
|
|
|
|
|
'Link: </img/default.png>;rel=preload;as=image'
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
'wgResourceBasePath' => '/w',
|
|
|
|
|
'wgLogo' => '/img/default.png',
|
|
|
|
|
'wgLogoHD' => [
|
|
|
|
|
'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)'
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
'wgResourceBasePath' => '/w',
|
|
|
|
|
'wgLogo' => '/img/default.png',
|
|
|
|
|
'wgLogoHD' => [
|
|
|
|
|
'svg' => '/img/vector.svg',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'Link: </img/vector.svg>;rel=preload;as=image'
|
|
|
|
|
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
'wgResourceBasePath' => '/w',
|
|
|
|
|
'wgLogo' => '/w/test.jpg',
|
|
|
|
|
'wgLogoHD' => false,
|
|
|
|
|
'wgUploadPath' => '/w/images',
|
|
|
|
|
'IP' => dirname( __DIR__ ) . '/data/media',
|
|
|
|
|
],
|
|
|
|
|
'Link: </w/test.jpg?edcf2>;rel=preload;as=image',
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-24 17:30:33 +00:00
|
|
|
/**
|
|
|
|
|
* @return OutputPage
|
|
|
|
|
*/
|
2018-07-25 18:41:42 +00:00
|
|
|
private function newInstance( $config = [], WebRequest $request = null, $options = [] ) {
|
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 + [
|
|
|
|
|
'AppleTouchIcon' => false,
|
|
|
|
|
'DisableLangConversion' => true,
|
|
|
|
|
'EnableCanonicalServerLink' => false,
|
|
|
|
|
'Favicon' => false,
|
|
|
|
|
'Feed' => false,
|
|
|
|
|
'LanguageCode' => false,
|
|
|
|
|
'ReferrerPolicy' => false,
|
|
|
|
|
'RightsPage' => false,
|
|
|
|
|
'RightsUrl' => false,
|
|
|
|
|
'UniversalEditButton' => false,
|
|
|
|
|
] ),
|
|
|
|
|
$context->getConfig()
|
2017-01-24 17:30:33 +00:00
|
|
|
] ) );
|
|
|
|
|
|
2018-07-25 18:41:42 +00:00
|
|
|
if ( !in_array( 'notitle', (array)$options ) ) {
|
|
|
|
|
$context->setTitle( Title::newFromText( 'My test page' ) );
|
|
|
|
|
}
|
2018-07-23 18:26:32 +00:00
|
|
|
|
|
|
|
|
if ( $request ) {
|
|
|
|
|
$context->setRequest( $request );
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-24 17:30:33 +00:00
|
|
|
return new OutputPage( $context );
|
|
|
|
|
}
|
2013-01-14 03:26:15 +00:00
|
|
|
}
|