MediaWiki  master
ResourceLoaderClientHtml.php
Go to the documentation of this file.
1 <?php
21 use Wikimedia\WrappedString;
22 use Wikimedia\WrappedStringList;
23 
31 
33  private $context;
34 
36  private $resourceLoader;
37 
39  private $options;
40 
42  private $config = [];
43 
45  private $modules = [];
46 
48  private $moduleStyles = [];
49 
51  private $exemptStates = [];
52 
54  private $data;
55 
63  public function __construct( ResourceLoaderContext $context, array $options = [] ) {
64  $this->context = $context;
65  $this->resourceLoader = $context->getResourceLoader();
66  $this->options = $options + [
67  'target' => null,
68  'safemode' => null,
69  'nonce' => null,
70  ];
71  }
72 
78  public function setConfig( array $vars ) {
79  foreach ( $vars as $key => $value ) {
80  $this->config[$key] = $value;
81  }
82  }
83 
89  public function setModules( array $modules ) {
90  $this->modules = $modules;
91  }
92 
98  public function setModuleStyles( array $modules ) {
99  $this->moduleStyles = $modules;
100  }
101 
109  public function setExemptStates( array $states ) {
110  $this->exemptStates = $states;
111  }
112 
113  private function getData() : array {
114  if ( $this->data ) {
115  // @codeCoverageIgnoreStart
116  return $this->data;
117  // @codeCoverageIgnoreEnd
118  }
119 
120  $rl = $this->resourceLoader;
121  $data = [
122  'states' => [
123  // moduleName => state
124  ],
125  'general' => [],
126  'styles' => [],
127  // Embedding for private modules
128  'embed' => [
129  'styles' => [],
130  'general' => [],
131  ],
132  // Deprecations for style-only modules
133  'styleDeprecations' => [],
134  ];
135 
136  foreach ( $this->modules as $name ) {
137  $module = $rl->getModule( $name );
138  if ( !$module ) {
139  continue;
140  }
141 
142  $group = $module->getGroup();
143  $context = $this->getContext( $group, ResourceLoaderModule::TYPE_COMBINED );
144  $shouldEmbed = $module->shouldEmbedModule( $this->context );
145 
146  if ( ( $group === 'user' || $shouldEmbed ) && $module->isKnownEmpty( $context ) ) {
147  // This is a user-specific or embedded module, which means its output
148  // can be specific to the current page or user. As such, we can optimise
149  // the way we load it based on the current version of the module.
150  // Avoid needless embed for empty module, preset ready state.
151  $data['states'][$name] = 'ready';
152  } elseif ( $group === 'user' || $shouldEmbed ) {
153  // - For group=user: We need to provide a pre-generated load.php
154  // url to the client that has the 'user' and 'version' parameters
155  // filled in. Without this, the client would wrongly use the static
156  // version hash, per T64602.
157  // - For shouldEmbed=true: Embed via mw.loader.implement, per T36907.
158  $data['embed']['general'][] = $name;
159  // Avoid duplicate request from mw.loader
160  $data['states'][$name] = 'loading';
161  } else {
162  // Load via mw.loader.load()
163  $data['general'][] = $name;
164  }
165  }
166 
167  foreach ( $this->moduleStyles as $name ) {
168  $module = $rl->getModule( $name );
169  if ( !$module ) {
170  continue;
171  }
172 
173  if ( $module->getType() !== ResourceLoaderModule::LOAD_STYLES ) {
174  $logger = $rl->getLogger();
175  $logger->error( 'Unexpected general module "{module}" in styles queue.', [
176  'module' => $name,
177  ] );
178  continue;
179  }
180 
181  // Stylesheet doesn't trigger mw.loader callback.
182  // Set "ready" state to allow script modules to depend on this module (T87871).
183  // And to avoid duplicate requests at run-time from mw.loader.
184  $data['states'][$name] = 'ready';
185 
186  $group = $module->getGroup();
187  $context = $this->getContext( $group, ResourceLoaderModule::TYPE_STYLES );
188  if ( $module->shouldEmbedModule( $this->context ) ) {
189  // Avoid needless embed for private embeds we know are empty.
190  // (Set "ready" state directly instead, which we do a few lines above.)
191  if ( !$module->isKnownEmpty( $context ) ) {
192  // Embed via <style> element
193  $data['embed']['styles'][] = $name;
194  }
195  // For other style modules, always request them, regardless of whether they are
196  // currently known to be empty. Because:
197  // 1. Those modules are requested in batch, so there is no extra request overhead
198  // or extra HTML element to be avoided.
199  // 2. Checking isKnownEmpty for those can be expensive and slow down page view
200  // generation (T230260).
201  // 3. We don't want cached HTML to vary on the current state of a module.
202  // If the module becomes non-empty a few minutes later, it should start working
203  // on cached HTML without requiring a purge.
204  //
205  // But, user-specific modules:
206  // * ... are used on page views not publicly cached.
207  // * ... are in their own group and thus a require a request we can avoid
208  // * ... have known-empty status preloaded by ResourceLoader.
209  } elseif ( $group !== 'user' || !$module->isKnownEmpty( $context ) ) {
210  // Load from load.php?only=styles via <link rel=stylesheet>
211  $data['styles'][] = $name;
212  }
213  $deprecation = $module->getDeprecationInformation( $context );
214  if ( $deprecation ) {
215  $data['styleDeprecations'][] = $deprecation;
216  }
217  }
218 
219  return $data;
220  }
221 
225  public function getDocumentAttributes() {
226  return [ 'class' => 'client-nojs' ];
227  }
228 
244  public function getHeadHtml( $nojsClass = null ) {
245  $nonce = $this->options['nonce'];
246  $data = $this->getData();
247  $chunks = [];
248 
249  // Change "client-nojs" class to client-js. This allows easy toggling of UI components.
250  // This must happen synchronously on every page view to avoid flashes of wrong content.
251  // See also startup/startup.js.
252  $nojsClass = $nojsClass ?? $this->getDocumentAttributes()['class'];
253  $jsClass = preg_replace( '/(^|\s)client-nojs(\s|$)/', '$1client-js$2', $nojsClass );
254  $jsClassJson = $this->context->encodeJson( $jsClass );
255  $script = <<<JAVASCRIPT
256 document.documentElement.className = {$jsClassJson};
257 JAVASCRIPT;
258 
259  // Inline script: Declare mw.config variables for this page.
260  if ( $this->config ) {
261  $confJson = $this->context->encodeJson( $this->config );
262  $script .= <<<JAVASCRIPT
263 RLCONF = {$confJson};
264 JAVASCRIPT;
265  }
266 
267  // Inline script: Declare initial module states for this page.
268  $states = array_merge( $this->exemptStates, $data['states'] );
269  if ( $states ) {
270  $stateJson = $this->context->encodeJson( $states );
271  $script .= <<<JAVASCRIPT
272 RLSTATE = {$stateJson};
273 JAVASCRIPT;
274  }
275 
276  // Inline script: Declare general modules to load on this page.
277  if ( $data['general'] ) {
278  $pageModulesJson = $this->context->encodeJson( $data['general'] );
279  $script .= <<<JAVASCRIPT
280 RLPAGEMODULES = {$pageModulesJson};
281 JAVASCRIPT;
282  }
283 
284  if ( !$this->context->getDebug() ) {
285  $script = ResourceLoader::filter( 'minify-js', $script, [ 'cache' => false ] );
286  }
287 
288  $chunks[] = Html::inlineScript( $script, $nonce );
289 
290  // Inline RLQ: Embedded modules
291  if ( $data['embed']['general'] ) {
292  $chunks[] = $this->getLoad(
293  $data['embed']['general'],
294  ResourceLoaderModule::TYPE_COMBINED,
295  $nonce
296  );
297  }
298 
299  // External stylesheets (only=styles)
300  if ( $data['styles'] ) {
301  $chunks[] = $this->getLoad(
302  $data['styles'],
303  ResourceLoaderModule::TYPE_STYLES,
304  $nonce
305  );
306  }
307 
308  // Inline stylesheets (embedded only=styles)
309  if ( $data['embed']['styles'] ) {
310  $chunks[] = $this->getLoad(
311  $data['embed']['styles'],
312  ResourceLoaderModule::TYPE_STYLES,
313  $nonce
314  );
315  }
316 
317  // Async scripts. Once the startup is loaded, inline RLQ scripts will run.
318  // Pass-through a custom 'target' from OutputPage (T143066).
319  $startupQuery = [ 'raw' => '1' ];
320  foreach ( [ 'target', 'safemode' ] as $param ) {
321  if ( $this->options[$param] !== null ) {
322  $startupQuery[$param] = (string)$this->options[$param];
323  }
324  }
325  $chunks[] = $this->getLoad(
326  'startup',
327  ResourceLoaderModule::TYPE_SCRIPTS,
328  $nonce,
329  $startupQuery
330  );
331 
332  return WrappedString::join( "\n", $chunks );
333  }
334 
338  public function getBodyHtml() {
339  $data = $this->getData();
340  $chunks = [];
341 
342  // Deprecations for only=styles modules
343  if ( $data['styleDeprecations'] ) {
345  implode( '', $data['styleDeprecations'] ),
346  $this->options['nonce']
347  );
348  }
349 
350  return WrappedString::join( "\n", $chunks );
351  }
352 
353  private function getContext( $group, $type ) {
354  return self::makeContext( $this->context, $group, $type );
355  }
356 
357  private function getLoad( $modules, $only, $nonce, array $extraQuery = [] ) {
358  return self::makeLoad( $this->context, (array)$modules, $only, $extraQuery, $nonce );
359  }
360 
361  private static function makeContext( ResourceLoaderContext $mainContext, $group, $type,
362  array $extraQuery = []
363  ) {
364  // Allow caller to setVersion() and setModules()
365  $ret = new DerivativeResourceLoaderContext( $mainContext );
366  // Set 'only' if not combined
367  $ret->setOnly( $type === ResourceLoaderModule::TYPE_COMBINED ? null : $type );
368  // Remove user parameter in most cases
369  if ( $group !== 'user' && $group !== 'private' ) {
370  $ret->setUser( null );
371  }
372  if ( isset( $extraQuery['raw'] ) ) {
373  $ret->setRaw( true );
374  }
375  return $ret;
376  }
377 
389  public static function makeLoad( ResourceLoaderContext $mainContext, array $modules, $only,
390  array $extraQuery = [], $nonce = null
391  ) {
392  $rl = $mainContext->getResourceLoader();
393  $chunks = [];
394 
395  // Sort module names so requests are more uniform
396  sort( $modules );
397 
398  if ( $mainContext->getDebug() && count( $modules ) > 1 ) {
399  $chunks = [];
400  // Recursively call us for every item
401  foreach ( $modules as $name ) {
402  $chunks[] = self::makeLoad( $mainContext, [ $name ], $only, $extraQuery, $nonce );
403  }
404  return new WrappedStringList( "\n", $chunks );
405  }
406 
407  // Create keyed-by-source and then keyed-by-group list of module objects from modules list
408  $sortedModules = [];
409  foreach ( $modules as $name ) {
410  $module = $rl->getModule( $name );
411  if ( !$module ) {
412  $rl->getLogger()->warning( 'Unknown module "{module}"', [ 'module' => $name ] );
413  continue;
414  }
415  $sortedModules[$module->getSource()][$module->getGroup()][$name] = $module;
416  }
417 
418  foreach ( $sortedModules as $source => $groups ) {
419  foreach ( $groups as $group => $grpModules ) {
420  $context = self::makeContext( $mainContext, $group, $only, $extraQuery );
421 
422  // Separate sets of linked and embedded modules while preserving order
423  $moduleSets = [];
424  $idx = -1;
425  foreach ( $grpModules as $name => $module ) {
426  $shouldEmbed = $module->shouldEmbedModule( $context );
427  if ( !$moduleSets || $moduleSets[$idx][0] !== $shouldEmbed ) {
428  $moduleSets[++$idx] = [ $shouldEmbed, [] ];
429  }
430  $moduleSets[$idx][1][$name] = $module;
431  }
432 
433  // Link/embed each set
434  foreach ( $moduleSets as list( $embed, $moduleSet ) ) {
435  $moduleSetNames = array_keys( $moduleSet );
436  $context->setModules( $moduleSetNames );
437  if ( $embed ) {
438  // Decide whether to use style or script element
439  if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
440  $chunks[] = Html::inlineStyle(
441  $rl->makeModuleResponse( $context, $moduleSet )
442  );
443  } else {
445  $rl->makeModuleResponse( $context, $moduleSet ),
446  $nonce
447  );
448  }
449  } else {
450  // Special handling for the user group; because users might change their stuff
451  // on-wiki like user pages, or user preferences; we need to find the highest
452  // timestamp of these user-changeable modules so we can ensure cache misses on change
453  // This should NOT be done for the site group (T29564) because anons get that too
454  // and we shouldn't be putting timestamps in CDN-cached HTML
455  if ( $group === 'user' ) {
456  $context->setVersion( $rl->makeVersionQuery( $context, $moduleSetNames ) );
457  }
458 
459  // Must setModules() before createLoaderURL()
460  $url = $rl->createLoaderURL( $source, $context, $extraQuery );
461 
462  // Decide whether to use 'style' or 'script' element
463  if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
464  $chunk = Html::linkedStyle( $url );
465  } elseif ( $context->getRaw() ) {
466  // This request is asking for the module to be delivered standalone,
467  // (aka "raw") without communicating to any mw.loader client.
468  // Use cases:
469  // - startup (naturally because this is what will define mw.loader)
470  // - html5shiv (loads synchronously in old IE before the async startup module arrives)
471  $chunk = Html::element( 'script', [
472  'async' => true,
473  'src' => $url
474  ] );
475  } else {
477  'mw.loader.load(' . $mainContext->encodeJson( $url ) . ');',
478  $nonce
479  );
480  }
481 
482  if ( $group == 'noscript' ) {
483  $chunks[] = Html::rawElement( 'noscript', [], $chunk );
484  } else {
485  $chunks[] = $chunk;
486  }
487  }
488  }
489  }
490  }
491 
492  return new WrappedStringList( "\n", $chunks );
493  }
494 }
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
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:33
ResourceLoaderClientHtml
Load and configure a ResourceLoader client on an HTML page.
Definition: ResourceLoaderClientHtml.php:30
ResourceLoaderClientHtml\makeContext
static makeContext(ResourceLoaderContext $mainContext, $group, $type, array $extraQuery=[])
Definition: ResourceLoaderClientHtml.php:361
ResourceLoaderClientHtml\getContext
getContext( $group, $type)
Definition: ResourceLoaderClientHtml.php:353
ResourceLoaderContext\getResourceLoader
getResourceLoader()
Definition: ResourceLoaderContext.php:121
ResourceLoader\makeInlineScript
static makeInlineScript( $script, $nonce=null)
Make an HTML script that runs given JS code after startup and base modules.
Definition: ResourceLoader.php:1643
ResourceLoaderClientHtml\getData
getData()
Definition: ResourceLoaderClientHtml.php:113
ResourceLoaderClientHtml\getDocumentAttributes
getDocumentAttributes()
Definition: ResourceLoaderClientHtml.php:225
ResourceLoaderClientHtml\setExemptStates
setExemptStates(array $states)
Set state of special modules that are handled by the caller manually.
Definition: ResourceLoaderClientHtml.php:109
ResourceLoaderClientHtml\$context
ResourceLoaderContext $context
Definition: ResourceLoaderClientHtml.php:33
ResourceLoaderClientHtml\$resourceLoader
ResourceLoader $resourceLoader
Definition: ResourceLoaderClientHtml.php:36
ResourceLoaderClientHtml\setConfig
setConfig(array $vars)
Set mw.config variables.
Definition: ResourceLoaderClientHtml.php:78
ResourceLoaderClientHtml\$modules
string[] $modules
Definition: ResourceLoaderClientHtml.php:45
ResourceLoaderClientHtml\getHeadHtml
getHeadHtml( $nojsClass=null)
The order of elements in the head is as follows:
Definition: ResourceLoaderClientHtml.php:244
ResourceLoaderClientHtml\$options
array< string, string|null|false > $options
Definition: ResourceLoaderClientHtml.php:39
ResourceLoaderContext\getDebug
getDebug()
Definition: ResourceLoaderContext.php:236
ResourceLoaderClientHtml\$data
array $data
Definition: ResourceLoaderClientHtml.php:54
ResourceLoaderClientHtml\$moduleStyles
string[] $moduleStyles
Definition: ResourceLoaderClientHtml.php:48
Html\inlineStyle
static inlineStyle( $contents, $media='all', $attribs=[])
Output a "<style>" tag with the given contents for the given media type (if any).
Definition: Html.php:619
ResourceLoaderClientHtml\getBodyHtml
getBodyHtml()
Definition: ResourceLoaderClientHtml.php:338
Html\inlineScript
static inlineScript( $contents, $nonce=null)
Output an HTML script tag with the given contents.
Definition: Html.php:572
DerivativeResourceLoaderContext
A mutable version of ResourceLoaderContext.
Definition: DerivativeResourceLoaderContext.php:33
ResourceLoaderClientHtml\$config
array< string, mixed > $config
Definition: ResourceLoaderClientHtml.php:42
ResourceLoaderClientHtml\makeLoad
static makeLoad(ResourceLoaderContext $mainContext, array $modules, $only, array $extraQuery=[], $nonce=null)
Explicitly load or embed modules on a page.
Definition: ResourceLoaderClientHtml.php:389
ResourceLoader
ResourceLoader is a loading system for JavaScript and CSS resources.
Definition: ResourceLoader.php:56
ResourceLoaderClientHtml\setModules
setModules(array $modules)
Ensure one or more modules are loaded.
Definition: ResourceLoaderClientHtml.php:89
Html\linkedStyle
static linkedStyle( $url, $media='all')
Output a "<link rel=stylesheet>" linking to the given URL for the given media type (if any).
Definition: Html.php:648
ResourceLoaderClientHtml\setModuleStyles
setModuleStyles(array $modules)
Ensure the styles of one or more modules are loaded.
Definition: ResourceLoaderClientHtml.php:98
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
$source
$source
Definition: mwdoc-filter.php:34
ResourceLoaderClientHtml\$exemptStates
array< string, string > $exemptStates
Definition: ResourceLoaderClientHtml.php:51
ResourceLoaderClientHtml\getLoad
getLoad( $modules, $only, $nonce, array $extraQuery=[])
Definition: ResourceLoaderClientHtml.php:357
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
ResourceLoaderClientHtml\__construct
__construct(ResourceLoaderContext $context, array $options=[])
Definition: ResourceLoaderClientHtml.php:63
ResourceLoaderContext\getRaw
getRaw()
Definition: ResourceLoaderContext.php:256
ResourceLoaderContext\encodeJson
encodeJson( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
Definition: ResourceLoaderContext.php:403
$type
$type
Definition: testCompression.php:52