MediaWiki  master
ResourceLoaderClientHtml.php
Go to the documentation of this file.
1 <?php
23 
30 
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 ) {
78  foreach ( $vars as $key => $value ) {
79  $this->config[$key] = $value;
80  }
81  }
82 
88  public function setModules( array $modules ) {
89  $this->modules = $modules;
90  }
91 
97  public function setModuleStyles( array $modules ) {
98  $this->moduleStyles = $modules;
99  }
100 
108  public function setExemptStates( array $states ) {
109  $this->exemptStates = $states;
110  }
111 
115  private function getData() {
116  if ( $this->data ) {
117  // @codeCoverageIgnoreStart
118  return $this->data;
119  // @codeCoverageIgnoreEnd
120  }
121 
122  $rl = $this->resourceLoader;
123  $data = [
124  'states' => [
125  // moduleName => state
126  ],
127  'general' => [],
128  'styles' => [],
129  // Embedding for private modules
130  'embed' => [
131  'styles' => [],
132  'general' => [],
133  ],
134  // Deprecations for style-only modules
135  'styleDeprecations' => [],
136  ];
137 
138  foreach ( $this->modules as $name ) {
139  $module = $rl->getModule( $name );
140  if ( !$module ) {
141  continue;
142  }
143 
144  $group = $module->getGroup();
145  $context = $this->getContext( $group, ResourceLoaderModule::TYPE_COMBINED );
146  $shouldEmbed = $module->shouldEmbedModule( $this->context );
147 
148  if ( ( $group === 'user' || $shouldEmbed ) && $module->isKnownEmpty( $context ) ) {
149  // This is a user-specific or embedded module, which means its output
150  // can be specific to the current page or user. As such, we can optimise
151  // the way we load it based on the current version of the module.
152  // Avoid needless embed for empty module, preset ready state.
153  $data['states'][$name] = 'ready';
154  } elseif ( $group === 'user' || $shouldEmbed ) {
155  // - For group=user: We need to provide a pre-generated load.php
156  // url to the client that has the 'user' and 'version' parameters
157  // filled in. Without this, the client would wrongly use the static
158  // version hash, per T64602.
159  // - For shouldEmbed=true: Embed via mw.loader.implement, per T36907.
160  $data['embed']['general'][] = $name;
161  // Avoid duplicate request from mw.loader
162  $data['states'][$name] = 'loading';
163  } else {
164  // Load via mw.loader.load()
165  $data['general'][] = $name;
166  }
167  }
168 
169  foreach ( $this->moduleStyles as $name ) {
170  $module = $rl->getModule( $name );
171  if ( !$module ) {
172  continue;
173  }
174 
175  if ( $module->getType() !== ResourceLoaderModule::LOAD_STYLES ) {
176  $logger = $rl->getLogger();
177  $logger->error( 'Unexpected general module "{module}" in styles queue.', [
178  'module' => $name,
179  ] );
180  continue;
181  }
182 
183  // Stylesheet doesn't trigger mw.loader callback.
184  // Set "ready" state to allow script modules to depend on this module (T87871).
185  // And to avoid duplicate requests at run-time from mw.loader.
186  $data['states'][$name] = 'ready';
187 
188  $group = $module->getGroup();
189  $context = $this->getContext( $group, ResourceLoaderModule::TYPE_STYLES );
190  if ( $module->shouldEmbedModule( $this->context ) ) {
191  // Avoid needless embed for private embeds we know are empty.
192  // (Set "ready" state directly instead, which we do a few lines above.)
193  if ( !$module->isKnownEmpty( $context ) ) {
194  // Embed via <style> element
195  $data['embed']['styles'][] = $name;
196  }
197  // For other style modules, always request them, regardless of whether they are
198  // currently known to be empty. Because:
199  // 1. Those modules are requested in batch, so there is no extra request overhead
200  // or extra HTML element to be avoided.
201  // 2. Checking isKnownEmpty for those can be expensive and slow down page view
202  // generation (T230260).
203  // 3. We don't want cached HTML to vary on the current state of a module.
204  // If the module becomes non-empty a few minutes later, it should start working
205  // on cached HTML without requiring a purge.
206  //
207  // But, user-specific modules:
208  // * ... are used on page views not publicly cached.
209  // * ... are in their own group and thus a require a request we can avoid
210  // * ... have known-empty status preloaded by ResourceLoader.
211  } elseif ( $group !== 'user' || !$module->isKnownEmpty( $context ) ) {
212  // Load from load.php?only=styles via <link rel=stylesheet>
213  $data['styles'][] = $name;
214  }
215  $deprecation = $module->getDeprecationInformation();
216  if ( $deprecation ) {
217  $data['styleDeprecations'][] = $deprecation;
218  }
219  }
220 
221  return $data;
222  }
223 
227  public function getDocumentAttributes() {
228  return [ 'class' => 'client-nojs' ];
229  }
230 
245  public function getHeadHtml() {
246  $nonce = $this->options['nonce'];
247  $data = $this->getData();
248  $chunks = [];
249 
250  // Change "client-nojs" class to client-js. This allows easy toggling of UI components.
251  // This must happen synchronously on every page view to avoid flashes of wrong content.
252  // See also #getDocumentAttributes() and /resources/src/startup.js.
253  $script = <<<'JAVASCRIPT'
254 document.documentElement.className = document.documentElement.className
255  .replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );
256 JAVASCRIPT;
257 
258  // Inline script: Declare mw.config variables for this page.
259  if ( $this->config ) {
260  $confJson = ResourceLoader::encodeJsonForScript( $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 = ResourceLoader::encodeJsonForScript( $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 = ResourceLoader::encodeJsonForScript( $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 ) {
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 = []
362  ) {
363  // Create new ResourceLoaderContext so that $extraQuery is supported (eg. for 'sync=1').
364  $req = new FauxRequest( array_merge( $mainContext->getRequest()->getValues(), $extraQuery ) );
365  // Set 'only' if not combined
366  $req->setVal( 'only', $type === ResourceLoaderModule::TYPE_COMBINED ? null : $type );
367  // Remove user parameter in most cases
368  if ( $group !== 'user' && $group !== 'private' ) {
369  $req->setVal( 'user', null );
370  }
371  $context = new ResourceLoaderContext( $mainContext->getResourceLoader(), $req );
372  // Allow caller to setVersion() and setModules()
374  $ret->setContentOverrideCallback( $mainContext->getContentOverrideCallback() );
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  $context->setModules( array_keys( $moduleSet ) );
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  // Must setModules() before makeVersionQuery()
456  $context->setVersion( $rl->makeVersionQuery( $context ) );
457  }
458 
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  // Use cases:
468  // - startup (naturally because this is what will define mw.loader)
469  // - html5shiv (loads synchronously in old IE before the async startup module arrives)
470  // - QUnit (needed in SpecialJavaScriptTest before async startup)
471  $chunk = Html::element( 'script', [
472  // The 'sync' option is only supported in combination with 'raw'.
473  'async' => !isset( $extraQuery['sync'] ),
474  'src' => $url
475  ] );
476  } else {
478  Xml::encodeJsCall( 'mw.loader.load', [ $url ] ),
479  $nonce
480  );
481  }
482 
483  if ( $group == 'noscript' ) {
484  $chunks[] = Html::rawElement( 'noscript', [], $chunk );
485  } else {
486  $chunks[] = $chunk;
487  }
488  }
489  }
490  }
491  }
492 
493  return new WrappedStringList( "\n", $chunks );
494  }
495 }
and how to run hooks for an and one after Each event has a preferably in CamelCase For ArticleDelete hook A clump of code and data that should be run when an event happens This can be either a function and a chunk of data
Definition: hooks.txt:6
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
setConfig(array $vars)
Set mw.config variables.
setModuleStyles(array $modules)
Ensure the styles of one or more modules are loaded.
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
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
static filter( $filter, $data, array $options=[])
Run JavaScript or CSS data through a filter, caching the filtered result for future calls...
getLoad( $modules, $only, $nonce, array $extraQuery=[])
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1983
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition: hooks.txt:181
$source
$value
static inlineScript( $contents, $nonce=null)
Output an HTML script tag with the given contents.
Definition: Html.php:572
__construct(ResourceLoaderContext $context, array $options=[])
Allows changing specific properties of a context object, without changing the main one...
getHeadHtml()
The order of elements in the head is as follows:
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Definition: Xml.php:677
setExemptStates(array $states)
Set state of special modules that are handled by the caller manually.
static makeContext(ResourceLoaderContext $mainContext, $group, $type, array $extraQuery=[])
Load and configure a ResourceLoader client on an HTML page.
static encodeJsonForScript( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode...
getContentOverrideCallback()
Return the replaced-content mapping callback.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:773
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
static makeInlineScript( $script, $nonce=null)
Returns an HTML script tag that runs given JS code after startup and base modules.
static makeLoad(ResourceLoaderContext $mainContext, array $modules, $only, array $extraQuery=[], $nonce=null)
Explicily load or embed modules on a page.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
this hook is for auditing only $req
Definition: hooks.txt:966
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing options(say) and put it in one place. Instead of having little title-reversing if-blocks spread all over the codebase in showAnArticle
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
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:277
setModules(array $modules)
Ensure one or more modules are loaded.
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition: hooks.txt:2216
Object passed around to modules which contains information about the state of a specific loader reque...