Merge "Convert LanguageConverter to using getLocalServerObjectCache()"
[lhc/web/wiklou.git] / includes / resourceloader / ResourceLoader.php
index 9ce2c5a..143f5cc 100644 (file)
@@ -109,7 +109,7 @@ class ResourceLoader implements LoggerAwareInterface {
                        // 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();
 
@@ -140,6 +140,9 @@ class ResourceLoader implements LoggerAwareInterface {
                        }
                }
 
+               // 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 ) {
@@ -610,17 +613,49 @@ class ResourceLoader implements LoggerAwareInterface {
         *
         * @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 );
        }
 
        /**
@@ -759,10 +794,14 @@ class ResourceLoader implements LoggerAwareInterface {
         */
        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
@@ -857,7 +896,7 @@ class ResourceLoader implements LoggerAwareInterface {
                $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
                        }
@@ -970,6 +1009,7 @@ MESSAGE;
                foreach ( $modules as $name => $module ) {
                        try {
                                $content = $module->getModuleContent( $context );
+                               $implementKey = $name . '@' . $module->getVersionHash( $context );
                                $strContent = '';
 
                                // Append output
@@ -981,7 +1021,7 @@ MESSAGE;
                                                        $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':
@@ -992,9 +1032,23 @@ MESSAGE;
                                                $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'] : []
@@ -1072,8 +1126,9 @@ MESSAGE;
        /**
         * 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
@@ -1084,22 +1139,13 @@ MESSAGE;
         * @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
@@ -1234,9 +1280,9 @@ MESSAGE;
         * Values considered empty:
         *
         * - null
-        * - array()
+        * - []
         * - new XmlJsCode( '{}' )
-        * - new stdClass() // (object) array()
+        * - new stdClass() // (object) []
         *
         * @param Array $array
         */