Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for @media rule #553

Merged
merged 9 commits into from
Feb 18, 2025
75 changes: 55 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,9 @@
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.html.InternalPolicy;
import org.owasp.validator.html.Policy;
import org.owasp.validator.html.ScanException;
Expand Down Expand Up @@ -96,6 +99,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 +347,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 +525,52 @@ 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()) {
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;
}
}
111 changes: 111 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,15 @@
*/
package org.owasp.validator.css;

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

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 +104,107 @@ protected void parseStyleDeclaration(final boolean inSheet) throws CSSException
}
}
}

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

mediaList.append(parseMediaQuery());
while (current == LexicalUnits.COMMA) {
nextIgnoreSpaces();
mediaList.append(parseMediaQuery());
}

return mediaList;
}

protected CssMediaQuery parseMediaQuery() {
CssMediaQuery query = new CssMediaQuery();
switch (current) {
case LexicalUnits.LEFT_BRACE:
query.setMediaType(CssMediaType.ALL);
query.addMediaFeature(parseMediaFeature());
break;
case LexicalUnits.IDENTIFIER:
final CssMediaQueryLogicalOperator logicalOperator = CssMediaQueryLogicalOperator.parse(scanner.getStringValue());
if (logicalOperator != null) {
query.setLogicalOperator(logicalOperator);
if (nextIgnoreSpaces() != LexicalUnits.IDENTIFIER) {
throw createCSSParseException("identifier");
}
}
final CssMediaType mediaType = CssMediaType.parse(scanner.getStringValue());
if (mediaType == null) {
throw createCSSParseException("identifier");
}
query.setMediaType(mediaType);
nextIgnoreSpaces();
break;
default:
throw createCSSParseException("identifier");
}

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

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("right.brace");
}
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);
}
}
}
41 changes: 41 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,9 +31,13 @@
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;
import org.owasp.validator.html.model.Attribute;
import org.owasp.validator.html.model.Property;
import org.owasp.validator.html.util.HTMLEntityEncoder;
import org.w3c.css.sac.AttributeCondition;
Expand All @@ -56,6 +60,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 +412,41 @@ 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
Attribute mediaAttribute = policy.getTagByLowercaseName("style").getAttributeByName("media");
if (mediaAttribute == null) {
return false;
}

String mediaTypeString = mediaQuery.getMediaType().toString().toLowerCase();
boolean isValidMediaType = mediaAttribute.containsAllowedValue(mediaTypeString) || mediaAttribute.matchesAllowedExpression(mediaTypeString);
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;
}
}
36 changes: 36 additions & 0 deletions src/main/java/org/owasp/validator/css/media/CssMediaQuery.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.owasp.validator.css.media;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CssMediaQuery {

private CssMediaQueryLogicalOperator logicalOperator;
private CssMediaType mediaType;
private final List<CssMediaFeature> mediaFeatures = new ArrayList<>();

public CssMediaQueryLogicalOperator getLogicalOperator() {
return logicalOperator;
}

public void setLogicalOperator(CssMediaQueryLogicalOperator logicalOperator) {
this.logicalOperator = logicalOperator;
}

public CssMediaType getMediaType() {
return mediaType;
}

public void setMediaType(CssMediaType mediaType) {
this.mediaType = mediaType;
}

public void addMediaFeature(CssMediaFeature mediaFeature) {
mediaFeatures.add(mediaFeature);
}

public List<CssMediaFeature> getMediaFeatures() {
return Collections.unmodifiableList(mediaFeatures);
}
}
Loading
Loading