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
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
Loading