MediaWiki  master
ResourceLoaderSkinModule.php
Go to the documentation of this file.
1 <?php
20 use Wikimedia\Minify\CSSMin;
21 
32  public $targets = [ 'desktop', 'mobile' ];
33 
97  private const FEATURE_FILES = [
98  'normalize' => [
99  'all' => [ 'resources/src/mediawiki.skinning/normalize.less' ],
100  ],
101  'logo' => [
102  // Applies the logo and ensures it downloads prior to printing.
103  'all' => [ 'resources/src/mediawiki.skinning/logo.less' ],
104  // Reserves whitespace for the logo in a pseudo element.
105  'print' => [ 'resources/src/mediawiki.skinning/logo-print.less' ],
106  ],
107  'content' => [
108  'screen' => [ 'resources/src/mediawiki.skinning/content.less' ],
109  ],
110  'content-media' => [
111  'screen' => [ 'resources/src/mediawiki.skinning/content.media.less' ],
112  ],
113  'content-links' => [
114  'screen' => [ 'resources/src/mediawiki.skinning/content.externallinks.less' ]
115  ],
116  'interface' => [
117  'screen' => [ 'resources/src/mediawiki.skinning/interface.less' ],
118  ],
119  'elements' => [
120  'screen' => [ 'resources/src/mediawiki.skinning/elements.css' ],
121  ],
122  'legacy' => [
123  'all' => [ 'resources/src/mediawiki.skinning/messageBoxes.less' ],
124  'print' => [ 'resources/src/mediawiki.skinning/commonPrint.css' ],
125  'screen' => [ 'resources/src/mediawiki.skinning/legacy.less' ],
126  ],
127  'i18n-ordered-lists' => [
128  'screen' => [ 'resources/src/mediawiki.skinning/i18n-ordered-lists.less' ],
129  ],
130  'i18n-all-lists-margins' => [
131  'screen' => [ 'resources/src/mediawiki.skinning/i18n-all-lists-margins.less' ],
132  ],
133  'i18n-headings' => [
134  'screen' => [ 'resources/src/mediawiki.skinning/i18n-headings.less' ],
135  ],
136  'toc' => [
137  'all' => [ 'resources/src/mediawiki.skinning/toc/common.css' ],
138  'screen' => [ 'resources/src/mediawiki.skinning/toc/screen.less' ],
139  'print' => [ 'resources/src/mediawiki.skinning/toc/print.css' ],
140  ],
141  ];
142 
144  private $features;
145 
147  private const DEFAULT_FEATURES = [
148  'content' => false,
149  'content-links' => false,
150  'content-media' => false, // Will default to `true` when $wgUseNewMediaStructure is enabled everywhere
151  'elements' => false,
152  'i18n-all-lists-margins' => false,
153  'i18n-headings' => false,
154  'i18n-ordered-lists' => false,
155  'interface' => false,
156  'legacy' => false,
157  'logo' => false,
158  'normalize' => false,
159  'toc' => true,
160  ];
161 
162  private const LESS_MESSAGES = [
163  // `toc` feature, used in screen.less
164  'hidetoc',
165  'showtoc',
166  ];
167 
168  public function __construct(
169  array $options = [],
170  $localBasePath = null,
171  $remoteBasePath = null
172  ) {
173  $options['lessMessages'] = array_merge(
174  $options['lessMessages'] ?? [],
175  self::LESS_MESSAGES
176  );
177  parent::__construct( $options, $localBasePath, $remoteBasePath );
178  $features = $options['features'] ??
179  // For historic reasons if nothing is declared logo and legacy features are enabled.
180  [
181  'logo' => true,
182  'legacy' => true
183  ];
184  $enabledFeatures = [];
185  $compatibilityMode = false;
186  foreach ( $features as $key => $enabled ) {
187  if ( is_bool( $enabled ) ) {
188  $feature = $key;
189  $enabledFeatures[$key] = $enabled;
190  } else {
191  $feature = $enabled;
192  // operating in array mode.
193  $enabledFeatures[$enabled] = true;
194  $compatibilityMode = true;
195  }
196  if ( !isset( self::FEATURE_FILES[$feature] ) || !isset( self::DEFAULT_FEATURES[$feature] ) ) {
197  // We could be an old version of MediaWiki and a new feature is being requested (T271441).
198  continue;
199  }
200  }
201  // If the module didn't specify an option use the default features values.
202  // This allows new features to be turned on automatically.
203  if ( !$compatibilityMode ) {
204  foreach ( self::DEFAULT_FEATURES as $key => $enabled ) {
205  if ( !isset( $enabledFeatures[$key] ) ) {
206  if ( $key === 'content-media' ) {
207  // Only ship this by default if enabled, since it's going
208  // to be adding some unnecessary overhead where unused.
209  // Also, assume that if a skin is being picky about which
210  // features it wants, it'll pull this in when it's ready
211  // for it.
212  $enabledFeatures[$key] = (bool)$this->getConfig()->get( 'UseNewMediaStructure' );
213  } else {
214  $enabledFeatures[$key] = $enabled;
215  }
216  }
217  }
218  }
219  $this->features = array_filter(
220  array_keys( $enabledFeatures ),
221  static function ( $key ) use ( $enabledFeatures ) {
222  return $enabledFeatures[ $key ];
223  }
224  );
225  }
226 
233  public function getStyleFiles( ResourceLoaderContext $context ) {
234  $styles = parent::getStyleFiles( $context );
235 
236  list( $defaultLocalBasePath, $defaultRemoteBasePath ) =
238 
239  $featureFilePaths = [];
240 
241  foreach ( self::FEATURE_FILES as $feature => $files ) {
242  if ( in_array( $feature, $this->features ) ) {
243  foreach ( $files as $mediaType => $files ) {
244  foreach ( $files as $filepath ) {
245  $featureFilePaths[$mediaType][] = new ResourceLoaderFilePath(
246  $filepath,
247  $defaultLocalBasePath,
248  $defaultRemoteBasePath
249  );
250  }
251  }
252  }
253  }
254 
255  // Styles defines in options are added to the $featureFilePaths to ensure
256  // that $featureFilePaths styles precede module defined ones.
257  // This is particularly important given the `normalize` styles need to be the first
258  // outputted (see T269618).
259  foreach ( $styles as $mediaType => $paths ) {
260  $featureFilePaths[$mediaType] = array_merge( $featureFilePaths[$mediaType] ?? [], $paths );
261  }
262 
263  return $featureFilePaths;
264  }
265 
270  public function getStyles( ResourceLoaderContext $context ) {
271  $logo = $this->getLogoData( $this->getConfig() );
272  $styles = parent::getStyles( $context );
273  $this->normalizeStyles( $styles );
274 
275  $isLogoFeatureEnabled = in_array( 'logo', $this->features );
276  if ( $isLogoFeatureEnabled ) {
277  $default = !is_array( $logo ) ? $logo : $logo['1x'];
278  $styles['all'][] = '.mw-wiki-logo { background-image: ' .
279  CSSMin::buildUrlValue( $default ) .
280  '; }';
281 
282  if ( is_array( $logo ) ) {
283  if ( isset( $logo['svg'] ) ) {
284  $styles['all'][] = '.mw-wiki-logo { ' .
285  'background-image: -webkit-linear-gradient(transparent, transparent), ' .
286  CSSMin::buildUrlValue( $logo['svg'] ) . '; ' .
287  'background-image: linear-gradient(transparent, transparent), ' .
288  CSSMin::buildUrlValue( $logo['svg'] ) . ';' .
289  'background-size: 135px auto; }';
290  } else {
291  if ( isset( $logo['1.5x'] ) ) {
292  $styles[
293  '(-webkit-min-device-pixel-ratio: 1.5), ' .
294  '(min--moz-device-pixel-ratio: 1.5), ' .
295  '(min-resolution: 1.5dppx), ' .
296  '(min-resolution: 144dpi)'
297  ][] = '.mw-wiki-logo { background-image: ' .
298  CSSMin::buildUrlValue( $logo['1.5x'] ) . ';' .
299  'background-size: 135px auto; }';
300  }
301  if ( isset( $logo['2x'] ) ) {
302  $styles[
303  '(-webkit-min-device-pixel-ratio: 2), ' .
304  '(min--moz-device-pixel-ratio: 2), ' .
305  '(min-resolution: 2dppx), ' .
306  '(min-resolution: 192dpi)'
307  ][] = '.mw-wiki-logo { background-image: ' .
308  CSSMin::buildUrlValue( $logo['2x'] ) . ';' .
309  'background-size: 135px auto; }';
310  }
311  }
312  }
313  }
314 
315  return $styles;
316  }
317 
322  public function getPreloadLinks( ResourceLoaderContext $context ) {
323  return $this->getLogoPreloadlinks();
324  }
325 
330  private function getLogoPreloadlinks() : array {
331  if ( !in_array( 'logo', $this->features ) ) {
332  return [];
333  }
334 
335  $logo = $this->getLogoData( $this->getConfig() );
336 
337  if ( !is_array( $logo ) ) {
338  // No media queries required if we only have one variant
339  return [ $logo => [ 'as' => 'image' ] ];
340  }
341 
342  if ( isset( $logo['svg'] ) ) {
343  // No media queries required if we only have a 1x and svg variant
344  // because all preload-capable browsers support SVGs
345  return [ $logo['svg'] => [ 'as' => 'image' ] ];
346  }
347 
348  $logosPerDppx = [];
349  foreach ( $logo as $dppx => $src ) {
350  // Keys are in this format: "1.5x"
351  $dppx = substr( $dppx, 0, -1 );
352  $logosPerDppx[$dppx] = $src;
353  }
354 
355  // Because PHP can't have floats as array keys
356  uksort( $logosPerDppx, static function ( $a, $b ) {
357  $a = floatval( $a );
358  $b = floatval( $b );
359  // Sort from smallest to largest (e.g. 1x, 1.5x, 2x)
360  return $a <=> $b;
361  } );
362 
363  $logos = [];
364  foreach ( $logosPerDppx as $dppx => $src ) {
365  $logos[] = [
366  'dppx' => $dppx,
367  'src' => $src
368  ];
369  }
370 
371  $logosCount = count( $logos );
372  $preloadLinks = [];
373  // Logic must match ResourceLoaderSkinModule:
374  // - 1x applies to resolution < 1.5dppx
375  // - 1.5x applies to resolution >= 1.5dppx && < 2dppx
376  // - 2x applies to resolution >= 2dppx
377  // Note that min-resolution and max-resolution are both inclusive.
378  for ( $i = 0; $i < $logosCount; $i++ ) {
379  if ( $i === 0 ) {
380  // Smallest dppx
381  // min-resolution is ">=" (larger than or equal to)
382  // "not min-resolution" is essentially "<"
383  $media_query = 'not all and (min-resolution: ' . $logos[1]['dppx'] . 'dppx)';
384  } elseif ( $i !== $logosCount - 1 ) {
385  // In between
386  // Media query expressions can only apply "not" to the entire expression
387  // (e.g. can't express ">= 1.5 and not >= 2).
388  // Workaround: Use <= 1.9999 in place of < 2.
389  $upper_bound = floatval( $logos[$i + 1]['dppx'] ) - 0.000001;
390  $media_query = '(min-resolution: ' . $logos[$i]['dppx'] .
391  'dppx) and (max-resolution: ' . $upper_bound . 'dppx)';
392  } else {
393  // Largest dppx
394  $media_query = '(min-resolution: ' . $logos[$i]['dppx'] . 'dppx)';
395  }
396 
397  $preloadLinks[$logos[$i]['src']] = [
398  'as' => 'image',
399  'media' => $media_query
400  ];
401  }
402 
403  return $preloadLinks;
404  }
405 
414  private function normalizeStyles( array &$styles ) : void {
415  foreach ( $styles as $key => $val ) {
416  if ( !is_array( $val ) ) {
417  $styles[$key] = [ $val ];
418  }
419  }
420  }
421 
432  public static function getAvailableLogos( $conf ) : array {
433  $logos = $conf->get( 'Logos' );
434  if ( $logos === false ) {
435  // no logos were defined... this will either
436  // 1. Load from wgLogo and wgLogoHD
437  // 2. Trigger runtime exception if those are not defined.
438  $logos = [];
439  }
440 
441  // If logos['1x'] is not defined, see if we can use wgLogo
442  if ( !isset( $logos[ '1x' ] ) ) {
443  $logo = $conf->get( 'Logo' );
444  if ( $logo ) {
445  $logos['1x'] = $logo;
446  }
447  }
448 
449  try {
450  $logoHD = $conf->get( 'LogoHD' );
451  // make sure not false
452  if ( $logoHD ) {
453  // wfDeprecated( __METHOD__ . ' with $wgLogoHD set instead of $wgLogos', '1.35', false, 1 );
454  $logos += $logoHD;
455  }
456  } catch ( ConfigException $e ) {
457  // no backwards compatibility changes needed.
458  }
459 
460  // check the configuration is valid
461  if ( !isset( $logos['1x'] ) ) {
462  throw new RuntimeException( "The key `1x` is required for wgLogos or wgLogo must be defined." );
463  }
464  // return the modified logos!
465  return $logos;
466  }
467 
476  protected function getLogoData( Config $conf ) {
477  $logoHD = self::getAvailableLogos( $conf );
478  $logo = $logoHD['1x'];
479 
480  $logo1Url = OutputPage::transformResourcePath( $conf, $logo );
481 
482  $logoUrls = [
483  '1x' => $logo1Url,
484  ];
485 
486  if ( isset( $logoHD['svg'] ) ) {
487  $logoUrls['svg'] = OutputPage::transformResourcePath(
488  $conf,
489  $logoHD['svg']
490  );
491  } elseif ( isset( $logoHD['1.5x'] ) || isset( $logoHD['2x'] ) ) {
492  // Only 1.5x and 2x are supported
493  if ( isset( $logoHD['1.5x'] ) ) {
494  $logoUrls['1.5x'] = OutputPage::transformResourcePath(
495  $conf,
496  $logoHD['1.5x']
497  );
498  }
499  if ( isset( $logoHD['2x'] ) ) {
500  $logoUrls['2x'] = OutputPage::transformResourcePath(
501  $conf,
502  $logoHD['2x']
503  );
504  }
505  } else {
506  // Return a string rather than a one-element array, getLogoPreloadlinks depends on this
507  return $logo1Url;
508  }
509 
510  return $logoUrls;
511  }
512 
517  public function isKnownEmpty( ResourceLoaderContext $context ) {
518  // Regardless of whether the files are specified, we always
519  // provide mw-wiki-logo styles.
520  return false;
521  }
522 
529  protected function getLessVars( ResourceLoaderContext $context ) {
530  $lessVars = parent::getLessVars( $context );
531  $logos = self::getAvailableLogos( $this->getConfig() );
532 
533  if ( isset( $logos['wordmark'] ) ) {
534  $logo = $logos['wordmark'];
535  $lessVars[ 'logo-enabled' ] = true;
536  $lessVars[ 'logo-wordmark-url' ] = CSSMin::buildUrlValue( $logo['src'] );
537  $lessVars[ 'logo-wordmark-width' ] = intval( $logo['width'] );
538  $lessVars[ 'logo-wordmark-height' ] = intval( $logo['height'] );
539  } else {
540  $lessVars[ 'logo-enabled' ] = false;
541  }
542  return $lessVars;
543  }
544 
545  public function getDefinitionSummary( ResourceLoaderContext $context ) {
546  $summary = parent::getDefinitionSummary( $context );
547  $summary[] = [
548  'logos' => self::getAvailableLogos( $this->getConfig() ),
549  ];
550  return $summary;
551  }
552 }
ResourceLoaderLessVarFileModule
Module augmented with context-specific LESS variables.
Definition: ResourceLoaderLessVarFileModule.php:31
ResourceLoaderSkinModule\getPreloadLinks
getPreloadLinks(ResourceLoaderContext $context)
Definition: ResourceLoaderSkinModule.php:322
ResourceLoaderSkinModule\LESS_MESSAGES
const LESS_MESSAGES
Definition: ResourceLoaderSkinModule.php:162
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:33
ResourceLoaderFileModule\$styles
array $styles
List of paths to CSS files to always include.
Definition: ResourceLoaderFileModule.php:93
ResourceLoaderSkinModule\__construct
__construct(array $options=[], $localBasePath=null, $remoteBasePath=null)
Constructs a new module from an options array.See $wgResourceModules for the available options....
Definition: ResourceLoaderSkinModule.php:168
true
return true
Definition: router.php:90
ResourceLoaderSkinModule\normalizeStyles
normalizeStyles(array &$styles)
Ensure all media keys use array values.
Definition: ResourceLoaderSkinModule.php:414
ResourceLoaderFilePath
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path,...
Definition: ResourceLoaderFilePath.php:28
ResourceLoaderSkinModule\getDefinitionSummary
getDefinitionSummary(ResourceLoaderContext $context)
Get the definition summary for this module.
Definition: ResourceLoaderSkinModule.php:545
ResourceLoaderSkinModule\$features
string[] $features
Definition: ResourceLoaderSkinModule.php:144
Config
Interface for configuration instances.
Definition: Config.php:30
ResourceLoaderSkinModule\getLessVars
getLessVars(ResourceLoaderContext $context)
Get language-specific LESS variables for this module.
Definition: ResourceLoaderSkinModule.php:529
ConfigException
Exceptions for config failures.
Definition: ConfigException.php:29
ResourceLoaderSkinModule\getLogoData
getLogoData(Config $conf)
Definition: ResourceLoaderSkinModule.php:476
ResourceLoaderSkinModule\getAvailableLogos
static getAvailableLogos( $conf)
Return an array of all available logos that a skin may use.
Definition: ResourceLoaderSkinModule.php:432
ResourceLoaderFileModule\extractBasePaths
static extractBasePaths(array $options=[], $localBasePath=null, $remoteBasePath=null)
Extract a pair of local and remote base paths from module definition information.
Definition: ResourceLoaderFileModule.php:302
ResourceLoaderSkinModule\FEATURE_FILES
const FEATURE_FILES
Every skin should define which features it would like to reuse for core inside a ResourceLoader modul...
Definition: ResourceLoaderSkinModule.php:97
ResourceLoaderSkinModule\getLogoPreloadlinks
getLogoPreloadlinks()
Helper method for getPreloadLinks()
Definition: ResourceLoaderSkinModule.php:330
ResourceLoaderSkinModule\getStyles
getStyles(ResourceLoaderContext $context)
Definition: ResourceLoaderSkinModule.php:270
ResourceLoaderFileModule\$localBasePath
string $localBasePath
Local base path, see __construct()
Definition: ResourceLoaderFileModule.php:42
ResourceLoaderSkinModule\isKnownEmpty
isKnownEmpty(ResourceLoaderContext $context)
Definition: ResourceLoaderSkinModule.php:517
ResourceLoaderSkinModule\getStyleFiles
getStyleFiles(ResourceLoaderContext $context)
Get styles defined in the module definition, plus any enabled feature styles.
Definition: ResourceLoaderSkinModule.php:233
ResourceLoaderFileModule\$remoteBasePath
string $remoteBasePath
Remote base path, see __construct()
Definition: ResourceLoaderFileModule.php:45
ResourceLoaderSkinModule
Module for skin stylesheets.
Definition: ResourceLoaderSkinModule.php:28
ResourceLoaderModule\getConfig
getConfig()
Definition: ResourceLoaderModule.php:221
ResourceLoaderSkinModule\$targets
$targets
All skins are assumed to be compatible with mobile.
Definition: ResourceLoaderSkinModule.php:32
OutputPage\transformResourcePath
static transformResourcePath(Config $config, $path)
Transform path to web-accessible static resource.
Definition: OutputPage.php:3958