From c1f9b1f7b1b77776192048005dcc66dcf3df2bfb Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 27 Dec 2014 15:41:37 +0100 Subject: Update to MediaWiki 1.24.1 --- .../DerivativeResourceLoaderContext.php | 202 +++++++ includes/resourceloader/ResourceLoader.php | 670 ++++++++++++++------- includes/resourceloader/ResourceLoaderContext.php | 49 +- .../ResourceLoaderEditToolbarModule.php | 102 ++++ .../resourceloader/ResourceLoaderFileModule.php | 473 ++++++++++----- .../ResourceLoaderFilePageModule.php | 2 +- includes/resourceloader/ResourceLoaderFilePath.php | 74 +++ .../resourceloader/ResourceLoaderLESSFunctions.php | 67 --- .../ResourceLoaderLanguageDataModule.php | 84 +-- .../ResourceLoaderLanguageNamesModule.php | 79 +++ includes/resourceloader/ResourceLoaderModule.php | 221 +++++-- .../ResourceLoaderNoscriptModule.php | 6 +- .../resourceloader/ResourceLoaderSiteModule.php | 12 +- .../resourceloader/ResourceLoaderStartUpModule.php | 412 +++++++++---- .../ResourceLoaderUserCSSPrefsModule.php | 31 +- .../ResourceLoaderUserGroupsModule.php | 21 +- .../resourceloader/ResourceLoaderUserModule.php | 18 +- .../ResourceLoaderUserOptionsModule.php | 16 +- .../ResourceLoaderUserTokensModule.php | 10 +- .../resourceloader/ResourceLoaderWikiModule.php | 110 ++-- 20 files changed, 1879 insertions(+), 780 deletions(-) create mode 100644 includes/resourceloader/DerivativeResourceLoaderContext.php create mode 100644 includes/resourceloader/ResourceLoaderEditToolbarModule.php create mode 100644 includes/resourceloader/ResourceLoaderFilePath.php delete mode 100644 includes/resourceloader/ResourceLoaderLESSFunctions.php create mode 100644 includes/resourceloader/ResourceLoaderLanguageNamesModule.php (limited to 'includes/resourceloader') diff --git a/includes/resourceloader/DerivativeResourceLoaderContext.php b/includes/resourceloader/DerivativeResourceLoaderContext.php new file mode 100644 index 00000000..d114d7ed --- /dev/null +++ b/includes/resourceloader/DerivativeResourceLoaderContext.php @@ -0,0 +1,202 @@ +context = $context; + } + + public function getModules() { + if ( !is_null( $this->modules ) ) { + return $this->modules; + } else { + return $this->context->getModules(); + } + } + + /** + * @param string[] $modules + */ + public function setModules( array $modules ) { + $this->modules = $modules; + } + + public function getLanguage() { + if ( !is_null( $this->language ) ) { + return $this->language; + } else { + return $this->context->getLanguage(); + } + } + + /** + * @param string $language + */ + public function setLanguage( $language ) { + $this->language = $language; + $this->direction = null; // Invalidate direction since it might be based on language + $this->hash = null; + } + + public function getDirection() { + if ( !is_null( $this->direction ) ) { + return $this->direction; + } else { + return $this->context->getDirection(); + } + } + + /** + * @param string $direction + */ + public function setDirection( $direction ) { + $this->direction = $direction; + $this->hash = null; + } + + public function getSkin() { + if ( !is_null( $this->skin ) ) { + return $this->skin; + } else { + return $this->context->getSkin(); + } + } + + /** + * @param string $skin + */ + public function setSkin( $skin ) { + $this->skin = $skin; + $this->hash = null; + } + + public function getUser() { + if ( !is_null( $this->user ) ) { + return $this->user; + } else { + return $this->context->getUser(); + } + } + + /** + * @param string $user + */ + public function setUser( $user ) { + $this->user = $user; + $this->hash = null; + } + + public function getDebug() { + if ( !is_null( $this->debug ) ) { + return $this->debug; + } else { + return $this->context->getDebug(); + } + } + + /** + * @param bool $debug + */ + public function setDebug( $debug ) { + $this->debug = $debug; + $this->hash = null; + } + + public function getOnly() { + if ( !is_null( $this->only ) ) { + return $this->only; + } else { + return $this->context->getOnly(); + } + } + + /** + * @param string $only + */ + public function setOnly( $only ) { + $this->only = $only; + $this->hash = null; + } + + public function getVersion() { + if ( !is_null( $this->version ) ) { + return $this->version; + } else { + return $this->context->getVersion(); + } + } + + /** + * @param string $version + */ + public function setVersion( $version ) { + $this->version = $version; + $this->hash = null; + } + + public function getRaw() { + if ( !is_null( $this->raw ) ) { + return $this->raw; + } else { + return $this->context->getRaw(); + } + } + + /** + * @param bool $raw + */ + public function setRaw( $raw ) { + $this->raw = $raw; + } + + public function getRequest() { + return $this->context->getRequest(); + } + + public function getResourceLoader() { + return $this->context->getResourceLoader(); + } + +} 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 $wgEnableJavaScriptTest is false. Edit your LocalSettings.php to enable it.' ); + if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) { + throw new MWException( 'Attempt to register JavaScript test modules ' + . 'but $wgEnableJavaScriptTest is false. ' + . 'Edit your LocalSettings.php 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: + . 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 ); @@ -1154,6 +1368,30 @@ class ResourceLoader { return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE ); } + /** + * 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; } } diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index 22ff6a7e..7af7b898 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -27,7 +27,6 @@ * of a specific loader request */ class ResourceLoaderContext { - /* Protected Members */ protected $resourceLoader; @@ -46,12 +45,10 @@ class ResourceLoaderContext { /* Methods */ /** - * @param $resourceLoader ResourceLoader - * @param $request WebRequest + * @param ResourceLoader $resourceLoader + * @param WebRequest $request */ - public function __construct( $resourceLoader, WebRequest $request ) { - global $wgDefaultSkin, $wgResourceLoaderDebug; - + public function __construct( ResourceLoader $resourceLoader, WebRequest $request ) { $this->resourceLoader = $resourceLoader; $this->request = $request; @@ -62,7 +59,9 @@ class ResourceLoaderContext { // Various parameters $this->skin = $request->getVal( 'skin' ); $this->user = $request->getVal( 'user' ); - $this->debug = $request->getFuzzyBool( 'debug', $wgResourceLoaderDebug ); + $this->debug = $request->getFuzzyBool( + 'debug', $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' ) + ); $this->only = $request->getVal( 'only' ); $this->version = $request->getVal( 'version' ); $this->raw = $request->getFuzzyBool( 'raw' ); @@ -70,7 +69,7 @@ class ResourceLoaderContext { $skinnames = Skin::getSkinNames(); // If no skin is specified, or we don't recognize the skin, use the default skin if ( !$this->skin || !isset( $skinnames[$this->skin] ) ) { - $this->skin = $wgDefaultSkin; + $this->skin = $resourceLoader->getConfig()->get( 'DefaultSkin' ); } } @@ -79,12 +78,10 @@ class ResourceLoaderContext { * an array of module names like array( 'jquery.foo', 'jquery.bar', * 'jquery.ui.baz', 'jquery.ui.quux' ) * @param string $modules Packed module name list - * @return array of module names + * @return array Array of module names */ public static function expandModuleNames( $modules ) { $retval = array(); - // For backwards compatibility with an earlier hack, replace ! with . - $modules = str_replace( '!', '.', $modules ); $exploded = explode( '|', $modules ); foreach ( $exploded as $group ) { if ( strpos( $group, ',' ) === false ) { @@ -111,11 +108,14 @@ class ResourceLoaderContext { } /** - * Return a dummy ResourceLoaderContext object suitable for passing into things that don't "really" need a context + * Return a dummy ResourceLoaderContext object suitable for passing into + * things that don't "really" need a context. * @return ResourceLoaderContext */ public static function newDummyContext() { - return new self( null, new FauxRequest( array() ) ); + return new self( new ResourceLoader( + ConfigFactory::getDefaultInstance()->makeConfig( 'main' ) + ), new FauxRequest( array() ) ); } /** @@ -144,11 +144,8 @@ class ResourceLoaderContext { */ public function getLanguage() { if ( $this->language === null ) { - global $wgLang; - $this->language = $this->request->getVal( 'lang' ); - if ( !$this->language ) { - $this->language = $wgLang->getCode(); - } + // Must be a valid language code after this point (bug 62849) + $this->language = RequestContext::sanitizeLangCode( $this->request->getVal( 'lang' ) ); } return $this->language; } @@ -160,7 +157,7 @@ class ResourceLoaderContext { if ( $this->direction === null ) { $this->direction = $this->request->getVal( 'dir' ); if ( !$this->direction ) { - # directionality based on user language (see bug 6100) + // Determine directionality based on user language (bug 6100) $this->direction = Language::factory( $this->getLanguage() )->getDir(); } } @@ -189,14 +186,14 @@ class ResourceLoaderContext { } /** - * @return String|null + * @return string|null */ public function getOnly() { return $this->only; } /** - * @return String|null + * @return string|null */ public function getVersion() { return $this->version; @@ -213,21 +210,21 @@ class ResourceLoaderContext { * @return bool */ public function shouldIncludeScripts() { - return is_null( $this->only ) || $this->only === 'scripts'; + return is_null( $this->getOnly() ) || $this->getOnly() === 'scripts'; } /** * @return bool */ public function shouldIncludeStyles() { - return is_null( $this->only ) || $this->only === 'styles'; + return is_null( $this->getOnly() ) || $this->getOnly() === 'styles'; } /** * @return bool */ public function shouldIncludeMessages() { - return is_null( $this->only ) || $this->only === 'messages'; + return is_null( $this->getOnly() ) || $this->getOnly() === 'messages'; } /** @@ -236,8 +233,8 @@ class ResourceLoaderContext { public function getHash() { if ( !isset( $this->hash ) ) { $this->hash = implode( '|', array( - $this->getLanguage(), $this->getDirection(), $this->skin, $this->user, - $this->debug, $this->only, $this->version + $this->getLanguage(), $this->getDirection(), $this->getSkin(), $this->getUser(), + $this->getDebug(), $this->getOnly(), $this->getVersion() ) ); } return $this->hash; diff --git a/includes/resourceloader/ResourceLoaderEditToolbarModule.php b/includes/resourceloader/ResourceLoaderEditToolbarModule.php new file mode 100644 index 00000000..2e07911c --- /dev/null +++ b/includes/resourceloader/ResourceLoaderEditToolbarModule.php @@ -0,0 +1,102 @@ + '\\\\', '"' => '\\"' ) ); + $value = preg_replace_callback( '/[\x01-\x1f\x7f-\x9f]/', function ( $match ) { + return '\\' . base_convert( ord( $match[0] ), 10, 16 ) . ' '; + }, $value ); + return '"' . $value . '"'; + } + + /** + * Get language-specific LESS variables for this module. + * + * @return array + */ + private function getLessVars( ResourceLoaderContext $context ) { + $language = Language::factory( $context->getLanguage() ); + + // This is very conveniently formatted and we can pass it right through + $vars = $language->getImageFiles(); + + // lessc tries to be helpful and parse our variables as LESS source code + foreach ( $vars as $key => &$value ) { + $value = self::cssSerializeString( $value ); + } + + return $vars; + } + + /** + * @param ResourceLoaderContext $context + * @return int UNIX timestamp + */ + public function getModifiedTime( ResourceLoaderContext $context ) { + return max( + parent::getModifiedTime( $context ), + $this->getHashMtime( $context ) + ); + } + + /** + * @param ResourceLoaderContext $context + * @return string Hash + */ + public function getModifiedHash( ResourceLoaderContext $context ) { + return md5( + parent::getModifiedHash( $context ) . + serialize( $this->getLessVars( $context ) ) + ); + } + + /** + * Get a LESS compiler instance for this module. + * + * Set our variables in it. + * + * @throws MWException + * @param ResourceLoaderContext $context + * @return lessc + */ + protected function getLessCompiler( ResourceLoaderContext $context = null ) { + $compiler = parent::getLessCompiler(); + $compiler->setVariables( $this->getLessVars( $context ) ); + return $compiler; + } +} diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index 9ed181ed..dc8b14a2 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -26,111 +26,131 @@ * ResourceLoader module based on local JavaScript/CSS files. */ class ResourceLoaderFileModule extends ResourceLoaderModule { - /* Protected Members */ - /** String: Local base path, see __construct() */ + /** @var string Local base path, see __construct() */ protected $localBasePath = ''; - /** String: Remote base path, see __construct() */ + + /** @var string Remote base path, see __construct() */ protected $remoteBasePath = ''; + /** - * Array: List of paths to JavaScript files to always include + * @var array List of paths to JavaScript files to always include * @par Usage: * @code * array( [file-path], [file-path], ... ) * @endcode */ protected $scripts = array(); + /** - * Array: List of JavaScript files to include when using a specific language + * @var array List of JavaScript files to include when using a specific language * @par Usage: * @code * array( [language-code] => array( [file-path], [file-path], ... ), ... ) * @endcode */ protected $languageScripts = array(); + /** - * Array: List of JavaScript files to include when using a specific skin + * @var array List of JavaScript files to include when using a specific skin * @par Usage: * @code * array( [skin-name] => array( [file-path], [file-path], ... ), ... ) * @endcode */ protected $skinScripts = array(); + /** - * Array: List of paths to JavaScript files to include in debug mode + * @var array List of paths to JavaScript files to include in debug mode * @par Usage: * @code * array( [skin-name] => array( [file-path], [file-path], ... ), ... ) * @endcode */ protected $debugScripts = array(); + /** - * Array: List of paths to JavaScript files to include in the startup module + * @var array List of paths to JavaScript files to include in the startup module * @par Usage: * @code * array( [file-path], [file-path], ... ) * @endcode */ protected $loaderScripts = array(); + /** - * Array: List of paths to CSS files to always include + * @var array List of paths to CSS files to always include * @par Usage: * @code * array( [file-path], [file-path], ... ) * @endcode */ protected $styles = array(); + /** - * Array: List of paths to CSS files to include when using specific skins + * @var array List of paths to CSS files to include when using specific skins * @par Usage: * @code * array( [file-path], [file-path], ... ) * @endcode */ protected $skinStyles = array(); + /** - * Array: List of modules this module depends on + * @var array List of modules this module depends on * @par Usage: * @code * array( [file-path], [file-path], ... ) * @endcode */ protected $dependencies = array(); + + /** + * @var string File name containing the body of the skip function + */ + protected $skipFunction = null; + /** - * Array: List of message keys used by this module + * @var array List of message keys used by this module * @par Usage: * @code * array( [message-key], [message-key], ... ) * @endcode */ protected $messages = array(); - /** String: Name of group to load this module in */ + + /** @var string Name of group to load this module in */ protected $group; - /** String: Position on the page to load this module at */ + + /** @var string Position on the page to load this module at */ protected $position = 'bottom'; - /** Boolean: Link to raw files in debug mode */ + + /** @var bool Link to raw files in debug mode */ protected $debugRaw = true; - /** Boolean: Whether mw.loader.state() call should be omitted */ + + /** @var bool Whether mw.loader.state() call should be omitted */ protected $raw = false; + protected $targets = array( 'desktop' ); /** - * Boolean: Whether getStyleURLsForDebug should return raw file paths, + * @var bool Whether getStyleURLsForDebug should return raw file paths, * or return load.php urls */ protected $hasGeneratedStyles = false; /** - * Array: Cache for mtime + * @var array Cache for mtime * @par Usage: * @code * array( [hash] => [mtime], [hash] => [mtime], ... ) * @endcode */ protected $modifiedTime = array(); + /** - * Array: Place where readStyleFile() tracks file dependencies + * @var array Place where readStyleFile() tracks file dependencies * @par Usage: * @code * array( [file-path], [file-path], ... ) @@ -148,7 +168,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults * to $IP * @param string $remoteBasePath Base path to prepend to all remote paths in $options. Defaults - * to $wgScriptPath + * to $wgResourceBasePath * * Below is a description for the $options array: * @throws MWException @@ -157,10 +177,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * array( * // Base path to prepend to all local paths in $options. Defaults to $IP * 'localBasePath' => [base path], - * // Base path to prepend to all remote paths in $options. Defaults to $wgScriptPath + * // Base path to prepend to all remote paths in $options. Defaults to $wgResourceBasePath * 'remoteBasePath' => [base path], * // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath * 'remoteExtPath' => [base path], + * // Equivalent of remoteBasePath, but relative to $wgStylePath + * 'remoteSkinPath' => [base path], * // Scripts to always include * 'scripts' => [file path string or array of file path strings], * // Scripts to include in specific language contexts @@ -189,25 +211,24 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * 'group' => [group name string], * // Position on the page to load this module at * 'position' => ['bottom' (default) or 'top'] + * // Function that, if it returns true, makes the loader skip this module. + * // The file must contain valid JavaScript for execution in a private function. + * // The file must not contain the "function () {" and "}" wrapper though. + * 'skipFunction' => [file path] * ) * @endcode */ - public function __construct( $options = array(), $localBasePath = null, + public function __construct( + $options = array(), + $localBasePath = null, $remoteBasePath = null ) { - global $IP, $wgScriptPath, $wgResourceBasePath; - $this->localBasePath = $localBasePath === null ? $IP : $localBasePath; - if ( $remoteBasePath !== null ) { - $this->remoteBasePath = $remoteBasePath; - } else { - $this->remoteBasePath = $wgResourceBasePath === null ? $wgScriptPath : $wgResourceBasePath; - } - - if ( isset( $options['remoteExtPath'] ) ) { - global $wgExtensionAssetsPath; - $this->remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath']; - } + // localBasePath and remoteBasePath both have unbelievably long fallback chains + // and need to be handled separately. + list( $this->localBasePath, $this->remoteBasePath ) = + self::extractBasePaths( $options, $localBasePath, $remoteBasePath ); + // Extract, validate and normalise remaining options foreach ( $options as $member => $option ) { switch ( $member ) { // Lists of file paths @@ -241,13 +262,16 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { case 'dependencies': case 'messages': case 'targets': - $this->{$member} = (array)$option; + // Normalise + $option = array_values( array_unique( (array)$option ) ); + sort( $option ); + + $this->{$member} = $option; break; // Single strings case 'group': case 'position': - case 'localBasePath': - case 'remoteBasePath': + case 'skipFunction': $this->{$member} = (string)$option; break; // Single booleans @@ -257,16 +281,64 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { break; } } + } + + /** + * Extract a pair of local and remote base paths from module definition information. + * Implementation note: the amount of global state used in this function is staggering. + * + * @param array $options Module definition + * @param string $localBasePath Path to use if not provided in module definition. Defaults + * to $IP + * @param string $remoteBasePath Path to use if not provided in module definition. Defaults + * to $wgResourceBasePath + * @return array Array( localBasePath, remoteBasePath ) + */ + public static function extractBasePaths( + $options = array(), + $localBasePath = null, + $remoteBasePath = null + ) { + global $IP, $wgResourceBasePath; + + // The different ways these checks are done, and their ordering, look very silly, + // but were preserved for backwards-compatibility just in case. Tread lightly. + + $localBasePath = $localBasePath === null ? $IP : $localBasePath; + if ( $remoteBasePath === null ) { + $remoteBasePath = $wgResourceBasePath; + } + + if ( isset( $options['remoteExtPath'] ) ) { + global $wgExtensionAssetsPath; + $remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath']; + } + + if ( isset( $options['remoteSkinPath'] ) ) { + global $wgStylePath; + $remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath']; + } + + if ( array_key_exists( 'localBasePath', $options ) ) { + $localBasePath = (string)$options['localBasePath']; + } + + if ( array_key_exists( 'remoteBasePath', $options ) ) { + $remoteBasePath = (string)$options['remoteBasePath']; + } + // Make sure the remote base path is a complete valid URL, // but possibly protocol-relative to avoid cache pollution - $this->remoteBasePath = wfExpandUrl( $this->remoteBasePath, PROTO_RELATIVE ); + $remoteBasePath = wfExpandUrl( $remoteBasePath, PROTO_RELATIVE ); + + return array( $localBasePath, $remoteBasePath ); } /** * Gets all scripts for a given context concatenated together. * * @param ResourceLoaderContext $context Context in which to generate script - * @return string: JavaScript code for $context + * @return string JavaScript code for $context */ public function getScript( ResourceLoaderContext $context ) { $files = $this->getScriptFiles( $context ); @@ -293,27 +365,28 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { } /** - * Gets loader script. + * Get loader script. * - * @return string: JavaScript code to be added to startup module + * @return string|bool JavaScript code to be added to startup module */ public function getLoaderScript() { - if ( count( $this->loaderScripts ) == 0 ) { + if ( count( $this->loaderScripts ) === 0 ) { return false; } return $this->readScriptFiles( $this->loaderScripts ); } /** - * Gets all styles for a given context concatenated together. + * Get all styles for a given context. * - * @param ResourceLoaderContext $context Context in which to generate styles - * @return string: CSS code for $context + * @param ResourceLoaderContext $context + * @return array CSS code for $context as an associative array mapping media type to CSS text. */ public function getStyles( ResourceLoaderContext $context ) { $styles = $this->readStyleFiles( $this->getStyleFiles( $context ), - $this->getFlip( $context ) + $this->getFlip( $context ), + $context ); // Collect referenced files $this->localFileRefs = array_unique( $this->localFileRefs ); @@ -360,7 +433,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Gets list of message keys used by this module. * - * @return array: List of message keys + * @return array List of message keys */ public function getMessages() { return $this->messages; @@ -369,7 +442,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Gets the name of the group this module should be loaded in. * - * @return string: Group name + * @return string Group name */ public function getGroup() { return $this->group; @@ -385,12 +458,33 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Gets list of names of modules this module depends on. * - * @return array: List of module names + * @return array List of module names */ public function getDependencies() { return $this->dependencies; } + /** + * Get the skip function. + * + * @return string|null + */ + public function getSkipFunction() { + if ( !$this->skipFunction ) { + return null; + } + + $localPath = $this->getLocalPath( $this->skipFunction ); + if ( !file_exists( $localPath ) ) { + throw new MWException( __METHOD__ . ": skip function file not found: \"$localPath\"" ); + } + $contents = file_get_contents( $localPath ); + if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) { + $contents = $this->validateScriptFile( $localPath, $contents ); + } + return $contents; + } + /** * @return bool */ @@ -409,7 +503,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * * @param ResourceLoaderContext $context Context in which to calculate * the modified time - * @return int: UNIX timestamp + * @return int UNIX timestamp * @see ResourceLoaderModule::getFileDependencies */ public function getModifiedTime( ResourceLoaderContext $context ) { @@ -425,10 +519,11 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { foreach ( $styles as $styleFiles ) { $files = array_merge( $files, $styleFiles ); } - $skinFiles = self::tryForKey( - self::collateFilePathListByOption( $this->skinStyles, 'media', 'all' ), - $context->getSkin(), - 'default' + + $skinFiles = self::collateFilePathListByOption( + self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), + 'media', + 'all' ); foreach ( $skinFiles as $styleFiles ) { $files = array_merge( $files, $styleFiles ); @@ -443,6 +538,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ), $this->loaderScripts ); + if ( $this->skipFunction ) { + $files[] = $this->skipFunction; + } $files = array_map( array( $this, 'getLocalPath' ), $files ); // File deps need to be treated separately because they're already prefixed $files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) ); @@ -450,36 +548,82 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { // If a module is nothing but a list of dependencies, we need to avoid // giving max() an empty array if ( count( $files ) === 0 ) { + $this->modifiedTime[$context->getHash()] = 1; wfProfileOut( __METHOD__ ); - return $this->modifiedTime[$context->getHash()] = 1; + return $this->modifiedTime[$context->getHash()]; } wfProfileIn( __METHOD__ . '-filemtime' ); $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) ); wfProfileOut( __METHOD__ . '-filemtime' ); + $this->modifiedTime[$context->getHash()] = max( $filesMtime, - $this->getMsgBlobMtime( $context->getLanguage() ) ); + $this->getMsgBlobMtime( $context->getLanguage() ), + $this->getDefinitionMtime( $context ) + ); wfProfileOut( __METHOD__ ); return $this->modifiedTime[$context->getHash()]; } + /** + * Get the definition summary for this module. + * + * @param ResourceLoaderContext $context + * @return array + */ + public function getDefinitionSummary( ResourceLoaderContext $context ) { + $summary = array( + 'class' => get_class( $this ), + ); + foreach ( array( + 'scripts', + 'debugScripts', + 'loaderScripts', + 'styles', + 'languageScripts', + 'skinScripts', + 'skinStyles', + 'dependencies', + 'messages', + 'targets', + 'group', + 'position', + 'skipFunction', + 'localBasePath', + 'remoteBasePath', + 'debugRaw', + 'raw', + ) as $member ) { + $summary[$member] = $this->{$member}; + }; + return $summary; + } + /* Protected Methods */ /** - * @param string $path + * @param string|ResourceLoaderFilePath $path * @return string */ protected function getLocalPath( $path ) { + if ( $path instanceof ResourceLoaderFilePath ) { + return $path->getLocalPath(); + } + return "{$this->localBasePath}/$path"; } /** - * @param string $path + * @param string|ResourceLoaderFilePath $path * @return string */ protected function getRemotePath( $path ) { + if ( $path instanceof ResourceLoaderFilePath ) { + return $path->getRemotePath(); + } + return "{$this->remoteBasePath}/$path"; } @@ -488,7 +632,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * * @since 1.22 * @param string $path - * @return string: the stylesheet language name + * @return string The stylesheet language name */ public function getStyleSheetLang( $path ) { return preg_match( '/\.less$/i', $path ) ? 'less' : 'css'; @@ -499,9 +643,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * * @param array $list List of file paths in any combination of index/path * or path/options pairs - * @param string $option option name - * @param mixed $default default value if the option isn't set - * @return array: List of file paths, collated by $option + * @param string $option Option name + * @param mixed $default Default value if the option isn't set + * @return array List of file paths, collated by $option */ protected static function collateFilePathListByOption( array $list, $option, $default ) { $collatedFiles = array(); @@ -525,31 +669,31 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { } /** - * Gets a list of element that match a key, optionally using a fallback key. + * Get a list of element that match a key, optionally using a fallback key. * * @param array $list List of lists to select from * @param string $key Key to look for in $map * @param string $fallback Key to look for in $list if $key doesn't exist - * @return array: List of elements from $map which matched $key or $fallback, - * or an empty list in case of no match + * @return array List of elements from $map which matched $key or $fallback, + * or an empty list in case of no match */ protected static function tryForKey( array $list, $key, $fallback = null ) { if ( isset( $list[$key] ) && is_array( $list[$key] ) ) { return $list[$key]; } elseif ( is_string( $fallback ) && isset( $list[$fallback] ) - && is_array( $list[$fallback] ) ) - { + && is_array( $list[$fallback] ) + ) { return $list[$fallback]; } return array(); } /** - * Gets a list of file paths for all scripts in this module, in order of propper execution. + * Get a list of file paths for all scripts in this module, in order of proper execution. * * @param ResourceLoaderContext $context - * @return array: List of file paths + * @return array List of file paths */ protected function getScriptFiles( ResourceLoaderContext $context ) { $files = array_merge( @@ -561,39 +705,82 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $files = array_merge( $files, $this->debugScripts ); } - return array_unique( $files ); + return array_unique( $files, SORT_REGULAR ); } /** - * Gets a list of file paths for all styles in this module, in order of propper inclusion. + * Get a list of file paths for all styles in this module, in order of proper inclusion. * * @param ResourceLoaderContext $context - * @return array: List of file paths + * @return array List of file paths */ - protected function getStyleFiles( ResourceLoaderContext $context ) { + public function getStyleFiles( ResourceLoaderContext $context ) { return array_merge_recursive( self::collateFilePathListByOption( $this->styles, 'media', 'all' ), self::collateFilePathListByOption( - self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 'media', 'all' + self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), + 'media', + 'all' ) ); } /** - * Returns all style files used by this module + * Gets a list of file paths for all skin styles in the module used by + * the skin. + * + * @param string $skinName The name of the skin + * @return array A list of file paths collated by media type + */ + protected function getSkinStyleFiles( $skinName ) { + return self::collateFilePathListByOption( + self::tryForKey( $this->skinStyles, $skinName ), + 'media', + 'all' + ); + } + + /** + * Gets a list of file paths for all skin style files in the module, + * for all available skins. + * + * @return array A list of file paths collated by media type + */ + protected function getAllSkinStyleFiles() { + $styleFiles = array(); + $internalSkinNames = array_keys( Skin::getSkinNames() ); + $internalSkinNames[] = 'default'; + + foreach ( $internalSkinNames as $internalSkinName ) { + $styleFiles = array_merge_recursive( + $styleFiles, + $this->getSkinStyleFiles( $internalSkinName ) + ); + } + + return $styleFiles; + } + + /** + * Returns all style files and all skin style files used by this module. + * * @return array */ public function getAllStyleFiles() { - $files = array(); - foreach( (array)$this->styles as $key => $value ) { - if ( is_array( $value ) ) { - $path = $key; - } else { - $path = $value; + $collatedStyleFiles = array_merge_recursive( + self::collateFilePathListByOption( $this->styles, 'media', 'all' ), + $this->getAllSkinStyleFiles() + ); + + $result = array(); + + foreach ( $collatedStyleFiles as $media => $styleFiles ) { + foreach ( $styleFiles as $styleFile ) { + $result[] = $this->getLocalPath( $styleFile ); } - $files[] = $this->getLocalPath( $path ); } - return $files; + + return $result; } /** @@ -601,21 +788,20 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * * @param array $scripts List of file paths to scripts to read, remap and concetenate * @throws MWException - * @return string: Concatenated and remapped JavaScript data from $scripts + * @return string Concatenated and remapped JavaScript data from $scripts */ protected function readScriptFiles( array $scripts ) { - global $wgResourceLoaderValidateStaticJS; if ( empty( $scripts ) ) { return ''; } $js = ''; - foreach ( array_unique( $scripts ) as $fileName ) { + foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) { $localPath = $this->getLocalPath( $fileName ); if ( !file_exists( $localPath ) ) { throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" ); } $contents = file_get_contents( $localPath ); - if ( $wgResourceLoaderValidateStaticJS ) { + if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) { // Static files don't really need to be checked as often; unlike // on-wiki module they shouldn't change unexpectedly without // admin interference. @@ -631,26 +817,24 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * * @param array $styles List of media type/list of file paths pairs, to read, remap and * concetenate - * * @param bool $flip + * @param ResourceLoaderContext $context (optional) * - * @return array: List of concatenated and remapped CSS data from $styles, + * @throws MWException + * @return array List of concatenated and remapped CSS data from $styles, * keyed by media type */ - protected function readStyleFiles( array $styles, $flip ) { + public function readStyleFiles( array $styles, $flip, $context = null ) { if ( empty( $styles ) ) { return array(); } foreach ( $styles as $media => $files ) { - $uniqueFiles = array_unique( $files ); - $styles[$media] = implode( - "\n", - array_map( - array( $this, 'readStyleFile' ), - $uniqueFiles, - array_fill( 0, count( $uniqueFiles ), $flip ) - ) - ); + $uniqueFiles = array_unique( $files, SORT_REGULAR ); + $styleFiles = array(); + foreach ( $uniqueFiles as $file ) { + $styleFiles[] = $this->readStyleFile( $file, $flip, $context ); + } + $styles[$media] = implode( "\n", $styleFiles ); } return $styles; } @@ -662,20 +846,23 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * * @param string $path File path of style file to read * @param bool $flip + * @param ResourceLoaderContext $context (optional) * - * @return string: CSS data in script file - * @throws MWException if the file doesn't exist + * @return string CSS data in script file + * @throws MWException If the file doesn't exist */ - protected function readStyleFile( $path, $flip ) { + protected function readStyleFile( $path, $flip, $context = null ) { $localPath = $this->getLocalPath( $path ); + $remotePath = $this->getRemotePath( $path ); if ( !file_exists( $localPath ) ) { $msg = __METHOD__ . ": style file not found: \"$localPath\""; wfDebugLog( 'resourceloader', $msg ); throw new MWException( $msg ); } - if ( $this->getStyleSheetLang( $path ) === 'less' ) { - $style = $this->compileLESSFile( $localPath ); + if ( $this->getStyleSheetLang( $localPath ) === 'less' ) { + $compiler = $this->getLessCompiler( $context ); + $style = $this->compileLessFile( $localPath, $compiler ); $this->hasGeneratedStyles = true; } else { $style = file_get_contents( $localPath ); @@ -684,20 +871,15 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { if ( $flip ) { $style = CSSJanus::transform( $style, true, false ); } - $dirname = dirname( $path ); - if ( $dirname == '.' ) { - // If $path doesn't have a directory component, don't prepend a dot - $dirname = ''; - } - $dir = $this->getLocalPath( $dirname ); - $remoteDir = $this->getRemotePath( $dirname ); + $localDir = dirname( $localPath ); + $remoteDir = dirname( $remotePath ); // Get and register local file references $this->localFileRefs = array_merge( $this->localFileRefs, - CSSMin::getLocalFileReferences( $style, $dir ) + CSSMin::getLocalFileReferences( $style, $localDir ) ); return CSSMin::remap( - $style, $dir, $remoteDir, true + $style, $localDir, $remoteDir, true ); } @@ -713,64 +895,43 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile'] * - * @return array of strings + * @return array Array of strings */ public function getTargets() { return $this->targets; } /** - * Generate a cache key for a LESS file. + * Compile a LESS file into CSS. * - * The cache key varies on the file name and the names and values of global - * LESS variables. + * Keeps track of all used files and adds them to localFileRefs. * * @since 1.22 - * @param string $fileName File name of root LESS file. - * @return string: Cache key + * @throws Exception If lessc encounters a parse error + * @param string $fileName File path of LESS source + * @param lessc $compiler Compiler to use, if not default + * @return string CSS source */ - protected static function getLESSCacheKey( $fileName ) { - $vars = json_encode( ResourceLoader::getLESSVars() ); - $hash = md5( $fileName . $vars ); - return wfMemcKey( 'resourceloader', 'less', $hash ); + protected function compileLessFile( $fileName, $compiler = null ) { + if ( !$compiler ) { + $compiler = $this->getLessCompiler(); + } + $result = $compiler->compileFile( $fileName ); + $this->localFileRefs += array_keys( $compiler->allParsedFiles() ); + return $result; } /** - * Compile a LESS file into CSS. + * Get a LESS compiler instance for this module in given context. * - * If invalid, returns replacement CSS source consisting of the compilation - * error message encoded as a comment. To save work, we cache a result object - * which comprises the compiled CSS and the names & mtimes of the files - * that were processed. lessphp compares the cached & current mtimes and - * recompiles as necessary. + * Just calls ResourceLoader::getLessCompiler() by default to get a global compiler. * - * @since 1.22 - * @param string $fileName File path of LESS source - * @return string: CSS source + * @param ResourceLoaderContext $context + * @throws MWException + * @since 1.24 + * @return lessc */ - protected function compileLESSFile( $fileName ) { - $key = self::getLESSCacheKey( $fileName ); - $cache = wfGetCache( CACHE_ANYTHING ); - - // The input to lessc. Either an associative array representing the - // cached results of a previous compilation, or the string file name if - // no cache result exists. - $source = $cache->get( $key ); - if ( !is_array( $source ) || !isset( $source['root'] ) ) { - $source = $fileName; - } - - $compiler = ResourceLoader::getLessCompiler(); - $result = null; - - $result = $compiler->cachedCompile( $source ); - - if ( !is_array( $result ) ) { - throw new MWException( 'LESS compiler result has type ' . gettype( $result ) . '; array expected.' ); - } - - $this->localFileRefs += array_keys( $result['files'] ); - $cache->set( $key, $result ); - return $result['compiled']; + protected function getLessCompiler( ResourceLoaderContext $context = null ) { + return ResourceLoader::getLessCompiler( $this->getConfig() ); } } diff --git a/includes/resourceloader/ResourceLoaderFilePageModule.php b/includes/resourceloader/ResourceLoaderFilePageModule.php index 61ed5206..8c7fbe76 100644 --- a/includes/resourceloader/ResourceLoaderFilePageModule.php +++ b/includes/resourceloader/ResourceLoaderFilePageModule.php @@ -26,7 +26,7 @@ class ResourceLoaderFilePageModule extends ResourceLoaderWikiModule { /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return array */ protected function getPages( ResourceLoaderContext $context ) { diff --git a/includes/resourceloader/ResourceLoaderFilePath.php b/includes/resourceloader/ResourceLoaderFilePath.php new file mode 100644 index 00000000..dd239d09 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderFilePath.php @@ -0,0 +1,74 @@ +path = $path; + $this->localBasePath = $localBasePath; + $this->remoteBasePath = $remoteBasePath; + } + + /** + * @return string + */ + public function getLocalPath() { + return "{$this->localBasePath}/{$this->path}"; + } + + /** + * @return string + */ + public function getRemotePath() { + return "{$this->remoteBasePath}/{$this->path}"; + } + + /** + * @return string + */ + public function getPath() { + return $this->path; + } +} diff --git a/includes/resourceloader/ResourceLoaderLESSFunctions.php b/includes/resourceloader/ResourceLoaderLESSFunctions.php deleted file mode 100644 index c7570f64..00000000 --- a/includes/resourceloader/ResourceLoaderLESSFunctions.php +++ /dev/null @@ -1,67 +0,0 @@ -parser->sourceName, PATHINFO_DIRNAME ); - $url = $frame[2][0]; - $file = realpath( $base . '/' . $url ); - return $less->toBool( $file - && strpos( $url, '//' ) === false - && filesize( $file ) < CSSMin::EMBED_SIZE_LIMIT - && CSSMin::getMimeType( $file ) !== false ); - } - - /** - * Convert an image URI to a base64-encoded data URI. - * - * @par Example: - * @code - * .fancy-button { - * background-image: embed('../images/button-bg.png'); - * } - * @endcode - */ - public static function embed( $frame, $less ) { - $base = pathinfo( $less->parser->sourceName, PATHINFO_DIRNAME ); - $url = $frame[2][0]; - $file = realpath( $base . '/' . $url ); - - $data = CSSMin::encodeImageAsDataURI( $file ); - $less->addParsedFile( $file ); - return 'url(' . $data . ')'; - } -} diff --git a/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/includes/resourceloader/ResourceLoaderLanguageDataModule.php index fa0fbf85..09d90d6e 100644 --- a/includes/resourceloader/ResourceLoaderLanguageDataModule.php +++ b/includes/resourceloader/ResourceLoaderLanguageDataModule.php @@ -27,99 +27,51 @@ */ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule { - protected $language; protected $targets = array( 'desktop', 'mobile' ); - /** - * Get the grammar forms for the site content language. - * - * @return array - */ - protected function getSiteLangGrammarForms() { - return $this->language->getGrammarForms(); - } - - /** - * Get the plural forms for the site content language. - * - * @return array - */ - protected function getPluralRules() { - return $this->language->getPluralRules(); - } - - /** - * Get the digit groupin Pattern for the site content language. - * - * @return array - */ - protected function getDigitGroupingPattern() { - return $this->language->digitGroupingPattern(); - } - - /** - * Get the digit transform table for the content language - * - * @return array - */ - protected function getDigitTransformTable() { - return $this->language->digitTransformTable(); - } - - /** - * Get seperator transform table required for converting - * the . and , sign to appropriate forms in site content language. - * - * @return array - */ - protected function getSeparatorTransformTable() { - return $this->language->separatorTransformTable(); - } /** * Get all the dynamic data for the content language to an array. * - * NOTE: Before calling this you HAVE to make sure $this->language is set. - * + * @param ResourceLoaderContext $context * @return array */ - protected function getData() { + protected function getData( ResourceLoaderContext $context ) { + $language = Language::factory( $context->getLanguage() ); return array( - 'digitTransformTable' => $this->getDigitTransformTable(), - 'separatorTransformTable' => $this->getSeparatorTransformTable(), - 'grammarForms' => $this->getSiteLangGrammarForms(), - 'pluralRules' => $this->getPluralRules(), - 'digitGroupingPattern' => $this->getDigitGroupingPattern(), + 'digitTransformTable' => $language->digitTransformTable(), + 'separatorTransformTable' => $language->separatorTransformTable(), + 'grammarForms' => $language->getGrammarForms(), + 'pluralRules' => $language->getPluralRules(), + 'digitGroupingPattern' => $language->digitGroupingPattern(), + 'fallbackLanguages' => $language->getFallbackLanguages(), ); } /** - * @param $context ResourceLoaderContext - * @return string: JavaScript code + * @param ResourceLoaderContext $context + * @return string JavaScript code */ public function getScript( ResourceLoaderContext $context ) { - $this->language = Language::factory( $context->getLanguage() ); return Xml::encodeJsCall( 'mw.language.setData', array( - $this->language->getCode(), - $this->getData() + $context->getLanguage(), + $this->getData( $context ) ) ); } /** - * @param $context ResourceLoaderContext - * @return int: UNIX timestamp + * @param ResourceLoaderContext $context + * @return int UNIX timestamp */ public function getModifiedTime( ResourceLoaderContext $context ) { return max( 1, $this->getHashMtime( $context ) ); } /** - * @param $context ResourceLoaderContext - * @return string: Hash + * @param ResourceLoaderContext $context + * @return string Hash */ public function getModifiedHash( ResourceLoaderContext $context ) { - $this->language = Language::factory( $context->getLanguage() ); - - return md5( serialize( $this->getData() ) ); + return md5( serialize( $this->getData( $context ) ) ); } /** diff --git a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php new file mode 100644 index 00000000..fe0c8454 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php @@ -0,0 +1,79 @@ +getLanguage(), + 'all' + ); + } + + /** + * @param ResourceLoaderContext $context + * @return string JavaScript code + */ + public function getScript( ResourceLoaderContext $context ) { + return Xml::encodeJsCall( 'mw.language.setData', array( + $context->getLanguage(), + 'languageNames', + $this->getData( $context ) + ) ); + } + + public function getDependencies() { + return array( 'mediawiki.language.init' ); + } + + /** + * @param ResourceLoaderContext $context + * @return int UNIX timestamp + */ + public function getModifiedTime( ResourceLoaderContext $context ) { + return max( 1, $this->getHashMtime( $context ) ); + } + + /** + * @param ResourceLoaderContext $context + * @return string Hash + */ + public function getModifiedHash( ResourceLoaderContext $context ) { + return md5( serialize( $this->getData( $context ) ) ); + } + +} diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index 11264fc8..45eb70f8 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -26,7 +26,6 @@ * Abstraction for resource loader modules, with name registration and maxage functionality. */ abstract class ResourceLoaderModule { - # Type of resource const TYPE_SCRIPTS = 'scripts'; const TYPE_STYLES = 'styles'; @@ -65,13 +64,18 @@ abstract class ResourceLoaderModule { // In-object cache for message blob mtime protected $msgBlobMtime = array(); + /** + * @var Config + */ + protected $config; + /* Methods */ /** * Get this module's name. This is set when the module is registered * with ResourceLoader::register() * - * @return mixed: Name (string) or null if no name was set + * @return string|null Name (string) or null if no name was set */ public function getName() { return $this->name; @@ -91,7 +95,7 @@ abstract class ResourceLoaderModule { * Get this module's origin. This is set when the module is registered * with ResourceLoader::register() * - * @return int: ResourceLoaderModule class constant, the subclass default + * @return int ResourceLoaderModule class constant, the subclass default * if not set manually */ public function getOrigin() { @@ -102,7 +106,7 @@ abstract class ResourceLoaderModule { * Set this module's origin. This is called by ResourceLoader::register() * when registering the module. Other code should not call this. * - * @param int $origin origin + * @param int $origin Origin */ public function setOrigin( $origin ) { $this->origin = $origin; @@ -123,13 +127,34 @@ abstract class ResourceLoaderModule { * Includes all relevant JS except loader scripts. * * @param ResourceLoaderContext $context - * @return string: JavaScript code + * @return string JavaScript code */ public function getScript( ResourceLoaderContext $context ) { // Stub, override expected return ''; } + /** + * @return Config + * @since 1.24 + */ + public function getConfig() { + if ( $this->config === null ) { + // Ugh, fall back to default + $this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); + } + + return $this->config; + } + + /** + * @param Config $config + * @since 1.24 + */ + public function setConfig( Config $config ) { + $this->config = $config; + } + /** * Get the URL or URLs to load for this module's JS in debug mode. * The default behavior is to return a load.php?only=scripts URL for @@ -142,20 +167,20 @@ abstract class ResourceLoaderModule { * MUST return either an only= URL or a non-load.php URL. * * @param ResourceLoaderContext $context - * @return array: Array of URLs + * @return array Array of URLs */ public function getScriptURLsForDebug( ResourceLoaderContext $context ) { - $url = ResourceLoader::makeLoaderURL( - array( $this->getName() ), - $context->getLanguage(), - $context->getSkin(), - $context->getUser(), - $context->getVersion(), - true, // debug - 'scripts', // only - $context->getRequest()->getBool( 'printable' ), - $context->getRequest()->getBool( 'handheld' ) + $resourceLoader = $context->getResourceLoader(); + $derivative = new DerivativeResourceLoaderContext( $context ); + $derivative->setModules( array( $this->getName() ) ); + $derivative->setOnly( 'scripts' ); + $derivative->setDebug( true ); + + $url = $resourceLoader->createLoaderURL( + $this->getSource(), + $derivative ); + return array( $url ); } @@ -173,7 +198,7 @@ abstract class ResourceLoaderModule { * Get all CSS for this module for a given skin. * * @param ResourceLoaderContext $context - * @return array: List of CSS strings or array of CSS strings keyed by media type. + * @return array List of CSS strings or array of CSS strings keyed by media type. * like array( 'screen' => '.foo { width: 0 }' ); * or array( 'screen' => array( '.foo { width: 0 }' ) ); */ @@ -189,20 +214,20 @@ abstract class ResourceLoaderModule { * load the files directly. See also getScriptURLsForDebug() * * @param ResourceLoaderContext $context - * @return array: array( mediaType => array( URL1, URL2, ... ), ... ) + * @return array Array( mediaType => array( URL1, URL2, ... ), ... ) */ public function getStyleURLsForDebug( ResourceLoaderContext $context ) { - $url = ResourceLoader::makeLoaderURL( - array( $this->getName() ), - $context->getLanguage(), - $context->getSkin(), - $context->getUser(), - $context->getVersion(), - true, // debug - 'styles', // only - $context->getRequest()->getBool( 'printable' ), - $context->getRequest()->getBool( 'handheld' ) + $resourceLoader = $context->getResourceLoader(); + $derivative = new DerivativeResourceLoaderContext( $context ); + $derivative->setModules( array( $this->getName() ) ); + $derivative->setOnly( 'styles' ); + $derivative->setDebug( true ); + + $url = $resourceLoader->createLoaderURL( + $this->getSource(), + $derivative ); + return array( 'all' => array( $url ) ); } @@ -211,7 +236,7 @@ abstract class ResourceLoaderModule { * * To get a JSON blob with messages, use MessageBlobStore::get() * - * @return array: List of message keys. Keys may occur more than once + * @return array List of message keys. Keys may occur more than once */ public function getMessages() { // Stub, override expected @@ -221,7 +246,7 @@ abstract class ResourceLoaderModule { /** * Get the group this module is in. * - * @return string: Group name + * @return string Group name */ public function getGroup() { // Stub, override expected @@ -231,7 +256,7 @@ abstract class ResourceLoaderModule { /** * Get the origin of this module. Should only be overridden for foreign modules. * - * @return string: Origin name, 'local' for local modules + * @return string Origin name, 'local' for local modules */ public function getSource() { // Stub, override expected @@ -263,7 +288,7 @@ abstract class ResourceLoaderModule { /** * Get the loader JS for this module, if set. * - * @return mixed: JavaScript loader code as a string or boolean false if no custom loader set + * @return mixed JavaScript loader code as a string or boolean false if no custom loader set */ public function getLoaderScript() { // Stub, override expected @@ -278,7 +303,7 @@ abstract class ResourceLoaderModule { * * To add dependencies dynamically on the client side, use a custom * loader script, see getLoaderScript() - * @return array: List of module names as strings + * @return array List of module names as strings */ public function getDependencies() { // Stub, override expected @@ -288,18 +313,36 @@ abstract class ResourceLoaderModule { /** * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile'] * - * @return array: Array of strings + * @return array Array of strings */ public function getTargets() { return $this->targets; } + /** + * Get the skip function. + * + * Modules that provide fallback functionality can provide a "skip function". This + * function, if provided, will be passed along to the module registry on the client. + * When this module is loaded (either directly or as a dependency of another module), + * then this function is executed first. If the function returns true, the module will + * instantly be considered "ready" without requesting the associated module resources. + * + * The value returned here must be valid javascript for execution in a private function. + * It must not contain the "function () {" and "}" wrapper though. + * + * @return string|null A JavaScript function body returning a boolean value, or null + */ + public function getSkipFunction() { + return null; + } + /** * Get the files this module depends on indirectly for a given skin. * Currently these are only image files referenced by the module's CSS. * * @param string $skin Skin name - * @return array: List of files + * @return array List of files */ public function getFileDependencies( $skin ) { // Try in-object cache first @@ -335,7 +378,7 @@ abstract class ResourceLoaderModule { * Get the last modification timestamp of the message blob for this * module in a given language. * @param string $lang Language code - * @return int: UNIX timestamp, or 0 if the module doesn't have messages + * @return int UNIX timestamp, or 0 if the module doesn't have messages */ public function getMsgBlobMtime( $lang ) { if ( !isset( $this->msgBlobMtime[$lang] ) ) { @@ -363,7 +406,7 @@ abstract class ResourceLoaderModule { * Set a preloaded message blob last modification timestamp. Used so we * can load this information for all modules at once. * @param string $lang Language code - * @param $mtime Integer: UNIX timestamp or 0 if there is no such blob + * @param int $mtime UNIX timestamp or 0 if there is no such blob */ public function setMsgBlobMtime( $lang, $mtime ) { $this->msgBlobMtime[$lang] = $mtime; @@ -387,7 +430,7 @@ abstract class ResourceLoaderModule { * yourself and take its result into consideration. * * @param ResourceLoaderContext $context Context object - * @return integer UNIX timestamp + * @return int UNIX timestamp */ public function getModifiedTime( ResourceLoaderContext $context ) { // 0 would mean now @@ -398,7 +441,8 @@ abstract class ResourceLoaderModule { * Helper method for calculating when the module's hash (if it has one) changed. * * @param ResourceLoaderContext $context - * @return integer: UNIX timestamp or 0 if there is no hash provided + * @return int UNIX timestamp or 0 if no hash was provided + * by getModifiedHash() */ public function getHashMtime( ResourceLoaderContext $context ) { $hash = $this->getModifiedHash( $context ); @@ -407,7 +451,7 @@ abstract class ResourceLoaderModule { } $cache = wfGetCache( CACHE_ANYTHING ); - $key = wfMemcKey( 'resourceloader', 'modulemodifiedhash', $this->getName() ); + $key = wfMemcKey( 'resourceloader', 'modulemodifiedhash', $this->getName(), $hash ); $data = $cache->get( $key ); if ( is_array( $data ) && $data['hash'] === $hash ) { @@ -425,16 +469,100 @@ abstract class ResourceLoaderModule { } /** - * Get the last modification timestamp of the message blob for this - * module in a given language. + * Get the hash for whatever this module may contain. + * + * This is the method subclasses should implement if they want to make + * use of getHashMTime() inside getModifiedTime(). * * @param ResourceLoaderContext $context - * @return string|null: Hash + * @return string|null Hash */ public function getModifiedHash( ResourceLoaderContext $context ) { return null; } + /** + * Helper method for calculating when this module's definition summary was last changed. + * + * @since 1.23 + * + * @param ResourceLoaderContext $context + * @return int UNIX timestamp or 0 if no definition summary was provided + * by getDefinitionSummary() + */ + public function getDefinitionMtime( ResourceLoaderContext $context ) { + wfProfileIn( __METHOD__ ); + $summary = $this->getDefinitionSummary( $context ); + if ( $summary === null ) { + wfProfileOut( __METHOD__ ); + return 0; + } + + $hash = md5( json_encode( $summary ) ); + + $cache = wfGetCache( CACHE_ANYTHING ); + + // Embed the hash itself in the cache key. This allows for a few nifty things: + // - During deployment, servers with old and new versions of the code communicating + // with the same memcached will not override the same key repeatedly increasing + // the timestamp. + // - In case of the definition changing and then changing back in a short period of time + // (e.g. in case of a revert or a corrupt server) the old timestamp and client-side cache + // url will be re-used. + // - If different context-combinations (e.g. same skin, same language or some combination + // thereof) result in the same definition, they will use the same hash and timestamp. + $key = wfMemcKey( 'resourceloader', 'moduledefinition', $this->getName(), $hash ); + + $data = $cache->get( $key ); + if ( is_int( $data ) && $data > 0 ) { + // We've seen this hash before, re-use the timestamp of when we first saw it. + wfProfileOut( __METHOD__ ); + return $data; + } + + wfDebugLog( 'resourceloader', __METHOD__ . ": New definition hash for module " + . "{$this->getName()} in context {$context->getHash()}: $hash." ); + + $timestamp = time(); + $cache->set( $key, $timestamp ); + + wfProfileOut( __METHOD__ ); + return $timestamp; + } + + /** + * Get the definition summary for this module. + * + * This is the method subclasses should implement if they want to make + * use of getDefinitionMTime() inside getModifiedTime(). + * + * Return an array containing values from all significant properties of this + * module's definition. Be sure to include things that are explicitly ordered, + * in their actaul order (bug 37812). + * + * Avoid including things that are insiginificant (e.g. order of message + * keys is insignificant and should be sorted to avoid unnecessary cache + * invalidation). + * + * Avoid including things already considered by other methods inside your + * getModifiedTime(), such as file mtime timestamps. + * + * Serialisation is done using json_encode, which means object state is not + * taken into account when building the hash. This data structure must only + * contain arrays and scalars as values (avoid object instances) which means + * it requires abstraction. + * + * @since 1.23 + * + * @param ResourceLoaderContext $context + * @return array|null + */ + public function getDefinitionSummary( ResourceLoaderContext $context ) { + return array( + 'class' => get_class( $this ), + ); + } + /** * Check whether this module is known to be empty. If a child class * has an easy and cheap way to determine that this module is @@ -448,7 +576,7 @@ abstract class ResourceLoaderModule { return false; } - /** @var JSParser lazy-initialized; use self::javaScriptParser() */ + /** @var JSParser Lazy-initialized; use self::javaScriptParser() */ private static $jsParser; private static $parseCacheVersion = 1; @@ -458,11 +586,10 @@ abstract class ResourceLoaderModule { * * @param string $fileName * @param string $contents - * @return string: JS with the original, or a replacement error + * @return string JS with the original, or a replacement error */ protected function validateScriptFile( $fileName, $contents ) { - global $wgResourceLoaderValidateJS; - if ( $wgResourceLoaderValidateJS ) { + if ( $this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) { // Try for cache hit // Use CACHE_ANYTHING since filtering is very slow compared to DB queries $key = wfMemcKey( 'resourceloader', 'jsparse', self::$parseCacheVersion, md5( $contents ) ); diff --git a/includes/resourceloader/ResourceLoaderNoscriptModule.php b/includes/resourceloader/ResourceLoaderNoscriptModule.php index bd026f3f..61927d77 100644 --- a/includes/resourceloader/ResourceLoaderNoscriptModule.php +++ b/includes/resourceloader/ResourceLoaderNoscriptModule.php @@ -33,9 +33,9 @@ class ResourceLoaderNoscriptModule extends ResourceLoaderWikiModule { * Gets list of pages used by this module. Obviously, it makes absolutely no * sense to include JavaScript files here... :D * - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * - * @return Array: List of pages + * @return array List of pages */ protected function getPages( ResourceLoaderContext $context ) { return array( 'MediaWiki:Noscript.css' => array( 'type' => 'style' ) ); @@ -46,7 +46,7 @@ class ResourceLoaderNoscriptModule extends ResourceLoaderWikiModule { /** * Gets group name * - * @return String: Name of group + * @return string Name of group */ public function getGroup() { return 'noscript'; diff --git a/includes/resourceloader/ResourceLoaderSiteModule.php b/includes/resourceloader/ResourceLoaderSiteModule.php index 05754d37..1d9721aa 100644 --- a/includes/resourceloader/ResourceLoaderSiteModule.php +++ b/includes/resourceloader/ResourceLoaderSiteModule.php @@ -32,19 +32,17 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule { /** * Gets list of pages used by this module * - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * - * @return Array: List of pages + * @return array List of pages */ protected function getPages( ResourceLoaderContext $context ) { - global $wgUseSiteJs, $wgUseSiteCss; - $pages = array(); - if ( $wgUseSiteJs ) { + if ( $this->getConfig()->get( 'UseSiteJs' ) ) { $pages['MediaWiki:Common.js'] = array( 'type' => 'script' ); $pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.js'] = array( 'type' => 'script' ); } - if ( $wgUseSiteCss ) { + if ( $this->getConfig()->get( 'UseSiteCss' ) ) { $pages['MediaWiki:Common.css'] = array( 'type' => 'style' ); $pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.css'] = array( 'type' => 'style' ); @@ -58,7 +56,7 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule { /** * Gets group name * - * @return String: Name of group + * @return string Name of group */ public function getGroup() { return 'site'; diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index 20f6e0ba..78fe8e01 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -27,21 +27,23 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { /* Protected Members */ protected $modifiedTime = array(); + protected $configVars = array(); protected $targets = array( 'desktop', 'mobile' ); /* Protected Methods */ /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return array */ - protected function getConfig( $context ) { - global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension, - $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, - $wgVariantArticlePath, $wgActionPaths, $wgVersion, - $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, - $wgSitename, $wgFileExtensions, $wgExtensionAssetsPath, - $wgCookiePrefix, $wgResourceLoaderMaxQueryLength; + protected function getConfigSettings( $context ) { + + $hash = $context->getHash(); + if ( isset( $this->configVars[$hash] ) ) { + return $this->configVars[$hash]; + } + + global $wgContLang; $mainPage = Title::newMainPage(); @@ -59,113 +61,262 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { } } + $conf = $this->getConfig(); // Build list of variables $vars = array( - 'wgLoadScript' => $wgLoadScript, + 'wgLoadScript' => wfScript( 'load' ), 'debug' => $context->getDebug(), 'skin' => $context->getSkin(), - 'stylepath' => $wgStylePath, + 'stylepath' => $conf->get( 'StylePath' ), 'wgUrlProtocols' => wfUrlProtocols(), - 'wgArticlePath' => $wgArticlePath, - 'wgScriptPath' => $wgScriptPath, - 'wgScriptExtension' => $wgScriptExtension, - 'wgScript' => $wgScript, - 'wgVariantArticlePath' => $wgVariantArticlePath, + 'wgArticlePath' => $conf->get( 'ArticlePath' ), + 'wgScriptPath' => $conf->get( 'ScriptPath' ), + 'wgScriptExtension' => $conf->get( 'ScriptExtension' ), + 'wgScript' => wfScript(), + 'wgSearchType' => $conf->get( 'SearchType' ), + 'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ), // Force object to avoid "empty" associative array from // becoming [] instead of {} in JS (bug 34604) - 'wgActionPaths' => (object)$wgActionPaths, - 'wgServer' => $wgServer, + 'wgActionPaths' => (object)$conf->get( 'ActionPaths' ), + 'wgServer' => $conf->get( 'Server' ), + 'wgServerName' => $conf->get( 'ServerName' ), 'wgUserLanguage' => $context->getLanguage(), 'wgContentLanguage' => $wgContLang->getCode(), - 'wgVersion' => $wgVersion, - 'wgEnableAPI' => $wgEnableAPI, - 'wgEnableWriteAPI' => $wgEnableWriteAPI, + 'wgVersion' => $conf->get( 'Version' ), + 'wgEnableAPI' => $conf->get( 'EnableAPI' ), + 'wgEnableWriteAPI' => $conf->get( 'EnableWriteAPI' ), 'wgMainPageTitle' => $mainPage->getPrefixedText(), 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(), 'wgNamespaceIds' => $namespaceIds, - 'wgSiteName' => $wgSitename, - 'wgFileExtensions' => array_values( array_unique( $wgFileExtensions ) ), - 'wgDBname' => $wgDBname, + 'wgContentNamespaces' => MWNamespace::getContentNamespaces(), + 'wgSiteName' => $conf->get( 'Sitename' ), + 'wgFileExtensions' => array_values( array_unique( $conf->get( 'FileExtensions' ) ) ), + 'wgDBname' => $conf->get( 'DBname' ), // This sucks, it is only needed on Special:Upload, but I could // not find a way to add vars only for a certain module - 'wgFileCanRotate' => BitmapHandler::canRotate(), + 'wgFileCanRotate' => SpecialUpload::rotationEnabled(), 'wgAvailableSkins' => Skin::getSkinNames(), - 'wgExtensionAssetsPath' => $wgExtensionAssetsPath, + 'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ), // MediaWiki sets cookies to have this prefix by default - 'wgCookiePrefix' => $wgCookiePrefix, - 'wgResourceLoaderMaxQueryLength' => $wgResourceLoaderMaxQueryLength, + 'wgCookiePrefix' => $conf->get( 'CookiePrefix' ), + 'wgCookieDomain' => $conf->get( 'CookieDomain' ), + 'wgCookiePath' => $conf->get( 'CookiePath' ), + 'wgCookieExpiration' => $conf->get( 'CookieExpiration' ), + 'wgResourceLoaderMaxQueryLength' => $conf->get( 'ResourceLoaderMaxQueryLength' ), 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces, 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ), + 'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ), + 'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ), ); wfRunHooks( 'ResourceLoaderGetConfigVars', array( &$vars ) ); - return $vars; + $this->configVars[$hash] = $vars; + return $this->configVars[$hash]; + } + + /** + * Recursively get all explicit and implicit dependencies for to the given module. + * + * @param array $registryData + * @param string $moduleName + * @return array + */ + protected static function getImplicitDependencies( array $registryData, $moduleName ) { + static $dependencyCache = array(); + + // The list of implicit dependencies won't be altered, so we can + // cache them without having to worry. + if ( !isset( $dependencyCache[$moduleName] ) ) { + + if ( !isset( $registryData[$moduleName] ) ) { + // Dependencies may not exist + $dependencyCache[$moduleName] = array(); + } else { + $data = $registryData[$moduleName]; + $dependencyCache[$moduleName] = $data['dependencies']; + + foreach ( $data['dependencies'] as $dependency ) { + // Recursively get the dependencies of the dependencies + $dependencyCache[$moduleName] = array_merge( + $dependencyCache[$moduleName], + self::getImplicitDependencies( $registryData, $dependency ) + ); + } + } + } + + return $dependencyCache[$moduleName]; + } + + /** + * Optimize the dependency tree in $this->modules and return it. + * + * The optimization basically works like this: + * Given we have module A with the dependencies B and C + * and module B with the dependency C. + * Now we don't have to tell the client to explicitly fetch module + * C as that's already included in module B. + * + * This way we can reasonably reduce the amout of module registration + * data send to the client. + * + * @param array &$registryData Modules keyed by name with properties: + * - string 'version' + * - array 'dependencies' + * - string|null 'group' + * - string 'source' + * - string|false 'loader' + */ + public static function compileUnresolvedDependencies( array &$registryData ) { + foreach ( $registryData as $name => &$data ) { + if ( $data['loader'] !== false ) { + continue; + } + $dependencies = $data['dependencies']; + foreach ( $data['dependencies'] as $dependency ) { + $implicitDependencies = self::getImplicitDependencies( $registryData, $dependency ); + $dependencies = array_diff( $dependencies, $implicitDependencies ); + } + // Rebuild keys + $data['dependencies'] = array_values( $dependencies ); + } } + /** - * Gets registration code for all modules + * Get registration code for all modules. * - * @param $context ResourceLoaderContext object - * @return String: JavaScript code for registering all modules with the client loader + * @param ResourceLoaderContext $context + * @return string JavaScript code for registering all modules with the client loader */ - public static function getModuleRegistrations( ResourceLoaderContext $context ) { - global $wgCacheEpoch; + public function getModuleRegistrations( ResourceLoaderContext $context ) { wfProfileIn( __METHOD__ ); - $out = ''; - $registrations = array(); $resourceLoader = $context->getResourceLoader(); $target = $context->getRequest()->getVal( 'target', 'desktop' ); - // Register sources - $out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() ); + $out = ''; + $registryData = array(); - // Register modules + // Get registry data foreach ( $resourceLoader->getModuleNames() as $name ) { $module = $resourceLoader->getModule( $name ); $moduleTargets = $module->getTargets(); if ( !in_array( $target, $moduleTargets ) ) { continue; } - $deps = $module->getDependencies(); - $group = $module->getGroup(); - $source = $module->getSource(); - // Support module loader scripts - $loader = $module->getLoaderScript(); - if ( $loader !== false ) { - $version = wfTimestamp( TS_ISO_8601_BASIC, - $module->getModifiedTime( $context ) ); - $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $source, $loader ); + + if ( $module->isRaw() ) { + // Don't register "raw" modules (like 'jquery' and 'mediawiki') client-side because + // depending on them is illegal anyway and would only lead to them being reloaded + // causing any state to be lost (like jQuery plugins, mw.config etc.) continue; } - // Automatically register module // getModifiedTime() is supposed to return a UNIX timestamp, but it doesn't always // seem to do that, and custom implementations might forget. Coerce it to TS_UNIX $moduleMtime = wfTimestamp( TS_UNIX, $module->getModifiedTime( $context ) ); - $mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $wgCacheEpoch ) ); - // Modules without dependencies, a group or a foreign source pass two arguments (name, timestamp) to - // mw.loader.register() - if ( !count( $deps ) && $group === null && $source === 'local' ) { - $registrations[] = array( $name, $mtime ); - } - // Modules with dependencies but no group or foreign source pass three arguments - // (name, timestamp, dependencies) to mw.loader.register() - elseif ( $group === null && $source === 'local' ) { - $registrations[] = array( $name, $mtime, $deps ); + $mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $this->getConfig()->get( 'CacheEpoch' ) ) ); + + // FIXME: Convert to numbers, wfTimestamp always gives us stings, even for TS_UNIX + + $skipFunction = $module->getSkipFunction(); + if ( $skipFunction !== null && !ResourceLoader::inDebugMode() ) { + $skipFunction = $resourceLoader->filter( 'minify-js', + $skipFunction, + // There will potentially be lots of these little string in the registrations + // manifest, we don't want to blow up the startup module with + // "/* cache key: ... */" all over it in non-debug mode. + /* cacheReport = */ false + ); } - // Modules with a group but no foreign source pass four arguments (name, timestamp, dependencies, group) - // to mw.loader.register() - elseif ( $source === 'local' ) { - $registrations[] = array( $name, $mtime, $deps, $group ); + + $registryData[$name] = array( + 'version' => $mtime, + 'dependencies' => $module->getDependencies(), + 'group' => $module->getGroup(), + 'source' => $module->getSource(), + 'loader' => $module->getLoaderScript(), + 'skip' => $skipFunction, + ); + } + + self::compileUnresolvedDependencies( $registryData ); + + // Register sources + $out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() ); + + // Concatenate module loader scripts and figure out the different call + // signatures for mw.loader.register + $registrations = array(); + foreach ( $registryData as $name => $data ) { + if ( $data['loader'] !== false ) { + $out .= ResourceLoader::makeCustomLoaderScript( + $name, + wfTimestamp( TS_ISO_8601_BASIC, $data['version'] ), + $data['dependencies'], + $data['group'], + $data['source'], + $data['loader'] + ); + continue; } - // Modules with a foreign source pass five arguments (name, timestamp, dependencies, group, source) - // to mw.loader.register() - else { - $registrations[] = array( $name, $mtime, $deps, $group, $source ); + + if ( + !count( $data['dependencies'] ) && + $data['group'] === null && + $data['source'] === 'local' && + $data['skip'] === null + ) { + // Modules with no dependencies, group, foreign source or skip function; + // call mw.loader.register(name, timestamp) + $registrations[] = array( $name, $data['version'] ); + } elseif ( + $data['group'] === null && + $data['source'] === 'local' && + $data['skip'] === null + ) { + // Modules with dependencies but no group, foreign source or skip function; + // call mw.loader.register(name, timestamp, dependencies) + $registrations[] = array( $name, $data['version'], $data['dependencies'] ); + } elseif ( + $data['source'] === 'local' && + $data['skip'] === null + ) { + // Modules with a group but no foreign source or skip function; + // call mw.loader.register(name, timestamp, dependencies, group) + $registrations[] = array( + $name, + $data['version'], + $data['dependencies'], + $data['group'] + ); + } elseif ( $data['skip'] === null ) { + // Modules with a foreign source but no skip function; + // call mw.loader.register(name, timestamp, dependencies, group, source) + $registrations[] = array( + $name, + $data['version'], + $data['dependencies'], + $data['group'], + $data['source'] + ); + } else { + // Modules with a skip function; + // call mw.loader.register(name, timestamp, dependencies, group, source, skip) + $registrations[] = array( + $name, + $data['version'], + $data['dependencies'], + $data['group'], + $data['source'], + $data['skip'] + ); } } + + // Register modules $out .= ResourceLoader::makeLoaderRegisterScript( $registrations ); wfProfileOut( __METHOD__ ); @@ -182,55 +333,75 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { } /** - * @param $context ResourceLoaderContext + * Base modules required for the the base environment of ResourceLoader + * + * @return array + */ + public static function getStartupModules() { + return array( 'jquery', 'mediawiki' ); + } + + /** + * Get the load URL of the startup modules. + * + * This is a helper for getScript(), but can also be called standalone, such + * as when generating an AppCache manifest. + * + * @param ResourceLoaderContext $context * @return string */ - public function getScript( ResourceLoaderContext $context ) { - global $IP, $wgLegacyJavaScriptGlobals; + public static function getStartupModulesUrl( ResourceLoaderContext $context ) { + $moduleNames = self::getStartupModules(); - $out = file_get_contents( "$IP/resources/startup.js" ); - if ( $context->getOnly() === 'scripts' ) { + // Get the latest version + $loader = $context->getResourceLoader(); + $version = 0; + foreach ( $moduleNames as $moduleName ) { + $version = max( $version, + $loader->getModule( $moduleName )->getModifiedTime( $context ) + ); + } + + $query = array( + 'modules' => ResourceLoader::makePackedModulesString( $moduleNames ), + 'only' => 'scripts', + 'lang' => $context->getLanguage(), + 'skin' => $context->getSkin(), + 'debug' => $context->getDebug() ? 'true' : 'false', + 'version' => wfTimestamp( TS_ISO_8601_BASIC, $version ) + ); + // Ensure uniform query order + ksort( $query ); + return wfAppendQuery( wfScript( 'load' ), $query ); + } - // The core modules: - $moduleNames = array( 'jquery', 'mediawiki' ); - wfRunHooks( 'ResourceLoaderGetStartupModules', array( &$moduleNames ) ); + /** + * @param ResourceLoaderContext $context + * @return string + */ + public function getScript( ResourceLoaderContext $context ) { + global $IP; - // Get the latest version - $loader = $context->getResourceLoader(); - $version = 0; - foreach ( $moduleNames as $moduleName ) { - $version = max( $version, - $loader->getModule( $moduleName )->getModifiedTime( $context ) - ); - } - // Build load query for StartupModules - $query = array( - 'modules' => ResourceLoader::makePackedModulesString( $moduleNames ), - 'only' => 'scripts', - 'lang' => $context->getLanguage(), - 'skin' => $context->getSkin(), - 'debug' => $context->getDebug() ? 'true' : 'false', - 'version' => wfTimestamp( TS_ISO_8601_BASIC, $version ) - ); - // Ensure uniform query order - ksort( $query ); + $out = file_get_contents( "$IP/resources/src/startup.js" ); + if ( $context->getOnly() === 'scripts' ) { // Startup function - $configuration = $this->getConfig( $context ); - $registrations = self::getModuleRegistrations( $context ); - $registrations = str_replace( "\n", "\n\t", trim( $registrations ) ); // fix indentation - $out .= "var startUp = function() {\n" . - "\tmw.config = new " . Xml::encodeJsCall( 'mw.Map', array( $wgLegacyJavaScriptGlobals ) ) . "\n" . + $configuration = $this->getConfigSettings( $context ); + $registrations = $this->getModuleRegistrations( $context ); + // Fix indentation + $registrations = str_replace( "\n", "\n\t", trim( $registrations ) ); + $out .= "var startUp = function () {\n" . + "\tmw.config = new " . + Xml::encodeJsCall( 'mw.Map', array( $this->getConfig()->get( 'LegacyJavaScriptGlobals' ) ) ) . "\n" . "\t$registrations\n" . "\t" . Xml::encodeJsCall( 'mw.config.set', array( $configuration ) ) . "};\n"; // Conditional script injection - $scriptTag = Html::linkedScript( wfAppendQuery( wfScript( 'load' ), $query ) ); + $scriptTag = Html::linkedScript( self::getStartupModulesUrl( $context ) ); $out .= "if ( isCompatible() ) {\n" . "\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) . - "}\n" . - "delete isCompatible;"; + "}"; } return $out; @@ -244,11 +415,11 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { } /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return array|mixed */ public function getModifiedTime( ResourceLoaderContext $context ) { - global $IP, $wgCacheEpoch; + global $IP; $hash = $context->getHash(); if ( isset( $this->modifiedTime[$hash] ) ) { @@ -260,19 +431,44 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { $loader = $context->getResourceLoader(); $loader->preloadModuleInfo( $loader->getModuleNames(), $context ); - $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" ); - // ATTENTION!: Because of the line above, this is not going to cause + $time = max( + wfTimestamp( TS_UNIX, $this->getConfig()->get( 'CacheEpoch' ) ), + filemtime( "$IP/resources/src/startup.js" ), + $this->getHashMtime( $context ) + ); + + // ATTENTION!: Because of the line below, this is not going to cause // infinite recursion - think carefully before making changes to this // code! - $time = wfTimestamp( TS_UNIX, $wgCacheEpoch ); + // Pre-populate modifiedTime with something because the the loop over + // all modules below includes the the startup module (this module). + $this->modifiedTime[$hash] = 1; + foreach ( $loader->getModuleNames() as $name ) { $module = $loader->getModule( $name ); $time = max( $time, $module->getModifiedTime( $context ) ); } - return $this->modifiedTime[$hash] = $time; + + $this->modifiedTime[$hash] = $time; + return $this->modifiedTime[$hash]; } - /* Methods */ + /** + * Hash of all dynamic data embedded in getScript(). + * + * Detect changes to mw.config settings embedded in #getScript (bug 28899). + * + * @param ResourceLoaderContext $context + * @return string Hash + */ + public function getModifiedHash( ResourceLoaderContext $context ) { + $data = array( + 'vars' => $this->getConfigSettings( $context ), + 'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ), + ); + + return md5( serialize( $data ) ); + } /** * @return string diff --git a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php index bda86539..40274c63 100644 --- a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php +++ b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php @@ -36,27 +36,27 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule { /* Methods */ /** - * @param $context ResourceLoaderContext - * @return array|int|Mixed + * @param ResourceLoaderContext $context + * @return array|int|mixed */ public function getModifiedTime( ResourceLoaderContext $context ) { $hash = $context->getHash(); - if ( isset( $this->modifiedTime[$hash] ) ) { - return $this->modifiedTime[$hash]; + if ( !isset( $this->modifiedTime[$hash] ) ) { + global $wgUser; + $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() ); } - global $wgUser; - return $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() ); + return $this->modifiedTime[$hash]; } /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return array */ public function getStyles( ResourceLoaderContext $context ) { - global $wgAllowUserCssPrefs, $wgUser; + global $wgUser; - if ( !$wgAllowUserCssPrefs ) { + if ( !$this->getConfig()->get( 'AllowUserCssPrefs' ) ) { return array(); } @@ -71,17 +71,8 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule { ( $options['underline'] ? 'underline' : 'none' ) . "; }"; } else { # The scripts of these languages are very hard to read with underlines - $rules[] = 'a:lang(ar), a:lang(ckb), a:lang(kk-arab), ' . - 'a:lang(mzn), a:lang(ps), a:lang(ur) { text-decoration: none; }'; - } - if ( $options['justify'] ) { - $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n"; - } - if ( !$options['showtoc'] ) { - $rules[] = "#toc { display: none; }\n"; - } - if ( !$options['editsection'] ) { - $rules[] = ".mw-editsection { display: none; }\n"; + $rules[] = 'a:lang(ar), a:lang(kk-arab), a:lang(mzn), ' . + 'a:lang(ps), a:lang(ur) { text-decoration: none; }'; } if ( $options['editfont'] !== 'default' ) { // Double-check that $options['editfont'] consists of safe characters only diff --git a/includes/resourceloader/ResourceLoaderUserGroupsModule.php b/includes/resourceloader/ResourceLoaderUserGroupsModule.php index 9064263f..7cf19420 100644 --- a/includes/resourceloader/ResourceLoaderUserGroupsModule.php +++ b/includes/resourceloader/ResourceLoaderUserGroupsModule.php @@ -25,21 +25,28 @@ */ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule { - /* Protected Methods */ + /* Protected Members */ + protected $origin = self::ORIGIN_USER_SITEWIDE; + protected $targets = array( 'desktop', 'mobile' ); + + /* Protected Methods */ /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return array */ protected function getPages( ResourceLoaderContext $context ) { - global $wgUser, $wgUseSiteJs, $wgUseSiteCss; + global $wgUser; $userName = $context->getUser(); if ( $userName === null ) { return array(); } - if ( !$wgUseSiteJs && !$wgUseSiteCss ) { + + $useSiteJs = $this->getConfig()->get( 'UseSiteJs' ); + $useSiteCss = $this->getConfig()->get( 'UseSiteCss' ); + if ( !$useSiteJs && !$useSiteCss ) { return array(); } @@ -55,13 +62,13 @@ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule { $pages = array(); foreach ( $user->getEffectiveGroups() as $group ) { - if ( in_array( $group, array( '*', 'user' ) ) ) { + if ( $group == '*' ) { continue; } - if ( $wgUseSiteJs ) { + if ( $useSiteJs ) { $pages["MediaWiki:Group-$group.js"] = array( 'type' => 'script' ); } - if ( $wgUseSiteCss ) { + if ( $useSiteCss ) { $pages["MediaWiki:Group-$group.css"] = array( 'type' => 'style' ); } } diff --git a/includes/resourceloader/ResourceLoaderUserModule.php b/includes/resourceloader/ResourceLoaderUserModule.php index 7a04e473..1b6d1de0 100644 --- a/includes/resourceloader/ResourceLoaderUserModule.php +++ b/includes/resourceloader/ResourceLoaderUserModule.php @@ -27,21 +27,27 @@ */ class ResourceLoaderUserModule extends ResourceLoaderWikiModule { - /* Protected Methods */ + /* Protected Members */ + protected $origin = self::ORIGIN_USER_INDIVIDUAL; + /* Protected Methods */ + /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return array */ protected function getPages( ResourceLoaderContext $context ) { - global $wgAllowUserJs, $wgAllowUserCss; $username = $context->getUser(); if ( $username === null ) { return array(); } - if ( !$wgAllowUserJs && !$wgAllowUserCss ) { + + $allowUserJs = $this->getConfig()->get( 'AllowUserJs' ); + $allowUserCss = $this->getConfig()->get( 'AllowUserCss' ); + + if ( !$allowUserJs && !$allowUserCss ) { return array(); } @@ -55,11 +61,11 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule { $userpage = $userpageTitle->getPrefixedDBkey(); // Needed so $excludepages works $pages = array(); - if ( $wgAllowUserJs ) { + if ( $allowUserJs ) { $pages["$userpage/common.js"] = array( 'type' => 'script' ); $pages["$userpage/" . $context->getSkin() . '.js'] = array( 'type' => 'script' ); } - if ( $wgAllowUserCss ) { + if ( $allowUserCss ) { $pages["$userpage/common.css"] = array( 'type' => 'style' ); $pages["$userpage/" . $context->getSkin() . '.css'] = array( 'type' => 'style' ); } diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php index 0b7e1964..bd97a8e5 100644 --- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php +++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php @@ -33,24 +33,26 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { protected $origin = self::ORIGIN_CORE_INDIVIDUAL; + protected $targets = array( 'desktop', 'mobile' ); + /* Methods */ /** - * @param $context ResourceLoaderContext - * @return array|int|Mixed + * @param ResourceLoaderContext $context + * @return array|int|mixed */ public function getModifiedTime( ResourceLoaderContext $context ) { $hash = $context->getHash(); - if ( isset( $this->modifiedTime[$hash] ) ) { - return $this->modifiedTime[$hash]; + if ( !isset( $this->modifiedTime[$hash] ) ) { + global $wgUser; + $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() ); } - global $wgUser; - return $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() ); + return $this->modifiedTime[$hash]; } /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return string */ public function getScript( ResourceLoaderContext $context ) { diff --git a/includes/resourceloader/ResourceLoaderUserTokensModule.php b/includes/resourceloader/ResourceLoaderUserTokensModule.php index 92ebbe93..668467ca 100644 --- a/includes/resourceloader/ResourceLoaderUserTokensModule.php +++ b/includes/resourceloader/ResourceLoaderUserTokensModule.php @@ -30,25 +30,27 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule { protected $origin = self::ORIGIN_CORE_INDIVIDUAL; + protected $targets = array( 'desktop', 'mobile' ); + /* Methods */ /** * Fetch the tokens for the current user. * - * @return array: List of tokens keyed by token type + * @return array List of tokens keyed by token type */ protected function contextUserTokens() { global $wgUser; return array( 'editToken' => $wgUser->getEditToken(), - 'patrolToken' => ApiQueryRecentChanges::getPatrolToken( null, null ), - 'watchToken' => ApiQueryInfo::getWatchToken( null, null ), + 'patrolToken' => $wgUser->getEditToken( 'patrol' ), + 'watchToken' => $wgUser->getEditToken( 'watch' ), ); } /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return string */ public function getScript( ResourceLoaderContext $context ) { diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index 3f10ae53..de61fc55 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -36,8 +36,8 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { # Origin is user-supplied code protected $origin = self::ORIGIN_USER_SITEWIDE; - // In-object cache for title mtimes - protected $titleMtimes = array(); + // In-object cache for title info + protected $titleInfo = array(); /* Abstract Protected Methods */ @@ -54,7 +54,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { * There is an optional media key, the value of which can be the * medium ('screen', 'print', etc.) of the stylesheet. * - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return array */ abstract protected function getPages( ResourceLoaderContext $context ); @@ -77,7 +77,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { } /** - * @param $title Title + * @param Title $title * @return null|string */ protected function getContent( $title ) { @@ -96,20 +96,20 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { return null; } - $model = $content->getModel(); - - if ( $model !== CONTENT_MODEL_CSS && $model !== CONTENT_MODEL_JAVASCRIPT ) { - wfDebugLog( 'resourceloader', __METHOD__ . ': bad content model $model for JS/CSS page!' ); + if ( $content->isSupportedFormat( CONTENT_FORMAT_JAVASCRIPT ) ) { + return $content->serialize( CONTENT_FORMAT_JAVASCRIPT ); + } elseif ( $content->isSupportedFormat( CONTENT_FORMAT_CSS ) ) { + return $content->serialize( CONTENT_FORMAT_CSS ); + } else { + wfDebugLog( 'resourceloader', __METHOD__ . ": bad content model {$content->getModel()} for JS/CSS page!" ); return null; } - - return $content->getNativeData(); //NOTE: this is safe, we know it's JS or CSS } /* Methods */ /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return string */ public function getScript( ResourceLoaderContext $context ) { @@ -125,22 +125,17 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { $script = $this->getContent( $title ); if ( strval( $script ) !== '' ) { $script = $this->validateScriptFile( $titleText, $script ); - if ( strpos( $titleText, '*/' ) === false ) { - $scripts .= "/* $titleText */\n"; - } - $scripts .= $script . "\n"; + $scripts .= ResourceLoader::makeComment( $titleText ) . $script . "\n"; } } return $scripts; } /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return array */ public function getStyles( ResourceLoaderContext $context ) { - global $wgScriptPath; - $styles = array(); foreach ( $this->getPages( $context ) as $titleText => $options ) { if ( $options['type'] !== 'style' ) { @@ -158,47 +153,84 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { if ( $this->getFlip( $context ) ) { $style = CSSJanus::transform( $style, true, false ); } - $style = CSSMin::remap( $style, false, $wgScriptPath, true ); + $style = CSSMin::remap( $style, false, $this->getConfig()->get( 'ScriptPath' ), true ); if ( !isset( $styles[$media] ) ) { $styles[$media] = array(); } - if ( strpos( $titleText, '*/' ) === false ) { - $style = "/* $titleText */\n" . $style; - } + $style = ResourceLoader::makeComment( $titleText ) . $style; $styles[$media][] = $style; } return $styles; } /** - * @param $context ResourceLoaderContext + * @param ResourceLoaderContext $context * @return int|mixed */ public function getModifiedTime( ResourceLoaderContext $context ) { $modifiedTime = 1; // wfTimestamp() interprets 0 as "now" - $mtimes = $this->getTitleMtimes( $context ); - if ( count( $mtimes ) ) { + $titleInfo = $this->getTitleInfo( $context ); + if ( count( $titleInfo ) ) { + $mtimes = array_map( function( $value ) { + return $value['timestamp']; + }, $titleInfo ); $modifiedTime = max( $modifiedTime, max( $mtimes ) ); } - $modifiedTime = max( $modifiedTime, $this->getMsgBlobMtime( $context->getLanguage() ) ); + $modifiedTime = max( + $modifiedTime, + $this->getMsgBlobMtime( $context->getLanguage() ), + $this->getDefinitionMtime( $context ) + ); return $modifiedTime; } /** - * @param $context ResourceLoaderContext + * Get the definition summary for this module. + * + * @param ResourceLoaderContext $context + * @return array + */ + public function getDefinitionSummary( ResourceLoaderContext $context ) { + return array( + 'class' => get_class( $this ), + 'pages' => $this->getPages( $context ), + ); + } + + /** + * @param ResourceLoaderContext $context * @return bool */ public function isKnownEmpty( ResourceLoaderContext $context ) { - return count( $this->getTitleMtimes( $context ) ) == 0; + $titleInfo = $this->getTitleInfo( $context ); + // Bug 68488: For modules in the "user" group, we should actually + // check that the pages are empty (page_len == 0), but for other + // groups, just check the pages exist so that we don't end up + // caching temporarily-blank pages without the appropriate + //