2014-11-11 19:50:44 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
|
|
|
|
* @file
|
|
|
|
|
* @author Trevor Parscal
|
|
|
|
|
*/
|
2022-05-06 09:09:56 +00:00
|
|
|
namespace MediaWiki\ResourceLoader;
|
|
|
|
|
|
2024-09-27 17:43:25 +00:00
|
|
|
use DomainException;
|
2022-05-06 09:09:56 +00:00
|
|
|
use InvalidArgumentException;
|
2021-02-18 04:38:29 +00:00
|
|
|
use Wikimedia\Minify\CSSMin;
|
2014-11-11 19:50:44 +00:00
|
|
|
|
|
|
|
|
/**
|
2019-09-14 04:32:54 +00:00
|
|
|
* Module for generated and embedded images.
|
2014-11-11 19:50:44 +00:00
|
|
|
*
|
2019-09-14 04:32:54 +00:00
|
|
|
* @ingroup ResourceLoader
|
2014-11-11 19:50:44 +00:00
|
|
|
* @since 1.25
|
|
|
|
|
*/
|
2022-05-06 09:09:56 +00:00
|
|
|
class ImageModule extends Module {
|
2024-06-04 22:00:29 +00:00
|
|
|
/** @var bool */
|
|
|
|
|
private $useMaskImage;
|
2021-09-22 20:16:51 +00:00
|
|
|
/** @var array|null */
|
2019-10-12 14:13:38 +00:00
|
|
|
protected $definition;
|
2015-04-07 16:02:46 +00:00
|
|
|
|
2014-11-11 19:50:44 +00:00
|
|
|
/**
|
|
|
|
|
* Local base path, see __construct()
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
protected $localBasePath = '';
|
|
|
|
|
|
2024-09-07 19:25:51 +00:00
|
|
|
/** @inheritDoc */
|
2014-11-11 19:50:44 +00:00
|
|
|
protected $origin = self::ORIGIN_CORE_SITEWIDE;
|
|
|
|
|
|
2022-05-06 09:09:56 +00:00
|
|
|
/** @var Image[][]|null */
|
2019-02-23 20:35:15 +00:00
|
|
|
protected $imageObjects = null;
|
|
|
|
|
/** @var array */
|
2016-02-17 09:09:32 +00:00
|
|
|
protected $images = [];
|
2019-02-23 20:35:15 +00:00
|
|
|
/** @var string|null */
|
2018-06-25 22:55:19 +00:00
|
|
|
protected $defaultColor = null;
|
2024-09-07 19:25:51 +00:00
|
|
|
/** @var bool */
|
2018-07-05 23:51:30 +00:00
|
|
|
protected $useDataURI = true;
|
2019-02-23 20:35:15 +00:00
|
|
|
/** @var array|null */
|
|
|
|
|
protected $globalVariants = null;
|
|
|
|
|
/** @var array */
|
2016-02-17 09:09:32 +00:00
|
|
|
protected $variants = [];
|
2019-02-23 20:35:15 +00:00
|
|
|
/** @var string|null */
|
2015-03-23 21:04:54 +00:00
|
|
|
protected $prefix = null;
|
2024-09-07 19:25:51 +00:00
|
|
|
/** @var string */
|
2015-03-27 17:27:47 +00:00
|
|
|
protected $selectorWithoutVariant = '.{prefix}-{name}';
|
2024-09-07 19:25:51 +00:00
|
|
|
/** @var string */
|
2015-03-27 17:27:47 +00:00
|
|
|
protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
|
2014-11-11 19:50:44 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructs a new module from an options array.
|
|
|
|
|
*
|
|
|
|
|
* @param array $options List of options; if not given or empty, an empty module will be
|
|
|
|
|
* constructed
|
2018-06-26 21:14:43 +00:00
|
|
|
* @param string|null $localBasePath Base path to prepend to all local paths in $options. Defaults
|
2014-11-11 19:50:44 +00:00
|
|
|
* to $IP
|
|
|
|
|
*
|
|
|
|
|
* Below is a description for the $options array:
|
|
|
|
|
* @par Construction options:
|
|
|
|
|
* @code
|
2016-09-12 10:06:37 +00:00
|
|
|
* [
|
2024-06-04 22:00:29 +00:00
|
|
|
* // When set the icon will use mask-image instead of background-image for the CSS output. Using mask-image
|
|
|
|
|
* // allows colorization of SVGs in Codex. Defaults to false for backwards compatibility.
|
|
|
|
|
* 'useMaskImage' => false,
|
2014-11-11 19:50:44 +00:00
|
|
|
* // Base path to prepend to all local paths in $options. Defaults to $IP
|
|
|
|
|
* 'localBasePath' => [base path],
|
2015-04-07 16:02:46 +00:00
|
|
|
* // Path to JSON file that contains any of the settings below
|
|
|
|
|
* 'data' => [file path string]
|
2014-11-11 19:50:44 +00:00
|
|
|
* // CSS class prefix to use in all style rules
|
|
|
|
|
* 'prefix' => [CSS class prefix],
|
2015-03-23 21:04:54 +00:00
|
|
|
* // Alternatively: Format of CSS selector to use in all style rules
|
2015-03-27 17:27:47 +00:00
|
|
|
* 'selector' => [CSS selector template, variables: {prefix} {name} {variant}],
|
2015-03-23 21:04:54 +00:00
|
|
|
* // Alternatively: When using variants
|
2015-03-27 17:27:47 +00:00
|
|
|
* 'selectorWithoutVariant' => [CSS selector template, variables: {prefix} {name}],
|
|
|
|
|
* 'selectorWithVariant' => [CSS selector template, variables: {prefix} {name} {variant}],
|
2014-11-11 19:50:44 +00:00
|
|
|
* // List of variants that may be used for the image files
|
2016-09-12 10:06:37 +00:00
|
|
|
* 'variants' => [
|
2017-03-17 01:03:04 +00:00
|
|
|
* // This level of nesting can be omitted if you use the same images for every skin
|
|
|
|
|
* [skin name (or 'default')] => [
|
2016-09-12 10:06:37 +00:00
|
|
|
* [variant name] => [
|
2014-11-11 19:50:44 +00:00
|
|
|
* 'color' => [color string, e.g. '#ffff00'],
|
2015-03-15 01:52:45 +00:00
|
|
|
* 'global' => [boolean, if true, this variant is available
|
|
|
|
|
* for all images of this type],
|
2016-09-12 10:06:37 +00:00
|
|
|
* ],
|
2015-05-24 16:56:44 +00:00
|
|
|
* ...
|
2016-09-12 10:06:37 +00:00
|
|
|
* ],
|
2015-03-27 17:27:47 +00:00
|
|
|
* ...
|
2016-09-12 10:06:37 +00:00
|
|
|
* ],
|
2014-11-11 19:50:44 +00:00
|
|
|
* // List of image files and their options
|
2016-09-12 10:06:37 +00:00
|
|
|
* 'images' => [
|
2017-03-17 01:03:04 +00:00
|
|
|
* // This level of nesting can be omitted if you use the same images for every skin
|
|
|
|
|
* [skin name (or 'default')] => [
|
2016-09-12 10:06:37 +00:00
|
|
|
* [icon name] => [
|
2015-06-17 02:33:59 +00:00
|
|
|
* 'file' => [file path string or array whose values are file path strings
|
|
|
|
|
* and whose keys are 'default', 'ltr', 'rtl', a single
|
|
|
|
|
* language code like 'en', or a list of language codes like
|
|
|
|
|
* 'en,de,ar'],
|
2015-03-15 01:52:45 +00:00
|
|
|
* 'variants' => [array of variant name strings, variants
|
|
|
|
|
* available for this image],
|
2016-09-12 10:06:37 +00:00
|
|
|
* ],
|
2015-05-24 16:56:44 +00:00
|
|
|
* ...
|
2016-09-12 10:06:37 +00:00
|
|
|
* ],
|
2015-03-27 17:27:47 +00:00
|
|
|
* ...
|
2016-09-12 10:06:37 +00:00
|
|
|
* ],
|
|
|
|
|
* ]
|
2014-11-11 19:50:44 +00:00
|
|
|
* @endcode
|
|
|
|
|
*/
|
2019-10-05 07:38:41 +00:00
|
|
|
public function __construct( array $options = [], $localBasePath = null ) {
|
2024-06-04 22:00:29 +00:00
|
|
|
$this->useMaskImage = $options['useMaskImage'] ?? false;
|
2019-06-11 22:50:02 +00:00
|
|
|
$this->localBasePath = static::extractLocalBasePath( $options, $localBasePath );
|
2014-11-11 19:50:44 +00:00
|
|
|
|
2015-04-07 16:02:46 +00:00
|
|
|
$this->definition = $options;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parse definition and external JSON data, if referenced.
|
|
|
|
|
*/
|
2015-05-24 16:56:44 +00:00
|
|
|
protected function loadFromDefinition() {
|
2015-04-07 16:02:46 +00:00
|
|
|
if ( $this->definition === null ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$options = $this->definition;
|
|
|
|
|
$this->definition = null;
|
|
|
|
|
|
|
|
|
|
if ( isset( $options['data'] ) ) {
|
2017-03-17 02:14:05 +00:00
|
|
|
$dataPath = $this->getLocalPath( $options['data'] );
|
2015-04-07 16:02:46 +00:00
|
|
|
$data = json_decode( file_get_contents( $dataPath ), true );
|
|
|
|
|
$options = array_merge( $data, $options );
|
|
|
|
|
}
|
|
|
|
|
|
2015-03-23 21:04:54 +00:00
|
|
|
// Accepted combinations:
|
|
|
|
|
// * prefix
|
|
|
|
|
// * selector
|
|
|
|
|
// * selectorWithoutVariant + selectorWithVariant
|
|
|
|
|
// * prefix + selector
|
|
|
|
|
// * prefix + selectorWithoutVariant + selectorWithVariant
|
|
|
|
|
|
|
|
|
|
$prefix = isset( $options['prefix'] ) && $options['prefix'];
|
|
|
|
|
$selector = isset( $options['selector'] ) && $options['selector'];
|
2015-06-15 06:35:58 +00:00
|
|
|
$selectorWithoutVariant = isset( $options['selectorWithoutVariant'] )
|
|
|
|
|
&& $options['selectorWithoutVariant'];
|
|
|
|
|
$selectorWithVariant = isset( $options['selectorWithVariant'] )
|
|
|
|
|
&& $options['selectorWithVariant'];
|
2015-03-23 21:04:54 +00:00
|
|
|
|
|
|
|
|
if ( $selectorWithoutVariant && !$selectorWithVariant ) {
|
2015-06-15 06:35:58 +00:00
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
"Given 'selectorWithoutVariant' but no 'selectorWithVariant'."
|
|
|
|
|
);
|
2015-03-23 21:04:54 +00:00
|
|
|
}
|
|
|
|
|
if ( $selectorWithVariant && !$selectorWithoutVariant ) {
|
2015-06-15 06:35:58 +00:00
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
"Given 'selectorWithVariant' but no 'selectorWithoutVariant'."
|
|
|
|
|
);
|
2015-03-23 21:04:54 +00:00
|
|
|
}
|
|
|
|
|
if ( $selector && $selectorWithVariant ) {
|
2015-06-15 06:35:58 +00:00
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
"Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given."
|
|
|
|
|
);
|
2015-03-23 21:04:54 +00:00
|
|
|
}
|
|
|
|
|
if ( !$prefix && !$selector && !$selectorWithVariant ) {
|
2015-06-15 06:35:58 +00:00
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
"None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given."
|
|
|
|
|
);
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $options as $member => $option ) {
|
|
|
|
|
switch ( $member ) {
|
|
|
|
|
case 'images':
|
|
|
|
|
case 'variants':
|
|
|
|
|
if ( !is_array( $option ) ) {
|
2015-03-30 16:51:10 +00:00
|
|
|
throw new InvalidArgumentException(
|
2015-03-27 17:27:47 +00:00
|
|
|
"Invalid list error. '$option' given, array expected."
|
2014-11-11 19:50:44 +00:00
|
|
|
);
|
|
|
|
|
}
|
2015-05-24 16:56:44 +00:00
|
|
|
if ( !isset( $option['default'] ) ) {
|
|
|
|
|
// Backwards compatibility
|
2016-02-17 09:09:32 +00:00
|
|
|
$option = [ 'default' => $option ];
|
2015-05-24 16:56:44 +00:00
|
|
|
}
|
2022-09-21 19:05:03 +00:00
|
|
|
foreach ( $option as $data ) {
|
2019-02-28 20:06:53 +00:00
|
|
|
if ( !is_array( $data ) ) {
|
2015-05-24 16:56:44 +00:00
|
|
|
throw new InvalidArgumentException(
|
2019-02-28 20:06:53 +00:00
|
|
|
"Invalid list error. '$data' given, array expected."
|
2015-05-24 16:56:44 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-11-11 19:50:44 +00:00
|
|
|
$this->{$member} = $option;
|
|
|
|
|
break;
|
|
|
|
|
|
2018-07-05 23:51:30 +00:00
|
|
|
case 'useDataURI':
|
|
|
|
|
$this->{$member} = (bool)$option;
|
|
|
|
|
break;
|
2018-06-25 22:55:19 +00:00
|
|
|
case 'defaultColor':
|
2014-11-11 19:50:44 +00:00
|
|
|
case 'prefix':
|
2015-03-23 21:04:54 +00:00
|
|
|
case 'selectorWithoutVariant':
|
|
|
|
|
case 'selectorWithVariant':
|
2014-11-11 19:50:44 +00:00
|
|
|
$this->{$member} = (string)$option;
|
|
|
|
|
break;
|
2015-03-23 21:04:54 +00:00
|
|
|
|
|
|
|
|
case 'selector':
|
|
|
|
|
$this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option;
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get CSS class prefix used by this module.
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function getPrefix() {
|
2015-05-23 17:06:06 +00:00
|
|
|
$this->loadFromDefinition();
|
2014-11-11 19:50:44 +00:00
|
|
|
return $this->prefix;
|
|
|
|
|
}
|
|
|
|
|
|
2015-03-23 21:04:54 +00:00
|
|
|
/**
|
|
|
|
|
* Get CSS selector templates used by this module.
|
2019-04-04 10:27:22 +00:00
|
|
|
* @return string[]
|
2015-03-23 21:04:54 +00:00
|
|
|
*/
|
|
|
|
|
public function getSelectors() {
|
2015-05-23 17:06:06 +00:00
|
|
|
$this->loadFromDefinition();
|
2016-02-17 09:09:32 +00:00
|
|
|
return [
|
2015-03-23 21:04:54 +00:00
|
|
|
'selectorWithoutVariant' => $this->selectorWithoutVariant,
|
|
|
|
|
'selectorWithVariant' => $this->selectorWithVariant,
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2015-03-23 21:04:54 +00:00
|
|
|
}
|
|
|
|
|
|
2014-11-11 19:50:44 +00:00
|
|
|
/**
|
2022-05-06 09:09:56 +00:00
|
|
|
* Get an Image object for given image.
|
2014-11-11 19:50:44 +00:00
|
|
|
* @param string $name Image name
|
2022-05-06 09:09:56 +00:00
|
|
|
* @param Context $context
|
|
|
|
|
* @return Image|null
|
2014-11-11 19:50:44 +00:00
|
|
|
*/
|
2022-05-06 09:09:56 +00:00
|
|
|
public function getImage( $name, Context $context ): ?Image {
|
2015-05-23 17:06:06 +00:00
|
|
|
$this->loadFromDefinition();
|
2015-05-24 16:56:44 +00:00
|
|
|
$images = $this->getImages( $context );
|
2017-10-06 22:17:58 +00:00
|
|
|
return $images[$name] ?? null;
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2022-05-06 09:09:56 +00:00
|
|
|
* Get Image objects for all images.
|
|
|
|
|
* @param Context $context
|
|
|
|
|
* @return Image[] Array keyed by image name
|
2014-11-11 19:50:44 +00:00
|
|
|
*/
|
2022-05-06 09:09:56 +00:00
|
|
|
public function getImages( Context $context ): array {
|
2015-05-24 16:56:44 +00:00
|
|
|
$skin = $context->getSkin();
|
2019-02-23 20:35:15 +00:00
|
|
|
if ( $this->imageObjects === null ) {
|
2015-05-23 17:06:06 +00:00
|
|
|
$this->loadFromDefinition();
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->imageObjects = [];
|
2015-05-24 16:56:44 +00:00
|
|
|
}
|
2015-06-17 20:01:00 +00:00
|
|
|
if ( !isset( $this->imageObjects[$skin] ) ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->imageObjects[$skin] = [];
|
2015-06-17 20:01:00 +00:00
|
|
|
if ( !isset( $this->images[$skin] ) ) {
|
2017-10-06 22:17:58 +00:00
|
|
|
$this->images[$skin] = $this->images['default'] ?? [];
|
2015-05-24 16:56:44 +00:00
|
|
|
}
|
2015-06-17 20:01:00 +00:00
|
|
|
foreach ( $this->images[$skin] as $name => $options ) {
|
2017-03-17 02:14:05 +00:00
|
|
|
$fileDescriptor = is_array( $options ) ? $options['file'] : $options;
|
2014-11-11 19:50:44 +00:00
|
|
|
|
2015-03-27 17:27:47 +00:00
|
|
|
$allowedVariants = array_merge(
|
2017-10-06 22:38:36 +00:00
|
|
|
( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
|
2015-05-24 16:56:44 +00:00
|
|
|
$this->getGlobalVariants( $context )
|
2015-03-27 17:27:47 +00:00
|
|
|
);
|
2015-06-17 20:01:00 +00:00
|
|
|
if ( isset( $this->variants[$skin] ) ) {
|
2015-03-27 17:27:47 +00:00
|
|
|
$variantConfig = array_intersect_key(
|
2015-06-17 20:01:00 +00:00
|
|
|
$this->variants[$skin],
|
2015-03-27 17:27:47 +00:00
|
|
|
array_fill_keys( $allowedVariants, true )
|
2015-03-15 01:52:45 +00:00
|
|
|
);
|
2015-03-27 17:27:47 +00:00
|
|
|
} else {
|
2016-02-17 09:09:32 +00:00
|
|
|
$variantConfig = [];
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
2015-03-27 17:27:47 +00:00
|
|
|
|
2022-05-06 09:09:56 +00:00
|
|
|
$image = new Image(
|
2015-03-27 17:27:47 +00:00
|
|
|
$name,
|
|
|
|
|
$this->getName(),
|
2015-03-28 00:44:47 +00:00
|
|
|
$fileDescriptor,
|
2015-03-27 17:27:47 +00:00
|
|
|
$this->localBasePath,
|
2018-06-25 22:55:19 +00:00
|
|
|
$variantConfig,
|
|
|
|
|
$this->defaultColor
|
2015-03-27 17:27:47 +00:00
|
|
|
);
|
2015-06-17 20:01:00 +00:00
|
|
|
$this->imageObjects[$skin][$image->getName()] = $image;
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-17 20:01:00 +00:00
|
|
|
return $this->imageObjects[$skin];
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2015-03-27 17:27:47 +00:00
|
|
|
* Get list of variants in this module that are 'global', i.e., available
|
|
|
|
|
* for every image regardless of image options.
|
2022-05-06 09:09:56 +00:00
|
|
|
* @param Context $context
|
2014-11-11 19:50:44 +00:00
|
|
|
* @return string[]
|
|
|
|
|
*/
|
2022-05-06 09:09:56 +00:00
|
|
|
public function getGlobalVariants( Context $context ): array {
|
2015-05-24 16:56:44 +00:00
|
|
|
$skin = $context->getSkin();
|
2019-02-23 20:35:15 +00:00
|
|
|
if ( $this->globalVariants === null ) {
|
2015-05-23 17:06:06 +00:00
|
|
|
$this->loadFromDefinition();
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->globalVariants = [];
|
2015-05-24 16:56:44 +00:00
|
|
|
}
|
2015-06-17 20:01:00 +00:00
|
|
|
if ( !isset( $this->globalVariants[$skin] ) ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->globalVariants[$skin] = [];
|
2015-06-17 20:01:00 +00:00
|
|
|
if ( !isset( $this->variants[$skin] ) ) {
|
2017-10-06 22:17:58 +00:00
|
|
|
$this->variants[$skin] = $this->variants['default'] ?? [];
|
2015-05-24 16:56:44 +00:00
|
|
|
}
|
2015-06-17 20:01:00 +00:00
|
|
|
foreach ( $this->variants[$skin] as $name => $config ) {
|
2020-06-14 18:40:02 +00:00
|
|
|
if ( $config['global'] ?? false ) {
|
2015-06-17 20:01:00 +00:00
|
|
|
$this->globalVariants[$skin][] = $name;
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-17 20:01:00 +00:00
|
|
|
return $this->globalVariants[$skin];
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2022-05-06 09:09:56 +00:00
|
|
|
* @param Context $context
|
2014-11-11 19:50:44 +00:00
|
|
|
* @return array
|
|
|
|
|
*/
|
2022-05-06 09:09:56 +00:00
|
|
|
public function getStyles( Context $context ): array {
|
2015-05-23 17:06:06 +00:00
|
|
|
$this->loadFromDefinition();
|
2015-04-07 16:02:46 +00:00
|
|
|
|
2014-11-11 19:50:44 +00:00
|
|
|
// Build CSS rules
|
2016-02-17 09:09:32 +00:00
|
|
|
$rules = [];
|
2024-09-27 17:43:25 +00:00
|
|
|
|
|
|
|
|
$sources = $oldSources = $context->getResourceLoader()->getSources();
|
|
|
|
|
$this->getHookRunner()->onResourceLoaderModifyEmbeddedSourceUrls( $sources );
|
|
|
|
|
if ( array_keys( $sources ) !== array_keys( $oldSources ) ) {
|
|
|
|
|
throw new DomainException( 'ResourceLoaderModifyEmbeddedSourceUrls hook must not add or remove sources' );
|
|
|
|
|
}
|
|
|
|
|
$script = $sources[ $this->getSource() ];
|
|
|
|
|
|
2015-03-23 21:04:54 +00:00
|
|
|
$selectors = $this->getSelectors();
|
|
|
|
|
|
2015-05-24 16:56:44 +00:00
|
|
|
foreach ( $this->getImages( $context ) as $name => $image ) {
|
2017-03-15 19:09:17 +00:00
|
|
|
$declarations = $this->getStyleDeclarations( $context, $image, $script );
|
2015-03-23 21:04:54 +00:00
|
|
|
$selector = strtr(
|
|
|
|
|
$selectors['selectorWithoutVariant'],
|
2016-02-17 09:09:32 +00:00
|
|
|
[
|
2015-03-23 21:04:54 +00:00
|
|
|
'{prefix}' => $this->getPrefix(),
|
|
|
|
|
'{name}' => $name,
|
|
|
|
|
'{variant}' => '',
|
2016-02-17 09:09:32 +00:00
|
|
|
]
|
2015-03-23 21:04:54 +00:00
|
|
|
);
|
|
|
|
|
$rules[] = "$selector {\n\t$declarations\n}";
|
2014-11-11 19:50:44 +00:00
|
|
|
|
|
|
|
|
foreach ( $image->getVariants() as $variant ) {
|
2017-03-15 19:09:17 +00:00
|
|
|
$declarations = $this->getStyleDeclarations( $context, $image, $script, $variant );
|
2015-03-23 21:04:54 +00:00
|
|
|
$selector = strtr(
|
|
|
|
|
$selectors['selectorWithVariant'],
|
2016-02-17 09:09:32 +00:00
|
|
|
[
|
2015-03-23 21:04:54 +00:00
|
|
|
'{prefix}' => $this->getPrefix(),
|
|
|
|
|
'{name}' => $name,
|
|
|
|
|
'{variant}' => $variant,
|
2016-02-17 09:09:32 +00:00
|
|
|
]
|
2015-03-23 21:04:54 +00:00
|
|
|
);
|
|
|
|
|
$rules[] = "$selector {\n\t$declarations\n}";
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$style = implode( "\n", $rules );
|
2024-06-04 22:00:29 +00:00
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
return [ 'all' => $style ];
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
|
|
|
|
|
2017-03-15 19:09:17 +00:00
|
|
|
/**
|
resourceloader: Add version to ResourceLoaderImage urls for long-cache
The code previously here did not work well as it merely forwarded the
hash from the current web request. This had numerous issues:
1. It was often null because requests for stylesheets do not cary
a version hash.
2. When requested by JavaScript, the version hash would be a
combination-hash of many unrelated modules, thus when requested as
part of different batches, it would produce different urls which
is not ideal.
The impact of this is minimal currently because we basically never use
these urls, as SVGs are almost always embedded instead of ref'ed by url.
PNG urls are only generated for non-JS modules and then only used in older
browsers not supporting SVG. And, even after all that, for the edge case
of an SVG being ref'ed by url, they would be stored in LocalStorage by
mw.loader with the name+version of the module the image belonged to, not
the version hash of the batch request it came with.
Which means that, yes, localstorage key for "somemodule+someversion" would
have different values for different users, based on which batch the value
came with, because the image urls were using the version hash of the batch
request from ResourceLoaderContext. This is weird, but didn't cause bugs
or inefficiencies because the user would never be exposed to the other
possible urls for that image because we always check LocalStorage first.
It did cause fragmentation server-side in Varnish, though.
This is all fixed now by always including a version, and setting it to
the version of the module. This means there is no more Varnish fragmentation
for these. And it means that browsers are now allowed to cache the images
served from these urls for 30+ days (immutable) instead of only 5min,
which is what happened when they didn't have a version parameter (or set to
null).
Bug: T233343
Change-Id: I4af7fda03698ed4c288d154e7787fb2f3cbbe6c5
2019-09-26 16:26:52 +00:00
|
|
|
* This method must not be used by getDefinitionSummary as doing so would cause
|
2023-03-27 23:21:06 +00:00
|
|
|
* an infinite loop (we use Image::getUrl below which calls
|
resourceloader: Add version to ResourceLoaderImage urls for long-cache
The code previously here did not work well as it merely forwarded the
hash from the current web request. This had numerous issues:
1. It was often null because requests for stylesheets do not cary
a version hash.
2. When requested by JavaScript, the version hash would be a
combination-hash of many unrelated modules, thus when requested as
part of different batches, it would produce different urls which
is not ideal.
The impact of this is minimal currently because we basically never use
these urls, as SVGs are almost always embedded instead of ref'ed by url.
PNG urls are only generated for non-JS modules and then only used in older
browsers not supporting SVG. And, even after all that, for the edge case
of an SVG being ref'ed by url, they would be stored in LocalStorage by
mw.loader with the name+version of the module the image belonged to, not
the version hash of the batch request it came with.
Which means that, yes, localstorage key for "somemodule+someversion" would
have different values for different users, based on which batch the value
came with, because the image urls were using the version hash of the batch
request from ResourceLoaderContext. This is weird, but didn't cause bugs
or inefficiencies because the user would never be exposed to the other
possible urls for that image because we always check LocalStorage first.
It did cause fragmentation server-side in Varnish, though.
This is all fixed now by always including a version, and setting it to
the version of the module. This means there is no more Varnish fragmentation
for these. And it means that browsers are now allowed to cache the images
served from these urls for 30+ days (immutable) instead of only 5min,
which is what happened when they didn't have a version parameter (or set to
null).
Bug: T233343
Change-Id: I4af7fda03698ed4c288d154e7787fb2f3cbbe6c5
2019-09-26 16:26:52 +00:00
|
|
|
* Module:getVersionHash, which calls Module::getDefinitionSummary).
|
|
|
|
|
*
|
2022-05-06 09:09:56 +00:00
|
|
|
* @param Context $context
|
|
|
|
|
* @param Image $image Image to get the style for
|
2017-03-15 19:09:17 +00:00
|
|
|
* @param string $script URL to load.php
|
|
|
|
|
* @param string|null $variant Variant to get the style for
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function getStyleDeclarations(
|
2022-05-06 09:09:56 +00:00
|
|
|
Context $context,
|
|
|
|
|
Image $image,
|
2017-03-15 19:09:17 +00:00
|
|
|
$script,
|
|
|
|
|
$variant = null
|
|
|
|
|
) {
|
2018-07-05 23:51:30 +00:00
|
|
|
$imageDataUri = $this->useDataURI ? $image->getDataUri( $context, $variant, 'original' ) : false;
|
2017-03-15 19:09:17 +00:00
|
|
|
$primaryUrl = $imageDataUri ?: $image->getUrl( $context, $script, $variant, 'original' );
|
|
|
|
|
$declarations = $this->getCssDeclarations(
|
2023-07-13 01:39:09 +00:00
|
|
|
$primaryUrl
|
2017-03-15 19:09:17 +00:00
|
|
|
);
|
|
|
|
|
return implode( "\n\t", $declarations );
|
|
|
|
|
}
|
|
|
|
|
|
2014-11-11 19:50:44 +00:00
|
|
|
/**
|
2023-07-13 14:28:10 +00:00
|
|
|
* Format the CSS declaration for the image as a background-image property.
|
2015-03-23 20:55:12 +00:00
|
|
|
*
|
2014-11-11 19:50:44 +00:00
|
|
|
* @param string $primary Primary URI
|
2023-07-13 01:39:09 +00:00
|
|
|
* @return string[] CSS declarations
|
2014-11-11 19:50:44 +00:00
|
|
|
*/
|
2023-07-13 01:39:09 +00:00
|
|
|
protected function getCssDeclarations( $primary ): array {
|
2017-09-13 19:26:05 +00:00
|
|
|
$primaryUrl = CSSMin::buildUrlValue( $primary );
|
2024-06-04 22:00:29 +00:00
|
|
|
if ( $this->supportsMaskImage() ) {
|
|
|
|
|
return [
|
2024-09-05 23:46:04 +00:00
|
|
|
"-webkit-mask-image: $primaryUrl;",
|
2024-06-04 22:00:29 +00:00
|
|
|
"mask-image: $primaryUrl;",
|
|
|
|
|
];
|
|
|
|
|
}
|
2016-02-17 09:09:32 +00:00
|
|
|
return [
|
2023-02-07 23:31:01 +00:00
|
|
|
"background-image: $primaryUrl;",
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
|
|
|
|
|
2024-06-04 22:00:29 +00:00
|
|
|
/**
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function supportsMaskImage() {
|
|
|
|
|
return $this->useMaskImage;
|
|
|
|
|
}
|
|
|
|
|
|
2014-11-11 19:50:44 +00:00
|
|
|
/**
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function supportsURLLoading() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-14 19:31:19 +00:00
|
|
|
/**
|
|
|
|
|
* Get the definition summary for this module.
|
|
|
|
|
*
|
2022-05-06 09:09:56 +00:00
|
|
|
* @param Context $context
|
2015-04-14 19:31:19 +00:00
|
|
|
* @return array
|
|
|
|
|
*/
|
2022-05-06 09:09:56 +00:00
|
|
|
public function getDefinitionSummary( Context $context ) {
|
2015-05-23 17:06:06 +00:00
|
|
|
$this->loadFromDefinition();
|
2015-04-14 19:31:19 +00:00
|
|
|
$summary = parent::getDefinitionSummary( $context );
|
2016-08-17 18:57:57 +00:00
|
|
|
|
|
|
|
|
$options = [];
|
2016-02-17 09:09:32 +00:00
|
|
|
foreach ( [
|
2015-04-14 19:31:19 +00:00
|
|
|
'localBasePath',
|
|
|
|
|
'images',
|
|
|
|
|
'variants',
|
|
|
|
|
'prefix',
|
|
|
|
|
'selectorWithoutVariant',
|
|
|
|
|
'selectorWithVariant',
|
2016-02-17 09:09:32 +00:00
|
|
|
] as $member ) {
|
2016-08-17 18:57:57 +00:00
|
|
|
$options[$member] = $this->{$member};
|
2019-04-04 10:27:22 +00:00
|
|
|
}
|
2016-08-17 18:57:57 +00:00
|
|
|
|
|
|
|
|
$summary[] = [
|
|
|
|
|
'options' => $options,
|
|
|
|
|
'fileHashes' => $this->getFileHashes( $context ),
|
|
|
|
|
];
|
2015-04-14 19:31:19 +00:00
|
|
|
return $summary;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-08-17 18:57:57 +00:00
|
|
|
* Helper method for getDefinitionSummary.
|
2022-05-06 09:09:56 +00:00
|
|
|
* @param Context $context
|
2017-09-09 20:47:04 +00:00
|
|
|
* @return array
|
2015-04-14 19:31:19 +00:00
|
|
|
*/
|
2022-05-06 09:09:56 +00:00
|
|
|
private function getFileHashes( Context $context ) {
|
2015-05-23 17:06:06 +00:00
|
|
|
$this->loadFromDefinition();
|
2016-02-17 09:09:32 +00:00
|
|
|
$files = [];
|
2022-09-21 19:05:03 +00:00
|
|
|
foreach ( $this->getImages( $context ) as $image ) {
|
2015-04-14 19:31:19 +00:00
|
|
|
$files[] = $image->getPath( $context );
|
|
|
|
|
}
|
|
|
|
|
$files = array_values( array_unique( $files ) );
|
2016-08-17 18:57:57 +00:00
|
|
|
return array_map( [ __CLASS__, 'safeFileHash' ], $files );
|
2015-04-14 19:31:19 +00:00
|
|
|
}
|
|
|
|
|
|
2017-03-17 02:14:05 +00:00
|
|
|
/**
|
2022-05-06 09:09:56 +00:00
|
|
|
* @param string|FilePath $path
|
2017-03-17 02:14:05 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function getLocalPath( $path ) {
|
2022-05-06 09:09:56 +00:00
|
|
|
if ( $path instanceof FilePath ) {
|
2017-03-17 02:14:05 +00:00
|
|
|
return $path->getLocalPath();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "{$this->localBasePath}/$path";
|
|
|
|
|
}
|
|
|
|
|
|
2014-11-11 19:50:44 +00:00
|
|
|
/**
|
|
|
|
|
* Extract a local base path from module definition information.
|
|
|
|
|
*
|
|
|
|
|
* @param array $options Module definition
|
2018-06-26 21:14:43 +00:00
|
|
|
* @param string|null $localBasePath Path to use if not provided in module definition. Defaults
|
2020-06-14 18:40:02 +00:00
|
|
|
* to $IP.
|
2014-11-11 19:50:44 +00:00
|
|
|
* @return string Local base path
|
|
|
|
|
*/
|
2019-10-05 07:38:41 +00:00
|
|
|
public static function extractLocalBasePath( array $options, $localBasePath = null ) {
|
2014-11-11 19:50:44 +00:00
|
|
|
global $IP;
|
|
|
|
|
|
|
|
|
|
if ( array_key_exists( 'localBasePath', $options ) ) {
|
|
|
|
|
$localBasePath = (string)$options['localBasePath'];
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-05 20:37:13 +00:00
|
|
|
return $localBasePath ?? $IP;
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|
2015-05-25 14:04:57 +00:00
|
|
|
|
2016-08-18 19:00:51 +00:00
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function getType() {
|
|
|
|
|
return self::LOAD_STYLES;
|
|
|
|
|
}
|
2014-11-11 19:50:44 +00:00
|
|
|
}
|