MediaWiki master
ImageModule.php
Go to the documentation of this file.
1<?php
22
23use DomainException;
24use InvalidArgumentException;
25use Wikimedia\Minify\CSSMin;
26
33class ImageModule extends Module {
35 private $useMaskImage;
37 protected $definition;
38
43 protected $localBasePath = '';
44
46 protected $origin = self::ORIGIN_CORE_SITEWIDE;
47
49 protected $imageObjects = null;
51 protected $images = [];
53 protected $defaultColor = null;
55 protected $useDataURI = true;
57 protected $globalVariants = null;
59 protected $variants = [];
61 protected $prefix = null;
63 protected $selectorWithoutVariant = '.{prefix}-{name}';
65 protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
66
125 public function __construct( array $options = [], $localBasePath = null ) {
126 $this->useMaskImage = $options['useMaskImage'] ?? false;
127 $this->localBasePath = static::extractLocalBasePath( $options, $localBasePath );
128
129 $this->definition = $options;
130 }
131
135 protected function loadFromDefinition() {
136 if ( $this->definition === null ) {
137 return;
138 }
139
140 $options = $this->definition;
141 $this->definition = null;
142
143 if ( isset( $options['data'] ) ) {
144 $dataPath = $this->getLocalPath( $options['data'] );
145 $data = json_decode( file_get_contents( $dataPath ), true );
146 $options = array_merge( $data, $options );
147 }
148
149 // Accepted combinations:
150 // * prefix
151 // * selector
152 // * selectorWithoutVariant + selectorWithVariant
153 // * prefix + selector
154 // * prefix + selectorWithoutVariant + selectorWithVariant
155
156 $prefix = isset( $options['prefix'] ) && $options['prefix'];
157 $selector = isset( $options['selector'] ) && $options['selector'];
158 $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] )
159 && $options['selectorWithoutVariant'];
160 $selectorWithVariant = isset( $options['selectorWithVariant'] )
161 && $options['selectorWithVariant'];
162
164 throw new InvalidArgumentException(
165 "Given 'selectorWithoutVariant' but no 'selectorWithVariant'."
166 );
167 }
169 throw new InvalidArgumentException(
170 "Given 'selectorWithVariant' but no 'selectorWithoutVariant'."
171 );
172 }
173 if ( $selector && $selectorWithVariant ) {
174 throw new InvalidArgumentException(
175 "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given."
176 );
177 }
178 if ( !$prefix && !$selector && !$selectorWithVariant ) {
179 throw new InvalidArgumentException(
180 "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given."
181 );
182 }
183
184 foreach ( $options as $member => $option ) {
185 switch ( $member ) {
186 case 'images':
187 case 'variants':
188 if ( !is_array( $option ) ) {
189 throw new InvalidArgumentException(
190 "Invalid list error. '$option' given, array expected."
191 );
192 }
193 if ( !isset( $option['default'] ) ) {
194 // Backwards compatibility
195 $option = [ 'default' => $option ];
196 }
197 foreach ( $option as $data ) {
198 if ( !is_array( $data ) ) {
199 throw new InvalidArgumentException(
200 "Invalid list error. '$data' given, array expected."
201 );
202 }
203 }
204 $this->{$member} = $option;
205 break;
206
207 case 'useDataURI':
208 $this->{$member} = (bool)$option;
209 break;
210 case 'defaultColor':
211 case 'prefix':
212 case 'selectorWithoutVariant':
213 case 'selectorWithVariant':
214 $this->{$member} = (string)$option;
215 break;
216
217 case 'selector':
218 $this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option;
219 }
220 }
221 }
222
227 public function getPrefix() {
228 $this->loadFromDefinition();
229 return $this->prefix;
230 }
231
236 public function getSelectors() {
237 $this->loadFromDefinition();
238 return [
239 'selectorWithoutVariant' => $this->selectorWithoutVariant,
240 'selectorWithVariant' => $this->selectorWithVariant,
241 ];
242 }
243
250 public function getImage( $name, Context $context ): ?Image {
251 $this->loadFromDefinition();
252 $images = $this->getImages( $context );
253 return $images[$name] ?? null;
254 }
255
261 public function getImages( Context $context ): array {
262 $skin = $context->getSkin();
263 if ( $this->imageObjects === null ) {
264 $this->loadFromDefinition();
265 $this->imageObjects = [];
266 }
267 if ( !isset( $this->imageObjects[$skin] ) ) {
268 $this->imageObjects[$skin] = [];
269 if ( !isset( $this->images[$skin] ) ) {
270 $this->images[$skin] = $this->images['default'] ?? [];
271 }
272 foreach ( $this->images[$skin] as $name => $options ) {
273 $fileDescriptor = is_array( $options ) ? $options['file'] : $options;
274
275 $allowedVariants = array_merge(
276 ( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
277 $this->getGlobalVariants( $context )
278 );
279 if ( isset( $this->variants[$skin] ) ) {
280 $variantConfig = array_intersect_key(
281 $this->variants[$skin],
282 array_fill_keys( $allowedVariants, true )
283 );
284 } else {
285 $variantConfig = [];
286 }
287
288 $image = new Image(
289 $name,
290 $this->getName(),
291 $fileDescriptor,
292 $this->localBasePath,
293 $variantConfig,
294 $this->defaultColor
295 );
296 $this->imageObjects[$skin][$image->getName()] = $image;
297 }
298 }
299
300 return $this->imageObjects[$skin];
301 }
302
309 public function getGlobalVariants( Context $context ): array {
310 $skin = $context->getSkin();
311 if ( $this->globalVariants === null ) {
312 $this->loadFromDefinition();
313 $this->globalVariants = [];
314 }
315 if ( !isset( $this->globalVariants[$skin] ) ) {
316 $this->globalVariants[$skin] = [];
317 if ( !isset( $this->variants[$skin] ) ) {
318 $this->variants[$skin] = $this->variants['default'] ?? [];
319 }
320 foreach ( $this->variants[$skin] as $name => $config ) {
321 if ( $config['global'] ?? false ) {
322 $this->globalVariants[$skin][] = $name;
323 }
324 }
325 }
326
327 return $this->globalVariants[$skin];
328 }
329
330 public function getStyles( Context $context ): array {
331 $this->loadFromDefinition();
332
333 // Build CSS rules
334 $rules = [];
335
336 $sources = $oldSources = $context->getResourceLoader()->getSources();
337 $this->getHookRunner()->onResourceLoaderModifyEmbeddedSourceUrls( $sources );
338 if ( array_keys( $sources ) !== array_keys( $oldSources ) ) {
339 throw new DomainException( 'ResourceLoaderModifyEmbeddedSourceUrls hook must not add or remove sources' );
340 }
341 $script = $sources[ $this->getSource() ];
342
343 $selectors = $this->getSelectors();
344
345 foreach ( $this->getImages( $context ) as $name => $image ) {
346 $declarations = $this->getStyleDeclarations( $context, $image, $script );
347 $selector = strtr(
348 $selectors['selectorWithoutVariant'],
349 [
350 '{prefix}' => $this->getPrefix(),
351 '{name}' => $name,
352 '{variant}' => '',
353 ]
354 );
355 $rules[] = "$selector {\n\t$declarations\n}";
356
357 foreach ( $image->getVariants() as $variant ) {
358 $declarations = $this->getStyleDeclarations( $context, $image, $script, $variant );
359 $selector = strtr(
360 $selectors['selectorWithVariant'],
361 [
362 '{prefix}' => $this->getPrefix(),
363 '{name}' => $name,
364 '{variant}' => $variant,
365 ]
366 );
367 $rules[] = "$selector {\n\t$declarations\n}";
368 }
369 }
370
371 $style = implode( "\n", $rules );
372
373 return [ 'all' => $style ];
374 }
375
387 private function getStyleDeclarations(
388 Context $context,
389 Image $image,
390 $script,
391 $variant = null
392 ) {
393 $imageDataUri = $this->useDataURI ? $image->getDataUri( $context, $variant, 'original' ) : false;
394 $primaryUrl = $imageDataUri ?: $image->getUrl( $context, $script, $variant, 'original' );
395 $declarations = $this->getCssDeclarations(
396 $primaryUrl
397 );
398 return implode( "\n\t", $declarations );
399 }
400
407 protected function getCssDeclarations( $primary ): array {
408 $primaryUrl = CSSMin::buildUrlValue( $primary );
409 if ( $this->supportsMaskImage() ) {
410 return [
411 "-webkit-mask-image: $primaryUrl;",
412 "mask-image: $primaryUrl;",
413 ];
414 }
415 return [
416 "background-image: $primaryUrl;",
417 ];
418 }
419
423 public function supportsMaskImage() {
424 return $this->useMaskImage;
425 }
426
430 public function supportsURLLoading() {
431 return false;
432 }
433
440 public function getDefinitionSummary( Context $context ) {
441 $this->loadFromDefinition();
442 $summary = parent::getDefinitionSummary( $context );
443
444 $options = [];
445 foreach ( [
446 'localBasePath',
447 'images',
448 'variants',
449 'prefix',
450 'selectorWithoutVariant',
451 'selectorWithVariant',
452 ] as $member ) {
453 $options[$member] = $this->{$member};
454 }
455
456 $summary[] = [
457 'options' => $options,
458 'fileHashes' => $this->getFileHashes( $context ),
459 ];
460 return $summary;
461 }
462
468 private function getFileHashes( Context $context ) {
469 $this->loadFromDefinition();
470 $files = [];
471 foreach ( $this->getImages( $context ) as $image ) {
472 $files[] = $image->getPath( $context );
473 }
474 $files = array_values( array_unique( $files ) );
475 return array_map( [ __CLASS__, 'safeFileHash' ], $files );
476 }
477
482 protected function getLocalPath( $path ) {
483 if ( $path instanceof FilePath ) {
484 return $path->getLocalPath();
485 }
486
487 return "{$this->localBasePath}/$path";
488 }
489
498 public static function extractLocalBasePath( array $options, $localBasePath = null ) {
499 global $IP;
500
501 if ( array_key_exists( 'localBasePath', $options ) ) {
502 $localBasePath = (string)$options['localBasePath'];
503 }
504
505 return $localBasePath ?? $IP;
506 }
507
511 public function getType() {
512 return self::LOAD_STYLES;
513 }
514}
if(!defined( 'MEDIAWIKI')) if(ini_get('mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition Setup.php:105
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.
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:42
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition Module.php:47
string null $name
Module name.
Definition Module.php:63