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 ) {
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 
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.
235  $versionHash = ResourceLoader::makeHash( $versionHash );
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 
319  private function getStoreVary( ResourceLoaderContext $context ) : string {
320  return implode( ':', [
321  $context->getSkin(),
322  $this->getConfig()->get( 'ResourceLoaderStorageVersion' ),
323  $context->getLanguage(),
324  ] );
325  }
326 
331  public function getScript( ResourceLoaderContext $context ) : string {
332  global $IP;
333  $conf = $this->getConfig();
334 
335  if ( $context->getOnly() !== 'scripts' ) {
336  return '/* Requires only=scripts */';
337  }
338 
339  $startupCode = file_get_contents( "$IP/resources/src/startup/startup.js" );
340 
341  // The files read here MUST be kept in sync with maintenance/jsduck/eg-iframe.html,
342  // and MUST be considered by 'fileHashes' in StartUpModule::getDefinitionSummary().
343  $mwLoaderCode = file_get_contents( "$IP/resources/src/startup/mediawiki.js" ) .
344  file_get_contents( "$IP/resources/src/startup/mediawiki.requestIdleCallback.js" );
345  if ( $context->getDebug() ) {
346  $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/mediawiki.log.js" );
347  }
348  if ( $conf->get( 'ResourceLoaderEnableJSProfiler' ) ) {
349  $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/profiler.js" );
350  }
351 
352  // Perform replacements for mediawiki.js
353  $mwLoaderPairs = [
354  '$VARS.reqBase' => $context->encodeJson( $context->getReqBase() ),
355  '$VARS.baseModules' => $context->encodeJson( $this->getBaseModules() ),
356  '$VARS.maxQueryLength' => $context->encodeJson(
357  $conf->get( 'ResourceLoaderMaxQueryLength' )
358  ),
359  // The client-side module cache can be disabled by site configuration.
360  // It is also always disabled in debug mode.
361  '$VARS.storeEnabled' => $context->encodeJson(
362  $conf->get( 'ResourceLoaderStorageEnabled' ) && !$context->getDebug()
363  ),
364  '$VARS.wgLegacyJavaScriptGlobals' => $context->encodeJson(
365  $conf->get( 'LegacyJavaScriptGlobals' )
366  ),
367  '$VARS.storeKey' => $context->encodeJson( $this->getStoreKey() ),
368  '$VARS.storeVary' => $context->encodeJson( $this->getStoreVary( $context ) ),
369  '$VARS.groupUser' => $context->encodeJson( $this->getGroupId( 'user' ) ),
370  '$VARS.groupPrivate' => $context->encodeJson( $this->getGroupId( 'private' ) ),
371  ];
372  $profilerStubs = [
373  '$CODE.profileExecuteStart();' => 'mw.loader.profiler.onExecuteStart( module );',
374  '$CODE.profileExecuteEnd();' => 'mw.loader.profiler.onExecuteEnd( module );',
375  '$CODE.profileScriptStart();' => 'mw.loader.profiler.onScriptStart( module );',
376  '$CODE.profileScriptEnd();' => 'mw.loader.profiler.onScriptEnd( module );',
377  ];
378  if ( $conf->get( 'ResourceLoaderEnableJSProfiler' ) ) {
379  // When profiling is enabled, insert the calls.
380  $mwLoaderPairs += $profilerStubs;
381  } else {
382  // When disabled (by default), insert nothing.
383  $mwLoaderPairs += array_fill_keys( array_keys( $profilerStubs ), '' );
384  }
385  $mwLoaderCode = strtr( $mwLoaderCode, $mwLoaderPairs );
386 
387  // Perform string replacements for startup.js
388  $pairs = [
389  // Raw JavaScript code (not JSON)
390  '$CODE.registrations();' => trim( $this->getModuleRegistrations( $context ) ),
391  '$CODE.defineLoader();' => $mwLoaderCode,
392  ];
393  $startupCode = strtr( $startupCode, $pairs );
394 
395  return $startupCode;
396  }
397 
401  public function supportsURLLoading() : bool {
402  return false;
403  }
404 
408  public function enableModuleContentVersion() : bool {
409  // Enabling this means that ResourceLoader::getVersionHash will simply call getScript()
410  // and hash it to determine the version (as used by E-Tag HTTP response header).
411  return true;
412  }
413 }
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
ResourceLoaderStartUpModule\getModuleRegistrations
getModuleRegistrations(ResourceLoaderContext $context)
Get registration code for all modules.
Definition: ResourceLoaderStartUpModule.php:154
$resourceLoader
$resourceLoader
Definition: load.php:42
ResourceLoaderContext\getOnly
getOnly()
Definition: ResourceLoaderContext.php:243
ResourceLoaderStartUpModule\getGroupId
getGroupId( $groupName)
Definition: ResourceLoaderStartUpModule.php:282
ResourceLoaderStartUpModule\supportsURLLoading
supportsURLLoading()
Definition: ResourceLoaderStartUpModule.php:401
ResourceLoaderContext\getLanguage
getLanguage()
Definition: ResourceLoaderContext.php:154
ResourceLoaderModule\$versionHash
array $versionHash
Map of (context hash => cached module version hash)
Definition: ResourceLoaderModule.php:61
ResourceLoaderStartUpModule\getScript
getScript(ResourceLoaderContext $context)
Definition: ResourceLoaderStartUpModule.php:331
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:52
ResourceLoaderStartUpModule
Module for ResourceLoader initialization.
Definition: ResourceLoaderStartUpModule.php:43
ResourceLoaderStartUpModule\getBaseModules
getBaseModules()
Base modules implicitly available to all modules.
Definition: ResourceLoaderStartUpModule.php:299
$context
$context
Definition: load.php:43
ResourceLoaderStartUpModule\getStoreVary
getStoreVary(ResourceLoaderContext $context)
Get the key on which the JavaScript module cache (mw.loader.store) will vary.
Definition: ResourceLoaderStartUpModule.php:319
ResourceLoaderStartUpModule\$groupIds
$groupIds
Definition: ResourceLoaderStartUpModule.php:46
ResourceLoaderModule
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: ResourceLoaderModule.php:36
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
ResourceLoaderStartUpModule\enableModuleContentVersion
enableModuleContentVersion()
Definition: ResourceLoaderStartUpModule.php:408
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
ResourceLoaderModule\getConfig
getConfig()
Definition: ResourceLoaderModule.php:210