3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
19 * @author Trevor Parscal
20 * @author Roan Kattouw
23 defined( 'MEDIAWIKI' ) ||
die( 1 );
26 * Module based on local JS/CSS files. This is the most common type of module.
28 class ResourceLoaderFileModule
extends ResourceLoaderModule
{
29 /* Protected Members */
31 protected $scripts = array();
32 protected $styles = array();
33 protected $messages = array();
35 protected $dependencies = array();
36 protected $debugScripts = array();
37 protected $languageScripts = array();
38 protected $skinScripts = array();
39 protected $skinStyles = array();
40 protected $loaders = array();
41 protected $parameters = array();
43 // In-object cache for file dependencies
44 protected $fileDeps = array();
45 // In-object cache for mtime
46 protected $modifiedTime = array();
51 * Construct a new module from an options array.
53 * @param $options array Options array. If empty, an empty module will be constructed
57 * // Required module options (mutually exclusive)
58 * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
60 * // Optional module options
61 * 'languageScripts' => array(
62 * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
65 * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
66 * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
68 * // Non-raw module options
69 * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
70 * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
71 * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
72 * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
73 * 'skinStyles' => array(
74 * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
75 * array( 'dir/file1.css' => array( 'media' => 'print' )
78 * 'messages' => array( 'message1', 'message2' ... ),
82 * @param $basePath String: base path to prepend to all paths in $options
84 public function __construct( $options = array(), $basePath = null ) {
85 foreach ( $options as $option => $value ) {
89 case 'languageScripts':
92 $this->{$option} = (array)$value;
93 // Automatically prefix script paths
94 if ( is_string( $basePath ) ) {
95 foreach ( $this->{$option} as $key => $value ) {
96 $this->{$option}[$key] = $basePath . $value;
102 $this->{$option} = (array)$value;
103 // Automatically prefix style paths
104 if ( is_string( $basePath ) ) {
105 foreach ( $this->{$option} as $key => $value ) {
106 if ( is_array( $value ) ) {
107 $this->{$option}[$basePath . $key] = $value;
108 unset( $this->{$option}[$key] );
110 $this->{$option}[$key] = $basePath . $value;
117 $this->{$option} = (array)$value;
120 $this->group
= (string)$value;
127 * Add script files to this module. In order to be valid, a module
128 * must contain at least one script file.
130 * @param $scripts Mixed: path to script file (string) or array of paths
132 public function addScripts( $scripts ) {
133 $this->scripts
= array_merge( $this->scripts
, (array)$scripts );
137 * Add style (CSS) files to this module.
139 * @param $styles Mixed: path to CSS file (string) or array of paths
141 public function addStyles( $styles ) {
142 $this->styles
= array_merge( $this->styles
, (array)$styles );
146 * Add messages to this module.
148 * @param $messages Mixed: message key (string) or array of message keys
150 public function addMessages( $messages ) {
151 $this->messages
= array_merge( $this->messages
, (array)$messages );
155 * Sets the group of this module.
157 * @param $group string group name
159 public function setGroup( $group ) {
160 $this->group
= $group;
164 * Add dependencies. Dependency information is taken into account when
165 * loading a module on the client side. When adding a module on the
166 * server side, dependency information is NOT taken into account and
167 * YOU are responsible for adding dependent modules as well. If you
168 * don't do this, the client side loader will send a second request
169 * back to the server to fetch the missing modules, which kind of
170 * defeats the point of using the resource loader in the first place.
172 * To add dependencies dynamically on the client side, use a custom
173 * loader (see addLoaders())
175 * @param $dependencies Mixed: module name (string) or array of module names
177 public function addDependencies( $dependencies ) {
178 $this->dependencies
= array_merge( $this->dependencies
, (array)$dependencies );
182 * Add debug scripts to the module. These scripts are only included
185 * @param $scripts Mixed: path to script file (string) or array of paths
187 public function addDebugScripts( $scripts ) {
188 $this->debugScripts
= array_merge( $this->debugScripts
, (array)$scripts );
192 * Add language-specific scripts. These scripts are only included for
195 * @param $lang String: language code
196 * @param $scripts Mixed: path to script file (string) or array of paths
198 public function addLanguageScripts( $lang, $scripts ) {
199 $this->languageScripts
= array_merge_recursive(
200 $this->languageScripts
,
201 array( $lang => $scripts )
206 * Add skin-specific scripts. These scripts are only included for
209 * @param $skin String: skin name, or 'default'
210 * @param $scripts Mixed: path to script file (string) or array of paths
212 public function addSkinScripts( $skin, $scripts ) {
213 $this->skinScripts
= array_merge_recursive(
215 array( $skin => $scripts )
220 * Add skin-specific CSS. These CSS files are only included for a
221 * given skin. If there are no skin-specific CSS files for a skin,
222 * the files defined for 'default' will be used, if any.
224 * @param $skin String: skin name, or 'default'
225 * @param $scripts Mixed: path to CSS file (string) or array of paths
227 public function addSkinStyles( $skin, $scripts ) {
228 $this->skinStyles
= array_merge_recursive(
230 array( $skin => $scripts )
235 * Add loader scripts. These scripts are loaded on every page and are
236 * responsible for registering this module using
237 * mediaWiki.loader.register(). If there are no loader scripts defined,
238 * the resource loader will register the module itself.
240 * Loader scripts are used to determine a module's dependencies
241 * dynamically on the client side (e.g. based on browser type/version).
242 * Note that loader scripts are included on every page, so they should
243 * be lightweight and use mediaWiki.loader.register()'s callback
244 * feature to defer dependency calculation.
246 * @param $scripts Mixed: path to script file (string) or array of paths
248 public function addLoaders( $scripts ) {
249 $this->loaders
= array_merge( $this->loaders
, (array)$scripts );
252 public function getScript( ResourceLoaderContext
$context ) {
253 $retval = $this->getPrimaryScript() . "\n" .
254 $this->getLanguageScript( $context->getLanguage() ) . "\n" .
255 $this->getSkinScript( $context->getSkin() );
257 if ( $context->getDebug() ) {
258 $retval .= $this->getDebugScript();
264 public function getStyles( ResourceLoaderContext
$context ) {
266 foreach ( $this->getPrimaryStyles() as $media => $style ) {
267 if ( !isset( $styles[$media] ) ) {
268 $styles[$media] = '';
270 $styles[$media] .= $style;
272 foreach ( $this->getSkinStyles( $context->getSkin() ) as $media => $style ) {
273 if ( !isset( $styles[$media] ) ) {
274 $styles[$media] = '';
276 $styles[$media] .= $style;
279 // Collect referenced files
281 foreach ( $styles as $style ) {
282 // Extract and store the list of referenced files
283 $files = array_merge( $files, CSSMin
::getLocalFileReferences( $style ) );
286 // Only store if modified
287 if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
288 $encFiles = FormatJson
::encode( $files );
289 $dbw = wfGetDB( DB_MASTER
);
290 $dbw->replace( 'module_deps',
291 array( array( 'md_module', 'md_skin' ) ), array(
292 'md_module' => $this->getName(),
293 'md_skin' => $context->getSkin(),
294 'md_deps' => $encFiles,
302 public function getMessages() {
303 return $this->messages
;
306 public function getGroup() {
310 public function getDependencies() {
311 return $this->dependencies
;
314 public function getLoaderScript() {
315 if ( count( $this->loaders
) == 0 ) {
319 return self
::concatScripts( $this->loaders
);
323 * Get the last modified timestamp of this module, which is calculated
324 * as the highest last modified timestamp of its constituent files and
325 * the files it depends on (see getFileDependencies()). Only files
326 * relevant to the given language and skin are taken into account, and
327 * files only relevant in debug mode are not taken into account when
330 * @param $context ResourceLoaderContext object
331 * @return Integer: UNIX timestamp
333 public function getModifiedTime( ResourceLoaderContext
$context ) {
334 if ( isset( $this->modifiedTime
[$context->getHash()] ) ) {
335 return $this->modifiedTime
[$context->getHash()];
337 wfProfileIn( __METHOD__
);
339 // Sort of nasty way we can get a flat list of files depended on by all styles
341 foreach ( self
::organizeFilesByOption( $this->styles
, 'media', 'all' ) as $styleFiles ) {
342 $styles = array_merge( $styles, $styleFiles );
344 $skinFiles = (array) self
::getSkinFiles(
345 $context->getSkin(), self
::organizeFilesByOption( $this->skinStyles
, 'media', 'all' )
347 foreach ( $skinFiles as $styleFiles ) {
348 $styles = array_merge( $styles, $styleFiles );
351 // Final merge, this should result in a master list of dependent files
352 $files = array_merge(
355 $context->getDebug() ?
$this->debugScripts
: array(),
356 isset( $this->languageScripts
[$context->getLanguage()] ) ?
357 (array) $this->languageScripts
[$context->getLanguage()] : array(),
358 (array) self
::getSkinFiles( $context->getSkin(), $this->skinScripts
),
360 $this->getFileDependencies( $context->getSkin() )
363 wfProfileIn( __METHOD__
.'-filemtime' );
364 $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__
, 'remapFilename' ), $files ) ) );
365 wfProfileOut( __METHOD__
.'-filemtime' );
366 $this->modifiedTime
[$context->getHash()] = max( $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) );
367 wfProfileOut( __METHOD__
);
368 return $this->modifiedTime
[$context->getHash()];
371 /* Protected Members */
374 * Get the primary JS for this module. This is pulled from the
375 * script files added through addScripts()
379 protected function getPrimaryScript() {
380 return self
::concatScripts( $this->scripts
);
384 * Get the primary CSS for this module. This is pulled from the CSS
385 * files added through addStyles()
389 protected function getPrimaryStyles() {
390 return self
::concatStyles( $this->styles
);
394 * Get the debug JS for this module. This is pulled from the script
395 * files added through addDebugScripts()
399 protected function getDebugScript() {
400 return self
::concatScripts( $this->debugScripts
);
404 * Get the language-specific JS for a given language. This is pulled
405 * from the language-specific script files added through addLanguageScripts()
409 protected function getLanguageScript( $lang ) {
410 if ( !isset( $this->languageScripts
[$lang] ) ) {
413 return self
::concatScripts( $this->languageScripts
[$lang] );
417 * Get the skin-specific JS for a given skin. This is pulled from the
418 * skin-specific JS files added through addSkinScripts()
422 protected function getSkinScript( $skin ) {
423 return self
::concatScripts( self
::getSkinFiles( $skin, $this->skinScripts
) );
427 * Get the skin-specific CSS for a given skin. This is pulled from the
428 * skin-specific CSS files added through addSkinStyles()
430 * @return Array: list of CSS strings keyed by media type
432 protected function getSkinStyles( $skin ) {
433 return self
::concatStyles( self
::getSkinFiles( $skin, $this->skinStyles
) );
437 * Helper function to get skin-specific data from an array.
439 * @param $skin String: skin name
440 * @param $map Array: map of skin names to arrays
441 * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
443 protected static function getSkinFiles( $skin, $map ) {
446 if ( isset( $map[$skin] ) && $map[$skin] ) {
447 $retval = $map[$skin];
448 } else if ( isset( $map['default'] ) ) {
449 $retval = $map['default'];
456 * Get the contents of a set of files and concatenate them, with
457 * newlines in between. Each file is used only once.
459 * @param $files Array of file names
460 * @return String: concatenated contents of $files
462 protected static function concatScripts( $files ) {
463 return implode( "\n",
467 array( __CLASS__
, 'remapFilename' ),
468 array_unique( (array) $files ) ) ) );
471 protected static function organizeFilesByOption( $files, $option, $default ) {
472 $organizedFiles = array();
473 foreach ( (array) $files as $key => $value ) {
474 if ( is_int( $key ) ) {
475 // File name as the value
476 if ( !isset( $organizedFiles[$default] ) ) {
477 $organizedFiles[$default] = array();
479 $organizedFiles[$default][] = $value;
480 } else if ( is_array( $value ) ) {
481 // File name as the key, options array as the value
482 $media = isset( $value[$option] ) ?
$value[$option] : $default;
483 if ( !isset( $organizedFiles[$media] ) ) {
484 $organizedFiles[$media] = array();
486 $organizedFiles[$media][] = $key;
489 return $organizedFiles;
493 * Get the contents of a set of CSS files, remap then and concatenate
494 * them, with newlines in between. Each file is used only once.
496 * @param $styles Array of file names
497 * @return Array: list of concatenated and remapped contents of $files keyed by media type
499 protected static function concatStyles( $styles ) {
500 $styles = self
::organizeFilesByOption( $styles, 'media', 'all' );
501 foreach ( $styles as $media => $files ) {
505 array( __CLASS__
, 'remapStyle' ),
506 array_unique( (array) $files ) ) );
512 * Remap a relative to $IP. Used as a callback for array_map()
514 * @param $file String: file name
515 * @return string $IP/$file
517 protected static function remapFilename( $file ) {
524 * Get the contents of a CSS file and run it through CSSMin::remap().
525 * This wrapper is needed so we can use array_map() in concatStyles()
527 * @param $file String: file name
528 * @return string Remapped CSS
530 protected static function remapStyle( $file ) {
531 global $wgScriptPath;
532 return CSSMin
::remap(
533 file_get_contents( self
::remapFilename( $file ) ),
535 $wgScriptPath . '/' . dirname( $file ),