<?php
/**
+ * Base class for resource loading system.
+ *
* 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
/** Associative array mapping module name to info associative array */
protected $moduleInfos = array();
+
+ /** Associative array mapping framework ids to a list of names of test suite modules */
+ /** like array( 'qunit' => array( 'mediawiki.tests.qunit.suites', 'ext.foo.tests', .. ), .. ) */
+ protected $testModuleNames = array();
/** array( 'source-id' => array( 'loadScript' => 'http://.../load.php' ) ) **/
protected $sources = array();
$cache->set( $key, $result );
} catch ( Exception $exception ) {
// Return exception as a comment
- $result = "/*\n{$exception->__toString()}\n*/\n";
+ $result = $this->makeComment( $exception->__toString() );
}
wfProfileOut( __METHOD__ );
* Registers core modules and runs registration hooks.
*/
public function __construct() {
- global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript;
+ global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript, $wgEnableJavaScriptTest;
wfProfileIn( __METHOD__ );
wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
$this->register( $wgResourceModules );
+ if ( $wgEnableJavaScriptTest === true ) {
+ $this->registerTestModules();
+ }
+
+
wfProfileOut( __METHOD__ );
}
* Registers a module with the ResourceLoader system.
*
* @param $name Mixed: Name of module as a string or List of name/object pairs as an array
- * @param $info Module info array. For backwards compatibility with 1.17alpha,
+ * @param $info array Module info array. For backwards compatibility with 1.17alpha,
* this may also be a ResourceLoaderModule object. Optional when using
* multiple-registration calling style.
* @throws MWException: If a duplicate module registration is attempted
);
}
- // Check $name for illegal characters
- if ( preg_match( '/[|,!]/', $name ) ) {
- throw new MWException( "ResourceLoader module name '$name' is invalid. Names may not contain pipes (|), commas (,) or exclamation marks (!)" );
+ // Check $name for validity
+ if ( !self::isValidModuleName( $name ) ) {
+ throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" );
}
// Attach module
wfProfileOut( __METHOD__ );
}
+ /**
+ */
+ public function registerTestModules() {
+ global $IP, $wgEnableJavaScriptTest;
+
+ if ( $wgEnableJavaScriptTest !== true ) {
+ throw new MWException( 'Attempt to register JavaScript test modules but <tt>$wgEnableJavaScriptTest</tt> is false. Edit your <tt>LocalSettings.php</tt> to enable it.' );
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ // Get core test suites
+ $testModules = array();
+ $testModules['qunit'] = include( "$IP/tests/qunit/QUnitTestResources.php" );
+ // Get other test suites (e.g. from extensions)
+ wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) );
+
+ // Add the testrunner (which configures QUnit) to the dependencies.
+ // Since it must be ready before any of the test suites are executed.
+ foreach( $testModules['qunit'] as $moduleName => $moduleProps ) {
+ $testModules['qunit'][$moduleName]['dependencies'][] = 'mediawiki.tests.qunit.testrunner';
+ }
+
+ foreach( $testModules as $id => $names ) {
+ // Register test modules
+ $this->register( $testModules[$id] );
+
+ // Keep track of their names so that they can be loaded together
+ $this->testModuleNames[$id] = array_keys( $testModules[$id] );
+ }
+
+ wfProfileOut( __METHOD__ );
+ }
+
/**
* Add a foreign source of modules.
*
public function getModuleNames() {
return array_keys( $this->moduleInfos );
}
+
+ /**
+ * Get a list of test module names for one (or all) frameworks.
+ * If the given framework id is unknkown, or if the in-object variable is not an array,
+ * then it will return an empty array.
+ *
+ * @param $framework String: Optional. Get only the test module names for one
+ * particular framework.
+ * @return Array
+ */
+ public function getTestModuleNames( $framework = 'all' ) {
+ /// @TODO: api siteinfo prop testmodulenames modulenames
+ if ( $framework == 'all' ) {
+ return $this->testModuleNames;
+ } elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
+ return $this->testModuleNames[$framework];
+ } else {
+ return array();
+ }
+ }
/**
* Get the ResourceLoaderModule object for a given module name.
ob_start();
wfProfileIn( __METHOD__ );
- $exceptions = '';
+ $errors = '';
// Split requested modules into two groups, modules and missing
$modules = array();
$missing = array();
foreach ( $context->getModules() as $name ) {
if ( isset( $this->moduleInfos[$name] ) ) {
+ $module = $this->getModule( $name );
+ // Do not allow private modules to be loaded from the web.
+ // This is a security issue, see bug 34907.
+ if ( $module->getGroup() === 'private' ) {
+ $errors .= $this->makeComment( "Cannot show private module \"$name\"" );
+ continue;
+ }
$modules[$name] = $this->getModule( $name );
} else {
$missing[] = $name;
$this->preloadModuleInfo( array_keys( $modules ), $context );
} catch( Exception $e ) {
// Add exception to the output as a comment
- $exceptions .= "/*\n{$e->__toString()}\n*/\n";
+ $errors .= $this->makeComment( $e->__toString() );
}
wfProfileIn( __METHOD__.'-getModifiedTime' );
- $private = false;
// To send Last-Modified and support If-Modified-Since, we need to detect
// the last modified time
$mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
* @var $module ResourceLoaderModule
*/
try {
- // Bypass Squid and other shared caches if the request includes any private modules
- if ( $module->getGroup() === 'private' ) {
- $private = true;
- }
// Calculate maximum modified time
$mtime = max( $mtime, $module->getModifiedTime( $context ) );
} catch ( Exception $e ) {
// Add exception to the output as a comment
- $exceptions .= "/*\n{$e->__toString()}\n*/\n";
+ $errors .= $this->makeComment( $e->__toString() );
}
}
wfProfileOut( __METHOD__.'-getModifiedTime' );
// Send content type and cache related headers
- $this->sendResponseHeaders( $context, $mtime, $private );
+ $this->sendResponseHeaders( $context, $mtime );
// If there's an If-Modified-Since header, respond with a 304 appropriately
if ( $this->tryRespondLastModified( $context, $mtime ) ) {
$response = $this->makeModuleResponse( $context, $modules, $missing );
// Prepend comments indicating exceptions
- $response = $exceptions . $response;
+ $response = $errors . $response;
// Capture any PHP warnings from the output buffer and append them to the
// response in a comment if we're in debug mode.
if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
- $response = "/*\n$warnings\n*/\n" . $response;
+ $response = $this->makeComment( $warnings ) . $response;
}
- // Remove the output buffer and output the response
- ob_end_clean();
- echo $response;
-
- // Save response to file cache unless there are private modules or errors
- if ( isset( $fileCache ) && !$private && !$exceptions && !$missing ) {
+ // Save response to file cache unless there are errors
+ if ( isset( $fileCache ) && !$errors && !$missing ) {
// Cache single modules...and other requests if there are enough hits
if ( ResourceFileCache::useFileCache( $context ) ) {
if ( $fileCache->isCacheWorthy() ) {
}
}
+ // Remove the output buffer and output the response
+ ob_end_clean();
+ echo $response;
+
wfProfileOut( __METHOD__ );
}
* Send content type and last modified headers to the client.
* @param $context ResourceLoaderContext
* @param $mtime string TS_MW timestamp to use for last-modified
- * @param $private bool True iff response contains any private modules
* @return void
*/
- protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $private ) {
+ protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime ) {
global $wgResourceLoaderMaxage;
// If a version wasn't specified we need a shorter expiry time for updates
// to propagate to clients quickly
header( 'Cache-Control: private, no-cache, must-revalidate' );
header( 'Pragma: no-cache' );
} else {
- if ( $private ) {
- header( "Cache-Control: private, max-age=$maxage" );
- $exp = $maxage;
- } else {
- header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
- $exp = min( $maxage, $smaxage );
- }
+ header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
+ $exp = min( $maxage, $smaxage );
header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
}
}
header( 'HTTP/1.0 304 Not Modified' );
header( 'Status: 304 Not Modified' );
- wfProfileOut( __METHOD__ );
return true;
}
}
/**
* Send out code for a response from file cache if possible
*
- * @param $fileCache ObjectFileCache: Cache object for this request URL
+ * @param $fileCache ResourceFileCache: Cache object for this request URL
* @param $context ResourceLoaderContext: Context in which to generate a response
* @return bool If this found a cache file and handled the response
*/
return false; // cache miss
}
+ protected function makeComment( $text ) {
+ $encText = str_replace( '*/', '* /', $text );
+ return "/*\n$encText\n*/\n";
+ }
+
/**
* Generates code for a response
*
$blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
} catch ( Exception $e ) {
// Add exception to the output as a comment
- $exceptions .= "/*\n{$e->__toString()}\n*/\n";
+ $exceptions .= $this->makeComment( $e->__toString() );
}
} else {
$blobs = array();
}
// Generate output
+ $isRaw = false;
foreach ( $modules as $name => $module ) {
/**
* @var $module ResourceLoaderModule
// Styles
$styles = array();
if ( $context->shouldIncludeStyles() ) {
- // If we are in debug mode, we'll want to return an array of URLs
- // See comment near shouldIncludeScripts() for more details
- if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
- $styles = $module->getStyleURLsForDebug( $context );
- } else {
- $styles = $module->getStyles( $context );
+ // Don't create empty stylesheets like array( '' => '' ) for modules
+ // that don't *have* any stylesheets (bug 38024).
+ $stylePairs = $module->getStyles( $context );
+ if ( count ( $stylePairs ) ) {
+ // If we are in debug mode without &only= set, we'll want to return an array of URLs
+ // See comment near shouldIncludeScripts() for more details
+ if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
+ $styles = $module->getStyleURLsForDebug( $context );
+ } else {
+ // Minify CSS before embedding in mw.loader.implement call
+ // (unless in debug mode)
+ if ( !$context->getDebug() ) {
+ foreach ( $stylePairs as $media => $style ) {
+ if ( is_string( $style ) ) {
+ $stylePairs[$media] = $this->filter( 'minify-css', $style );
+ }
+ }
+ }
+ // Combine styles into @media groups as one big string
+ $styles = array( '' => self::makeCombinedStyles( $stylePairs ) );
+ }
}
}
}
break;
case 'styles':
- $out .= self::makeCombinedStyles( $styles );
+ // We no longer seperate into media, they are all concatenated now with
+ // custom media type groups into @media .. {} sections.
+ // Module returns either an empty array or an array with '' (no media type) as
+ // only key.
+ $out .= isset( $styles[''] ) ? $styles[''] : '';
break;
case 'messages':
$out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
break;
default:
- // Minify CSS before embedding in mw.loader.implement call
- // (unless in debug mode)
- if ( !$context->getDebug() ) {
- foreach ( $styles as $media => $style ) {
- if ( is_string( $style ) ) {
- $styles[$media] = $this->filter( 'minify-css', $style );
- }
- }
- }
- $out .= self::makeLoaderImplementScript( $name, $scripts, $styles,
- new XmlJsCode( $messagesBlob ) );
+ $out .= self::makeLoaderImplementScript(
+ $name,
+ $scripts,
+ $styles,
+ new XmlJsCode( $messagesBlob )
+ );
break;
}
} catch ( Exception $e ) {
// Add exception to the output as a comment
- $exceptions .= "/*\n{$e->__toString()}\n*/\n";
+ $exceptions .= $this->makeComment( $e->__toString() );
// Register module as missing
$missing[] = $name;
unset( $modules[$name] );
}
+ $isRaw |= $module->isRaw();
wfProfileOut( __METHOD__ . '-' . $name );
}
// Update module states
- if ( $context->shouldIncludeScripts() ) {
+ if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) {
// Set the state of modules loaded as only scripts to ready
- if ( count( $modules ) && $context->getOnly() === 'scripts'
- && !isset( $modules['startup'] ) )
- {
+ if ( count( $modules ) && $context->getOnly() === 'scripts' ) {
$out .= self::makeLoaderStateScript(
array_fill_keys( array_keys( $modules ), 'ready' ) );
}
* Returns JS code to call to mw.loader.implement for a module with
* given properties.
*
- * @param $name Module name
+ * @param $name string Module name
* @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code
- * @param $styles Mixed: List of CSS strings keyed by media type, or list of lists of URLs to
+ * @param $styles Mixed: Array of CSS strings keyed by media type, or an array of lists of URLs to
* CSS files keyed by media type
* @param $messages Mixed: List of messages associated with this module. May either be an
* associative array mapping message key to value, or a JSON-encoded message blob containing
*/
public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
if ( is_string( $scripts ) ) {
- $scripts = new XmlJsCode( "function( $ ) {{$scripts}}" );
+ $scripts = new XmlJsCode( "function () {\n{$scripts}\n}" );
} elseif ( !is_array( $scripts ) ) {
throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
}
array(
$name,
$scripts,
+ // Force objects. mw.loader.implement requires them to be javascript objects.
+ // Although these variables are associative arrays, which become javascript
+ // objects through json_encode. In many cases they will be empty arrays, and
+ // PHP/json_encode() consider empty arrays to be numerical arrays and
+ // output javascript "[]" instead of "{}". This fixes that.
(object)$styles,
(object)$messages
) );
public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script ) {
$script = str_replace( "\n", "\n\t", trim( $script ) );
return Xml::encodeJsCall(
- "( function( name, version, dependencies, group, source ) {\n\t$script\n} )",
+ "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
array( $name, $version, $dependencies, $group, $source ) );
}
* @return string
*/
public static function makeLoaderConditionalScript( $script ) {
- $script = str_replace( "\n", "\n\t", trim( $script ) );
- return "if(window.mw){\n\t$script\n}\n";
+ return "if(window.mw){\n" . trim( $script ) . "\n}";
}
/**
ksort( $query );
return $query;
}
+
+ /**
+ * Check a module name for validity.
+ *
+ * Module names may not contain pipes (|), commas (,) or exclamation marks (!) and can be
+ * at most 255 bytes.
+ *
+ * @param $moduleName string Module name to check
+ * @return bool Whether $moduleName is a valid module name
+ */
+ public static function isValidModuleName( $moduleName ) {
+ return !preg_match( '/[|,!]/', $moduleName ) && strlen( $moduleName ) <= 255;
+ }
}