MediaWiki  master
ResourceLoaderStartUpModule.php
Go to the documentation of this file.
1 <?php
24 
46  protected $targets = [ 'desktop', 'mobile' ];
47 
48  private $groupIds = [
49  // These reserved numbers MUST start at 0 and not skip any. These are preset
50  // for forward compatiblity so that they can be safely referenced by mediawiki.js,
51  // even when the code is cached and the order of registrations (and implicit
52  // group ids) changes between versions of the software.
53  'user' => 0,
54  'private' => 1,
55  ];
56 
62  public function getConfigSettings( ResourceLoaderContext $context ) : array {
63  $conf = $this->getConfig();
64 
70  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
71  $namespaceIds = $contLang->getNamespaceIds();
72  $caseSensitiveNamespaces = [];
73  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
74  foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
75  $namespaceIds[$contLang->lc( $name )] = $index;
76  if ( !$nsInfo->isCapitalized( $index ) ) {
77  $caseSensitiveNamespaces[] = $index;
78  }
79  }
80 
81  $illegalFileChars = $conf->get( 'IllegalFileChars' );
82 
83  // Build list of variables
84  $skin = $context->getSkin();
85 
86  // Start of supported and stable config vars (for use by extensions/gadgets).
87  $vars = [
88  'debug' => $context->getDebug(),
89  'skin' => $skin,
90  'stylepath' => $conf->get( 'StylePath' ),
91  'wgArticlePath' => $conf->get( 'ArticlePath' ),
92  'wgScriptPath' => $conf->get( 'ScriptPath' ),
93  'wgScript' => $conf->get( 'Script' ),
94  'wgSearchType' => $conf->get( 'SearchType' ),
95  'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
96  'wgServer' => $conf->get( 'Server' ),
97  'wgServerName' => $conf->get( 'ServerName' ),
98  'wgUserLanguage' => $context->getLanguage(),
99  'wgContentLanguage' => $contLang->getCode(),
100  'wgVersion' => $conf->get( 'Version' ),
101  'wgEnableAPI' => true, // Deprecated since MW 1.32
102  'wgEnableWriteAPI' => true, // Deprecated since MW 1.32
103  'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
104  'wgNamespaceIds' => $namespaceIds,
105  'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
106  'wgSiteName' => $conf->get( 'Sitename' ),
107  'wgDBname' => $conf->get( 'DBname' ),
109  'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
110  'wgCommentByteLimit' => null,
111  'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
112  'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
113  ];
114  // End of stable config vars.
115 
116  // Internal variables for use by MediaWiki core and/or ResourceLoader.
117  $vars += [
118  // @internal For mediawiki.widgets
119  'wgUrlProtocols' => wfUrlProtocols(),
120  // @internal For mediawiki.page.watch
121  // Force object to avoid "empty" associative array from
122  // becoming [] instead of {} in JS (T36604)
123  'wgActionPaths' => (object)$conf->get( 'ActionPaths' ),
124  // @internal For mediawiki.language
125  'wgTranslateNumerals' => $conf->get( 'TranslateNumerals' ),
126  // @internal For mediawiki.Title
127  'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
128  // @internal For mediawiki.cookie
129  'wgCookiePrefix' => $conf->get( 'CookiePrefix' ),
130  'wgCookieDomain' => $conf->get( 'CookieDomain' ),
131  'wgCookiePath' => $conf->get( 'CookiePath' ),
132  'wgCookieExpiration' => $conf->get( 'CookieExpiration' ),
133  // @internal For mediawiki.Title
134  'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
135  'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
136  // @internal For mediawiki.ForeignUpload
137  'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
138  'wgEnableUploads' => $conf->get( 'EnableUploads' ),
139  ];
140 
141  Hooks::run( 'ResourceLoaderGetConfigVars', [ &$vars, $skin, $conf ] );
142 
143  return $vars;
144  }
145 
155  protected static function getImplicitDependencies(
156  array $registryData,
157  string $moduleName,
158  array $handled = []
159  ) : array {
160  static $dependencyCache = [];
161 
162  // No modules will be added or changed server-side after this point,
163  // so we can safely cache parts of the tree for re-use.
164  if ( !isset( $dependencyCache[$moduleName] ) ) {
165  if ( !isset( $registryData[$moduleName] ) ) {
166  // Unknown module names are allowed here, this is only an optimisation.
167  // Checks for illegal and unknown dependencies happen as PHPUnit structure tests,
168  // and also client-side at run-time.
169  $flat = [];
170  } else {
171  $data = $registryData[$moduleName];
172  $flat = $data['dependencies'];
173 
174  // Prevent recursion
175  $handled[] = $moduleName;
176  foreach ( $data['dependencies'] as $dependency ) {
177  if ( in_array( $dependency, $handled, true ) ) {
178  // If we encounter a circular dependency, then stop the optimiser and leave the
179  // original dependencies array unmodified. Circular dependencies are not
180  // supported in ResourceLoader. Awareness of them exists here so that we can
181  // optimise the registry when it isn't broken, and otherwise transport the
182  // registry unchanged. The client will handle this further.
184  } else {
185  // Recursively add the dependencies of the dependencies
186  $flat = array_merge(
187  $flat,
188  self::getImplicitDependencies( $registryData, $dependency, $handled )
189  );
190  }
191  }
192  }
193 
194  $dependencyCache[$moduleName] = $flat;
195  }
196 
197  return $dependencyCache[$moduleName];
198  }
199 
221  public static function compileUnresolvedDependencies( array &$registryData ) {
222  foreach ( $registryData as $name => &$data ) {
223  $dependencies = $data['dependencies'];
224  try {
225  foreach ( $data['dependencies'] as $dependency ) {
226  $implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
227  $dependencies = array_diff( $dependencies, $implicitDependencies );
228  }
229  } catch ( ResourceLoaderCircularDependencyError $err ) {
230  // Leave unchanged
231  $dependencies = $data['dependencies'];
232  }
233 
234  // Rebuild keys
235  $data['dependencies'] = array_values( $dependencies );
236  }
237  }
238 
246  $resourceLoader = $context->getResourceLoader();
247  // Future developers: Use WebRequest::getRawVal() instead getVal().
248  // The getVal() method performs slow Language+UTF logic. (f303bb9360)
249  $target = $context->getRequest()->getRawVal( 'target', 'desktop' );
250  $safemode = $context->getRequest()->getRawVal( 'safemode' ) === '1';
251  // Bypass target filter if this request is Special:JavaScriptTest.
252  // To prevent misuse in production, this is only allowed if testing is enabled server-side.
253  $byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
254 
255  $out = '';
256  $states = [];
257  $registryData = [];
258  $moduleNames = $resourceLoader->getModuleNames();
259 
260  // Preload with a batch so that the below calls to getVersionHash() for each module
261  // don't require on-demand loading of more information.
262  try {
263  $resourceLoader->preloadModuleInfo( $moduleNames, $context );
264  } catch ( Exception $e ) {
265  // Don't fail the request (T152266)
266  // Also print the error in the main output
267  $resourceLoader->outputErrorAndLog( $e,
268  'Preloading module info from startup failed: {exception}',
269  [ 'exception' => $e ]
270  );
271  }
272 
273  // Get registry data
274  foreach ( $moduleNames as $name ) {
275  $module = $resourceLoader->getModule( $name );
276  $moduleTargets = $module->getTargets();
277  if (
278  ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) )
279  || ( $safemode && $module->getOrigin() > ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL )
280  ) {
281  continue;
282  }
283 
284  if ( $module instanceof ResourceLoaderStartUpModule ) {
285  // Don't register 'startup' to the client because loading it lazily or depending
286  // on it doesn't make sense, because the startup module *is* the client.
287  // Registering would be a waste of bandwidth and memory and risks somehow causing
288  // it to load a second time.
289 
290  // ATTENTION: Because of the line below, this is not going to cause infinite recursion.
291  // Think carefully before making changes to this code!
292  // The below code is going to call ResourceLoaderModule::getVersionHash() for every module.
293  // For StartUpModule (this module) the hash is computed based on the manifest content,
294  // which is the very thing we are computing right here. As such, this must skip iterating
295  // over 'startup' itself.
296  continue;
297  }
298 
299  try {
300  $versionHash = $module->getVersionHash( $context );
301  } catch ( Exception $e ) {
302  // Don't fail the request (T152266)
303  // Also print the error in the main output
304  $resourceLoader->outputErrorAndLog( $e,
305  'Calculating version for "{module}" failed: {exception}',
306  [
307  'module' => $name,
308  'exception' => $e,
309  ]
310  );
311  $versionHash = '';
312  $states[$name] = 'error';
313  }
314 
315  if ( $versionHash !== '' && strlen( $versionHash ) !== ResourceLoader::HASH_LENGTH ) {
316  $e = new RuntimeException( "Badly formatted module version hash" );
317  $resourceLoader->outputErrorAndLog( $e,
318  "Module '{module}' produced an invalid version hash: '{version}'.",
319  [
320  'module' => $name,
321  'version' => $versionHash,
322  ]
323  );
324  // Module implementation either broken or deviated from ResourceLoader::makeHash
325  // Asserted by tests/phpunit/structure/ResourcesTest.
326  $versionHash = ResourceLoader::makeHash( $versionHash );
327  }
328 
329  $skipFunction = $module->getSkipFunction();
330  if ( $skipFunction !== null && !$context->getDebug() ) {
331  $skipFunction = ResourceLoader::filter( 'minify-js', $skipFunction );
332  }
333 
334  $registryData[$name] = [
335  'version' => $versionHash,
336  'dependencies' => $module->getDependencies( $context ),
337  'group' => $this->getGroupId( $module->getGroup() ),
338  'source' => $module->getSource(),
339  'skip' => $skipFunction,
340  ];
341  }
342 
343  self::compileUnresolvedDependencies( $registryData );
344 
345  // Register sources
346  $out .= ResourceLoader::makeLoaderSourcesScript( $context, $resourceLoader->getSources() );
347 
348  // Figure out the different call signatures for mw.loader.register
349  $registrations = [];
350  foreach ( $registryData as $name => $data ) {
351  // Call mw.loader.register(name, version, dependencies, group, source, skip)
352  $registrations[] = [
353  $name,
354  $data['version'],
355  $data['dependencies'],
356  $data['group'],
357  // Swap default (local) for null
358  $data['source'] === 'local' ? null : $data['source'],
359  $data['skip']
360  ];
361  }
362 
363  // Register modules
364  $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $context, $registrations );
365 
366  if ( $states ) {
367  $out .= "\n" . ResourceLoader::makeLoaderStateScript( $context, $states );
368  }
369 
370  return $out;
371  }
372 
373  private function getGroupId( $groupName ) : ?int {
374  if ( $groupName === null ) {
375  return null;
376  }
377 
378  if ( !array_key_exists( $groupName, $this->groupIds ) ) {
379  $this->groupIds[$groupName] = count( $this->groupIds );
380  }
381 
382  return $this->groupIds[$groupName];
383  }
384 
390  private function getBaseModules() : array {
391  return [ 'jquery', 'mediawiki.base' ];
392  }
393 
400  private function getStoreKey() : string {
401  return 'MediaWikiModuleStore:' . $this->getConfig()->get( 'DBname' );
402  }
403 
410  private function getStoreVary( ResourceLoaderContext $context ) : string {
411  return implode( ':', [
412  $context->getSkin(),
413  $this->getConfig()->get( 'ResourceLoaderStorageVersion' ),
414  $context->getLanguage(),
415  ] );
416  }
417 
422  public function getScript( ResourceLoaderContext $context ) : string {
423  global $IP;
424  $conf = $this->getConfig();
425 
426  if ( $context->getOnly() !== 'scripts' ) {
427  return '/* Requires only=script */';
428  }
429 
430  $startupCode = file_get_contents( "$IP/resources/src/startup/startup.js" );
431 
432  // The files read here MUST be kept in sync with maintenance/jsduck/eg-iframe.html,
433  // and MUST be considered by 'fileHashes' in StartUpModule::getDefinitionSummary().
434  $mwLoaderCode = file_get_contents( "$IP/resources/src/startup/mediawiki.js" ) .
435  file_get_contents( "$IP/resources/src/startup/mediawiki.requestIdleCallback.js" );
436  if ( $context->getDebug() ) {
437  $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/mediawiki.log.js" );
438  }
439  if ( $conf->get( 'ResourceLoaderEnableJSProfiler' ) ) {
440  $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/profiler.js" );
441  }
442 
443  // Perform replacements for mediawiki.js
444  $mwLoaderPairs = [
445  '$VARS.reqBase' => $context->encodeJson( $context->getReqBase() ),
446  '$VARS.baseModules' => $context->encodeJson( $this->getBaseModules() ),
447  '$VARS.maxQueryLength' => $context->encodeJson(
448  $conf->get( 'ResourceLoaderMaxQueryLength' )
449  ),
450  // The client-side module cache can be disabled by site configuration.
451  // It is also always disabled in debug mode.
452  '$VARS.storeEnabled' => $context->encodeJson(
453  $conf->get( 'ResourceLoaderStorageEnabled' ) && !$context->getDebug()
454  ),
455  '$VARS.wgLegacyJavaScriptGlobals' => $context->encodeJson(
456  $conf->get( 'LegacyJavaScriptGlobals' )
457  ),
458  '$VARS.storeKey' => $context->encodeJson( $this->getStoreKey() ),
459  '$VARS.storeVary' => $context->encodeJson( $this->getStoreVary( $context ) ),
460  '$VARS.groupUser' => $context->encodeJson( $this->getGroupId( 'user' ) ),
461  '$VARS.groupPrivate' => $context->encodeJson( $this->getGroupId( 'private' ) ),
462  ];
463  $profilerStubs = [
464  '$CODE.profileExecuteStart();' => 'mw.loader.profiler.onExecuteStart( module );',
465  '$CODE.profileExecuteEnd();' => 'mw.loader.profiler.onExecuteEnd( module );',
466  '$CODE.profileScriptStart();' => 'mw.loader.profiler.onScriptStart( module );',
467  '$CODE.profileScriptEnd();' => 'mw.loader.profiler.onScriptEnd( module );',
468  ];
469  if ( $conf->get( 'ResourceLoaderEnableJSProfiler' ) ) {
470  // When profiling is enabled, insert the calls.
471  $mwLoaderPairs += $profilerStubs;
472  } else {
473  // When disabled (by default), insert nothing.
474  $mwLoaderPairs += array_fill_keys( array_keys( $profilerStubs ), '' );
475  }
476  $mwLoaderCode = strtr( $mwLoaderCode, $mwLoaderPairs );
477 
478  // Perform string replacements for startup.js
479  $pairs = [
480  '$VARS.configuration' => $context->encodeJson(
481  $this->getConfigSettings( $context )
482  ),
483  // Raw JavaScript code (not JSON)
484  '$CODE.registrations();' => trim( $this->getModuleRegistrations( $context ) ),
485  '$CODE.defineLoader();' => $mwLoaderCode,
486  ];
487  $startupCode = strtr( $startupCode, $pairs );
488 
489  return $startupCode;
490  }
491 
495  public function supportsURLLoading() : bool {
496  return false;
497  }
498 
502  public function enableModuleContentVersion() : bool {
503  // Enabling this means that ResourceLoader::getVersionHash will simply call getScript()
504  // and hash it to determine the version (as used by E-Tag HTTP response header).
505  return true;
506  }
507 }
ResourceLoaderStartUpModule\$targets
$targets
Definition: ResourceLoaderStartUpModule.php:46
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:33
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
ResourceLoaderStartUpModule\getConfigSettings
getConfigSettings(ResourceLoaderContext $context)
Definition: ResourceLoaderStartUpModule.php:62
ResourceLoaderStartUpModule\getModuleRegistrations
getModuleRegistrations(ResourceLoaderContext $context)
Get registration code for all modules.
Definition: ResourceLoaderStartUpModule.php:245
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:129
$resourceLoader
$resourceLoader
Definition: load.php:39
ResourceLoaderContext\getOnly
getOnly()
Definition: ResourceLoaderContext.php:241
Title\convertByteClassToUnicodeClass
static convertByteClassToUnicodeClass( $byteClass)
Utility method for converting a character sequence from bytes to Unicode.
Definition: Title.php:706
ResourceLoaderStartUpModule\getGroupId
getGroupId( $groupName)
Definition: ResourceLoaderStartUpModule.php:373
ResourceLoaderStartUpModule\supportsURLLoading
supportsURLLoading()
Definition: ResourceLoaderStartUpModule.php:495
WikiMap\getWikiIdFromDbDomain
static getWikiIdFromDbDomain( $domain)
Get the wiki ID of a database domain.
Definition: WikiMap.php:269
ResourceLoaderContext\getLanguage
getLanguage()
Definition: ResourceLoaderContext.php:154
ResourceLoaderModule\$versionHash
array $versionHash
Map of (context hash => cached module version hash)
Definition: ResourceLoaderModule.php:62
ResourceLoaderStartUpModule\getScript
getScript(ResourceLoaderContext $context)
Definition: ResourceLoaderStartUpModule.php:422
wfUrlProtocols
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
Definition: GlobalFunctions.php:719
ResourceLoaderStartUpModule\getStoreKey
getStoreKey()
Get the localStorage key for the entire module store.
Definition: ResourceLoaderStartUpModule.php:400
ResourceLoaderModule\$name
string null $name
Module name.
Definition: ResourceLoaderModule.php:53
ResourceLoaderStartUpModule
Module for ResourceLoader initialization.
Definition: ResourceLoaderStartUpModule.php:45
ResourceLoaderStartUpModule\getBaseModules
getBaseModules()
Base modules implicitly available to all modules.
Definition: ResourceLoaderStartUpModule.php:390
$context
$context
Definition: load.php:40
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:37
ResourceLoaderStartUpModule\getStoreVary
getStoreVary(ResourceLoaderContext $context)
Get the key on which the JavaScript module cache (mw.loader.store) will vary.
Definition: ResourceLoaderStartUpModule.php:410
ResourceLoaderStartUpModule\$groupIds
$groupIds
Definition: ResourceLoaderStartUpModule.php:48
ResourceLoaderModule
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: ResourceLoaderModule.php:37
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:155
ResourceLoaderStartUpModule\enableModuleContentVersion
enableModuleContentVersion()
Definition: ResourceLoaderStartUpModule.php:502
Title\legalChars
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:692
ResourceLoaderCircularDependencyError
Definition: ResourceLoaderCircularDependencyError.php:25
$IP
$IP
Definition: WebStart.php:41
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
ResourceLoaderStartUpModule\compileUnresolvedDependencies
static compileUnresolvedDependencies(array &$registryData)
Optimize the dependency tree in $this->modules.
Definition: ResourceLoaderStartUpModule.php:221
ResourceLoaderModule\getConfig
getConfig()
Definition: ResourceLoaderModule.php:194