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