All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.amp.validator.utils.CssSpecUtils Maven / Gradle / Ivy

There is a newer version: 1.0.42
Show newest version
/*
 *
 * ====================================================================
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *  ====================================================================
 */

/*
 * Changes to the original project are Copyright 2019, Yahoo Inc..
 */

package dev.amp.validator.utils;

import com.steadystate.css.parser.Token;
import dev.amp.validator.Context;
import dev.amp.validator.ParsedAttrSpec;
import dev.amp.validator.UrlErrorInStylesheetAdapter;
import dev.amp.validator.ValidateTagResult;
import dev.amp.validator.ValidatorProtos;
import dev.amp.validator.css.Canonicalizer;
import dev.amp.validator.css.CssParser;
import dev.amp.validator.css.CssTokenUtil;
import dev.amp.validator.css.CssValidationException;
import dev.amp.validator.css.Declaration;
import dev.amp.validator.css.EOFToken;
import dev.amp.validator.css.ErrorToken;
import dev.amp.validator.css.ParsedCssUrl;
import dev.amp.validator.css.ParsedDocCssSpec;
import dev.amp.validator.css.Stylesheet;
import dev.amp.validator.css.TokenType;
import dev.amp.validator.visitor.Amp4AdsVisitor;
import dev.amp.validator.visitor.ImportantPropertyVisitor;
import dev.amp.validator.visitor.KeyframesVisitor;
import dev.amp.validator.visitor.RuleVisitor;
import dev.amp.validator.visitor.UrlFunctionVisitor;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import static dev.amp.validator.utils.TagSpecUtils.getTagDescriptiveName;

/**
 * Methods to handle Css Spec processing.
 *
 * @author nhant01
 * @author GeorgeLuo
 */

public final class CssSpecUtils {
    /**
     * private constructor
     */
    private CssSpecUtils() {
    }

    /**
     * Returns a Stylesheet object with nested parse_css.Rules.
     * 

* The top level Rules in a Stylesheet are always a series of * QualifiedRule's or AtRule's. * * @param tokenList the css content as token list * @param atRuleSpec block type rules for * all CSS AT rules this canonicalizer should handle. * @param defaultSpec default block type for types not * found in atRuleSpec. * @param errors output array for the errors. * @return a stylesheet object model * @throws CssValidationException css validation exception */ public static Stylesheet parseAStylesheet(@Nonnull final List tokenList, @Nonnull final Map atRuleSpec, @Nonnull final BlockType defaultSpec, @Nonnull final List errors) throws CssValidationException { final Canonicalizer canonicalizer = new Canonicalizer(atRuleSpec, defaultSpec); final Stylesheet stylesheet = new Stylesheet(); stylesheet.setRules(canonicalizer.parseAListOfRules(tokenList, /* topLevel */ true, errors)); CssTokenUtil.copyPosTo(tokenList.get(0), stylesheet); stylesheet.setEOF((EOFToken) CssTokenUtil.copyPosTo(tokenList.get(tokenList.size() - 1), new EOFToken())); return stylesheet; } /** * Extracts the URLs within the provided stylesheet, emitting them into * parsedUrls and errors into errors. * * @param stylesheet to parse urls from * @param parsedUrls urls found in stylesheet * @param errors collection of tokens to populate * @throws CssValidationException css validation exception */ public static void extractUrls(@Nonnull final Stylesheet stylesheet, @Nonnull final List parsedUrls, @Nonnull final List errors) throws CssValidationException { final int errorsOldLength = errors.size(); final UrlFunctionVisitor visitor = new UrlFunctionVisitor(parsedUrls, errors); stylesheet.accept(visitor); // If anything went wrong, delete the urls we've already emitted. if (errorsOldLength != errors.size()) { final int parsedUrlsOldLength = parsedUrls.size(); parsedUrls.subList(parsedUrlsOldLength, parsedUrls.size()).clear(); } } /** * Same as the stylesheet variant above, but operates on a single declaration at * a time. Usedful when operating on parsed style attributes. * * @param declaration the decl to parse * @param parsedUrls collection of urls * @param errors reference * @throws CssValidationException CssValidationException */ public static void extractUrlsFromDeclaration(@Nonnull final Declaration declaration, @Nonnull final List parsedUrls, @Nonnull final List errors) throws CssValidationException { final int errorsOldLength = errors.size(); final UrlFunctionVisitor visitor = new UrlFunctionVisitor(parsedUrls, errors); declaration.accept(visitor); // If anything went wrong, delete the urls we've already emitted. if (errorsOldLength != errors.size()) { final int parsedUrlsOldLength = parsedUrls.size(); parsedUrls.subList(parsedUrlsOldLength, parsedUrls.size()).clear(); } } /** * Extracts the declarations marked `!important` within within the provided * stylesheet, emitting them into `important`. * * @param stylesheet to walk through * @param important list to populate * @throws CssValidationException css validation exception */ public static void extractImportantDeclarations(@Nonnull final Stylesheet stylesheet, @Nonnull final List important) throws CssValidationException { final ImportantPropertyVisitor visitor = new ImportantPropertyVisitor(important); stylesheet.accept(visitor); } /** * Strips vendor prefixes from identifiers, e.g. property names or names * of at rules. E.g., "-moz-keyframes" to "keyframes". * * @param prefixedString string with prefix * @return input string less the prefix component */ public static String stripVendorPrefix(@Nonnull final String prefixedString) { // Checking for '-' is an optimization. if (!prefixedString.equals("") && prefixedString.charAt(0) == '-') { if (prefixedString.startsWith("-o-")) { return prefixedString.substring("-o-".length()); } if (prefixedString.startsWith("-moz-")) { return prefixedString.substring("-moz-".length()); } if (prefixedString.startsWith("-ms-")) { return prefixedString.substring("-ms-".length()); } if (prefixedString.startsWith("-webkit-")) { return prefixedString.substring("-webkit-".length()); } } return prefixedString; } /** * Strips 'min-' or 'max-' from the start of a media feature identifier, if * present. E.g., "min-width" to "width". * * @param prefixedString the string with a prefix * @return the prefix-stripped string */ public static String stripMinMax(@Nonnull final String prefixedString) { if (prefixedString.startsWith("min-")) { return prefixedString.substring("min-".length()); } if (prefixedString./*OK*/ startsWith("max-")) { return prefixedString.substring("max-".length()); } return prefixedString; } /** * @param token value to match * @param str to match against * @return true iff ascii value of token and string match */ public static boolean asciiMatch(@Nonnull final Token token, @Nonnull final String str) { return token.toString().toLowerCase().equals(str.toLowerCase()); } /** * validate the keyframes of css content * * @param styleSheet to validate * @param errors generated from css parsing * @throws CssValidationException css validation exception */ public static void validateKeyframesCss(@Nonnull final Stylesheet styleSheet, @Nonnull final List errors) throws CssValidationException { final RuleVisitor visitor = new KeyframesVisitor(errors); styleSheet.accept(visitor); } /** * validate a css document against amp4ads specs * * @param styleSheet to validate * @param errors generated from css parsing * @throws CssValidationException css validation exception */ public static void validateAmp4AdsCss(@Nonnull final Stylesheet styleSheet, @Nonnull final List errors) throws CssValidationException { final RuleVisitor visitor = new Amp4AdsVisitor(errors); styleSheet.accept(visitor); } /** * Returns true if the given Declaration is considered valid. * * @param cssSpec the css spec of interest. * @param declarationName the declaration to query for in the css spec. * @return true iff the declaration is found in scc spec's allowed declarations */ public static boolean isDeclarationValid(@Nonnull final ValidatorProtos.CssSpec cssSpec, @Nonnull final String declarationName) { if (cssSpec.getDeclarationList().size() == 0) { return true; } return cssSpec.getDeclarationList().indexOf(stripVendorPrefix(declarationName)) > -1; } /** * Returns a string of the allowed Declarations. * * @param cssSpec of interest * @return a string representation of allowed declarations */ public static String allowedDeclarationsString(@Nonnull final ValidatorProtos.CssSpec cssSpec) { if (cssSpec.getDeclarationList().size() > MAX_NUM_ALLOWED_DECLARATIONS) { return ""; } return "[\'" + String.join("\', \'", cssSpec.getDeclarationList()) + "\']"; } /** * Parses a CSS URL token; typically takes the form "url(http://foo)". * Preconditions: tokens[token_idx] is a URL token * and token_idx + 1 is in range. * * @param tokens to parse * @param tokenIdx starting index from tokens * @param parsed ParsedCssUrl to populate * @throws CssValidationException css validation exception */ public static void parseUrlToken(@Nonnull final List tokens, final int tokenIdx, @Nonnull final ParsedCssUrl parsed) throws CssValidationException { if (tokenIdx + 1 >= tokens.size()) { throw new CssValidationException("Url token not within range of tokens"); } final Token token = tokens.get(tokenIdx); if (CssTokenUtil.getTokenType(token) != TokenType.URL) { throw new CssValidationException("Url token not within range of tokens"); } CssTokenUtil.copyPosTo(token, parsed); parsed.setUtf8Url(token.toString()); } /** * Parses a CSS function token named 'url', including the string and closing * paren. Typically takes the form "url('http://foo')". * Returns the token_idx past the closing paren, or -1 if parsing fails. * Preconditions: tokens[token_idx] is a URL token * and tokens[token_idx].StringValue() == "url" * * @param tokens to validate * @param tokenIdx index to start from * @param parsed ParsedCssUrl object to populate * @return the token_idx past the closing paren, or -1 if parsing fails. * @throws CssValidationException css validation exception */ public static int parseUrlFunction(@Nonnull final List tokens, int tokenIdx, @Nonnull final ParsedCssUrl parsed) throws CssValidationException { final Token token = tokens.get(tokenIdx); if (CssTokenUtil.getTokenType(token) != TokenType.FUNCTION_TOKEN) { throw new CssValidationException("Token at index is not a function token"); } if (!token.toString().equals("url(")) { throw new CssValidationException("Token value is not url"); } if (CssTokenUtil.getTokenType(tokens.get(tokens.size() - 1)) != TokenType.EOF_TOKEN) { throw new CssValidationException("Last token is not EOF token"); } CssTokenUtil.copyPosTo(token, parsed); tokenIdx++; // We've digested the function token above. // Safe: tokens ends w/ EOF_TOKEN. if (tokenIdx >= tokens.size()) { throw new CssValidationException("Index outside of tokens range"); } // Consume optional whitespace. while (CssTokenUtil.getTokenType(tokens.get(tokenIdx)) == TokenType.WHITESPACE) { tokenIdx++; // Safe: tokens ends w/ EOF_TOKEN. if (tokenIdx >= tokens.size()) { throw new CssValidationException("Index outside of tokens range"); } } // Consume URL. if (CssTokenUtil.getTokenType(tokens.get(tokenIdx)) != TokenType.STRING) { return -1; } parsed.setUtf8Url((tokens.get(tokenIdx).toString())); tokenIdx++; // Safe: tokens ends w/ EOF_TOKEN. if (tokenIdx >= tokens.size()) { throw new CssValidationException("token index outside of tokens range"); } // Consume optional whitespace. while (CssTokenUtil.getTokenType(tokens.get(tokenIdx)) == TokenType.WHITESPACE) { tokenIdx++; // Safe: tokens ends w/ EOF_TOKEN. if (tokenIdx >= tokens.size()) { throw new CssValidationException("token index outside of tokens range"); } } // Consume ')' if (CssTokenUtil.getTokenType(tokens.get(tokenIdx)) != TokenType.CLOSE_PAREN) { return -1; } return tokenIdx + 1; } /** * Parse inline style content into Declaration objects. * * @param tokenList the css content in a list of tokens. * @param errors output array for the errors. * @return Returns a array of Declaration objects. * @throws CssValidationException Css Validation Exception */ public static List parseInlineStyle(@Nonnull final List tokenList, @Nonnull final List errors) throws CssValidationException { final Canonicalizer canonicalizer = new Canonicalizer(new HashMap(), BlockType.PARSE_AS_DECLARATIONS); return canonicalizer.parseAListOfDeclarations(tokenList, errors); } /** * Helper method for ValidateAttributes. * * @param parsedAttrSpec parsed attribute spec * @param context the context * @param tagSpec tag spec * @param attrName attribute name * @param attrValue attribute value * @param result validate tag result * @throws IOException for css tokenize * @throws CssValidationException css validation exception */ public static void validateAttrCss( @Nonnull final ParsedAttrSpec parsedAttrSpec, @Nonnull final Context context, @Nonnull final ValidatorProtos.TagSpec tagSpec, @Nonnull final String attrName, @Nonnull final String attrValue, @Nonnull final ValidateTagResult result) throws IOException, CssValidationException { final int attrByteLen = ByteUtils.byteLength(attrValue); // Track the number of CSS bytes. If this tagspec is selected as the best // match, this count will be added to the overall document inline style byte // count for determining if that byte count has been exceeded. result.setInlineStyleCssBytes(attrByteLen); final List cssErrors = new ArrayList<>(); // The line/col we are passing in here is not the actual start point in the // text for the attribute string. It's the start point for the tag. This // means that any line/col values for tokens are also similarly offset // incorrectly. For error messages, this means we just use the line/col of // the tag instead of the token so as to minimize confusion. This could be // improved further. // TODO(https://github.com/ampproject/amphtml/issues/27507): Compute // attribute offsets for use in CSS error messages. final CssParser cssParser = new CssParser(attrValue, context.getLineCol().getLineNumber(), context.getLineCol().getColumnNumber(), cssErrors); final List tokenList = cssParser.tokenize(); final List declarations = parseInlineStyle(tokenList, cssErrors); for (final ErrorToken errorToken : cssErrors) { // Override the first parameter with the name of this style tag. final List params = errorToken.getParams(); // Override the first parameter with the name of this style tag. params.set(0, tagSpec.getTagName()); context.addError( errorToken.getCode(), errorToken.getLine(), errorToken.getCol(), params, /* url */ "", result.getValidationResult()); } // If there were errors parsing, exit from validating further. if (cssErrors.size() > 0) { return; } final ParsedDocCssSpec maybeDocCssSpec = context.matchingDocCssSpec(); if (maybeDocCssSpec != null) { // Determine if we've exceeded the maximum bytes per inline style // requirements. if (maybeDocCssSpec.getSpec().getMaxBytesPerInlineStyle() >= 0 && attrByteLen > maybeDocCssSpec.getSpec().getMaxBytesPerInlineStyle()) { List params = new ArrayList<>(); params.add(TagSpecUtils.getTagSpecName(tagSpec)); params.add(Integer.toString(attrByteLen)); params.add(Integer.toString(maybeDocCssSpec.getSpec().getMaxBytesPerInlineStyle())); if (maybeDocCssSpec.getSpec().getMaxBytesIsWarning()) { context.addWarning( ValidatorProtos.ValidationError.Code.INLINE_STYLE_TOO_LONG, context.getLineCol(), params, maybeDocCssSpec.getSpec().getSpecUrl(), result.getValidationResult()); //TODO - tagchowder doesn't seem to maintain duplicate attributes. //encounteredTag.dedupeAttrs(); } else { context.addError( ValidatorProtos.ValidationError.Code.INLINE_STYLE_TOO_LONG, context.getLineCol(), params, maybeDocCssSpec.getSpec().getSpecUrl(), result.getValidationResult()); } } // Loop over the declarations found in the document, verify that they are // in the allowed list for this DocCssSpec, and have allowed values if // relevant. for (final Declaration declaration : declarations) { final String firstIdent = declaration.firstIdent(); // Validate declarations only when they are not all allowed. if (!maybeDocCssSpec.getSpec().getAllowAllDeclarationInStyle()) { // Allowed declarations vary by context. SVG has its own set of CSS // declarations not supported generally in HTML. final ValidatorProtos.CssDeclaration cssDeclaration = parsedAttrSpec.getSpec().getValueDocSvgCss() ? maybeDocCssSpec.getCssDeclarationSvgByName(declaration.getName()) : maybeDocCssSpec.getCssDeclarationByName(declaration.getName()); // If there is no matching declaration in the rules, then this // declaration is not allowed. if (cssDeclaration == null) { final List params = new ArrayList<>(); params.add(declaration.getName()); params.add(attrName); params.add(getTagDescriptiveName(tagSpec)); context.addError( ValidatorProtos.ValidationError.Code.DISALLOWED_PROPERTY_IN_ATTR_VALUE, context.getLineCol(), params, context.getRules().getStylesSpecUrl(), result.getValidationResult()); // Don't emit additional errors for this declaration. continue; } else if (cssDeclaration.getValueCaseiList().size() > 0) { boolean hasValidValue = false; for (final String value : cssDeclaration.getValueCaseiList()) { if (firstIdent.toLowerCase().equals(value)) { hasValidValue = true; break; } } if (!hasValidValue) { // Declaration value not allowed. final List params = new ArrayList<>(); params.add(getTagDescriptiveName(tagSpec)); params.add(declaration.getName()); params.add(firstIdent); context.addError( ValidatorProtos.ValidationError.Code .CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE, context.getLineCol(), params, context.getRules().getStylesSpecUrl(), result.getValidationResult()); } } else if (cssDeclaration.hasValueRegexCasei()) { final Pattern valueRegex = context.getRules().getFullMatchCaseiRegex((cssDeclaration.getValueRegexCasei())); if (!valueRegex.matcher(firstIdent).matches()) { final List params = new ArrayList<>(); params.add(getTagDescriptiveName(tagSpec)); params.add(declaration.getName()); params.add(firstIdent); context.addError( ValidatorProtos.ValidationError.Code .CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE, context.getLineCol(), /* params */ params, context.getRules().getStylesSpecUrl(), result.getValidationResult()); } } } if (declaration.getName().contains("i-amphtml-")) { List params = new ArrayList<>(); params.add(declaration.getName()); params.add(attrName); params.add(TagSpecUtils.getTagSpecName(tagSpec)); context.addError( ValidatorProtos.ValidationError.Code.DISALLOWED_PROPERTY_IN_ATTR_VALUE, context.getLineCol().getLineNumber() + declaration.getLine(), context.getLineCol().getColumnNumber() + declaration.getCol(), params, "", result.getValidationResult()); // Don't emit additional errors for this declaration. continue; } if (!maybeDocCssSpec.getSpec().getAllowImportant()) { if (declaration.getImportant()) { // TODO(gregable): Use a more specific error message for // `!important` errors. List params = new ArrayList<>(); params.add(attrName); params.add(TagSpecUtils.getTagSpecName(tagSpec)); params.add("CSS !important"); context.addError( ValidatorProtos.ValidationError.Code.INVALID_ATTR_VALUE, context.getLineCol(), params, context.getRules().getStylesSpecUrl(), result.getValidationResult()); } } final List urlErrors = new ArrayList<>(); final List parsedUrls = new ArrayList<>(); extractUrlsFromDeclaration(declaration, parsedUrls, urlErrors); for (final ErrorToken errorToken : urlErrors) { // Override the first parameter with the name of the tag. List params = errorToken.getParams(); params.set(0, TagSpecUtils.getTagSpecName(tagSpec)); context.addError( errorToken.getCode(), context.getLineCol(), params, "", result.getValidationResult()); } if (urlErrors.size() > 0) { continue; } for (final ParsedCssUrl url : parsedUrls) { // Validate that the URL itself matches the spec. // Only image specs apply to inline styles. Fonts are only defined in // @font-face rules which we require a full stylesheet to define. if (maybeDocCssSpec.getSpec().hasImageUrlSpec()) { final UrlErrorInStylesheetAdapter adapter = new UrlErrorInStylesheetAdapter( context.getLineCol().getLineNumber(), context.getLineCol().getColumnNumber()); AttributeSpecUtils.validateUrlAndProtocol( maybeDocCssSpec.getImageUrlSpec(), adapter, context, url.getUtf8Url(), tagSpec, result.getValidationResult()); } // Subtract off URL lengths from doc-level inline style bytes, if // specified by the DocCssSpec. if (!maybeDocCssSpec.getSpec().getUrlBytesIncluded() && !UrlUtils.isDataUrl(url.getUtf8Url())) { result.setInlineStyleCssBytes(result.getInlineStyleCssBytes() - ByteUtils.byteLength(url.getUtf8Url())); } } } } } /** * Max number of allowed declarations. */ private static final int MAX_NUM_ALLOWED_DECLARATIONS = 5; /** * Enum describing how to parse the rules inside a CSS AT Rule. */ public enum BlockType { /** * Parse this simple block as a list of rules (Either Qualified Rules or AT Rules) */ PARSE_AS_RULES, /** * Parse this simple block as a list of declarations */ PARSE_AS_DECLARATIONS, /** * Ignore this simple block, do not parse. This is generally used * in conjunction with a later step emitting an error for this rule. */ PARSE_AS_IGNORE, } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy