| /* |
| * Copyright 2011 Google Inc. |
| * |
| * 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. |
| */ |
| package com.google.gwt.safecss.shared; |
| |
| import com.google.gwt.core.shared.GWT; |
| import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting; |
| import com.google.gwt.thirdparty.guava.common.base.Preconditions; |
| |
| import java.util.HashMap; |
| import java.util.Stack; |
| |
| /** |
| * SafeStyles utilities whose implementation differs between Development and |
| * Production Mode. |
| * |
| * <p> |
| * This class has a super-source peer that provides the Production Mode |
| * implementation. |
| * |
| * <p> |
| * Do not use this class - it is used for implementation only, and its methods |
| * may change in the future. |
| */ |
| public class SafeStylesHostedModeUtils { |
| |
| /** |
| * Name of system property that if set, enables checks in server-side code |
| * (even if assertions are disabled). |
| */ |
| public static final String FORCE_CHECK_VALID_STYLES = |
| "com.google.gwt.safecss.ForceCheckValidStyles"; |
| |
| private static boolean forceCheck; |
| |
| static { |
| setForceCheckValidStyleFromProperty(); |
| } |
| |
| /** |
| * Check if the specified style property name is valid. |
| * |
| * <p> |
| * NOTE: This method does <em>NOT</em> guarantee the safety of a style name. |
| * It looks for common errors, but does not check for every possible error. It |
| * is intended to help validate a string that the user has already asserted is |
| * safe. |
| * </p> |
| * |
| * @param name the name to check |
| * @return null if valid, an error string if not |
| * @see <a |
| * href="http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier">CSS |
| * 2.1 identifiers</a> |
| */ |
| @VisibleForTesting |
| public static String isValidStyleName(String name) { |
| // Empty name. |
| if (name == null || name.isEmpty()) { |
| return "Style property names cannot be null or empty"; |
| } |
| |
| // Starts with a digit, or a hyphen followed by a digit. |
| char firstReal = name.charAt(0); |
| if (firstReal == '-' && name.length() > 0) { |
| firstReal = name.charAt(1); |
| } |
| if (firstReal >= '0' && firstReal <= '9') { |
| return "Style property names cannot start with a digit or a hyphen followed by a digit: " |
| + name; |
| } |
| |
| // Starts with double hyphen. |
| if (name.startsWith("--")) { |
| return "Style property names cannot start with a double hyphen: " + name; |
| } |
| |
| /* |
| * Technically an escaped colon/semi-colon (eg. "property\;name") is valid, |
| * but in practice nobody uses it and its safer to exclude it. |
| */ |
| if (name.indexOf(';') >= 0) { |
| return "Style property names cannot contain a semi-colon: " + name; |
| } else if (name.indexOf(':') >= 0) { |
| return "Style property names cannot contain a colon: " + name; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Check if the specified style property value is valid. |
| * |
| * <p> |
| * NOTE: This method does <em>NOT</em> guarantee the safety of a style value. |
| * It looks for common errors, but does not check for every possible error. It |
| * is intended to help validate a string that the user has already asserted is |
| * safe. |
| * </p> |
| * |
| * @param value the value to check |
| * @return null if valid, an error string if not |
| * @see <a href="http://www.w3.org/TR/CSS21/syndata.html#declaration">CSS 2.1 |
| * declarations and properties</a> |
| */ |
| @VisibleForTesting |
| public static String isValidStyleValue(String value) { |
| // Empty value. |
| if (value == null || value.length() == 0) { |
| return "Style property values cannot be null or empty"; |
| } |
| |
| /* |
| * A value can contain any token, but parenthesis, brackets, braces, and |
| * single/double quotes must be paired. Semi-colons are only allowed in |
| * strings, such as in a url. |
| */ |
| |
| // Create a map of pairable 'open' characters to 'close' characters. |
| final HashMap<Character, Character> pairs = new HashMap<Character, Character>(); |
| pairs.put('(', ')'); |
| pairs.put('[', ']'); |
| pairs.put('{', '}'); |
| |
| // Iterate over the characters in the value. |
| final Stack<Character> pairsStack = new Stack<Character>(); |
| final Stack<Integer> pairsPos = new Stack<Integer>(); |
| Character inQuote = null; // The current quote character. |
| int inQuotePos = -1; |
| boolean inUrl = false; |
| boolean ignoreNext = false; |
| for (int i = 0; i < value.length(); i++) { |
| // This character was escaped. |
| if (ignoreNext) { |
| ignoreNext = false; |
| continue; |
| } |
| |
| char ch = value.charAt(i); |
| if (ch == '\\') { |
| // Ignore the character immediately after an escape character. |
| ignoreNext = true; |
| continue; |
| } |
| |
| if (inUrl) { |
| // Check for closing parenthesis. |
| if (ch == ')') { |
| // Close the URL. |
| inUrl = false; |
| } else if (ch == '(') { |
| // Parenthesis must be escaped within a url. |
| return "Unescaped parentheses within a url at index " + i + ": " + value; |
| } |
| |
| /* |
| * Almost all tokens are valid within a URL. |
| * |
| * |
| * TODO(jlabanca): Check for unescaped quotes and whitespace in URLs. |
| * Quotes and whitespace (other than the optional ones that wrap the |
| * URL) should be escaped. |
| */ |
| continue; |
| } |
| |
| if (inQuote != null) { |
| // Check for a matching end quote. |
| if (ch == inQuote) { |
| inQuote = null; |
| } |
| |
| // Else - still in quote, all tokens valid. |
| continue; |
| } |
| |
| if (ch == '"' || ch == '\'') { |
| // Found an open quote. |
| inQuote = ch; |
| inQuotePos = i; |
| } else if ((ch == 'u' || ch == 'U') && value.length() >= i + 4 |
| && value.substring(i, i + 4).equalsIgnoreCase("url(")) { |
| // Starting a URL. |
| inUrl = true; |
| i = i + 3; // Advance to the URL. |
| } else if (pairs.containsKey(ch)) { |
| // Opened a pairable. |
| pairsStack.push(ch); |
| pairsPos.push(i); |
| } else if (pairs.values().contains(ch)) { |
| // Closed a pairable. |
| if (pairsStack.isEmpty() || pairs.get(pairsStack.pop()) != ch) { |
| // Unmatched close token. |
| return "Style property value contains unpaired '" + ch + "' at index " + i + ": " + value; |
| } |
| pairsPos.pop(); |
| } else if (ch == ';') { |
| // Contains an unescaped semi-colon. |
| return "Style property values cannot contain a semi-colon (except within quotes): " + value; |
| } else if (ch == ':') { |
| // Contains an unescaped colon. |
| return "Style property values cannot contain a colon (except within quotes): " + value; |
| } |
| } |
| |
| // Unmatched open quote. |
| if (inQuote != null) { |
| return "Style property value contains unpaired open quote at index " + inQuotePos + ": " |
| + value; |
| } |
| |
| // Unterminated url. |
| if (inUrl) { |
| return "Style property value contains an unterminated url: " + value; |
| } |
| |
| // Unmatched open pairable. |
| if (!pairsStack.isEmpty()) { |
| char openToken = pairsStack.pop(); |
| int index = pairsPos.pop(); |
| return "Style property value contains unpaired '" + openToken + "' at index " + index + ": " |
| + value; |
| } |
| |
| // Ends in an escape character, |
| if (ignoreNext) { |
| return "Style property values cannot end in an escape character: " + value; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Checks if the provided string is a valid style property name. |
| * |
| * @param name the style name |
| * @see <a |
| * href="http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier">CSS |
| * 2.1 identifiers</a> |
| */ |
| public static void maybeCheckValidStyleName(String name) { |
| if (GWT.isClient() || forceCheck) { |
| String errorText = isValidStyleName(name); |
| Preconditions.checkArgument(errorText == null, errorText); |
| } else { |
| assert isValidStyleName(name) == null : isValidStyleName(name); |
| } |
| } |
| |
| /** |
| * Checks if the provided string is a valid style property value. |
| * |
| * @param value the style value |
| * @see <a href="http://www.w3.org/TR/CSS21/syndata.html#declaration">CSS 2.1 |
| * declarations and properties</a> |
| */ |
| public static void maybeCheckValidStyleValue(String value) { |
| if (GWT.isClient() || forceCheck) { |
| String errorText = isValidStyleValue(value); |
| Preconditions.checkArgument(errorText == null, errorText); |
| } else { |
| assert isValidStyleValue(value) == null : isValidStyleValue(value); |
| } |
| } |
| |
| /** |
| * Sets a global flag that controls whether or not |
| * {@link #maybeCheckValidStyleName(String)} and |
| * {@link #maybeCheckValidStyleValue(String)} should perform their checks in a |
| * server-side environment. |
| * |
| * @param check if true, perform server-side checks. |
| */ |
| public static void setForceCheckValidStyle(boolean check) { |
| forceCheck = check; |
| } |
| |
| /** |
| * Sets a global flag that controls whether or not |
| * {@link #maybeCheckValidStyleName(String)} and |
| * {@link #maybeCheckValidStyleValue(String)} should perform their checks in a |
| * server-side environment from the value of the |
| * {@value #FORCE_CHECK_VALID_STYLES} property. |
| */ |
| static void setForceCheckValidStyleFromProperty() { |
| forceCheck = System.getProperty(FORCE_CHECK_VALID_STYLES) != null; |
| } |
| } |