gplx/mediawiki/tests/phpunit/includes/skins/SkinTest.php

795 lines
22 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Context\RequestContext;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Interwiki\InterwikiLookup;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MainConfigNames;
use MediaWiki\Output\OutputPage;
use MediaWiki\Page\PageReferenceValue;
use MediaWiki\Permissions\Authority;
use MediaWiki\Request\FauxRequest;
use MediaWiki\Tests\Unit\MockBlockTrait;
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
use MediaWiki\Title\Title;
use MediaWiki\Title\TitleValue;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityLookup;
use MediaWiki\User\UserIdentityValue;
/**
* @covers \Skin
* @group Database
*/
class SkinTest extends MediaWikiIntegrationTestCase {
use MockAuthorityTrait;
use MockBlockTrait;
use TempUserTestTrait;
/**
* @covers \Skin
*/
public function testGetSkinName() {
$skin = new SkinFallback();
$this->assertEquals( 'fallback', $skin->getSkinName(), 'Default' );
$skin = new SkinFallback( 'testname' );
$this->assertEquals( 'testname', $skin->getSkinName(), 'Constructor argument' );
}
public function testGetDefaultModules() {
$skin = $this->getMockBuilder( Skin::class )
->onlyMethods( [ 'outputPage' ] )
->getMock();
$modules = $skin->getDefaultModules();
$this->assertTrue( isset( $modules['core'] ), 'core key is set by default' );
$this->assertTrue( isset( $modules['styles'] ), 'style key is set by default' );
}
/**
* @param bool $isSyndicated
* @param string $html
* @return OutputPage
*/
private function getMockOutputPage( $isSyndicated, $html ) {
$mock = $this->createMock( OutputPage::class );
$mock->expects( $this->once() )
->method( 'isSyndicated' )
->willReturn( $isSyndicated );
$mock->method( 'getHTML' )
->willReturn( $html );
return $mock;
}
public static function provideGetDefaultModulesForOutput() {
return [
[
false,
'',
[]
],
[
true,
'',
[ 'mediawiki.feedlink' ]
],
[
false,
'FOO mw-ui-button BAR',
[ 'mediawiki.ui.button' ]
],
[
true,
'FOO mw-ui-button BAR',
[ 'mediawiki.ui.button', 'mediawiki.feedlink' ]
],
];
}
/**
* @dataProvider provideGetDefaultModulesForOutput
*/
public function testGetDefaultModulesForContent( $isSyndicated, $html, array $expectedModuleStyles ) {
$skin = new class extends Skin {
public function outputPage() {
}
};
$fakeContext = new RequestContext();
$fakeContext->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
$fakeContext->setOutput( $this->getMockOutputPage( $isSyndicated, $html ) );
$skin->setContext( $fakeContext );
$modules = $skin->getDefaultModules();
$actualStylesModule = array_merge( ...array_values( $modules['styles'] ) );
foreach ( $expectedModuleStyles as $expected ) {
$this->assertContains( $expected, $actualStylesModule );
}
}
public function provideGetDefaultModulesForRights() {
yield 'no rights' => [
$this->mockRegisteredNullAuthority(), // $authority
false, // $hasModule
];
yield 'has all rights' => [
$this->mockRegisteredUltimateAuthority(), // $authority
true, // $hasModule
];
}
/**
* @dataProvider provideGetDefaultModulesForRights
*/
public function testGetDefaultModulesForRights( Authority $authority, bool $hasModule ) {
$skin = new class extends Skin {
public function outputPage() {
}
};
$fakeContext = new RequestContext();
$fakeContext->setAuthority( $authority );
$fakeContext->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
$skin->setContext( $fakeContext );
$defaultModules = $skin->getDefaultModules();
$this->assertArrayHasKey( 'watch', $defaultModules );
if ( $hasModule ) {
$this->assertContains( 'mediawiki.page.watch.ajax', $defaultModules['watch'] );
} else {
$this->assertNotContains( 'mediawiki.page.watch.ajax', $defaultModules['watch'] );
}
}
public function providGetPageClasses() {
yield 'normal page has namespace' => [
new TitleValue( NS_MAIN, 'Test' ), // $title
$this->mockRegisteredUltimateAuthority(), // $authority
[ 'ns-0' ], // $expectedClasses
];
yield 'valid special page' => [
new TitleValue( NS_SPECIAL, 'Userlogin' ), // $title
$this->mockRegisteredUltimateAuthority(), // $authority
[ 'mw-special-Userlogin' ], // $expectedClasses
];
yield 'invalid special page' => [
new TitleValue( NS_SPECIAL, 'BLABLABLABLA_I_AM_INVALID' ), // $title
$this->mockRegisteredUltimateAuthority(), // $authority
[ 'mw-invalidspecialpage' ], // $expectedClasses
];
yield 'talk page' => [
new TitleValue( NS_TALK, 'Test' ), // $title
$this->mockRegisteredUltimateAuthority(), // $authority
[ 'ns-talk' ], // $expectedClasses
];
yield 'subject' => [
new TitleValue( NS_MAIN, 'Test' ), // $title
$this->mockRegisteredUltimateAuthority(), // $authority
[ 'ns-subject' ], // $expectedClasses
];
yield 'editable' => [
new TitleValue( NS_MAIN, 'Test' ), // $title
$this->mockRegisteredAuthorityWithPermissions( [ 'edit' ] ), // $authority
[ 'mw-editable' ], // $expectedClasses
];
yield 'not editable' => [
new TitleValue( NS_MAIN, 'Test' ), // $title
$this->mockRegisteredNullAuthority(), // $authority
[], // $expectedClasses
[ 'mw-editable' ], // $unexpectedClasses
];
}
/**
* @dataProvider providGetPageClasses
*/
public function testGetPageClasses(
LinkTarget $title,
Authority $authority,
array $expectedClasses,
array $unexpectedClasses = []
) {
$skin = new class extends Skin {
public function outputPage() {
}
};
$fakeContext = new RequestContext();
$fakeContext->setAuthority( $authority );
$skin->setContext( $fakeContext );
$classes = $skin->getPageClasses( Title::newFromLinkTarget( $title ) );
foreach ( $expectedClasses as $class ) {
$this->assertStringContainsString( $class, $classes );
}
foreach ( $unexpectedClasses as $class ) {
$this->assertStringNotContainsString( $class, $classes );
}
}
/**
* @dataProvider provideSkinResponsiveOptions
*/
public function testIsResponsive( array $options, bool $expected ) {
$skin = new class( $options ) extends Skin {
/**
* @inheritDoc
*/
public function outputPage() {
}
/**
* @inheritDoc
*/
public function getUser() {
$user = TestUserRegistry::getImmutableTestUser( [] )->getUser();
\MediaWiki\MediaWikiServices::getInstance()->getUserOptionsManager()->setOption(
$user,
'skin-responsive',
$this->options['userPreference']
);
return $user;
}
};
$this->assertSame( $expected, $skin->isResponsive() );
}
public static function provideSkinResponsiveOptions() {
yield 'responsive not set' => [
[ 'name' => 'test', 'userPreference' => true ],
false
];
yield 'responsive false' => [
[ 'name' => 'test', 'responsive' => false, 'userPreference' => true ],
false
];
yield 'responsive true' => [
[ 'name' => 'test', 'responsive' => true, 'userPreference' => true ],
true
];
yield 'responsive true, user preference false' => [
[ 'name' => 'test', 'responsive' => true, 'userPreference' => false ],
false
];
yield 'responsive false, user preference false' => [
[ 'name' => 'test', 'responsive' => false, 'userPreference' => false ],
false
];
}
public static function provideMakeLink() {
return [
'Empty href with link class' => [
[
'text' => 'Test',
'href' => '',
'class' => [
'class1',
'class2'
]
],
[ 'link-class' => 'link-class' ],
'<a href="" class="class1 class2 link-class">Test</a>',
],
'link with link-html' => [
[
'text' => '',
'href' => '#go',
'link-html' => '<i>label</i>'
],
[ 'text-wrapper' => [ 'tag' => 'span' ] ],
'<a href="#go"><i>label</i> </a>',
],
'Basic text wrapper' => [
[
'text' => 'Test',
],
[ 'text-wrapper' => [ 'tag' => 'span' ] ],
'<span>Test</span>'
],
'Text wrapper with tooltip ID in id attribute' => [
[
'text' => 'Test',
'id' => 'ii'
],
[ 'text-wrapper' => [ 'tag' => 'span' ] ],
'<span title="(tooltip-ii)">Test</span>'
],
'Text wrapper with tooltip ID in single-id' => [
[
'text' => 'Test',
'id' => 'foo',
'single-id' => 'ii'
],
[ 'text-wrapper' => [ 'tag' => 'span' ] ],
'<span title="(tooltip-ii)">Test</span>'
],
'Multi-level text wrapper with tooltip' => [
[
'text' => 'Test',
'id' => 'ii'
],
[ 'text-wrapper' => [
[ 'tag' => 'b' ],
[ 'tag' => 'i' ]
] ],
'<b title="(tooltip-ii)"><i>Test</i></b>'
],
'Multi-level text wrapper with link' => [
[
'text' => 'Test',
'id' => 'ii',
'href' => '#',
],
[ 'text-wrapper' => [
[ 'tag' => 'b' ],
[ 'tag' => 'i' ]
] ],
'<a id="ii" href="#" title="(tooltip-ii)(word-separator)(brackets: (accesskey-ii))" ' .
'accesskey="(accesskey-ii)"><b><i>Test</i></b></a>'
],
'Specified HTML' => [
[
'html' => '<b>1</b>',
],
[],
'<b>1</b>'
],
'Data attribute' => [
[
'text' => 'Test',
'href' => '#',
'data' => [ 'foo' => 'bar' ]
],
[],
'<a href="#" data-foo="bar">Test</a>'
],
'tooltip only' => [
[
'text' => 'Save',
'id' => 'save',
'href' => '#',
'tooltiponly' => true,
],
[],
'<a id="save" href="#" title="(tooltip-save)">Save</a>'
]
];
}
/**
* @dataProvider provideMakeLink
* @param array $data
* @param array $options
* @param string $expected
*/
public function testMakeLinkLink( array $data, array $options, string $expected ) {
$this->setUserLang( 'qqx' );
$skin = new class extends Skin {
public function outputPage() {
}
};
$link = $skin->makeLink(
'test',
$data,
$options
);
$this->assertHTMLEquals(
$expected,
$link
);
}
public static function provideGetPersonalToolsForMakeListItem() {
return [
[
[
'foo' => [
'class' => 'foo',
'link-html' => '<i>text</i>',
'text' => 'Hello',
],
],
false,
[
'foo' => [
'links' => [
[
'single-id' => 'pt-foo',
'text' => 'Hello',
'link-html' => '<i>text</i>',
'class' => 'foo',
]
],
'id' => 'pt-foo',
]
],
],
[
[
'foo' => [
'class' => 'foo',
'link-html' => '<i>text</i>',
'text' => 'Hello',
],
],
true,
[
'foo' => [
'links' => [
[
'single-id' => 'pt-foo',
'text' => 'Hello',
'link-html' => '<i>text</i>',
]
],
'id' => 'pt-foo',
'class' => 'foo',
]
],
]
];
}
/**
* @dataProvider provideGetPersonalToolsForMakeListItem
* @param array $urls
* @param bool $applyClassesToListItems
* @param array $expected
*/
public function testGetPersonalToolsForMakeListItem( array $urls, bool $applyClassesToListItems, array $expected ) {
$skin = new class extends Skin {
public function outputPage() {
}
};
$this->assertSame(
$expected,
$skin->getPersonalToolsForMakeListItem(
$urls,
$applyClassesToListItems
)
);
}
public function testGetRelevantUser_get_set() {
$skin = new class extends Skin {
public function outputPage() {
}
};
$relevantUser = UserIdentityValue::newRegistered( 1, 'SomeUser' );
$skin->setRelevantUser( $relevantUser );
$this->assertSame( $relevantUser, $skin->getRelevantUser() );
$this->installMockBlockManager(
[
'target' => $relevantUser,
'hideName' => true,
]
);
$ctx = RequestContext::getMain();
$ctx->setAuthority( $this->mockAnonNullAuthority() );
$skin->setContext( $ctx );
$this->assertNull( $skin->getRelevantUser() );
$ctx->setAuthority( $this->mockAnonUltimateAuthority() );
$skin->setContext( $ctx );
$skin->setRelevantUser( $relevantUser );
$skin->setRelevantUser( $relevantUser );
$this->assertSame( $relevantUser, $skin->getRelevantUser() );
}
public static function provideGetRelevantUser_load_from_title() {
yield 'Not user namespace' => [
'relevantPage' => PageReferenceValue::localReference( NS_MAIN, '123.123.123.123' ),
'expectedUser' => null
];
yield 'User namespace' => [
'relevantPage' => PageReferenceValue::localReference( NS_USER, '123.123.123.123' ),
'expectedUser' => UserIdentityValue::newAnonymous( '123.123.123.123' )
];
yield 'User talk namespace' => [
'relevantPage' => PageReferenceValue::localReference( NS_USER_TALK, '123.123.123.123' ),
'expectedUser' => UserIdentityValue::newAnonymous( '123.123.123.123' )
];
yield 'User page subpage' => [
'relevantPage' => PageReferenceValue::localReference( NS_USER, '123.123.123.123/bla' ),
'expectedUser' => UserIdentityValue::newAnonymous( '123.123.123.123' )
];
yield 'Non-registered user with name' => [
'relevantPage' => PageReferenceValue::localReference( NS_USER, 'I_DO_NOT_EXIST' ),
'expectedUser' => null
];
}
/**
* @dataProvider provideGetRelevantUser_load_from_title
*/
public function testGetRelevantUser_load_from_title(
PageReferenceValue $relevantPage,
?UserIdentity $expectedUser
) {
$skin = new class extends Skin {
public function outputPage() {
}
};
$skin->setRelevantTitle( Title::newFromPageReference( $relevantPage ) );
$relevantUser = $skin->getRelevantUser();
if ( $expectedUser ) {
$this->assertTrue( $expectedUser->equals( $relevantUser ) );
} else {
$this->assertNull( $relevantUser );
}
}
public function testGetRelevantUser_load_existing() {
$skin = new class extends Skin {
public function outputPage() {
}
};
$user = new UserIdentityValue( 42, 'foo' );
$userIdentityLookup = $this->createMock( UserIdentityLookup::class );
$userIdentityLookup->method( 'getUserIdentityByName' )
->willReturnCallback( function ( $name ) use ( $user ) {
if ( $name === $user->getName() ) {
return $user;
}
return $this->createMock( UserIdentity::class );
} );
$this->setService( 'UserIdentityLookup', $userIdentityLookup );
$skin->setRelevantTitle(
Title::makeTitle( NS_USER, $user->getName() )
);
$this->assertTrue( $user->equals( $skin->getRelevantUser() ) );
$this->assertSame( $user->getId(), $skin->getRelevantUser()->getId() );
}
public function testBuildSidebarCache() {
// T303007: Skin subclasses and Skin hooks may vary their sidebar contents.
$this->overrideConfigValues( [
MainConfigNames::UseDatabaseMessages => true,
MainConfigNames::EnableSidebarCache => true,
MainConfigNames::SidebarCacheExpiry => 3600,
] );
// Mock time (T344191)
$clock = 1301644800.0;
$this->getServiceContainer()->getMainWANObjectCache()->setMockTime( $clock );
$id = 0;
$this->setTemporaryHook( 'SkinBuildSidebar',
static function ( Skin $skin, array &$bar ) use ( &$id, &$clock ) {
$id++;
$clock += 1.0;
if ( $skin->getSkinName() === 'foo' ) {
$bar['myhook'] = "foo $id";
}
if ( $skin->getSkinName() === 'bar' ) {
$bar['myhook'] = "bar $id";
}
}
);
$context = RequestContext::newExtraneousContext( Title::makeTitle( NS_SPECIAL, 'Blankpage' ) );
$foo1 = new class( 'foo' ) extends Skin {
public function outputPage() {
}
};
$foo2 = new class( 'foo' ) extends Skin {
public function outputPage() {
}
};
$bar = new class( 'bar' ) extends Skin {
public function outputPage() {
}
};
$foo1->setContext( $context );
$foo2->setContext( $context );
$bar->setContext( $context );
$this->assertArrayContains( [ 'myhook' => 'foo 1' ], $foo1->buildSidebar(), 'fresh' );
$clock += 0.01;
$this->assertArrayContains( [ 'myhook' => 'foo 1' ], $foo2->buildSidebar(), 'cache hit' );
$this->assertArrayContains( [ 'myhook' => 'bar 2' ], $bar->buildSidebar(), 'cache miss' );
}
public function testBuildSidebarWithUserAddedContent() {
$this->overrideConfigValues( [
MainConfigNames::UseDatabaseMessages => true,
MainConfigNames::EnableSidebarCache => false
] );
$foo1 = new class( 'foo' ) extends Skin {
public function outputPage() {
}
};
$this->editPage( 'MediaWiki:Sidebar', <<<EOS
* navigation
** mainpage|mainpage-description
** recentchanges-url|recentchanges
** randompage-url|randompage
** helppage|help-mediawiki
* SEARCH
* TOOLBOX
** A|B
* LANGUAGES
** C|D
EOS );
$context = RequestContext::newExtraneousContext( Title::makeTitle( NS_MAIN, 'Main Page' ) );
$foo1->setContext( $context );
$this->assertArrayContains( [ [ 'id' => 'n-B', 'text' => 'B' ] ], $foo1->buildSidebar()['TOOLBOX'], 'Toolbox has user defined links' );
$hasUserDefinedLinks = false;
$languageLinks = $foo1->buildSidebar()['LANGUAGES'];
foreach ( $languageLinks as $languageLink ) {
if ( $languageLink['id'] === 'n-D' ) {
$hasUserDefinedLinks = true;
break;
}
}
$this->assertSame( false, $hasUserDefinedLinks, 'Languages does not support user defined links' );
}
public function testIgnoreAutoblocksInUserLinks() {
// Don't allow extensions to modify the TOOLBOX array
$this->clearHook( 'SidebarBeforeOutput' );
// Set the page to the IP
$this->overrideConfigValues( [
MainConfigNames::UploadNavigationUrl => false,
MainConfigNames::EnableUploads => false,
MainConfigNames::EnableSpecialMute => false,
] );
$skin = new class extends Skin {
public function outputPage() {
}
};
$context = RequestContext::newExtraneousContext( Title::makeTitle( NS_SPECIAL, 'Contributions/127.0.0.1' ) );
$context->setUser( $this->getTestSysop()->getUser() );
$skin->setContext( $context );
$anonymousUser = $this->getServiceContainer()->getUserFactory()
->newFromUserIdentity( UserIdentityValue::newAnonymous( '127.0.0.1' ) );
$skin->setRelevantUser( $anonymousUser );
// Block a user and autoblock the IP as well
$blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
$this->assertNull( $blockStore->newFromTarget( '127.0.0.1' ) );
$user = $this->getTestUser()->getUser();
RequestContext::getMain()->getRequest()->setIP( '127.0.0.1' );
$this->editPage(
'Test page', 'Test Content', 'test', NS_MAIN, $user
);
$block = new DatabaseBlock( [
'address' => $user->getName(),
'by' => $user,
'reason' => 'test',
'timestamp' => wfTimestampNow(),
'expiry' => 'infinity',
'enableAutoblock' => true,
] );
$blockStore->insertBlock( $block );
$this->assertNotNull( $blockStore->newFromTarget( '127.0.0.1' ), 'Autoblock exists' );
// Assert that the autoblock is ignored and the ip remains blockable
$result = array_keys( $skin->buildSidebar()['TOOLBOX'] );
$expected = [ 'contributions', 'log', 'blockip' ];
$this->assertArrayEquals(
$expected,
array_intersect( $expected, $result )
);
}
public function testBuildSidebarForContributionsPageOfTemporaryAccount() {
// Don't allow extensions to modify the TOOLBOX array as we assert pretty strictly against it.
$this->clearHook( 'SidebarBeforeOutput' );
$this->overrideConfigValues( [
MainConfigNames::UploadNavigationUrl => false,
MainConfigNames::EnableUploads => false,
MainConfigNames::EnableSpecialMute => true,
] );
$foo1 = new class( 'foo' ) extends Skin {
public function outputPage() {
}
};
// Simulate the settings and context for Special:Contributions for a temporary account
// (no article related and relevant user set).
$this->enableAutoCreateTempUser();
$tempUser = $this->getServiceContainer()->getTempUserCreator()
->create( null, new FauxRequest() )->getUser();
$context = RequestContext::newExtraneousContext(
Title::makeTitle( NS_SPECIAL, 'Contributions/' . $tempUser->getName() )
);
$context->setUser( $this->getTestSysop()->getUser() );
$foo1->setContext( $context );
$foo1->setRelevantUser( $tempUser );
$foo1->getOutput()->setArticleRelated( false );
// Verify that the "userrights" key is not present, by checking that the list of keys is as expected.
$this->assertArrayEquals(
[ 'contributions', 'log', 'blockip', 'mute', 'print', 'specialpages' ],
array_keys( $foo1->buildSidebar()['TOOLBOX'] )
);
}
public function testGetLanguagesHidden() {
$this->overrideConfigValues( [
MainConfigNames::HideInterlanguageLinks => true,
] );
$skin = new class extends Skin {
public function outputPage() {
}
};
$this->assertSame( [], $skin->getLanguages() );
}
public function testGetLanguages() {
$this->overrideConfigValues( [
MainConfigNames::HideInterlanguageLinks => false,
MainConfigNames::InterlanguageLinkCodeMap => [ 'talk' => 'fr' ],
MainConfigNames::LanguageCode => 'qqx',
] );
$mockOutputPage = $this->createMock( OutputPage::class );
$mockOutputPage->method( 'getLanguageLinks' )
// The 'talk' interwiki is a deliberate conflict with the
// Talk namespace (T363538)
->willReturn( [ 'en:Foo', 'talk:Page' ] );
$fakeContext = new RequestContext();
$fakeContext->setTitle( Title::makeTitle( NS_MAIN, 'Test' ) );
$fakeContext->setOutput( $mockOutputPage );
$fakeContext->setLanguage( 'en' );
$hookContainer = $this->createMock( HookContainer::class );
$this->setService( 'HookContainer', $hookContainer );
$mockIwLookup = $this->createMock( InterwikiLookup::class );
$mockIwLookup->method( 'isValidInterwiki' )->willReturn( true );
$mockIwLookup->method( 'fetch' )->willReturnCallback( static function ( string $prefix ) {
return new Interwiki(
$prefix,
"https://$prefix.example.com/$1"
);
} );
$this->setService( 'InterwikiLookup', $mockIwLookup );
$skin = new class extends Skin {
public function outputPage() {
}
};
$skin->setContext( $fakeContext );
$this->assertSame( [
[
'href' => 'https://en.example.com/Foo',
'text' => 'English',
'title' => 'Foo English',
'class' => 'interlanguage-link interwiki-en',
'link-class' => 'interlanguage-link-target',
'lang' => 'en',
'hreflang' => 'en',
'data-title' => 'Foo',
'data-language-autonym' => 'English',
'data-language-local-name' => 'English',
],
[
'href' => 'https://talk.example.com/Page',
'text' => 'Français',
'title' => 'Page français',
'class' => 'interlanguage-link interwiki-talk',
'link-class' => 'interlanguage-link-target',
'lang' => 'fr',
'hreflang' => 'fr',
'data-title' => 'Page',
'data-language-autonym' => 'Français',
'data-language-local-name' => 'français',
],
], $skin->getLanguages() );
}
}