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/ResourceLoaderImageModule.php (8 issues)

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 for generated and embedded images.
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
 */
23
24
/**
25
 * ResourceLoader module for generated and embedded images.
26
 *
27
 * @since 1.25
28
 */
29
class ResourceLoaderImageModule extends ResourceLoaderModule {
30
31
	protected $definition = null;
32
33
	/**
34
	 * Local base path, see __construct()
35
	 * @var string
36
	 */
37
	protected $localBasePath = '';
38
39
	protected $origin = self::ORIGIN_CORE_SITEWIDE;
40
41
	protected $images = [];
42
	protected $variants = [];
43
	protected $prefix = null;
44
	protected $selectorWithoutVariant = '.{prefix}-{name}';
45
	protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
46
	protected $targets = [ 'desktop', 'mobile' ];
47
48
	/** @var string Position on the page to load this module at */
49
	protected $position = 'bottom';
50
51
	/**
52
	 * Constructs a new module from an options array.
53
	 *
54
	 * @param array $options List of options; if not given or empty, an empty module will be
55
	 *     constructed
56
	 * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults
57
	 *     to $IP
58
	 *
59
	 * Below is a description for the $options array:
60
	 * @par Construction options:
61
	 * @code
62
	 *     [
63
	 *         // Base path to prepend to all local paths in $options. Defaults to $IP
64
	 *         'localBasePath' => [base path],
65
	 *         // Path to JSON file that contains any of the settings below
66
	 *         'data' => [file path string]
67
	 *         // CSS class prefix to use in all style rules
68
	 *         'prefix' => [CSS class prefix],
69
	 *         // Alternatively: Format of CSS selector to use in all style rules
70
	 *         'selector' => [CSS selector template, variables: {prefix} {name} {variant}],
71
	 *         // Alternatively: When using variants
72
	 *         'selectorWithoutVariant' => [CSS selector template, variables: {prefix} {name}],
73
	 *         'selectorWithVariant' => [CSS selector template, variables: {prefix} {name} {variant}],
74
	 *         // List of variants that may be used for the image files
75
	 *         'variants' => [
76
	 *             [theme name] => [
77
	 *                 [variant name] => [
78
	 *                     'color' => [color string, e.g. '#ffff00'],
79
	 *                     'global' => [boolean, if true, this variant is available
80
	 *                                  for all images of this type],
81
	 *                 ],
82
	 *                 ...
83
	 *             ],
84
	 *             ...
85
	 *         ],
86
	 *         // List of image files and their options
87
	 *         'images' => [
88
	 *             [theme name] => [
89
	 *                 [icon name] => [
90
	 *                     'file' => [file path string or array whose values are file path strings
91
	 *                                    and whose keys are 'default', 'ltr', 'rtl', a single
92
	 *                                    language code like 'en', or a list of language codes like
93
	 *                                    'en,de,ar'],
94
	 *                     'variants' => [array of variant name strings, variants
95
	 *                                    available for this image],
96
	 *                 ],
97
	 *                 ...
98
	 *             ],
99
	 *             ...
100
	 *         ],
101
	 *     ]
102
	 * @endcode
103
	 * @throws InvalidArgumentException
104
	 */
105
	public function __construct( $options = [], $localBasePath = null ) {
106
		$this->localBasePath = self::extractLocalBasePath( $options, $localBasePath );
107
108
		$this->definition = $options;
109
	}
110
111
	/**
112
	 * Parse definition and external JSON data, if referenced.
113
	 */
114
	protected function loadFromDefinition() {
115
		if ( $this->definition === null ) {
116
			return;
117
		}
118
119
		$options = $this->definition;
120
		$this->definition = null;
121
122
		if ( isset( $options['data'] ) ) {
123
			$dataPath = $this->localBasePath . '/' . $options['data'];
124
			$data = json_decode( file_get_contents( $dataPath ), true );
125
			$options = array_merge( $data, $options );
126
		}
127
128
		// Accepted combinations:
129
		// * prefix
130
		// * selector
131
		// * selectorWithoutVariant + selectorWithVariant
132
		// * prefix + selector
133
		// * prefix + selectorWithoutVariant + selectorWithVariant
134
135
		$prefix = isset( $options['prefix'] ) && $options['prefix'];
136
		$selector = isset( $options['selector'] ) && $options['selector'];
137
		$selectorWithoutVariant = isset( $options['selectorWithoutVariant'] )
138
			&& $options['selectorWithoutVariant'];
139
		$selectorWithVariant = isset( $options['selectorWithVariant'] )
140
			&& $options['selectorWithVariant'];
141
142
		if ( $selectorWithoutVariant && !$selectorWithVariant ) {
143
			throw new InvalidArgumentException(
144
				"Given 'selectorWithoutVariant' but no 'selectorWithVariant'."
145
			);
146
		}
147
		if ( $selectorWithVariant && !$selectorWithoutVariant ) {
148
			throw new InvalidArgumentException(
149
				"Given 'selectorWithVariant' but no 'selectorWithoutVariant'."
150
			);
151
		}
152
		if ( $selector && $selectorWithVariant ) {
153
			throw new InvalidArgumentException(
154
				"Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given."
155
			);
156
		}
157
		if ( !$prefix && !$selector && !$selectorWithVariant ) {
158
			throw new InvalidArgumentException(
159
				"None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given."
160
			);
161
		}
162
163
		foreach ( $options as $member => $option ) {
164
			switch ( $member ) {
165
				case 'images':
166
				case 'variants':
167
					if ( !is_array( $option ) ) {
168
						throw new InvalidArgumentException(
169
							"Invalid list error. '$option' given, array expected."
170
						);
171
					}
172
					if ( !isset( $option['default'] ) ) {
173
						// Backwards compatibility
174
						$option = [ 'default' => $option ];
175
					}
176
					foreach ( $option as $skin => $data ) {
177
						if ( !is_array( $option ) ) {
178
							throw new InvalidArgumentException(
179
								"Invalid list error. '$option' given, array expected."
180
							);
181
						}
182
					}
183
					$this->{$member} = $option;
184
					break;
185
186
				case 'position':
187
				case 'prefix':
188
				case 'selectorWithoutVariant':
189
				case 'selectorWithVariant':
190
					$this->{$member} = (string)$option;
191
					break;
192
193
				case 'selector':
194
					$this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option;
195
			}
196
		}
197
	}
198
199
	/**
200
	 * Get CSS class prefix used by this module.
201
	 * @return string
202
	 */
203
	public function getPrefix() {
204
		$this->loadFromDefinition();
205
		return $this->prefix;
206
	}
207
208
	/**
209
	 * Get CSS selector templates used by this module.
210
	 * @return string
211
	 */
212
	public function getSelectors() {
213
		$this->loadFromDefinition();
214
		return [
215
			'selectorWithoutVariant' => $this->selectorWithoutVariant,
216
			'selectorWithVariant' => $this->selectorWithVariant,
217
		];
218
	}
219
220
	/**
221
	 * Get a ResourceLoaderImage object for given image.
222
	 * @param string $name Image name
223
	 * @param ResourceLoaderContext $context
224
	 * @return ResourceLoaderImage|null
225
	 */
226
	public function getImage( $name, ResourceLoaderContext $context ) {
227
		$this->loadFromDefinition();
228
		$images = $this->getImages( $context );
229
		return isset( $images[$name] ) ? $images[$name] : null;
230
	}
231
232
	/**
233
	 * Get ResourceLoaderImage objects for all images.
234
	 * @param ResourceLoaderContext $context
235
	 * @return ResourceLoaderImage[] Array keyed by image name
236
	 */
237
	public function getImages( ResourceLoaderContext $context ) {
238
		$skin = $context->getSkin();
239
		if ( !isset( $this->imageObjects ) ) {
240
			$this->loadFromDefinition();
241
			$this->imageObjects = [];
0 ignored issues
show
The property imageObjects does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
242
		}
243
		if ( !isset( $this->imageObjects[$skin] ) ) {
244
			$this->imageObjects[$skin] = [];
245 View Code Duplication
			if ( !isset( $this->images[$skin] ) ) {
246
				$this->images[$skin] = isset( $this->images['default'] ) ?
247
					$this->images['default'] :
248
					[];
249
			}
250
			foreach ( $this->images[$skin] as $name => $options ) {
251
				$fileDescriptor = is_string( $options ) ? $options : $options['file'];
252
253
				$allowedVariants = array_merge(
254
					is_array( $options ) && isset( $options['variants'] ) ? $options['variants'] : [],
255
					$this->getGlobalVariants( $context )
256
				);
257
				if ( isset( $this->variants[$skin] ) ) {
258
					$variantConfig = array_intersect_key(
259
						$this->variants[$skin],
260
						array_fill_keys( $allowedVariants, true )
261
					);
262
				} else {
263
					$variantConfig = [];
264
				}
265
266
				$image = new ResourceLoaderImage(
267
					$name,
268
					$this->getName(),
269
					$fileDescriptor,
270
					$this->localBasePath,
271
					$variantConfig
272
				);
273
				$this->imageObjects[$skin][$image->getName()] = $image;
274
			}
275
		}
276
277
		return $this->imageObjects[$skin];
278
	}
279
280
	/**
281
	 * Get list of variants in this module that are 'global', i.e., available
282
	 * for every image regardless of image options.
283
	 * @param ResourceLoaderContext $context
284
	 * @return string[]
285
	 */
286
	public function getGlobalVariants( ResourceLoaderContext $context ) {
287
		$skin = $context->getSkin();
288
		if ( !isset( $this->globalVariants ) ) {
289
			$this->loadFromDefinition();
290
			$this->globalVariants = [];
0 ignored issues
show
The property globalVariants does not seem to exist. Did you mean variants?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
291
		}
292
		if ( !isset( $this->globalVariants[$skin] ) ) {
0 ignored issues
show
The property globalVariants does not seem to exist. Did you mean variants?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
293
			$this->globalVariants[$skin] = [];
0 ignored issues
show
The property globalVariants does not seem to exist. Did you mean variants?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
294 View Code Duplication
			if ( !isset( $this->variants[$skin] ) ) {
295
				$this->variants[$skin] = isset( $this->variants['default'] ) ?
296
					$this->variants['default'] :
297
					[];
298
			}
299
			foreach ( $this->variants[$skin] as $name => $config ) {
300
				if ( isset( $config['global'] ) && $config['global'] ) {
301
					$this->globalVariants[$skin][] = $name;
0 ignored issues
show
The property globalVariants does not seem to exist. Did you mean variants?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
302
				}
303
			}
304
		}
305
306
		return $this->globalVariants[$skin];
0 ignored issues
show
The property globalVariants does not seem to exist. Did you mean variants?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
307
	}
308
309
	/**
310
	 * @param ResourceLoaderContext $context
311
	 * @return array
312
	 */
313
	public function getStyles( ResourceLoaderContext $context ) {
314
		$this->loadFromDefinition();
315
316
		// Build CSS rules
317
		$rules = [];
318
		$script = $context->getResourceLoader()->getLoadScript( $this->getSource() );
319
		$selectors = $this->getSelectors();
320
321
		foreach ( $this->getImages( $context ) as $name => $image ) {
322
			$declarations = $this->getCssDeclarations(
323
				$image->getDataUri( $context, null, 'original' ),
0 ignored issues
show
It seems like $image->getDataUri($context, null, 'original') targeting ResourceLoaderImage::getDataUri() can also be of type false; however, ResourceLoaderImageModule::getCssDeclarations() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
324
				$image->getUrl( $context, $script, null, 'rasterized' )
325
			);
326
			$declarations = implode( "\n\t", $declarations );
327
			$selector = strtr(
328
				$selectors['selectorWithoutVariant'],
329
				[
330
					'{prefix}' => $this->getPrefix(),
331
					'{name}' => $name,
332
					'{variant}' => '',
333
				]
334
			);
335
			$rules[] = "$selector {\n\t$declarations\n}";
336
337
			foreach ( $image->getVariants() as $variant ) {
338
				$declarations = $this->getCssDeclarations(
339
					$image->getDataUri( $context, $variant, 'original' ),
0 ignored issues
show
It seems like $image->getDataUri($cont..., $variant, 'original') targeting ResourceLoaderImage::getDataUri() can also be of type false; however, ResourceLoaderImageModule::getCssDeclarations() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
340
					$image->getUrl( $context, $script, $variant, 'rasterized' )
341
				);
342
				$declarations = implode( "\n\t", $declarations );
343
				$selector = strtr(
344
					$selectors['selectorWithVariant'],
345
					[
346
						'{prefix}' => $this->getPrefix(),
347
						'{name}' => $name,
348
						'{variant}' => $variant,
349
					]
350
				);
351
				$rules[] = "$selector {\n\t$declarations\n}";
352
			}
353
		}
354
355
		$style = implode( "\n", $rules );
356
		return [ 'all' => $style ];
357
	}
358
359
	/**
360
	 * SVG support using a transparent gradient to guarantee cross-browser
361
	 * compatibility (browsers able to understand gradient syntax support also SVG).
362
	 * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique
363
	 *
364
	 * Keep synchronized with the .background-image-svg LESS mixin in
365
	 * /resources/src/mediawiki.less/mediawiki.mixins.less.
366
	 *
367
	 * @param string $primary Primary URI
368
	 * @param string $fallback Fallback URI
369
	 * @return string[] CSS declarations to use given URIs as background-image
370
	 */
371
	protected function getCssDeclarations( $primary, $fallback ) {
372
		return [
373
			"background-image: url($fallback);",
374
			"background-image: linear-gradient(transparent, transparent), url($primary);",
375
			// Do not serve SVG to Opera 12, bad rendering with border-radius or background-size (T87504)
376
			"background-image: -o-linear-gradient(transparent, transparent), url($fallback);",
377
		];
378
	}
379
380
	/**
381
	 * @return bool
382
	 */
383
	public function supportsURLLoading() {
384
		return false;
385
	}
386
387
	/**
388
	 * Get the definition summary for this module.
389
	 *
390
	 * @param ResourceLoaderContext $context
391
	 * @return array
392
	 */
393
	public function getDefinitionSummary( ResourceLoaderContext $context ) {
394
		$this->loadFromDefinition();
395
		$summary = parent::getDefinitionSummary( $context );
396
397
		$options = [];
398
		foreach ( [
399
			'localBasePath',
400
			'images',
401
			'variants',
402
			'prefix',
403
			'selectorWithoutVariant',
404
			'selectorWithVariant',
405
		] as $member ) {
406
			$options[$member] = $this->{$member};
407
		};
408
409
		$summary[] = [
410
			'options' => $options,
411
			'fileHashes' => $this->getFileHashes( $context ),
412
		];
413
		return $summary;
414
	}
415
416
	/**
417
	 * Helper method for getDefinitionSummary.
418
	 */
419
	protected function getFileHashes( ResourceLoaderContext $context ) {
420
		$this->loadFromDefinition();
421
		$files = [];
422
		foreach ( $this->getImages( $context ) as $name => $image ) {
423
			$files[] = $image->getPath( $context );
424
		}
425
		$files = array_values( array_unique( $files ) );
426
		return array_map( [ __CLASS__, 'safeFileHash' ], $files );
427
	}
428
429
	/**
430
	 * Extract a local base path from module definition information.
431
	 *
432
	 * @param array $options Module definition
433
	 * @param string $localBasePath Path to use if not provided in module definition. Defaults
434
	 *     to $IP
435
	 * @return string Local base path
436
	 */
437
	public static function extractLocalBasePath( $options, $localBasePath = null ) {
438
		global $IP;
439
440
		if ( $localBasePath === null ) {
441
			$localBasePath = $IP;
442
		}
443
444
		if ( array_key_exists( 'localBasePath', $options ) ) {
445
			$localBasePath = (string)$options['localBasePath'];
446
		}
447
448
		return $localBasePath;
449
	}
450
451
	/**
452
	 * @return string
453
	 */
454
	public function getPosition() {
455
		$this->loadFromDefinition();
456
		return $this->position;
457
	}
458
459
	/**
460
	 * @return string
461
	 */
462
	public function getType() {
463
		return self::LOAD_STYLES;
464
	}
465
}
466