/* * Copyright 2013 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.basicdirectives; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.ContiguousSet; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.google.template.soy.data.SoyValue; import com.google.template.soy.internal.targetexpr.TargetExpr; import com.google.template.soy.jssrc.restricted.JsExpr; import com.google.template.soy.jssrc.restricted.SoyLibraryAssistedJsSrcPrintDirective; import com.google.template.soy.pysrc.restricted.PyExpr; import com.google.template.soy.pysrc.restricted.SoyPySrcPrintDirective; import com.google.template.soy.shared.restricted.Sanitizers; import com.google.template.soy.shared.restricted.SoyJavaPrintDirective; import com.google.template.soy.shared.restricted.SoyPurePrintDirective; import com.google.template.soy.shared.restricted.TagWhitelist.OptionalSafeTag; import java.util.List; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; /** * Implements the |cleanHtml directive, which removes all but a small, safe subset of HTML from its * input. Note that all attributes are removed except dir for directionality. * * <p>It may take a variable number of arguments which are additional tags to be considered safe, * from {@link OptionalSafeTag}. * * <p>Note that this directive is not autoescape canceling, and can thus be used in strict * templates. The directive returns its result as an object of type SanitizedContent of kind HTML. */ @Singleton @SoyPurePrintDirective final class CleanHtmlDirective implements SoyJavaPrintDirective, SoyLibraryAssistedJsSrcPrintDirective, SoyPySrcPrintDirective { private static final Joiner ARG_JOINER = Joiner.on(", "); // The directive may be called with a variable number of arguments indicating additional tags to // be considered safe, so we need to support each args size up to the number of possible // additional tags. private static final Set<Integer> VALID_ARGS_SIZES = ContiguousSet.create( Range.closed(0, OptionalSafeTag.values().length), DiscreteDomain.integers()); @Inject public CleanHtmlDirective() {} @Override public String getName() { return "|cleanHtml"; } @Override public final Set<Integer> getValidArgsSizes() { return VALID_ARGS_SIZES; } @Override public boolean shouldCancelAutoescape() { return false; } @Override public SoyValue applyForJava(SoyValue value, List<SoyValue> args) { ImmutableSet<OptionalSafeTag> optionalSafeTags = FluentIterable.from(args) .transform(SOY_VALUE_TO_STRING) // FROM_TAG_NAME throws IllegalArgumentException for invalid OptionalSafeTags. .transform(OptionalSafeTag.FROM_TAG_NAME) .toSet(); return Sanitizers.cleanHtml(value, optionalSafeTags); } @Override public JsExpr applyForJsSrc(JsExpr value, List<JsExpr> args) { String optionalSafeTagsArg = generateOptionalSafeTagsArg(args); return new JsExpr( "soy.$$cleanHtml(" + value.getText() + optionalSafeTagsArg + ")", Integer.MAX_VALUE); } @Override public ImmutableSet<String> getRequiredJsLibNames() { return ImmutableSet.of("soy"); } @Override public PyExpr applyForPySrc(PyExpr value, List<PyExpr> args) { String optionalSafeTagsArg = generateOptionalSafeTagsArg(args); return new PyExpr( "sanitize.clean_html(" + value.getText() + optionalSafeTagsArg + ")", Integer.MAX_VALUE); } /** * Converts a list of TargetExpr's into a list of safe tags as an argument for the supported * backends. This will iterate over the expressions, ensure they're valid safe tags, and convert * them into an array of Strings. * * <p>The generated output is valid for JS and Python. Any other languages should reevaluate if * they require changes. * * @param args A list of possible safe tags. * @return A string containing the safe tags argument. */ private String generateOptionalSafeTagsArg(List<? extends TargetExpr> args) { String optionalSafeTagsArg = ""; if (!args.isEmpty()) { // TODO(msamuel): Instead of parsing generated JS, we should have a CheckArgumentsPass that // allows directives and functions to examine their input expressions prior to compilation and // relay the input file and line number to the template author along with an error message. Iterable<String> optionalSafeTagExprs = Iterables.transform(args, TARGET_EXPR_TO_STRING); // Verify that all exprs are single-quoted valid OptionalSafeTags. for (String singleQuoted : optionalSafeTagExprs) { if (singleQuoted.length() < 2 || singleQuoted.charAt(0) != '\'' || singleQuoted.charAt(singleQuoted.length() - 1) != '\'') { throw new IllegalArgumentException( String.format( "The cleanHtml directive expects arguments to be tag name string " + "literals, such as 'span'. Encountered: %s", singleQuoted)); } String tagName = singleQuoted.substring(1, singleQuoted.length() - 1); OptionalSafeTag.fromTagName(tagName); // throws if invalid } optionalSafeTagsArg = ", [" + ARG_JOINER.join(optionalSafeTagExprs) + "]"; } return optionalSafeTagsArg; } private static final Function<SoyValue, String> SOY_VALUE_TO_STRING = new Function<SoyValue, String>() { @Override public String apply(SoyValue soyValue) { return soyValue.stringValue(); } }; private static final Function<TargetExpr, String> TARGET_EXPR_TO_STRING = new Function<TargetExpr, String>() { @Override public String apply(TargetExpr expr) { return expr.getText(); } }; }