MediaWiki  1.28.3
ResourceLoaderClientHtml.php
Go to the documentation of this file.
1 <?php
22 
29 
31  private $context;
32 
34  private $resourceLoader;
35 
37  private $target;
38 
40  private $config = [];
41 
43  private $modules = [];
44 
46  private $moduleStyles = [];
47 
49  private $moduleScripts = [];
50 
52  private $exemptStates = [];
53 
55  private $data;
56 
61  public function __construct( ResourceLoaderContext $context, $target = null ) {
62  $this->context = $context;
63  $this->resourceLoader = $context->getResourceLoader();
64  $this->target = $target;
65  }
66 
72  public function setConfig( array $vars ) {
73  foreach ( $vars as $key => $value ) {
74  $this->config[$key] = $value;
75  }
76  }
77 
83  public function setModules( array $modules ) {
84  $this->modules = $modules;
85  }
86 
93  public function setModuleStyles( array $modules ) {
94  $this->moduleStyles = $modules;
95  }
96 
103  public function setModuleScripts( array $modules ) {
104  $this->moduleScripts = $modules;
105  }
106 
114  public function setExemptStates( array $states ) {
115  $this->exemptStates = $states;
116  }
117 
121  private function getData() {
122  if ( $this->data ) {
123  // @codeCoverageIgnoreStart
124  return $this->data;
125  // @codeCoverageIgnoreEnd
126  }
127 
128  $rl = $this->resourceLoader;
129  $data = [
130  'states' => [
131  // moduleName => state
132  ],
133  'general' => [
134  // position => [ moduleName ]
135  'top' => [],
136  'bottom' => [],
137  ],
138  'styles' => [
139  // moduleName
140  ],
141  'scripts' => [
142  // position => [ moduleName ]
143  'top' => [],
144  'bottom' => [],
145  ],
146  // Embedding for private modules
147  'embed' => [
148  'styles' => [],
149  'general' => [
150  'top' => [],
151  'bottom' => [],
152  ],
153  ],
154 
155  ];
156 
157  foreach ( $this->modules as $name ) {
158  $module = $rl->getModule( $name );
159  if ( !$module ) {
160  continue;
161  }
162 
163  $group = $module->getGroup();
164  $position = $module->getPosition();
165 
166  if ( $group === 'private' ) {
167  // Embed via mw.loader.implement per T36907.
168  $data['embed']['general'][$position][] = $name;
169  // Avoid duplicate request from mw.loader
170  $data['states'][$name] = 'loading';
171  } else {
172  // Load via mw.loader.load()
173  $data['general'][$position][] = $name;
174  }
175  }
176 
177  foreach ( $this->moduleStyles as $name ) {
178  $module = $rl->getModule( $name );
179  if ( !$module ) {
180  continue;
181  }
182 
183  if ( $module->getType() !== ResourceLoaderModule::LOAD_STYLES ) {
184  $logger = $rl->getLogger();
185  $logger->debug( 'Unexpected general module "{module}" in styles queue.', [
186  'module' => $name,
187  ] );
188  } else {
189  // Stylesheet doesn't trigger mw.loader callback.
190  // Set "ready" state to allow dependencies and avoid duplicate requests. (T87871)
191  $data['states'][$name] = 'ready';
192  }
193 
194  $group = $module->getGroup();
196  if ( $module->isKnownEmpty( $context ) ) {
197  // Avoid needless request for empty module
198  $data['states'][$name] = 'ready';
199  } else {
200  if ( $group === 'private' ) {
201  // Embed via style element
202  $data['embed']['styles'][] = $name;
203  // Avoid duplicate request from mw.loader
204  $data['states'][$name] = 'ready';
205  } else {
206  // Load from load.php?only=styles via <link rel=stylesheet>
207  $data['styles'][] = $name;
208  }
209  }
210  }
211 
212  foreach ( $this->moduleScripts as $name ) {
213  $module = $rl->getModule( $name );
214  if ( !$module ) {
215  continue;
216  }
217 
218  $group = $module->getGroup();
219  $position = $module->getPosition();
221  if ( $module->isKnownEmpty( $context ) ) {
222  // Avoid needless request for empty module
223  $data['states'][$name] = 'ready';
224  } else {
225  // Load from load.php?only=scripts via <script src></script>
226  $data['scripts'][$position][] = $name;
227 
228  // Avoid duplicate request from mw.loader
229  $data['states'][$name] = 'loading';
230  }
231  }
232 
233  return $data;
234  }
235 
239  public function getDocumentAttributes() {
240  return [ 'class' => 'client-nojs' ];
241  }
242 
257  public function getHeadHtml() {
258  $data = $this->getData();
259  $chunks = [];
260 
261  // Change "client-nojs" class to client-js. This allows easy toggling of UI components.
262  // This happens synchronously on every page view to avoid flashes of wrong content.
263  // See also #getDocumentAttributes() and /resources/src/startup.js.
264  $chunks[] = Html::inlineScript(
265  'document.documentElement.className = document.documentElement.className'
266  . '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );'
267  );
268 
269  // Inline RLQ: Set page variables
270  if ( $this->config ) {
272  ResourceLoader::makeConfigSetScript( $this->config )
273  );
274  }
275 
276  // Inline RLQ: Initial module states
277  $states = array_merge( $this->exemptStates, $data['states'] );
278  if ( $states ) {
281  );
282  }
283 
284  // Inline RLQ: Embedded modules
285  if ( $data['embed']['general']['top'] ) {
286  $chunks[] = $this->getLoad(
287  $data['embed']['general']['top'],
289  );
290  }
291 
292  // Inline RLQ: Load general modules
293  if ( $data['general']['top'] ) {
295  Xml::encodeJsCall( 'mw.loader.load', [ $data['general']['top'] ] )
296  );
297  }
298 
299  // Inline RLQ: Load only=scripts
300  if ( $data['scripts']['top'] ) {
301  $chunks[] = $this->getLoad(
302  $data['scripts']['top'],
304  );
305  }
306 
307  // External stylesheets
308  if ( $data['styles'] ) {
309  $chunks[] = $this->getLoad(
310  $data['styles'],
312  );
313  }
314 
315  // Inline stylesheets (embedded only=styles)
316  if ( $data['embed']['styles'] ) {
317  $chunks[] = $this->getLoad(
318  $data['embed']['styles'],
320  );
321  }
322 
323  // Async scripts. Once the startup is loaded, inline RLQ scripts will run.
324  // Pass-through a custom target from OutputPage (T143066).
325  $startupQuery = $this->target ? [ 'target' => $this->target ] : [];
326  $chunks[] = $this->getLoad(
327  'startup',
329  $startupQuery
330  );
331 
332  return WrappedStringList::join( "\n", $chunks );
333  }
334 
338  public function getBodyHtml() {
339  $data = $this->getData();
340  $chunks = [];
341 
342  // Inline RLQ: Embedded modules
343  if ( $data['embed']['general']['bottom'] ) {
344  $chunks[] = $this->getLoad(
345  $data['embed']['general']['bottom'],
347  );
348  }
349 
350  // Inline RLQ: Load only=scripts
351  if ( $data['scripts']['bottom'] ) {
352  $chunks[] = $this->getLoad(
353  $data['scripts']['bottom'],
355  );
356  }
357 
358  // Inline RLQ: Load general modules
359  if ( $data['general']['bottom'] ) {
361  Xml::encodeJsCall( 'mw.loader.load', [ $data['general']['bottom'] ] )
362  );
363  }
364 
365  return WrappedStringList::join( "\n", $chunks );
366  }
367 
368  private function getContext( $group, $type ) {
369  return self::makeContext( $this->context, $group, $type );
370  }
371 
372  private function getLoad( $modules, $only, array $extraQuery = [] ) {
373  return self::makeLoad( $this->context, (array)$modules, $only, $extraQuery );
374  }
375 
376  private static function makeContext( ResourceLoaderContext $mainContext, $group, $type,
377  array $extraQuery = []
378  ) {
379  // Create new ResourceLoaderContext so that $extraQuery may trigger isRaw().
380  $req = new FauxRequest( array_merge( $mainContext->getRequest()->getValues(), $extraQuery ) );
381  // Set 'only' if not combined
382  $req->setVal( 'only', $type === ResourceLoaderModule::TYPE_COMBINED ? null : $type );
383  // Remove user parameter in most cases
384  if ( $group !== 'user' && $group !== 'private' ) {
385  $req->setVal( 'user', null );
386  }
387  $context = new ResourceLoaderContext( $mainContext->getResourceLoader(), $req );
388  // Allow caller to setVersion() and setModules()
390  }
391 
401  public static function makeLoad( ResourceLoaderContext $mainContext, array $modules, $only,
402  array $extraQuery = []
403  ) {
404  $rl = $mainContext->getResourceLoader();
405  $chunks = [];
406 
407  if ( $mainContext->getDebug() && count( $modules ) > 1 ) {
408  $chunks = [];
409  // Recursively call us for every item
410  foreach ( $modules as $name ) {
411  $chunks[] = self::makeLoad( $mainContext, [ $name ], $only, $extraQuery );
412  }
413  return new WrappedStringList( "\n", $chunks );
414  }
415 
416  // Sort module names so requests are more uniform
417  sort( $modules );
418  // Create keyed-by-source and then keyed-by-group list of module objects from modules list
419  $sortedModules = [];
420  foreach ( $modules as $name ) {
421  $module = $rl->getModule( $name );
422  if ( !$module ) {
423  $rl->getLogger()->warning( 'Unknown module "{module}"', [ 'module' => $name ] );
424  continue;
425  }
426  $sortedModules[$module->getSource()][$module->getGroup()][$name] = $module;
427  }
428 
429  foreach ( $sortedModules as $source => $groups ) {
430  foreach ( $groups as $group => $grpModules ) {
431  $context = self::makeContext( $mainContext, $group, $only, $extraQuery );
432  $context->setModules( array_keys( $grpModules ) );
433 
434  if ( $group === 'private' ) {
435  // Decide whether to use style or script element
436  if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
437  $chunks[] = Html::inlineStyle(
438  $rl->makeModuleResponse( $context, $grpModules )
439  );
440  } else {
442  $rl->makeModuleResponse( $context, $grpModules )
443  );
444  }
445  continue;
446  }
447 
448  // See if we have one or more raw modules
449  $isRaw = false;
450  foreach ( $grpModules as $key => $module ) {
451  $isRaw |= $module->isRaw();
452  }
453 
454  // Special handling for the user group; because users might change their stuff
455  // on-wiki like user pages, or user preferences; we need to find the highest
456  // timestamp of these user-changeable modules so we can ensure cache misses on change
457  // This should NOT be done for the site group (bug 27564) because anons get that too
458  // and we shouldn't be putting timestamps in CDN-cached HTML
459  if ( $group === 'user' ) {
460  // Must setModules() before makeVersionQuery()
461  $context->setVersion( $rl->makeVersionQuery( $context ) );
462  }
463 
464  $url = $rl->createLoaderURL( $source, $context, $extraQuery );
465 
466  // Decide whether to use 'style' or 'script' element
467  if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
468  $chunk = Html::linkedStyle( $url );
469  } else {
470  if ( $context->getRaw() || $isRaw ) {
471  $chunk = Html::element( 'script', [
472  // In SpecialJavaScriptTest, QUnit must load synchronous
473  'async' => !isset( $extraQuery['sync'] ),
474  'src' => $url
475  ] );
476  } else {
478  Xml::encodeJsCall( 'mw.loader.load', [ $url ] )
479  );
480  }
481  }
482 
483  if ( $group == 'noscript' ) {
484  $chunks[] = Html::rawElement( 'noscript', [], $chunk );
485  } else {
486  $chunks[] = $chunk;
487  }
488  }
489  }
490 
491  return new WrappedStringList( "\n", $chunks );
492  }
493 }
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
setConfig(array $vars)
Set mw.config variables.
static inlineScript($contents)
Output a "