MediaWiki  master
ResourceLoaderImageModule.php
Go to the documentation of this file.
1 <?php
29 
31  protected $definition;
32 
37  protected $localBasePath = '';
38 
39  protected $origin = self::ORIGIN_CORE_SITEWIDE;
40 
42  protected $imageObjects = null;
44  protected $images = [];
46  protected $defaultColor = null;
47  protected $useDataURI = true;
49  protected $globalVariants = null;
51  protected $variants = [];
53  protected $prefix = null;
54  protected $selectorWithoutVariant = '.{prefix}-{name}';
55  protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
56  protected $targets = [ 'desktop', 'mobile' ];
57 
114  public function __construct( array $options = [], $localBasePath = null ) {
115  $this->localBasePath = static::extractLocalBasePath( $options, $localBasePath );
116 
117  $this->definition = $options;
118  }
119 
123  protected function loadFromDefinition() {
124  if ( $this->definition === null ) {
125  return;
126  }
127 
128  $options = $this->definition;
129  $this->definition = null;
130 
131  if ( isset( $options['data'] ) ) {
132  $dataPath = $this->getLocalPath( $options['data'] );
133  $data = json_decode( file_get_contents( $dataPath ), true );
134  $options = array_merge( $data, $options );
135  }
136 
137  // Accepted combinations:
138  // * prefix
139  // * selector
140  // * selectorWithoutVariant + selectorWithVariant
141  // * prefix + selector
142  // * prefix + selectorWithoutVariant + selectorWithVariant
143 
144  $prefix = isset( $options['prefix'] ) && $options['prefix'];
145  $selector = isset( $options['selector'] ) && $options['selector'];
146  $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] )
147  && $options['selectorWithoutVariant'];
148  $selectorWithVariant = isset( $options['selectorWithVariant'] )
149  && $options['selectorWithVariant'];
150 
152  throw new InvalidArgumentException(
153  "Given 'selectorWithoutVariant' but no 'selectorWithVariant'."
154  );
155  }
157  throw new InvalidArgumentException(
158  "Given 'selectorWithVariant' but no 'selectorWithoutVariant'."
159  );
160  }
161  if ( $selector && $selectorWithVariant ) {
162  throw new InvalidArgumentException(
163  "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given."
164  );
165  }
166  if ( !$prefix && !$selector && !$selectorWithVariant ) {
167  throw new InvalidArgumentException(
168  "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given."
169  );
170  }
171 
172  foreach ( $options as $member => $option ) {
173  switch ( $member ) {
174  case 'images':
175  case 'variants':
176  if ( !is_array( $option ) ) {
177  throw new InvalidArgumentException(
178  "Invalid list error. '$option' given, array expected."
179  );
180  }
181  if ( !isset( $option['default'] ) ) {
182  // Backwards compatibility
183  $option = [ 'default' => $option ];
184  }
185  foreach ( $option as $skin => $data ) {
186  if ( !is_array( $data ) ) {
187  throw new InvalidArgumentException(
188  "Invalid list error. '$data' given, array expected."
189  );
190  }
191  }
192  $this->{$member} = $option;
193  break;
194 
195  case 'useDataURI':
196  $this->{$member} = (bool)$option;
197  break;
198  case 'defaultColor':
199  case 'prefix':
200  case 'selectorWithoutVariant':
201  case 'selectorWithVariant':
202  $this->{$member} = (string)$option;
203  break;
204 
205  case 'selector':
206  $this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option;
207  }
208  }
209  }
210 
215  public function getPrefix() {
216  $this->loadFromDefinition();
217  return $this->prefix;
218  }
219 
224  public function getSelectors() {
225  $this->loadFromDefinition();
226  return [
227  'selectorWithoutVariant' => $this->selectorWithoutVariant,
228  'selectorWithVariant' => $this->selectorWithVariant,
229  ];
230  }
231 
239  $this->loadFromDefinition();
240  $images = $this->getImages( $context );
241  return $images[$name] ?? null;
242  }
243 
250  $skin = $context->getSkin();
251  if ( $this->imageObjects === null ) {
252  $this->loadFromDefinition();
253  $this->imageObjects = [];
254  }
255  if ( !isset( $this->imageObjects[$skin] ) ) {
256  $this->imageObjects[$skin] = [];
257  if ( !isset( $this->images[$skin] ) ) {
258  $this->images[$skin] = $this->images['default'] ?? [];
259  }
260  foreach ( $this->images[$skin] as $name => $options ) {
261  $fileDescriptor = is_array( $options ) ? $options['file'] : $options;
262 
263  $allowedVariants = array_merge(
264  ( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
265  $this->getGlobalVariants( $context )
266  );
267  if ( isset( $this->variants[$skin] ) ) {
268  $variantConfig = array_intersect_key(
269  $this->variants[$skin],
270  array_fill_keys( $allowedVariants, true )
271  );
272  } else {
273  $variantConfig = [];
274  }
275 
276  $image = new ResourceLoaderImage(
277  $name,
278  $this->getName(),
279  $fileDescriptor,
280  $this->localBasePath,
281  $variantConfig,
282  $this->defaultColor
283  );
284  $this->imageObjects[$skin][$image->getName()] = $image;
285  }
286  }
287 
288  return $this->imageObjects[$skin];
289  }
290 
298  $skin = $context->getSkin();
299  if ( $this->globalVariants === null ) {
300  $this->loadFromDefinition();
301  $this->globalVariants = [];
302  }
303  if ( !isset( $this->globalVariants[$skin] ) ) {
304  $this->globalVariants[$skin] = [];
305  if ( !isset( $this->variants[$skin] ) ) {
306  $this->variants[$skin] = $this->variants['default'] ?? [];
307  }
308  foreach ( $this->variants[$skin] as $name => $config ) {
309  if ( isset( $config['global'] ) && $config['global'] ) {
310  $this->globalVariants[$skin][] = $name;
311  }
312  }
313  }
314 
315  return $this->globalVariants[$skin];
316  }
317 
323  $this->loadFromDefinition();
324 
325  // Build CSS rules
326  $rules = [];
327  $script = $context->getResourceLoader()->getLoadScript( $this->getSource() );
328  $selectors = $this->getSelectors();
329 
330  foreach ( $this->getImages( $context ) as $name => $image ) {
331  $declarations = $this->getStyleDeclarations( $context, $image, $script );
332  $selector = strtr(
333  $selectors['selectorWithoutVariant'],
334  [
335  '{prefix}' => $this->getPrefix(),
336  '{name}' => $name,
337  '{variant}' => '',
338  ]
339  );
340  $rules[] = "$selector {\n\t$declarations\n}";
341 
342  foreach ( $image->getVariants() as $variant ) {
343  $declarations = $this->getStyleDeclarations( $context, $image, $script, $variant );
344  $selector = strtr(
345  $selectors['selectorWithVariant'],
346  [
347  '{prefix}' => $this->getPrefix(),
348  '{name}' => $name,
349  '{variant}' => $variant,
350  ]
351  );
352  $rules[] = "$selector {\n\t$declarations\n}";
353  }
354  }
355 
356  $style = implode( "\n", $rules );
357  return [ 'all' => $style ];
358  }
359 
371  private function getStyleDeclarations(
373  ResourceLoaderImage $image,
374  $script,
375  $variant = null
376  ) {
377  $imageDataUri = $this->useDataURI ? $image->getDataUri( $context, $variant, 'original' ) : false;
378  $primaryUrl = $imageDataUri ?: $image->getUrl( $context, $script, $variant, 'original' );
379  $declarations = $this->getCssDeclarations(
380  $primaryUrl,
381  $image->getUrl( $context, $script, $variant, 'rasterized' )
382  );
383  return implode( "\n\t", $declarations );
384  }
385 
398  protected function getCssDeclarations( $primary, $fallback ) {
399  $primaryUrl = CSSMin::buildUrlValue( $primary );
400  $fallbackUrl = CSSMin::buildUrlValue( $fallback );
401  return [
402  "background-image: $fallbackUrl;",
403  "background-image: linear-gradient(transparent, transparent), $primaryUrl;",
404  ];
405  }
406 
410  public function supportsURLLoading() {
411  return false;
412  }
413 
421  $this->loadFromDefinition();
422  $summary = parent::getDefinitionSummary( $context );
423 
424  $options = [];
425  foreach ( [
426  'localBasePath',
427  'images',
428  'variants',
429  'prefix',
430  'selectorWithoutVariant',
431  'selectorWithVariant',
432  ] as $member ) {
433  $options[$member] = $this->{$member};
434  }
435 
436  $summary[] = [
437  'options' => $options,
438  'fileHashes' => $this->getFileHashes( $context ),
439  ];
440  return $summary;
441  }
442 
449  $this->loadFromDefinition();
450  $files = [];
451  foreach ( $this->getImages( $context ) as $name => $image ) {
452  $files[] = $image->getPath( $context );
453  }
454  $files = array_values( array_unique( $files ) );
455  return array_map( [ __CLASS__, 'safeFileHash' ], $files );
456  }
457 
462  protected function getLocalPath( $path ) {
463  if ( $path instanceof ResourceLoaderFilePath ) {
464  return $path->getLocalPath();
465  }
466 
467  return "{$this->localBasePath}/$path";
468  }
469 
478  public static function extractLocalBasePath( array $options, $localBasePath = null ) {
479  global $IP;
480 
481  if ( $localBasePath === null ) {
483  }
484 
485  if ( array_key_exists( 'localBasePath', $options ) ) {
486  $localBasePath = (string)$options['localBasePath'];
487  }
488 
489  return $localBasePath;
490  }
491 
495  public function getType() {
496  return self::LOAD_STYLES;
497  }
498 }
getImages(ResourceLoaderContext $context)
Get ResourceLoaderImage objects for all images.
Class encapsulating an image used in a ResourceLoaderImageModule.
$context
Definition: load.php:45
Module for generated and embedded images.
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
$IP
Definition: WebStart.php:41
getUrl(ResourceLoaderContext $context, $script, $variant, $format)
Get the load.php URL that will produce this image.
getFileHashes(ResourceLoaderContext $context)
Helper method for getDefinitionSummary.
getStyleDeclarations(ResourceLoaderContext $context, ResourceLoaderImage $image, $script, $variant=null)
This method must not be used by getDefinitionSummary as doing so would cause an infinite loop (we use...
__construct(array $options=[], $localBasePath=null)
Constructs a new module from an options array.
getGlobalVariants(ResourceLoaderContext $context)
Get list of variants in this module that are &#39;global&#39;, i.e., available for every image regardless of ...
getSource()
Get the source of this module.
getDefinitionSummary(ResourceLoaderContext $context)
Get the definition summary for this module.
getStyles(ResourceLoaderContext $context)
getDataUri(ResourceLoaderContext $context, $variant, $format)
Get the data: URI that will produce this image.
getPrefix()
Get CSS class prefix used by this module.
static buildUrlValue( $url)
Build a CSS &#39;url()&#39; value for the given URL, quoting parentheses (and other funny characters) and esc...
Definition: CSSMin.php:216
getImage( $name, ResourceLoaderContext $context)
Get a ResourceLoaderImage object for given image.
string $localBasePath
Local base path, see __construct()
getSelectors()
Get CSS selector templates used by this module.
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path...
ResourceLoaderImage [][] null $imageObjects
$fallback
Definition: MessagesAb.php:11
static extractLocalBasePath(array $options, $localBasePath=null)
Extract a local base path from module definition information.
string null $name
Module name.
getCssDeclarations( $primary, $fallback)
SVG support using a transparent gradient to guarantee cross-browser compatibility (browsers able to u...
getName()
Get this module&#39;s name.
loadFromDefinition()
Parse definition and external JSON data, if referenced.
Context object that contains information about the state of a specific ResourceLoader web request...