ResourceLoaderFileModule   F
last analyzed

Complexity

Total Complexity 131

Size/Duplication

Total Lines 1020
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
dl 0
loc 1020
rs 1.1368
c 0
b 0
f 0
wmc 131
lcom 1
cbo 15

36 Methods

Rating   Name   Duplication   Size   Complexity  
D __construct() 0 88 27
C extractBasePaths() 0 37 7
A getScript() 0 4 1
A getScriptURLsForDebug() 0 10 2
A supportsURLLoading() 0 3 1
A getStyles() 0 11 1
A getStyleURLsForDebug() 0 20 4
A getMessages() 0 3 1
A getGroup() 0 3 1
A getPosition() 0 3 1
A getDependencies() 0 3 1
A getSkipFunction() 0 15 4
A isRaw() 0 3 1
A enableModuleContentVersion() 0 3 1
B getFileHashes() 0 43 5
B getDefinitionSummary() 0 35 2
A getLocalPath() 0 7 2
A getRemotePath() 0 7 2
A getStyleSheetLang() 0 3 2
B collateFilePathListByOption() 0 20 7
B tryForKey() 0 11 6
A getScriptFiles() 0 12 2
A getLanguageScripts() 0 15 4
A getStyleFiles() 0 10 1
A getSkinStyleFiles() 0 7 1
A getAllSkinStyleFiles() 0 14 2
A getAllStyleFiles() 0 16 3
B readScriptFiles() 0 21 5
B readStyleFiles() 0 19 5
B readStyleFile() 0 34 6
A getFlip() 0 3 2
A getTargets() 0 3 1
B getType() 0 15 10
B compileLessFile() 0 39 4
A getTemplates() 0 20 4
A stripBom() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like ResourceLoaderFileModule often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ResourceLoaderFileModule, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * ResourceLoader module based on local JavaScript/CSS files.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @author Trevor Parscal
22
 * @author Roan Kattouw
23
 */
24
25
/**
26
 * ResourceLoader module based on local JavaScript/CSS files.
27
 */
28
class ResourceLoaderFileModule extends ResourceLoaderModule {
29
	/* Protected Members */
30
31
	/** @var string Local base path, see __construct() */
32
	protected $localBasePath = '';
33
34
	/** @var string Remote base path, see __construct() */
35
	protected $remoteBasePath = '';
36
37
	/** @var array Saves a list of the templates named by the modules. */
38
	protected $templates = [];
39
40
	/**
41
	 * @var array List of paths to JavaScript files to always include
42
	 * @par Usage:
43
	 * @code
44
	 * [ [file-path], [file-path], ... ]
45
	 * @endcode
46
	 */
47
	protected $scripts = [];
48
49
	/**
50
	 * @var array List of JavaScript files to include when using a specific language
51
	 * @par Usage:
52
	 * @code
53
	 * [ [language-code] => [ [file-path], [file-path], ... ], ... ]
54
	 * @endcode
55
	 */
56
	protected $languageScripts = [];
57
58
	/**
59
	 * @var array List of JavaScript files to include when using a specific skin
60
	 * @par Usage:
61
	 * @code
62
	 * [ [skin-name] => [ [file-path], [file-path], ... ], ... ]
63
	 * @endcode
64
	 */
65
	protected $skinScripts = [];
66
67
	/**
68
	 * @var array List of paths to JavaScript files to include in debug mode
69
	 * @par Usage:
70
	 * @code
71
	 * [ [skin-name] => [ [file-path], [file-path], ... ], ... ]
72
	 * @endcode
73
	 */
74
	protected $debugScripts = [];
75
76
	/**
77
	 * @var array List of paths to CSS files to always include
78
	 * @par Usage:
79
	 * @code
80
	 * [ [file-path], [file-path], ... ]
81
	 * @endcode
82
	 */
83
	protected $styles = [];
84
85
	/**
86
	 * @var array List of paths to CSS files to include when using specific skins
87
	 * @par Usage:
88
	 * @code
89
	 * [ [file-path], [file-path], ... ]
90
	 * @endcode
91
	 */
92
	protected $skinStyles = [];
93
94
	/**
95
	 * @var array List of modules this module depends on
96
	 * @par Usage:
97
	 * @code
98
	 * [ [file-path], [file-path], ... ]
99
	 * @endcode
100
	 */
101
	protected $dependencies = [];
102
103
	/**
104
	 * @var string File name containing the body of the skip function
105
	 */
106
	protected $skipFunction = null;
107
108
	/**
109
	 * @var array List of message keys used by this module
110
	 * @par Usage:
111
	 * @code
112
	 * [ [message-key], [message-key], ... ]
113
	 * @endcode
114
	 */
115
	protected $messages = [];
116
117
	/** @var string Name of group to load this module in */
118
	protected $group;
119
120
	/** @var string Position on the page to load this module at */
121
	protected $position = 'bottom';
122
123
	/** @var bool Link to raw files in debug mode */
124
	protected $debugRaw = true;
125
126
	/** @var bool Whether mw.loader.state() call should be omitted */
127
	protected $raw = false;
128
129
	protected $targets = [ 'desktop' ];
130
131
	/** @var bool Whether CSSJanus flipping should be skipped for this module */
132
	protected $noflip = false;
133
134
	/**
135
	 * @var bool Whether getStyleURLsForDebug should return raw file paths,
136
	 * or return load.php urls
137
	 */
138
	protected $hasGeneratedStyles = false;
139
140
	/**
141
	 * @var array Place where readStyleFile() tracks file dependencies
142
	 * @par Usage:
143
	 * @code
144
	 * [ [file-path], [file-path], ... ]
145
	 * @endcode
146
	 */
147
	protected $localFileRefs = [];
148
149
	/**
150
	 * @var array Place where readStyleFile() tracks file dependencies for non-existent files.
151
	 * Used in tests to detect missing dependencies.
152
	 */
153
	protected $missingLocalFileRefs = [];
154
155
	/* Methods */
156
157
	/**
158
	 * Constructs a new module from an options array.
159
	 *
160
	 * @param array $options List of options; if not given or empty, an empty module will be
161
	 *     constructed
162
	 * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults
163
	 *     to $IP
164
	 * @param string $remoteBasePath Base path to prepend to all remote paths in $options. Defaults
165
	 *     to $wgResourceBasePath
166
	 *
167
	 * Below is a description for the $options array:
168
	 * @throws InvalidArgumentException
169
	 * @par Construction options:
170
	 * @code
171
	 *     [
172
	 *         // Base path to prepend to all local paths in $options. Defaults to $IP
173
	 *         'localBasePath' => [base path],
174
	 *         // Base path to prepend to all remote paths in $options. Defaults to $wgResourceBasePath
175
	 *         'remoteBasePath' => [base path],
176
	 *         // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath
177
	 *         'remoteExtPath' => [base path],
178
	 *         // Equivalent of remoteBasePath, but relative to $wgStylePath
179
	 *         'remoteSkinPath' => [base path],
180
	 *         // Scripts to always include
181
	 *         'scripts' => [file path string or array of file path strings],
182
	 *         // Scripts to include in specific language contexts
183
	 *         'languageScripts' => [
184
	 *             [language code] => [file path string or array of file path strings],
185
	 *         ],
186
	 *         // Scripts to include in specific skin contexts
187
	 *         'skinScripts' => [
188
	 *             [skin name] => [file path string or array of file path strings],
189
	 *         ],
190
	 *         // Scripts to include in debug contexts
191
	 *         'debugScripts' => [file path string or array of file path strings],
192
	 *         // Modules which must be loaded before this module
193
	 *         'dependencies' => [module name string or array of module name strings],
194
	 *         'templates' => [
195
	 *             [template alias with file.ext] => [file path to a template file],
196
	 *         ],
197
	 *         // Styles to always load
198
	 *         'styles' => [file path string or array of file path strings],
199
	 *         // Styles to include in specific skin contexts
200
	 *         'skinStyles' => [
201
	 *             [skin name] => [file path string or array of file path strings],
202
	 *         ],
203
	 *         // Messages to always load
204
	 *         'messages' => [array of message key strings],
205
	 *         // Group which this module should be loaded together with
206
	 *         'group' => [group name string],
207
	 *         // Position on the page to load this module at
208
	 *         'position' => ['bottom' (default) or 'top']
209
	 *         // Function that, if it returns true, makes the loader skip this module.
210
	 *         // The file must contain valid JavaScript for execution in a private function.
211
	 *         // The file must not contain the "function () {" and "}" wrapper though.
212
	 *         'skipFunction' => [file path]
213
	 *     ]
214
	 * @endcode
215
	 */
216
	public function __construct(
217
		$options = [],
218
		$localBasePath = null,
219
		$remoteBasePath = null
220
	) {
221
		// Flag to decide whether to automagically add the mediawiki.template module
222
		$hasTemplates = false;
223
		// localBasePath and remoteBasePath both have unbelievably long fallback chains
224
		// and need to be handled separately.
225
		list( $this->localBasePath, $this->remoteBasePath ) =
226
			self::extractBasePaths( $options, $localBasePath, $remoteBasePath );
227
228
		// Extract, validate and normalise remaining options
229
		foreach ( $options as $member => $option ) {
230
			switch ( $member ) {
231
				// Lists of file paths
232
				case 'scripts':
233
				case 'debugScripts':
234
				case 'styles':
235
					$this->{$member} = (array)$option;
236
					break;
237
				case 'templates':
238
					$hasTemplates = true;
239
					$this->{$member} = (array)$option;
240
					break;
241
				// Collated lists of file paths
242
				case 'languageScripts':
243
				case 'skinScripts':
244
				case 'skinStyles':
245
					if ( !is_array( $option ) ) {
246
						throw new InvalidArgumentException(
247
							"Invalid collated file path list error. " .
248
							"'$option' given, array expected."
249
						);
250
					}
251
					foreach ( $option as $key => $value ) {
252
						if ( !is_string( $key ) ) {
253
							throw new InvalidArgumentException(
254
								"Invalid collated file path list key error. " .
255
								"'$key' given, string expected."
256
							);
257
						}
258
						$this->{$member}[$key] = (array)$value;
259
					}
260
					break;
261
				case 'deprecated':
262
					$this->deprecated = $option;
263
					break;
264
				// Lists of strings
265
				case 'dependencies':
266
				case 'messages':
267
				case 'targets':
268
					// Normalise
269
					$option = array_values( array_unique( (array)$option ) );
270
					sort( $option );
271
272
					$this->{$member} = $option;
273
					break;
274
				// Single strings
275
				case 'position':
276
				case 'group':
277
				case 'skipFunction':
278
					$this->{$member} = (string)$option;
279
					break;
280
				// Single booleans
281
				case 'debugRaw':
282
				case 'raw':
283
				case 'noflip':
284
					$this->{$member} = (bool)$option;
285
					break;
286
			}
287
		}
288
		if ( $hasTemplates ) {
289
			$this->dependencies[] = 'mediawiki.template';
290
			// Ensure relevant template compiler module gets loaded
291
			foreach ( $this->templates as $alias => $templatePath ) {
292
				if ( is_int( $alias ) ) {
293
					$alias = $templatePath;
294
				}
295
				$suffix = explode( '.', $alias );
296
				$suffix = end( $suffix );
297
				$compilerModule = 'mediawiki.template.' . $suffix;
298
				if ( $suffix !== 'html' && !in_array( $compilerModule, $this->dependencies ) ) {
299
					$this->dependencies[] = $compilerModule;
300
				}
301
			}
302
		}
303
	}
304
305
	/**
306
	 * Extract a pair of local and remote base paths from module definition information.
307
	 * Implementation note: the amount of global state used in this function is staggering.
308
	 *
309
	 * @param array $options Module definition
310
	 * @param string $localBasePath Path to use if not provided in module definition. Defaults
311
	 *     to $IP
312
	 * @param string $remoteBasePath Path to use if not provided in module definition. Defaults
313
	 *     to $wgResourceBasePath
314
	 * @return array Array( localBasePath, remoteBasePath )
315
	 */
316
	public static function extractBasePaths(
317
		$options = [],
318
		$localBasePath = null,
319
		$remoteBasePath = null
320
	) {
321
		global $IP, $wgResourceBasePath;
322
323
		// The different ways these checks are done, and their ordering, look very silly,
324
		// but were preserved for backwards-compatibility just in case. Tread lightly.
325
326
		if ( $localBasePath === null ) {
327
			$localBasePath = $IP;
328
		}
329
		if ( $remoteBasePath === null ) {
330
			$remoteBasePath = $wgResourceBasePath;
331
		}
332
333
		if ( isset( $options['remoteExtPath'] ) ) {
334
			global $wgExtensionAssetsPath;
335
			$remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
336
		}
337
338
		if ( isset( $options['remoteSkinPath'] ) ) {
339
			global $wgStylePath;
340
			$remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath'];
341
		}
342
343
		if ( array_key_exists( 'localBasePath', $options ) ) {
344
			$localBasePath = (string)$options['localBasePath'];
345
		}
346
347
		if ( array_key_exists( 'remoteBasePath', $options ) ) {
348
			$remoteBasePath = (string)$options['remoteBasePath'];
349
		}
350
351
		return [ $localBasePath, $remoteBasePath ];
352
	}
353
354
	/**
355
	 * Gets all scripts for a given context concatenated together.
356
	 *
357
	 * @param ResourceLoaderContext $context Context in which to generate script
358
	 * @return string JavaScript code for $context
359
	 */
360
	public function getScript( ResourceLoaderContext $context ) {
361
		$files = $this->getScriptFiles( $context );
362
		return $this->getDeprecationInformation() . $this->readScriptFiles( $files );
363
	}
364
365
	/**
366
	 * @param ResourceLoaderContext $context
367
	 * @return array
368
	 */
369
	public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
370
		$urls = [];
371
		foreach ( $this->getScriptFiles( $context ) as $file ) {
372
			$urls[] = OutputPage::transformResourcePath(
373
				$this->getConfig(),
374
				$this->getRemotePath( $file )
375
			);
376
		}
377
		return $urls;
378
	}
379
380
	/**
381
	 * @return bool
382
	 */
383
	public function supportsURLLoading() {
384
		return $this->debugRaw;
385
	}
386
387
	/**
388
	 * Get all styles for a given context.
389
	 *
390
	 * @param ResourceLoaderContext $context
391
	 * @return array CSS code for $context as an associative array mapping media type to CSS text.
392
	 */
393
	public function getStyles( ResourceLoaderContext $context ) {
394
		$styles = $this->readStyleFiles(
395
			$this->getStyleFiles( $context ),
396
			$this->getFlip( $context ),
397
			$context
398
		);
399
		// Collect referenced files
400
		$this->saveFileDependencies( $context, $this->localFileRefs );
401
402
		return $styles;
403
	}
404
405
	/**
406
	 * @param ResourceLoaderContext $context
407
	 * @return array
408
	 */
409
	public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
410
		if ( $this->hasGeneratedStyles ) {
411
			// Do the default behaviour of returning a url back to load.php
412
			// but with only=styles.
413
			return parent::getStyleURLsForDebug( $context );
414
		}
415
		// Our module consists entirely of real css files,
416
		// in debug mode we can load those directly.
417
		$urls = [];
418
		foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
419
			$urls[$mediaType] = [];
420
			foreach ( $list as $file ) {
421
				$urls[$mediaType][] = OutputPage::transformResourcePath(
422
					$this->getConfig(),
423
					$this->getRemotePath( $file )
424
				);
425
			}
426
		}
427
		return $urls;
428
	}
429
430
	/**
431
	 * Gets list of message keys used by this module.
432
	 *
433
	 * @return array List of message keys
434
	 */
435
	public function getMessages() {
436
		return $this->messages;
437
	}
438
439
	/**
440
	 * Gets the name of the group this module should be loaded in.
441
	 *
442
	 * @return string Group name
443
	 */
444
	public function getGroup() {
445
		return $this->group;
446
	}
447
448
	/**
449
	 * @return string
450
	 */
451
	public function getPosition() {
452
		return $this->position;
453
	}
454
455
	/**
456
	 * Gets list of names of modules this module depends on.
457
	 * @param ResourceLoaderContext|null $context
458
	 * @return array List of module names
459
	 */
460
	public function getDependencies( ResourceLoaderContext $context = null ) {
461
		return $this->dependencies;
462
	}
463
464
	/**
465
	 * Get the skip function.
466
	 * @return null|string
467
	 * @throws MWException
468
	 */
469
	public function getSkipFunction() {
470
		if ( !$this->skipFunction ) {
471
			return null;
472
		}
473
474
		$localPath = $this->getLocalPath( $this->skipFunction );
475
		if ( !file_exists( $localPath ) ) {
476
			throw new MWException( __METHOD__ . ": skip function file not found: \"$localPath\"" );
477
		}
478
		$contents = $this->stripBom( file_get_contents( $localPath ) );
479
		if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) {
480
			$contents = $this->validateScriptFile( $localPath, $contents );
481
		}
482
		return $contents;
483
	}
484
485
	/**
486
	 * @return bool
487
	 */
488
	public function isRaw() {
489
		return $this->raw;
490
	}
491
492
	/**
493
	 * Disable module content versioning.
494
	 *
495
	 * This class uses getDefinitionSummary() instead, to avoid filesystem overhead
496
	 * involved with building the full module content inside a startup request.
497
	 *
498
	 * @return bool
499
	 */
500
	public function enableModuleContentVersion() {
501
		return false;
502
	}
503
504
	/**
505
	 * Helper method to gather file hashes for getDefinitionSummary.
506
	 *
507
	 * This function is context-sensitive, only computing hashes of files relevant to the
508
	 * given language, skin, etc.
509
	 *
510
	 * @see ResourceLoaderModule::getFileDependencies
511
	 * @param ResourceLoaderContext $context
512
	 * @return array
513
	 */
514
	protected function getFileHashes( ResourceLoaderContext $context ) {
515
		$files = [];
516
517
		// Flatten style files into $files
518
		$styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' );
519
		foreach ( $styles as $styleFiles ) {
520
			$files = array_merge( $files, $styleFiles );
521
		}
522
523
		$skinFiles = self::collateFilePathListByOption(
524
			self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
525
			'media',
526
			'all'
527
		);
528
		foreach ( $skinFiles as $styleFiles ) {
529
			$files = array_merge( $files, $styleFiles );
530
		}
531
532
		// Final merge, this should result in a master list of dependent files
533
		$files = array_merge(
534
			$files,
535
			$this->scripts,
536
			$this->templates,
537
			$context->getDebug() ? $this->debugScripts : [],
538
			$this->getLanguageScripts( $context->getLanguage() ),
539
			self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
540
		);
541
		if ( $this->skipFunction ) {
542
			$files[] = $this->skipFunction;
543
		}
544
		$files = array_map( [ $this, 'getLocalPath' ], $files );
545
		// File deps need to be treated separately because they're already prefixed
546
		$files = array_merge( $files, $this->getFileDependencies( $context ) );
547
		// Filter out any duplicates from getFileDependencies() and others.
548
		// Most commonly introduced by compileLessFile(), which always includes the
549
		// entry point Less file we already know about.
550
		$files = array_values( array_unique( $files ) );
551
552
		// Don't include keys or file paths here, only the hashes. Including that would needlessly
553
		// cause global cache invalidation when files move or if e.g. the MediaWiki path changes.
554
		// Any significant ordering is already detected by the definition summary.
555
		return array_map( [ __CLASS__, 'safeFileHash' ], $files );
556
	}
557
558
	/**
559
	 * Get the definition summary for this module.
560
	 *
561
	 * @param ResourceLoaderContext $context
562
	 * @return array
563
	 */
564
	public function getDefinitionSummary( ResourceLoaderContext $context ) {
565
		$summary = parent::getDefinitionSummary( $context );
566
567
		$options = [];
568
		foreach ( [
569
			// The following properties are omitted because they don't affect the module reponse:
570
			// - localBasePath (Per T104950; Changes when absolute directory name changes. If
571
			//    this affects 'scripts' and other file paths, getFileHashes accounts for that.)
572
			// - remoteBasePath (Per T104950)
573
			// - dependencies (provided via startup module)
574
			// - targets
575
			// - group (provided via startup module)
576
			// - position (only used by OutputPage)
577
			'scripts',
578
			'debugScripts',
579
			'styles',
580
			'languageScripts',
581
			'skinScripts',
582
			'skinStyles',
583
			'messages',
584
			'templates',
585
			'skipFunction',
586
			'debugRaw',
587
			'raw',
588
		] as $member ) {
589
			$options[$member] = $this->{$member};
590
		};
591
592
		$summary[] = [
593
			'options' => $options,
594
			'fileHashes' => $this->getFileHashes( $context ),
595
			'messageBlob' => $this->getMessageBlob( $context ),
596
		];
597
		return $summary;
598
	}
599
600
	/**
601
	 * @param string|ResourceLoaderFilePath $path
602
	 * @return string
603
	 */
604
	protected function getLocalPath( $path ) {
605
		if ( $path instanceof ResourceLoaderFilePath ) {
606
			return $path->getLocalPath();
607
		}
608
609
		return "{$this->localBasePath}/$path";
610
	}
611
612
	/**
613
	 * @param string|ResourceLoaderFilePath $path
614
	 * @return string
615
	 */
616
	protected function getRemotePath( $path ) {
617
		if ( $path instanceof ResourceLoaderFilePath ) {
618
			return $path->getRemotePath();
619
		}
620
621
		return "{$this->remoteBasePath}/$path";
622
	}
623
624
	/**
625
	 * Infer the stylesheet language from a stylesheet file path.
626
	 *
627
	 * @since 1.22
628
	 * @param string $path
629
	 * @return string The stylesheet language name
630
	 */
631
	public function getStyleSheetLang( $path ) {
632
		return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
633
	}
634
635
	/**
636
	 * Collates file paths by option (where provided).
637
	 *
638
	 * @param array $list List of file paths in any combination of index/path
639
	 *     or path/options pairs
640
	 * @param string $option Option name
641
	 * @param mixed $default Default value if the option isn't set
642
	 * @return array List of file paths, collated by $option
643
	 */
644
	protected static function collateFilePathListByOption( array $list, $option, $default ) {
645
		$collatedFiles = [];
646
		foreach ( (array)$list as $key => $value ) {
647
			if ( is_int( $key ) ) {
648
				// File name as the value
649
				if ( !isset( $collatedFiles[$default] ) ) {
650
					$collatedFiles[$default] = [];
651
				}
652
				$collatedFiles[$default][] = $value;
653
			} elseif ( is_array( $value ) ) {
654
				// File name as the key, options array as the value
655
				$optionValue = isset( $value[$option] ) ? $value[$option] : $default;
656
				if ( !isset( $collatedFiles[$optionValue] ) ) {
657
					$collatedFiles[$optionValue] = [];
658
				}
659
				$collatedFiles[$optionValue][] = $key;
660
			}
661
		}
662
		return $collatedFiles;
663
	}
664
665
	/**
666
	 * Get a list of element that match a key, optionally using a fallback key.
667
	 *
668
	 * @param array $list List of lists to select from
669
	 * @param string $key Key to look for in $map
670
	 * @param string $fallback Key to look for in $list if $key doesn't exist
671
	 * @return array List of elements from $map which matched $key or $fallback,
672
	 *  or an empty list in case of no match
673
	 */
674
	protected static function tryForKey( array $list, $key, $fallback = null ) {
675
		if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
676
			return $list[$key];
677
		} elseif ( is_string( $fallback )
678
			&& isset( $list[$fallback] )
679
			&& is_array( $list[$fallback] )
680
		) {
681
			return $list[$fallback];
682
		}
683
		return [];
684
	}
685
686
	/**
687
	 * Get a list of file paths for all scripts in this module, in order of proper execution.
688
	 *
689
	 * @param ResourceLoaderContext $context
690
	 * @return array List of file paths
691
	 */
692
	protected function getScriptFiles( ResourceLoaderContext $context ) {
693
		$files = array_merge(
694
			$this->scripts,
695
			$this->getLanguageScripts( $context->getLanguage() ),
696
			self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
697
		);
698
		if ( $context->getDebug() ) {
699
			$files = array_merge( $files, $this->debugScripts );
700
		}
701
702
		return array_unique( $files, SORT_REGULAR );
703
	}
704
705
	/**
706
	 * Get the set of language scripts for the given language,
707
	 * possibly using a fallback language.
708
	 *
709
	 * @param string $lang
710
	 * @return array
711
	 */
712
	private function getLanguageScripts( $lang ) {
713
		$scripts = self::tryForKey( $this->languageScripts, $lang );
714
		if ( $scripts ) {
715
			return $scripts;
716
		}
717
		$fallbacks = Language::getFallbacksFor( $lang );
718
		foreach ( $fallbacks as $lang ) {
719
			$scripts = self::tryForKey( $this->languageScripts, $lang );
720
			if ( $scripts ) {
721
				return $scripts;
722
			}
723
		}
724
725
		return [];
726
	}
727
728
	/**
729
	 * Get a list of file paths for all styles in this module, in order of proper inclusion.
730
	 *
731
	 * @param ResourceLoaderContext $context
732
	 * @return array List of file paths
733
	 */
734
	public function getStyleFiles( ResourceLoaderContext $context ) {
735
		return array_merge_recursive(
736
			self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
737
			self::collateFilePathListByOption(
738
				self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
739
				'media',
740
				'all'
741
			)
742
		);
743
	}
744
745
	/**
746
	 * Gets a list of file paths for all skin styles in the module used by
747
	 * the skin.
748
	 *
749
	 * @param string $skinName The name of the skin
750
	 * @return array A list of file paths collated by media type
751
	 */
752
	protected function getSkinStyleFiles( $skinName ) {
753
		return self::collateFilePathListByOption(
754
			self::tryForKey( $this->skinStyles, $skinName ),
755
			'media',
756
			'all'
757
		);
758
	}
759
760
	/**
761
	 * Gets a list of file paths for all skin style files in the module,
762
	 * for all available skins.
763
	 *
764
	 * @return array A list of file paths collated by media type
765
	 */
766
	protected function getAllSkinStyleFiles() {
767
		$styleFiles = [];
768
		$internalSkinNames = array_keys( Skin::getSkinNames() );
769
		$internalSkinNames[] = 'default';
770
771
		foreach ( $internalSkinNames as $internalSkinName ) {
772
			$styleFiles = array_merge_recursive(
773
				$styleFiles,
774
				$this->getSkinStyleFiles( $internalSkinName )
775
			);
776
		}
777
778
		return $styleFiles;
779
	}
780
781
	/**
782
	 * Returns all style files and all skin style files used by this module.
783
	 *
784
	 * @return array
785
	 */
786
	public function getAllStyleFiles() {
787
		$collatedStyleFiles = array_merge_recursive(
788
			self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
789
			$this->getAllSkinStyleFiles()
790
		);
791
792
		$result = [];
793
794
		foreach ( $collatedStyleFiles as $media => $styleFiles ) {
795
			foreach ( $styleFiles as $styleFile ) {
796
				$result[] = $this->getLocalPath( $styleFile );
797
			}
798
		}
799
800
		return $result;
801
	}
802
803
	/**
804
	 * Gets the contents of a list of JavaScript files.
805
	 *
806
	 * @param array $scripts List of file paths to scripts to read, remap and concetenate
807
	 * @throws MWException
808
	 * @return string Concatenated and remapped JavaScript data from $scripts
809
	 */
810
	protected function readScriptFiles( array $scripts ) {
811
		if ( empty( $scripts ) ) {
812
			return '';
813
		}
814
		$js = '';
815
		foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
816
			$localPath = $this->getLocalPath( $fileName );
817
			if ( !file_exists( $localPath ) ) {
818
				throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" );
819
			}
820
			$contents = $this->stripBom( file_get_contents( $localPath ) );
821
			if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) {
822
				// Static files don't really need to be checked as often; unlike
823
				// on-wiki module they shouldn't change unexpectedly without
824
				// admin interference.
825
				$contents = $this->validateScriptFile( $fileName, $contents );
826
			}
827
			$js .= $contents . "\n";
828
		}
829
		return $js;
830
	}
831
832
	/**
833
	 * Gets the contents of a list of CSS files.
834
	 *
835
	 * @param array $styles List of media type/list of file paths pairs, to read, remap and
836
	 * concetenate
837
	 * @param bool $flip
838
	 * @param ResourceLoaderContext $context
839
	 *
840
	 * @throws MWException
841
	 * @return array List of concatenated and remapped CSS data from $styles,
842
	 *     keyed by media type
843
	 *
844
	 * @since 1.27 Calling this method without a ResourceLoaderContext instance
845
	 *   is deprecated.
846
	 */
847
	public function readStyleFiles( array $styles, $flip, $context = null ) {
848
		if ( $context === null ) {
849
			wfDeprecated( __METHOD__ . ' without a ResourceLoader context', '1.27' );
850
			$context = ResourceLoaderContext::newDummyContext();
851
		}
852
853
		if ( empty( $styles ) ) {
854
			return [];
855
		}
856
		foreach ( $styles as $media => $files ) {
857
			$uniqueFiles = array_unique( $files, SORT_REGULAR );
858
			$styleFiles = [];
859
			foreach ( $uniqueFiles as $file ) {
860
				$styleFiles[] = $this->readStyleFile( $file, $flip, $context );
861
			}
862
			$styles[$media] = implode( "\n", $styleFiles );
863
		}
864
		return $styles;
865
	}
866
867
	/**
868
	 * Reads a style file.
869
	 *
870
	 * This method can be used as a callback for array_map()
871
	 *
872
	 * @param string $path File path of style file to read
873
	 * @param bool $flip
874
	 * @param ResourceLoaderContext $context
875
	 *
876
	 * @return string CSS data in script file
877
	 * @throws MWException If the file doesn't exist
878
	 */
879
	protected function readStyleFile( $path, $flip, $context ) {
880
		$localPath = $this->getLocalPath( $path );
881
		$remotePath = $this->getRemotePath( $path );
882
		if ( !file_exists( $localPath ) ) {
883
			$msg = __METHOD__ . ": style file not found: \"$localPath\"";
884
			wfDebugLog( 'resourceloader', $msg );
885
			throw new MWException( $msg );
886
		}
887
888
		if ( $this->getStyleSheetLang( $localPath ) === 'less' ) {
889
			$style = $this->compileLessFile( $localPath, $context );
890
			$this->hasGeneratedStyles = true;
891
		} else {
892
			$style = $this->stripBom( file_get_contents( $localPath ) );
893
		}
894
895
		if ( $flip ) {
896
			$style = CSSJanus::transform( $style, true, false );
897
		}
898
		$localDir = dirname( $localPath );
899
		$remoteDir = dirname( $remotePath );
900
		// Get and register local file references
901
		$localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
902
		foreach ( $localFileRefs as $file ) {
903
			if ( file_exists( $file ) ) {
904
				$this->localFileRefs[] = $file;
905
			} else {
906
				$this->missingLocalFileRefs[] = $file;
907
			}
908
		}
909
		// Don't cache this call. remap() ensures data URIs embeds are up to date,
910
		// and urls contain correct content hashes in their query string. (T128668)
911
		return CSSMin::remap( $style, $localDir, $remoteDir, true );
912
	}
913
914
	/**
915
	 * Get whether CSS for this module should be flipped
916
	 * @param ResourceLoaderContext $context
917
	 * @return bool
918
	 */
919
	public function getFlip( $context ) {
920
		return $context->getDirection() === 'rtl' && !$this->noflip;
921
	}
922
923
	/**
924
	 * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
925
	 *
926
	 * @return array Array of strings
927
	 */
928
	public function getTargets() {
929
		return $this->targets;
930
	}
931
932
	/**
933
	 * Get the module's load type.
934
	 *
935
	 * @since 1.28
936
	 * @return string
937
	 */
938
	public function getType() {
939
		$canBeStylesOnly = !(
940
			// All options except 'styles', 'skinStyles' and 'debugRaw'
941
			$this->scripts
942
			|| $this->debugScripts
943
			|| $this->templates
944
			|| $this->languageScripts
945
			|| $this->skinScripts
946
			|| $this->dependencies
947
			|| $this->messages
948
			|| $this->skipFunction
949
			|| $this->raw
950
		);
951
		return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
952
	}
953
954
	/**
955
	 * Compile a LESS file into CSS.
956
	 *
957
	 * Keeps track of all used files and adds them to localFileRefs.
958
	 *
959
	 * @since 1.22
960
	 * @since 1.27 Added $context paramter.
961
	 * @throws Exception If less.php encounters a parse error
962
	 * @param string $fileName File path of LESS source
963
	 * @param ResourceLoaderContext $context Context in which to generate script
964
	 * @return string CSS source
965
	 */
966
	protected function compileLessFile( $fileName, ResourceLoaderContext $context ) {
967
		static $cache;
968
969
		if ( !$cache ) {
970
			$cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
971
		}
972
973
		// Construct a cache key from the LESS file name and a hash digest
974
		// of the LESS variables used for compilation.
975
		$vars = $this->getLessVars( $context );
976
		ksort( $vars );
977
		$varsHash = hash( 'md4', serialize( $vars ) );
978
		$cacheKey = $cache->makeGlobalKey( 'LESS', $fileName, $varsHash );
979
		$cachedCompile = $cache->get( $cacheKey );
980
981
		// If we got a cached value, we have to validate it by getting a
982
		// checksum of all the files that were loaded by the parser and
983
		// ensuring it matches the cached entry's.
984
		if ( isset( $cachedCompile['hash'] ) ) {
985
			$contentHash = FileContentsHasher::getFileContentsHash( $cachedCompile['files'] );
986
			if ( $contentHash === $cachedCompile['hash'] ) {
987
				$this->localFileRefs = array_merge( $this->localFileRefs, $cachedCompile['files'] );
988
				return $cachedCompile['css'];
989
			}
990
		}
991
992
		$compiler = $context->getResourceLoader()->getLessCompiler( $vars );
993
		$css = $compiler->parseFile( $fileName )->getCss();
0 ignored issues
show
Bug introduced by
The method getCss does only exist in Less_Parser, but not in Less_Tree_Ruleset.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
994
		$files = $compiler->AllParsedFiles();
995
		$this->localFileRefs = array_merge( $this->localFileRefs, $files );
996
997
		$cache->set( $cacheKey, [
998
			'css'   => $css,
999
			'files' => $files,
1000
			'hash'  => FileContentsHasher::getFileContentsHash( $files ),
1001
		], 60 * 60 * 24 );  // 86400 seconds, or 24 hours.
1002
1003
		return $css;
1004
	}
1005
1006
	/**
1007
	 * Takes named templates by the module and returns an array mapping.
1008
	 * @return array of templates mapping template alias to content
1009
	 * @throws MWException
1010
	 */
1011
	public function getTemplates() {
1012
		$templates = [];
1013
1014
		foreach ( $this->templates as $alias => $templatePath ) {
1015
			// Alias is optional
1016
			if ( is_int( $alias ) ) {
1017
				$alias = $templatePath;
1018
			}
1019
			$localPath = $this->getLocalPath( $templatePath );
1020
			if ( file_exists( $localPath ) ) {
1021
				$content = file_get_contents( $localPath );
1022
				$templates[$alias] = $this->stripBom( $content );
1023
			} else {
1024
				$msg = __METHOD__ . ": template file not found: \"$localPath\"";
1025
				wfDebugLog( 'resourceloader', $msg );
1026
				throw new MWException( $msg );
1027
			}
1028
		}
1029
		return $templates;
1030
	}
1031
1032
	/**
1033
	 * Takes an input string and removes the UTF-8 BOM character if present
1034
	 *
1035
	 * We need to remove these after reading a file, because we concatenate our files and
1036
	 * the BOM character is not valid in the middle of a string.
1037
	 * We already assume UTF-8 everywhere, so this should be safe.
1038
	 *
1039
	 * @return string input minus the intial BOM char
1040
	 */
1041
	protected function stripBom( $input ) {
1042
		if ( substr_compare( "\xef\xbb\xbf", $input, 0, 3 ) === 0 ) {
1043
			return substr( $input, 3 );
1044
		}
1045
		return $input;
1046
	}
1047
}
1048