Introduce SkinFactory

Modeled similar to ConfigFactory, this lets skins
register themselves via a callback, allowing for
proper dependency injection.

Loading via $wgValidSkinNames is still supported,
but considered "legacy", not deprecated though.

Skin::newFromKey is now deprecated (and had only
one caller in an extension, which I'll update
afterwards).

Change-Id: I1960483f87c2ef55c994545239b728fa376f17f4
This commit is contained in:
Kunal Mehta 2014-08-09 13:36:35 +01:00
parent cccd402c41
commit eb37e9d1ff
11 changed files with 305 additions and 87 deletions

View file

@ -36,7 +36,6 @@ $wgAutoloadLocalClasses = array(
'AuthPlugin' => 'includes/AuthPlugin.php',
'AuthPluginUser' => 'includes/AuthPlugin.php',
'Autopromote' => 'includes/Autopromote.php',
'BaseTemplate' => 'includes/SkinTemplate.php',
'Block' => 'includes/Block.php',
'Category' => 'includes/Category.php',
'Categoryfinder' => 'includes/Categoryfinder.php',
@ -120,7 +119,6 @@ $wgAutoloadLocalClasses = array(
'MagicWordArray' => 'includes/MagicWord.php',
'MailAddress' => 'includes/UserMailer.php',
'MediaWiki' => 'includes/MediaWiki.php',
'MediaWikiI18N' => 'includes/SkinTemplate.php',
'MediaWikiVersionFetcher' => 'includes/MediaWikiVersionFetcher.php',
'Message' => 'includes/Message.php',
'MessageBlobStore' => 'includes/MessageBlobStore.php',
@ -143,7 +141,6 @@ $wgAutoloadLocalClasses = array(
'PreferencesForm' => 'includes/Preferences.php',
'PrefixSearch' => 'includes/PrefixSearch.php',
'ProtectionForm' => 'includes/ProtectionForm.php',
'QuickTemplate' => 'includes/SkinTemplate.php',
'RawMessage' => 'includes/Message.php',
'ReverseChronologicalPager' => 'includes/Pager.php',
'RevisionItem' => 'includes/RevisionList.php',
@ -156,10 +153,6 @@ $wgAutoloadLocalClasses = array(
'SiteConfiguration' => 'includes/SiteConfiguration.php',
'SiteStats' => 'includes/SiteStats.php',
'SiteStatsInit' => 'includes/SiteStats.php',
'Skin' => 'includes/Skin.php',
'SkinTemplate' => 'includes/SkinTemplate.php',
'SkinFallback' => 'includes/SkinFallback.php',
'SkinFallbackTemplate' => 'includes/SkinFallbackTemplate.php',
'SquidPurgeClient' => 'includes/SquidPurgeClient.php',
'SquidPurgeClientPool' => 'includes/SquidPurgeClient.php',
'StatCounter' => 'includes/StatCounter.php',
@ -933,6 +926,17 @@ $wgAutoloadLocalClasses = array(
'Sites' => 'includes/site/SiteSQLStore.php',
'SiteStore' => 'includes/site/SiteStore.php',
# includes/skins
'BaseTemplate' => 'includes/skins/SkinTemplate.php',
'MediaWikiI18N' => 'includes/skins/SkinTemplate.php',
'QuickTemplate' => 'includes/skins/SkinTemplate.php',
'Skin' => 'includes/skins/Skin.php',
'SkinException' => 'includes/skins/SkinException.php',
'SkinFactory' => 'includes/skins/SkinFactory.php',
'SkinFallback' => 'includes/skins/SkinFallback.php',
'SkinFallbackTemplate' => 'includes/skins/SkinFallbackTemplate.php',
'SkinTemplate' => 'includes/skins/SkinTemplate.php',
# includes/specialpage
'ChangesListSpecialPage' => 'includes/specialpage/ChangesListSpecialPage.php',
'FormSpecialPage' => 'includes/specialpage/FormSpecialPage.php',

View file

@ -352,12 +352,19 @@ class RequestContext implements IContextSource {
$skin = null;
wfRunHooks( 'RequestContextCreateSkin', array( $this, &$skin ) );
$fallback = $this->getConfig()->get( 'FallbackSkin' );
$factory = SkinFactory::getDefaultInstance();
// If the hook worked try to set a skin from it
if ( $skin instanceof Skin ) {
$this->skin = $skin;
} elseif ( is_string( $skin ) ) {
$this->skin = Skin::newFromKey( $skin );
try {
$this->skin = $factory->makeSkin( $skin );
} catch ( SkinException $e ) {
$this->skin = $factory->makeSkin( $fallback );
}
}
// If this is still null (the hook didn't run or didn't work)
@ -372,7 +379,12 @@ class RequestContext implements IContextSource {
$userSkin = $this->getConfig()->get( 'DefaultSkin' );
}
$this->skin = Skin::newFromKey( $userSkin );
try {
$this->skin = $factory->makeSkin( $userSkin );
} catch ( SkinException $e ) {
$this->skin = $factory->makeSkin( $fallback );
}
}
// After all that set a context on whatever skin got created

View file

@ -47,60 +47,7 @@ abstract class Skin extends ContextSource {
* @return array Associative array of strings
*/
static function getSkinNames() {
global $wgValidSkinNames;
static $skinsInitialised = false;
if ( !$skinsInitialised || !count( $wgValidSkinNames ) ) {
# Get a list of available skins
# Build using the regular expression '^(.*).php$'
# Array keys are all lower case, array value keep the case used by filename
#
wfProfileIn( __METHOD__ . '-init' );
global $wgStyleDirectory;
$skinDir = dir( $wgStyleDirectory );
if ( $skinDir !== false && $skinDir !== null ) {
# while code from www.php.net
while ( false !== ( $file = $skinDir->read() ) ) {
// Skip non-PHP files, hidden files, and '.dep' includes
$matches = array();
if ( preg_match( '/^([^.]*)\.php$/', $file, $matches ) ) {
$aSkin = $matches[1];
// Explicitly disallow loading core skins via the autodiscovery mechanism.
//
// They should be loaded already (in a non-autodicovery way), but old files might still
// exist on the server because our MW version upgrade process is widely documented as
// requiring just copying over all files, without removing old ones.
//
// This is one of the reasons we should have never used autodiscovery in the first
// place. This hack can be safely removed when autodiscovery is gone.
if ( in_array( $aSkin, array( 'CologneBlue', 'Modern', 'MonoBook', 'Vector' ) ) ) {
wfLogWarning(
"An old copy of the $aSkin skin was found in your skins/ directory. " .
"You should remove it to avoid problems in the future." .
"See https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery for details."
);
continue;
}
wfLogWarning(
"A skin using autodiscovery mechanism, $aSkin, was found in your skins/ directory. " .
"The mechanism will be removed in MediaWiki 1.25 and the skin will no longer be recognized. " .
"See https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery for information how to fix this."
);
$wgValidSkinNames[strtolower( $aSkin )] = $aSkin;
}
}
$skinDir->close();
}
$skinsInitialised = true;
wfProfileOut( __METHOD__ . '-init' );
}
return $wgValidSkinNames;
return SkinFactory::getDefaultInstance()->getSkinNames();
}
/**
@ -197,34 +144,20 @@ abstract class Skin extends ContextSource {
* Factory method for loading a skin of a given type
* @param string $key 'monobook', 'vector', etc.
* @return Skin
* @deprecated Use SkinFactory instead
*/
static function &newFromKey( $key ) {
global $wgStyleDirectory, $wgFallbackSkin;
wfDeprecated( __METHOD__, '1.24' );
global $wgFallbackSkin;
$key = Skin::normalizeKey( $key );
$skinNames = Skin::getSkinNames();
$skinName = $skinNames[$key];
$className = "Skin{$skinName}";
# Grab the skin class and initialise it.
if ( !class_exists( $className ) ) {
require_once "{$wgStyleDirectory}/{$skinName}.php";
# Check if we got if not fallback to default skin
if ( !class_exists( $className ) ) {
# DO NOT die if the class isn't found. This breaks maintenance
# scripts and can cause a user account to be unrecoverable
# except by SQL manipulation if a previously valid skin name
# is no longer valid.
wfDebug( "Skin class does not exist: $className\n" );
$fallback = $skinNames[Skin::normalizeKey( $wgFallbackSkin )];
$className = "Skin{$fallback}";
}
$factory = SkinFactory::getDefaultInstance();
try {
$skin = $factory->makeSkin( $key );
} catch ( SkinException $e ) {
$skin = $factory->makeSkin( $wgFallbackSkin );
}
$skin = new $className( $key );
return $skin;
}

View file

@ -0,0 +1,29 @@
<?php
/**
* Copyright 2014
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* Exceptions for skin-related failures
*
* @since 1.24
*/
class SkinException extends MWException {
}

View file

@ -0,0 +1,202 @@
<?php
/**
* Copyright 2014
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* Factory class to create Skin objects
*
* @since 1.24
*/
class SkinFactory {
/**
* Map of name => callback
* @var array
*/
private $factoryFunctions = array();
/**
* Map of name => human readable name
* @var array
*/
private $displayNames = array();
/**
* @var SkinFactory
*/
private static $self;
public static function getDefaultInstance() {
if ( !self::$self ) {
self::$self = new self;
}
return self::$self;
}
/**
* Register a new Skin factory function
* Will override if it's already registered
* @param string $name
* @param string $displayName
* @param callable $callback That takes the skin name as an argument
* @throws InvalidArgumentException If an invalid callback is provided
*/
public function register( $name, $displayName, $callback ) {
if ( !is_callable( $callback ) ) {
throw new InvalidArgumentException( 'Invalid callback provided' );
}
$this->factoryFunctions[$name] = $callback;
$this->displayNames[$name] = $displayName;
}
/**
* @return array
*/
private function getLegacySkinNames() {
global $wgValidSkinNames;
static $skinsInitialised = false;
if ( !$skinsInitialised || !count( $wgValidSkinNames ) ) {
# Get a list of available skins
# Build using the regular expression '^(.*).php$'
# Array keys are all lower case, array value keep the case used by filename
#
wfProfileIn( __METHOD__ . '-init' );
global $wgStyleDirectory;
$skinDir = dir( $wgStyleDirectory );
if ( $skinDir !== false && $skinDir !== null ) {
# while code from www.php.net
while ( false !== ( $file = $skinDir->read() ) ) {
// Skip non-PHP files, hidden files, and '.dep' includes
$matches = array();
if ( preg_match( '/^([^.]*)\.php$/', $file, $matches ) ) {
$aSkin = $matches[1];
// Explicitly disallow loading core skins via the autodiscovery mechanism.
//
// They should be loaded already (in a non-autodicovery way), but old files might still
// exist on the server because our MW version upgrade process is widely documented as
// requiring just copying over all files, without removing old ones.
//
// This is one of the reasons we should have never used autodiscovery in the first
// place. This hack can be safely removed when autodiscovery is gone.
if ( in_array( $aSkin, array( 'CologneBlue', 'Modern', 'MonoBook', 'Vector' ) ) ) {
wfLogWarning(
"An old copy of the $aSkin skin was found in your skins/ directory. " .
"You should remove it to avoid problems in the future." .
"See https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery for details."
);
continue;
}
wfLogWarning(
"A skin using autodiscovery mechanism, $aSkin, was found in your skins/ directory. " .
"The mechanism will be removed in MediaWiki 1.25 and the skin will no longer be recognized. " .
"See https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery for information how to fix this."
);
$wgValidSkinNames[strtolower( $aSkin )] = $aSkin;
}
}
$skinDir->close();
}
$skinsInitialised = true;
wfProfileOut( __METHOD__ . '-init' );
}
return $wgValidSkinNames;
}
/**
* Returns an associative array of:
* skin name => human readable name
*
* @return array
*/
public function getSkinNames() {
return array_merge(
$this->getLegacySkinNames(),
$this->displayNames
);
}
/**
* Get a legacy skin which uses $wgValidSkinNames
* or autoloading
*
* @param string $name
* @return Skin|bool false if the skin couldn't be constructed
*/
private function getLegacySkin( $name ) {
$skinNames = $this->getLegacySkinNames();
if ( !isset( $skinNames[$name] ) ) {
return false;
}
$skinName = $skinNames[$name];
$className = "Skin{$skinName}";
# Grab the skin class and initialise it.
if ( !class_exists( $className ) ) {
global $wgStyleDirectory;
require_once "{$wgStyleDirectory}/{$skinName}.php";
# Check if we got it
if ( !class_exists( $className ) ) {
# DO NOT die if the class isn't found. This breaks maintenance
# scripts and can cause a user account to be unrecoverable
# except by SQL manipulation if a previously valid skin name
# is no longer valid.
return false;
}
}
$skin = new $className( $name );
return $skin;
}
/**
* Create a given Skin using the registered callback for $name.
* @param string $name Name of the skin you want
* @throws SkinException If a factory function isn't registered for $name
* @throws UnexpectedValueException If the factory function returns a non-Skin object
* @return Skin
*/
public function makeSkin( $name ) {
if ( !isset( $this->factoryFunctions[$name] ) ) {
// Check the legacy method of skin loading
$legacy = $this->getLegacySkin( $name );
if ( $legacy ) {
return $legacy;
}
throw new SkinException( "No registered builder available for $name." );
}
$skin = call_user_func( $this->factoryFunctions[$name], $name );
if ( $skin instanceof Skin ) {
return $skin;
} else {
throw new UnexpectedValueException( "The builder for $name returned a non-Skin object." );
}
}
}

View file

@ -195,7 +195,7 @@ mw.loader.implement("test.quux",function($,jQuery){mw.test.baz({token:123});},{"
$method = $class->getMethod( 'makeResourceLoaderLink' );
$method->setAccessible( true );
$ctx = new RequestContext();
$ctx->setSkin( Skin::newFromKey( 'vector' ) );
$ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) );
$ctx->setLanguage( 'en' );
$out = new OutputPage( $ctx );
$rl = $out->getResourceLoader();

View file

@ -0,0 +1,38 @@
<?php
class SkinFactoryTest extends MediaWikiTestCase {
/**
* @covers SkinFactory::register
*/
public function testRegister() {
$factory = new SkinFactory();
$factory->register( 'fallback', 'Fallback', function() {
return new SkinFallback();
} );
$this->assertTrue( true ); // No exception thrown
$this->setExpectedException( 'InvalidArgumentException' );
$factory->register( 'invalid', 'Invalid', 'Invalid callback' );
}
/**
* @covers SkinFactory::makeSkin
*/
public function testMakeSkinWithNoBuilders() {
$factory = new SkinFactory();
$this->setExpectedException( 'SkinException' );
$factory->makeSkin( 'nobuilderregistered' );
}
/**
* @covers SkinFactory::makeSkin
*/
public function testMakeSkinWithInvalidCallback() {
$factory = new SkinFactory();
$factory->register( 'unittest', 'Unittest', function () {
return true; // Not a Skin object
} );
$this->setExpectedException( 'UnexpectedValueException' );
$factory->makeSkin( 'unittest' );
}
}