*/
protected $dependencies = array();
+ /**
+ * @var string File name containing the body of the skip function
+ */
+ protected $skipFunction = null;
+
/**
* @var array List of message keys used by this module
* @par Usage:
* 'remoteBasePath' => [base path],
* // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath
* 'remoteExtPath' => [base path],
+ * // Equivalent of remoteBasePath, but relative to $wgStylePath
+ * 'remoteSkinPath' => [base path],
* // Scripts to always include
* 'scripts' => [file path string or array of file path strings],
* // Scripts to include in specific language contexts
* 'group' => [group name string],
* // Position on the page to load this module at
* 'position' => ['bottom' (default) or 'top']
+ * // Function that, if it returns true, makes the loader skip this module.
+ * // The file must contain valid JavaScript for execution in a private function.
+ * // The file must not contain the "function () {" and "}" wrapper though.
+ * 'skipFunction' => [file path]
* )
* @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'];
- }
+ // 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;
// Single booleans
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 = $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 );
}
/**
/**
* Get loader script.
*
- * @return string|false JavaScript code to be added to startup module
+ * @return string|bool JavaScript code to be added to startup module
*/
public function getLoaderScript() {
if ( count( $this->loaderScripts ) === 0 ) {
return $this->dependencies;
}
+ /**
+ * Get the skip function.
+ *
+ * @return string|null
+ */
+ public function getSkipFunction() {
+ if ( !$this->skipFunction ) {
+ return null;
+ }
+
+ $localPath = $this->getLocalPath( $this->skipFunction );
+ if ( !file_exists( $localPath ) ) {
+ throw new MWException( __METHOD__ . ": skip function file not found: \"$localPath\"" );
+ }
+ $contents = file_get_contents( $localPath );
+ if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) {
+ $contents = $this->validateScriptFile( $localPath, $contents );
+ }
+ return $contents;
+ }
+
/**
* @return bool
*/
self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ),
$this->loaderScripts
);
+ if ( $this->skipFunction ) {
+ $files[] = $this->skipFunction;
+ }
$files = array_map( array( $this, 'getLocalPath' ), $files );
// File deps need to be treated separately because they're already prefixed
$files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) );
'targets',
'group',
'position',
+ 'skipFunction',
'localBasePath',
'remoteBasePath',
'debugRaw',
/* 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 );
}
/**
}
/**
- * Returns all style files used by this module
+ * Gets a list of file paths for all skin styles in the module used by
+ * the skin.
+ *
+ * @param string $skinName The name of the skin
+ * @return array A list of file paths collated by media type
+ */
+ protected function getSkinStyleFiles( $skinName ) {
+ return self::collateFilePathListByOption(
+ self::tryForKey( $this->skinStyles, $skinName ),
+ 'media',
+ 'all'
+ );
+ }
+
+ /**
+ * Gets a list of file paths for all skin style files in the module,
+ * for all available skins.
+ *
+ * @return array A list of file paths collated by media type
+ */
+ protected function getAllSkinStyleFiles() {
+ $styleFiles = array();
+ $internalSkinNames = array_keys( Skin::getSkinNames() );
+ $internalSkinNames[] = 'default';
+
+ foreach ( $internalSkinNames as $internalSkinName ) {
+ $styleFiles = array_merge_recursive(
+ $styleFiles,
+ $this->getSkinStyleFiles( $internalSkinName )
+ );
+ }
+
+ return $styleFiles;
+ }
+
+ /**
+ * Returns all style files and all skin style files used by this module.
+ *
* @return array
*/
public function getAllStyleFiles() {
- $files = array();
- foreach ( (array)$this->styles as $key => $value ) {
- if ( is_array( $value ) ) {
- $path = $key;
- } else {
- $path = $value;
+ $collatedStyleFiles = array_merge_recursive(
+ self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
+ $this->getAllSkinStyleFiles()
+ );
+
+ $result = array();
+
+ foreach ( $collatedStyleFiles as $media => $styleFiles ) {
+ foreach ( $styleFiles as $styleFile ) {
+ $result[] = $this->getLocalPath( $styleFile );
}
- $files[] = $this->getLocalPath( $path );
}
- return $files;
+
+ return $result;
}
/**
* @return string Concatenated and remapped JavaScript data from $scripts
*/
protected function readScriptFiles( array $scripts ) {
- global $wgResourceLoaderValidateStaticJS;
if ( empty( $scripts ) ) {
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\"" );
}
$contents = file_get_contents( $localPath );
- if ( $wgResourceLoaderValidateStaticJS ) {
+ if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) {
// Static files don't really need to be checked as often; unlike
// on-wiki module they shouldn't change unexpectedly without
// admin interference.
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 );
* @param bool $flip
*
* @return string CSS data in script file
- * @throws MWException if the file doesn't exist
+ * @throws MWException If the file doesn't exist
*/
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
);
}
* @param string $fileName File name of root LESS file.
* @return string Cache key
*/
- protected static function getLESSCacheKey( $fileName ) {
- $vars = json_encode( ResourceLoader::getLESSVars() );
+ protected function getLESSCacheKey( $fileName ) {
+ $vars = json_encode( ResourceLoader::getLESSVars( $this->getConfig() ) );
$hash = md5( $fileName . $vars );
return wfMemcKey( 'resourceloader', 'less', $hash );
}
* @return string CSS source
*/
protected function compileLESSFile( $fileName ) {
- $key = self::getLESSCacheKey( $fileName );
+ $key = $this->getLESSCacheKey( $fileName );
$cache = wfGetCache( CACHE_ANYTHING );
// The input to lessc. Either an associative array representing the
$source = $fileName;
}
- $compiler = ResourceLoader::getLessCompiler();
+ $compiler = ResourceLoader::getLessCompiler( $this->getConfig() );
$result = null;
$result = $compiler->cachedCompile( $source );