MediaWiki REL1_32
ResourceLoaderClientHtml.php
Go to the documentation of this file.
1<?php
21use Wikimedia\WrappedString;
22use Wikimedia\WrappedStringList;
23
30
32 private $context;
33
36
38 private $options;
39
41 private $config = [];
42
44 private $modules = [];
45
47 private $moduleStyles = [];
48
50 private $moduleScripts = [];
51
53 private $exemptStates = [];
54
56 private $data;
57
66 $this->context = $context;
67 $this->resourceLoader = $context->getResourceLoader();
68 $this->options = $options + [
69 'target' => null,
70 'safemode' => null,
71 'nonce' => null,
72 ];
73 }
74
80 public function setConfig( array $vars ) {
81 foreach ( $vars as $key => $value ) {
82 $this->config[$key] = $value;
83 }
84 }
85
91 public function setModules( array $modules ) {
92 $this->modules = $modules;
93 }
94
100 public function setModuleStyles( array $modules ) {
101 $this->moduleStyles = $modules;
102 }
103
110 public function setModuleScripts( array $modules ) {
111 $this->moduleScripts = $modules;
112 }
113
121 public function setExemptStates( array $states ) {
122 $this->exemptStates = $states;
123 }
124
128 private function getData() {
129 if ( $this->data ) {
130 // @codeCoverageIgnoreStart
131 return $this->data;
132 // @codeCoverageIgnoreEnd
133 }
134
136 $data = [
137 'states' => [
138 // moduleName => state
139 ],
140 'general' => [],
141 'styles' => [],
142 'scripts' => [],
143 // Embedding for private modules
144 'embed' => [
145 'styles' => [],
146 'general' => [],
147 ],
148 // Deprecations for style-only modules
149 'styleDeprecations' => [],
150 ];
151
152 foreach ( $this->modules as $name ) {
153 $module = $rl->getModule( $name );
154 if ( !$module ) {
155 continue;
156 }
157
158 $group = $module->getGroup();
160 if ( $module->isKnownEmpty( $context ) ) {
161 // Avoid needless request or embed for empty module
162 $data['states'][$name] = 'ready';
163 continue;
164 }
165
166 if ( $group === 'user' || $module->shouldEmbedModule( $this->context ) ) {
167 // Call makeLoad() to decide how to load these, instead of
168 // loading via mw.loader.load().
169 // - For group=user: We need to provide a pre-generated load.php
170 // url to the client that has the 'user' and 'version' parameters
171 // filled in. Without this, the client would wrongly use the static
172 // version hash, per T64602.
173 // - For shouldEmbed=true: Embed via mw.loader.implement, per T36907.
174 $data['embed']['general'][] = $name;
175 // Avoid duplicate request from mw.loader
176 $data['states'][$name] = 'loading';
177 } else {
178 // Load via mw.loader.load()
179 $data['general'][] = $name;
180 }
181 }
182
183 foreach ( $this->moduleStyles as $name ) {
184 $module = $rl->getModule( $name );
185 if ( !$module ) {
186 continue;
187 }
188
189 if ( $module->getType() !== ResourceLoaderModule::LOAD_STYLES ) {
190 $logger = $rl->getLogger();
191 $logger->error( 'Unexpected general module "{module}" in styles queue.', [
192 'module' => $name,
193 ] );
194 continue;
195 }
196
197 // Stylesheet doesn't trigger mw.loader callback.
198 // Set "ready" state to allow script modules to depend on this module (T87871).
199 // And to avoid duplicate requests at run-time from mw.loader.
200 $data['states'][$name] = 'ready';
201
202 $group = $module->getGroup();
204 // Avoid needless request for empty module
205 if ( !$module->isKnownEmpty( $context ) ) {
206 if ( $module->shouldEmbedModule( $this->context ) ) {
207 // Embed via style element
208 $data['embed']['styles'][] = $name;
209 } else {
210 // Load from load.php?only=styles via <link rel=stylesheet>
211 $data['styles'][] = $name;
212 }
213 }
214 $deprecation = $module->getDeprecationInformation();
215 if ( $deprecation ) {
216 $data['styleDeprecations'][] = $deprecation;
217 }
218 }
219
220 foreach ( $this->moduleScripts as $name ) {
221 $module = $rl->getModule( $name );
222 if ( !$module ) {
223 continue;
224 }
225
226 $group = $module->getGroup();
228 if ( $module->isKnownEmpty( $context ) ) {
229 // Avoid needless request for empty module
230 $data['states'][$name] = 'ready';
231 } else {
232 // Load from load.php?only=scripts via <script src></script>
233 $data['scripts'][] = $name;
234
235 // Avoid duplicate request from mw.loader
236 $data['states'][$name] = 'loading';
237 }
238 }
239
240 return $data;
241 }
242
246 public function getDocumentAttributes() {
247 return [ 'class' => 'client-nojs' ];
248 }
249
264 public function getHeadHtml() {
265 $nonce = $this->options['nonce'];
266 $data = $this->getData();
267 $chunks = [];
268
269 // Change "client-nojs" class to client-js. This allows easy toggling of UI components.
270 // This happens synchronously on every page view to avoid flashes of wrong content.
271 // See also #getDocumentAttributes() and /resources/src/startup.js.
272 $chunks[] = Html::inlineScript(
273 'document.documentElement.className = document.documentElement.className'
274 . '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );',
275 $nonce
276 );
277
278 // Inline RLQ: Set page variables
279 if ( $this->config ) {
282 $nonce
283 );
284 }
285
286 // Inline RLQ: Initial module states
287 $states = array_merge( $this->exemptStates, $data['states'] );
288 if ( $states ) {
291 $nonce
292 );
293 }
294
295 // Inline RLQ: Embedded modules
296 if ( $data['embed']['general'] ) {
297 $chunks[] = $this->getLoad(
298 $data['embed']['general'],
300 $nonce
301 );
302 }
303
304 // Inline RLQ: Load general modules
305 if ( $data['general'] ) {
307 'RLPAGEMODULES='
309 . ';'
310 . 'mw.loader.load(RLPAGEMODULES);',
311 $nonce
312 );
313 }
314
315 // Inline RLQ: Load only=scripts
316 if ( $data['scripts'] ) {
317 $chunks[] = $this->getLoad(
318 $data['scripts'],
320 $nonce
321 );
322 }
323
324 // External stylesheets (only=styles)
325 if ( $data['styles'] ) {
326 $chunks[] = $this->getLoad(
327 $data['styles'],
329 $nonce
330 );
331 }
332
333 // Inline stylesheets (embedded only=styles)
334 if ( $data['embed']['styles'] ) {
335 $chunks[] = $this->getLoad(
336 $data['embed']['styles'],
338 $nonce
339 );
340 }
341
342 // Async scripts. Once the startup is loaded, inline RLQ scripts will run.
343 // Pass-through a custom 'target' from OutputPage (T143066).
344 $startupQuery = [];
345 foreach ( [ 'target', 'safemode' ] as $param ) {
346 if ( $this->options[$param] !== null ) {
347 $startupQuery[$param] = (string)$this->options[$param];
348 }
349 }
350 $chunks[] = $this->getLoad(
351 'startup',
353 $nonce,
354 $startupQuery
355 );
356
357 return WrappedString::join( "\n", $chunks );
358 }
359
363 public function getBodyHtml() {
364 $data = $this->getData();
365 $chunks = [];
366
367 // Deprecations for only=styles modules
368 if ( $data['styleDeprecations'] ) {
370 implode( '', $data['styleDeprecations'] ),
371 $this->options['nonce']
372 );
373 }
374
375 return WrappedString::join( "\n", $chunks );
376 }
377
378 private function getContext( $group, $type ) {
379 return self::makeContext( $this->context, $group, $type );
380 }
381
382 private function getLoad( $modules, $only, $nonce, array $extraQuery = [] ) {
383 return self::makeLoad( $this->context, (array)$modules, $only, $extraQuery, $nonce );
384 }
385
386 private static function makeContext( ResourceLoaderContext $mainContext, $group, $type,
387 array $extraQuery = []
388 ) {
389 // Create new ResourceLoaderContext so that $extraQuery may trigger isRaw().
390 $req = new FauxRequest( array_merge( $mainContext->getRequest()->getValues(), $extraQuery ) );
391 // Set 'only' if not combined
392 $req->setVal( 'only', $type === ResourceLoaderModule::TYPE_COMBINED ? null : $type );
393 // Remove user parameter in most cases
394 if ( $group !== 'user' && $group !== 'private' ) {
395 $req->setVal( 'user', null );
396 }
397 $context = new ResourceLoaderContext( $mainContext->getResourceLoader(), $req );
398 // Allow caller to setVersion() and setModules()
400 $ret->setContentOverrideCallback( $mainContext->getContentOverrideCallback() );
401 return $ret;
402 }
403
415 public static function makeLoad( ResourceLoaderContext $mainContext, array $modules, $only,
416 array $extraQuery = [], $nonce = null
417 ) {
418 $rl = $mainContext->getResourceLoader();
419 $chunks = [];
420
421 // Sort module names so requests are more uniform
422 sort( $modules );
423
424 if ( $mainContext->getDebug() && count( $modules ) > 1 ) {
425 $chunks = [];
426 // Recursively call us for every item
427 foreach ( $modules as $name ) {
428 $chunks[] = self::makeLoad( $mainContext, [ $name ], $only, $extraQuery, $nonce );
429 }
430 return new WrappedStringList( "\n", $chunks );
431 }
432
433 // Create keyed-by-source and then keyed-by-group list of module objects from modules list
434 $sortedModules = [];
435 foreach ( $modules as $name ) {
436 $module = $rl->getModule( $name );
437 if ( !$module ) {
438 $rl->getLogger()->warning( 'Unknown module "{module}"', [ 'module' => $name ] );
439 continue;
440 }
441 $sortedModules[$module->getSource()][$module->getGroup()][$name] = $module;
442 }
443
444 foreach ( $sortedModules as $source => $groups ) {
445 foreach ( $groups as $group => $grpModules ) {
446 $context = self::makeContext( $mainContext, $group, $only, $extraQuery );
447
448 // Separate sets of linked and embedded modules while preserving order
449 $moduleSets = [];
450 $idx = -1;
451 foreach ( $grpModules as $name => $module ) {
452 $shouldEmbed = $module->shouldEmbedModule( $context );
453 if ( !$moduleSets || $moduleSets[$idx][0] !== $shouldEmbed ) {
454 $moduleSets[++$idx] = [ $shouldEmbed, [] ];
455 }
456 $moduleSets[$idx][1][$name] = $module;
457 }
458
459 // Link/embed each set
460 foreach ( $moduleSets as list( $embed, $moduleSet ) ) {
461 $context->setModules( array_keys( $moduleSet ) );
462 if ( $embed ) {
463 // Decide whether to use style or script element
464 if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
465 $chunks[] = Html::inlineStyle(
466 $rl->makeModuleResponse( $context, $moduleSet )
467 );
468 } else {
470 $rl->makeModuleResponse( $context, $moduleSet ),
471 $nonce
472 );
473 }
474 } else {
475 // See if we have one or more raw modules
476 $isRaw = false;
477 foreach ( $moduleSet as $key => $module ) {
478 $isRaw |= $module->isRaw();
479 }
480
481 // Special handling for the user group; because users might change their stuff
482 // on-wiki like user pages, or user preferences; we need to find the highest
483 // timestamp of these user-changeable modules so we can ensure cache misses on change
484 // This should NOT be done for the site group (T29564) because anons get that too
485 // and we shouldn't be putting timestamps in CDN-cached HTML
486 if ( $group === 'user' ) {
487 // Must setModules() before makeVersionQuery()
488 $context->setVersion( $rl->makeVersionQuery( $context ) );
489 }
490
491 $url = $rl->createLoaderURL( $source, $context, $extraQuery );
492
493 // Decide whether to use 'style' or 'script' element
494 if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
495 $chunk = Html::linkedStyle( $url );
496 } else {
497 if ( $context->getRaw() || $isRaw ) {
498 $chunk = Html::element( 'script', [
499 // In SpecialJavaScriptTest, QUnit must load synchronous
500 'async' => !isset( $extraQuery['sync'] ),
501 'src' => $url
502 ] );
503 } else {
505 Xml::encodeJsCall( 'mw.loader.load', [ $url ] ),
506 $nonce
507 );
508 }
509 }
510
511 if ( $group == 'noscript' ) {
512 $chunks[] = Html::rawElement( 'noscript', [], $chunk );
513 } else {
514 $chunks[] = $chunk;
515 }
516 }
517 }
518 }
519 }
520
521 return new WrappedStringList( "\n", $chunks );
522 }
523}
getContext()
Allows changing specific properties of a context object, without changing the main one.
WebRequest clone which takes values from a provided array.
Bootstrap a ResourceLoader client on an HTML page.
getLoad( $modules, $only, $nonce, array $extraQuery=[])
setModules(array $modules)
Ensure one or more modules are loaded.
static makeLoad(ResourceLoaderContext $mainContext, array $modules, $only, array $extraQuery=[], $nonce=null)
Explicily load or embed modules on a page.
setModuleScripts(array $modules)
Ensure the scripts of one or more modules are loaded.
__construct(ResourceLoaderContext $context, array $options=[])
getHeadHtml()
The order of elements in the head is as follows:
setConfig(array $vars)
Set mw.config variables.
setExemptStates(array $states)
Set state of special modules that are handled by the caller manually.
static makeContext(ResourceLoaderContext $mainContext, $group, $type, array $extraQuery=[])
setModuleStyles(array $modules)
Ensure the styles of one or more modules are loaded.
Object passed around to modules which contains information about the state of a specific loader reque...
Dynamic JavaScript and CSS resource loading system.
static makeInlineScript( $script, $nonce=null)
Returns an HTML script tag that runs given JS code after startup and base modules.
static makeLoaderStateScript( $states, $state=null)
Returns a JS call to mw.loader.state, which sets the state of one ore more modules to a given value.
static makeConfigSetScript(array $configuration)
Returns JS code which will set the MediaWiki configuration array to the given value.
getModule( $name)
Get the ResourceLoaderModule object for a given module name.
static encodeJsonForScript( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
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
this hook is for auditing only $req
Definition hooks.txt:1018
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition hooks.txt:2278
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
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
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:2054
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$source