2017-02-20 04:33:24 +00:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @file
|
2018-03-02 23:43:40 +00:00
|
|
|
|
* @license GPL-2.0-or-later
|
2017-02-20 04:33:24 +00:00
|
|
|
|
*/
|
|
|
|
|
|
2021-10-27 12:44:52 +00:00
|
|
|
|
use MediaWiki\MediaWikiServices;
|
2020-06-03 03:25:07 +00:00
|
|
|
|
use MediaWiki\Revision\SlotRecord;
|
2018-08-24 19:07:10 +00:00
|
|
|
|
use Wikimedia\CSS\Grammar\CheckedMatcher;
|
2021-01-08 03:11:35 +00:00
|
|
|
|
use Wikimedia\CSS\Grammar\GrammarMatch;
|
2018-10-17 17:28:43 +00:00
|
|
|
|
use Wikimedia\CSS\Grammar\MatcherFactory;
|
2020-10-19 08:23:39 +00:00
|
|
|
|
use Wikimedia\CSS\Objects\ComponentValue;
|
2018-08-24 19:07:10 +00:00
|
|
|
|
use Wikimedia\CSS\Objects\ComponentValueList;
|
2017-02-20 04:33:24 +00:00
|
|
|
|
use Wikimedia\CSS\Objects\Token;
|
2018-10-17 17:28:43 +00:00
|
|
|
|
use Wikimedia\CSS\Parser\Parser as CSSParser;
|
2017-02-20 04:33:24 +00:00
|
|
|
|
use Wikimedia\CSS\Sanitizer\FontFeatureValuesAtRuleSanitizer;
|
|
|
|
|
use Wikimedia\CSS\Sanitizer\KeyframesAtRuleSanitizer;
|
|
|
|
|
use Wikimedia\CSS\Sanitizer\MediaAtRuleSanitizer;
|
|
|
|
|
use Wikimedia\CSS\Sanitizer\NamespaceAtRuleSanitizer;
|
|
|
|
|
use Wikimedia\CSS\Sanitizer\PageAtRuleSanitizer;
|
|
|
|
|
use Wikimedia\CSS\Sanitizer\Sanitizer;
|
|
|
|
|
use Wikimedia\CSS\Sanitizer\StylePropertySanitizer;
|
|
|
|
|
use Wikimedia\CSS\Sanitizer\StyleRuleSanitizer;
|
|
|
|
|
use Wikimedia\CSS\Sanitizer\StylesheetSanitizer;
|
|
|
|
|
use Wikimedia\CSS\Sanitizer\SupportsAtRuleSanitizer;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* TemplateStyles extension hooks
|
|
|
|
|
*/
|
|
|
|
|
class TemplateStylesHooks {
|
|
|
|
|
|
2018-10-17 17:28:43 +00:00
|
|
|
|
/** @var MatcherFactory|null */
|
|
|
|
|
private static $matcherFactory = null;
|
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
/** @var Sanitizer[] */
|
|
|
|
|
private static $sanitizers = [];
|
|
|
|
|
|
2019-12-09 18:42:00 +00:00
|
|
|
|
/** @var (false|Token[])[] */
|
2018-10-17 17:28:43 +00:00
|
|
|
|
private static $wrappers = [];
|
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
/**
|
|
|
|
|
* @return Config
|
2018-02-06 02:55:05 +00:00
|
|
|
|
* @codeCoverageIgnore
|
2017-02-20 04:33:24 +00:00
|
|
|
|
*/
|
|
|
|
|
public static function getConfig() {
|
2020-11-11 15:42:40 +00:00
|
|
|
|
return \MediaWiki\MediaWikiServices::getInstance()->getConfigFactory()
|
|
|
|
|
->makeConfig( 'templatestyles' );
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-17 17:28:43 +00:00
|
|
|
|
/**
|
|
|
|
|
* @return MatcherFactory
|
|
|
|
|
* @codeCoverageIgnore
|
|
|
|
|
*/
|
|
|
|
|
private static function getMatcherFactory() {
|
|
|
|
|
if ( !self::$matcherFactory ) {
|
|
|
|
|
self::$matcherFactory = new TemplateStylesMatcherFactory(
|
2020-11-11 15:42:40 +00:00
|
|
|
|
self::getConfig()->get( 'TemplateStylesAllowedUrls' )
|
2018-10-17 17:28:43 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return self::$matcherFactory;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate an extra wrapper-selector
|
|
|
|
|
* @param string $wrapper
|
2020-10-19 08:23:39 +00:00
|
|
|
|
* @return ComponentValue[]|false Representation of the selector, or false on failure
|
2018-10-17 17:28:43 +00:00
|
|
|
|
*/
|
|
|
|
|
private static function validateExtraWrapper( $wrapper ) {
|
|
|
|
|
if ( !isset( self::$wrappers[$wrapper] ) ) {
|
|
|
|
|
$cssParser = CSSParser::newFromString( $wrapper );
|
|
|
|
|
$components = $cssParser->parseComponentValueList();
|
|
|
|
|
if ( $cssParser->getParseErrors() ) {
|
|
|
|
|
$match = false;
|
|
|
|
|
} else {
|
|
|
|
|
$match = self::getMatcherFactory()->cssSimpleSelectorSeq()
|
2021-01-08 03:11:35 +00:00
|
|
|
|
->matchAgainst( $components, [ 'mark-significance' => true ] );
|
2018-10-17 17:28:43 +00:00
|
|
|
|
}
|
2020-09-15 17:34:01 +00:00
|
|
|
|
self::$wrappers[$wrapper] = $match ? $components->toComponentValueArray() : false;
|
2018-10-17 17:28:43 +00:00
|
|
|
|
}
|
|
|
|
|
return self::$wrappers[$wrapper];
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
/**
|
|
|
|
|
* @param string $class Class to limit selectors to
|
2018-10-17 17:28:43 +00:00
|
|
|
|
* @param string|null $extraWrapper Extra selector to limit selectors to
|
2017-02-20 04:33:24 +00:00
|
|
|
|
* @return Sanitizer
|
|
|
|
|
*/
|
2018-10-17 17:28:43 +00:00
|
|
|
|
public static function getSanitizer( $class, $extraWrapper = null ) {
|
|
|
|
|
$key = $extraWrapper !== null ? "$class $extraWrapper" : $class;
|
|
|
|
|
if ( !isset( self::$sanitizers[$key] ) ) {
|
2017-07-23 07:39:22 +00:00
|
|
|
|
$config = self::getConfig();
|
2018-10-17 17:28:43 +00:00
|
|
|
|
$matcherFactory = self::getMatcherFactory();
|
2017-02-20 04:33:24 +00:00
|
|
|
|
|
|
|
|
|
$propertySanitizer = new StylePropertySanitizer( $matcherFactory );
|
|
|
|
|
$propertySanitizer->setKnownProperties( array_diff_key(
|
|
|
|
|
$propertySanitizer->getKnownProperties(),
|
2021-04-19 00:02:32 +00:00
|
|
|
|
array_flip( $config->get( 'TemplateStylesDisallowedProperties' ) )
|
2017-02-20 04:33:24 +00:00
|
|
|
|
) );
|
|
|
|
|
Hooks::run( 'TemplateStylesPropertySanitizer', [ &$propertySanitizer, $matcherFactory ] );
|
|
|
|
|
|
2018-08-24 19:07:10 +00:00
|
|
|
|
$htmlOrBodySimpleSelectorSeqMatcher = new CheckedMatcher(
|
|
|
|
|
$matcherFactory->cssSimpleSelectorSeq(),
|
2021-05-03 10:21:33 +00:00
|
|
|
|
static function ( ComponentValueList $values, GrammarMatch $match, array $options ) {
|
2018-08-24 19:07:10 +00:00
|
|
|
|
foreach ( $match->getCapturedMatches() as $m ) {
|
|
|
|
|
if ( $m->getName() !== 'element' ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$str = (string)$m;
|
|
|
|
|
return $str === 'html' || $str === 'body';
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2018-10-17 17:28:43 +00:00
|
|
|
|
$prependSelectors = [
|
|
|
|
|
new Token( Token::T_DELIM, '.' ),
|
|
|
|
|
new Token( Token::T_IDENT, $class ),
|
|
|
|
|
];
|
|
|
|
|
if ( $extraWrapper !== null ) {
|
2020-10-19 08:23:39 +00:00
|
|
|
|
$extraComponentValues = self::validateExtraWrapper( $extraWrapper );
|
|
|
|
|
if ( !$extraComponentValues ) {
|
2018-10-17 17:28:43 +00:00
|
|
|
|
throw new InvalidArgumentException( "Invalid value for \$extraWrapper: $extraWrapper" );
|
|
|
|
|
}
|
|
|
|
|
$prependSelectors = array_merge(
|
|
|
|
|
$prependSelectors,
|
|
|
|
|
[ new Token( Token::T_WHITESPACE, [ 'significant' => true ] ) ],
|
2020-10-19 08:23:39 +00:00
|
|
|
|
$extraComponentValues
|
2018-10-17 17:28:43 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-20 05:12:36 +00:00
|
|
|
|
$disallowedAtRules = $config->get( 'TemplateStylesDisallowedAtRules' );
|
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
$ruleSanitizers = [
|
|
|
|
|
'styles' => new StyleRuleSanitizer(
|
|
|
|
|
$matcherFactory->cssSelectorList(),
|
|
|
|
|
$propertySanitizer,
|
|
|
|
|
[
|
2018-10-17 17:28:43 +00:00
|
|
|
|
'prependSelectors' => $prependSelectors,
|
2018-08-24 19:07:10 +00:00
|
|
|
|
'hoistableComponentMatcher' => $htmlOrBodySimpleSelectorSeqMatcher,
|
2017-02-20 04:33:24 +00:00
|
|
|
|
]
|
|
|
|
|
),
|
|
|
|
|
'@font-face' => new TemplateStylesFontFaceAtRuleSanitizer( $matcherFactory ),
|
|
|
|
|
'@font-feature-values' => new FontFeatureValuesAtRuleSanitizer( $matcherFactory ),
|
|
|
|
|
'@keyframes' => new KeyframesAtRuleSanitizer( $matcherFactory, $propertySanitizer ),
|
|
|
|
|
'@page' => new PageAtRuleSanitizer( $matcherFactory, $propertySanitizer ),
|
|
|
|
|
'@media' => new MediaAtRuleSanitizer( $matcherFactory->cssMediaQueryList() ),
|
|
|
|
|
'@supports' => new SupportsAtRuleSanitizer( $matcherFactory, [
|
|
|
|
|
'declarationSanitizer' => $propertySanitizer,
|
|
|
|
|
] ),
|
|
|
|
|
];
|
2021-03-20 05:12:36 +00:00
|
|
|
|
$ruleSanitizers = array_diff_key( $ruleSanitizers, array_flip( $disallowedAtRules ) );
|
|
|
|
|
if ( isset( $ruleSanitizers['@media'] ) ) {
|
|
|
|
|
// In case @media was disallowed
|
2017-02-20 04:33:24 +00:00
|
|
|
|
$ruleSanitizers['@media']->setRuleSanitizers( $ruleSanitizers );
|
|
|
|
|
}
|
2021-03-20 05:12:36 +00:00
|
|
|
|
if ( isset( $ruleSanitizers['@supports'] ) ) {
|
|
|
|
|
// In case @supports was disallowed
|
2017-02-20 04:33:24 +00:00
|
|
|
|
$ruleSanitizers['@supports']->setRuleSanitizers( $ruleSanitizers );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$allRuleSanitizers = $ruleSanitizers + [
|
|
|
|
|
// Omit @import, it's not secure. Maybe someday we'll make an "@-mw-import" or something.
|
|
|
|
|
'@namespace' => new NamespaceAtRuleSanitizer( $matcherFactory ),
|
|
|
|
|
];
|
2021-03-20 05:12:36 +00:00
|
|
|
|
$allRuleSanitizers = array_diff_key( $allRuleSanitizers, $disallowedAtRules );
|
2017-02-20 04:33:24 +00:00
|
|
|
|
$sanitizer = new StylesheetSanitizer( $allRuleSanitizers );
|
|
|
|
|
Hooks::run( 'TemplateStylesStylesheetSanitizer',
|
|
|
|
|
[ &$sanitizer, $propertySanitizer, $matcherFactory ]
|
|
|
|
|
);
|
2018-10-17 17:28:43 +00:00
|
|
|
|
self::$sanitizers[$key] = $sanitizer;
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|
2018-10-17 17:28:43 +00:00
|
|
|
|
return self::$sanitizers[$key];
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update $wgTextModelsToParse
|
|
|
|
|
*/
|
|
|
|
|
public static function onRegistration() {
|
|
|
|
|
// This gets called before ConfigFactory is set up, so I guess we need
|
|
|
|
|
// to use globals.
|
|
|
|
|
global $wgTextModelsToParse, $wgTemplateStylesAutoParseContent;
|
|
|
|
|
|
|
|
|
|
if ( in_array( CONTENT_MODEL_CSS, $wgTextModelsToParse, true ) &&
|
|
|
|
|
$wgTemplateStylesAutoParseContent
|
|
|
|
|
) {
|
|
|
|
|
$wgTextModelsToParse[] = 'sanitized-css';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add `<templatestyles>` to the parser.
|
2019-11-15 05:37:54 +00:00
|
|
|
|
* @param Parser $parser Parser object being cleared
|
2021-10-03 20:27:36 +00:00
|
|
|
|
* @suppress PhanUndeclaredProperty
|
2017-02-20 04:33:24 +00:00
|
|
|
|
*/
|
2019-11-15 05:37:54 +00:00
|
|
|
|
public static function onParserFirstCallInit( Parser $parser ) {
|
2020-11-11 15:42:40 +00:00
|
|
|
|
$parser->setHook( 'templatestyles', [ __CLASS__, 'handleTag' ] );
|
2017-11-30 18:52:30 +00:00
|
|
|
|
$parser->extTemplateStylesCache = new MapCacheLRU( 100 ); // 100 is arbitrary
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the default content model to 'sanitized-css' when appropriate.
|
|
|
|
|
* @param Title $title the Title in question
|
|
|
|
|
* @param string &$model The model name
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public static function onContentHandlerDefaultModelFor( $title, &$model ) {
|
2018-10-18 13:32:49 +00:00
|
|
|
|
// Allow overwriting attributes with config settings.
|
|
|
|
|
// Attributes can not use namespaces as keys, as processing them does not preserve
|
|
|
|
|
// integer keys.
|
|
|
|
|
$enabledNamespaces = self::getConfig()->get( 'TemplateStylesNamespaces' ) +
|
|
|
|
|
array_fill_keys(
|
|
|
|
|
ExtensionRegistry::getInstance()->getAttribute( 'TemplateStylesNamespaces' ),
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
if ( !empty( $enabledNamespaces[$title->getNamespace()] ) &&
|
|
|
|
|
$title->isSubpage() && substr( $title->getText(), -4 ) === '.css'
|
|
|
|
|
) {
|
|
|
|
|
$model = 'sanitized-css';
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Edit our CSS content model like core's CSS
|
|
|
|
|
* @param Title $title Title being edited
|
|
|
|
|
* @param string &$lang CodeEditor language to use
|
|
|
|
|
* @param string $model Content model
|
|
|
|
|
* @param string $format Content format
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public static function onCodeEditorGetPageLanguage( $title, &$lang, $model, $format ) {
|
|
|
|
|
if ( $model === 'sanitized-css' && self::getConfig()->get( 'TemplateStylesUseCodeEditor' ) ) {
|
|
|
|
|
$lang = 'css';
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-30 18:52:30 +00:00
|
|
|
|
/**
|
|
|
|
|
* Clear our cache when the parser is reset
|
|
|
|
|
* @param Parser $parser
|
2021-10-03 20:27:36 +00:00
|
|
|
|
* @suppress PhanUndeclaredProperty
|
2017-11-30 18:52:30 +00:00
|
|
|
|
*/
|
|
|
|
|
public static function onParserClearState( Parser $parser ) {
|
|
|
|
|
$parser->extTemplateStylesCache->clear();
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
/**
|
|
|
|
|
* Parser hook for `<templatestyles>`
|
|
|
|
|
* @param string $text Contents of the tag (ignored).
|
2020-11-11 15:42:40 +00:00
|
|
|
|
* @param string[] $params Tag attributes
|
2017-02-20 04:33:24 +00:00
|
|
|
|
* @param Parser $parser
|
|
|
|
|
* @param PPFrame $frame
|
|
|
|
|
* @return string HTML
|
2021-10-03 20:27:36 +00:00
|
|
|
|
* @suppress SecurityCheck-XSS,PhanUndeclaredProperty
|
2017-02-20 04:33:24 +00:00
|
|
|
|
*/
|
|
|
|
|
public static function handleTag( $text, $params, $parser, $frame ) {
|
2021-09-03 00:55:55 +00:00
|
|
|
|
$config = self::getConfig();
|
|
|
|
|
if ( $config->get( 'TemplateStylesDisable' ) ) {
|
2018-01-17 01:30:46 +00:00
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
if ( !isset( $params['src'] ) || trim( $params['src'] ) === '' ) {
|
2018-05-26 21:45:34 +00:00
|
|
|
|
return self::formatTagError( $parser, [ 'templatestyles-missing-src' ] );
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-17 17:28:43 +00:00
|
|
|
|
$extraWrapper = null;
|
|
|
|
|
if ( isset( $params['wrapper'] ) && trim( $params['wrapper'] ) !== '' ) {
|
|
|
|
|
$extraWrapper = trim( $params['wrapper'] );
|
|
|
|
|
if ( !self::validateExtraWrapper( $extraWrapper ) ) {
|
|
|
|
|
return self::formatTagError( $parser, [ 'templatestyles-invalid-wrapper' ] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
// Default to the Template namespace because that's the most likely
|
|
|
|
|
// situation. We can't allow for subpage syntax like src="/styles.css"
|
|
|
|
|
// or the like, though, because stuff like substing and Parsoid would
|
|
|
|
|
// wind up wanting to make that relative to the wrong page.
|
2021-09-03 00:55:55 +00:00
|
|
|
|
$title = Title::newFromText( $params['src'], $config->get( 'TemplateStylesDefaultNamespace' ) );
|
2017-02-20 04:33:24 +00:00
|
|
|
|
if ( !$title ) {
|
2018-05-26 21:45:34 +00:00
|
|
|
|
return self::formatTagError( $parser, [ 'templatestyles-invalid-src' ] );
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-06-03 03:25:07 +00:00
|
|
|
|
$revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
|
2017-02-20 04:33:24 +00:00
|
|
|
|
|
|
|
|
|
// It's not really a "template", but it has the same implications
|
|
|
|
|
// for needing reparse when the stylesheet is edited.
|
2020-06-03 03:25:07 +00:00
|
|
|
|
$parser->getOutput()->addTemplate(
|
|
|
|
|
$title,
|
|
|
|
|
$title->getArticleId(),
|
|
|
|
|
$revRecord ? $revRecord->getId() : null
|
|
|
|
|
);
|
2017-02-20 04:33:24 +00:00
|
|
|
|
|
2020-06-03 03:25:07 +00:00
|
|
|
|
$content = $revRecord ? $revRecord->getContent( SlotRecord::MAIN ) : null;
|
2017-02-20 04:33:24 +00:00
|
|
|
|
if ( !$content ) {
|
2018-08-10 22:16:25 +00:00
|
|
|
|
$titleText = $title->getPrefixedText();
|
2018-05-26 21:45:34 +00:00
|
|
|
|
return self::formatTagError( $parser, [
|
|
|
|
|
'templatestyles-bad-src-missing',
|
2018-08-10 22:16:25 +00:00
|
|
|
|
$titleText,
|
|
|
|
|
wfEscapeWikiText( $titleText )
|
2018-05-26 21:45:34 +00:00
|
|
|
|
] );
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|
|
|
|
|
if ( !$content instanceof TemplateStylesContent ) {
|
2018-08-10 22:16:25 +00:00
|
|
|
|
$titleText = $title->getPrefixedText();
|
2018-05-26 21:45:34 +00:00
|
|
|
|
return self::formatTagError( $parser, [
|
|
|
|
|
'templatestyles-bad-src',
|
2018-08-10 22:16:25 +00:00
|
|
|
|
$titleText,
|
|
|
|
|
wfEscapeWikiText( $titleText ),
|
2018-05-26 21:45:34 +00:00
|
|
|
|
ContentHandler::getLocalizedName( $content->getModel() )
|
|
|
|
|
] );
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-30 18:52:30 +00:00
|
|
|
|
// If the revision actually has an ID, cache based on that.
|
|
|
|
|
// Otherwise, cache by hash.
|
2020-06-03 03:25:07 +00:00
|
|
|
|
if ( $revRecord->getId() ) {
|
|
|
|
|
$cacheKey = 'r' . $revRecord->getId();
|
2017-11-30 18:52:30 +00:00
|
|
|
|
} else {
|
|
|
|
|
$cacheKey = sha1( $content->getNativeData() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Include any non-default wrapper class in the cache key too
|
|
|
|
|
$wrapClass = $parser->getOptions()->getWrapOutputClass();
|
2018-02-06 02:55:05 +00:00
|
|
|
|
if ( $wrapClass === false ) { // deprecated
|
2017-11-30 18:52:30 +00:00
|
|
|
|
$wrapClass = 'mw-parser-output';
|
|
|
|
|
}
|
2018-10-17 17:28:43 +00:00
|
|
|
|
if ( $wrapClass !== 'mw-parser-output' || $extraWrapper !== null ) {
|
2017-11-30 18:52:30 +00:00
|
|
|
|
$cacheKey .= '/' . $wrapClass;
|
2018-10-17 17:28:43 +00:00
|
|
|
|
if ( $extraWrapper !== null ) {
|
|
|
|
|
$cacheKey .= '/' . $extraWrapper;
|
|
|
|
|
}
|
2017-11-30 18:52:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Already cached?
|
|
|
|
|
if ( $parser->extTemplateStylesCache->has( $cacheKey ) ) {
|
|
|
|
|
return $parser->extTemplateStylesCache->get( $cacheKey );
|
|
|
|
|
}
|
2017-02-20 04:33:24 +00:00
|
|
|
|
|
2019-04-04 07:07:20 +00:00
|
|
|
|
$targetDir = $parser->getTargetLanguage()->getDir();
|
|
|
|
|
$contentDir = $parser->getContentLanguage()->getDir();
|
2021-10-27 12:44:52 +00:00
|
|
|
|
|
|
|
|
|
$contentHandlerFactory = MediaWikiServices::getInstance()->getContentHandlerFactory();
|
|
|
|
|
$contentHandler = $contentHandlerFactory->getContentHandler( $content->getModel() );
|
|
|
|
|
'@phan-var TemplateStylesContentHandler $contentHandler';
|
|
|
|
|
$status = $contentHandler->sanitize(
|
|
|
|
|
$content,
|
|
|
|
|
[
|
|
|
|
|
'flip' => $targetDir !== $contentDir,
|
|
|
|
|
'minify' => !ResourceLoader::inDebugMode(),
|
|
|
|
|
'class' => $wrapClass,
|
|
|
|
|
'extraWrapper' => $extraWrapper,
|
|
|
|
|
]
|
|
|
|
|
);
|
2017-02-20 04:33:24 +00:00
|
|
|
|
$style = $status->isOk() ? $status->getValue() : '/* Fatal error, no CSS will be output */';
|
|
|
|
|
|
|
|
|
|
// Prepend errors. This should normally never happen, but might if an
|
|
|
|
|
// update or configuration change causes something that was formerly
|
|
|
|
|
// valid to become invalid or something like that.
|
|
|
|
|
if ( !$status->isGood() ) {
|
|
|
|
|
$comment = wfMessage(
|
|
|
|
|
'templatestyles-errorcomment',
|
|
|
|
|
$title->getPrefixedText(),
|
2020-06-03 03:25:07 +00:00
|
|
|
|
$revRecord->getId(),
|
2019-12-09 14:40:27 +00:00
|
|
|
|
$status->getWikiText( false, 'rawmessage' )
|
2017-02-20 04:33:24 +00:00
|
|
|
|
)->text();
|
|
|
|
|
$comment = trim( strtr( $comment, [
|
|
|
|
|
// Use some lookalike unicode characters to avoid things that might
|
|
|
|
|
// otherwise confuse browsers.
|
|
|
|
|
'*' => '•', '-' => '‐', '<' => '⧼', '>' => '⧽',
|
|
|
|
|
] ) );
|
|
|
|
|
$style = "/*\n$comment\n*/\n$style";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hide the CSS from Parser::doBlockLevels
|
|
|
|
|
$marker = Parser::MARKER_PREFIX . '-templatestyles-' .
|
|
|
|
|
sprintf( '%08X', $parser->mMarkerIndex++ ) . Parser::MARKER_SUFFIX;
|
2021-02-19 22:09:55 +00:00
|
|
|
|
$parser->getStripState()->addNoWiki( $marker, $style );
|
2017-02-20 04:33:24 +00:00
|
|
|
|
|
|
|
|
|
// Return the inline <style>, which the Parser will wrap in a 'general'
|
|
|
|
|
// strip marker.
|
2017-11-24 19:20:47 +00:00
|
|
|
|
$ret = Html::inlineStyle( $marker, 'all', [
|
|
|
|
|
'data-mw-deduplicate' => "TemplateStyles:$cacheKey",
|
|
|
|
|
] );
|
2017-11-30 18:52:30 +00:00
|
|
|
|
$parser->extTemplateStylesCache->set( $cacheKey, $ret );
|
|
|
|
|
return $ret;
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-05-26 21:45:34 +00:00
|
|
|
|
/**
|
|
|
|
|
* Format an error in the `<templatestyles>` tag
|
|
|
|
|
* @param Parser $parser
|
|
|
|
|
* @param array $msg Arguments to wfMessage()
|
|
|
|
|
* @return string HTML
|
|
|
|
|
*/
|
|
|
|
|
private static function formatTagError( Parser $parser, array $msg ) {
|
|
|
|
|
$parser->addTrackingCategory( 'templatestyles-page-error-category' );
|
|
|
|
|
return '<strong class="error">' .
|
|
|
|
|
call_user_func_array( 'wfMessage', $msg )->inContentLanguage()->parse() .
|
|
|
|
|
'</strong>';
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-20 04:33:24 +00:00
|
|
|
|
}
|