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