3 * Abstraction for resource loader modules.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
21 * @author Trevor Parscal
22 * @author Roan Kattouw
26 * Abstraction for resource loader modules, with name registration and maxage functionality.
28 abstract class ResourceLoaderModule
{
30 const TYPE_SCRIPTS
= 'scripts';
31 const TYPE_STYLES
= 'styles';
32 const TYPE_COMBINED
= 'combined';
34 # sitewide core module like a skin file or jQuery component
35 const ORIGIN_CORE_SITEWIDE
= 1;
37 # per-user module generated by the software
38 const ORIGIN_CORE_INDIVIDUAL
= 2;
40 # sitewide module generated from user-editable files, like MediaWiki:Common.js, or
41 # modules accessible to multiple users, such as those generated by the Gadgets extension.
42 const ORIGIN_USER_SITEWIDE
= 3;
44 # per-user module generated from user-editable files, like User:Me/vector.js
45 const ORIGIN_USER_INDIVIDUAL
= 4;
47 # an access constant; make sure this is kept as the largest number in this group
48 const ORIGIN_ALL
= 10;
50 # script and style modules form a hierarchy of trustworthiness, with core modules like
51 # skins and jQuery as most trustworthy, and user scripts as least trustworthy. We can
52 # limit the types of scripts and styles we allow to load on, say, sensitive special
53 # pages like Special:UserLogin and Special:Preferences
54 protected $origin = self
::ORIGIN_CORE_SITEWIDE
;
56 /* Protected Members */
58 protected $name = null;
59 protected $targets = array( 'desktop' );
61 // In-object cache for file dependencies
62 protected $fileDeps = array();
63 // In-object cache for message blob mtime
64 protected $msgBlobMtime = array();
74 * Get this module's name. This is set when the module is registered
75 * with ResourceLoader::register()
77 * @return string|null Name (string) or null if no name was set
79 public function getName() {
84 * Set this module's name. This is called by ResourceLoader::register()
85 * when registering the module. Other code should not call this.
87 * @param string $name Name
89 public function setName( $name ) {
94 * Get this module's origin. This is set when the module is registered
95 * with ResourceLoader::register()
97 * @return int ResourceLoaderModule class constant, the subclass default
100 public function getOrigin() {
101 return $this->origin
;
105 * Set this module's origin. This is called by ResourceLoader::register()
106 * when registering the module. Other code should not call this.
108 * @param int $origin Origin
110 public function setOrigin( $origin ) {
111 $this->origin
= $origin;
115 * @param ResourceLoaderContext $context
118 public function getFlip( $context ) {
121 return $wgContLang->getDir() !== $context->getDirection();
125 * Get all JS for this module for a given language and skin.
126 * Includes all relevant JS except loader scripts.
128 * @param ResourceLoaderContext $context
129 * @return string JavaScript code
131 public function getScript( ResourceLoaderContext
$context ) {
132 // Stub, override expected
137 * Takes named templates by the module and returns an array mapping.
139 * @return array of templates mapping template alias to content
141 public function getTemplates() {
142 // Stub, override expected.
150 public function getConfig() {
151 if ( $this->config
=== null ) {
152 // Ugh, fall back to default
153 $this->config
= ConfigFactory
::getDefaultInstance()->makeConfig( 'main' );
156 return $this->config
;
160 * @param Config $config
163 public function setConfig( Config
$config ) {
164 $this->config
= $config;
168 * Get the URL or URLs to load for this module's JS in debug mode.
169 * The default behavior is to return a load.php?only=scripts URL for
170 * the module, but file-based modules will want to override this to
171 * load the files directly.
173 * This function is called only when 1) we're in debug mode, 2) there
174 * is no only= parameter and 3) supportsURLLoading() returns true.
175 * #2 is important to prevent an infinite loop, therefore this function
176 * MUST return either an only= URL or a non-load.php URL.
178 * @param ResourceLoaderContext $context
179 * @return array Array of URLs
181 public function getScriptURLsForDebug( ResourceLoaderContext
$context ) {
182 $resourceLoader = $context->getResourceLoader();
183 $derivative = new DerivativeResourceLoaderContext( $context );
184 $derivative->setModules( array( $this->getName() ) );
185 $derivative->setOnly( 'scripts' );
186 $derivative->setDebug( true );
188 $url = $resourceLoader->createLoaderURL(
193 return array( $url );
197 * Whether this module supports URL loading. If this function returns false,
198 * getScript() will be used even in cases (debug mode, no only param) where
199 * getScriptURLsForDebug() would normally be used instead.
202 public function supportsURLLoading() {
207 * Get all CSS for this module for a given skin.
209 * @param ResourceLoaderContext $context
210 * @return array List of CSS strings or array of CSS strings keyed by media type.
211 * like array( 'screen' => '.foo { width: 0 }' );
212 * or array( 'screen' => array( '.foo { width: 0 }' ) );
214 public function getStyles( ResourceLoaderContext
$context ) {
215 // Stub, override expected
220 * Get the URL or URLs to load for this module's CSS in debug mode.
221 * The default behavior is to return a load.php?only=styles URL for
222 * the module, but file-based modules will want to override this to
223 * load the files directly. See also getScriptURLsForDebug()
225 * @param ResourceLoaderContext $context
226 * @return array Array( mediaType => array( URL1, URL2, ... ), ... )
228 public function getStyleURLsForDebug( ResourceLoaderContext
$context ) {
229 $resourceLoader = $context->getResourceLoader();
230 $derivative = new DerivativeResourceLoaderContext( $context );
231 $derivative->setModules( array( $this->getName() ) );
232 $derivative->setOnly( 'styles' );
233 $derivative->setDebug( true );
235 $url = $resourceLoader->createLoaderURL(
240 return array( 'all' => array( $url ) );
244 * Get the messages needed for this module.
246 * To get a JSON blob with messages, use MessageBlobStore::get()
248 * @return array List of message keys. Keys may occur more than once
250 public function getMessages() {
251 // Stub, override expected
256 * Get the group this module is in.
258 * @return string Group name
260 public function getGroup() {
261 // Stub, override expected
266 * Get the origin of this module. Should only be overridden for foreign modules.
268 * @return string Origin name, 'local' for local modules
270 public function getSource() {
271 // Stub, override expected
276 * Where on the HTML page should this module's JS be loaded?
277 * - 'top': in the "<head>"
278 * - 'bottom': at the bottom of the "<body>"
282 public function getPosition() {
287 * Whether this module's JS expects to work without the client-side ResourceLoader module.
288 * Returning true from this function will prevent mw.loader.state() call from being
289 * appended to the bottom of the script.
293 public function isRaw() {
298 * Get the loader JS for this module, if set.
300 * @return mixed JavaScript loader code as a string or boolean false if no custom loader set
302 public function getLoaderScript() {
303 // Stub, override expected
308 * Get a list of modules this module depends on.
310 * Dependency information is taken into account when loading a module
311 * on the client side.
313 * To add dependencies dynamically on the client side, use a custom
314 * loader script, see getLoaderScript()
315 * @return array List of module names as strings
317 public function getDependencies() {
318 // Stub, override expected
323 * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
325 * @return array Array of strings
327 public function getTargets() {
328 return $this->targets
;
332 * Get the skip function.
334 * Modules that provide fallback functionality can provide a "skip function". This
335 * function, if provided, will be passed along to the module registry on the client.
336 * When this module is loaded (either directly or as a dependency of another module),
337 * then this function is executed first. If the function returns true, the module will
338 * instantly be considered "ready" without requesting the associated module resources.
340 * The value returned here must be valid javascript for execution in a private function.
341 * It must not contain the "function () {" and "}" wrapper though.
343 * @return string|null A JavaScript function body returning a boolean value, or null
345 public function getSkipFunction() {
350 * Get the files this module depends on indirectly for a given skin.
351 * Currently these are only image files referenced by the module's CSS.
353 * @param string $skin Skin name
354 * @return array List of files
356 public function getFileDependencies( $skin ) {
357 // Try in-object cache first
358 if ( isset( $this->fileDeps
[$skin] ) ) {
359 return $this->fileDeps
[$skin];
362 $dbr = wfGetDB( DB_SLAVE
);
363 $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
364 'md_module' => $this->getName(),
368 if ( !is_null( $deps ) ) {
369 $this->fileDeps
[$skin] = (array)FormatJson
::decode( $deps, true );
371 $this->fileDeps
[$skin] = array();
373 return $this->fileDeps
[$skin];
377 * Set preloaded file dependency information. Used so we can load this
378 * information for all modules at once.
379 * @param string $skin Skin name
380 * @param array $deps Array of file names
382 public function setFileDependencies( $skin, $deps ) {
383 $this->fileDeps
[$skin] = $deps;
387 * Get the last modification timestamp of the message blob for this
388 * module in a given language.
389 * @param string $lang Language code
390 * @return int UNIX timestamp
392 public function getMsgBlobMtime( $lang ) {
393 if ( !isset( $this->msgBlobMtime
[$lang] ) ) {
394 if ( !count( $this->getMessages() ) ) {
398 $dbr = wfGetDB( DB_SLAVE
);
399 $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
400 'mr_resource' => $this->getName(),
404 // If no blob was found, but the module does have messages, that means we need
405 // to regenerate it. Return NOW
406 if ( $msgBlobMtime === false ) {
407 $msgBlobMtime = wfTimestampNow();
409 $this->msgBlobMtime
[$lang] = wfTimestamp( TS_UNIX
, $msgBlobMtime );
411 return $this->msgBlobMtime
[$lang];
415 * Set a preloaded message blob last modification timestamp. Used so we
416 * can load this information for all modules at once.
417 * @param string $lang Language code
418 * @param int $mtime UNIX timestamp
420 public function setMsgBlobMtime( $lang, $mtime ) {
421 $this->msgBlobMtime
[$lang] = $mtime;
424 /* Abstract Methods */
427 * Get this module's last modification timestamp for a given
428 * combination of language, skin and debug mode flag. This is typically
429 * the highest of each of the relevant components' modification
430 * timestamps. Whenever anything happens that changes the module's
431 * contents for these parameters, the mtime should increase.
433 * NOTE: The mtime of the module's messages is NOT automatically included.
434 * If you want this to happen, you'll need to call getMsgBlobMtime()
435 * yourself and take its result into consideration.
437 * NOTE: The mtime of the module's hash is NOT automatically included.
438 * If your module provides a getModifiedHash() method, you'll need to call getHashMtime()
439 * yourself and take its result into consideration.
441 * @param ResourceLoaderContext $context Context object
442 * @return int UNIX timestamp
444 public function getModifiedTime( ResourceLoaderContext
$context ) {
449 * Helper method for calculating when the module's hash (if it has one) changed.
451 * @param ResourceLoaderContext $context
452 * @return int UNIX timestamp
454 public function getHashMtime( ResourceLoaderContext
$context ) {
455 $hash = $this->getModifiedHash( $context );
456 if ( !is_string( $hash ) ) {
460 // Embed the hash itself in the cache key. This allows for a few nifty things:
461 // - During deployment, servers with old and new versions of the code communicating
462 // with the same memcached will not override the same key repeatedly increasing
464 // - In case of the definition changing and then changing back in a short period of time
465 // (e.g. in case of a revert or a corrupt server) the old timestamp and client-side cache
466 // url will be re-used.
467 // - If different context-combinations (e.g. same skin, same language or some combination
468 // thereof) result in the same definition, they will use the same hash and timestamp.
469 $cache = wfGetCache( CACHE_ANYTHING
);
470 $key = wfMemcKey( 'resourceloader', 'hashmtime', $this->getName(), $hash );
472 $data = $cache->get( $key );
473 if ( is_int( $data ) && $data > 0 ) {
474 // We've seen this hash before, re-use the timestamp of when we first saw it.
479 $cache->set( $key, $timestamp );
484 * Get the hash for whatever this module may contain.
486 * This is the method subclasses should implement if they want to make
487 * use of getHashMTime() inside getModifiedTime().
489 * @param ResourceLoaderContext $context
490 * @return string|null Hash
492 public function getModifiedHash( ResourceLoaderContext
$context ) {
497 * Helper method for calculating when this module's definition summary was last changed.
501 * @param ResourceLoaderContext $context
502 * @return int UNIX timestamp
504 public function getDefinitionMtime( ResourceLoaderContext
$context ) {
505 $summary = $this->getDefinitionSummary( $context );
506 if ( $summary === null ) {
510 $hash = md5( json_encode( $summary ) );
511 $cache = wfGetCache( CACHE_ANYTHING
);
512 $key = wfMemcKey( 'resourceloader', 'moduledefinition', $this->getName(), $hash );
514 $data = $cache->get( $key );
515 if ( is_int( $data ) && $data > 0 ) {
516 // We've seen this hash before, re-use the timestamp of when we first saw it.
520 wfDebugLog( 'resourceloader', __METHOD__
. ": New definition for module "
521 . "{$this->getName()} in context \"{$context->getHash()}\"" );
522 // WMF logging for T94810
524 if ( isset( $wgRequest ) && $context->getUser() ) {
525 wfDebugLog( 'resourceloader', __METHOD__
. ": Request with user parameter in "
526 . "context \"{$context->getHash()}\" from " . $wgRequest->getRequestURL() );
530 $cache->set( $key, $timestamp );
535 * Get the definition summary for this module.
537 * This is the method subclasses should implement if they want to make
538 * use of getDefinitionMTime() inside getModifiedTime().
540 * Return an array containing values from all significant properties of this
541 * module's definition. Be sure to include things that are explicitly ordered,
542 * in their actaul order (bug 37812).
544 * Avoid including things that are insiginificant (e.g. order of message
545 * keys is insignificant and should be sorted to avoid unnecessary cache
548 * Avoid including things already considered by other methods inside your
549 * getModifiedTime(), such as file mtime timestamps.
551 * Serialisation is done using json_encode, which means object state is not
552 * taken into account when building the hash. This data structure must only
553 * contain arrays and scalars as values (avoid object instances) which means
554 * it requires abstraction.
558 * @param ResourceLoaderContext $context
561 public function getDefinitionSummary( ResourceLoaderContext
$context ) {
563 'class' => get_class( $this ),
568 * Check whether this module is known to be empty. If a child class
569 * has an easy and cheap way to determine that this module is
570 * definitely going to be empty, it should override this method to
571 * return true in that case. Callers may optimize the request for this
572 * module away if this function returns true.
573 * @param ResourceLoaderContext $context
576 public function isKnownEmpty( ResourceLoaderContext
$context ) {
580 /** @var JSParser Lazy-initialized; use self::javaScriptParser() */
581 private static $jsParser;
582 private static $parseCacheVersion = 1;
585 * Validate a given script file; if valid returns the original source.
586 * If invalid, returns replacement JS source that throws an exception.
588 * @param string $fileName
589 * @param string $contents
590 * @return string JS with the original, or a replacement error
592 protected function validateScriptFile( $fileName, $contents ) {
593 if ( $this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) {
595 // Use CACHE_ANYTHING since filtering is very slow compared to DB queries
596 $key = wfMemcKey( 'resourceloader', 'jsparse', self
::$parseCacheVersion, md5( $contents ) );
597 $cache = wfGetCache( CACHE_ANYTHING
);
598 $cacheEntry = $cache->get( $key );
599 if ( is_string( $cacheEntry ) ) {
603 $parser = self
::javaScriptParser();
605 $parser->parse( $contents, $fileName, 1 );
607 } catch ( Exception
$e ) {
608 // We'll save this to cache to avoid having to validate broken JS over and over...
609 $err = $e->getMessage();
610 $result = "throw new Error(" . Xml
::encodeJsVar( "JavaScript parse error: $err" ) . ");";
613 $cache->set( $key, $result );
623 protected static function javaScriptParser() {
624 if ( !self
::$jsParser ) {
625 self
::$jsParser = new JSParser();
627 return self
::$jsParser;
631 * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist
632 * but returns 1 instead.
633 * @param string $filename File name
634 * @return int UNIX timestamp
636 protected static function safeFilemtime( $filename ) {
637 wfSuppressWarnings();
638 $mtime = filemtime( $filename ) ?
: 1;