MediaWiki REL1_40
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
115 public function __construct( array $options = [], $localBasePath = null ) {
116 $this->localBasePath = static::extractLocalBasePath( $options, $localBasePath );
117
118 $this->definition = $options;
119 }
120
124 protected function loadFromDefinition() {
125 if ( $this->definition === null ) {
126 return;
127 }
128
129 $options = $this->definition;
130 $this->definition = null;
131
132 if ( isset( $options['data'] ) ) {
133 $dataPath = $this->getLocalPath( $options['data'] );
134 $data = json_decode( file_get_contents( $dataPath ), true );
135 $options = array_merge( $data, $options );
136 }
137
138 // Accepted combinations:
139 // * prefix
140 // * selector
141 // * selectorWithoutVariant + selectorWithVariant
142 // * prefix + selector
143 // * prefix + selectorWithoutVariant + selectorWithVariant
144
145 $prefix = isset( $options['prefix'] ) && $options['prefix'];
146 $selector = isset( $options['selector'] ) && $options['selector'];
147 $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] )
148 && $options['selectorWithoutVariant'];
149 $selectorWithVariant = isset( $options['selectorWithVariant'] )
150 && $options['selectorWithVariant'];
151
153 throw new InvalidArgumentException(
154 "Given 'selectorWithoutVariant' but no 'selectorWithVariant'."
155 );
156 }
158 throw new InvalidArgumentException(
159 "Given 'selectorWithVariant' but no 'selectorWithoutVariant'."
160 );
161 }
162 if ( $selector && $selectorWithVariant ) {
163 throw new InvalidArgumentException(
164 "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given."
165 );
166 }
167 if ( !$prefix && !$selector && !$selectorWithVariant ) {
168 throw new InvalidArgumentException(
169 "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given."
170 );
171 }
172
173 foreach ( $options as $member => $option ) {
174 switch ( $member ) {
175 case 'images':
176 case 'variants':
177 if ( !is_array( $option ) ) {
178 throw new InvalidArgumentException(
179 "Invalid list error. '$option' given, array expected."
180 );
181 }
182 if ( !isset( $option['default'] ) ) {
183 // Backwards compatibility
184 $option = [ 'default' => $option ];
185 }
186 foreach ( $option as $data ) {
187 if ( !is_array( $data ) ) {
188 throw new InvalidArgumentException(
189 "Invalid list error. '$data' given, array expected."
190 );
191 }
192 }
193 $this->{$member} = $option;
194 break;
195
196 case 'useDataURI':
197 $this->{$member} = (bool)$option;
198 break;
199 case 'defaultColor':
200 case 'prefix':
201 case 'selectorWithoutVariant':
202 case 'selectorWithVariant':
203 $this->{$member} = (string)$option;
204 break;
205
206 case 'selector':
207 $this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option;
208 }
209 }
210 }
211
216 public function getPrefix() {
217 $this->loadFromDefinition();
218 return $this->prefix;
219 }
220
225 public function getSelectors() {
226 $this->loadFromDefinition();
227 return [
228 'selectorWithoutVariant' => $this->selectorWithoutVariant,
229 'selectorWithVariant' => $this->selectorWithVariant,
230 ];
231 }
232
239 public function getImage( $name, Context $context ): ?Image {
240 $this->loadFromDefinition();
241 $images = $this->getImages( $context );
242 return $images[$name] ?? null;
243 }
244
250 public function getImages( Context $context ): array {
251 $skin = $context->getSkin();
252 if ( $this->imageObjects === null ) {
253 $this->loadFromDefinition();
254 $this->imageObjects = [];
255 }
256 if ( !isset( $this->imageObjects[$skin] ) ) {
257 $this->imageObjects[$skin] = [];
258 if ( !isset( $this->images[$skin] ) ) {
259 $this->images[$skin] = $this->images['default'] ?? [];
260 }
261 foreach ( $this->images[$skin] as $name => $options ) {
262 $fileDescriptor = is_array( $options ) ? $options['file'] : $options;
263
264 $allowedVariants = array_merge(
265 ( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
266 $this->getGlobalVariants( $context )
267 );
268 if ( isset( $this->variants[$skin] ) ) {
269 $variantConfig = array_intersect_key(
270 $this->variants[$skin],
271 array_fill_keys( $allowedVariants, true )
272 );
273 } else {
274 $variantConfig = [];
275 }
276
277 $image = new Image(
278 $name,
279 $this->getName(),
280 $fileDescriptor,
281 $this->localBasePath,
282 $variantConfig,
283 $this->defaultColor
284 );
285 $this->imageObjects[$skin][$image->getName()] = $image;
286 }
287 }
288
289 return $this->imageObjects[$skin];
290 }
291
298 public function getGlobalVariants( Context $context ): array {
299 $skin = $context->getSkin();
300 if ( $this->globalVariants === null ) {
301 $this->loadFromDefinition();
302 $this->globalVariants = [];
303 }
304 if ( !isset( $this->globalVariants[$skin] ) ) {
305 $this->globalVariants[$skin] = [];
306 if ( !isset( $this->variants[$skin] ) ) {
307 $this->variants[$skin] = $this->variants['default'] ?? [];
308 }
309 foreach ( $this->variants[$skin] as $name => $config ) {
310 if ( $config['global'] ?? false ) {
311 $this->globalVariants[$skin][] = $name;
312 }
313 }
314 }
315
316 return $this->globalVariants[$skin];
317 }
318
323 public function getStyles( Context $context ): array {
324 $this->loadFromDefinition();
325
326 // Build CSS rules
327 $rules = [];
328 $script = $context->getResourceLoader()->getLoadScript( $this->getSource() );
329 $selectors = $this->getSelectors();
330
331 foreach ( $this->getImages( $context ) as $name => $image ) {
332 $declarations = $this->getStyleDeclarations( $context, $image, $script );
333 $selector = strtr(
334 $selectors['selectorWithoutVariant'],
335 [
336 '{prefix}' => $this->getPrefix(),
337 '{name}' => $name,
338 '{variant}' => '',
339 ]
340 );
341 $rules[] = "$selector {\n\t$declarations\n}";
342
343 foreach ( $image->getVariants() as $variant ) {
344 $declarations = $this->getStyleDeclarations( $context, $image, $script, $variant );
345 $selector = strtr(
346 $selectors['selectorWithVariant'],
347 [
348 '{prefix}' => $this->getPrefix(),
349 '{name}' => $name,
350 '{variant}' => $variant,
351 ]
352 );
353 $rules[] = "$selector {\n\t$declarations\n}";
354 }
355 }
356
357 $style = implode( "\n", $rules );
358 return [ 'all' => $style ];
359 }
360
372 private function getStyleDeclarations(
373 Context $context,
374 Image $image,
375 $script,
376 $variant = null
377 ) {
378 $imageDataUri = $this->useDataURI ? $image->getDataUri( $context, $variant, 'original' ) : false;
379 $primaryUrl = $imageDataUri ?: $image->getUrl( $context, $script, $variant, 'original' );
380 $declarations = $this->getCssDeclarations(
381 $primaryUrl,
382 $image->getUrl( $context, $script, $variant, 'rasterized' )
383 );
384 return implode( "\n\t", $declarations );
385 }
386
395 protected function getCssDeclarations( $primary, $fallback ): array {
396 $primaryUrl = CSSMin::buildUrlValue( $primary );
397 return [
398 "background-image: $primaryUrl;",
399 ];
400 }
401
405 public function supportsURLLoading() {
406 return false;
407 }
408
415 public function getDefinitionSummary( Context $context ) {
416 $this->loadFromDefinition();
417 $summary = parent::getDefinitionSummary( $context );
418
419 $options = [];
420 foreach ( [
421 'localBasePath',
422 'images',
423 'variants',
424 'prefix',
425 'selectorWithoutVariant',
426 'selectorWithVariant',
427 ] as $member ) {
428 $options[$member] = $this->{$member};
429 }
430
431 $summary[] = [
432 'options' => $options,
433 'fileHashes' => $this->getFileHashes( $context ),
434 ];
435 return $summary;
436 }
437
443 private function getFileHashes( Context $context ) {
444 $this->loadFromDefinition();
445 $files = [];
446 foreach ( $this->getImages( $context ) as $image ) {
447 $files[] = $image->getPath( $context );
448 }
449 $files = array_values( array_unique( $files ) );
450 return array_map( [ __CLASS__, 'safeFileHash' ], $files );
451 }
452
457 protected function getLocalPath( $path ) {
458 if ( $path instanceof FilePath ) {
459 return $path->getLocalPath();
460 }
461
462 return "{$this->localBasePath}/$path";
463 }
464
473 public static function extractLocalBasePath( array $options, $localBasePath = null ) {
474 global $IP;
475
476 if ( array_key_exists( 'localBasePath', $options ) ) {
477 $localBasePath = (string)$options['localBasePath'];
478 }
479
480 return $localBasePath ?? $IP;
481 }
482
486 public function getType() {
487 return self::LOAD_STYLES;
488 }
489}
490
492class_alias( ImageModule::class, 'ResourceLoaderImageModule' );
$fallback
Definition MessagesAb.php:8
if(!defined( 'MEDIAWIKI')) if(ini_get('mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:93
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)
This method formerly provided fallback rasterized images for browsers that do not support SVG.
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