Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

resourceloader/ResourceLoaderFileModule.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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