MediaWiki  master
ResourceLoaderStartUpModule.php
Go to the documentation of this file.
1 <?php
44  protected $targets = [ 'desktop', 'mobile' ];
45 
46  private $groupIds = [
47  // These reserved numbers MUST start at 0 and not skip any. These are preset
48  // for forward compatibility so that they can be safely referenced by mediawiki.js,
49  // even when the code is cached and the order of registrations (and implicit
50  // group ids) changes between versions of the software.
51  'user' => 0,
52  'private' => 1,
53  ];
54 
64  protected static function getImplicitDependencies(
65  array $registryData,
66  string $moduleName,
67  array $handled = []
68  ): array {
69  static $dependencyCache = [];
70 
71  // No modules will be added or changed server-side after this point,
72  // so we can safely cache parts of the tree for re-use.
73  if ( !isset( $dependencyCache[$moduleName] ) ) {
74  if ( !isset( $registryData[$moduleName] ) ) {
75  // Unknown module names are allowed here, this is only an optimisation.
76  // Checks for illegal and unknown dependencies happen as PHPUnit structure tests,
77  // and also client-side at run-time.
78  $flat = [];
79  } else {
80  $data = $registryData[$moduleName];
81  $flat = $data['dependencies'];
82 
83  // Prevent recursion
84  $handled[] = $moduleName;
85  foreach ( $data['dependencies'] as $dependency ) {
86  if ( in_array( $dependency, $handled, true ) ) {
87  // If we encounter a circular dependency, then stop the optimiser and leave the
88  // original dependencies array unmodified. Circular dependencies are not
89  // supported in ResourceLoader. Awareness of them exists here so that we can
90  // optimise the registry when it isn't broken, and otherwise transport the
91  // registry unchanged. The client will handle this further.
93  } else {
94  // Recursively add the dependencies of the dependencies
95  $flat = array_merge(
96  $flat,
97  self::getImplicitDependencies( $registryData, $dependency, $handled )
98  );
99  }
100  }
101  }
102 
103  $dependencyCache[$moduleName] = $flat;
104  }
105 
106  return $dependencyCache[$moduleName];
107  }
108 
128  public static function compileUnresolvedDependencies( array &$registryData ): void {
129  foreach ( $registryData as $name => &$data ) {
130  $dependencies = $data['dependencies'];
131  try {
132  foreach ( $data['dependencies'] as $dependency ) {
133  $implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
134  $dependencies = array_diff( $dependencies, $implicitDependencies );
135  }
136  } catch ( ResourceLoaderCircularDependencyError $err ) {
137  // Leave unchanged
138  $dependencies = $data['dependencies'];
139  }
140 
141  // Rebuild keys
142  $data['dependencies'] = array_values( $dependencies );
143  }
144  }
145 
152  public function getModuleRegistrations( ResourceLoaderContext $context ): string {
153  $resourceLoader = $context->getResourceLoader();
154  // Future developers: Use WebRequest::getRawVal() instead getVal().
155  // The getVal() method performs slow Language+UTF logic. (f303bb9360)
156  $target = $context->getRequest()->getRawVal( 'target', 'desktop' );
157  $safemode = $context->getRequest()->getRawVal( 'safemode' ) === '1';
158  // Bypass target filter if this request is Special:JavaScriptTest.
159  // To prevent misuse in production, this is only allowed if testing is enabled server-side.
160  $byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
161 
162  $out = '';
163  $states = [];
164  $registryData = [];
165  $moduleNames = $resourceLoader->getModuleNames();
166 
167  // Preload with a batch so that the below calls to getVersionHash() for each module
168  // don't require on-demand loading of more information.
169  try {
170  $resourceLoader->preloadModuleInfo( $moduleNames, $context );
171  } catch ( Exception $e ) {
172  // Don't fail the request (T152266)
173  // Also print the error in the main output
174  $resourceLoader->outputErrorAndLog( $e,
175  'Preloading module info from startup failed: {exception}',
176  [ 'exception' => $e ]
177  );
178  }
179 
180  // Get registry data
181  foreach ( $moduleNames as $name ) {
182  $module = $resourceLoader->getModule( $name );
183  $moduleTargets = $module->getTargets();
184  if (
185  ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) )
186  || ( $safemode && $module->getOrigin() > ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL )
187  ) {
188  continue;
189  }
190 
191  if ( $module instanceof ResourceLoaderStartUpModule ) {
192  // Don't register 'startup' to the client because loading it lazily or depending
193  // on it doesn't make sense, because the startup module *is* the client.
194  // Registering would be a waste of bandwidth and memory and risks somehow causing
195  // it to load a second time.
196 
197  // ATTENTION: Because of the line below, this is not going to cause infinite recursion.
198  // Think carefully before making changes to this code!
199  // The below code is going to call ResourceLoaderModule::getVersionHash() for every module.
200  // For StartUpModule (this module) the hash is computed based on the manifest content,
201  // which is the very thing we are computing right here. As such, this must skip iterating
202  // over 'startup' itself.
203  continue;
204  }
205 
206  // Optimization: Exclude modules in the `noscript` group. These are only ever used
207  // directly by HTML without use of JavaScript (T291735).
208  if ( $module->getGroup() === 'noscript' ) {
209  continue;
210  }
211 
212  try {
213  // The version should be formatted by ResourceLoader::makeHash and be of
214  // length ResourceLoader::HASH_LENGTH (or empty string).
215  // The getVersionHash method is final and is covered by tests, as is makeHash().
216  $versionHash = $module->getVersionHash( $context );
217  } catch ( Exception $e ) {
218  // Don't fail the request (T152266)
219  // Also print the error in the main output
220  $resourceLoader->outputErrorAndLog( $e,
221  'Calculating version for "{module}" failed: {exception}',
222  [
223  'module' => $name,
224  'exception' => $e,
225  ]
226  );
227  $versionHash = '';
228  $states[$name] = 'error';
229  }
230 
231  $skipFunction = $module->getSkipFunction();
232  if ( $skipFunction !== null && !$context->getDebug() ) {
233  $skipFunction = ResourceLoader::filter( 'minify-js', $skipFunction );
234  }
235 
236  $registryData[$name] = [
237  'version' => $versionHash,
238  'dependencies' => $module->getDependencies( $context ),
239  'es6' => $module->requiresES6(),
240  'group' => $this->getGroupId( $module->getGroup() ),
241  'source' => $module->getSource(),
242  'skip' => $skipFunction,
243  ];
244  }
245 
246  self::compileUnresolvedDependencies( $registryData );
247 
248  // Register sources
249  $out .= ResourceLoader::makeLoaderSourcesScript( $context, $resourceLoader->getSources() );
250 
251  // Figure out the different call signatures for mw.loader.register
252  $registrations = [];
253  foreach ( $registryData as $name => $data ) {
254  // Call mw.loader.register(name, version, dependencies, group, source, skip)
255  $registrations[] = [
256  $name,
257  // HACK: signify ES6 with a ! added at the end of the version
258  // This avoids having to add another register() parameter, and generating
259  // a bunch of nulls for ES6-only modules
260  $data['version'] . ( $data['es6'] ? '!' : '' ),
261  $data['dependencies'],
262  $data['group'],
263  // Swap default (local) for null
264  $data['source'] === 'local' ? null : $data['source'],
265  $data['skip']
266  ];
267  }
268 
269  // Register modules
270  $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $context, $registrations );
271 
272  if ( $states ) {
273  $out .= "\n" . ResourceLoader::makeLoaderStateScript( $context, $states );
274  }
275 
276  return $out;
277  }
278 
279  private function getGroupId( $groupName ): ?int {
280  if ( $groupName === null ) {
281  return null;
282  }
283 
284  if ( !array_key_exists( $groupName, $this->groupIds ) ) {
285  $this->groupIds[$groupName] = count( $this->groupIds );
286  }
287 
288  return $this->groupIds[$groupName];
289  }
290 
296  private function getBaseModules(): array {
297  return [ 'jquery', 'mediawiki.base' ];
298  }
299 
306  private function getStoreKey(): string {
307  return 'MediaWikiModuleStore:' . $this->getConfig()->get( 'DBname' );
308  }
309 
314  private function getMaxQueryLength(): int {
315  $len = $this->getConfig()->get( 'ResourceLoaderMaxQueryLength' );
316  // - Ignore -1, which in MW 1.34 and earlier was used to mean "unlimited".
317  // - Ignore invalid values, e.g. non-int or other negative values.
318  if ( $len === false || $len < 0 ) {
319  // Default
320  $len = 2000;
321  }
322  return $len;
323  }
324 
331  private function getStoreVary( ResourceLoaderContext $context ): string {
332  return implode( ':', [
333  $context->getSkin(),
334  $this->getConfig()->get( 'ResourceLoaderStorageVersion' ),
335  $context->getLanguage(),
336  ] );
337  }
338 
343  public function getScript( ResourceLoaderContext $context ): string {
344  global $IP;
345  $conf = $this->getConfig();
346 
347  if ( $context->getOnly() !== 'scripts' ) {
348  return '/* Requires only=scripts */';
349  }
350 
351  $startupCode = file_get_contents( "$IP/resources/src/startup/startup.js" );
352 
353  // The files read here MUST be kept in sync with maintenance/jsduck/eg-iframe.html.
354  $mwLoaderCode = file_get_contents( "$IP/resources/src/startup/mediawiki.js" ) .
355  file_get_contents( "$IP/resources/src/startup/mediawiki.loader.js" ) .
356  file_get_contents( "$IP/resources/src/startup/mediawiki.requestIdleCallback.js" );
357  if ( $conf->get( 'ResourceLoaderEnableJSProfiler' ) ) {
358  $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/profiler.js" );
359  }
360 
361  // Perform replacements for mediawiki.js
362  $mwLoaderPairs = [
363  // This should always be an object, even if the base vars are empty
364  // (such as when using the default lang/skin).
365  '$VARS.reqBase' => $context->encodeJson( (object)$context->getReqBase() ),
366  '$VARS.baseModules' => $context->encodeJson( $this->getBaseModules() ),
367  '$VARS.maxQueryLength' => $context->encodeJson( $this->getMaxQueryLength() ),
368  // The client-side module cache can be disabled by site configuration.
369  // It is also always disabled in debug mode.
370  '$VARS.storeDisabled' => $context->encodeJson(
371  !$conf->get( 'ResourceLoaderStorageEnabled' ) || $context->getDebug()
372  ),
373  '$VARS.storeKey' => $context->encodeJson( $this->getStoreKey() ),
374  '$VARS.storeVary' => $context->encodeJson( $this->getStoreVary( $context ) ),
375  '$VARS.groupUser' => $context->encodeJson( $this->getGroupId( 'user' ) ),
376  '$VARS.groupPrivate' => $context->encodeJson( $this->getGroupId( 'private' ) ),
377  // Only expose private mw.redefineFallbacksForTest in test mode.
378  '$CODE.maybeRedefineFallbacksForTest();' => $conf->get( 'EnableJavaScriptTest' ) ?
379  'mw.redefineFallbacksForTest = defineFallbacks;' :
380  '',
381  ];
382  $profilerStubs = [
383  '$CODE.profileExecuteStart();' => 'mw.loader.profiler.onExecuteStart( module );',
384  '$CODE.profileExecuteEnd();' => 'mw.loader.profiler.onExecuteEnd( module );',
385  '$CODE.profileScriptStart();' => 'mw.loader.profiler.onScriptStart( module );',
386  '$CODE.profileScriptEnd();' => 'mw.loader.profiler.onScriptEnd( module );',
387  ];
388  $debugStubs = [
389  '$CODE.consoleLog();' => 'console.log.apply( console, arguments );',
390  ];
391  // When profiling is enabled, insert the calls. When disabled (by default), insert nothing.
392  $mwLoaderPairs += $conf->get( 'ResourceLoaderEnableJSProfiler' )
393  ? $profilerStubs
394  : array_fill_keys( array_keys( $profilerStubs ), '' );
395  $mwLoaderPairs += $context->getDebug()
396  ? $debugStubs
397  : array_fill_keys( array_keys( $debugStubs ), '' );
398  $mwLoaderCode = strtr( $mwLoaderCode, $mwLoaderPairs );
399 
400  // Perform string replacements for startup.js
401  $pairs = [
402  // Raw JavaScript code (not JSON)
403  '$CODE.registrations();' => trim( $this->getModuleRegistrations( $context ) ),
404  '$CODE.defineLoader();' => $mwLoaderCode,
405  ];
406  $startupCode = strtr( $startupCode, $pairs );
407 
408  return $startupCode;
409  }
410 
414  public function supportsURLLoading(): bool {
415  return false;
416  }
417 
421  public function enableModuleContentVersion(): bool {
422  // Enabling this means that ResourceLoader::getVersionHash will simply call getScript()
423  // and hash it to determine the version (as used by E-Tag HTTP response header).
424  return true;
425  }
426 }
ResourceLoader\filter
static filter( $filter, $data, array $options=[])
Run JavaScript or CSS data through a filter, caching the filtered result for future calls.
Definition: ResourceLoader.php:182
ResourceLoaderStartUpModule\$targets
$targets
Definition: ResourceLoaderStartUpModule.php:44
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:34
ResourceLoaderContext\getReqBase
getReqBase()
Get the request base parameters, omitting any defaults.
Definition: ResourceLoaderContext.php:412
ResourceLoaderStartUpModule\getModuleRegistrations
getModuleRegistrations(ResourceLoaderContext $context)
Get registration code for all modules.
Definition: ResourceLoaderStartUpModule.php:152
ResourceLoaderContext\getResourceLoader
getResourceLoader()
Definition: ResourceLoaderContext.php:150
ResourceLoader\makeLoaderRegisterScript
static makeLoaderRegisterScript(ResourceLoaderContext $context, array $modules)
Format JS code which calls mw.loader.register() with the given parameters.
Definition: ResourceLoader.php:1504
ResourceLoaderContext\getOnly
getOnly()
Definition: ResourceLoaderContext.php:274
ResourceLoaderStartUpModule\getGroupId
getGroupId( $groupName)
Definition: ResourceLoaderStartUpModule.php:279
ResourceLoaderStartUpModule\supportsURLLoading
supportsURLLoading()
Definition: ResourceLoaderStartUpModule.php:414
ResourceLoaderContext\getRequest
getRequest()
Definition: ResourceLoaderContext.php:165
ResourceLoaderContext\getDebug
getDebug()
Definition: ResourceLoaderContext.php:267
ResourceLoader\makeLoaderStateScript
static makeLoaderStateScript(ResourceLoaderContext $context, array $states)
Returns a JS call to mw.loader.state, which sets the state of modules to a given value:
Definition: ResourceLoader.php:1436
ResourceLoaderContext\getLanguage
getLanguage()
Definition: ResourceLoaderContext.php:183
ResourceLoaderModule\$versionHash
array $versionHash
Map of (context hash => cached module version hash)
Definition: ResourceLoaderModule.php:64
ResourceLoaderStartUpModule\getScript
getScript(ResourceLoaderContext $context)
Definition: ResourceLoaderStartUpModule.php:343
ResourceLoaderStartUpModule\getStoreKey
getStoreKey()
Get the localStorage key for the entire module store.
Definition: ResourceLoaderStartUpModule.php:306
ResourceLoaderModule\$name
string null $name
Module name.
Definition: ResourceLoaderModule.php:55
ResourceLoaderStartUpModule
Module for ResourceLoader initialization.
Definition: ResourceLoaderStartUpModule.php:43
ResourceLoaderStartUpModule\getBaseModules
getBaseModules()
Base modules implicitly available to all modules.
Definition: ResourceLoaderStartUpModule.php:296
ResourceLoaderContext\getSkin
getSkin()
Definition: ResourceLoaderContext.php:215
ResourceLoaderStartUpModule\getStoreVary
getStoreVary(ResourceLoaderContext $context)
Get the key on which the JavaScript module cache (mw.loader.store) will vary.
Definition: ResourceLoaderStartUpModule.php:331
ResourceLoaderStartUpModule\$groupIds
$groupIds
Definition: ResourceLoaderStartUpModule.php:46
ResourceLoaderModule
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: ResourceLoaderModule.php:39
ResourceLoaderStartUpModule\getMaxQueryLength
getMaxQueryLength()
Definition: ResourceLoaderStartUpModule.php:314
ResourceLoaderStartUpModule\getImplicitDependencies
static getImplicitDependencies(array $registryData, string $moduleName, array $handled=[])
Recursively get all explicit and implicit dependencies for to the given module.
Definition: ResourceLoaderStartUpModule.php:64
ResourceLoader\makeLoaderSourcesScript
static makeLoaderSourcesScript(ResourceLoaderContext $context, array $sources)
Format JS code which calls mw.loader.addSource() with the given parameters.
Definition: ResourceLoader.php:1546
ResourceLoaderStartUpModule\enableModuleContentVersion
enableModuleContentVersion()
Definition: ResourceLoaderStartUpModule.php:421
ResourceLoaderCircularDependencyError
Definition: ResourceLoaderCircularDependencyError.php:25
$IP
$IP
Definition: WebStart.php:49
ResourceLoaderStartUpModule\compileUnresolvedDependencies
static compileUnresolvedDependencies(array &$registryData)
Optimize the dependency tree in $this->modules.
Definition: ResourceLoaderStartUpModule.php:128
ResourceLoaderContext\encodeJson
encodeJson( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
Definition: ResourceLoaderContext.php:437
ResourceLoaderModule\getConfig
getConfig()
Definition: ResourceLoaderModule.php:234