* https://www.mediawiki.org/wiki/ResourceLoader
*/
class ResourceLoader implements LoggerAwareInterface {
- /** @var int */
- const CACHE_VERSION = 8;
+ /** @var Config $config */
+ protected $config;
+ /** @var MessageBlobStore */
+ protected $blobStore;
- /** @var bool */
- protected static $debugMode = null;
+ /** @var LoggerInterface */
+ private $logger;
- /**
- * Module name/ResourceLoaderModule object pairs
- * @var array
- */
+ /** @var ResourceLoaderModule[] Map of (module name => ResourceLoaderModule) */
protected $modules = [];
-
- /**
- * Associative array mapping module name to info associative array
- * @var array
- */
+ /** @var array[] Map of (module name => associative info array) */
protected $moduleInfos = [];
-
- /** @var Config $config */
- protected $config;
-
/**
- * List of module names that contain QUnit test suites
- * @var string[]
+ * Associative array mapping framework ids to a list of names of test suite modules
+ * like [ 'qunit' => [ 'mediawiki.tests.qunit.suites', 'ext.foo.tests', ... ], ... ]
+ * @var array
*/
+ protected $testModuleNames = [];
+ /** @var string[] List of module names that contain QUnit test suites */
protected $testSuiteModuleNames = [];
- /**
- * E.g. [ 'source-id' => 'http://.../load.php' ]
- * @var array
- */
+ /** @var array Map of (source => path); E.g. [ 'source-id' => 'http://.../load.php' ] */
protected $sources = [];
-
- /**
- * Errors accumulated during current respond() call.
- * @var array
- */
+ /** @var array Errors accumulated during current respond() call */
protected $errors = [];
-
- /**
- * List of extra HTTP response headers provided by loaded modules.
- *
- * Populated by makeModuleResponse().
- *
- * @var array
- */
+ /** @var string[] Extra HTTP response headers from modules loaded in makeModuleResponse() */
protected $extraHeaders = [];
- /**
- * @var MessageBlobStore
- */
- protected $blobStore;
+ /** @var bool */
+ protected static $debugMode = null;
- /**
- * @var LoggerInterface
- */
- private $logger;
+ /** @var int */
+ const CACHE_VERSION = 8;
- /** @var string JavaScript / CSS pragma to disable minification. **/
+ /** @var string JavaScript / CSS pragma to disable minification. * */
const FILTER_NOMIN = '/*@nomin*/';
/**
}
/**
+ * @internal For use by ResourceLoaderStartUpModule only.
+ */
+ const HASH_LENGTH = 5;
+
+ /**
+ * Create a hash for module versioning purposes.
+ *
+ * This hash is used in three ways:
+ *
+ * - To differentiate between the current version and a past version
+ * of a module by the same name.
+ *
+ * In the cache key of localStorage in the browser (mw.loader.store).
+ * This store keeps only one version of any given module. As long as the
+ * next version the client encounters has a different hash from the last
+ * version it saw, it will correctly discard it in favour of a network fetch.
+ *
+ * A browser may evict a site's storage container for any reason (e.g. when
+ * the user hasn't visited a site for some time, and/or when the device is
+ * low on storage space). Anecdotally it seems devices rarely keep unused
+ * storage beyond 2 weeks on mobile devices and 4 weeks on desktop.
+ * But, there is no hard limit or expiration on localStorage.
+ * ResourceLoader's Client also clears localStorage when the user changes
+ * their language preference or when they (temporarily) use Debug Mode.
+ *
+ * The only hard factors that reduce the range of possible versions are
+ * 1) the name and existence of a given module, and
+ * 2) the TTL for mw.loader.store, and
+ * 3) the `$wgResourceLoaderStorageVersion` configuration variable.
+ *
+ * - To identify a batch response of modules from load.php in an HTTP cache.
+ *
+ * When fetching modules in a batch from load.php, a combined hash
+ * is created by the JS code, and appended as query parameter.
+ *
+ * In cache proxies (e.g. Varnish, Nginx) and in the browser's HTTP cache,
+ * these urls are used to identify other previously cached responses.
+ * The range of possible versions a given version has to be unique amongst
+ * is determined by the maximum duration each response is stored for, which
+ * is controlled by `$wgResourceLoaderMaxage['versioned']`.
+ *
+ * - To detect race conditions between multiple web servers in a MediaWiki
+ * deployment of which some have the newer version and some still the older
+ * version.
+ *
+ * An HTTP request from a browser for the Startup manifest may be responded
+ * to by a server with the newer version. The browser may then use that to
+ * request a given module, which may then be responded to by a server with
+ * the older version. To avoid caching this for too long (which would pollute
+ * all other users without repairing itself), the combined hash that the JS
+ * client adds to the url is verified by the server (in ::sendResponseHeaders).
+ * If they don't match, we instruct cache proxies and clients to not cache
+ * this response as long as they normally would. This is also the reason
+ * that the algorithm used here in PHP must match the one used in JS.
+ *
+ * The fnv132 digest creates a 32-bit integer, which goes upto 4 Giga and
+ * needs up to 7 chars in base 36.
+ * Within 7 characters, base 36 can count up to 78,364,164,096 (78 Giga),
+ * (but with fnv132 we'd use very little of this range, mostly padding).
+ * Within 6 characters, base 36 can count up to 2,176,782,336 (2 Giga).
+ * Within 5 characters, base 36 can count up to 60,466,176 (60 Mega).
+ *
* @since 1.26
* @param string $value
* @return string Hash
*/
public static function makeHash( $value ) {
$hash = hash( 'fnv132', $value );
- return Wikimedia\base_convert( $hash, 16, 36, 7 );
+ // The base_convert will pad it (if too short),
+ // then substr() will trim it (if too long).
+ return substr(
+ Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
+ 0,
+ self::HASH_LENGTH
+ );
}
/**
// Do not allow private modules to be loaded from the web.
// This is a security issue, see T36907.
if ( $module->getGroup() === 'private' ) {
+ // Not a serious error, just means something is trying to access it (T101806)
$this->logger->debug( "Request for private module '$name' denied" );
- $this->errors[] = "Cannot show private module \"$name\"";
+ $this->errors[] = "Cannot build private module \"$name\"";
continue;
}
$modules[$name] = $module;
/**
* Returns JS code which, when called, will register a given list of messages.
*
- * @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.
+ * @param mixed $messages Associative array mapping message key to value.
* @return string JavaScript code
*/
public static function makeMessageSetScript( $messages ) {
*
* @internal
* @since 1.32
- * @param bool|string|array $data
+ * @param mixed $data
* @return string JSON
*/
public static function encodeJsonForScript( $data ) {