ResourceLoaderImageModule   C
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 437
Duplicated Lines 2.29 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 10
loc 437
rs 5.7894
c 0
b 0
f 0
wmc 65
lcom 1
cbo 4

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
F loadFromDefinition() 0 84 28
A getPrefix() 0 4 1
A getSelectors() 0 7 1
A getImage() 0 5 2
D getImages() 5 42 10
C getGlobalVariants() 5 22 8
B getStyles() 0 45 3
A getCssDeclarations() 0 8 1
A supportsURLLoading() 0 3 1
A extractLocalBasePath() 0 13 3
A getPosition() 0 4 1
A getDefinitionSummary() 0 22 2
A getFileHashes() 0 9 2
A getType() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ResourceLoaderImageModule 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 ResourceLoaderImageModule, and based on these observations, apply Extract Interface, too.

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
Bug introduced by
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
Bug introduced by
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
Bug introduced by
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
Bug introduced by
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
Bug introduced by
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
Bug introduced by
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
Security Bug introduced by
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
Security Bug introduced by
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