getConfigFactory() ->makeConfig( 'templatestyles' ); } return self::$config; } /** * Get our MatcherFactory * @return MatcherFactory * @codeCoverageIgnore */ private static function getMatcherFactory() { if ( !self::$matcherFactory ) { $config = self::getConfig(); self::$matcherFactory = new TemplateStylesMatcherFactory( $config->get( 'TemplateStylesAllowedUrls' ) ); } return self::$matcherFactory; } /** * Validate an extra wrapper-selector * @param string $wrapper * @return Token[]|false Token representation of the selector, or false on failure */ 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() ->match( $components, [ 'mark-significance' => true ] ); } self::$wrappers[$wrapper] = $match ? $components->toTokenArray() : false; } return self::$wrappers[$wrapper]; } /** * Get our Sanitizer * @param string $class Class to limit selectors to * @param string|null $extraWrapper Extra selector to limit selectors to * @return Sanitizer */ public static function getSanitizer( $class, $extraWrapper = null ) { $key = $extraWrapper !== null ? "$class $extraWrapper" : $class; if ( !isset( self::$sanitizers[$key] ) ) { $config = self::getConfig(); $matcherFactory = self::getMatcherFactory(); $propertySanitizer = new StylePropertySanitizer( $matcherFactory ); $propertySanitizer->setKnownProperties( array_diff_key( $propertySanitizer->getKnownProperties(), array_flip( $config->get( 'TemplateStylesPropertyBlacklist' ) ) ) ); Hooks::run( 'TemplateStylesPropertySanitizer', [ &$propertySanitizer, $matcherFactory ] ); $htmlOrBodySimpleSelectorSeqMatcher = new CheckedMatcher( $matcherFactory->cssSimpleSelectorSeq(), function ( ComponentValueList $values, Match $match, array $options ) { foreach ( $match->getCapturedMatches() as $m ) { if ( $m->getName() !== 'element' ) { continue; } $str = (string)$m; return $str === 'html' || $str === 'body'; } return false; } ); $prependSelectors = [ new Token( Token::T_DELIM, '.' ), new Token( Token::T_IDENT, $class ), ]; if ( $extraWrapper !== null ) { $extraTokens = self::validateExtraWrapper( $extraWrapper ); if ( !$extraTokens ) { throw new InvalidArgumentException( "Invalid value for \$extraWrapper: $extraWrapper" ); } $prependSelectors = array_merge( $prependSelectors, [ new Token( Token::T_WHITESPACE, [ 'significant' => true ] ) ], $extraTokens ); } $atRuleBlacklist = array_flip( $config->get( 'TemplateStylesAtRuleBlacklist' ) ); $ruleSanitizers = [ 'styles' => new StyleRuleSanitizer( $matcherFactory->cssSelectorList(), $propertySanitizer, [ 'prependSelectors' => $prependSelectors, 'hoistableComponentMatcher' => $htmlOrBodySimpleSelectorSeqMatcher, ] ), '@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, ] ), ]; $ruleSanitizers = array_diff_key( $ruleSanitizers, $atRuleBlacklist ); if ( isset( $ruleSanitizers['@media'] ) ) { // In case @media was blacklisted $ruleSanitizers['@media']->setRuleSanitizers( $ruleSanitizers ); } if ( isset( $ruleSanitizers['@supports'] ) ) { // In case @supports was blacklisted $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 ), ]; $allRuleSanitizers = array_diff_key( $allRuleSanitizers, $atRuleBlacklist ); $sanitizer = new StylesheetSanitizer( $allRuleSanitizers ); Hooks::run( 'TemplateStylesStylesheetSanitizer', [ &$sanitizer, $propertySanitizer, $matcherFactory ] ); self::$sanitizers[$key] = $sanitizer; } return self::$sanitizers[$key]; } /** * 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 `` to the parser. * @param Parser $parser Parser object being cleared * @return bool */ public static function onParserFirstCallInit( Parser $parser ) { $parser->setHook( 'templatestyles', 'TemplateStylesHooks::handleTag' ); /** @phan-suppress-next-line PhanUndeclaredProperty */ $parser->extTemplateStylesCache = new MapCacheLRU( 100 ); // 100 is arbitrary return true; } /** * Fix Tidy screw-ups * * It seems some versions of Tidy try to wrap the contents of a `