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
24 * Abstraction for resource loader modules, with name registration and maxage functionality.
26 abstract class ResourceLoaderModule
{
27 /* Protected Members */
29 protected $name = null;
34 * Get this module's name. This is set when the module is registered
35 * with ResourceLoader::register()
37 * @return Mixed: name (string) or null if no name was set
39 public function getName() {
44 * Set this module's name. This is called by ResourceLodaer::register()
45 * when registering the module. Other code should not call this.
47 * @param $name String: name
49 public function setName( $name ) {
54 * The maximum number of seconds to cache this module for in the
55 * client-side (browser) cache. Override this only if you have a good
56 * reason not to use $wgResourceLoaderClientMaxage.
58 * @return Integer: cache maxage in seconds
60 public function getClientMaxage() {
61 global $wgResourceLoaderClientMaxage;
62 return $wgResourceLoaderClientMaxage;
66 * The maximum number of seconds to cache this module for in the
67 * server-side (Squid / proxy) cache. Override this only if you have a
68 * good reason not to use $wgResourceLoaderServerMaxage.
70 * @return Integer: cache maxage in seconds
72 public function getServerMaxage() {
73 global $wgResourceLoaderServerMaxage;
74 return $wgResourceLoaderServerMaxage;
78 * Get whether CSS for this module should be flipped
80 public function getFlip( $context ) {
81 return $context->getDirection() === 'rtl';
84 /* Abstract Methods */
87 * Get all JS for this module for a given language and skin.
88 * Includes all relevant JS except loader scripts.
90 * @param $context ResourceLoaderContext object
93 public abstract function getScript( ResourceLoaderContext
$context );
96 * Get all CSS for this module for a given skin.
98 * @param $context ResourceLoaderContext object
99 * @return array: strings of CSS keyed by media type
101 public abstract function getStyles( ResourceLoaderContext
$context );
104 * Get the messages needed for this module.
106 * To get a JSON blob with messages, use MessageBlobStore::get()
108 * @return array of message keys. Keys may occur more than once
110 public abstract function getMessages();
113 * Get the loader JS for this module, if set.
115 * @return Mixed: loader JS (string) or false if no custom loader set
117 public abstract function getLoaderScript();
120 * Get a list of modules this module depends on.
122 * Dependency information is taken into account when loading a module
123 * on the client side. When adding a module on the server side,
124 * dependency information is NOT taken into account and YOU are
125 * responsible for adding dependent modules as well. If you don't do
126 * this, the client side loader will send a second request back to the
127 * server to fetch the missing modules, which kind of defeats the
128 * purpose of the resource loader.
130 * To add dependencies dynamically on the client side, use a custom
131 * loader script, see getLoaderScript()
132 * @return Array of module names (strings)
134 public abstract function getDependencies();
137 * Get this module's last modification timestamp for a given
138 * combination of language, skin and debug mode flag. This is typically
139 * the highest of each of the relevant components' modification
140 * timestamps. Whenever anything happens that changes the module's
141 * contents for these parameters, the mtime should increase.
143 * @param $context ResourceLoaderContext object
144 * @return int UNIX timestamp
146 public abstract function getModifiedTime( ResourceLoaderContext
$context );
150 * Module based on local JS/CSS files. This is the most common type of module.
152 class ResourceLoaderFileModule
extends ResourceLoaderModule
{
153 /* Protected Members */
155 protected $scripts = array();
156 protected $styles = array();
157 protected $messages = array();
158 protected $dependencies = array();
159 protected $debugScripts = array();
160 protected $languageScripts = array();
161 protected $skinScripts = array();
162 protected $skinStyles = array();
163 protected $loaders = array();
164 protected $parameters = array();
166 // In-object cache for file dependencies
167 protected $fileDeps = array();
168 // In-object cache for mtime
169 protected $modifiedTime = array();
174 * Construct a new module from an options array.
176 * @param $options array Options array. If empty, an empty module will be constructed
180 * // Required module options (mutually exclusive)
181 * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
183 * // Optional module options
184 * 'languageScripts' => array(
185 * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
188 * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
189 * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
191 * // Non-raw module options
192 * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
193 * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
194 * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
195 * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
196 * 'skinStyles' => array(
197 * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
198 * array( 'dir/file1.css' => array( 'media' => 'print' )
201 * 'messages' => array( 'message1', 'message2' ... ),
204 public function __construct( $options = array() ) {
205 foreach ( $options as $option => $value ) {
208 $this->scripts
= (array)$value;
211 $this->styles
= (array)$value;
214 $this->messages
= (array)$value;
217 $this->dependencies
= (array)$value;
220 $this->debugScripts
= (array)$value;
222 case 'languageScripts':
223 $this->languageScripts
= (array)$value;
226 $this->skinScripts
= (array)$value;
229 $this->skinStyles
= (array)$value;
232 $this->loaders
= (array)$value;
239 * Add script files to this module. In order to be valid, a module
240 * must contain at least one script file.
242 * @param $scripts Mixed: path to script file (string) or array of paths
244 public function addScripts( $scripts ) {
245 $this->scripts
= array_merge( $this->scripts
, (array)$scripts );
249 * Add style (CSS) files to this module.
251 * @param $styles Mixed: path to CSS file (string) or array of paths
253 public function addStyles( $styles ) {
254 $this->styles
= array_merge( $this->styles
, (array)$styles );
258 * Add messages to this module.
260 * @param $messages Mixed: message key (string) or array of message keys
262 public function addMessages( $messages ) {
263 $this->messages
= array_merge( $this->messages
, (array)$messages );
267 * Add dependencies. Dependency information is taken into account when
268 * loading a module on the client side. When adding a module on the
269 * server side, dependency information is NOT taken into account and
270 * YOU are responsible for adding dependent modules as well. If you
271 * don't do this, the client side loader will send a second request
272 * back to the server to fetch the missing modules, which kind of
273 * defeats the point of using the resource loader in the first place.
275 * To add dependencies dynamically on the client side, use a custom
276 * loader (see addLoaders())
278 * @param $dependencies Mixed: module name (string) or array of module names
280 public function addDependencies( $dependencies ) {
281 $this->dependencies
= array_merge( $this->dependencies
, (array)$dependencies );
285 * Add debug scripts to the module. These scripts are only included
288 * @param $scripts Mixed: path to script file (string) or array of paths
290 public function addDebugScripts( $scripts ) {
291 $this->debugScripts
= array_merge( $this->debugScripts
, (array)$scripts );
295 * Add language-specific scripts. These scripts are only included for
298 * @param $lang String: language code
299 * @param $scripts Mixed: path to script file (string) or array of paths
301 public function addLanguageScripts( $lang, $scripts ) {
302 $this->languageScripts
= array_merge_recursive(
303 $this->languageScripts
,
304 array( $lang => $scripts )
309 * Add skin-specific scripts. These scripts are only included for
312 * @param $skin String: skin name, or 'default'
313 * @param $scripts Mixed: path to script file (string) or array of paths
315 public function addSkinScripts( $skin, $scripts ) {
316 $this->skinScripts
= array_merge_recursive(
318 array( $skin => $scripts )
323 * Add skin-specific CSS. These CSS files are only included for a
324 * given skin. If there are no skin-specific CSS files for a skin,
325 * the files defined for 'default' will be used, if any.
327 * @param $skin String: skin name, or 'default'
328 * @param $scripts Mixed: path to CSS file (string) or array of paths
330 public function addSkinStyles( $skin, $scripts ) {
331 $this->skinStyles
= array_merge_recursive(
333 array( $skin => $scripts )
338 * Add loader scripts. These scripts are loaded on every page and are
339 * responsible for registering this module using
340 * mediaWiki.loader.register(). If there are no loader scripts defined,
341 * the resource loader will register the module itself.
343 * Loader scripts are used to determine a module's dependencies
344 * dynamically on the client side (e.g. based on browser type/version).
345 * Note that loader scripts are included on every page, so they should
346 * be lightweight and use mediaWiki.loader.register()'s callback
347 * feature to defer dependency calculation.
349 * @param $scripts Mixed: path to script file (string) or array of paths
351 public function addLoaders( $scripts ) {
352 $this->loaders
= array_merge( $this->loaders
, (array)$scripts );
355 public function getScript( ResourceLoaderContext
$context ) {
356 $retval = $this->getPrimaryScript() . "\n" .
357 $this->getLanguageScript( $context->getLanguage() ) . "\n" .
358 $this->getSkinScript( $context->getSkin() );
360 if ( $context->getDebug() ) {
361 $retval .= $this->getDebugScript();
367 public function getStyles( ResourceLoaderContext
$context ) {
369 foreach ( $this->getPrimaryStyles() as $media => $style ) {
370 if ( !isset( $styles[$media] ) ) {
371 $styles[$media] = '';
373 $styles[$media] .= $style;
375 foreach ( $this->getSkinStyles( $context->getSkin() ) as $media => $style ) {
376 if ( !isset( $styles[$media] ) ) {
377 $styles[$media] = '';
379 $styles[$media] .= $style;
382 // Collect referenced files
384 foreach ( $styles as $media => $style ) {
385 // Extract and store the list of referenced files
386 $files = array_merge( $files, CSSMin
::getLocalFileReferences( $style ) );
389 // Only store if modified
390 if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
391 $encFiles = FormatJson
::encode( $files );
392 $dbw = wfGetDb( DB_MASTER
);
393 $dbw->replace( 'module_deps',
394 array( array( 'md_module', 'md_skin' ) ), array(
395 'md_module' => $this->getName(),
396 'md_skin' => $context->getSkin(),
397 'md_deps' => $encFiles,
401 // Save into memcached
404 $key = wfMemcKey( 'resourceloader', 'module_deps', $this->getName(), $context->getSkin() );
405 $wgMemc->set( $key, $encFiles );
411 public function getMessages() {
412 return $this->messages
;
415 public function getDependencies() {
416 return $this->dependencies
;
419 public function getLoaderScript() {
420 if ( count( $this->loaders
) == 0 ) {
424 return self
::concatScripts( $this->loaders
);
428 * Get the last modified timestamp of this module, which is calculated
429 * as the highest last modified timestamp of its constituent files and
430 * the files it depends on (see getFileDependencies()). Only files
431 * relevant to the given language and skin are taken into account, and
432 * files only relevant in debug mode are not taken into account when
435 * @param $context ResourceLoaderContext object
436 * @return Integer: UNIX timestamp
438 public function getModifiedTime( ResourceLoaderContext
$context ) {
439 if ( isset( $this->modifiedTime
[$context->getHash()] ) ) {
440 return $this->modifiedTime
[$context->getHash()];
443 // Sort of nasty way we can get a flat list of files depended on by all styles
445 foreach ( self
::organizeFilesByOption( $this->styles
, 'media', 'all' ) as $media => $styleFiles ) {
446 $styles = array_merge( $styles, $styleFiles );
448 $skinFiles = (array) self
::getSkinFiles(
449 $context->getSkin(), self
::organizeFilesByOption( $this->skinStyles
, 'media', 'all' )
451 foreach ( $skinFiles as $media => $styleFiles ) {
452 $styles = array_merge( $styles, $styleFiles );
455 // Final merge, this should result in a master list of dependent files
456 $files = array_merge(
459 $context->getDebug() ?
$this->debugScripts
: array(),
460 isset( $this->languageScripts
[$context->getLanguage()] ) ?
461 (array) $this->languageScripts
[$context->getLanguage()] : array(),
462 (array) self
::getSkinFiles( $context->getSkin(), $this->skinScripts
),
464 $this->getFileDependencies( $context->getSkin() )
467 $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__
, 'remapFilename' ), $files ) ) );
469 // Get the mtime of the message blob
470 // TODO: This timestamp is queried a lot and queried separately for each module. Maybe it should be put in memcached?
471 $dbr = wfGetDb( DB_SLAVE
);
472 $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
473 'mr_resource' => $this->getName(),
474 'mr_lang' => $context->getLanguage()
477 $msgBlobMtime = $msgBlobMtime ?
wfTimestamp( TS_UNIX
, $msgBlobMtime ) : 0;
479 $this->modifiedTime
[$context->getHash()] = max( $filesMtime, $msgBlobMtime );
480 return $this->modifiedTime
[$context->getHash()];
483 /* Protected Members */
486 * Get the primary JS for this module. This is pulled from the
487 * script files added through addScripts()
491 protected function getPrimaryScript() {
492 return self
::concatScripts( $this->scripts
);
496 * Get the primary CSS for this module. This is pulled from the CSS
497 * files added through addStyles()
501 protected function getPrimaryStyles() {
502 return self
::concatStyles( $this->styles
);
506 * Get the debug JS for this module. This is pulled from the script
507 * files added through addDebugScripts()
511 protected function getDebugScript() {
512 return self
::concatScripts( $this->debugScripts
);
516 * Get the language-specific JS for a given language. This is pulled
517 * from the language-specific script files added through addLanguageScripts()
521 protected function getLanguageScript( $lang ) {
522 if ( !isset( $this->languageScripts
[$lang] ) ) {
525 return self
::concatScripts( $this->languageScripts
[$lang] );
529 * Get the skin-specific JS for a given skin. This is pulled from the
530 * skin-specific JS files added through addSkinScripts()
534 protected function getSkinScript( $skin ) {
535 return self
::concatScripts( self
::getSkinFiles( $skin, $this->skinScripts
) );
539 * Get the skin-specific CSS for a given skin. This is pulled from the
540 * skin-specific CSS files added through addSkinStyles()
542 * @return Array: list of CSS strings keyed by media type
544 protected function getSkinStyles( $skin ) {
545 return self
::concatStyles( self
::getSkinFiles( $skin, $this->skinStyles
) );
549 * Helper function to get skin-specific data from an array.
551 * @param $skin String: skin name
552 * @param $map Array: map of skin names to arrays
553 * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
555 protected static function getSkinFiles( $skin, $map ) {
558 if ( isset( $map[$skin] ) && $map[$skin] ) {
559 $retval = $map[$skin];
560 } else if ( isset( $map['default'] ) ) {
561 $retval = $map['default'];
568 * Get the files this module depends on indirectly for a given skin.
569 * Currently these are only image files referenced by the module's CSS.
571 * @param $skin String: skin name
572 * @return array of files
574 protected function getFileDependencies( $skin ) {
575 // Try in-object cache first
576 if ( isset( $this->fileDeps
[$skin] ) ) {
577 return $this->fileDeps
[$skin];
583 $key = wfMemcKey( 'resourceloader', 'module_deps', $this->getName(), $skin );
584 $deps = $wgMemc->get( $key );
587 $dbr = wfGetDb( DB_SLAVE
);
588 $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
589 'md_module' => $this->getName(),
594 $deps = '[]'; // Empty array so we can do negative caching
596 $wgMemc->set( $key, $deps );
599 $this->fileDeps
= FormatJson
::decode( $deps, true );
601 return $this->fileDeps
;
605 * Get the contents of a set of files and concatenate them, with
606 * newlines in between. Each file is used only once.
608 * @param $files Array of file names
609 * @return String: concatenated contents of $files
611 protected static function concatScripts( $files ) {
612 return implode( "\n", array_map( 'file_get_contents', array_map( array( __CLASS__
, 'remapFilename' ), array_unique( (array) $files ) ) ) );
615 protected static function organizeFilesByOption( $files, $option, $default ) {
616 $organizedFiles = array();
617 foreach ( (array) $files as $key => $value ) {
618 if ( is_int( $key ) ) {
619 // File name as the value
620 if ( !isset( $organizedFiles[$default] ) ) {
621 $organizedFiles[$default] = array();
623 $organizedFiles[$default][] = $value;
624 } else if ( is_array( $value ) ) {
625 // File name as the key, options array as the value
626 $media = isset( $value[$option] ) ?
$value[$option] : $default;
627 if ( !isset( $organizedFiles[$media] ) ) {
628 $organizedFiles[$media] = array();
630 $organizedFiles[$media][] = $key;
633 return $organizedFiles;
637 * Get the contents of a set of CSS files, remap then and concatenate
638 * them, with newlines in between. Each file is used only once.
640 * @param $files Array of file names
641 * @return Array: list of concatenated and remapped contents of $files keyed by media type
643 protected static function concatStyles( $styles ) {
644 $styles = self
::organizeFilesByOption( $styles, 'media', 'all' );
645 foreach ( $styles as $media => $files ) {
647 implode( "\n", array_map( array( __CLASS__
, 'remapStyle' ), array_unique( (array) $files ) ) );
653 * Remap a relative to $IP. Used as a callback for array_map()
655 * @param $file String: file name
656 * @return string $IP/$file
658 protected static function remapFilename( $file ) {
665 * Get the contents of a CSS file and run it through CSSMin::remap().
666 * This wrapper is needed so we can use array_map() in concatStyles()
668 * @param $file String: file name
669 * @return string Remapped CSS
671 protected static function remapStyle( $file ) {
672 global $wgUseDataURLs;
673 return CSSMin
::remap( file_get_contents( self
::remapFilename( $file ) ), dirname( $file ), $wgUseDataURLs );
678 * Abstraction for resource loader modules which pull from wiki pages
680 abstract class ResourceLoaderWikiModule
extends ResourceLoaderModule
{
682 /* Protected Members */
684 // In-object cache for modified time
685 protected $modifiedTime = null;
687 /* Abstract Protected Methods */
689 abstract protected function getPages( ResourceLoaderContext
$context );
691 /* Protected Methods */
693 protected function getStyleCode( array $styles ) {
694 foreach ( $styles as $media => $messages ) {
695 foreach ( $messages as $i => $message ) {
696 $style = wfMsgExt( $message, 'content' );
697 if ( !wfEmptyMsg( $message, $style ) ) {
698 $styles[$media][$i] = $style;
702 foreach ( $styles as $media => $messages ) {
703 $styles[$media] = implode( "\n", $messages );
710 public function getModifiedTime( ResourceLoaderContext
$context ) {
711 if ( isset( $this->modifiedTime
[$context->getHash()] ) ) {
712 return $this->modifiedTime
[$context->getHash()];
714 $pages = $this->getPages( $context );
715 foreach ( $pages as $i => $page ) {
716 $pages[$i] = Title
::makeTitle( NS_MEDIAWIKI
, $page );
718 // Do batch existence check
719 // TODO: This would work better if page_touched were loaded by this as well
720 $lb = new LinkBatch( $pages );
722 $this->modifiedTime
= 1; // wfTimestamp() interprets 0 as "now"
723 foreach ( $pages as $page ) {
724 if ( $page->exists() ) {
725 $this->modifiedTime
= max( $this->modifiedTime
, wfTimestamp( TS_UNIX
, $page->getTouched() ) );
728 return $this->modifiedTime
;
730 public function getMessages() { return array(); }
731 public function getLoaderScript() { return ''; }
732 public function getDependencies() { return array(); }
736 * Custom module for site customizations
738 class ResourceLoaderSiteModule
extends ResourceLoaderWikiModule
{
740 /* Protected Methods */
742 protected function getPages( ResourceLoaderContext
$context ) {
743 global $wgHandheldStyle;
745 // HACK: We duplicate the message names from generateUserJs() and generateUserCss here and weird things (i.e.
746 // mtime moving backwards) can happen when a MediaWiki:Something.js page is deleted
750 ucfirst( $context->getSkin() ) . '.js',
751 ucfirst( $context->getSkin() ) . '.css',
754 if ( $wgHandheldStyle ) {
755 $pages[] = 'Handheld.css';
762 public function getScript( ResourceLoaderContext
$context ) {
763 return Skin
::newFromKey( $context->getSkin() )->generateUserJs();
766 public function getStyles( ResourceLoaderContext
$context ) {
767 global $wgHandheldStyle;
769 'all' => array( 'Common.css', $context->getSkin() . '.css' ),
770 'print' => array( 'Print.css' ),
772 if ( $wgHandheldStyle ) {
773 $sources['handheld'] = array( 'Handheld.css' );
775 return $this->getStyleCode( $styles );
779 class ResourceLoaderStartUpModule
extends ResourceLoaderModule
{
780 /* Protected Members */
782 protected $modifiedTime = null;
786 public function getScript( ResourceLoaderContext
$context ) {
789 $scripts = file_get_contents( "$IP/resources/startup.js" );
791 if ( $context->getOnly() === 'scripts' ) {
792 // Get all module registrations
793 $registration = ResourceLoader
::getModuleRegistrations( $context );
794 // Build configuration
795 $config = FormatJson
::encode(
796 array( 'server' => $context->getServer(), 'debug' => $context->getDebug() )
798 // Add a well-known start-up function
799 $scripts .= "window.startUp = function() { $registration mediaWiki.config.set( $config ); };";
800 // Build load query for jquery and mediawiki modules
801 $query = wfArrayToCGI(
803 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ),
805 'lang' => $context->getLanguage(),
806 'dir' => $context->getDirection(),
807 'skin' => $context->getSkin(),
808 'debug' => $context->getDebug(),
809 'version' => wfTimestamp( TS_ISO_8601
, round( max(
810 ResourceLoader
::getModule( 'jquery' )->getModifiedTime( $context ),
811 ResourceLoader
::getModule( 'mediawiki' )->getModifiedTime( $context )
816 // Build HTML code for loading jquery and mediawiki modules
817 $loadScript = Html
::linkedScript( $context->getServer() . "?$query" );
818 // Add code to add jquery and mediawiki loading code; only if the current client is compatible
819 $scripts .= "if ( isCompatible() ) { document.write( '$loadScript' ); }";
820 // Delete the compatible function - it's not needed anymore
821 $scripts .= "delete window['isCompatible'];";
827 public function getModifiedTime( ResourceLoaderContext
$context ) {
830 if ( !is_null( $this->modifiedTime
) ) {
831 return $this->modifiedTime
;
834 // HACK getHighestModifiedTime() calls this function, so protect against infinite recursion
835 $this->modifiedTime
= filemtime( "$IP/resources/startup.js" );
836 $this->modifiedTime
= ResourceLoader
::getHighestModifiedTime( $context );
837 return $this->modifiedTime
;
840 public function getClientMaxage() {
841 return 300; // 5 minutes
844 public function getServerMaxage() {
845 return 300; // 5 minutes
848 public function getStyles( ResourceLoaderContext
$context ) { return array(); }
850 public function getFlip( $context ) {
853 return $wgContLang->getDir() !== $context->getDirection();
855 public function getMessages() { return array(); }
856 public function getLoaderScript() { return ''; }
857 public function getDependencies() { return array(); }