MediaWiki REL1_39
SkinModule.php
Go to the documentation of this file.
1<?php
21
22use Config;
24use InvalidArgumentException;
26use OutputPage;
27use Wikimedia\Minify\CSSMin;
28
39 public $targets = [ 'desktop', 'mobile' ];
40
139 private const FEATURE_FILES = [
140 'accessibility' => [
141 'all' => [ 'resources/src/mediawiki.skinning/accessibility.less' ],
142 ],
143 'normalize' => [
144 'all' => [ 'resources/src/mediawiki.skinning/normalize.less' ],
145 ],
146 'logo' => [
147 // Applies the logo and ensures it downloads prior to printing.
148 'all' => [ 'resources/src/mediawiki.skinning/logo.less' ],
149 // Reserves whitespace for the logo in a pseudo element.
150 'print' => [ 'resources/src/mediawiki.skinning/logo-print.less' ],
151 ],
152 'content-media' => [
153 'all' => [ 'resources/src/mediawiki.skinning/content.thumbnails-common.less' ],
154 'screen' => [ 'resources/src/mediawiki.skinning/content.thumbnails-screen.less' ],
155 'print' => [ 'resources/src/mediawiki.skinning/content.thumbnails-print.less' ],
156 ],
157 'content-links' => [
158 'screen' => [ 'resources/src/mediawiki.skinning/content.links.less' ]
159 ],
160 'content-links-external' => [
161 'screen' => [ 'resources/src/mediawiki.skinning/content.externallinks.less' ]
162 ],
163 'content-body' => [
164 'screen' => [ 'resources/src/mediawiki.skinning/content.body.less' ],
165 'print' => [ 'resources/src/mediawiki.skinning/content.body-print.less' ],
166 ],
167 'content-tables' => [
168 'screen' => [ 'resources/src/mediawiki.skinning/content.tables.less' ],
169 'print' => [ 'resources/src/mediawiki.skinning/content.tables-print.less' ]
170 ],
171 // Legacy shorthand for 6 features: interface-core, interface-edit-section-links,
172 // interface-indicators, interface-subtitle, interface-site-notice, interface-user-message
173 'interface' => [],
174 'interface-category' => [
175 'screen' => [ 'resources/src/mediawiki.skinning/interface.category.less' ],
176 'print' => [ 'resources/src/mediawiki.skinning/interface.category-print.less' ],
177 ],
178 'interface-core' => [
179 'screen' => [ 'resources/src/mediawiki.skinning/interface.less' ],
180 'print' => [ 'resources/src/mediawiki.skinning/interface-print.less' ],
181 ],
182 'interface-edit-section-links' => [
183 'screen' => [ 'resources/src/mediawiki.skinning/interface-edit-section-links.less' ],
184 ],
185 'interface-indicators' => [
186 'screen' => [ 'resources/src/mediawiki.skinning/interface-indicators.less' ],
187 ],
188 'interface-site-notice' => [
189 'screen' => [ 'resources/src/mediawiki.skinning/interface-site-notice.less' ],
190 ],
191 'interface-subtitle' => [
192 'screen' => [ 'resources/src/mediawiki.skinning/interface-subtitle.less' ],
193 ],
194 'interface-message-box' => [
195 'all' => [ 'resources/src/mediawiki.skinning/messageBoxes.less' ],
196 ],
197 'interface-user-message' => [
198 'screen' => [ 'resources/src/mediawiki.skinning/interface-user-message.less' ],
199 ],
200 'elements' => [
201 'screen' => [ 'resources/src/mediawiki.skinning/elements.less' ],
202 'print' => [ 'resources/src/mediawiki.skinning/elements-print.less' ],
203 ],
204 // The styles of the legacy feature was removed in 1.39. This can be removed when no skins are referencing it
205 // (Dropping this line will trigger InvalidArgumentException: Feature 'legacy' is not recognised)
206 'legacy' => [],
207 'i18n-ordered-lists' => [
208 'screen' => [ 'resources/src/mediawiki.skinning/i18n-ordered-lists.less' ],
209 ],
210 'i18n-all-lists-margins' => [
211 'screen' => [ 'resources/src/mediawiki.skinning/i18n-all-lists-margins.less' ],
212 ],
213 'i18n-headings' => [
214 'screen' => [ 'resources/src/mediawiki.skinning/i18n-headings.less' ],
215 ],
216 'toc' => [
217 'all' => [ 'resources/src/mediawiki.skinning/toc/common.css' ],
218 'screen' => [ 'resources/src/mediawiki.skinning/toc/screen.less' ],
219 'print' => [ 'resources/src/mediawiki.skinning/toc/print.css' ],
220 ],
221 ];
222
224 private $features;
225
233 private const DEFAULT_FEATURES_SPECIFIED = [
234 'accessibility' => true,
235 'content-body' => true,
236 'interface-core' => true,
237 'toc' => true,
238 ];
239
248 private const DEFAULT_FEATURES_ABSENT = [
249 'logo',
250 ];
251
252 private const LESS_MESSAGES = [
253 // `toc` feature, used in screen.less
254 'hidetoc',
255 'showtoc',
256 ];
257
278 public function __construct(
279 array $options = [],
280 $localBasePath = null,
281 $remoteBasePath = null
282 ) {
283 $features = $options['features'] ?? self::DEFAULT_FEATURES_ABSENT;
284 $listMode = array_keys( $features ) === range( 0, count( $features ) - 1 );
285
286 $messages = '';
287 // NOTE: Compatibility is only applied when features are provided
288 // in map-form. The list-form does not currently get these.
289 $features = $listMode ? self::applyFeaturesCompatibility(
290 array_fill_keys( $features, true ), false, $messages
291 ) : self::applyFeaturesCompatibility( $features, true, $messages );
292
293 foreach ( $features as $key => $enabled ) {
294 if ( !isset( self::FEATURE_FILES[$key] ) ) {
295 throw new InvalidArgumentException( "Feature '$key' is not recognised" );
296 }
297 }
298
299 $this->features = $listMode
300 ? array_keys( array_filter( $features ) )
301 : array_keys( array_filter( $features + self::DEFAULT_FEATURES_SPECIFIED ) );
302
303 // Only the `toc` feature makes use of interface messages.
304 // For skins not using the `toc` feature, make sure LocalisationCache
305 // remains untouched (T270027).
306 if ( in_array( 'toc', $this->features ) ) {
307 $options['lessMessages'] = array_merge(
308 $options['lessMessages'] ?? [],
309 self::LESS_MESSAGES
310 );
311 }
312
313 if ( $messages !== '' ) {
314 $messages .= 'More information can be found at [[mw:Manual:ResourceLoaderSkinModule]]. ';
315 $options['deprecated'] = $messages;
316 }
317 parent::__construct( $options, $localBasePath, $remoteBasePath );
318 }
319
327 protected static function applyFeaturesCompatibility(
328 array $features, bool $addUnspecifiedFeatures = true, &$messages = ''
329 ): array {
330 // The `content` feature is mapped to `content-media`.
331 if ( isset( $features[ 'content' ] ) ) {
332 $features[ 'content-media' ] = $features[ 'content' ];
333 unset( $features[ 'content' ] );
334 $messages .= '[1.37] The use of the `content` feature with ResourceLoaderSkinModule'
335 . ' is deprecated. Use `content-media` instead. ';
336 }
337
338 // The `content-thumbnails` feature is mapped to `content-media`.
339 if ( isset( $features[ 'content-thumbnails' ] ) ) {
340 $features[ 'content-media' ] = $features[ 'content-thumbnails' ];
341 $messages .= '[1.37] The use of the `content-thumbnails` feature with ResourceLoaderSkinModule'
342 . ' is deprecated. Use `content-media` instead. ';
343 unset( $features[ 'content-thumbnails' ] );
344 }
345
346 // If `content-links` feature is set but no preference for `content-links-external` is set
347 if ( $addUnspecifiedFeatures && isset( $features[ 'content-links' ] )
348 && !isset( $features[ 'content-links-external' ] )
349 ) {
350 // Assume the same true/false preference for both.
351 $features[ 'content-links-external' ] = $features[ 'content-links' ];
352 }
353
354 // The legacy feature no longer exists (T89981) but to avoid fatals in skins is retained.
355 if ( isset( $features['legacy'] ) && $features['legacy'] ) {
356 $messages .= '[1.37] The use of the `legacy` feature with ResourceLoaderSkinModule is deprecated'
357 . '(T89981) and is a NOOP since 1.39 (T304325). This should be urgently omited to retain compatibility '
358 . 'with future MediaWiki versions';
359 }
360
361 // The `content-links` feature was split out from `elements`.
362 // Make sure skins asking for `elements` also get these by default.
363 if ( $addUnspecifiedFeatures && isset( $features[ 'element' ] ) && !isset( $features[ 'content-links' ] ) ) {
364 $features[ 'content-links' ] = $features[ 'element' ];
365 }
366
367 // `content-parser-output` was renamed to `content-body`.
368 // No need to go through deprecation process here since content-parser-output added and removed in 1.36.
369 // Remove this check when no matches for
370 // https://codesearch.wmcloud.org/search/?q=content-parser-output&i=nope&files=&excludeFiles=&repos=
371 if ( isset( $features[ 'content-parser-output' ] ) ) {
372 $features[ 'content-body' ] = $features[ 'content-parser-output' ];
373 unset( $features[ 'content-parser-output' ] );
374 }
375
376 // The interface module is a short hand for several modules. Enable them now.
377 if ( isset( $features[ 'interface' ] ) && $features[ 'interface' ] ) {
378 unset( $features[ 'interface' ] );
379 $features[ 'interface-core' ] = true;
380 $features[ 'interface-indicators' ] = true;
381 $features[ 'interface-subtitle' ] = true;
382 $features[ 'interface-user-message' ] = true;
383 $features[ 'interface-site-notice' ] = true;
384 $features[ 'interface-edit-section-links' ] = true;
385 }
386 return $features;
387 }
388
395 public function getStyleFiles( Context $context ) {
396 $styles = parent::getStyleFiles( $context );
397
398 // Bypass the current module paths so that these files are served from core,
399 // instead of the individual skin's module directory.
400 list( $defaultLocalBasePath, $defaultRemoteBasePath ) =
402 [],
403 null,
404 $this->getConfig()->get( MainConfigNames::ResourceBasePath )
405 );
406
407 $featureFilePaths = [];
408
409 foreach ( self::FEATURE_FILES as $feature => $featureFiles ) {
410 if ( in_array( $feature, $this->features ) ) {
411 foreach ( $featureFiles as $mediaType => $files ) {
412 foreach ( $files as $filepath ) {
413 $featureFilePaths[$mediaType][] = new FilePath(
414 $filepath,
415 $defaultLocalBasePath,
416 $defaultRemoteBasePath
417 );
418 }
419 }
420 if ( $feature === 'content-media' && (
421 !$this->getConfig()->get( MainConfigNames::ParserEnableLegacyMediaDOM ) ||
422 $this->getConfig()->get( MainConfigNames::UseContentMediaStyles )
423 ) ) {
424 $featureFilePaths['all'][] = new FilePath(
425 'resources/src/mediawiki.skinning/content.media-common.less',
426 $defaultLocalBasePath,
427 $defaultRemoteBasePath
428 );
429 $featureFilePaths['screen'][] = new FilePath(
430 'resources/src/mediawiki.skinning/content.media-screen.less',
431 $defaultLocalBasePath,
432 $defaultRemoteBasePath
433 );
434 $featureFilePaths['print'][] = new FilePath(
435 'resources/src/mediawiki.skinning/content.media-print.less',
436 $defaultLocalBasePath,
437 $defaultRemoteBasePath
438 );
439 }
440 }
441 }
442
443 // Styles defines in options are added to the $featureFilePaths to ensure
444 // that $featureFilePaths styles precede module defined ones.
445 // This is particularly important given the `normalize` styles need to be the first
446 // outputted (see T269618).
447 foreach ( $styles as $mediaType => $paths ) {
448 $featureFilePaths[$mediaType] = array_merge( $featureFilePaths[$mediaType] ?? [], $paths );
449 }
450
451 return $featureFilePaths;
452 }
453
458 public function getStyles( Context $context ) {
459 $logo = $this->getLogoData( $this->getConfig(), $context->getLanguage() );
460 $styles = parent::getStyles( $context );
461 $this->normalizeStyles( $styles );
462
463 $isLogoFeatureEnabled = in_array( 'logo', $this->features );
464 if ( $isLogoFeatureEnabled ) {
465 $default = !is_array( $logo ) ? $logo : ( $logo['1x'] ?? null );
466 // Can't add logo CSS if no logo defined.
467 if ( !$default ) {
468 return $styles;
469 }
470 $styles['all'][] = '.mw-wiki-logo { background-image: ' .
471 CSSMin::buildUrlValue( $default ) .
472 '; }';
473
474 if ( is_array( $logo ) ) {
475 if ( isset( $logo['svg'] ) ) {
476 $styles['all'][] = '.mw-wiki-logo { ' .
477 'background-image: linear-gradient(transparent, transparent), ' .
478 CSSMin::buildUrlValue( $logo['svg'] ) . ';' .
479 'background-size: 135px auto; }';
480 } else {
481 if ( isset( $logo['1.5x'] ) ) {
482 $styles[
483 '(-webkit-min-device-pixel-ratio: 1.5), ' .
484 '(min-resolution: 1.5dppx), ' .
485 '(min-resolution: 144dpi)'
486 ][] = '.mw-wiki-logo { background-image: ' .
487 CSSMin::buildUrlValue( $logo['1.5x'] ) . ';' .
488 'background-size: 135px auto; }';
489 }
490 if ( isset( $logo['2x'] ) ) {
491 $styles[
492 '(-webkit-min-device-pixel-ratio: 2), ' .
493 '(min-resolution: 2dppx), ' .
494 '(min-resolution: 192dpi)'
495 ][] = '.mw-wiki-logo { background-image: ' .
496 CSSMin::buildUrlValue( $logo['2x'] ) . ';' .
497 'background-size: 135px auto; }';
498 }
499 }
500 }
501 }
502
503 return $styles;
504 }
505
510 public function getPreloadLinks( Context $context ): array {
511 if ( !in_array( 'logo', $this->features ) ) {
512 return [];
513 }
514
515 $logo = $this->getLogoData( $this->getConfig(), $context->getLanguage() );
516
517 if ( !is_array( $logo ) ) {
518 // No media queries required if we only have one variant
519 return [ $logo => [ 'as' => 'image' ] ];
520 }
521
522 if ( isset( $logo['svg'] ) ) {
523 // No media queries required if we only have a 1x and svg variant
524 // because all preload-capable browsers support SVGs
525 return [ $logo['svg'] => [ 'as' => 'image' ] ];
526 }
527
528 $logosPerDppx = [];
529 foreach ( $logo as $dppx => $src ) {
530 // Keys are in this format: "1.5x"
531 $dppx = substr( $dppx, 0, -1 );
532 $logosPerDppx[$dppx] = $src;
533 }
534
535 // Because PHP can't have floats as array keys
536 uksort( $logosPerDppx, static function ( $a, $b ) {
537 $a = floatval( $a );
538 $b = floatval( $b );
539 // Sort from smallest to largest (e.g. 1x, 1.5x, 2x)
540 return $a <=> $b;
541 } );
542
543 $logos = [];
544 foreach ( $logosPerDppx as $dppx => $src ) {
545 $logos[] = [
546 'dppx' => $dppx,
547 'src' => $src
548 ];
549 }
550
551 $logosCount = count( $logos );
552 $preloadLinks = [];
553 // Logic must match SkinModule:
554 // - 1x applies to resolution < 1.5dppx
555 // - 1.5x applies to resolution >= 1.5dppx && < 2dppx
556 // - 2x applies to resolution >= 2dppx
557 // Note that min-resolution and max-resolution are both inclusive.
558 for ( $i = 0; $i < $logosCount; $i++ ) {
559 if ( $i === 0 ) {
560 // Smallest dppx
561 // min-resolution is ">=" (larger than or equal to)
562 // "not min-resolution" is essentially "<"
563 $media_query = 'not all and (min-resolution: ' . $logos[1]['dppx'] . 'dppx)';
564 } elseif ( $i !== $logosCount - 1 ) {
565 // In between
566 // Media query expressions can only apply "not" to the entire expression
567 // (e.g. can't express ">= 1.5 and not >= 2).
568 // Workaround: Use <= 1.9999 in place of < 2.
569 $upper_bound = floatval( $logos[$i + 1]['dppx'] ) - 0.000001;
570 $media_query = '(min-resolution: ' . $logos[$i]['dppx'] .
571 'dppx) and (max-resolution: ' . $upper_bound . 'dppx)';
572 } else {
573 // Largest dppx
574 $media_query = '(min-resolution: ' . $logos[$i]['dppx'] . 'dppx)';
575 }
576
577 $preloadLinks[$logos[$i]['src']] = [
578 'as' => 'image',
579 'media' => $media_query
580 ];
581 }
582
583 return $preloadLinks;
584 }
585
594 private function normalizeStyles( array &$styles ): void {
595 foreach ( $styles as $key => $val ) {
596 if ( !is_array( $val ) ) {
597 $styles[$key] = [ $val ];
598 }
599 }
600 }
601
608 private static function getRelativeSizedLogo( array $logoElement ) {
609 $width = $logoElement['width'];
610 $height = $logoElement['height'];
611 $widthRelative = $width / 16;
612 $heightRelative = $height / 16;
613 // Allow skins to scale the wordmark with browser font size (T207789)
614 $logoElement['style'] = 'width: ' . $widthRelative . 'em; height: ' . $heightRelative . 'em;';
615 return $logoElement;
616 }
617
633 public static function getAvailableLogos( Config $conf, string $lang = null ): array {
634 $logos = $conf->get( MainConfigNames::Logos );
635 if ( $logos === false ) {
636 // no logos were defined... this will either
637 // 1. Load from wgLogo and wgLogoHD
638 // 2. Trigger runtime exception if those are not defined.
639 $logos = [];
640 }
641 if ( $lang && isset( $logos['variants'][$lang] ) ) {
642 foreach ( $logos['variants'][$lang] as $type => $value ) {
643 $logos[$type] = $value;
644 }
645 }
646
647 // If logos['1x'] is not defined, see if we can use wgLogo
648 if ( !isset( $logos[ '1x' ] ) ) {
649 $logo = $conf->get( MainConfigNames::Logo );
650 if ( $logo ) {
651 $logos['1x'] = $logo;
652 }
653 }
654
655 try {
656 $logoHD = $conf->get( MainConfigNames::LogoHD );
657 // make sure not false
658 if ( $logoHD ) {
659 // wfDeprecated( __METHOD__ . ' with $wgLogoHD set instead of $wgLogos', '1.35', false, 1 );
660 $logos += $logoHD;
661 }
662 } catch ( ConfigException $e ) {
663 // no backwards compatibility changes needed.
664 }
665
666 if ( isset( $logos['wordmark'] ) ) {
667 // Allow skins to scale the wordmark with browser font size (T207789)
668 $logos['wordmark'] = self::getRelativeSizedLogo( $logos['wordmark'] );
669 }
670 if ( isset( $logos['tagline'] ) ) {
671 $logos['tagline'] = self::getRelativeSizedLogo( $logos['tagline'] );
672 }
673
674 return $logos;
675 }
676
686 protected function getLogoData( Config $conf, string $lang = null ) {
687 $logoHD = self::getAvailableLogos( $conf, $lang );
688 $logo = $logoHD['1x'];
689
690 $logo1Url = OutputPage::transformResourcePath( $conf, $logo );
691
692 $logoUrls = [
693 '1x' => $logo1Url,
694 ];
695
696 if ( isset( $logoHD['svg'] ) ) {
697 $logoUrls['svg'] = OutputPage::transformResourcePath(
698 $conf,
699 $logoHD['svg']
700 );
701 } elseif ( isset( $logoHD['1.5x'] ) || isset( $logoHD['2x'] ) ) {
702 // Only 1.5x and 2x are supported
703 if ( isset( $logoHD['1.5x'] ) ) {
704 $logoUrls['1.5x'] = OutputPage::transformResourcePath(
705 $conf,
706 $logoHD['1.5x']
707 );
708 }
709 if ( isset( $logoHD['2x'] ) ) {
710 $logoUrls['2x'] = OutputPage::transformResourcePath(
711 $conf,
712 $logoHD['2x']
713 );
714 }
715 } else {
716 // Return a string rather than a one-element array, getLogoPreloadlinks depends on this
717 return $logo1Url;
718 }
719
720 return $logoUrls;
721 }
722
727 public function isKnownEmpty( Context $context ) {
728 // Regardless of whether the files are specified, we always
729 // provide mw-wiki-logo styles.
730 return false;
731 }
732
739 protected function getLessVars( Context $context ) {
740 $lessVars = parent::getLessVars( $context );
741 $logos = self::getAvailableLogos( $this->getConfig() );
742
743 if ( isset( $logos['wordmark'] ) ) {
744 $logo = $logos['wordmark'];
745 $lessVars[ 'logo-enabled' ] = true;
746 $lessVars[ 'logo-wordmark-url' ] = CSSMin::buildUrlValue( $logo['src'] );
747 $lessVars[ 'logo-wordmark-width' ] = intval( $logo['width'] );
748 $lessVars[ 'logo-wordmark-height' ] = intval( $logo['height'] );
749 } else {
750 $lessVars[ 'logo-enabled' ] = false;
751 }
752 return $lessVars;
753 }
754
755 public function getDefinitionSummary( Context $context ) {
756 $summary = parent::getDefinitionSummary( $context );
757 $summary[] = [
758 'logos' => self::getAvailableLogos( $this->getConfig() ),
759 ];
760 return $summary;
761 }
762}
763
765class_alias( SkinModule::class, 'ResourceLoaderSkinModule' );
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
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()
Context object that contains information about the state of a specific ResourceLoader web request.
Definition Context.php:46
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.
$targets
All skins are assumed to be compatible with mobile.
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)
This is one of the Core classes and should be read at least once by any new developers.
Interface for configuration instances.
Definition Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
return true
Definition router.php:92
if(!isset( $args[0])) $lang