MediaWiki  master
ImageModule.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\ResourceLoader;
22 
23 use InvalidArgumentException;
24 use Wikimedia\Minify\CSSMin;
25 
32 class 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 
492 class_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.
Definition: ImageModule.php:32
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()
Definition: ImageModule.php:40
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