Skip to content

Commit

Permalink
Support for @media rule (#553)
Browse files Browse the repository at this point in the history
* add(gh-552): support for @media rule

* fix(gh-552): failing test due to missing literal in policy

* test(gh-552): add some more test cases for abbreviated syntax

* refactor(gh-552): move new policy rules from default policy to anythinggoes

* test(gh-552): default policy and extend it accordingly

* add(gh-552): handling of or, adds new test cases, refactors parsing

* refactor(gh-552): make Java 7 compliant

* fix(gh-552): change logical operator to OR for regex pattern matching
  • Loading branch information
jonah1und1 authored Feb 18, 2025
1 parent ff0cf8d commit 833e1f2
Show file tree
Hide file tree
Showing 10 changed files with 1,018 additions and 20 deletions.
80 changes: 60 additions & 20 deletions src/main/java/org/owasp/validator/css/CssHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
import java.util.LinkedList;
import java.util.List;
import java.util.ResourceBundle;
import org.owasp.validator.css.media.CssMediaFeature;
import org.owasp.validator.css.media.CssMediaQuery;
import org.owasp.validator.css.media.CssMediaQueryList;
import org.owasp.validator.css.media.CssMediaType;
import org.owasp.validator.html.InternalPolicy;
import org.owasp.validator.html.Policy;
import org.owasp.validator.html.ScanException;
Expand Down Expand Up @@ -96,6 +100,10 @@ public class CssHandler implements DocumentHandler {
*/
private boolean selectorOpen = false;

private MediaState mediaState = MediaState.OUTSIDE;

private enum MediaState {INSIDE, OUTSIDE, DENIED}

/**
* Constructs a handler for stylesheets using the given policy. The List of embedded stylesheets
* produced by this constructor is now available via the getImportedStylesheetsURIList() method.
Expand Down Expand Up @@ -340,26 +348,6 @@ public void endFontFace() throws CSSException {
// CSS2 Font Face declaration - ignore this for now
}

/*
* (non-Javadoc)
*
* @see org.w3c.css.sac.DocumentHandler#startMedia(org.w3c.css.sac.SACMediaList)
*/
@Override
public void startMedia(SACMediaList media) throws CSSException {
// CSS2 Media declaration - ignore this for now
}

/*
* (non-Javadoc)
*
* @see org.w3c.css.sac.DocumentHandler#endMedia(org.w3c.css.sac.SACMediaList)
*/
@Override
public void endMedia(SACMediaList media) throws CSSException {
// CSS2 Media declaration - ignore this for now
}

/*
* (non-Javadoc)
*
Expand Down Expand Up @@ -538,4 +526,56 @@ public void property(String name, LexicalUnit value, boolean important) throws C
}));
}
}

@Override
public void startMedia(SACMediaList media) throws CSSException {
CssMediaQueryList mediaQueryList = (CssMediaQueryList) media;

boolean first = true;
for (CssMediaQuery query : mediaQueryList.getMediaQueries()) {
if (!validator.isValidMediaQuery(query)) {
continue;
}
if (first) {
styleSheet.append("@media ");
first = false;
} else {
styleSheet.append(", ");
}
if (query.getLogicalOperator() != null) {
styleSheet.append(query.getLogicalOperator()).append(' ');
}
styleSheet.append(query.getMediaType());
for (CssMediaFeature feature : query.getMediaFeatures()) {
if (feature == query.getMediaFeatures().get(0) && query.getMediaType() == CssMediaType.IMPLIED_ALL) {
styleSheet.append("(");
} else {
styleSheet.append(" and (");
}
styleSheet.append(feature.getName());
if (feature.getExpression() != null) {
styleSheet.append(": ");
styleSheet.append(validator.lexicalValueToString(feature.getExpression()));
}
styleSheet.append(')');
}
}

if (!first) {
styleSheet.append(" {");
styleSheet.append('\n');
mediaState = MediaState.INSIDE;
} else {
mediaState = MediaState.DENIED;
}
}

@Override
public void endMedia(SACMediaList media) throws CSSException {
if (mediaState == MediaState.INSIDE) {
styleSheet.append('}');
styleSheet.append('\n');
}
mediaState = MediaState.OUTSIDE;
}
}
154 changes: 154 additions & 0 deletions src/main/java/org/owasp/validator/css/CssParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@
*/
package org.owasp.validator.css;

import static org.owasp.validator.css.media.CssMediaQueryLogicalOperator.AND;
import static org.owasp.validator.css.media.CssMediaQueryLogicalOperator.OR;

import org.apache.batik.css.parser.CSSSACMediaList;
import org.apache.batik.css.parser.LexicalUnits;
import org.owasp.validator.css.media.CssMediaFeature;
import org.owasp.validator.css.media.CssMediaQuery;
import org.owasp.validator.css.media.CssMediaQueryList;
import org.owasp.validator.css.media.CssMediaQueryLogicalOperator;
import org.owasp.validator.css.media.CssMediaType;
import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.LexicalUnit;
Expand Down Expand Up @@ -96,4 +105,149 @@ protected void parseStyleDeclaration(final boolean inSheet) throws CSSException
}
}
}

@Override
protected CSSSACMediaList parseMediaList() {
CssMediaQueryList mediaList = new CssMediaQueryList();

mediaList.append(parseMediaQuery());
while (hasAnotherMediaQuery()) {
nextIgnoreSpaces();
mediaList.append(parseMediaQuery());
}

return mediaList;
}

private boolean hasAnotherMediaQuery() {
return current == LexicalUnits.COMMA || (current == LexicalUnits.IDENTIFIER && scanner.getStringValue().equals(OR.toString()));
}

protected CssMediaQuery parseMediaQuery() {
CssMediaQuery query = new CssMediaQuery();
CssMediaType mediaType = null;
CssMediaQueryLogicalOperator logicalOperator = null;
switch (current) {
case LexicalUnits.LEFT_BRACE:
mediaType = CssMediaType.IMPLIED_ALL;
break;
case LexicalUnits.IDENTIFIER:
logicalOperator = CssMediaQueryLogicalOperator.parse(scanner.getStringValue());
if (logicalOperator != null) {
switch (logicalOperator) {
case ONLY:
parseLogicalOperatorOnly();
mediaType = parseMediaType();
break;
case NOT:
mediaType = parseLogicalOperatorNot();
break;
case AND:
case OR:
case COMMA:
throw createCSSParseException("identifier");
}
} else {
mediaType = parseMediaType();
}
break;
default:
throw createCSSParseException("identifier");
}
query.setMediaType(mediaType);
query.setLogicalOperator(logicalOperator);

if (mediaType == CssMediaType.IMPLIED_ALL) {
query.addMediaFeature(parseMediaFeature());
}

while (current == LexicalUnits.IDENTIFIER && CssMediaQueryLogicalOperator.parse(scanner.getStringValue()) == AND) {
nextIgnoreSpaces();
query.addMediaFeature(parseMediaFeature());
}
return query;
}

private CssMediaType parseMediaType() {
CssMediaType mediaType;
mediaType = CssMediaType.parse(scanner.getStringValue());
if (mediaType == null) {
throw createCSSParseException("identifier");
}
nextIgnoreSpaces();
return mediaType;
}

private CssMediaType parseLogicalOperatorNot() {
CssMediaType mediaType;
if (nextIgnoreSpaces() == LexicalUnits.IDENTIFIER) {
mediaType = parseMediaType();
} else {
mediaType = CssMediaType.IMPLIED_ALL;
}
return mediaType;
}

private void parseLogicalOperatorOnly() {
if (nextIgnoreSpaces() != LexicalUnits.IDENTIFIER) {
throw createCSSParseException("identifier");
}
}

protected CssMediaFeature parseMediaFeature() {
if (current != LexicalUnits.LEFT_BRACE) {
throw createCSSParseException("'(' expected.");
}
nextIgnoreSpaces();
String namePrefix = "";
if (current == LexicalUnits.MINUS) {
nextIgnoreSpaces();
namePrefix = "-";
}
if (current != LexicalUnits.IDENTIFIER) {
throw createCSSParseException("identifier");
}
String name = namePrefix + scanner.getStringValue();
nextIgnoreSpaces();
LexicalUnit exp = null;
if (current == LexicalUnits.COLON) {
nextIgnoreSpaces();
exp = parseTerm(null);
}
if (current != LexicalUnits.RIGHT_BRACE) {
throw createCSSParseException("')' expected.");
}
nextIgnoreSpaces();

return new CssMediaFeature(name, exp);
}

@Override
protected void parseMediaRule() {
CSSSACMediaList ml = parseMediaList();
try {
documentHandler.startMedia(ml);

if (current != LexicalUnits.LEFT_CURLY_BRACE) {
reportError("left.curly.brace");
} else {
nextIgnoreSpaces();

loop:
for (; ; ) {
switch (current) {
case LexicalUnits.EOF:
case LexicalUnits.RIGHT_CURLY_BRACE:
break loop;
default:
parseRuleSet();
}
}

nextIgnoreSpaces();
}
} finally {
documentHandler.endMedia(ml);
}
}
}
44 changes: 44 additions & 0 deletions src/main/java/org/owasp/validator/css/CssValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
import java.text.DecimalFormat;
import java.util.Iterator;
import java.util.regex.Pattern;
import org.apache.batik.css.parser.CSSLexicalUnit;
import org.owasp.validator.css.media.CssMediaFeature;
import org.owasp.validator.css.media.CssMediaQuery;
import org.owasp.validator.html.Policy;
import org.owasp.validator.html.ScanException;
import org.owasp.validator.html.model.AntiSamyPattern;
Expand All @@ -56,6 +59,8 @@
*/
public class CssValidator {

private static final LexicalUnit EMPTYSTRINGLEXICALUNIT = CSSLexicalUnit.createString(LexicalUnit.SAC_STRING_VALUE, "", null);

private final Policy policy;

/**
Expand Down Expand Up @@ -406,6 +411,45 @@ public String lexicalValueToString(LexicalUnit lu) {
}
}

/**
* Returns whether the given {@link CssMediaQuery} is valid
*
* @param mediaQuery mediaQuery
* @return valid mediaQuery?
*/
public boolean isValidMediaQuery(CssMediaQuery mediaQuery) {
// check mediaType against allowed media-HTML-Attribute
Property mediatype = policy.getPropertyByName("_mediatype");
if (mediatype == null) {
return false;
}

String mediaTypeString = mediaQuery.getMediaType().toString().toLowerCase();
boolean isRegExpAllowed = false;
for (Pattern pattern : mediatype.getAllowedRegExp()) {
isRegExpAllowed |= pattern.matcher(mediaTypeString).matches();
}
boolean isValidMediaType = mediatype.getAllowedValues().contains(mediaTypeString) || isRegExpAllowed;
if (!isValidMediaType) {
return false;
}

for (CssMediaFeature feature : mediaQuery.getMediaFeatures()) {
LexicalUnit expression = feature.getExpression();
if (expression == null) {
expression = EMPTYSTRINGLEXICALUNIT;
}
if (!isValidMediaFeature(feature.getName(), expression)) {
return false;
}
}
return true;
}

private boolean isValidMediaFeature(String name, LexicalUnit lu) {
return isValidProperty("_mediafeature_" + name, lu);
}

/**
* Returns color value as int.
* Maps percentages to values between 0 and 255.
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/org/owasp/validator/css/media/CssMediaFeature.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.owasp.validator.css.media;

import org.w3c.css.sac.LexicalUnit;

public class CssMediaFeature {

private final String name;
private final LexicalUnit expression;

/**
* Constructor.
*
* @param name Feature-name
* @param expression expression, may be null
*/
public CssMediaFeature(String name, LexicalUnit expression) {
this.name = name;
this.expression = expression;
}

public String getName() {
return name;
}

public LexicalUnit getExpression() {
return expression;
}
}
Loading

0 comments on commit 833e1f2

Please sign in to comment.