Completed
Branch master (bbf110)
by
unknown
25:51
created

ResourceLoaderClientHtml::getData()   D

Complexity

Conditions 13
Paths 129

Size

Total Lines 114
Code Lines 62

Duplication

Lines 20
Ratio 17.54 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 62
c 1
b 0
f 0
nc 129
nop 0
dl 20
loc 114
rs 4.6605

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This program is free software; you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation; either version 2 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
 * GNU General Public License for more details.
12
 *
13
 * You should have received a copy of the GNU General Public License along
14
 * with this program; if not, write to the Free Software Foundation, Inc.,
15
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
 * http://www.gnu.org/copyleft/gpl.html
17
 *
18
 * @file
19
 */
20
21
use WrappedString\WrappedStringList;
22
23
/**
24
 * Bootstrap a ResourceLoader client on an HTML page.
25
 *
26
 * @since 1.28
27
 */
28
class ResourceLoaderClientHtml {
29
30
	/** @var ResourceLoaderContext */
31
	private $context;
32
33
	/** @var ResourceLoader */
34
	private $resourceLoader;
35
36
	/** @var array */
37
	private $config = [];
38
39
	/** @var array */
40
	private $modules = [];
41
42
	/** @var array */
43
	private $moduleStyles = [];
44
45
	/** @var array */
46
	private $moduleScripts = [];
47
48
	/** @var array */
49
	private $exemptStates = [];
50
51
	/** @var array */
52
	private $data;
53
54
	/**
55
	 * @param ResourceLoaderContext $context
56
	 */
57
	public function __construct( ResourceLoaderContext $context ) {
58
		$this->context = $context;
59
		$this->resourceLoader = $context->getResourceLoader();
60
	}
61
62
	/**
63
	 * Set mw.config variables.
64
	 *
65
	 * @param array $vars Array of key/value pairs
66
	 */
67
	public function setConfig( array $vars ) {
68
		foreach ( $vars as $key => $value ) {
69
			$this->config[$key] = $value;
70
		}
71
	}
72
73
	/**
74
	 * Ensure one or more modules are loaded.
75
	 *
76
	 * @param array $modules Array of module names
77
	 */
78
	public function setModules( array $modules ) {
79
		$this->modules = $modules;
80
	}
81
82
	/**
83
	 * Ensure the styles of one or more modules are loaded.
84
	 *
85
	 * @deprecated since 1.28
86
	 * @param array $modules Array of module names
87
	 */
88
	public function setModuleStyles( array $modules ) {
89
		$this->moduleStyles = $modules;
90
	}
91
92
	/**
93
	 * Ensure the scripts of one or more modules are loaded.
94
	 *
95
	 * @deprecated since 1.28
96
	 * @param array $modules Array of module names
97
	 */
98
	public function setModuleScripts( array $modules ) {
99
		$this->moduleScripts = $modules;
100
	}
101
102
	/**
103
	 * Set state of special modules that are handled by the caller manually.
104
	 *
105
	 * See OutputPage::buildExemptModules() for use cases.
106
	 *
107
	 * @param array $modules Module state keyed by module name
0 ignored issues
show
Bug introduced by
There is no parameter named $modules. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
108
	 */
109
	public function setExemptStates( array $states ) {
110
		$this->exemptStates = $states;
111
	}
112
113
	/**
114
	 * @return array
115
	 */
116
	private function getData() {
117
		if ( $this->data ) {
118
			// @codeCoverageIgnoreStart
119
			return $this->data;
120
			// @codeCoverageIgnoreEnd
121
		}
122
123
		$rl = $this->resourceLoader;
124
		$data = [
125
			'states' => [
126
				// moduleName => state
127
			],
128
			'general' => [
129
				// position => [ moduleName ]
130
				'top' => [],
131
				'bottom' => [],
132
			],
133
			'styles' => [
134
				// moduleName
135
			],
136
			'scripts' => [
137
				// position => [ moduleName ]
138
				'top' => [],
139
				'bottom' => [],
140
			],
141
			// Embedding for private modules
142
			'embed' => [
143
				'styles' => [],
144
				'general' => [
145
					'top' => [],
146
					'bottom' => [],
147
				],
148
			],
149
150
		];
151
152
		foreach ( $this->modules as $name ) {
153
			$module = $rl->getModule( $name );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $module is correct as $rl->getModule($name) (which targets ResourceLoader::getModule()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
154
			if ( !$module ) {
155
				continue;
156
			}
157
158
			$group = $module->getGroup();
159
			$position = $module->getPosition();
160
161 View Code Duplication
			if ( $group === 'private' ) {
162
				// Embed via mw.loader.implement per T36907.
163
				$data['embed']['general'][$position][] = $name;
164
				// Avoid duplicate request from mw.loader
165
				$data['states'][$name] = 'loading';
166
			} else {
167
				// Load via mw.loader.load()
168
				$data['general'][$position][] = $name;
169
			}
170
		}
171
172
		foreach ( $this->moduleStyles as $name ) {
173
			$module = $rl->getModule( $name );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $module is correct as $rl->getModule($name) (which targets ResourceLoader::getModule()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
174
			if ( !$module ) {
175
				continue;
176
			}
177
178
			if ( $module->getType() !== ResourceLoaderModule::LOAD_STYLES ) {
179
				$logger = $rl->getLogger();
180
				$logger->debug( 'Unexpected general module "{module}" in styles queue.', [
181
					'module' => $name,
182
				] );
183
			} else {
184
				// Stylesheet doesn't trigger mw.loader callback.
185
				// Set "ready" state to allow dependencies and avoid duplicate requests. (T87871)
186
				$data['states'][$name] = 'ready';
187
			}
188
189
			$group = $module->getGroup();
190
			$context = $this->getContext( $group, ResourceLoaderModule::TYPE_STYLES );
191
			if ( $module->isKnownEmpty( $context ) ) {
192
				// Avoid needless request for empty module
193
				$data['states'][$name] = 'ready';
194 View Code Duplication
			} else {
195
				if ( $group === 'private' ) {
196
					// Embed via style element
197
					$data['embed']['styles'][] = $name;
198
					// Avoid duplicate request from mw.loader
199
					$data['states'][$name] = 'ready';
200
				} else {
201
					// Load from load.php?only=styles via <link rel=stylesheet>
202
					$data['styles'][] = $name;
203
				}
204
			}
205
		}
206
207
		foreach ( $this->moduleScripts as $name ) {
208
			$module = $rl->getModule( $name );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $module is correct as $rl->getModule($name) (which targets ResourceLoader::getModule()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
209
			if ( !$module ) {
210
				continue;
211
			}
212
213
			$group = $module->getGroup();
214
			$position = $module->getPosition();
215
			$context = $this->getContext( $group, ResourceLoaderModule::TYPE_SCRIPTS );
216
			if ( $module->isKnownEmpty( $context ) ) {
217
				// Avoid needless request for empty module
218
				$data['states'][$name] = 'ready';
219
			} else {
220
				// Load from load.php?only=scripts via <script src></script>
221
				$data['scripts'][$position][] = $name;
222
223
				// Avoid duplicate request from mw.loader
224
				$data['states'][$name] = 'loading';
225
			}
226
		}
227
228
		return $data;
229
	}
230
231
	/**
232
	 * @return array Attribute key-value pairs for the HTML document element
233
	 */
234
	public function getDocumentAttributes() {
235
		return [ 'class' => 'client-nojs' ];
236
	}
237
238
	/**
239
	 * The order of elements in the head is as follows:
240
	 * - Inline scripts.
241
	 * - Stylesheets.
242
	 * - Async external script-src.
243
	 *
244
	 * Reasons:
245
	 * - Script execution may be blocked on preceeding stylesheets.
246
	 * - Async scripts are not blocked on stylesheets.
247
	 * - Inline scripts can't be asynchronous.
248
	 * - For styles, earlier is better.
249
	 *
250
	 * @return string|WrappedStringList HTML
251
	 */
252
	public function getHeadHtml() {
253
		$data = $this->getData();
254
		$chunks = [];
255
256
		// Change "client-nojs" class to client-js. This allows easy toggling of UI components.
257
		// This happens synchronously on every page view to avoid flashes of wrong content.
258
		// See also #getDocumentAttributes() and /resources/src/startup.js.
259
		$chunks[] = Html::inlineScript(
260
			'document.documentElement.className = document.documentElement.className'
261
			. '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );'
262
		);
263
264
		// Inline RLQ: Set page variables
265
		if ( $this->config ) {
266
			$chunks[] = ResourceLoader::makeInlineScript(
267
				ResourceLoader::makeConfigSetScript( $this->config )
0 ignored issues
show
Security Bug introduced by
It seems like \ResourceLoader::makeCon...etScript($this->config) targeting ResourceLoader::makeConfigSetScript() can also be of type false; however, ResourceLoader::makeInlineScript() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
268
			);
269
		}
270
271
		// Inline RLQ: Initial module states
272
		$states = array_merge( $this->exemptStates, $data['states'] );
273
		if ( $states ) {
274
			$chunks[] = ResourceLoader::makeInlineScript(
275
				ResourceLoader::makeLoaderStateScript( $states )
0 ignored issues
show
Security Bug introduced by
It seems like \ResourceLoader::makeLoaderStateScript($states) targeting ResourceLoader::makeLoaderStateScript() can also be of type false; however, ResourceLoader::makeInlineScript() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
276
			);
277
		}
278
279
		// Inline RLQ: Embedded modules
280 View Code Duplication
		if ( $data['embed']['general']['top'] ) {
281
			$chunks[] = $this->getLoad(
282
				$data['embed']['general']['top'],
283
				ResourceLoaderModule::TYPE_COMBINED
284
			);
285
		}
286
287
		// Inline RLQ: Load general modules
288 View Code Duplication
		if ( $data['general']['top'] ) {
289
			$chunks[] = ResourceLoader::makeInlineScript(
290
				Xml::encodeJsCall( 'mw.loader.load', [ $data['general']['top'] ] )
0 ignored issues
show
Security Bug introduced by
It seems like \Xml::encodeJsCall('mw.l...ata['general']['top'])) targeting Xml::encodeJsCall() can also be of type false; however, ResourceLoader::makeInlineScript() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
291
			);
292
		}
293
294
		// Inline RLQ: Load only=scripts
295 View Code Duplication
		if ( $data['scripts']['top'] ) {
296
			$chunks[] = $this->getLoad(
297
				$data['scripts']['top'],
298
				ResourceLoaderModule::TYPE_SCRIPTS
299
			);
300
		}
301
302
		// External stylesheets
303
		if ( $data['styles'] ) {
304
			$chunks[] = $this->getLoad(
305
				$data['styles'],
306
				ResourceLoaderModule::TYPE_STYLES
307
			);
308
		}
309
310
		// Inline stylesheets (embedded only=styles)
311 View Code Duplication
		if ( $data['embed']['styles'] ) {
312
			$chunks[] = $this->getLoad(
313
				$data['embed']['styles'],
314
				ResourceLoaderModule::TYPE_STYLES
315
			);
316
		}
317
318
		// Async scripts. Once the startup is loaded, inline RLQ scripts will run.
319
		$chunks[] = $this->getLoad( 'startup', ResourceLoaderModule::TYPE_SCRIPTS );
320
321
		return WrappedStringList::join( "\n", $chunks );
322
	}
323
324
	/**
325
	 * @return string|WrappedStringList HTML
326
	 */
327
	public function getBodyHtml() {
328
		$data = $this->getData();
329
		$chunks = [];
330
331
		// Inline RLQ: Embedded modules
332 View Code Duplication
		if ( $data['embed']['general']['bottom'] ) {
333
			$chunks[] = $this->getLoad(
334
				$data['embed']['general']['bottom'],
335
				ResourceLoaderModule::TYPE_COMBINED
336
			);
337
		}
338
339
		// Inline RLQ: Load only=scripts
340 View Code Duplication
		if ( $data['scripts']['bottom'] ) {
341
			$chunks[] = $this->getLoad(
342
				$data['scripts']['bottom'],
343
				ResourceLoaderModule::TYPE_SCRIPTS
344
			);
345
		}
346
347
		// Inline RLQ: Load general modules
348 View Code Duplication
		if ( $data['general']['bottom'] ) {
349
			$chunks[] = ResourceLoader::makeInlineScript(
350
				Xml::encodeJsCall( 'mw.loader.load', [ $data['general']['bottom'] ] )
0 ignored issues
show
Security Bug introduced by
It seems like \Xml::encodeJsCall('mw.l...['general']['bottom'])) targeting Xml::encodeJsCall() can also be of type false; however, ResourceLoader::makeInlineScript() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
351
			);
352
		}
353
354
		return WrappedStringList::join( "\n", $chunks );
355
	}
356
357
	private function getContext( $group, $type ) {
358
		return self::makeContext( $this->context, $group, $type );
359
	}
360
361
	private function getLoad( $modules, $only ) {
362
		return self::makeLoad( $this->context, (array)$modules, $only );
363
	}
364
365
	private static function makeContext( ResourceLoaderContext $mainContext, $group, $type,
366
		array $extraQuery = []
367
	) {
368
		// Create new ResourceLoaderContext so that $extraQuery may trigger isRaw().
369
		$req = new FauxRequest( array_merge( $mainContext->getRequest()->getValues(), $extraQuery ) );
370
		// Set 'only' if not combined
371
		$req->setVal( 'only', $type === ResourceLoaderModule::TYPE_COMBINED ? null : $type );
372
		// Remove user parameter in most cases
373
		if ( $group !== 'user' && $group !== 'private' ) {
374
			$req->setVal( 'user', null );
375
		}
376
		$context = new ResourceLoaderContext( $mainContext->getResourceLoader(), $req );
377
		// Allow caller to setVersion() and setModules()
378
		return new DerivativeResourceLoaderContext( $context );
379
	}
380
381
	/**
382
	 * Explicily load or embed modules on a page.
383
	 *
384
	 * @param ResourceLoaderContext $mainContext
385
	 * @param array $modules One or more module names
386
	 * @param string $only ResourceLoaderModule TYPE_ class constant
387
	 * @param array $extraQuery [optional] Array with extra query parameters for the request
388
	 * @return string|WrappedStringList HTML
389
	 */
390
	public static function makeLoad( ResourceLoaderContext $mainContext, array $modules, $only,
391
		array $extraQuery = []
392
	) {
393
		$rl = $mainContext->getResourceLoader();
394
		$chunks = [];
395
396
		if ( $mainContext->getDebug() && count( $modules ) > 1 ) {
397
			$chunks = [];
398
			// Recursively call us for every item
399
			foreach ( $modules as $name ) {
400
				$chunks[] = self::makeLoad( $mainContext, [ $name ], $only, $extraQuery );
401
			}
402
			return new WrappedStringList( "\n", $chunks );
403
		}
404
405
		// Sort module names so requests are more uniform
406
		sort( $modules );
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 );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $module is correct as $rl->getModule($name) (which targets ResourceLoader::getModule()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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
				if ( $group === 'private' ) {
423
					// Decide whether to use style or script element
424
					if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
425
						$chunks[] = Html::inlineStyle(
426
							$rl->makeModuleResponse( $context, $grpModules )
427
						);
428
					} else {
429
						$chunks[] = ResourceLoader::makeInlineScript(
430
							$rl->makeModuleResponse( $context, $grpModules )
431
						);
432
					}
433
					continue;
434
				}
435
436
				// See if we have one or more raw modules
437
				$isRaw = false;
438
				foreach ( $grpModules as $key => $module ) {
439
					$isRaw |= $module->isRaw();
440
				}
441
442
				// Special handling for the user group; because users might change their stuff
443
				// on-wiki like user pages, or user preferences; we need to find the highest
444
				// timestamp of these user-changeable modules so we can ensure cache misses on change
445
				// This should NOT be done for the site group (bug 27564) because anons get that too
446
				// and we shouldn't be putting timestamps in CDN-cached HTML
447
				if ( $group === 'user' ) {
448
					$version = $rl->getCombinedVersion( $context, array_keys( $grpModules ) );
449
					$context->setVersion( $version );
0 ignored issues
show
Security Bug introduced by
It seems like $version defined by $rl->getCombinedVersion(...rray_keys($grpModules)) on line 448 can also be of type false; however, DerivativeResourceLoaderContext::setVersion() does only seem to accept string|null, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
450
				}
451
452
				$context->setModules( array_keys( $grpModules ) );
453
				$url = $rl->createLoaderURL( $source, $context, $extraQuery );
454
455
				// Decide whether to use 'style' or 'script' element
456
				if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
457
					$chunk = Html::linkedStyle( $url );
458
				} else {
459
					if ( $context->getRaw() || $isRaw ) {
460
						$chunk = Html::element( 'script', [
461
							// In SpecialJavaScriptTest, QUnit must load synchronous
462
							'async' => !isset( $extraQuery['sync'] ),
463
							'src' => $url
464
						] );
465
					} else {
466
						$chunk = ResourceLoader::makeInlineScript(
467
							Xml::encodeJsCall( 'mw.loader.load', [ $url ] )
0 ignored issues
show
Security Bug introduced by
It seems like \Xml::encodeJsCall('mw.loader.load', array($url)) targeting Xml::encodeJsCall() can also be of type false; however, ResourceLoader::makeInlineScript() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
468
						);
469
					}
470
				}
471
472
				if ( $group == 'noscript' ) {
473
					$chunks[] = Html::rawElement( 'noscript', [], $chunk );
0 ignored issues
show
Bug introduced by
It seems like $chunk defined by \ResourceLoader::makeInl...er.load', array($url))) on line 466 can also be of type object<WrappedString\WrappedString>; however, Html::rawElement() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
474
				} else {
475
					$chunks[] = $chunk;
476
				}
477
			}
478
		}
479
480
		return new WrappedStringList( "\n", $chunks );
481
	}
482
}
483