diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2014-12-27 15:41:37 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2014-12-31 11:43:28 +0100 |
commit | c1f9b1f7b1b77776192048005dcc66dcf3df2bfb (patch) | |
tree | 2b38796e738dd74cb42ecd9bfd151803108386bc /includes/resourceloader/ResourceLoader.php | |
parent | b88ab0086858470dd1f644e64cb4e4f62bb2be9b (diff) |
Update to MediaWiki 1.24.1
Diffstat (limited to 'includes/resourceloader/ResourceLoader.php')
-rw-r--r-- | includes/resourceloader/ResourceLoader.php | 670 |
1 files changed, 453 insertions, 217 deletions
diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 6380efcf..4f1414bc 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -25,35 +25,39 @@ /** * Dynamic JavaScript and CSS resource loading system. * - * Most of the documention is on the MediaWiki documentation wiki starting at: - * http://www.mediawiki.org/wiki/ResourceLoader + * Most of the documentation is on the MediaWiki documentation wiki starting at: + * https://www.mediawiki.org/wiki/ResourceLoader */ class ResourceLoader { - - /* Protected Static Members */ + /** @var int */ protected static $filterCacheVersion = 7; - protected static $requiredSourceProperties = array( 'loadScript' ); - /** Array: List of module name/ResourceLoaderModule object pairs */ + /** @var bool */ + protected static $debugMode = null; + + /** @var array Module name/ResourceLoaderModule object pairs */ protected $modules = array(); - /** Associative array mapping module name to info associative array */ + /** @var array 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', .. ), .. ) */ + /** @var Config $config */ + private $config; + + /** + * @var 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' ) ) **/ + /** @var array E.g. array( 'source-id' => 'http://.../load.php' ) */ protected $sources = array(); /** @var bool */ protected $hasErrors = false; - /* Protected Methods */ - /** - * Loads information stored in the database about modules. + * Load information stored in the database about modules. * * This method grabs modules dependencies from the database and updates modules * objects. @@ -64,11 +68,12 @@ class ResourceLoader { * performance improvement. * * @param array $modules List of module names to preload information for - * @param $context ResourceLoaderContext: Context to load the information within + * @param ResourceLoaderContext $context Context to load the information within */ public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) { if ( !count( $modules ) ) { - return; // or else Database*::select() will explode, plus it's cheaper! + // Or else Database*::select() will explode, plus it's cheaper! + return; } $dbr = wfGetDB( DB_SLAVE ); $skin = $context->getSkin(); @@ -84,21 +89,26 @@ class ResourceLoader { // Set modules' dependencies $modulesWithDeps = array(); foreach ( $res as $row ) { - $this->getModule( $row->md_module )->setFileDependencies( $skin, - FormatJson::decode( $row->md_deps, true ) - ); - $modulesWithDeps[] = $row->md_module; + $module = $this->getModule( $row->md_module ); + if ( $module ) { + $module->setFileDependencies( $skin, FormatJson::decode( $row->md_deps, true ) ); + $modulesWithDeps[] = $row->md_module; + } } // Register the absence of a dependency row too foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) { - $this->getModule( $name )->setFileDependencies( $skin, array() ); + $module = $this->getModule( $name ); + if ( $module ) { + $this->getModule( $name )->setFileDependencies( $skin, array() ); + } } // Get message blob mtimes. Only do this for modules with messages $modulesWithMessages = array(); foreach ( $modules as $name ) { - if ( count( $this->getModule( $name )->getMessages() ) ) { + $module = $this->getModule( $name ); + if ( $module && count( $module->getMessages() ) ) { $modulesWithMessages[] = $name; } } @@ -110,39 +120,43 @@ class ResourceLoader { ), __METHOD__ ); foreach ( $res as $row ) { - $this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang, - wfTimestamp( TS_UNIX, $row->mr_timestamp ) ); - unset( $modulesWithoutMessages[$row->mr_resource] ); + $module = $this->getModule( $row->mr_resource ); + if ( $module ) { + $module->setMsgBlobMtime( $lang, wfTimestamp( TS_UNIX, $row->mr_timestamp ) ); + unset( $modulesWithoutMessages[$row->mr_resource] ); + } } } foreach ( array_keys( $modulesWithoutMessages ) as $name ) { - $this->getModule( $name )->setMsgBlobMtime( $lang, 0 ); + $module = $this->getModule( $name ); + if ( $module ) { + $module->setMsgBlobMtime( $lang, 0 ); + } } } /** - * Runs JavaScript or CSS data through a filter, caching the filtered result for future calls. + * Run JavaScript or CSS data through a filter, caching the filtered result for future calls. * * Available filters are: - * - minify-js \see JavaScriptMinifier::minify - * - minify-css \see CSSMin::minify + * + * - minify-js \see JavaScriptMinifier::minify + * - minify-css \see CSSMin::minify * * If $data is empty, only contains whitespace or the filter was unknown, * $data is returned unmodified. * * @param string $filter Name of filter to run * @param string $data Text to filter, such as JavaScript or CSS text - * @return String: Filtered data, or a comment containing an error message + * @param string $cacheReport Whether to include the cache key report + * @return string Filtered data, or a comment containing an error message */ - protected function filter( $filter, $data ) { - global $wgResourceLoaderMinifierStatementsOnOwnLine, $wgResourceLoaderMinifierMaxLineLength; + public function filter( $filter, $data, $cacheReport = true ) { wfProfileIn( __METHOD__ ); // For empty/whitespace-only data or for unknown filters, don't perform // any caching or processing - if ( trim( $data ) === '' - || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) - { + if ( trim( $data ) === '' || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) { wfProfileOut( __METHOD__ ); return $data; } @@ -165,14 +179,18 @@ class ResourceLoader { switch ( $filter ) { case 'minify-js': $result = JavaScriptMinifier::minify( $data, - $wgResourceLoaderMinifierStatementsOnOwnLine, - $wgResourceLoaderMinifierMaxLineLength + $this->config->get( 'ResourceLoaderMinifierStatementsOnOwnLine' ), + $this->config->get( 'ResourceLoaderMinifierMaxLineLength' ) ); - $result .= "\n/* cache key: $key */"; + if ( $cacheReport ) { + $result .= "\n/* cache key: $key */"; + } break; case 'minify-css': $result = CSSMin::minify( $data ); - $result .= "\n/* cache key: $key */"; + if ( $cacheReport ) { + $result .= "\n/* cache key: $key */"; + } break; } @@ -194,26 +212,34 @@ class ResourceLoader { /* Methods */ /** - * Registers core modules and runs registration hooks. + * Register core modules and runs registration hooks. + * @param Config|null $config */ - public function __construct() { - global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript, $wgEnableJavaScriptTest; + public function __construct( Config $config = null ) { + global $IP; wfProfileIn( __METHOD__ ); + if ( $config === null ) { + wfDebug( __METHOD__ . ' was called without providing a Config instance' ); + $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); + } + + $this->config = $config; + // Add 'local' source first - $this->addSource( 'local', array( 'loadScript' => $wgLoadScript, 'apiScript' => wfScript( 'api' ) ) ); + $this->addSource( 'local', wfScript( 'load' ) ); // Add other sources - $this->addSource( $wgResourceLoaderSources ); + $this->addSource( $config->get( 'ResourceLoaderSources' ) ); // Register core modules $this->register( include "$IP/resources/Resources.php" ); // Register extension modules wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) ); - $this->register( $wgResourceModules ); + $this->register( $config->get( 'ResourceModules' ) ); - if ( $wgEnableJavaScriptTest === true ) { + if ( $config->get( 'EnableJavaScriptTest' ) === true ) { $this->registerTestModules(); } @@ -221,17 +247,24 @@ class ResourceLoader { } /** - * Registers a module with the ResourceLoader system. + * @return Config + */ + public function getConfig() { + return $this->config; + } + + /** + * Register 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 mixed $name Name of module as a string or List of name/object pairs as an array * @param array $info 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 - * @throws MWException: If a module name contains illegal characters (pipes or commas) - * @throws MWException: If something other than a ResourceLoaderModule is being registered - * @return Boolean: False if there were any errors, in which case one or more modules were not - * registered + * @throws MWException If a duplicate module registration is attempted + * @throws MWException If a module name contains illegal characters (pipes or commas) + * @throws MWException If something other than a ResourceLoaderModule is being registered + * @return bool False if there were any errors, in which case one or more modules were + * not registered */ public function register( $name, $info = null ) { wfProfileIn( __METHOD__ ); @@ -252,25 +285,64 @@ class ResourceLoader { // Check $name for validity if ( !self::isValidModuleName( $name ) ) { wfProfileOut( __METHOD__ ); - throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" ); + throw new MWException( "ResourceLoader module name '$name' is invalid, " + . "see ResourceLoader::isValidModuleName()" ); } // Attach module - if ( is_object( $info ) ) { - // Old calling convention - // Validate the input - if ( !( $info instanceof ResourceLoaderModule ) ) { - wfProfileOut( __METHOD__ ); - throw new MWException( 'ResourceLoader invalid module error. ' . - 'Instances of ResourceLoaderModule expected.' ); - } - + if ( $info instanceof ResourceLoaderModule ) { $this->moduleInfos[$name] = array( 'object' => $info ); $info->setName( $name ); $this->modules[$name] = $info; - } else { + } elseif ( is_array( $info ) ) { // New calling convention $this->moduleInfos[$name] = $info; + } else { + wfProfileOut( __METHOD__ ); + throw new MWException( + 'ResourceLoader module info type error for module \'' . $name . + '\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')' + ); + } + + // Last-minute changes + + // Apply custom skin-defined styles to existing modules. + if ( $this->isFileModule( $name ) ) { + foreach ( $this->config->get( 'ResourceModuleSkinStyles' ) as $skinName => $skinStyles ) { + // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles. + if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) { + continue; + } + + // If $name is preceded with a '+', the defined style files will be added to 'default' + // skinStyles, otherwise 'default' will be ignored as it normally would be. + if ( isset( $skinStyles[$name] ) ) { + $paths = (array)$skinStyles[$name]; + $styleFiles = array(); + } elseif ( isset( $skinStyles['+' . $name] ) ) { + $paths = (array)$skinStyles['+' . $name]; + $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ? + $this->moduleInfos[$name]['skinStyles']['default'] : + array(); + } else { + continue; + } + + // Add new file paths, remapping them to refer to our directories and not use settings + // from the module we're modifying. These can come from the base definition or be defined + // for each module. + list( $localBasePath, $remoteBasePath ) = + ResourceLoaderFileModule::extractBasePaths( $skinStyles ); + list( $localBasePath, $remoteBasePath ) = + ResourceLoaderFileModule::extractBasePaths( $paths, $localBasePath, $remoteBasePath ); + + foreach ( $paths as $path ) { + $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath ); + } + + $this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles; + } } } @@ -280,17 +352,19 @@ class ResourceLoader { /** */ public function registerTestModules() { - global $IP, $wgEnableJavaScriptTest; + global $IP; - if ( $wgEnableJavaScriptTest !== true ) { - throw new MWException( 'Attempt to register JavaScript test modules but <code>$wgEnableJavaScriptTest</code> is false. Edit your <code>LocalSettings.php</code> to enable it.' ); + if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) { + throw new MWException( 'Attempt to register JavaScript test modules ' + . 'but <code>$wgEnableJavaScriptTest</code> is false. ' + . 'Edit your <code>LocalSettings.php</code> to enable it.' ); } wfProfileIn( __METHOD__ ); // Get core test suites $testModules = array(); - $testModules['qunit'] = include "$IP/tests/qunit/QUnitTestResources.php"; + $testModules['qunit'] = array(); // Get other test suites (e.g. from extensions) wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) ); @@ -301,9 +375,12 @@ class ResourceLoader { // on document-ready, it will run once and finish. If some tests arrive // later (possibly after QUnit has already finished) they will be ignored. $module['position'] = 'top'; - $module['dependencies'][] = 'mediawiki.tests.qunit.testrunner'; + $module['dependencies'][] = 'test.mediawiki.qunit.testrunner'; } + $testModules['qunit'] = + ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules['qunit']; + foreach ( $testModules as $id => $names ) { // Register test modules $this->register( $testModules[$id] ); @@ -318,14 +395,12 @@ class ResourceLoader { /** * Add a foreign source of modules. * - * Source properties: - * 'loadScript': URL (either fully-qualified or protocol-relative) of load.php for this source - * - * @param $id Mixed: source ID (string), or array( id1 => props1, id2 => props2, ... ) - * @param array $properties source properties + * @param array|string $id Source ID (string), or array( id1 => loadUrl, id2 => loadUrl, ... ) + * @param string|array $loadUrl load.php url (string), or array with loadUrl key for + * backwards-compatibility. * @throws MWException */ - public function addSource( $id, $properties = null ) { + public function addSource( $id, $loadUrl = null ) { // Allow multiple sources to be registered in one call if ( is_array( $id ) ) { foreach ( $id as $key => $value ) { @@ -342,20 +417,24 @@ class ResourceLoader { ); } - // Validate properties - foreach ( self::$requiredSourceProperties as $prop ) { - if ( !isset( $properties[$prop] ) ) { - throw new MWException( "Required property $prop missing from source ID $id" ); + // Pre 1.24 backwards-compatibility + if ( is_array( $loadUrl ) ) { + if ( !isset( $loadUrl['loadScript'] ) ) { + throw new MWException( + __METHOD__ . ' was passed an array with no "loadScript" key.' + ); } + + $loadUrl = $loadUrl['loadScript']; } - $this->sources[$id] = $properties; + $this->sources[$id] = $loadUrl; } /** - * Get a list of module names + * Get a list of module names. * - * @return Array: List of module names + * @return array List of module names */ public function getModuleNames() { return array_keys( $this->moduleInfos ); @@ -363,18 +442,21 @@ class ResourceLoader { /** * 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 string $framework Optional. Get only the test module names for one - * particular framework. - * @return Array + * @param string $framework Get only the test module names for one + * particular framework (optional) + * @return array */ public function getTestModuleNames( $framework = 'all' ) { - /// @todo api siteinfo prop testmodulenames modulenames + /** @todo api siteinfo prop testmodulenames modulenames */ if ( $framework == 'all' ) { return $this->testModuleNames; - } elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) { + } elseif ( isset( $this->testModuleNames[$framework] ) + && is_array( $this->testModuleNames[$framework] ) + ) { return $this->testModuleNames[$framework]; } else { return array(); @@ -384,8 +466,13 @@ class ResourceLoader { /** * Get the ResourceLoaderModule object for a given module name. * + * If an array of module parameters exists but a ResourceLoaderModule object has not + * yet been instantiated, this method will instantiate and cache that object such that + * subsequent calls simply return the same object. + * * @param string $name Module name - * @return ResourceLoaderModule if module has been registered, null otherwise + * @return ResourceLoaderModule|null If module has been registered, return a + * ResourceLoaderModule instance. Otherwise, return null. */ public function getModule( $name ) { if ( !isset( $this->modules[$name] ) ) { @@ -405,7 +492,9 @@ class ResourceLoader { } else { $class = $info['class']; } + /** @var ResourceLoaderModule $object */ $object = new $class( $info ); + $object->setConfig( $this->getConfig() ); } $object->setName( $name ); $this->modules[$name] = $object; @@ -415,24 +504,55 @@ class ResourceLoader { } /** - * Get the list of sources + * Return whether the definition of a module corresponds to a simple ResourceLoaderFileModule. * - * @return Array: array( id => array of properties, .. ) + * @param string $name Module name + * @return bool + */ + protected function isFileModule( $name ) { + if ( !isset( $this->moduleInfos[$name] ) ) { + return false; + } + $info = $this->moduleInfos[$name]; + if ( isset( $info['object'] ) || isset( $info['class'] ) ) { + return false; + } + return true; + } + + /** + * Get the list of sources. + * + * @return array Like array( id => load.php url, .. ) */ public function getSources() { return $this->sources; } /** - * Outputs a response to a resource load-request, including a content-type header. + * Get the URL to the load.php endpoint for the given + * ResourceLoader source * - * @param $context ResourceLoaderContext: Context in which a response should be formed + * @since 1.24 + * @param string $source + * @throws MWException On an invalid $source name + * @return string */ - public function respond( ResourceLoaderContext $context ) { - global $wgCacheEpoch, $wgUseFileCache; + public function getLoadScript( $source ) { + if ( !isset( $this->sources[$source] ) ) { + throw new MWException( "The $source source was never registered in ResourceLoader." ); + } + return $this->sources[$source]; + } + /** + * Output a response to a load request, including the content-type header. + * + * @param ResourceLoaderContext $context Context in which a response should be formed + */ + public function respond( ResourceLoaderContext $context ) { // Use file cache if enabled and available... - if ( $wgUseFileCache ) { + if ( $this->config->get( 'UseFileCache' ) ) { $fileCache = ResourceFileCache::newFromContext( $context ); if ( $this->tryRespondFromFileCache( $fileCache, $context ) ) { return; // output handled @@ -451,12 +571,12 @@ class ResourceLoader { wfProfileIn( __METHOD__ ); $errors = ''; - // Split requested modules into two groups, modules and missing + // Find out which modules are missing and instantiate the others $modules = array(); $missing = array(); foreach ( $context->getModules() as $name ) { - if ( isset( $this->moduleInfos[$name] ) ) { - $module = $this->getModule( $name ); + $module = $this->getModule( $name ); + if ( $module ) { // Do not allow private modules to be loaded from the web. // This is a security issue, see bug 34907. if ( $module->getGroup() === 'private' ) { @@ -488,7 +608,7 @@ class ResourceLoader { // To send Last-Modified and support If-Modified-Since, we need to detect // the last modified time - $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch ); + $mtime = wfTimestamp( TS_UNIX, $this->config->get( 'CacheEpoch' ) ); foreach ( $modules as $module ) { /** * @var $module ResourceLoaderModule @@ -527,7 +647,7 @@ class ResourceLoader { } // Save response to file cache unless there are errors - if ( isset( $fileCache ) && !$errors && !$missing ) { + if ( isset( $fileCache ) && !$errors && !count( $missing ) ) { // Cache single modules...and other requests if there are enough hits if ( ResourceFileCache::useFileCache( $context ) ) { if ( $fileCache->isCacheWorthy() ) { @@ -550,24 +670,24 @@ class ResourceLoader { /** * Send content type and last modified headers to the client. - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @param string $mtime TS_MW timestamp to use for last-modified * @param bool $errors Whether there are commented-out errors in the response * @return void */ protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) { - global $wgResourceLoaderMaxage; + $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' ); // If a version wasn't specified we need a shorter expiry time for updates // to propagate to clients quickly // If there were errors, we also need a shorter expiry time so we can recover quickly if ( is_null( $context->getVersion() ) || $errors ) { - $maxage = $wgResourceLoaderMaxage['unversioned']['client']; - $smaxage = $wgResourceLoaderMaxage['unversioned']['server']; + $maxage = $rlMaxage['unversioned']['client']; + $smaxage = $rlMaxage['unversioned']['server']; // If a version was specified we can use a longer expiry time since changing // version numbers causes cache misses } else { - $maxage = $wgResourceLoaderMaxage['versioned']['client']; - $smaxage = $wgResourceLoaderMaxage['versioned']['server']; + $maxage = $rlMaxage['versioned']['client']; + $smaxage = $rlMaxage['versioned']['server']; } if ( $context->getOnly() === 'styles' ) { header( 'Content-Type: text/css; charset=utf-8' ); @@ -588,9 +708,12 @@ class ResourceLoader { } /** + * Respond with 304 Last Modified if appropiate. + * * If there's an If-Modified-Since header, respond with a 304 appropriately * and clear out the output buffer. If the client cache is too old then do nothing. - * @param $context ResourceLoaderContext + * + * @param ResourceLoaderContext $context * @param string $mtime The TS_MW timestamp to check the header against * @return bool True if 304 header sent and output handled */ @@ -623,22 +746,22 @@ class ResourceLoader { } /** - * Send out code for a response from file cache if possible + * Send out code for a response from file cache if possible. * - * @param $fileCache ResourceFileCache: Cache object for this request URL - * @param $context ResourceLoaderContext: Context in which to generate a response + * @param ResourceFileCache $fileCache Cache object for this request URL + * @param ResourceLoaderContext $context Context in which to generate a response * @return bool If this found a cache file and handled the response */ protected function tryRespondFromFileCache( ResourceFileCache $fileCache, ResourceLoaderContext $context ) { - global $wgResourceLoaderMaxage; + $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' ); // Buffer output to catch warnings. ob_start(); // Get the maximum age the cache can be $maxage = is_null( $context->getVersion() ) - ? $wgResourceLoaderMaxage['unversioned']['server'] - : $wgResourceLoaderMaxage['versioned']['server']; + ? $rlMaxage['unversioned']['server'] + : $rlMaxage['versioned']['server']; // Minimum timestamp the cache file must have $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) ); if ( !$good ) { @@ -674,10 +797,11 @@ class ResourceLoader { } /** - * Generate a CSS or JS comment block. Only use this for public data, - * not error message details. + * Generate a CSS or JS comment block. * - * @param $text string + * Only use this for public data, not error message details. + * + * @param string $text * @return string */ public static function makeComment( $text ) { @@ -686,10 +810,10 @@ class ResourceLoader { } /** - * Handle exception display + * Handle exception display. * - * @param Exception $e to be shown to the user - * @return string sanitized text that can be returned to the user + * @param Exception $e Exception to be shown to the user + * @return string Sanitized text that can be returned to the user */ public static function formatException( $e ) { global $wgShowExceptionDetails; @@ -702,30 +826,38 @@ class ResourceLoader { } /** - * Generates code for a response + * Generate code for a response. * - * @param $context ResourceLoaderContext: Context in which to generate a response + * @param ResourceLoaderContext $context Context in which to generate a response * @param array $modules List of module objects keyed by module name - * @param array $missing List of unavailable modules (optional) - * @return String: Response data + * @param array $missing List of requested module names that are unregistered (optional) + * @return string Response data */ public function makeModuleResponse( ResourceLoaderContext $context, - array $modules, $missing = array() + array $modules, array $missing = array() ) { $out = ''; $exceptions = ''; - if ( $modules === array() && $missing === array() ) { - return '/* No modules requested. Max made me put this here */'; + $states = array(); + + if ( !count( $modules ) && !count( $missing ) ) { + return "/* This file is the Web entry point for MediaWiki's ResourceLoader: + <https://www.mediawiki.org/wiki/ResourceLoader>. In this request, + no modules were requested. Max made me put this here. */"; } wfProfileIn( __METHOD__ ); + // Pre-fetch blobs if ( $context->shouldIncludeMessages() ) { try { - $blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() ); + $blobs = MessageBlobStore::getInstance()->get( $this, $modules, $context->getLanguage() ); } catch ( Exception $e ) { MWExceptionHandler::logException( $e ); - wfDebugLog( 'resourceloader', __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e" ); + wfDebugLog( + 'resourceloader', + __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e" + ); $this->hasErrors = true; // Add exception to the output as a comment $exceptions .= self::formatException( $e ); @@ -734,6 +866,10 @@ class ResourceLoader { $blobs = array(); } + foreach ( $missing as $name ) { + $states[$name] = 'missing'; + } + // Generate output $isRaw = false; foreach ( $modules as $name => $module ) { @@ -753,9 +889,15 @@ class ResourceLoader { $scripts = $module->getScriptURLsForDebug( $context ); } else { $scripts = $module->getScript( $context ); - if ( is_string( $scripts ) && strlen( $scripts ) && substr( $scripts, -1 ) !== ';' ) { - // bug 27054: Append semicolon to prevent weird bugs - // caused by files not terminating their statements right + // rtrim() because there are usually a few line breaks + // after the last ';'. A new line at EOF, a new line + // added by ResourceLoaderFileModule::readScriptFiles, etc. + if ( is_string( $scripts ) + && strlen( $scripts ) + && substr( rtrim( $scripts ), -1 ) !== ';' + ) { + // Append semicolon to prevent weird bugs caused by files not + // terminating their statements right (bug 27054) $scripts .= ";\n"; } } @@ -766,7 +908,7 @@ class ResourceLoader { // 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 ( 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() ) { @@ -838,8 +980,8 @@ class ResourceLoader { // Add exception to the output as a comment $exceptions .= self::formatException( $e ); - // Register module as missing - $missing[] = $name; + // Respond to client with error-state instead of module implementation + $states[$name] = 'error'; unset( $modules[$name] ); } $isRaw |= $module->isRaw(); @@ -848,14 +990,23 @@ class ResourceLoader { // Update module states if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) { - // Set the state of modules loaded as only scripts to ready if ( count( $modules ) && $context->getOnly() === 'scripts' ) { - $out .= self::makeLoaderStateScript( - array_fill_keys( array_keys( $modules ), 'ready' ) ); + // Set the state of modules loaded as only scripts to ready as + // they don't have an mw.loader.implement wrapper that sets the state + foreach ( $modules as $name => $module ) { + $states[$name] = 'ready'; + } + } + + // Set the state of modules we didn't respond to with mw.loader.implement + if ( count( $states ) ) { + $out .= self::makeLoaderStateScript( $states ); } - // Set the state of modules which were requested but unavailable as missing - if ( is_array( $missing ) && count( $missing ) ) { - $out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) ); + } else { + if ( count( $states ) ) { + $exceptions .= self::makeComment( + 'Problematic modules: ' . FormatJson::encode( $states, ResourceLoader::inDebugMode() ) + ); } } @@ -874,23 +1025,21 @@ class ResourceLoader { /* Static Methods */ /** - * Returns JS code to call to mw.loader.implement for a module with - * given properties. + * Return JS code that calls mw.loader.implement with given module properties. * * @param string $name Module name - * @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code - * @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 - * the same data, wrapped in an XmlJsCode object. - * + * @param mixed $scripts List of URLs to JavaScript files or String of JavaScript code + * @param mixed $styles Array of CSS strings keyed by media type, or an array of lists of URLs + * to CSS files keyed by media type + * @param mixed $messages 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 + * the same data, wrapped in an XmlJsCode object. * @throws MWException * @return string */ public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { if ( is_string( $scripts ) ) { - $scripts = new XmlJsCode( "function () {\n{$scripts}\n}" ); + $scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" ); } elseif ( !is_array( $scripts ) ) { throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' ); } @@ -914,24 +1063,26 @@ class ResourceLoader { /** * Returns JS code which, when called, will register a given list of messages. * - * @param $messages Mixed: Either an associative array mapping message key to value, or a - * JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object. - * + * @param mixed $messages Either an associative array mapping message key to value, or a + * JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object. * @return string */ public static function makeMessageSetScript( $messages ) { - return Xml::encodeJsCall( 'mw.messages.set', array( (object)$messages ) ); + return Xml::encodeJsCall( + 'mw.messages.set', + array( (object)$messages ), + ResourceLoader::inDebugMode() + ); } /** * Combines an associative array mapping media type to CSS into a * single stylesheet with "@media" blocks. * - * @param array $stylePairs Array keyed by media type containing (arrays of) CSS strings. - * - * @return Array + * @param array $stylePairs Array keyed by media type containing (arrays of) CSS strings + * @return array */ - private static function makeCombinedStyles( array $stylePairs ) { + public static function makeCombinedStyles( array $stylePairs ) { $out = array(); foreach ( $stylePairs as $media => $styles ) { // ResourceLoaderFileModule::getStyle can return the styles @@ -968,16 +1119,23 @@ class ResourceLoader { * - ResourceLoader::makeLoaderStateScript( array( $name => $state, ... ) ): * Set the state of modules with the given names to the given states * - * @param $name string - * @param $state - * + * @param string $name + * @param string $state * @return string */ public static function makeLoaderStateScript( $name, $state = null ) { if ( is_array( $name ) ) { - return Xml::encodeJsCall( 'mw.loader.state', array( $name ) ); + return Xml::encodeJsCall( + 'mw.loader.state', + array( $name ), + ResourceLoader::inDebugMode() + ); } else { - return Xml::encodeJsCall( 'mw.loader.state', array( $name, $state ) ); + return Xml::encodeJsCall( + 'mw.loader.state', + array( $name, $state ), + ResourceLoader::inDebugMode() + ); } } @@ -988,55 +1146,67 @@ class ResourceLoader { * and $group as supplied. * * @param string $name Module name - * @param $version Integer: Module version number as a timestamp + * @param int $version Module version number as a timestamp * @param array $dependencies List of module names on which this module depends * @param string $group Group which the module is in. * @param string $source Source of the module, or 'local' if not foreign. * @param string $script JavaScript code - * * @return string */ - public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script ) { + 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} )", - array( $name, $version, $dependencies, $group, $source ) ); + array( $name, $version, $dependencies, $group, $source ), + ResourceLoader::inDebugMode() + ); } /** * Returns JS code which calls mw.loader.register with the given * parameters. Has three calling conventions: * - * - ResourceLoader::makeLoaderRegisterScript( $name, $version, $dependencies, $group, $source ): - * Register a single module. + * - ResourceLoader::makeLoaderRegisterScript( $name, $version, + * $dependencies, $group, $source, $skip + * ): + * Register a single module. * * - ResourceLoader::makeLoaderRegisterScript( array( $name1, $name2 ) ): - * Register modules with the given names. + * Register modules with the given names. * * - ResourceLoader::makeLoaderRegisterScript( array( - * array( $name1, $version1, $dependencies1, $group1, $source1 ), - * array( $name2, $version2, $dependencies1, $group2, $source2 ), + * array( $name1, $version1, $dependencies1, $group1, $source1, $skip1 ), + * array( $name2, $version2, $dependencies1, $group2, $source2, $skip2 ), * ... * ) ): * Registers modules with the given names and parameters. * * @param string $name Module name - * @param $version Integer: Module version number as a timestamp + * @param int $version Module version number as a timestamp * @param array $dependencies List of module names on which this module depends - * @param string $group group which the module is in. - * @param string $source source of the module, or 'local' if not foreign - * + * @param string $group Group which the module is in + * @param string $source Source of the module, or 'local' if not foreign + * @param string $skip Script body of the skip function * @return string */ public static function makeLoaderRegisterScript( $name, $version = null, - $dependencies = null, $group = null, $source = null + $dependencies = null, $group = null, $source = null, $skip = null ) { if ( is_array( $name ) ) { - return Xml::encodeJsCall( 'mw.loader.register', array( $name ) ); + return Xml::encodeJsCall( + 'mw.loader.register', + array( $name ), + ResourceLoader::inDebugMode() + ); } else { $version = (int)$version > 1 ? (int)$version : 1; - return Xml::encodeJsCall( 'mw.loader.register', - array( $name, $version, $dependencies, $group, $source ) ); + return Xml::encodeJsCall( + 'mw.loader.register', + array( $name, $version, $dependencies, $group, $source, $skip ), + ResourceLoader::inDebugMode() + ); } } @@ -1047,19 +1217,26 @@ class ResourceLoader { * - ResourceLoader::makeLoaderSourcesScript( $id, $properties ): * Register a single source * - * - ResourceLoader::makeLoaderSourcesScript( array( $id1 => $props1, $id2 => $props2, ... ) ); + * - ResourceLoader::makeLoaderSourcesScript( array( $id1 => $loadUrl, $id2 => $loadUrl, ... ) ); * Register sources with the given IDs and properties. * - * @param string $id source ID - * @param array $properties source properties (see addSource()) - * + * @param string $id Source ID + * @param array $properties Source properties (see addSource()) * @return string */ public static function makeLoaderSourcesScript( $id, $properties = null ) { if ( is_array( $id ) ) { - return Xml::encodeJsCall( 'mw.loader.addSource', array( $id ) ); + return Xml::encodeJsCall( + 'mw.loader.addSource', + array( $id ), + ResourceLoader::inDebugMode() + ); } else { - return Xml::encodeJsCall( 'mw.loader.addSource', array( $id, $properties ) ); + return Xml::encodeJsCall( + 'mw.loader.addSource', + array( $id, $properties ), + ResourceLoader::inDebugMode() + ); } } @@ -1068,7 +1245,6 @@ class ResourceLoader { * present. * * @param string $script JavaScript code - * * @return string */ public static function makeLoaderConditionalScript( $script ) { @@ -1080,11 +1256,14 @@ class ResourceLoader { * the given value. * * @param array $configuration List of configuration values keyed by variable name - * * @return string */ public static function makeConfigSetScript( array $configuration ) { - return Xml::encodeJsCall( 'mw.config.set', array( $configuration ), ResourceLoader::inDebugMode() ); + return Xml::encodeJsCall( + 'mw.config.set', + array( $configuration ), + ResourceLoader::inDebugMode() + ); } /** @@ -1092,7 +1271,7 @@ class ResourceLoader { * * For example, array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ) * becomes 'foo.bar,baz|bar.baz,quux' - * @param array $modules of module names (strings) + * @param array $modules List of module names (strings) * @return string Packed query string */ public static function makePackedModulesString( $modules ) { @@ -1119,18 +1298,50 @@ class ResourceLoader { * @return bool */ public static function inDebugMode() { - global $wgRequest, $wgResourceLoaderDebug; - static $retval = null; - if ( !is_null( $retval ) ) { - return $retval; + if ( self::$debugMode === null ) { + global $wgRequest, $wgResourceLoaderDebug; + self::$debugMode = $wgRequest->getFuzzyBool( 'debug', + $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) + ); } - return $retval = $wgRequest->getFuzzyBool( 'debug', - $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) ); + return self::$debugMode; + } + + /** + * Reset static members used for caching. + * + * Global state and $wgRequest are evil, but we're using it right + * now and sometimes we need to be able to force ResourceLoader to + * re-evaluate the context because it has changed (e.g. in the test suite). + */ + public static function clearCache() { + self::$debugMode = null; } /** * Build a load.php URL - * @param array $modules of module names (strings) + * + * @since 1.24 + * @param string $source Name of the ResourceLoader source + * @param ResourceLoaderContext $context + * @param array $extraQuery + * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative) + */ + public function createLoaderURL( $source, ResourceLoaderContext $context, + $extraQuery = array() + ) { + $query = self::createLoaderQuery( $context, $extraQuery ); + $script = $this->getLoadScript( $source ); + + // Prevent the IE6 extension check from being triggered (bug 28840) + // by appending a character that's invalid in Windows extensions ('*') + return wfExpandUrl( wfAppendQuery( $script, $query ) . '&*', PROTO_RELATIVE ); + } + + /** + * Build a load.php URL + * @deprecated since 1.24, use createLoaderURL instead + * @param array $modules Array of module names (strings) * @param string $lang Language code * @param string $skin Skin name * @param string|null $user User name. If null, the &user= parameter is omitted @@ -1142,9 +1353,12 @@ class ResourceLoader { * @param array $extraQuery Extra query parameters to add * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative) */ - public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null, - $printable = false, $handheld = false, $extraQuery = array() ) { + public static function makeLoaderURL( $modules, $lang, $skin, $user = null, + $version = null, $debug = false, $only = null, $printable = false, + $handheld = false, $extraQuery = array() + ) { global $wgLoadScript; + $query = self::makeLoaderQuery( $modules, $lang, $skin, $user, $version, $debug, $only, $printable, $handheld, $extraQuery ); @@ -1155,6 +1369,30 @@ class ResourceLoader { } /** + * Helper for createLoaderURL() + * + * @since 1.24 + * @see makeLoaderQuery + * @param ResourceLoaderContext $context + * @param array $extraQuery + * @return array + */ + public static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = array() ) { + return self::makeLoaderQuery( + $context->getModules(), + $context->getLanguage(), + $context->getSkin(), + $context->getUser(), + $context->getVersion(), + $context->getDebug(), + $context->getOnly(), + $context->getRequest()->getBool( 'printable' ), + $context->getRequest()->getBool( 'handheld' ), + $extraQuery + ); + } + + /** * Build a query array (array representation of query string) for load.php. Helper * function for makeLoaderURL(). * @@ -1171,8 +1409,10 @@ class ResourceLoader { * * @return array */ - public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null, - $printable = false, $handheld = false, $extraQuery = array() ) { + public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, + $version = null, $debug = false, $only = null, $printable = false, + $handheld = false, $extraQuery = array() + ) { $query = array( 'modules' => self::makePackedModulesString( $modules ), 'lang' => $lang, @@ -1217,12 +1457,12 @@ class ResourceLoader { /** * Returns LESS compiler set up for use with MediaWiki * + * @param Config $config + * @throws MWException * @since 1.22 * @return lessc */ - public static function getLessCompiler() { - global $wgResourceLoaderLESSFunctions, $wgResourceLoaderLESSImportPaths; - + public static function getLessCompiler( Config $config ) { // When called from the installer, it is possible that a required PHP extension // is missing (at least for now; see bug 47564). If this is the case, throw an // exception (caught by the installer) to prevent a fatal error later on. @@ -1232,9 +1472,9 @@ class ResourceLoader { $less = new lessc(); $less->setPreserveComments( true ); - $less->setVariables( self::getLESSVars() ); - $less->setImportDir( $wgResourceLoaderLESSImportPaths ); - foreach ( $wgResourceLoaderLESSFunctions as $name => $func ) { + $less->setVariables( self::getLessVars( $config ) ); + $less->setImportDir( $config->get( 'ResourceLoaderLESSImportPaths' ) ); + foreach ( $config->get( 'ResourceLoaderLESSFunctions' ) as $name => $func ) { $less->registerFunction( $name, $func ); } return $less; @@ -1243,18 +1483,14 @@ class ResourceLoader { /** * Get global LESS variables. * - * $since 1.22 - * @return array: Map of variable names to string CSS values. + * @param Config $config + * @since 1.22 + * @return array Map of variable names to string CSS values. */ - public static function getLESSVars() { - global $wgResourceLoaderLESSVars; - - static $lessVars = null; - if ( $lessVars === null ) { - $lessVars = $wgResourceLoaderLESSVars; - // Sort by key to ensure consistent hashing for cache lookups. - ksort( $lessVars ); - } + public static function getLessVars( Config $config ) { + $lessVars = $config->get( 'ResourceLoaderLESSVars' ); + // Sort by key to ensure consistent hashing for cache lookups. + ksort( $lessVars ); return $lessVars; } } |