General cleanup of CSSRenderer

* Add phpdoc comments
* Rename some variables to be a bit more clear for new readers
* Break up render() to make things more readable and reduce cyclomatic
  complexity

Change-Id: Iceeb1f6eb09b61efe6b81f359d28741f54fe88ad
This commit is contained in:
Bryan Davis 2016-04-20 23:07:36 -06:00
parent 1a6879c457
commit b39d76be08
2 changed files with 104 additions and 47 deletions

View file

@ -1,6 +1,4 @@
<?php <?php
/** /**
* @file * @file
* @ingroup Extensions * @ingroup Extensions
@ -13,85 +11,131 @@
*/ */
class CSSRenderer { class CSSRenderer {
private $bymedia; /** @var array $byMedia */
private $byMedia;
function __construct() { function __construct() {
$this->bymedia = []; $this->byMedia = [];
} }
/** /**
* Adds (and merge) a parsed CSS tree to the render list. * Adds (and merge) a parsed CSS tree to the render list.
* *
* @param array $rules The parsed tree as created by CSSParser::rules() * @param array $rules The parsed tree as created by CSSParser::rules()
* @param string $media Forcibly specified @media block selector. Normally unspecified * @param string $media Forcibly specified @media block selector.
* and defaults to the empty string.
*/ */
function add( $rules, $media = '' ) { function add( $rules, $media = '' ) {
if ( !array_key_exists( $media, $this->bymedia ) ) { if ( !array_key_exists( $media, $this->byMedia ) ) {
$this->bymedia[$media] = []; $this->byMedia[$media] = [];
} }
foreach ( $rules as $at ) { foreach ( $rules as $rule ) {
switch ( strtolower( $at['name'] ) ) { switch ( strtolower( $rule['name'] ) ) {
case '@media': case '@media':
if ( $media == '' ) { if ( $media == '' ) {
$this->add( $at['rules'], "@media ".$at['text'] ); $this->add(
$rule['rules'], "@media {$rule['text']}"
);
} }
break; break;
case '': case '':
$this->bymedia[$media] = array_merge( $this->bymedia[$media], $at['rules'] ); $this->byMedia[$media] = array_merge(
$this->byMedia[$media], $rule['rules']
);
break; break;
} }
} }
} }
/** /**
* Renders the collected CSS trees into a string suitable for inclusion * Render the collected CSS trees into a string suitable for inclusion
* in a <style> tag. * in a <style> tag.
* *
* @param array $functionWhitelist List of functions that are allowed
* @param array $propertyBlacklist List of properties that not allowed
* @return string Rendered CSS * @return string Rendered CSS
*/ */
function render( $functionWhitelist = [], $propertyBlacklist = [] ) { function render(
array $functionWhitelist = [],
array $propertyBlacklist = []
) {
// Normalize whitelist and blacklist values to lowercase
$functionWhitelist = array_map( 'strtolower', $functionWhitelist );
$propertyBlacklist = array_map( 'strtolower', $propertyBlacklist );
$css = ''; $css = '';
foreach ( $this->byMedia as $media => $rules ) {
foreach ( $this->bymedia as $at => $rules ) { if ( $media !== '' ) {
if ( $at != '' ) { $css .= "{$media} {\n";
$css .= "$at {\n";
} }
foreach ( $rules as $rule ) { foreach ( $rules as $rule ) {
if ( $rule if ( $rule !== null ) {
and array_key_exists( 'selectors', $rule ) $css .= $this->renderRule(
and array_key_exists( 'decls', $rule ) ) $rule, $functionWhitelist, $propertyBlacklist );
{
$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 .= "} ";
} }
} }
if ( $at != '' ) { if ( $media !== '' ) {
$css .= "} "; $css .= '} ';
} }
} }
return $css; return $css;
} }
/**
* Render a single rule.
*
* @param array $rule Parsed rule
* @param array $functionWhitelist List of functions that are allowed
* @param array $propertyBlacklist List of properties that not allowed
* @return string Rendered CSS
*/
private function renderRule(
array $rule,
array $functionWhitelist,
array $propertyBlacklist
) {
$css = '';
if ( $rule &&
array_key_exists( 'selectors', $rule ) &&
array_key_exists( 'decls', $rule )
) {
$css .= implode( ',', $rule['selectors'] ) . '{';
foreach ( $rule['decls'] as $prop => $values ) {
$css .= $this->renderDecl(
$prop, $values, $functionWhitelist, $propertyBlacklist );
}
$css .= '} ';
}
return $css;
}
/**
* Render a property declaration.
*
* @param string $prop Property name
* @param array $values Parsed property values
* @param array $functionWhitelist List of functions that are allowed
* @param array $propertyBlacklist List of properties that not allowed
* @return string Rendered CSS
*/
private function renderDecl(
$prop,
array $values,
array $functionWhitelist,
array $propertyBlacklist
) {
if ( in_array( strtolower( $prop ), $propertyBlacklist ) ) {
// Property is blacklisted
return '';
}
foreach ( $values as $value ) {
if ( preg_match( '/^ (\S+) \s* \( $/x', $value, $match ) ) {
if ( !in_array( strtolower( $match[1] ), $functionWhitelist ) ) {
// Function is blacklisted
return '';
}
}
}
return $prop . ':' . implode( '', $values ) . ';';
}
} }

View file

@ -19,8 +19,8 @@ class CSSParseRenderTest extends MediaWikiTestCase {
$expect, $expect,
$source, $source,
$baseSelector = '.X ', $baseSelector = '.X ',
$functionWhitelist = [ 'whitelisted' ], array $functionWhitelist = [ 'whitelisted' ],
$propertyBlacklist = [ '-evil' ] array $propertyBlacklist = [ '-evil' ]
) { ) {
$tree = new CSSParser( $source ); $tree = new CSSParser( $source );
$rules = $tree->rules( $baseSelector ); $rules = $tree->rules( $baseSelector );
@ -239,6 +239,19 @@ prop2/*
;prop3 :not/**/whitelisted( val3 );} ;prop3 :not/**/whitelisted( val3 );}
CSS CSS
], ],
'Whitelist normalized' => [
'expect' => '.foo {bar:whitelisted(1);} ',
'css' => '.foo { bar: whitelisted(1); baz: url(1); }',
'prefix' => '',
'whitelist' => [ 'WHITELISTED' ],
],
'Blacklist normalized' => [
'expect' => '.foo {baz:1;} ',
'css' => '.foo { blacklisted: 1; baz: 1; }',
'prefix' => '',
'whitelist' => [],
'blacklist' => [ 'BLACKLISTED' ],
],
]; ];
} }
} }