/*
* Copyright 2010 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.template.soy.data;
import com.google.common.base.Preconditions;
import com.google.common.html.types.SafeHtml;
import com.google.common.html.types.SafeHtmlProto;
import com.google.common.html.types.SafeHtmls;
import com.google.common.html.types.SafeScriptProto;
import com.google.common.html.types.SafeScripts;
import com.google.common.html.types.SafeStyleProto;
import com.google.common.html.types.SafeStyleSheet;
import com.google.common.html.types.SafeStyleSheetProto;
import com.google.common.html.types.SafeStyleSheets;
import com.google.common.html.types.SafeStyles;
import com.google.common.html.types.SafeUrlProto;
import com.google.common.html.types.SafeUrls;
import com.google.common.html.types.TrustedResourceUrlProto;
import com.google.common.html.types.TrustedResourceUrls;
import com.google.common.html.types.UncheckedConversions;
import com.google.template.soy.data.internal.RenderableThunk;
import com.google.template.soy.data.restricted.SoyString;
import java.io.IOException;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.concurrent.Immutable;
/**
* A chunk of sanitized content of a known kind, e.g. the output of an HTML sanitizer.
*
*/
@ParametersAreNonnullByDefault
@Immutable
public abstract class SanitizedContent extends SoyData implements SoyString {
/**
* Creates a SanitizedContent object.
*
* <p>Package-private. Ideally, if one is available, you should use an existing serializer,
* sanitizer, verifier, or extractor that returns SanitizedContent objects. Or, you can use
* UnsafeSanitizedContentOrdainer in this package, to make it clear that creating these objects
* from arbitrary content is risky unless you absolutely know the input is safe. See the comments
* in UnsafeSanitizedContentOrdainer for more recommendations.
*
* @param content A string of valid content with the given content kind.
* @param kind Describes the kind of string that content is.
* @param dir The content's direction; null if unknown and thus to be estimated when necessary.
*/
static SanitizedContent create(String content, ContentKind kind, @Nullable Dir dir) {
return new ConstantContent(content, kind, dir);
}
/**
* Creates a lazy SanitizedContent object.
*
* <p>Package-private. This is meant exclusively for use by the rendering infrastructure
*
* @param thunk A lazy thunk that renders the valid content.
* @param kind Describes the kind of string that content is.
* @param dir The content's direction; null if unknown and thus to be estimated when necessary.
*/
static SanitizedContent createLazy(RenderableThunk thunk, ContentKind kind, @Nullable Dir dir) {
return new LazyContent(thunk, kind, dir);
}
/** A kind of textual content. */
public enum ContentKind {
/**
* A snippet of HTML that does not start or end inside a tag, comment, entity, or DOCTYPE; and
* that does not contain any executable code (JS, {@code <object>}s, etc.) from a different
* trust domain.
*/
HTML,
/**
* Executable Javascript code or expression, safe for insertion in a script-tag or event handler
* context, known to be free of any attacker-controlled scripts. This can either be
* side-effect-free Javascript (such as JSON) or Javascript that entirely under Google's
* control.
*/
JS,
/** A properly encoded portion of a URI. */
URI,
/** Resource URIs used in script sources, stylesheets, etc which are not in attacker control. */
TRUSTED_RESOURCE_URI,
/** An attribute name and value, such as {@code dir="ltr"}. */
ATTRIBUTES,
// TODO(gboyer): Consider separating rules, properties, declarations, and
// values into separate types, but for simplicity, we'll treat explicitly
// blessed SanitizedContent as allowed in all of these contexts.
// TODO(user): Also consider splitting CSS into CSS and CSS_SHEET (corresponding to
// SafeStyle and SafeStyleSheet)
/** A CSS3 declaration, property, value or group of semicolon separated declarations. */
CSS,
/**
* Unsanitized plain-text content.
*
* <p>This is effectively the "null" entry of this enum, and is sometimes used to explicitly
* mark content that should never be used unescaped. Since any string is safe to use as text,
* being of ContentKind.TEXT makes no guarantees about its safety in any other context such as
* HTML.
*
* <p>In the soy type system, {@code TEXT} is equivalent to the string type.
*/
TEXT;
}
private final ContentKind contentKind;
private final Dir contentDir;
/**
* Private constructor to limit subclasses to this file. This is important to ensure that all
* implementations of this class are fully vetted by security.
*/
private SanitizedContent(ContentKind contentKind, @Nullable Dir contentDir) {
this.contentKind = contentKind;
this.contentDir = contentDir;
}
/** Returns a string of valid content with kind {@link #getContentKind}. */
public abstract String getContent();
/** Returns the kind of content. */
public ContentKind getContentKind() {
return contentKind;
}
/**
* Returns the content's direction; null indicates that the direction is unknown, and is to be
* estimated when necessary.
*/
@Nullable
public Dir getContentDirection() {
return contentDir;
}
@Override
public boolean coerceToBoolean() {
return getContent().length() > 0; // Consistent with StringData
}
@Override
public String coerceToString() {
return toString();
}
@Override
public String toString() {
return getContent();
}
/**
* Returns the string value.
*
* <p>In contexts where a string value is required, SanitizedContent is permitted.
*/
@Override
public String stringValue() {
return getContent();
}
@Override
public boolean equals(@Nullable Object other) {
// TODO(user): js uses reference equality, this uses content comparison
return other instanceof SanitizedContent
&& this.contentKind == ((SanitizedContent) other).contentKind
&& this.contentDir == ((SanitizedContent) other).contentDir
&& this.getContent().equals(((SanitizedContent) other).getContent());
}
@Override
public int hashCode() {
return getContent().hashCode() + 31 * contentKind.hashCode();
}
/**
* Converts a Soy {@link SanitizedContent} of kind HTML into a {@link SafeHtml}.
*
* @throws IllegalStateException if this SanitizedContent's content kind is not {@link
* ContentKind#HTML}.
*/
public SafeHtml toSafeHtml() {
Preconditions.checkState(
getContentKind() == ContentKind.HTML,
"toSafeHtml() only valid for SanitizedContent of kind HTML, is: %s",
getContentKind());
return UncheckedConversions.safeHtmlFromStringKnownToSatisfyTypeContract(getContent());
}
/**
* Converts a Soy {@link SanitizedContent} of kind HTML into a {@link SafeHtmlProto}.
*
* @throws IllegalStateException if this SanitizedContent's content kind is not {@link
* ContentKind#HTML}.
*/
public SafeHtmlProto toSafeHtmlProto() {
Preconditions.checkState(
getContentKind() == ContentKind.HTML,
"toSafeHtmlProto() only valid for SanitizedContent of kind HTML, is: %s",
getContentKind());
return SafeHtmls.toProto(
UncheckedConversions.safeHtmlFromStringKnownToSatisfyTypeContract(getContent()));
}
/**
* Converts a Soy {@link SanitizedContent} of kind JS into a {@link SafeScriptProto}.
*
* @throws IllegalStateException if this SanitizedContent's content kind is not {@link
* ContentKind#JS}.
*/
public SafeScriptProto toSafeScriptProto() {
Preconditions.checkState(
getContentKind() == ContentKind.JS,
"toSafeScriptProto() only valid for SanitizedContent of kind JS, is: %s",
getContentKind());
return SafeScripts.toProto(
UncheckedConversions.safeScriptFromStringKnownToSatisfyTypeContract(getContent()));
}
/**
* Converts a Soy {@link SanitizedContent} of kind CSS into a {@link SafeStyleProto}.
*
* @throws IllegalStateException if this SanitizedContent's content kind is not {@link
* ContentKind#CSS}.
*/
public SafeStyleProto toSafeStyleProto() {
Preconditions.checkState(
getContentKind() == ContentKind.CSS,
"toSafeStyleProto() only valid for SanitizedContent of kind CSS, is: %s",
getContentKind());
// Sanity check: Try to prevent accidental misuse when this is a full stylesheet rather than a
// declaration list.
// The error may trigger incorrectly if the content contains curly brackets inside comments or
// quoted strings.
//
// This is a best-effort attempt to preserve SafeStyle's semantical guarantees.
Preconditions.checkState(
!getContent().contains("{"),
"Calling toSafeStyleProto() with content that doesn't look like CSS declarations. "
+ "Consider using toSafeStyleSheetProto().");
return SafeStyles.toProto(
UncheckedConversions.safeStyleFromStringKnownToSatisfyTypeContract(getContent()));
}
/**
* Converts a Soy {@link SanitizedContent} of kind CSS into a {@link SafeStyleSheet}.
*
* <p>To ensure correct behavior and usage, the SanitizedContent object should fulfill the
* contract of SafeStyleSheet - the CSS content should represent the top-level content of a style
* element within HTML.
*
* @throws IllegalStateException if this SanitizedContent's content kind is not {@link
* ContentKind#CSS}.
*/
public SafeStyleSheet toSafeStyleSheet() {
Preconditions.checkState(
getContentKind() == ContentKind.CSS,
"toSafeStyleSheet() only valid for SanitizedContent of kind CSS, is: %s",
getContentKind());
// Sanity check: Try to prevent accidental misuse when this is not really a stylesheet but
// instead just a declaration list (i.e. a SafeStyle). This does fail to accept a stylesheet
// that is only a comment or only @imports; if you have a legitimate reason for this, it would
// be fine to make this more sophisticated, but in practice it's unlikely and keeping this check
// simple helps ensure it is fast. Note that this isn't a true security boundary, but a
// best-effort attempt to preserve SafeStyleSheet's semantical guarantees.
Preconditions.checkState(
getContent().isEmpty() || getContent().indexOf('{') > 0,
"Calling toSafeStyleSheet() with content that doesn't look like a stylesheet");
return UncheckedConversions.safeStyleSheetFromStringKnownToSatisfyTypeContract(getContent());
}
/**
* Converts a Soy {@link SanitizedContent} of kind CSS into a {@link SafeStyleSheetProto}.
*
* <p>To ensure correct behavior and usage, the SanitizedContent object should fulfill the
* contract of SafeStyleSheet - the CSS content should represent the top-level content of a style
* element within HTML.
*
* @throws IllegalStateException if this SanitizedContent's content kind is not {@link
* ContentKind#CSS}.
*/
public SafeStyleSheetProto toSafeStyleSheetProto() {
Preconditions.checkState(
getContentKind() == ContentKind.CSS,
"toSafeStyleSheetProto() only valid for SanitizedContent of kind CSS, is: %s",
getContentKind());
return SafeStyleSheets.toProto(toSafeStyleSheet());
}
/**
* Converts a Soy {@link SanitizedContent} of kind URI into a {@link SafeUrlProto}.
*
* @throws IllegalStateException if this SanitizedContent's content kind is not {@link
* ContentKind#URI}.
*/
public SafeUrlProto toSafeUrlProto() {
Preconditions.checkState(
getContentKind() == ContentKind.URI,
"toSafeUrlProto() only valid for SanitizedContent of kind URI, is: %s",
getContentKind());
return SafeUrls.toProto(
UncheckedConversions.safeUrlFromStringKnownToSatisfyTypeContract(getContent()));
}
/**
* Converts a Soy {@link SanitizedContent} of kind TRUSTED_RESOURCE_URI into a {@link
* TrustedResourceUrlProto}.
*
* @throws IllegalStateException if this SanitizedContent's content kind is not {@link
* ContentKind#TRUSTED_RESOURCE_URI}.
*/
public TrustedResourceUrlProto toTrustedResourceUrlProto() {
Preconditions.checkState(
getContentKind() == ContentKind.TRUSTED_RESOURCE_URI,
"toTrustedResourceUrlProto() only valid for SanitizedContent of kind TRUSTED_RESOURCE_URI, "
+ "is: %s",
getContentKind());
return TrustedResourceUrls.toProto(
UncheckedConversions.trustedResourceUrlFromStringKnownToSatisfyTypeContract(getContent()));
}
private static final class ConstantContent extends SanitizedContent {
final String content;
ConstantContent(String content, ContentKind contentKind, @Nullable Dir contentDir) {
super(contentKind, contentDir);
this.content = content;
}
@Override
public void render(Appendable appendable) throws IOException {
appendable.append(content);
}
@Override
public String getContent() {
return content;
}
}
private static final class LazyContent extends SanitizedContent {
// N.B. This is nearly identical to StringData.LazyString. When changing this you
// probably need to change that also.
final RenderableThunk thunk;
LazyContent(RenderableThunk thunk, ContentKind contentKind, @Nullable Dir contentDir) {
super(contentKind, contentDir);
this.thunk = thunk;
}
@Override
public void render(Appendable appendable) throws IOException {
thunk.render(appendable);
}
@Override
public String getContent() {
return thunk.renderAsString();
}
}
}