MediaWiki REL1_41
SkinModule.php
Go to the documentation of this file.
1<?php
21
22use InvalidArgumentException;
27use Wikimedia\Minify\CSSMin;
28
36
135 private const FEATURE_FILES = [
136 'accessibility' => [
137 'all' => [ 'resources/src/mediawiki.skinning/accessibility.less' ],
138 ],
139 'normalize' => [
140 'all' => [ 'resources/src/mediawiki.skinning/normalize.less' ],
141 ],
142 'logo' => [
143 // Applies the logo and ensures it downloads prior to printing.
144 'all' => [ 'resources/src/mediawiki.skinning/logo.less' ],
145 // Reserves whitespace for the logo in a pseudo element.
146 'print' => [ 'resources/src/mediawiki.skinning/logo-print.less' ],
147 ],
148 'content-media' => [],
149 'content-links' => [
150 'screen' => [ 'resources/src/mediawiki.skinning/content.links.less' ]
151 ],
152 'content-links-external' => [
153 'screen' => [ 'resources/src/mediawiki.skinning/content.externallinks.less' ]
154 ],
155 'content-body' => [
156 'screen' => [ 'resources/src/mediawiki.skinning/content.body.less' ],
157 'print' => [ 'resources/src/mediawiki.skinning/content.body-print.less' ],
158 ],
159 'content-tables' => [
160 'screen' => [ 'resources/src/mediawiki.skinning/content.tables.less' ],
161 'print' => [ 'resources/src/mediawiki.skinning/content.tables-print.less' ]
162 ],
163 // Legacy shorthand for 6 features: interface-core, interface-edit-section-links,
164 // interface-indicators, interface-subtitle, interface-site-notice, interface-user-message
165 'interface' => [],
166 'interface-category' => [
167 'screen' => [ 'resources/src/mediawiki.skinning/interface.category.less' ],
168 'print' => [ 'resources/src/mediawiki.skinning/interface.category-print.less' ],
169 ],
170 'interface-core' => [
171 'screen' => [ 'resources/src/mediawiki.skinning/interface.less' ],
172 'print' => [ 'resources/src/mediawiki.skinning/interface-print.less' ],
173 ],
174 'interface-edit-section-links' => [
175 'screen' => [ 'resources/src/mediawiki.skinning/interface-edit-section-links.less' ],
176 ],
177 'interface-indicators' => [
178 'screen' => [ 'resources/src/mediawiki.skinning/interface-indicators.less' ],
179 ],
180 'interface-site-notice' => [
181 'screen' => [ 'resources/src/mediawiki.skinning/interface-site-notice.less' ],
182 ],
183 'interface-subtitle' => [
184 'screen' => [ 'resources/src/mediawiki.skinning/interface-subtitle.less' ],
185 ],
186 'interface-message-box' => [
187 'all' => [ 'resources/src/mediawiki.skinning/messageBoxes.less' ],
188 ],
189 'interface-user-message' => [
190 'screen' => [ 'resources/src/mediawiki.skinning/interface-user-message.less' ],
191 ],
192 'elements' => [
193 'screen' => [ 'resources/src/mediawiki.skinning/elements.less' ],
194 'print' => [ 'resources/src/mediawiki.skinning/elements-print.less' ],
195 ],
196 // The styles of the legacy feature was removed in 1.39. This can be removed when no skins are referencing it
197 // (Dropping this line will trigger InvalidArgumentException: Feature 'legacy' is not recognised)
198 'legacy' => [],
199 'i18n-ordered-lists' => [
200 'screen' => [ 'resources/src/mediawiki.skinning/i18n-ordered-lists.less' ],
201 ],
202 'i18n-all-lists-margins' => [
203 'screen' => [ 'resources/src/mediawiki.skinning/i18n-all-lists-margins.less' ],
204 ],
205 'i18n-headings' => [
206 'screen' => [ 'resources/src/mediawiki.skinning/i18n-headings.less' ],
207 ],
208 'toc' => [
209 'all' => [ 'resources/src/mediawiki.skinning/toc/common.css' ],
210 'screen' => [ 'resources/src/mediawiki.skinning/toc/screen.less' ],
211 'print' => [ 'resources/src/mediawiki.skinning/toc/print.css' ],
212 ],
213 ];
214
216 private $features;
217
225 private const DEFAULT_FEATURES_SPECIFIED = [
226 'accessibility' => true,
227 'content-body' => true,
228 'interface-core' => true,
229 'toc' => true
230 ];
231
240 private const DEFAULT_FEATURES_ABSENT = [
241 'logo',
242 ];
243
244 private const LESS_MESSAGES = [
245 // `toc` feature, used in screen.less
246 'hidetoc',
247 'showtoc',
248 ];
249
270 public function __construct(
271 array $options = [],
272 $localBasePath = null,
273 $remoteBasePath = null
274 ) {
275 $features = $options['features'] ?? self::DEFAULT_FEATURES_ABSENT;
276 $listMode = array_keys( $features ) === range( 0, count( $features ) - 1 );
277
278 $messages = '';
279 // NOTE: Compatibility is only applied when features are provided
280 // in map-form. The list-form does not currently get these.
281 $features = $listMode ? self::applyFeaturesCompatibility(
282 array_fill_keys( $features, true ), false, $messages
283 ) : self::applyFeaturesCompatibility( $features, true, $messages );
284
285 foreach ( $features as $key => $enabled ) {
286 if ( !isset( self::FEATURE_FILES[$key] ) ) {
287 throw new InvalidArgumentException( "Feature '$key' is not recognised" );
288 }
289 }
290
291 $this->features = $listMode
292 ? array_keys( array_filter( $features ) )
293 : array_keys( array_filter( $features + self::DEFAULT_FEATURES_SPECIFIED ) );
294
295 // Only the `toc` feature makes use of interface messages.
296 // For skins not using the `toc` feature, make sure LocalisationCache
297 // remains untouched (T270027).
298 if ( in_array( 'toc', $this->features ) ) {
299 $options['lessMessages'] = array_merge(
300 $options['lessMessages'] ?? [],
301 self::LESS_MESSAGES
302 );
303 }
304
305 if ( $messages !== '' ) {
306 $messages .= 'More information can be found at [[mw:Manual:ResourceLoaderSkinModule]]. ';
307 $options['deprecated'] = $messages;
308 }
309 parent::__construct( $options, $localBasePath, $remoteBasePath );
310 }
311
319 protected static function applyFeaturesCompatibility(
320 array $features, bool $addUnspecifiedFeatures = true, &$messages = ''
321 ): array {
322 // The `content` feature is mapped to `content-media`.
323 if ( isset( $features[ 'content' ] ) ) {
324 $features[ 'content-media' ] = $features[ 'content' ];
325 unset( $features[ 'content' ] );
326 $messages .= '[1.37] The use of the `content` feature with SkinModule'
327 . ' is deprecated. Use `content-media` instead. ';
328 }
329
330 // The `content-thumbnails` feature is mapped to `content-media`.
331 if ( isset( $features[ 'content-thumbnails' ] ) ) {
332 $features[ 'content-media' ] = $features[ 'content-thumbnails' ];
333 $messages .= '[1.37] The use of the `content-thumbnails` feature with SkinModule'
334 . ' is deprecated. Use `content-media` instead. ';
335 unset( $features[ 'content-thumbnails' ] );
336 }
337
338 // If `content-links` feature is set but no preference for `content-links-external` is set
339 if ( $addUnspecifiedFeatures && isset( $features[ 'content-links' ] )
340 && !isset( $features[ 'content-links-external' ] )
341 ) {
342 // Assume the same true/false preference for both.
343 $features[ 'content-links-external' ] = $features[ 'content-links' ];
344 }
345
346 // The legacy feature no longer exists (T89981) but to avoid fatals in skins is retained.
347 if ( isset( $features['legacy'] ) && $features['legacy'] ) {
348 $messages .= '[1.37] The use of the `legacy` feature with SkinModule is deprecated'
349 . '(T89981) and is a NOOP since 1.39 (T304325). This should be urgently omited to retain compatibility '
350 . 'with future MediaWiki versions';
351 }
352
353 // The `content-links` feature was split out from `elements`.
354 // Make sure skins asking for `elements` also get these by default.
355 if ( $addUnspecifiedFeatures && isset( $features[ 'element' ] ) && !isset( $features[ 'content-links' ] ) ) {
356 $features[ 'content-links' ] = $features[ 'element' ];
357 }
358
359 // `content-parser-output` was renamed to `content-body`.
360 // No need to go through deprecation process here since content-parser-output added and removed in 1.36.
361 // Remove this check when no matches for
362 // https://codesearch.wmcloud.org/search/?q=content-parser-output&i=nope&files=&excludeFiles=&repos=
363 if ( isset( $features[ 'content-parser-output' ] ) ) {
364 $features[ 'content-body' ] = $features[ 'content-parser-output' ];
365 unset( $features[ 'content-parser-output' ] );
366 }
367
368 // The interface module is a short hand for several modules. Enable them now.
369 if ( isset( $features[ 'interface' ] ) && $features[ 'interface' ] ) {
370 unset( $features[ 'interface' ] );
371 $features[ 'interface-core' ] = true;
372 $features[ 'interface-indicators' ] = true;
373 $features[ 'interface-subtitle' ] = true;
374 $features[ 'interface-user-message' ] = true;
375 $features[ 'interface-site-notice' ] = true;
376 $features[ 'interface-edit-section-links' ] = true;
377 }
378 return $features;
379 }
380
387 public function getStyleFiles( Context $context ) {
388 $styles = parent::getStyleFiles( $context );
389
390 // Bypass the current module paths so that these files are served from core,
391 // instead of the individual skin's module directory.
392 [ $defaultLocalBasePath, $defaultRemoteBasePath ] =
394 [],
395 null,
396 $this->getConfig()->get( MainConfigNames::ResourceBasePath )
397 );
398
399 $featureFilePaths = [];
400
401 foreach ( self::FEATURE_FILES as $feature => $featureFiles ) {
402 if ( in_array( $feature, $this->features ) ) {
403 foreach ( $featureFiles as $mediaType => $files ) {
404 foreach ( $files as $filepath ) {
405 $featureFilePaths[$mediaType][] = new FilePath(
406 $filepath,
407 $defaultLocalBasePath,
408 $defaultRemoteBasePath
409 );
410 }
411 }
412
413 if ( $feature === 'content-media' ) {
414 if ( $this->getConfig()->get( MainConfigNames::UseLegacyMediaStyles ) ) {
415 $featureFilePaths['all'][] = new FilePath(
416 'resources/src/mediawiki.skinning/content.thumbnails-common.less',
417 $defaultLocalBasePath,
418 $defaultRemoteBasePath
419 );
420 $featureFilePaths['screen'][] = new FilePath(
421 'resources/src/mediawiki.skinning/content.thumbnails-screen.less',
422 $defaultLocalBasePath,
423 $defaultRemoteBasePath
424 );
425 $featureFilePaths['print'][] = new FilePath(
426 'resources/src/mediawiki.skinning/content.thumbnails-print.less',
427 $defaultLocalBasePath,
428 $defaultRemoteBasePath
429 );
430 }
431 if (
432 !$this->getConfig()->get( MainConfigNames::ParserEnableLegacyMediaDOM ) ||
433 $this->getConfig()->get( MainConfigNames::UseContentMediaStyles )
434 ) {
435 $featureFilePaths['all'][] = new FilePath(
436 'resources/src/mediawiki.skinning/content.media-common.less',
437 $defaultLocalBasePath,
438 $defaultRemoteBasePath
439 );
440 $featureFilePaths['screen'][] = new FilePath(
441 'resources/src/mediawiki.skinning/content.media-screen.less',
442 $defaultLocalBasePath,
443 $defaultRemoteBasePath
444 );
445 $featureFilePaths['print'][] = new FilePath(
446 'resources/src/mediawiki.skinning/content.media-print.less',
447 $defaultLocalBasePath,
448 $defaultRemoteBasePath
449 );
450 }
451 }
452 }
453 }
454
455 // Styles defines in options are added to the $featureFilePaths to ensure
456 // that $featureFilePaths styles precede module defined ones.
457 // This is particularly important given the `normalize` styles need to be the first
458 // outputted (see T269618).
459 foreach ( $styles as $mediaType => $paths ) {
460 $featureFilePaths[$mediaType] = array_merge( $featureFilePaths[$mediaType] ?? [], $paths );
461 }
462
463 return $featureFilePaths;
464 }
465
470 public function getStyles( Context $context ) {
471 $logo = $this->getLogoData( $this->getConfig(), $context->getLanguage() );
472 $styles = parent::getStyles( $context );
473 $this->normalizeStyles( $styles );
474
475 $isLogoFeatureEnabled = in_array( 'logo', $this->features );
476 if ( $isLogoFeatureEnabled ) {
477 $default = !is_array( $logo ) ? $logo : ( $logo['svg'] ?? $logo['1x'] ?? null );
478 // Can't add logo CSS if no logo defined.
479 if ( !$default ) {
480 return $styles;
481 }
482 $styles['all'][] = '.mw-wiki-logo { background-image: ' .
483 CSSMin::buildUrlValue( $default ) .
484 '; }';
485
486 if ( is_array( $logo ) ) {
487 if ( isset( $logo['svg'] ) ) {
488 $styles['all'][] = '.mw-wiki-logo { ' .
489 'background-size: 135px auto; }';
490 } else {
491 if ( isset( $logo['1.5x'] ) ) {
492 $styles[
493 '(-webkit-min-device-pixel-ratio: 1.5), ' .
494 '(min-resolution: 1.5dppx), ' .
495 '(min-resolution: 144dpi)'
496 ][] = '.mw-wiki-logo { background-image: ' .
497 CSSMin::buildUrlValue( $logo['1.5x'] ) . ';' .
498 'background-size: 135px auto; }';
499 }
500 if ( isset( $logo['2x'] ) ) {
501 $styles[
502 '(-webkit-min-device-pixel-ratio: 2), ' .
503 '(min-resolution: 2dppx), ' .
504 '(min-resolution: 192dpi)'
505 ][] = '.mw-wiki-logo { background-image: ' .
506 CSSMin::buildUrlValue( $logo['2x'] ) . ';' .
507 'background-size: 135px auto; }';
508 }
509 }
510 }
511 }
512
513 return $styles;
514 }
515
520 public function getPreloadLinks( Context $context ): array {
521 if ( !in_array( 'logo', $this->features ) ) {
522 return [];
523 }
524
525 $logo = $this->getLogoData( $this->getConfig(), $context->getLanguage() );
526
527 if ( !is_array( $logo ) ) {
528 // No media queries required if we only have one variant
529 return [ $logo => [ 'as' => 'image' ] ];
530 }
531
532 if ( isset( $logo['svg'] ) ) {
533 // No media queries required if we only have a 1x and svg variant
534 // because all preload-capable browsers support SVGs
535 return [ $logo['svg'] => [ 'as' => 'image' ] ];
536 }
537
538 $logosPerDppx = [];
539 foreach ( $logo as $dppx => $src ) {
540 // Keys are in this format: "1.5x"
541 $dppx = substr( $dppx, 0, -1 );
542 $logosPerDppx[$dppx] = $src;
543 }
544
545 // Because PHP can't have floats as array keys
546 uksort( $logosPerDppx, static function ( $a, $b ) {
547 $a = floatval( $a );
548 $b = floatval( $b );
549 // Sort from smallest to largest (e.g. 1x, 1.5x, 2x)
550 return $a <=> $b;
551 } );
552
553 $logos = [];
554 foreach ( $logosPerDppx as $dppx => $src ) {
555 $logos[] = [
556 'dppx' => $dppx,
557 'src' => $src
558 ];
559 }
560
561 $logosCount = count( $logos );
562 $preloadLinks = [];
563 // Logic must match SkinModule:
564 // - 1x applies to resolution < 1.5dppx
565 // - 1.5x applies to resolution >= 1.5dppx && < 2dppx
566 // - 2x applies to resolution >= 2dppx
567 // Note that min-resolution and max-resolution are both inclusive.
568 for ( $i = 0; $i < $logosCount; $i++ ) {
569 if ( $i === 0 ) {
570 // Smallest dppx
571 // min-resolution is ">=" (larger than or equal to)
572 // "not min-resolution" is essentially "<"
573 $media_query = 'not all and (min-resolution: ' . $logos[1]['dppx'] . 'dppx)';
574 } elseif ( $i !== $logosCount - 1 ) {
575 // In between
576 // Media query expressions can only apply "not" to the entire expression
577 // (e.g. can't express ">= 1.5 and not >= 2).
578 // Workaround: Use <= 1.9999 in place of < 2.
579 $upper_bound = floatval( $logos[$i + 1]['dppx'] ) - 0.000001;
580 $media_query = '(min-resolution: ' . $logos[$i]['dppx'] .
581 'dppx) and (max-resolution: ' . $upper_bound . 'dppx)';
582 } else {
583 // Largest dppx
584 $media_query = '(min-resolution: ' . $logos[$i]['dppx'] . 'dppx)';
585 }
586
587 $preloadLinks[$logos[$i]['src']] = [
588 'as' => 'image',
589 'media' => $media_query
590 ];
591 }
592
593 return $preloadLinks;
594 }
595
604 private function normalizeStyles( array &$styles ): void {
605 foreach ( $styles as $key => $val ) {
606 if ( !is_array( $val ) ) {
607 $styles[$key] = [ $val ];
608 }
609 }
610 }
611
618 private static function getRelativeSizedLogo( array $logoElement ) {
619 $width = $logoElement['width'];
620 $height = $logoElement['height'];
621 $widthRelative = $width / 16;
622 $heightRelative = $height / 16;
623 // Allow skins to scale the wordmark with browser font size (T207789)
624 $logoElement['style'] = 'width: ' . $widthRelative . 'em; height: ' . $heightRelative . 'em;';
625 return $logoElement;
626 }
627
643 public static function getAvailableLogos( Config $conf, string $lang = null ): array {
644 $logos = $conf->get( MainConfigNames::Logos );
645 if ( $logos === false ) {
646 // no logos were defined... this will either
647 // 1. Load from wgLogo and wgLogoHD
648 // 2. Trigger runtime exception if those are not defined.
649 $logos = [];
650 }
651 if ( $lang && isset( $logos['variants'][$lang] ) ) {
652 foreach ( $logos['variants'][$lang] as $type => $value ) {
653 $logos[$type] = $value;
654 }
655 }
656
657 // If logos['1x'] is not defined, see if we can use wgLogo
658 if ( !isset( $logos[ '1x' ] ) ) {
659 $logo = $conf->get( MainConfigNames::Logo );
660 if ( $logo ) {
661 $logos['1x'] = $logo;
662 }
663 }
664
665 try {
666 $logoHD = $conf->get( MainConfigNames::LogoHD );
667 // make sure not false
668 if ( $logoHD ) {
669 // wfDeprecated( __METHOD__ . ' with $wgLogoHD set instead of $wgLogos', '1.35', false, 1 );
670 $logos += $logoHD;
671 }
672 } catch ( ConfigException $e ) {
673 // no backwards compatibility changes needed.
674 }
675
676 if ( isset( $logos['wordmark'] ) ) {
677 // Allow skins to scale the wordmark with browser font size (T207789)
678 $logos['wordmark'] = self::getRelativeSizedLogo( $logos['wordmark'] );
679 }
680 if ( isset( $logos['tagline'] ) ) {
681 $logos['tagline'] = self::getRelativeSizedLogo( $logos['tagline'] );
682 }
683
684 return $logos;
685 }
686
696 protected function getLogoData( Config $conf, string $lang = null ) {
697 $logoHD = self::getAvailableLogos( $conf, $lang );
698 $logo = $logoHD['1x'];
699
700 $logo1Url = OutputPage::transformResourcePath( $conf, $logo );
701
702 $logoUrls = [
703 '1x' => $logo1Url,
704 ];
705
706 if ( isset( $logoHD['svg'] ) ) {
707 $logoUrls['svg'] = OutputPage::transformResourcePath(
708 $conf,
709 $logoHD['svg']
710 );
711 } elseif ( isset( $logoHD['1.5x'] ) || isset( $logoHD['2x'] ) ) {
712 // Only 1.5x and 2x are supported
713 if ( isset( $logoHD['1.5x'] ) ) {
714 $logoUrls['1.5x'] = OutputPage::transformResourcePath(
715 $conf,
716 $logoHD['1.5x']
717 );
718 }
719 if ( isset( $logoHD['2x'] ) ) {
720 $logoUrls['2x'] = OutputPage::transformResourcePath(
721 $conf,
722 $logoHD['2x']
723 );
724 }
725 } else {
726 // Return a string rather than a one-element array, getLogoPreloadlinks depends on this
727 return $logo1Url;
728 }
729
730 return $logoUrls;
731 }
732
737 public function isKnownEmpty( Context $context ) {
738 // Regardless of whether the files are specified, we always
739 // provide mw-wiki-logo styles.
740 return false;
741 }
742
749 protected function getLessVars( Context $context ) {
750 $lessVars = parent::getLessVars( $context );
751 $logos = self::getAvailableLogos( $this->getConfig() );
752
753 if ( isset( $logos['wordmark'] ) ) {
754 $logo = $logos['wordmark'];
755 $lessVars[ 'logo-enabled' ] = true;
756 $lessVars[ 'logo-wordmark-url' ] = CSSMin::buildUrlValue( $logo['src'] );
757 $lessVars[ 'logo-wordmark-width' ] = intval( $logo['width'] );
758 $lessVars[ 'logo-wordmark-height' ] = intval( $logo['height'] );
759 } else {
760 $lessVars[ 'logo-enabled' ] = false;
761 }
762 return $lessVars;
763 }
764
765 public function getDefinitionSummary( Context $context ) {
766 $summary = parent::getDefinitionSummary( $context );
767 $summary[] = [
768 'logos' => self::getAvailableLogos( $this->getConfig() ),
769 ];
770 return $summary;
771 }
772}
773
775class_alias( SkinModule::class, 'ResourceLoaderSkinModule' );
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:88
Exceptions for config failures.
A class containing constants representing the names of configuration variables.
const UseContentMediaStyles
Name constant for the UseContentMediaStyles setting, for use with Config::get()
const ResourceBasePath
Name constant for the ResourceBasePath setting, for use with Config::get()
const ParserEnableLegacyMediaDOM
Name constant for the ParserEnableLegacyMediaDOM setting, for use with Config::get()
const UseLegacyMediaStyles
Name constant for the UseLegacyMediaStyles setting, for use with Config::get()
This is one of the Core classes and should be read at least once by any new developers.
Context object that contains information about the state of a specific ResourceLoader web request.
Definition Context.php:45
static extractBasePaths(array $options=[], $localBasePath=null, $remoteBasePath=null)
Extract a pair of local and remote base paths from module definition information.
string $remoteBasePath
Remote base path, see __construct()
string $localBasePath
Local base path, see __construct()
string[] $messages
List of message keys used by this module.
A path to a bundled file (such as JavaScript or CSS), along with a remote and local base path.
Definition FilePath.php:34
Module augmented with context-specific LESS variables.
Module for skin stylesheets.
static applyFeaturesCompatibility(array $features, bool $addUnspecifiedFeatures=true, &$messages='')
getLessVars(Context $context)
Get language-specific LESS variables for this module.
static getAvailableLogos(Config $conf, string $lang=null)
Return an array of all available logos that a skin may use.
getLogoData(Config $conf, string $lang=null)
getStyleFiles(Context $context)
Get styles defined in the module definition, plus any enabled feature styles.
getDefinitionSummary(Context $context)
Get the definition summary for this module.
__construct(array $options=[], $localBasePath=null, $remoteBasePath=null)
Interface for configuration instances.
Definition Config.php:32
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
return true
Definition router.php:92