blob: a9ddbbbaa6043e1cb64c8926ed8e1c23eba8a6ef [file] [log] [blame]
/*
* 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;
}
}