MediaWiki REL1_39
ImageModule.php
Go to the documentation of this file.
1<?php
22
23use InvalidArgumentException;
24use Wikimedia\Minify\CSSMin;
25
32class ImageModule extends Module {
34 protected $definition;
35
40 protected $localBasePath = '';
41
42 protected $origin = self::ORIGIN_CORE_SITEWIDE;
43
45 protected $imageObjects = null;
47 protected $images = [];
49 protected $defaultColor = null;
50 protected $useDataURI = true;
52 protected $globalVariants = null;
54 protected $variants = [];
56 protected $prefix = null;
57 protected $selectorWithoutVariant = '.{prefix}-{name}';
58 protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
59 protected $targets = [ 'desktop', 'mobile' ];
60
116 public function __construct( array $options = [], $localBasePath = null ) {
117 $this->localBasePath = static::extractLocalBasePath( $options, $localBasePath );
118
119 $this->definition = $options;
120 }
121
125 protected function loadFromDefinition() {
126 if ( $this->definition === null ) {
127 return;
128 }
129
130 $options = $this->definition;
131 $this->definition = null;
132
133 if ( isset( $options['data'] ) ) {
134 $dataPath = $this->getLocalPath( $options['data'] );
135 $data = json_decode( file_get_contents( $dataPath ), true );
136 $options = array_merge( $data, $options );
137 }
138
139 // Accepted combinations:
140 // * prefix
141 // * selector
142 // * selectorWithoutVariant + selectorWithVariant
143 // * prefix + selector
144 // * prefix + selectorWithoutVariant + selectorWithVariant
145
146 $prefix = isset( $options['prefix'] ) && $options['prefix'];
147 $selector = isset( $options['selector'] ) && $options['selector'];
148 $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] )
149 && $options['selectorWithoutVariant'];
150 $selectorWithVariant = isset( $options['selectorWithVariant'] )
151 && $options['selectorWithVariant'];
152
154 throw new InvalidArgumentException(
155 "Given 'selectorWithoutVariant' but no 'selectorWithVariant'."
156 );
157 }
159 throw new InvalidArgumentException(
160 "Given 'selectorWithVariant' but no 'selectorWithoutVariant'."
161 );
162 }
163 if ( $selector && $selectorWithVariant ) {
164 throw new InvalidArgumentException(
165 "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given."
166 );
167 }
168 if ( !$prefix && !$selector && !$selectorWithVariant ) {
169 throw new InvalidArgumentException(
170 "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given."
171 );
172 }
173
174 foreach ( $options as $member => $option ) {
175 switch ( $member ) {
176 case 'images':
177 case 'variants':
178 if ( !is_array( $option ) ) {
179 throw new InvalidArgumentException(
180 "Invalid list error. '$option' given, array expected."
181 );
182 }
183 if ( !isset( $option['default'] ) ) {
184 // Backwards compatibility
185 $option = [ 'default' => $option ];
186 }
187 foreach ( $option as $skin => $data ) {
188 if ( !is_array( $data ) ) {
189 throw new InvalidArgumentException(
190 "Invalid list error. '$data' given, array expected."
191 );
192 }
193 }
194 $this->{$member} = $option;
195 break;
196
197 case 'useDataURI':
198 $this->{$member} = (bool)$option;
199 break;
200 case 'defaultColor':
201 case 'prefix':
202 case 'selectorWithoutVariant':
203 case 'selectorWithVariant':
204 $this->{$member} = (string)$option;
205 break;
206
207 case 'selector':
208 $this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option;
209 }
210 }
211 }
212
217 public function getPrefix() {
218 $this->loadFromDefinition();
219 return $this->prefix;
220 }
221
226 public function getSelectors() {
227 $this->loadFromDefinition();
228 return [
229 'selectorWithoutVariant' => $this->selectorWithoutVariant,
230 'selectorWithVariant' => $this->selectorWithVariant,
231 ];
232 }
233
240 public function getImage( $name, Context $context ): ?Image {
241 $this->loadFromDefinition();
242 $images = $this->getImages( $context );
243 return $images[$name] ?? null;
244 }
245
251 public function getImages( Context $context ): array {
252 $skin = $context->getSkin();
253 if ( $this->imageObjects === null ) {
254 $this->loadFromDefinition();
255 $this->imageObjects = [];
256 }
257 if ( !isset( $this->imageObjects[$skin] ) ) {
258 $this->imageObjects[$skin] = [];
259 if ( !isset( $this->images[$skin] ) ) {
260 $this->images[$skin] = $this->images['default'] ?? [];
261 }
262 foreach ( $this->images[$skin] as $name => $options ) {
263 $fileDescriptor = is_array( $options ) ? $options['file'] : $options;
264
265 $allowedVariants = array_merge(
266 ( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
267 $this->getGlobalVariants( $context )
268 );
269 if ( isset( $this->variants[$skin] ) ) {
270 $variantConfig = array_intersect_key(
271 $this->variants[$skin],
272 array_fill_keys( $allowedVariants, true )
273 );
274 } else {
275 $variantConfig = [];
276 }
277
278 $image = new Image(
279 $name,
280 $this->getName(),
281 $fileDescriptor,
282 $this->localBasePath,
283 $variantConfig,
284 $this->defaultColor
285 );
286 $this->imageObjects[$skin][$image->getName()] = $image;
287 }
288 }
289
290 return $this->imageObjects[$skin];
291 }
292
299 public function getGlobalVariants( Context $context ): array {
300 $skin = $context->getSkin();
301 if ( $this->globalVariants === null ) {
302 $this->loadFromDefinition();
303 $this->globalVariants = [];
304 }
305 if ( !isset( $this->globalVariants[$skin] ) ) {
306 $this->globalVariants[$skin] = [];
307 if ( !isset( $this->variants[$skin] ) ) {
308 $this->variants[$skin] = $this->variants['default'] ?? [];
309 }
310 foreach ( $this->variants[$skin] as $name => $config ) {
311 if ( $config['global'] ?? false ) {
312 $this->globalVariants[$skin][] = $name;
313 }
314 }
315 }
316
317 return $this->globalVariants[$skin];
318 }
319
324 public function getStyles( Context $context ): array {
325 $this->loadFromDefinition();
326
327 // Build CSS rules
328 $rules = [];
329 $script = $context->getResourceLoader()->getLoadScript( $this->getSource() );
330 $selectors = $this->getSelectors();
331
332 foreach ( $this->getImages( $context ) as $name => $image ) {
333 $declarations = $this->getStyleDeclarations( $context, $image, $script );
334 $selector = strtr(
335 $selectors['selectorWithoutVariant'],
336 [
337 '{prefix}' => $this->getPrefix(),
338 '{name}' => $name,
339 '{variant}' => '',
340 ]
341 );
342 $rules[] = "$selector {\n\t$declarations\n}";
343
344 foreach ( $image->getVariants() as $variant ) {
345 $declarations = $this->getStyleDeclarations( $context, $image, $script, $variant );
346 $selector = strtr(
347 $selectors['selectorWithVariant'],
348 [
349 '{prefix}' => $this->getPrefix(),
350 '{name}' => $name,
351 '{variant}' => $variant,
352 ]
353 );
354 $rules[] = "$selector {\n\t$declarations\n}";
355 }
356 }
357
358 $style = implode( "\n", $rules );
359 return [ 'all' => $style ];
360 }
361
373 private function getStyleDeclarations(
374 Context $context,
375 Image $image,
376 $script,
377 $variant = null
378 ) {
379 $imageDataUri = $this->useDataURI ? $image->getDataUri( $context, $variant, 'original' ) : false;
380 $primaryUrl = $imageDataUri ?: $image->getUrl( $context, $script, $variant, 'original' );
381 $declarations = $this->getCssDeclarations(
382 $primaryUrl,
383 $image->getUrl( $context, $script, $variant, 'rasterized' )
384 );
385 return implode( "\n\t", $declarations );
386 }
387
400 protected function getCssDeclarations( $primary, $fallback ): array {
401 $primaryUrl = CSSMin::buildUrlValue( $primary );
402 $fallbackUrl = CSSMin::buildUrlValue( $fallback );
403 return [
404 "background-image: $fallbackUrl;",
405 "background-image: linear-gradient(transparent, transparent), $primaryUrl;",
406 ];
407 }
408
412 public function supportsURLLoading() {
413 return false;
414 }
415
422 public function getDefinitionSummary( Context $context ) {
423 $this->loadFromDefinition();
424 $summary = parent::getDefinitionSummary( $context );
425
426 $options = [];
427 foreach ( [
428 'localBasePath',
429 'images',
430 'variants',
431 'prefix',
432 'selectorWithoutVariant',
433 'selectorWithVariant',
434 ] as $member ) {
435 $options[$member] = $this->{$member};
436 }
437
438 $summary[] = [
439 'options' => $options,
440 'fileHashes' => $this->getFileHashes( $context ),
441 ];
442 return $summary;
443 }
444
450 private function getFileHashes( Context $context ) {
451 $this->loadFromDefinition();
452 $files = [];
453 foreach ( $this->getImages( $context ) as $name => $image ) {
454 $files[] = $image->getPath( $context );
455 }
456 $files = array_values( array_unique( $files ) );
457 return array_map( [ __CLASS__, 'safeFileHash' ], $files );
458 }
459
464 protected function getLocalPath( $path ) {
465 if ( $path instanceof FilePath ) {
466 return $path->getLocalPath();
467 }
468
469 return "{$this->localBasePath}/$path";
470 }
471
480 public static function extractLocalBasePath( array $options, $localBasePath = null ) {
481 global $IP;
482
483 if ( $localBasePath === null ) {
484 $localBasePath = $IP;
485 }
486
487 if ( array_key_exists( 'localBasePath', $options ) ) {
488 $localBasePath = (string)$options['localBasePath'];
489 }
490
491 return $localBasePath;
492 }
493
497 public function getType() {
498 return self::LOAD_STYLES;
499 }
500}
501
503class_alias( ImageModule::class, 'ResourceLoaderImageModule' );
$fallback
if(!defined( 'MEDIAWIKI')) if(ini_get('mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:91
Context object that contains information about the state of a specific ResourceLoader web request.
Definition Context.php:46
A path to a bundled file (such as JavaScript or CSS), along with a remote and local base path.
Definition FilePath.php:34
Module for generated and embedded images.
getCssDeclarations( $primary, $fallback)
SVG support using a transparent gradient to guarantee cross-browser compatibility (browsers able to u...
getSelectors()
Get CSS selector templates used by this module.
getPrefix()
Get CSS class prefix used by this module.
string $localBasePath
Local base path, see __construct()
static extractLocalBasePath(array $options, $localBasePath=null)
Extract a local base path from module definition information.
getImages(Context $context)
Get Image objects for all images.
__construct(array $options=[], $localBasePath=null)
Constructs a new module from an options array.
getDefinitionSummary(Context $context)
Get the definition summary for this module.
loadFromDefinition()
Parse definition and external JSON data, if referenced.
getGlobalVariants(Context $context)
Get list of variants in this module that are 'global', i.e., available for every image regardless of ...
getImage( $name, Context $context)
Get an Image object for given image.
Class encapsulating an image used in an ImageModule.
Definition Image.php:41
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition Module.php:48
string null $name
Module name.
Definition Module.php:64