// 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 ) {
*
* @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
* Values considered empty:
*
* - null
- * - array()
+ * - []
* - new XmlJsCode( '{}' )
- * - new stdClass() // (object) array()
+ * - new stdClass() // (object) []
*
* @param Array $array
*/