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  );
383  return implode( "\n\t", $declarations );
384  }
385 
392  protected function getCssDeclarations( $primary ): array {
393  $primaryUrl = CSSMin::buildUrlValue( $primary );
394  return [
395  "background-image: $primaryUrl;",
396  ];
397  }
398 
402  public function supportsURLLoading() {
403  return false;
404  }
405 
412  public function getDefinitionSummary( Context $context ) {
413  $this->loadFromDefinition();
414  $summary = parent::getDefinitionSummary( $context );
415 
416  $options = [];
417  foreach ( [
418  'localBasePath',
419  'images',
420  'variants',
421  'prefix',
422  'selectorWithoutVariant',
423  'selectorWithVariant',
424  ] as $member ) {
425  $options[$member] = $this->{$member};
426  }
427 
428  $summary[] = [
429  'options' => $options,
430  'fileHashes' => $this->getFileHashes( $context ),
431  ];
432  return $summary;
433  }
434 
440  private function getFileHashes( Context $context ) {
441  $this->loadFromDefinition();
442  $files = [];
443  foreach ( $this->getImages( $context ) as $image ) {
444  $files[] = $image->getPath( $context );
445  }
446  $files = array_values( array_unique( $files ) );
447  return array_map( [ __CLASS__, 'safeFileHash' ], $files );
448  }
449 
454  protected function getLocalPath( $path ) {
455  if ( $path instanceof FilePath ) {
456  return $path->getLocalPath();
457  }
458 
459  return "{$this->localBasePath}/$path";
460  }
461 
470  public static function extractLocalBasePath( array $options, $localBasePath = null ) {
471  global $IP;
472 
473  if ( array_key_exists( 'localBasePath', $options ) ) {
474  $localBasePath = (string)$options['localBasePath'];
475  }
476 
477  return $localBasePath ?? $IP;
478  }
479 
483  public function getType() {
484  return self::LOAD_STYLES;
485  }
486 }
487 
489 class_alias( ImageModule::class, 'ResourceLoaderImageModule' );
if(!defined( 'MEDIAWIKI')) if(ini_get( 'mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition: Setup.php:96
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.
Definition: ImageModule.php:32
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.
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:48
string null $name
Module name.
Definition: Module.php:64