MediaWiki master
ImageModule.php
Go to the documentation of this file.
1<?php
22
23use InvalidArgumentException;
24use Wikimedia\Minify\CSSMin;
25
32class ImageModule extends Module {
34 private $useMaskImage;
36 protected $definition;
37
42 protected $localBasePath = '';
43
44 protected $origin = self::ORIGIN_CORE_SITEWIDE;
45
47 protected $imageObjects = null;
49 protected $images = [];
51 protected $defaultColor = null;
52 protected $useDataURI = true;
54 protected $globalVariants = null;
56 protected $variants = [];
58 protected $prefix = null;
59 protected $selectorWithoutVariant = '.{prefix}-{name}';
60 protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
61
120 public function __construct( array $options = [], $localBasePath = null ) {
121 $this->useMaskImage = $options['useMaskImage'] ?? false;
122 $this->localBasePath = static::extractLocalBasePath( $options, $localBasePath );
123
124 $this->definition = $options;
125 }
126
130 protected function loadFromDefinition() {
131 if ( $this->definition === null ) {
132 return;
133 }
134
135 $options = $this->definition;
136 $this->definition = null;
137
138 if ( isset( $options['data'] ) ) {
139 $dataPath = $this->getLocalPath( $options['data'] );
140 $data = json_decode( file_get_contents( $dataPath ), true );
141 $options = array_merge( $data, $options );
142 }
143
144 // Accepted combinations:
145 // * prefix
146 // * selector
147 // * selectorWithoutVariant + selectorWithVariant
148 // * prefix + selector
149 // * prefix + selectorWithoutVariant + selectorWithVariant
150
151 $prefix = isset( $options['prefix'] ) && $options['prefix'];
152 $selector = isset( $options['selector'] ) && $options['selector'];
153 $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] )
154 && $options['selectorWithoutVariant'];
155 $selectorWithVariant = isset( $options['selectorWithVariant'] )
156 && $options['selectorWithVariant'];
157
159 throw new InvalidArgumentException(
160 "Given 'selectorWithoutVariant' but no 'selectorWithVariant'."
161 );
162 }
164 throw new InvalidArgumentException(
165 "Given 'selectorWithVariant' but no 'selectorWithoutVariant'."
166 );
167 }
168 if ( $selector && $selectorWithVariant ) {
169 throw new InvalidArgumentException(
170 "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given."
171 );
172 }
173 if ( !$prefix && !$selector && !$selectorWithVariant ) {
174 throw new InvalidArgumentException(
175 "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given."
176 );
177 }
178
179 foreach ( $options as $member => $option ) {
180 switch ( $member ) {
181 case 'images':
182 case 'variants':
183 if ( !is_array( $option ) ) {
184 throw new InvalidArgumentException(
185 "Invalid list error. '$option' given, array expected."
186 );
187 }
188 if ( !isset( $option['default'] ) ) {
189 // Backwards compatibility
190 $option = [ 'default' => $option ];
191 }
192 foreach ( $option as $data ) {
193 if ( !is_array( $data ) ) {
194 throw new InvalidArgumentException(
195 "Invalid list error. '$data' given, array expected."
196 );
197 }
198 }
199 $this->{$member} = $option;
200 break;
201
202 case 'useDataURI':
203 $this->{$member} = (bool)$option;
204 break;
205 case 'defaultColor':
206 case 'prefix':
207 case 'selectorWithoutVariant':
208 case 'selectorWithVariant':
209 $this->{$member} = (string)$option;
210 break;
211
212 case 'selector':
213 $this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option;
214 }
215 }
216 }
217
222 public function getPrefix() {
223 $this->loadFromDefinition();
224 return $this->prefix;
225 }
226
231 public function getSelectors() {
232 $this->loadFromDefinition();
233 return [
234 'selectorWithoutVariant' => $this->selectorWithoutVariant,
235 'selectorWithVariant' => $this->selectorWithVariant,
236 ];
237 }
238
245 public function getImage( $name, Context $context ): ?Image {
246 $this->loadFromDefinition();
247 $images = $this->getImages( $context );
248 return $images[$name] ?? null;
249 }
250
256 public function getImages( Context $context ): array {
257 $skin = $context->getSkin();
258 if ( $this->imageObjects === null ) {
259 $this->loadFromDefinition();
260 $this->imageObjects = [];
261 }
262 if ( !isset( $this->imageObjects[$skin] ) ) {
263 $this->imageObjects[$skin] = [];
264 if ( !isset( $this->images[$skin] ) ) {
265 $this->images[$skin] = $this->images['default'] ?? [];
266 }
267 foreach ( $this->images[$skin] as $name => $options ) {
268 $fileDescriptor = is_array( $options ) ? $options['file'] : $options;
269
270 $allowedVariants = array_merge(
271 ( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
272 $this->getGlobalVariants( $context )
273 );
274 if ( isset( $this->variants[$skin] ) ) {
275 $variantConfig = array_intersect_key(
276 $this->variants[$skin],
277 array_fill_keys( $allowedVariants, true )
278 );
279 } else {
280 $variantConfig = [];
281 }
282
283 $image = new Image(
284 $name,
285 $this->getName(),
286 $fileDescriptor,
287 $this->localBasePath,
288 $variantConfig,
289 $this->defaultColor
290 );
291 $this->imageObjects[$skin][$image->getName()] = $image;
292 }
293 }
294
295 return $this->imageObjects[$skin];
296 }
297
304 public function getGlobalVariants( Context $context ): array {
305 $skin = $context->getSkin();
306 if ( $this->globalVariants === null ) {
307 $this->loadFromDefinition();
308 $this->globalVariants = [];
309 }
310 if ( !isset( $this->globalVariants[$skin] ) ) {
311 $this->globalVariants[$skin] = [];
312 if ( !isset( $this->variants[$skin] ) ) {
313 $this->variants[$skin] = $this->variants['default'] ?? [];
314 }
315 foreach ( $this->variants[$skin] as $name => $config ) {
316 if ( $config['global'] ?? false ) {
317 $this->globalVariants[$skin][] = $name;
318 }
319 }
320 }
321
322 return $this->globalVariants[$skin];
323 }
324
329 public function getStyles( Context $context ): array {
330 $this->loadFromDefinition();
331
332 // Build CSS rules
333 $rules = [];
334 $script = $context->getResourceLoader()->getLoadScript( $this->getSource() );
335 $selectors = $this->getSelectors();
336
337 foreach ( $this->getImages( $context ) as $name => $image ) {
338 $declarations = $this->getStyleDeclarations( $context, $image, $script );
339 $selector = strtr(
340 $selectors['selectorWithoutVariant'],
341 [
342 '{prefix}' => $this->getPrefix(),
343 '{name}' => $name,
344 '{variant}' => '',
345 ]
346 );
347 $rules[] = "$selector {\n\t$declarations\n}";
348
349 foreach ( $image->getVariants() as $variant ) {
350 $declarations = $this->getStyleDeclarations( $context, $image, $script, $variant );
351 $selector = strtr(
352 $selectors['selectorWithVariant'],
353 [
354 '{prefix}' => $this->getPrefix(),
355 '{name}' => $name,
356 '{variant}' => $variant,
357 ]
358 );
359 $rules[] = "$selector {\n\t$declarations\n}";
360 }
361 }
362
363 $style = implode( "\n", $rules );
364
365 return [ 'all' => $style ];
366 }
367
379 private function getStyleDeclarations(
380 Context $context,
381 Image $image,
382 $script,
383 $variant = null
384 ) {
385 $imageDataUri = $this->useDataURI ? $image->getDataUri( $context, $variant, 'original' ) : false;
386 $primaryUrl = $imageDataUri ?: $image->getUrl( $context, $script, $variant, 'original' );
387 $declarations = $this->getCssDeclarations(
388 $primaryUrl
389 );
390 return implode( "\n\t", $declarations );
391 }
392
399 protected function getCssDeclarations( $primary ): array {
400 $primaryUrl = CSSMin::buildUrlValue( $primary );
401 if ( $this->supportsMaskImage() ) {
402 return [
403 "--webkit-mask-image: $primaryUrl;",
404 "mask-image: $primaryUrl;",
405 ];
406 }
407 return [
408 "background-image: $primaryUrl;",
409 ];
410 }
411
415 public function supportsMaskImage() {
416 return $this->useMaskImage;
417 }
418
422 public function supportsURLLoading() {
423 return false;
424 }
425
432 public function getDefinitionSummary( Context $context ) {
433 $this->loadFromDefinition();
434 $summary = parent::getDefinitionSummary( $context );
435
436 $options = [];
437 foreach ( [
438 'localBasePath',
439 'images',
440 'variants',
441 'prefix',
442 'selectorWithoutVariant',
443 'selectorWithVariant',
444 ] as $member ) {
445 $options[$member] = $this->{$member};
446 }
447
448 $summary[] = [
449 'options' => $options,
450 'fileHashes' => $this->getFileHashes( $context ),
451 ];
452 return $summary;
453 }
454
460 private function getFileHashes( Context $context ) {
461 $this->loadFromDefinition();
462 $files = [];
463 foreach ( $this->getImages( $context ) as $image ) {
464 $files[] = $image->getPath( $context );
465 }
466 $files = array_values( array_unique( $files ) );
467 return array_map( [ __CLASS__, 'safeFileHash' ], $files );
468 }
469
474 protected function getLocalPath( $path ) {
475 if ( $path instanceof FilePath ) {
476 return $path->getLocalPath();
477 }
478
479 return "{$this->localBasePath}/$path";
480 }
481
490 public static function extractLocalBasePath( array $options, $localBasePath = null ) {
491 global $IP;
492
493 if ( array_key_exists( 'localBasePath', $options ) ) {
494 $localBasePath = (string)$options['localBasePath'];
495 }
496
497 return $localBasePath ?? $IP;
498 }
499
503 public function getType() {
504 return self::LOAD_STYLES;
505 }
506}
if(!defined( 'MEDIAWIKI')) if(ini_get('mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:100
Context object that contains information about the state of a specific ResourceLoader web request.
Definition Context.php:45
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.
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.
getCssDeclarations( $primary)
Format the CSS declaration for the image as a background-image property.
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:42
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition Module.php:49
string null $name
Module name.
Definition Module.php:65