protected $moduleInfos = [];
/** @var Config $config */
- private $config;
+ protected $config;
/**
* 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', .. ), .. )
+ * like [ 'qunit' => [ 'mediawiki.tests.qunit.suites', 'ext.foo.tests', ... ], ... ]
* @var array
*/
protected $testModuleNames = [];
/**
- * E.g. array( 'source-id' => 'http://.../load.php' )
+ * E.g. [ 'source-id' => 'http://.../load.php' ]
* @var array
*/
protected $sources = [];
// Or else Database*::select() will explode, plus it's cheaper!
return;
}
- $dbr = wfGetDB( DB_SLAVE );
+ $dbr = wfGetDB( DB_REPLICA );
$skin = $context->getSkin();
$lang = $context->getLanguage();
}
}
+ // Batched version of ResourceLoaderWikiModule::getTitleInfo
+ ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
+
// Prime in-object cache for message blobs for modules with messages
$modules = [];
foreach ( $moduleNames as $name ) {
$this->config = $config;
// Add 'local' source first
- $this->addSource( 'local', wfScript( 'load' ) );
+ $this->addSource( 'local', $config->get( 'LoadScript' ) );
// Add other sources
$this->addSource( $config->get( 'ResourceLoaderSources' ) );
*
* Source IDs are typically the same as the Wiki ID or database name (e.g. lowercase a-z).
*
- * @param array|string $id Source ID (string), or array( id1 => loadUrl, id2 => loadUrl, ... )
+ * @param array|string $id Source ID (string), or [ id1 => loadUrl, id2 => loadUrl, ... ]
* @param string|array $loadUrl load.php url (string), or array with loadUrl key for
* backwards-compatibility.
* @throws MWException
/**
* Get the list of sources.
*
- * @return array Like array( id => load.php url, .. )
+ * @return array Like [ id => load.php url, ... ]
*/
public function getSources() {
return $this->sources;
*
* @since 1.26
* @param ResourceLoaderContext $context
- * @param array $modules List of ResourceLoaderModule objects
+ * @param string[] $modules List of known module names
* @return string Hash
*/
- public function getCombinedVersion( ResourceLoaderContext $context, array $modules ) {
- if ( !$modules ) {
+ public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
+ if ( !$moduleNames ) {
return '';
}
$hashes = array_map( function ( $module ) use ( $context ) {
return $this->getModule( $module )->getVersionHash( $context );
- }, $modules );
- return self::makeHash( implode( $hashes ) );
+ }, $moduleNames );
+ return self::makeHash( implode( '', $hashes ) );
+ }
+
+ /**
+ * Get the expected value of the 'version' query parameter.
+ *
+ * This is used by respond() to set a short Cache-Control header for requests with
+ * information newer than the current server has. This avoids pollution of edge caches.
+ * Typically during deployment. (T117587)
+ *
+ * This MUST match return value of `mw.loader#getCombinedVersion()` client-side.
+ *
+ * @since 1.28
+ * @param ResourceLoaderContext $context
+ * @param string[] $modules List of module names
+ * @return string Hash
+ */
+ public function makeVersionQuery( ResourceLoaderContext $context ) {
+ // As of MediaWiki 1.28, the server and client use the same algorithm for combining
+ // version hashes. There is no technical reason for this to be same, and for years the
+ // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
+ // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
+ // query parameter), then this method must continue to match the JS one.
+ $moduleNames = [];
+ foreach ( $context->getModules() as $name ) {
+ if ( !$this->getModule( $name ) ) {
+ // If a versioned request contains a missing module, the version is a mismatch
+ // as the client considered a module (and version) we don't have.
+ return '';
+ }
+ $moduleNames[] = $name;
+ }
+ return $this->getCombinedVersion( $context, $moduleNames );
}
/**
*/
protected function sendResponseHeaders( ResourceLoaderContext $context, $etag, $errors ) {
$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 ) {
+ // Use a short cache expiry so that updates propagate to clients quickly, if:
+ // - No version specified (shared resources, e.g. stylesheets)
+ // - There were errors (recover quickly)
+ // - Version mismatch (T117587, T47877)
+ if ( is_null( $context->getVersion() )
+ || $errors
+ || $context->getVersion() !== $this->makeVersionQuery( $context )
+ ) {
$maxage = $rlMaxage['unversioned']['client'];
$smaxage = $rlMaxage['unversioned']['server'];
// If a version was specified we can use a longer expiry time since changing
$good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
if ( !$good ) {
try { // RL always hits the DB on file cache miss...
- wfGetDB( DB_SLAVE );
+ wfGetDB( DB_REPLICA );
} catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
$good = $fileCache->isCacheGood(); // cache existence check
}
foreach ( $modules as $name => $module ) {
try {
$content = $module->getModuleContent( $context );
+ $implementKey = $name . '@' . $module->getVersionHash( $context );
$strContent = '';
// Append output
$strContent = $scripts;
} elseif ( is_array( $scripts ) ) {
// ...except when $scripts is an array of URLs
- $strContent = self::makeLoaderImplementScript( $name, $scripts, [], [], [] );
+ $strContent = self::makeLoaderImplementScript( $implementKey, $scripts, [], [], [] );
}
break;
case 'styles':
$strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
break;
default:
+ $scripts = isset( $content['scripts'] ) ? $content['scripts'] : '';
+ if ( is_string( $scripts ) ) {
+ if ( $name === 'site' || $name === 'user' ) {
+ // Legacy scripts that run in the global scope without a closure.
+ // mw.loader.implement will use globalEval if scripts is a string.
+ // Minify manually here, because general response minification is
+ // not effective due it being a string literal, not a function.
+ if ( !ResourceLoader::inDebugMode() ) {
+ $scripts = self::filter( 'minify-js', $scripts ); // T107377
+ }
+ } else {
+ $scripts = new XmlJsCode( $scripts );
+ }
+ }
$strContent = self::makeLoaderImplementScript(
- $name,
- isset( $content['scripts'] ) ? $content['scripts'] : '',
+ $implementKey,
+ $scripts,
isset( $content['styles'] ) ? $content['styles'] : [],
isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
isset( $content['templates'] ) ? $content['templates'] : []
/**
* Return JS code that calls mw.loader.implement with given module properties.
*
- * @param string $name Module name
- * @param mixed $scripts List of URLs to JavaScript files or String of JavaScript code
+ * @param string $name Module name or implement key (format "`[name]@[version]`")
+ * @param XmlJsCode|array|string $scripts Code as XmlJsCode (to be wrapped in a closure),
+ * list of URLs to JavaScript files, or a string of JavaScript for `$.globalEval`.
* @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
* @throws MWException
* @return string
*/
- public static function makeLoaderImplementScript(
+ protected static function makeLoaderImplementScript(
$name, $scripts, $styles, $messages, $templates
) {
- if ( is_string( $scripts ) ) {
- // Site and user module are a legacy scripts that run in the global scope (no closure).
- // Transportation as string instructs mw.loader.implement to use globalEval.
- if ( $name === 'site' || $name === 'user' ) {
- // Minify manually because the general makeModuleResponse() minification won't be
- // effective here due to the script being a string instead of a function. (T107377)
- if ( !ResourceLoader::inDebugMode() ) {
- $scripts = self::filter( 'minify-js', $scripts );
- }
- } else {
- $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts}\n}" );
- }
- } elseif ( !is_array( $scripts ) ) {
+
+ if ( $scripts instanceof XmlJsCode ) {
+ $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
+ } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
}
// mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
* - ResourceLoader::makeLoaderStateScript( $name, $state ):
* Set the state of a single module called $name to $state
*
- * - ResourceLoader::makeLoaderStateScript( array( $name => $state, ... ) ):
+ * - ResourceLoader::makeLoaderStateScript( [ $name => $state, ... ] ):
* Set the state of modules with the given names to the given states
*
* @param string $name
* Values considered empty:
*
* - null
- * - array()
+ * - []
* - new XmlJsCode( '{}' )
- * - new stdClass() // (object) array()
+ * - new stdClass() // (object) []
*
* @param Array $array
*/
* ):
* Register a single module.
*
- * - ResourceLoader::makeLoaderRegisterScript( array( $name1, $name2 ) ):
+ * - ResourceLoader::makeLoaderRegisterScript( [ $name1, $name2 ] ):
* Register modules with the given names.
*
- * - ResourceLoader::makeLoaderRegisterScript( array(
- * array( $name1, $version1, $dependencies1, $group1, $source1, $skip1 ),
- * array( $name2, $version2, $dependencies1, $group2, $source2, $skip2 ),
+ * - ResourceLoader::makeLoaderRegisterScript( [
+ * [ $name1, $version1, $dependencies1, $group1, $source1, $skip1 ],
+ * [ $name2, $version2, $dependencies1, $group2, $source2, $skip2 ],
* ...
- * ) ):
+ * ] ):
* Registers modules with the given names and parameters.
*
* @param string $name Module name
* - ResourceLoader::makeLoaderSourcesScript( $id, $properties ):
* Register a single source
*
- * - ResourceLoader::makeLoaderSourcesScript( array( $id1 => $loadUrl, $id2 => $loadUrl, ... ) );
+ * - ResourceLoader::makeLoaderSourcesScript( [ $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 $loadUrl load.php url
* @return string
*/
- public static function makeLoaderSourcesScript( $id, $properties = null ) {
+ public static function makeLoaderSourcesScript( $id, $loadUrl = null ) {
if ( is_array( $id ) ) {
return Xml::encodeJsCall(
'mw.loader.addSource',
} else {
return Xml::encodeJsCall(
'mw.loader.addSource',
- [ $id, $properties ],
+ [ $id, $loadUrl ],
ResourceLoader::inDebugMode()
);
}
/**
* Convert an array of module names to a packed query string.
*
- * For example, array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' )
+ * For example, [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ]
* becomes 'foo.bar,baz|bar.baz,quux'
* @param array $modules List of module names (strings)
* @return string Packed query string
*/
public static function makePackedModulesString( $modules ) {
- $groups = []; // array( prefix => array( suffixes ) )
+ $groups = []; // [ prefix => [ suffixes ] ]
foreach ( $modules as $module ) {
$pos = strrpos( $module, '.' );
$prefix = $pos === false ? '' : substr( $module, 0, $pos );
array_fill_keys( $this->config->get( 'ResourceLoaderLESSImportPaths' ), '' )
);
$parser->SetOption( 'relativeUrls', false );
- $parser->SetCacheDir( $this->config->get( 'CacheDirectory' ) ?: wfTempDir() );
return $parser;
}