* MediaWiki now supports multiple password types, including bcrypt and PBKDF2.
The default type can be changed with $wgPasswordDefault and the type
configurations can be changed with $wgPasswordConfig.
+* Skins can now define custom styles for default ResourceLoader modules using
+ the $wgResourceModuleSkinStyles global. See the Vector skin for examples.
=== Bug fixes in 1.24 ===
* (bug 49116) Footer copyright notice is now always displayed in user language
'ResourceLoaderContext' => 'includes/resourceloader/ResourceLoaderContext.php',
'ResourceLoaderFileModule' => 'includes/resourceloader/ResourceLoaderFileModule.php',
'ResourceLoaderFilePageModule' => 'includes/resourceloader/ResourceLoaderFilePageModule.php',
+ 'ResourceLoaderFilePath' => 'includes/resourceloader/ResourceLoaderFilePath.php',
'ResourceLoaderLESSFunctions' => 'includes/resourceloader/ResourceLoaderLESSFunctions.php',
'ResourceLoaderModule' => 'includes/resourceloader/ResourceLoaderModule.php',
'ResourceLoaderNoscriptModule' => 'includes/resourceloader/ResourceLoaderNoscriptModule.php',
*/
$wgResourceModules = array();
+/**
+ * Skin-specific styles for resource modules.
+ *
+ * These are later added to the 'skinStyles' list of the existing module. The 'styles' list can
+ * not be modified or disabled.
+ *
+ * For example, here is a module "bar" and how skin Foo would provide additional styles for it.
+ *
+ * @par Example:
+ * @code
+ * $wgResourceModules['bar'] = array(
+ * 'scripts' => 'resources/bar/bar.js',
+ * 'styles' => 'resources/bar/main.css',
+ * );
+ *
+ * $wgResourceModuleSkinStyles['foo'] = array(
+ * 'bar' => 'skins/Foo/bar.css',
+ * );
+ * @endcode
+ *
+ * This is mostly equivalent to:
+ *
+ * @par Equivalent:
+ * @code
+ * $wgResourceModules['bar'] = array(
+ * 'scripts' => 'resources/bar/bar.js',
+ * 'styles' => 'resources/bar/main.css',
+ * 'skinStyles' => array(
+ * 'foo' => skins/Foo/bar.css',
+ * ),
+ * );
+ * @endcode
+ *
+ * If the module already defines its own entry in `skinStyles` for a given skin, then
+ * $wgResourceModuleSkinStyles is ignored.
+ *
+ * If a module defines a `skinStyles['default']` the skin may want to extend that instead
+ * of replacing them. This can be done using the `+` prefix.
+ *
+ * @par Example:
+ * @code
+ * $wgResourceModules['bar'] = array(
+ * 'scripts' => 'resources/bar/bar.js',
+ * 'styles' => 'resources/bar/basic.css',
+ * 'skinStyles' => array(
+ * 'default' => 'resources/bar/additional.css',
+ * ),
+ * );
+ * // Note the '+' character:
+ * $wgResourceModuleSkinStyles['+foo'] = array(
+ * 'bar' => 'skins/Foo/bar.css',
+ * );
+ * @endcode
+ *
+ * This is mostly equivalent to:
+ *
+ * @par Equivalent:
+ * @code
+ * $wgResourceModules['bar'] = array(
+ * 'scripts' => 'resources/bar/bar.js',
+ * 'styles' => 'resources/bar/basic.css',
+ * 'skinStyles' => array(
+ * 'default' => 'resources/bar/additional.css',
+ * 'foo' => array(
+ * 'resources/bar/additional.css',
+ * 'skins/Foo/bar.css',
+ * ),
+ * ),
+ * );
+ * @endcode
+ *
+ * In other words, as a module author, use the `styles` list for stylesheets that may not be
+ * disabled by a skin. To provide default styles that may be extended or replaced,
+ * use `skinStyles['default']`.
+ *
+ * As with $wgResourceModules, paths default to being relative to the MediaWiki root.
+ * You should always provide a localBasePath and remoteBasePath (or remoteExtPath/remoteSkinPath).
+ * Either for all skin styles at once (first example below) or for each module separately (second
+ * example).
+ *
+ * @par Example:
+ * @code
+ * $wgResourceModuleSkinStyles['foo'] = array(
+ * 'bar' => 'bar.css',
+ * 'quux' => 'quux.css',
+ * 'remoteSkinPath' => 'Foo',
+ * 'localBasePath' => __DIR__,
+ * );
+ *
+ * $wgResourceModuleSkinStyles['foo'] = array(
+ * 'bar' => array(
+ * 'bar.css',
+ * 'remoteSkinPath' => 'Foo',
+ * 'localBasePath' => __DIR__,
+ * ),
+ * 'quux' => array(
+ * 'quux.css',
+ * 'remoteSkinPath' => 'Foo',
+ * 'localBasePath' => __DIR__,
+ * ),
+ * );
+ * @endcode
+ */
+$wgResourceModuleSkinStyles = array();
+
/**
* Extensions should register foreign module sources here. 'local' is a
* built-in source that is not in this array, but defined by
'\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')'
);
}
+
+ // Last-minute changes
+
+ // Apply custom skin-defined styles to existing modules.
+ if ( $this->isFileModule( $name ) ) {
+ global $wgResourceModuleSkinStyles;
+ foreach ( $wgResourceModuleSkinStyles as $skinName => $skinStyles ) {
+ // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
+ if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) {
+ continue;
+ }
+
+ // If $name is preceded with a '+', the defined style files will be added to 'default'
+ // skinStyles, otherwise 'default' will be ignored as it normally would be.
+ if ( isset( $skinStyles[ $name ] ) ) {
+ $paths = (array)$skinStyles[ $name ];
+ $styleFiles = array();
+ } else if ( isset( $skinStyles[ '+' . $name ] ) ) {
+ $paths = (array)$skinStyles[ '+' . $name ];
+ $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ?
+ $this->moduleInfos[$name]['skinStyles']['default'] :
+ array();
+ } else {
+ continue;
+ }
+
+ // Add new file paths, remapping them to refer to our directories and not use settings
+ // from the module we're modifying. These can come from the base definition or be defined
+ // for each module.
+ list( $localBasePath, $remoteBasePath ) =
+ ResourceLoaderFileModule::extractBasePaths( $skinStyles );
+ list( $localBasePath, $remoteBasePath ) =
+ ResourceLoaderFileModule::extractBasePaths( $paths, $localBasePath, $remoteBasePath );
+
+ foreach ( $paths as $path ) {
+ $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
+ }
+
+ $this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles;
+ }
+ }
}
wfProfileOut( __METHOD__ );
return $this->modules[$name];
}
+ /**
+ * Return whether the definition of a module corresponds to a simple ResourceLoaderFileModule.
+ *
+ * @param string $name Module name
+ * @return boolean
+ */
+ protected function isFileModule( $name ) {
+ if ( !isset( $this->moduleInfos[$name] ) ) {
+ return false;
+ }
+ $info = $this->moduleInfos[$name];
+ if ( isset( $info['object'] ) || isset( $info['class'] ) ) {
+ return false;
+ }
+ return true;
+ }
+
/**
* Get the list of sources.
*
* )
* @endcode
*/
- public function __construct( $options = array(), $localBasePath = null,
+ public function __construct(
+ $options = array(),
+ $localBasePath = null,
$remoteBasePath = null
) {
- global $IP, $wgScriptPath, $wgResourceBasePath;
- $this->localBasePath = $localBasePath === null ? $IP : $localBasePath;
- if ( $remoteBasePath !== null ) {
- $this->remoteBasePath = $remoteBasePath;
- } else {
- $this->remoteBasePath = $wgResourceBasePath === null ? $wgScriptPath : $wgResourceBasePath;
- }
-
- if ( isset( $options['remoteExtPath'] ) ) {
- global $wgExtensionAssetsPath;
- $this->remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
- }
-
- if ( isset( $options['remoteSkinPath'] ) ) {
- global $wgStylePath;
- $this->remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath'];
- }
+ // localBasePath and remoteBasePath both have unbelievably long fallback chains
+ // and need to be handled separately.
+ list( $this->localBasePath, $this->remoteBasePath ) =
+ self::extractBasePaths( $options, $localBasePath, $remoteBasePath );
+ // Extract, validate and normalise remaining options
foreach ( $options as $member => $option ) {
switch ( $member ) {
// Lists of file paths
// Single strings
case 'group':
case 'position':
- case 'localBasePath':
- case 'remoteBasePath':
case 'skipFunction':
$this->{$member} = (string)$option;
break;
break;
}
}
+ }
+
+ /**
+ * Extract a pair of local and remote base paths from module definition information.
+ * Implementation note: the amount of global state used in this function is staggering.
+ *
+ * @param array $options Module definition
+ * @param string $localBasePath Path to use if not provided in module definition. Defaults
+ * to $IP
+ * @param string $remoteBasePath Path to use if not provided in module definition. Defaults
+ * to $wgScriptPath
+ * @return array array( localBasePath, remoteBasePath )
+ */
+ public static function extractBasePaths(
+ $options = array(),
+ $localBasePath = null,
+ $remoteBasePath = null
+ ) {
+ global $IP, $wgScriptPath, $wgResourceBasePath;
+
+ // The different ways these checks are done, and their ordering, look very silly,
+ // but were preserved for backwards-compatibility just in case. Tread lightly.
+
+ $localBasePath = $localBasePath === null ? $IP : $localBasePath;
+ if ( $remoteBasePath !== null ) {
+ $remoteBasePath = $remoteBasePath;
+ } else {
+ $remoteBasePath = $wgResourceBasePath === null ? $wgScriptPath : $wgResourceBasePath;
+ }
+
+ if ( isset( $options['remoteExtPath'] ) ) {
+ global $wgExtensionAssetsPath;
+ $remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
+ }
+
+ if ( isset( $options['remoteSkinPath'] ) ) {
+ global $wgStylePath;
+ $remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath'];
+ }
+
+ if ( array_key_exists( 'localBasePath', $options ) ) {
+ $localBasePath = (string)$options['localBasePath'];
+ }
+
+ if ( array_key_exists( 'remoteBasePath', $options ) ) {
+ $remoteBasePath = (string)$options['remoteBasePath'];
+ }
+
// Make sure the remote base path is a complete valid URL,
// but possibly protocol-relative to avoid cache pollution
- $this->remoteBasePath = wfExpandUrl( $this->remoteBasePath, PROTO_RELATIVE );
+ $remoteBasePath = wfExpandUrl( $remoteBasePath, PROTO_RELATIVE );
+
+ return array( $localBasePath, $remoteBasePath );
}
/**
/* Protected Methods */
/**
- * @param string $path
+ * @param string|ResourceLoaderFilePath $path
* @return string
*/
protected function getLocalPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getLocalPath();
+ }
+
return "{$this->localBasePath}/$path";
}
/**
- * @param string $path
+ * @param string|ResourceLoaderFilePath $path
* @return string
*/
protected function getRemotePath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getRemotePath();
+ }
+
return "{$this->remoteBasePath}/$path";
}
$files = array_merge( $files, $this->debugScripts );
}
- return array_unique( $files );
+ return array_unique( $files, SORT_REGULAR );
}
/**
return '';
}
$js = '';
- foreach ( array_unique( $scripts ) as $fileName ) {
+ foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
$localPath = $this->getLocalPath( $fileName );
if ( !file_exists( $localPath ) ) {
throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" );
return array();
}
foreach ( $styles as $media => $files ) {
- $uniqueFiles = array_unique( $files );
+ $uniqueFiles = array_unique( $files, SORT_REGULAR );
$styleFiles = array();
foreach ( $uniqueFiles as $file ) {
$styleFiles[] = $this->readStyleFile( $file, $flip );
*/
protected function readStyleFile( $path, $flip ) {
$localPath = $this->getLocalPath( $path );
+ $remotePath = $this->getRemotePath( $path );
if ( !file_exists( $localPath ) ) {
$msg = __METHOD__ . ": style file not found: \"$localPath\"";
wfDebugLog( 'resourceloader', $msg );
throw new MWException( $msg );
}
- if ( $this->getStyleSheetLang( $path ) === 'less' ) {
+ if ( $this->getStyleSheetLang( $localPath ) === 'less' ) {
$style = $this->compileLESSFile( $localPath );
$this->hasGeneratedStyles = true;
} else {
if ( $flip ) {
$style = CSSJanus::transform( $style, true, false );
}
- $dirname = dirname( $path );
- if ( $dirname == '.' ) {
- // If $path doesn't have a directory component, don't prepend a dot
- $dirname = '';
- }
- $dir = $this->getLocalPath( $dirname );
- $remoteDir = $this->getRemotePath( $dirname );
+ $localDir = dirname( $localPath );
+ $remoteDir = dirname( $remotePath );
// Get and register local file references
$this->localFileRefs = array_merge(
$this->localFileRefs,
- CSSMin::getLocalFileReferences( $style, $dir )
+ CSSMin::getLocalFileReferences( $style, $localDir )
);
return CSSMin::remap(
- $style, $dir, $remoteDir, true
+ $style, $localDir, $remoteDir, true
);
}
--- /dev/null
+<?php
+/**
+ * An object to represent a path to a JavaScript/CSS file, along with a remote
+ * and local base path, for use with ResourceLoaderFileModule.
+ *
+ * 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
+ */
+
+/**
+ * An object to represent a path to a JavaScript/CSS file, along with a remote
+ * and local base path, for use with ResourceLoaderFileModule.
+ */
+class ResourceLoaderFilePath {
+ /* Protected Members */
+
+ /** @var string Local base path */
+ protected $localBasePath;
+
+ /** @var string Remote base path */
+ protected $remoteBasePath;
+
+ /**
+ * @var string Path to the file */
+ protected $path;
+
+ /* Methods */
+
+ /**
+ * @param string $path Path to the file.
+ * @param string $localBasePath Base path to prepend when generating a local path.
+ * @param string $remoteBasePath Base path to prepend when generating a remote path.
+ */
+ public function __construct( $path, $localBasePath, $remoteBasePath ) {
+ $this->path = $path;
+ $this->localBasePath = $localBasePath;
+ $this->remoteBasePath = $remoteBasePath;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLocalPath() {
+ return "{$this->localBasePath}/{$this->path}";
+ }
+
+ /**
+ * @return string
+ */
+ public function getRemotePath() {
+ return "{$this->remoteBasePath}/{$this->path}";
+ }
+
+ /**
+ * @return string
+ */
+ public function getPath() {
+ return $this->path;
+ }
+}
'mediawiki.special' => array(
'scripts' => 'resources/src/mediawiki.special/mediawiki.special.js',
'styles' => 'resources/src/mediawiki.special/mediawiki.special.css',
- 'skinStyles' => array(
- 'vector' => 'skins/Vector/special.less', // FIXME this should use $wgStyleDirectory
- ),
),
'mediawiki.special.block' => array(
'scripts' => 'resources/src/mediawiki.special/mediawiki.special.block.js',
'scripts' => 'resources/src/mediawiki.special/mediawiki.special.preferences.js',
'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.css',
'position' => 'top',
- 'skinStyles' => array(
- 'vector' => 'skins/Vector/special.preferences.less', // FIXME this should use $wgStyleDirectory
- ),
'messages' => array(
'prefs-tabs-navigation-hint',
),
'remoteSkinPath' => 'Vector',
'localBasePath' => __DIR__,
);
+
+// Apply module customizations
+$wgResourceModuleSkinStyles['vector'] = array(
+ 'mediawiki.special' => 'special.less',
+ 'mediawiki.special.preferences' => 'special.preferences.less',
+ 'remoteSkinPath' => 'Vector',
+ 'localBasePath' => __DIR__,
+);
$cases[] = array(
$method->invoke( $module, $file ),
$moduleName,
- $file,
+ ( $file instanceof ResourceLoaderFilePath ? $file->getPath() : $file ),
);
}
}