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 
130  public static function compileUnresolvedDependencies( array &$registryData ) : void {
131  foreach ( $registryData as $name => &$data ) {
132  $dependencies = $data['dependencies'];
133  try {
134  foreach ( $data['dependencies'] as $dependency ) {
135  $implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
136  $dependencies = array_diff( $dependencies, $implicitDependencies );
137  }
138  } catch ( ResourceLoaderCircularDependencyError $err ) {
139  // Leave unchanged
140  $dependencies = $data['dependencies'];
141  }
142 
143  // Rebuild keys
144  $data['dependencies'] = array_values( $dependencies );
145  }
146  }
147 
154  public function getModuleRegistrations( ResourceLoaderContext $context ) : string {
155  $resourceLoader = $context->getResourceLoader();
156  // Future developers: Use WebRequest::getRawVal() instead getVal().
157  // The getVal() method performs slow Language+UTF logic. (f303bb9360)
158  $target = $context->getRequest()->getRawVal( 'target', 'desktop' );
159  $safemode = $context->getRequest()->getRawVal( 'safemode' ) === '1';
160  // Bypass target filter if this request is Special:JavaScriptTest.
161  // To prevent misuse in production, this is only allowed if testing is enabled server-side.
162  $byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
163 
164  $out = '';
165  $states = [];
166  $registryData = [];
167  $moduleNames = $resourceLoader->getModuleNames();
168 
169  // Preload with a batch so that the below calls to getVersionHash() for each module
170  // don't require on-demand loading of more information.
171  try {
172  $resourceLoader->preloadModuleInfo( $moduleNames, $context );
173  } catch ( Exception $e ) {
174  // Don't fail the request (T152266)
175  // Also print the error in the main output
176  $resourceLoader->outputErrorAndLog( $e,
177  'Preloading module info from startup failed: {exception}',
178  [ 'exception' => $e ]
179  );
180  }
181 
182  // Get registry data
183  foreach ( $moduleNames as $name ) {
184  $module = $resourceLoader->getModule( $name );
185  $moduleTargets = $module->getTargets();
186  if (
187  ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) )
188  || ( $safemode && $module->getOrigin() > ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL )
189  ) {
190  continue;
191  }
192 
193  if ( $module instanceof ResourceLoaderStartUpModule ) {
194  // Don't register 'startup' to the client because loading it lazily or depending
195  // on it doesn't make sense, because the startup module *is* the client.
196  // Registering would be a waste of bandwidth and memory and risks somehow causing
197  // it to load a second time.
198 
199  // ATTENTION: Because of the line below, this is not going to cause infinite recursion.
200  // Think carefully before making changes to this code!
201  // The below code is going to call ResourceLoaderModule::getVersionHash() for every module.
202  // For StartUpModule (this module) the hash is computed based on the manifest content,
203  // which is the very thing we are computing right here. As such, this must skip iterating
204  // over 'startup' itself.
205  continue;
206  }
207 
208  try {
209  $versionHash = $module->getVersionHash( $context );
210  } catch ( Exception $e ) {
211  // Don't fail the request (T152266)
212  // Also print the error in the main output
213  $resourceLoader->outputErrorAndLog( $e,
214  'Calculating version for "{module}" failed: {exception}',
215  [
216  'module' => $name,
217  'exception' => $e,
218  ]
219  );
220  $versionHash = '';
221  $states[$name] = 'error';
222  }
223 
224  if ( $versionHash !== '' && strlen( $versionHash ) !== ResourceLoader::HASH_LENGTH ) {
225  $e = new RuntimeException( "Badly formatted module version hash" );
226  $resourceLoader->outputErrorAndLog( $e,
227  "Module '{module}' produced an invalid version hash: '{version}'.",
228  [
229  'module' => $name,
230  'version' => $versionHash,
231  ]
232  );
233  // Module implementation either broken or deviated from ResourceLoader::makeHash
234  // Asserted by tests/phpunit/structure/ResourcesTest.
236  }
237 
238  $skipFunction = $module->getSkipFunction();
239  if ( $skipFunction !== null && !$context->getDebug() ) {
240  $skipFunction = ResourceLoader::filter( 'minify-js', $skipFunction );
241  }
242 
243  $registryData[$name] = [
244  'version' => $versionHash,
245  'dependencies' => $module->getDependencies( $context ),
246  'group' => $this->getGroupId( $module->getGroup() ),
247  'source' => $module->getSource(),
248  'skip' => $skipFunction,
249  ];
250  }
251 
252  self::compileUnresolvedDependencies( $registryData );
253 
254  // Register sources
255  $out .= ResourceLoader::makeLoaderSourcesScript( $context, $resourceLoader->getSources() );
256 
257  // Figure out the different call signatures for mw.loader.register
258  $registrations = [];
259  foreach ( $registryData as $name => $data ) {
260  // Call mw.loader.register(name, version, dependencies, group, source, skip)
261  $registrations[] = [
262  $name,
263  $data['version'],
264  $data['dependencies'],
265  $data['group'],
266  // Swap default (local) for null
267  $data['source'] === 'local' ? null : $data['source'],
268  $data['skip']
269  ];
270  }
271 
272  // Register modules
273  $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $context, $registrations );
274 
275  if ( $states ) {
276  $out .= "\n" . ResourceLoader::makeLoaderStateScript( $context, $states );
277  }
278 
279  return $out;
280  }
281 
282  private function getGroupId( $groupName ) : ?int {
283  if ( $groupName === null ) {
284  return null;
285  }
286 
287  if ( !array_key_exists( $groupName, $this->groupIds ) ) {
288  $this->groupIds[$groupName] = count( $this->groupIds );
289  }
290 
291  return $this->groupIds[$groupName];
292  }
293 
299  private function getBaseModules() : array {
300  return [ 'jquery', 'mediawiki.base' ];
301  }
302 
309  private function getStoreKey() : string {
310  return 'MediaWikiModuleStore:' . $this->getConfig()->get( 'DBname' );
311  }
312 
317  private function getMaxQueryLength() : int {
318  $len = $this->getConfig()->get( 'ResourceLoaderMaxQueryLength' );
319  // - Ignore -1, which in MW 1.34 and earlier was used to mean "unlimited".
320  // - Ignore invalid values, e.g. non-int or other negative values.
321  if ( $len === false || $len < 0 ) {
322  // Default
323  $len = 2000;
324  }
325  return $len;
326  }
327 
334  private function getStoreVary( ResourceLoaderContext $context ) : string {
335  return implode( ':', [
336  $context->getSkin(),
337  $this->getConfig()->get( 'ResourceLoaderStorageVersion' ),
338  $context->getLanguage(),
339  ] );
340  }
341 
346  public function getScript( ResourceLoaderContext $context ) : string {
347  global $IP;
348  $conf = $this->getConfig();
349 
350  if ( $context->getOnly() !== 'scripts' ) {
351  return '/* Requires only=scripts */';
352  }
353 
354  $startupCode = file_get_contents( "$IP/resources/src/startup/startup.js" );
355 
356  // The files read here MUST be kept in sync with maintenance/jsduck/eg-iframe.html,
357  // and MUST be considered by 'fileHashes' in StartUpModule::getDefinitionSummary().
358  $mwLoaderCode = file_get_contents( "$IP/resources/src/startup/mediawiki.js" ) .
359  file_get_contents( "$IP/resources/src/startup/mediawiki.requestIdleCallback.js" );
360  if ( $context->getDebug() ) {
361  $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/mediawiki.log.js" );
362  }
363  if ( $conf->get( 'ResourceLoaderEnableJSProfiler' ) ) {
364  $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/profiler.js" );
365  }
366 
367  // Perform replacements for mediawiki.js
368  $mwLoaderPairs = [
369  // This should always be an object, even if the base vars are empty
370  // (such as when using the default lang/skin).
371  '$VARS.reqBase' => $context->encodeJson( (object)$context->getReqBase() ),
372  '$VARS.baseModules' => $context->encodeJson( $this->getBaseModules() ),
373  '$VARS.maxQueryLength' => $context->encodeJson( $this->getMaxQueryLength() ),
374  // The client-side module cache can be disabled by site configuration.
375  // It is also always disabled in debug mode.
376  '$VARS.storeEnabled' => $context->encodeJson(
377  $conf->get( 'ResourceLoaderStorageEnabled' ) && !$context->getDebug()
378  ),
379  '$VARS.wgLegacyJavaScriptGlobals' => $context->encodeJson(
380  $conf->get( 'LegacyJavaScriptGlobals' )
381  ),
382  '$VARS.storeKey' => $context->encodeJson( $this->getStoreKey() ),
383  '$VARS.storeVary' => $context->encodeJson( $this->getStoreVary( $context ) ),
384  '$VARS.groupUser' => $context->encodeJson( $this->getGroupId( 'user' ) ),
385  '$VARS.groupPrivate' => $context->encodeJson( $this->getGroupId( 'private' ) ),
386  ];
387  $profilerStubs = [
388  '$CODE.profileExecuteStart();' => 'mw.loader.profiler.onExecuteStart( module );',
389  '$CODE.profileExecuteEnd();' => 'mw.loader.profiler.onExecuteEnd( module );',
390  '$CODE.profileScriptStart();' => 'mw.loader.profiler.onScriptStart( module );',
391  '$CODE.profileScriptEnd();' => 'mw.loader.profiler.onScriptEnd( module );',
392  ];
393  if ( $conf->get( 'ResourceLoaderEnableJSProfiler' ) ) {
394  // When profiling is enabled, insert the calls.
395  $mwLoaderPairs += $profilerStubs;
396  } else {
397  // When disabled (by default), insert nothing.
398  $mwLoaderPairs += array_fill_keys( array_keys( $profilerStubs ), '' );
399  }
400  $mwLoaderCode = strtr( $mwLoaderCode, $mwLoaderPairs );
401 
402  // Perform string replacements for startup.js
403  $pairs = [
404  // Raw JavaScript code (not JSON)
405  '$CODE.registrations();' => trim( $this->getModuleRegistrations( $context ) ),
406  '$CODE.defineLoader();' => $mwLoaderCode,
407  ];
408  $startupCode = strtr( $startupCode, $pairs );
409 
410  return $startupCode;
411  }
412 
416  public function supportsURLLoading() : bool {
417  return false;
418  }
419 
423  public function enableModuleContentVersion() : bool {
424  // Enabling this means that ResourceLoader::getVersionHash will simply call getScript()
425  // and hash it to determine the version (as used by E-Tag HTTP response header).
426  return true;
427  }
428 }
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:180
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:33
ResourceLoaderContext\getReqBase
getReqBase()
Get the request base parameters, omitting any defaults.
Definition: ResourceLoaderContext.php:383
ResourceLoaderStartUpModule\getModuleRegistrations
getModuleRegistrations(ResourceLoaderContext $context)
Get registration code for all modules.
Definition: ResourceLoaderStartUpModule.php:154
ResourceLoaderContext\getResourceLoader
getResourceLoader()
Definition: ResourceLoaderContext.php:123
ResourceLoader\makeLoaderRegisterScript
static makeLoaderRegisterScript(ResourceLoaderContext $context, array $modules)
Format JS code which calls mw.loader.register() with the given parameters.
Definition: ResourceLoader.php:1554
ResourceLoaderContext\getOnly
getOnly()
Definition: ResourceLoaderContext.php:245
ResourceLoaderStartUpModule\getGroupId
getGroupId( $groupName)
Definition: ResourceLoaderStartUpModule.php:282
ResourceLoaderStartUpModule\supportsURLLoading
supportsURLLoading()
Definition: ResourceLoaderStartUpModule.php:416
ResourceLoaderContext\getRequest
getRequest()
Definition: ResourceLoaderContext.php:138
ResourceLoader\makeHash
static makeHash( $value)
Create a hash for module versioning purposes.
Definition: ResourceLoader.php:732
ResourceLoaderContext\getDebug
getDebug()
Definition: ResourceLoaderContext.php:238
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:1484
ResourceLoaderContext\getLanguage
getLanguage()
Definition: ResourceLoaderContext.php:156
ResourceLoaderModule\$versionHash
array $versionHash
Map of (context hash => cached module version hash)
Definition: ResourceLoaderModule.php:64
ResourceLoaderStartUpModule\getScript
getScript(ResourceLoaderContext $context)
Definition: ResourceLoaderStartUpModule.php:346
ResourceLoaderStartUpModule\getStoreKey
getStoreKey()
Get the localStorage key for the entire module store.
Definition: ResourceLoaderStartUpModule.php:309
ResourceLoaderModule\$name
string null $name
Module name.
Definition: ResourceLoaderModule.php:55
ResourceLoaderStartUpModule
Module for ResourceLoader initialization.
Definition: ResourceLoaderStartUpModule.php:43
ResourceLoader\HASH_LENGTH
const HASH_LENGTH
Definition: ResourceLoader.php:668
ResourceLoaderStartUpModule\getBaseModules
getBaseModules()
Base modules implicitly available to all modules.
Definition: ResourceLoaderStartUpModule.php:299
ResourceLoaderContext\getSkin
getSkin()
Definition: ResourceLoaderContext.php:188
ResourceLoaderStartUpModule\getStoreVary
getStoreVary(ResourceLoaderContext $context)
Get the key on which the JavaScript module cache (mw.loader.store) will vary.
Definition: ResourceLoaderStartUpModule.php:334
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:317
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:1596
ResourceLoaderStartUpModule\enableModuleContentVersion
enableModuleContentVersion()
Definition: ResourceLoaderStartUpModule.php:423
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:130
ResourceLoaderContext\encodeJson
encodeJson( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
Definition: ResourceLoaderContext.php:405
ResourceLoaderModule\getConfig
getConfig()
Definition: ResourceLoaderModule.php:218