com.google.gwt.safehtml.rebind.SafeHtmlTemplatesImplMethodCreator Maven / Gradle / Ivy
/*
* Copyright 2009 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.safehtml.rebind;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.i18n.rebind.AbstractResource.ResourceList;
import com.google.gwt.i18n.shared.GwtLocale;
import com.google.gwt.safecss.shared.SafeStyles;
import com.google.gwt.safehtml.client.SafeHtmlTemplates.Template;
import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.HtmlContext;
import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.LiteralChunk;
import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.ParameterChunk;
import com.google.gwt.safehtml.rebind.ParsedHtmlTemplate.TemplateChunk;
import com.google.gwt.safehtml.shared.OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.safehtml.shared.UriUtils;
import com.google.gwt.user.rebind.AbstractGeneratorClassCreator;
import com.google.gwt.user.rebind.AbstractMethodCreator;
/**
* Method body code generator for implementations of
* {@link com.google.gwt.safehtml.client.SafeHtmlTemplates}.
*/
public class SafeHtmlTemplatesImplMethodCreator extends AbstractMethodCreator {
/**
* Fully-qualified class name of the {@link String} class.
*/
private static final String JAVA_LANG_STRING_FQCN = String.class.getName();
/**
* Simple class name of the {@link SafeStyles} interface.
*/
private static final String SAFE_STYLES_CN = SafeStyles.class.getSimpleName();
/**
* Fully-qualified class name of the {@link SafeStyles} interface.
*/
private static final String SAFE_STYLES_FQCN = SafeStyles.class.getName();
/**
* Simple class name of the {@link SafeHtml} interface.
*/
private static final String SAFE_HTML_CN = SafeHtml.class.getSimpleName();
/**
* Fully-qualified class name of the {@link SafeHtml} interface.
*/
private static final String SAFE_HTML_FQCN = SafeHtml.class.getName();
/**
* Fully-qualified class name of the StringBlessedAsSafeHtml class.
*/
private static final String BLESSED_STRING_FQCN =
OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml.class.getName();
/**
* Fully-qualified class name of the {@link SafeHtmlUtils} class.
*/
private static final String ESCAPE_UTILS_FQCN = SafeHtmlUtils.class.getName();
/**
* Fully-qualified class name of the {@link UriUtils} class.
*/
private static final String URI_UTILS_FQCN = UriUtils.class.getName();
public SafeHtmlTemplatesImplMethodCreator(
AbstractGeneratorClassCreator classCreator) {
super(classCreator);
}
/**
* {@inheritDoc}
*/
@Override
public void createMethodFor(TreeLogger logger, JMethod targetMethod,
String key, ResourceList resourceList, GwtLocale locale)
throws UnableToCompleteException {
if (!targetMethod.getReturnType().getQualifiedSourceName().equals(
SafeHtmlTemplatesImplMethodCreator.SAFE_HTML_FQCN)) {
throw error(logger, "All methods in interfaces extending "
+ "SafeHtmlTemplates must have a return type of "
+ SafeHtmlTemplatesImplMethodCreator.SAFE_HTML_FQCN + ".");
}
Template templateAnnotation = targetMethod.getAnnotation(Template.class);
if (templateAnnotation == null) {
throw error(logger, "Required annotation @Template not present "
+ "on interface method " + targetMethod.toString());
}
String template = templateAnnotation.value();
JParameter[] params = targetMethod.getParameters();
emitMethodBodyFromTemplate(logger, template, params);
}
/**
* Emits an expression corresponding to a template variable in "attribute"
* context.
*
* The expression emitted applies appropriate escaping and/or sanitization
* to the parameter's value depending the Java type of the corresponding
* template method parameter:
*
*
* - If the parameter is of type {@link SafeStyles}, it is converted to a
* string using {@link SafeStyles#asString()}.
*
- Otherwise, if the parameter is not of type {@link String}, it is first
* converted to {@link String}.
*
- If the template parameter occurs at the start of a URI-valued attribute
* within the template, it is sanitized to ensure that it is safe in this
* context. This is done by passing the value through
* {@link UriUtils#sanitizeUri(String)}.
*
- The result is then HTML-escaped by passing it through
* {@link SafeHtmlUtils#htmlEscape(String)}.
*
*
* Note: Template method parameters of type {@link SafeHtml} are
* not treated specially in an attribute context, and will be HTML-
* escaped like regular strings. This is because {@link SafeHtml} values can
* contain non-escaped HTML markup, which is not valid within attributes.
*
* @param logger the logger to log failures to
* @param htmlContext the HTML context in which the corresponding template
* variable occurs in
* @param formalParameterName the name of the template method's formal
* parameter corresponding to the expression being emitted
* @param parameterType the Java type of the corresponding template method's
* parameter
*/
private void emitAttributeContextParameterExpression(TreeLogger logger,
HtmlContext htmlContext, String formalParameterName,
JType parameterType) {
/*
* Build up the expression from the "inside out", i.e. start with the formal
* parameter, convert to string if necessary, then wrap in validators if
* necessary, and finally HTML-escape if necessary.
*/
String expression = formalParameterName;
if (isSafeStyles(parameterType)) {
// SafeCss is safe in a CSS context, so we can use its string (but we still
// escape it).
expression = expression + ".asString()";
} else if (!JAVA_LANG_STRING_FQCN.equals(parameterType.getQualifiedSourceName())) {
// The parameter's value must be explicitly converted to String unless it
// is already of that type.
expression = "String.valueOf(" + expression + ")";
}
if ((htmlContext.getType() == HtmlContext.Type.URL_START)) {
expression = URI_UTILS_FQCN + ".sanitizeUri(" + expression + ")";
}
// TODO(xtof): Handle EscapedString subtype of SafeHtml, once it's been
// introduced.
expression = ESCAPE_UTILS_FQCN + ".htmlEscape(" + expression + ")";
print(expression);
}
/**
* Generates code that renders the provided HTML template into an instance
* of the {@link SafeHtml} type.
*
* The template is parsed as a HTML template (see
* {@link HtmlTemplateParser}). From the template's parsed form, code is
* generated that, when executed, will emit an instantiation of the template.
* The generated code appropriately escapes and/or sanitizes template
* parameters such that evaluating the emitted string as HTML in a browser
* will not result in script execution.
*
*
As such, strings emitted from generated template methods satisfy the
* type contract of the {@link SafeHtml} type, and can therefore be returned
* wrapped as {@link SafeHtml}.
*
* @param logger the logger to log failures to
* @param template the (X)HTML template to generate code for
* @param params the parameters of the corresponding template method
* @throws UnableToCompleteException if an error occurred that prevented
* code generation for the template
*/
private void emitMethodBodyFromTemplate(TreeLogger logger, String template,
JParameter[] params) throws UnableToCompleteException {
println("StringBuilder sb = new java.lang.StringBuilder();");
HtmlTemplateParser parser = new HtmlTemplateParser(logger);
parser.parseTemplate(template);
for (TemplateChunk chunk : parser.getParsedTemplate().getChunks()) {
if (chunk.getKind() == TemplateChunk.Kind.LITERAL) {
emitStringLiteral(((LiteralChunk) chunk).getLiteral());
} else if (chunk.getKind() == TemplateChunk.Kind.PARAMETER) {
ParameterChunk parameterChunk = (ParameterChunk) chunk;
int formalParameterIndex = parameterChunk.getParameterIndex();
if (formalParameterIndex < 0 || formalParameterIndex >= params.length) {
throw error(logger, "Argument " + formalParameterIndex
+ " beyond range of arguments: " + template);
}
String formalParameterName = "arg" + formalParameterIndex;
JType paramType = params[formalParameterIndex].getType();
emitParameterExpression(logger, parameterChunk.getContext(),
formalParameterName, paramType);
} else {
throw error(logger, "Unexpected chunk kind in parsed template "
+ template);
}
}
outdent();
outdent();
println("return new " + BLESSED_STRING_FQCN + "(sb.toString());");
}
/**
* Emits an expression corresponding to a template parameter.
*
*
* The expression emitted applies appropriate escaping/sanitization to the
* parameter's value, depending on the parameter's HTML context, and the Java
* type of the corresponding template method parameter.
*
* @param logger the logger to log failures to
* @param htmlContext the HTML context in which the corresponding template
* variable occurs in
* @param formalParameterName the name of the template method's formal
* parameter corresponding to the expression being emitted
* @param parameterType the Java type of the corresponding template method's
* parameter
* @throws UnableToCompleteException if the parameterType is not valid for the
* htmlContext
*/
private void emitParameterExpression(TreeLogger logger, HtmlContext htmlContext,
String formalParameterName, JType parameterType) throws UnableToCompleteException {
/*
* Verify that the parameter type is used in the correct context. Safe
* expressions are only safe in specific contexts.
*/
HtmlContext.Type contextType = htmlContext.getType();
if (isSafeHtml(parameterType) && HtmlContext.Type.TEXT != contextType) {
/*
* SafeHtml used in a non-text context. SafeHtml is escaped for a text
* context. In a non-text context, the string is not guaranteed to be
* safe.
*/
throw error(logger, SAFE_HTML_CN + " used in a non-text context. Did you mean to use "
+ JAVA_LANG_STRING_FQCN + " or " + SAFE_STYLES_CN + " instead?");
} else if (isSafeStyles(parameterType) && HtmlContext.Type.CSS_ATTRIBUTE_START != contextType) {
if (HtmlContext.Type.CSS_ATTRIBUTE == contextType) {
// SafeStyles can only be used at the start of a CSS attribute.
throw error(logger, SAFE_STYLES_CN + " cannot be used in the middle of a CSS attribute. "
+ "It must be used at the start a CSS attribute.");
} else {
/*
* SafeStyles used in a non-css attribute context. SafeStyles is only
* safe in a CSS attribute context. We could treat it as a normal
* parameter and escape the string value of the parameter, but it almost
* definitely isn't what the developer intended to do.
*/
throw error(logger, SAFE_STYLES_CN
+ " used in a non-CSS attribute context. Did you mean to use " + JAVA_LANG_STRING_FQCN
+ " or " + SAFE_HTML_CN + " instead?");
}
}
print("sb.append(");
switch (contextType) {
case CSS:
/*
* TODO(jlabanca): Handle CSS in a text context.
*
* The stream parser does not parse CSS; we could however improve safety
* via sub-formats that specify the in-css context.
*
* SafeStyles is safe in a CSS context when used inside of a CSS style
* rule, but they are not always safe. We could implement SafeCssRules,
* which would consist of SafeStyles inside of a CSS style rules found
* in a style tag.
*/
logger.log(TreeLogger.WARN, "Template with variable in CSS context: "
+ "The template code generator cannot guarantee HTML-safety of "
+ "the template -- please inspect manually");
emitTextContextParameterExpression(formalParameterName, parameterType);
break;
case TEXT:
emitTextContextParameterExpression(formalParameterName, parameterType);
break;
case CSS_ATTRIBUTE:
case CSS_ATTRIBUTE_START:
/*
* We already checked if the user tried to use SafeStyles in an invalid
* (non-CSS_ATTRIBUTE) context, but now we check if the user could have
* used SafeStyles in the current context.
*/
if (!isSafeStyles(parameterType)) {
// Warn against using unsafe parameters in a CSS attribute context.
logger.log(TreeLogger.WARN,
"Template with variable in CSS attribute context: The template code generator cannot"
+ " guarantee HTML-safety of the template -- please inspect manually or use "
+ SAFE_STYLES_CN + " to specify arguments in a CSS attribute context");
}
emitAttributeContextParameterExpression(logger, htmlContext,
formalParameterName, parameterType);
break;
case URL_START:
case ATTRIBUTE_VALUE:
emitAttributeContextParameterExpression(logger, htmlContext,
formalParameterName, parameterType);
break;
default:
throw error(logger, "unknown HTML context for formal template parameter "
+ formalParameterName + ": " + htmlContext);
}
println(");");
}
/**
* Emits a string literal.
*
* @param str the {@link String} to emit as a literal
*/
private void emitStringLiteral(String str) {
print("sb.append(");
print(wrap(str));
println(");");
}
/**
* Emits an expression corresponding to a template variable in "inner text"
* context.
*
*
The expression emitted applies appropriate escaping to the parameter's
* value depending the Java type of the corresponding template method
* parameter:
*
*
* - If the parameter is of a primitive (e.g., numeric, boolean) type, or
* of type {@link SafeHtml}, it is emitted as is, without escaping.
*
- Otherwise, an expression that passes the paramter's value through
* {@link SafeHtmlUtils#htmlEscape(String)} is emitted.
*
*
* @param formalParameterName the name of the template method's formal
* parameter corresponding to the expression being emitted
* @param parameterType the Java type of the corresponding template method's
* parameter
*/
private void emitTextContextParameterExpression(String formalParameterName,
JType parameterType) {
boolean parameterIsPrimitiveType = (parameterType.isPrimitive() != null);
boolean parameterIsNotStringTyped = !(JAVA_LANG_STRING_FQCN.equals(
parameterType.getQualifiedSourceName()));
if (SAFE_HTML_FQCN.equals(parameterType.getQualifiedSourceName())) {
// The parameter is of type SafeHtml and its wrapped string can
// therefore be emitted safely without escaping.
print(formalParameterName + ".asString()");
} else if (parameterIsPrimitiveType) {
// The string representations of primitive types never contain HTML
// special characters and can therefore be emitted without escaping.
print(formalParameterName);
} else {
// The parameter is of some other type, and its value must be HTML
// escaped. Furthermore, unless the parameter's type is {@link String},
// it must be explicitly converted to {@link String}.
print(ESCAPE_UTILS_FQCN + ".htmlEscape(");
if (parameterIsNotStringTyped) {
print("String.valueOf(");
}
print(formalParameterName);
if (parameterIsNotStringTyped) {
print(")");
}
print(")");
}
}
/**
* Check if the specified parameter type represents a {@link SafeHtml}.
*
* @param parameterType the Java parameter type
* @return true if the type represents a {@link SafeHtml}
*/
private boolean isSafeHtml(JType parameterType) {
return parameterType.getQualifiedSourceName().equals(SAFE_HTML_FQCN);
}
/**
* Check if the specified parameter type represents a {@link SafeStyles}.
*
* @param parameterType the Java parameter type
* @return true if the type represents a {@link SafeStyles}
*/
private boolean isSafeStyles(JType parameterType) {
return parameterType.getQualifiedSourceName().equals(SAFE_STYLES_FQCN);
}
}