MediaWiki master
ImageModule.php
Go to the documentation of this file.
1<?php
8
9use DomainException;
10use InvalidArgumentException;
11use Wikimedia\Minify\CSSMin;
12
19class ImageModule extends Module {
21 private $useMaskImage;
23 protected $definition;
24
29 protected $localBasePath = '';
30
32 protected $origin = self::ORIGIN_CORE_SITEWIDE;
33
35 protected $imageObjects = null;
37 protected $images = [];
39 protected $defaultColor = null;
41 protected $useDataURI = true;
43 protected $globalVariants = null;
45 protected $variants = [];
47 protected $prefix = null;
49 protected $selectorWithoutVariant = '.{prefix}-{name}';
51 protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
52
111 public function __construct( array $options = [], $localBasePath = null ) {
112 $this->useMaskImage = $options['useMaskImage'] ?? false;
113 $this->localBasePath = static::extractLocalBasePath( $options, $localBasePath );
114
115 $this->definition = $options;
116 }
117
121 protected function loadFromDefinition() {
122 if ( $this->definition === null ) {
123 return;
124 }
125
126 $options = $this->definition;
127 $this->definition = null;
128
129 if ( isset( $options['data'] ) ) {
130 $dataPath = $this->getLocalPath( $options['data'] );
131 $data = json_decode( file_get_contents( $dataPath ), true );
132 $options = array_merge( $data, $options );
133 }
134
135 // Accepted combinations:
136 // * prefix
137 // * selector
138 // * selectorWithoutVariant + selectorWithVariant
139 // * prefix + selector
140 // * prefix + selectorWithoutVariant + selectorWithVariant
141
142 $prefix = isset( $options['prefix'] ) && $options['prefix'];
143 $selector = isset( $options['selector'] ) && $options['selector'];
144 $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] )
145 && $options['selectorWithoutVariant'];
146 $selectorWithVariant = isset( $options['selectorWithVariant'] )
147 && $options['selectorWithVariant'];
148
150 throw new InvalidArgumentException(
151 "Given 'selectorWithoutVariant' but no 'selectorWithVariant'."
152 );
153 }
155 throw new InvalidArgumentException(
156 "Given 'selectorWithVariant' but no 'selectorWithoutVariant'."
157 );
158 }
159 if ( $selector && $selectorWithVariant ) {
160 throw new InvalidArgumentException(
161 "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given."
162 );
163 }
164 if ( !$prefix && !$selector && !$selectorWithVariant ) {
165 throw new InvalidArgumentException(
166 "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given."
167 );
168 }
169
170 foreach ( $options as $member => $option ) {
171 switch ( $member ) {
172 case 'images':
173 case 'variants':
174 if ( !is_array( $option ) ) {
175 throw new InvalidArgumentException(
176 "Invalid list error. '$option' given, array expected."
177 );
178 }
179 if ( !isset( $option['default'] ) ) {
180 // Backwards compatibility
181 $option = [ 'default' => $option ];
182 }
183 foreach ( $option as $data ) {
184 if ( !is_array( $data ) ) {
185 throw new InvalidArgumentException(
186 "Invalid list error. '$data' given, array expected."
187 );
188 }
189 }
190 $this->{$member} = $option;
191 break;
192
193 case 'useDataURI':
194 $this->{$member} = (bool)$option;
195 break;
196 case 'defaultColor':
197 case 'prefix':
198 case 'selectorWithoutVariant':
199 case 'selectorWithVariant':
200 $this->{$member} = (string)$option;
201 break;
202
203 case 'selector':
204 $this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option;
205 }
206 }
207 }
208
213 public function getPrefix() {
214 $this->loadFromDefinition();
215 return $this->prefix;
216 }
217
222 public function getSelectors() {
223 $this->loadFromDefinition();
224 return [
225 'selectorWithoutVariant' => $this->selectorWithoutVariant,
226 'selectorWithVariant' => $this->selectorWithVariant,
227 ];
228 }
229
236 public function getImage( $name, Context $context ): ?Image {
237 $this->loadFromDefinition();
238 $images = $this->getImages( $context );
239 return $images[$name] ?? null;
240 }
241
247 public function getImages( Context $context ): array {
248 $skin = $context->getSkin();
249 if ( $this->imageObjects === null ) {
250 $this->loadFromDefinition();
251 $this->imageObjects = [];
252 }
253 if ( !isset( $this->imageObjects[$skin] ) ) {
254 $this->imageObjects[$skin] = [];
255 if ( !isset( $this->images[$skin] ) ) {
256 $this->images[$skin] = $this->images['default'] ?? [];
257 }
258 foreach ( $this->images[$skin] as $name => $options ) {
259 $fileDescriptor = is_array( $options ) ? $options['file'] : $options;
260
261 $allowedVariants = array_merge(
262 ( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
263 $this->getGlobalVariants( $context )
264 );
265 if ( isset( $this->variants[$skin] ) ) {
266 $variantConfig = array_intersect_key(
267 $this->variants[$skin],
268 array_fill_keys( $allowedVariants, true )
269 );
270 } else {
271 $variantConfig = [];
272 }
273
274 $image = new Image(
275 $name,
276 $this->getName(),
277 $fileDescriptor,
278 $this->localBasePath,
279 $variantConfig,
280 $this->defaultColor
281 );
282 $this->imageObjects[$skin][$image->getName()] = $image;
283 }
284 }
285
286 return $this->imageObjects[$skin];
287 }
288
295 public function getGlobalVariants( Context $context ): array {
296 $skin = $context->getSkin();
297 if ( $this->globalVariants === null ) {
298 $this->loadFromDefinition();
299 $this->globalVariants = [];
300 }
301 if ( !isset( $this->globalVariants[$skin] ) ) {
302 $this->globalVariants[$skin] = [];
303 if ( !isset( $this->variants[$skin] ) ) {
304 $this->variants[$skin] = $this->variants['default'] ?? [];
305 }
306 foreach ( $this->variants[$skin] as $name => $config ) {
307 if ( $config['global'] ?? false ) {
308 $this->globalVariants[$skin][] = $name;
309 }
310 }
311 }
312
313 return $this->globalVariants[$skin];
314 }
315
316 public function getStyles( Context $context ): array {
317 $this->loadFromDefinition();
318
319 // Build CSS rules
320 $rules = [];
321
322 $sources = $oldSources = $context->getResourceLoader()->getSources();
323 $this->getHookRunner()->onResourceLoaderModifyEmbeddedSourceUrls( $sources );
324 if ( array_keys( $sources ) !== array_keys( $oldSources ) ) {
325 throw new DomainException( 'ResourceLoaderModifyEmbeddedSourceUrls hook must not add or remove sources' );
326 }
327 $script = $sources[ $this->getSource() ];
328
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
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 );
384 return implode( "\n\t", $declarations );
385 }
386
393 protected function getCssDeclarations( $primary ): array {
394 $primaryUrl = CSSMin::buildUrlValue( $primary );
395 if ( $this->supportsMaskImage() ) {
396 return [
397 "-webkit-mask-image: $primaryUrl;",
398 "mask-image: $primaryUrl;",
399 ];
400 }
401 return [
402 "background-image: $primaryUrl;",
403 ];
404 }
405
409 public function supportsMaskImage() {
410 return $this->useMaskImage;
411 }
412
416 public function supportsURLLoading() {
417 return false;
418 }
419
426 public function getDefinitionSummary( Context $context ) {
427 $this->loadFromDefinition();
428 $summary = parent::getDefinitionSummary( $context );
429
430 $options = [];
431 foreach ( [
432 'localBasePath',
433 'images',
434 'variants',
435 'prefix',
436 'selectorWithoutVariant',
437 'selectorWithVariant',
438 ] as $member ) {
439 $options[$member] = $this->{$member};
440 }
441
442 $summary[] = [
443 'options' => $options,
444 'fileHashes' => $this->getFileHashes( $context ),
445 ];
446 return $summary;
447 }
448
454 private function getFileHashes( Context $context ) {
455 $this->loadFromDefinition();
456 $files = [];
457 foreach ( $this->getImages( $context ) as $image ) {
458 $files[] = $image->getPath( $context );
459 }
460 $files = array_values( array_unique( $files ) );
461 return array_map( self::safeFileHash( ... ), $files );
462 }
463
468 protected function getLocalPath( $path ) {
469 if ( $path instanceof FilePath ) {
470 return $path->getLocalPath();
471 }
472
473 return "{$this->localBasePath}/$path";
474 }
475
484 public static function extractLocalBasePath( array $options, $localBasePath = null ) {
485 global $IP;
486
487 if ( array_key_exists( 'localBasePath', $options ) ) {
488 $localBasePath = (string)$options['localBasePath'];
489 }
490
491 return $localBasePath ?? $IP;
492 }
493
497 public function getType() {
498 return self::LOAD_STYLES;
499 }
500}
if(!defined('MEDIAWIKI')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:90
Context object that contains information about the state of a specific ResourceLoader web request.
Definition Context.php:32
A path to a bundled file (such as JavaScript or CSS), along with a remote and local base path.
Definition FilePath.php:20
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()
getStyles(Context $context)
Get all CSS for this module for a given skin.
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:28
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition Module.php:34
string null $name
Module name.
Definition Module.php:52