jenkins-bot has submitted this change and it was merged. Change subject: Add property filtering ......................................................................
Add property filtering Properties listed in $wgTemplateStylesPropertyBlacklist, or those that contain function-like values not listed in $wgTemplateStylesFunctionWhitelist cause the containing declaration to be omitted from rendering entirely. Additionally, rule selectors are unconditionally prepended with '#mw-content-text' so that they cannot be applied to UI elements outside the actual page content. Change-Id: Id3d7dff465363d0163e4a5a1f31e770b4b0a67e2 --- M CSSParser.php M CSSRenderer.php M TemplateStyles.hooks.php M extension.json R tests/phpunit/CSSParseRenderTest.php 5 files changed, 124 insertions(+), 47 deletions(-) Approvals: BryanDavis: Looks good to me, approved Brion VIBBER: Looks good to me, but someone else must approve jenkins-bot: Verified diff --git a/CSSParser.php b/CSSParser.php index 67abfc2..889d0cd 100644 --- a/CSSParser.php +++ b/CSSParser.php @@ -30,24 +30,24 @@ (?# Sequences of whitespace ) | \/\* (?: [^*]+ | \*[^\/] )* \*\/ [ \n\t]* (?# Comments and any trailing whitespace ) - | " (?: [^"\\\\\n]+ | \\\\\. )* ["\n] + | " (?: [^"\\\\\n]+ | \\\\. )* ["\n] (?# Double-quoted string literals (to newline when unclosed ) - | \' (?: [^\'\\\\\n]+ | \\\\\. )* [\'\n] - (#? Single-quoted string literals (to newline when unclosed ) + | \' (?: [^\'\\\\\n]+ | \\\\. )* [\'\n] + (?# Single-quoted string literals (to newline when unclosed ) | [+-]? (?: [0-9]* \. )? [0-9]+ (?: [_a-z][_a-z0-9-]* | % )? - (#? Numerical literals - including optional trailing units or percent sign ) + (?# Numerical literals - including optional trailing units or percent sign ) | @? -? (?: [_a-z] | \\\\[0-9a-f]{1,6} [ \n\t]? ) (?: [_a-z0-9-]+ | \\\\[0-9a-f]{1,6} [ \n\t]? | [^\0-\177] )* (?: [ \n\t]* \( )? - (#? Identifiers - including leading `@` for at-rule blocks ) - (#? Trailing open captures are captured to match functional values ) + (?# Identifiers - including leading `@` for at-rule blocks ) + (?# Trailing open captures are captured to match functional values ) | \# (?: [_a-z0-9-]+ | \\\\[0-9a-f]{1,6} [ \n\t]? | [^\0-\177] )* - (#? So-called hatch literals ) + (?# So-called hatch literals ) | u\+ [0-9a-f]{1,6} (?: - [0-9a-f]{1,6} )? - (#? Unicode range literals ) + (?# Unicode range literals ) | u\+ [0-9a-f?]{1,6} - (#? Unicode mask literals ) + (?# Unicode mask literals ) | .) - (#? Any unmatched token is reduced to single characters ) + (?# Any unmatched token is reduced to single characters ) /xis', $css, $match ); $space = false; @@ -69,8 +69,13 @@ // prevents trying to obfuscate ASCII in identifiers to prevent matches. $t = preg_replace_callback( '/\\\\([0-9a-f]{1,6})[ \n\t]?/', function( $match ) { - return html_entity_decode( '&#'.$match[1].';', ENT_NOQUOTES, 'UTF-8' ); + return html_entity_decode( '&#x'.$match[1].';', ENT_NOQUOTES, 'UTF-8' ); }, $t ); + + // close unclosed string literals + if ( preg_match( '/^([\'"])(.*)\n$/', $t, $match ) ) { + $t = $match[1] . $match[2] . $match[1]; + } $space = false; $this->tokens[] = $t; @@ -95,7 +100,7 @@ $this->index += $num; return $text; } - return ''; + return []; } private function consumeTo( $delim ) { @@ -129,7 +134,7 @@ $this->consumeWS(); if ( $this->peek( 0 )!=':' ) { $this->consumeTo( [';', '}', null] ); - if ( $this->peek( 0 ) == ';' ) { + if ( $this->peek( 0 ) ) { $this->consume(); $this->consumeWS(); } @@ -164,9 +169,6 @@ } } } - if ( $this->peek( 0 ) == '}' ) { - $this->consume(); - } return $decls; } @@ -182,13 +184,15 @@ * Returns: * [ selectors => [ selector* ], decls => [ decl* ] ] */ - public function parseRule() { + public function parseRule( $baseSelectors ) { $selectors = []; $text = ''; $this->consumeWS(); while ( !in_array( $this->peek( 0 ), ['{', ';', null] ) ) { if ( $this->peek( 0 ) == ',' ) { - $selectors[] = $text; + if ( $text != '' ) { + $selectors[] = $baseSelectors . $text; + } $this->consume(); $this->consumeWS(); $text = ''; @@ -196,10 +200,15 @@ $text .= $this->consume()[0]; } } - $selectors[] = $text; + if ( $text != '' ) { + $selectors[] = $baseSelectors . $text; + } if ( $this->peek( 0 ) == '{' ) { $this->consume(); return [ "selectors"=>$selectors, "decls"=>$this->parseDecls() ]; + } + if ( $this->peek( 0 ) ) { + $this->consume(); } return null; } @@ -225,13 +234,13 @@ * Returns: * [ [ name=>ATIDENT? , text=>body? , rules=>rules? ]* ] */ - public function rules( $end = [ null ] ) { + public function rules( $baseSelectors = '', $end = [ null ] ) { $atrules = []; $rules = []; $this->consumeWS(); while ( !in_array( $this->peek( 0 ), $end ) ) { - if ( in_array( $this->peek( 0 ), [ '@media' ] ) ) { - $at = $this->consume(); + if ( in_array( strtolower( $this->peek( 0 ) ), [ '@media' ] ) ) { + $at = $this->consume()[0]; $this->consumeWS(); $text = ''; while ( !in_array( $this->peek( 0 ), ['{', ';', null] ) ) { @@ -239,7 +248,7 @@ } if ( $this->peek( 0 ) == '{' ) { $this->consume(); - $r = $this->rules( [ '}', null ] ); + $r = $this->rules( $baseSelectors, [ '}', null ] ); if ( $r ) { $atrules[] = [ "name"=>$at, "text"=>$text, "rules"=>$r ]; } @@ -247,7 +256,7 @@ $atrules[] = [ "name"=>$at, "text"=>$text ]; } } elseif ( $this->peek( 0 )[0] == '@' ) { - $at = $this->consume(); + $at = $this->consume()[0]; $text = ''; while ( !in_array( $this->peek( 0 ), ['{', ';', null] ) ) { $text .= $this->consume()[0]; @@ -261,14 +270,19 @@ } else { $atrules[] = [ "name"=>$at, "text"=>$text ]; } + } elseif ( $this->peek( 0 ) == '}' ) { + + $this->consume(); + } else { - $rules[] = $this->parseRule(); + $rules[] = $this->parseRule( $baseSelectors ); } $this->consumeWS(); } if ( $rules ) { $atrules[] = [ "name"=>'', "rules"=>$rules ]; } + $this->consumeWS(); if ( $this->peek( 0 ) !== null ) { $this->consume(); } diff --git a/CSSRenderer.php b/CSSRenderer.php index 5135666..70d2631 100644 --- a/CSSRenderer.php +++ b/CSSRenderer.php @@ -32,7 +32,7 @@ } foreach ( $rules as $at ) { - switch ( $at['name'] ) { + switch ( strtolower( $at['name'] ) ) { case '@media': if ( $media == '' ) { $this->add( $at['rules'], "@media ".$at['text'] ); @@ -51,7 +51,7 @@ * * @return string Rendered CSS */ - function render() { + function render( $functionWhitelist = [], $propertyBlacklist = [] ) { $css = ''; @@ -60,11 +60,29 @@ $css .= "$at {\n"; } foreach ( $rules as $rule ) { - $css .= implode( ',', $rule['selectors'] ) . "{"; - foreach ( $rule['decls'] as $key => $value ) { - $css .= "$key:" . implode( '', $value ) . ';'; + if ( $rule + and array_key_exists( 'selectors', $rule ) + and array_key_exists( 'decls', $rule ) ) + { + $css .= implode( ',', $rule['selectors'] ) . "{"; + foreach ( $rule['decls'] as $key => $value ) { + if ( !in_array( strtolower( $key ), $propertyBlacklist ) ) { + $blacklisted = false; + foreach ( $value as $prop ) { + if ( preg_match( '/^ ([^ \n\t]+) [ \n\t]* \( $/x', $prop, $match ) ) { + if ( !in_array( strtolower( $match[1] ), $functionWhitelist ) ) { + $blacklisted = true; + break; + } + } + } + if ( !$blacklisted ) { + $css .= "$key:" . implode( '', $value ) . ';'; + } + } + } + $css .= "} "; } - $css .= "} "; } if ( $at != '' ) { $css .= "} "; diff --git a/TemplateStyles.hooks.php b/TemplateStyles.hooks.php index b75e356..375ccc8 100644 --- a/TemplateStyles.hooks.php +++ b/TemplateStyles.hooks.php @@ -15,9 +15,18 @@ return true; } - public static function onUnitTestsList( &$paths ) { - $paths[] = __DIR__ . '/tests/phpunit/'; + /** + * Add phpunit tests + * + * @param array &$files List of phpunit test files + */ + public static function onUnitTestsList( &$files ) { + $files[] = __DIR__ . '/tests/phpunit/'; return true; + } + + public static function makeConfig() { + return new GlobalVarConfig( 'TemplateStyles' ); } private static function decodeFromBlob( $blob ) { @@ -33,17 +42,12 @@ } public static function onOutputPageParserOutput( &$out, $parseroutput ) { - global $wgTemplateStylesNamespaces; - if ( $wgTemplateStylesNamespaces ) { - $namespaces = $wgTemplateStylesNamespaces; - } else { - $namespaces = [ NS_TEMPLATE ]; - } + $config = ConfigFactory::getDefaultInstance()->makeConfig( 'TemplateStyles' ); $renderer = new CSSRenderer(); $pages = []; - foreach ( $namespaces as $ns ) { + foreach ( self::getConfigArray( $config, 'Namespaces' ) as $ns ) { if ( array_key_exists( $ns, $parseroutput->getTemplates() ) ) { foreach ( $parseroutput->getTemplates()[$ns] as $title => $pageid ) { $pages[$pageid] = $title; @@ -69,18 +73,39 @@ } - $selfcss = $out->getProperty( 'templatestyles' ); + $selfcss = $parseroutput->getProperty( 'templatestyles' ); if ( $selfcss ) { - $selfcss = self::decodeFromBlob( unserialize( gzdecode( $selfcss ) ) ); + $selfcss = self::decodeFromBlob( $selfcss ); if ( $selfcss ) { $renderer->add( $selfcss ); } } - $css = $renderer->render(); + $css = $renderer->render( + self::getConfigArray( $config, 'FunctionWhitelist' ), + self::getConfigArray( $config, 'PropertyBlacklist' ) + ); if ( $css ) { $out->addInlineStyle( $css ); } + } + + /** + * Convert a object-style configuration value to a plain array by + * returning the array keys from the found configuration where the + * associated value is truthy. + * + * @param Config $config Configuration instance + * @param string $name Name of configuration option + * @return array Configuration data + */ + private static function getConfigArray( Config $config, $name ) { + return array_keys( array_filter( + $config->get( "TemplateStyles{$name}" ), + function ( $val ) { + return (bool)$val; + } + ) ); } /** @@ -101,7 +126,10 @@ $css = new CSSParser( $input ); if ( $css ) { - $parser->getOutput()->setProperty( 'templatestyles', self::encodeToBlob( $css->rules() ) ); + $parser->getOutput()->setProperty( + 'templatestyles', + self::encodeToBlob( $css->rules( '#mw-content-text ' ) ) + ); } // TODO: The UX would benefit from the CSS being run through the @@ -112,7 +140,7 @@ . Html::rawElement( 'p', [ 'class' => 'mw-templatestyles-caption' ], - wfMessage( 'templatedata-doc-title' ) ) + wfMessage( 'templatestyles-doc-title' ) ) . Html::element( 'pre', [ 'class' => 'mw-templatestyles-stylesheet' ], diff --git a/extension.json b/extension.json index 13f787a..03ab87a 100644 --- a/extension.json +++ b/extension.json @@ -37,9 +37,26 @@ "OutputPageParserOutput": [ "TemplateStylesHooks::onOutputPageParserOutput" ], - "UnitTestList": [ - "TemplateStyleHooks::onUnitTestList" + "UnitTestsList": [ + "TemplateStylesHooks::onUnitTestsList" ] + }, + "config": { + "TemplateStylesNamespaces": { + "10": true + }, + "TemplateStylesFunctionWhitelist": { + "rgb": true + }, + "TemplateStylesPropertyBlacklist": { + "url": true, + "behavior": true, + "-moz-binding": true, + "-o-link": true + } + }, + "ConfigRegistry": { + "TemplateStyles": "TemplateStylesHooks::makeConfig" } } diff --git a/tests/CSSParseRenderTest.php b/tests/phpunit/CSSParseRenderTest.php similarity index 100% rename from tests/CSSParseRenderTest.php rename to tests/phpunit/CSSParseRenderTest.php -- To view, visit https://gerrit.wikimedia.org/r/282424 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Id3d7dff465363d0163e4a5a1f31e770b4b0a67e2 Gerrit-PatchSet: 18 Gerrit-Project: mediawiki/extensions/TemplateStyles Gerrit-Branch: master Gerrit-Owner: coren <m...@uberbox.org> Gerrit-Reviewer: Brion VIBBER <br...@wikimedia.org> Gerrit-Reviewer: BryanDavis <bda...@wikimedia.org> Gerrit-Reviewer: Jdlrobson <jrob...@wikimedia.org> Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org> Gerrit-Reviewer: Legoktm <legoktm.wikipe...@gmail.com> Gerrit-Reviewer: Tim Starling <tstarl...@wikimedia.org> Gerrit-Reviewer: coren <m...@uberbox.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits