/*
* 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.parsepasses.contextautoesc;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.data.SanitizedContent;
import com.google.template.soy.data.SanitizedContentOperator;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.ErrorReporter.Checkpoint;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.shared.restricted.SoyPrintDirective;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.AutoescapeMode;
import com.google.template.soy.soytree.CallParamContentNode;
import com.google.template.soy.soytree.LetContentNode;
import com.google.template.soy.soytree.SoyFileNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyNode.RenderUnitNode;
import com.google.template.soy.soytree.TemplateBasicNode;
import com.google.template.soy.soytree.TemplateDelegateNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.TemplateRegistry;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
/**
* Inserts directives into print commands by looking at the context in which a print appears, and
* derives templates and rewrites calls so that each template is entered only in contexts consistent
* with its escaping conventions.
*
* <p>E.g. it will {@link ContextualAutoescaper#rewrite rewrite} <xmp class=prettyprint> {template
* example autoescape="contextual"}
*
* <p>Hello, {$world}! {/template} </xmp> to <xmp class=prettyprint> {template example
* autoescape="contextual"}
*
* <p>Hello, {$world |escapeHtml}! {/template} </xmp>
*
*/
public final class ContextualAutoescaper {
@VisibleForTesting
static final String AUTOESCAPE_ERROR_PREFIX =
"Invalid or ambiguous syntax prevents Soy from escaping this template correctly:\n";
private static final SoyErrorKind AUTOESCAPE_ERROR =
SoyErrorKind.of(AUTOESCAPE_ERROR_PREFIX + "{0}");
/**
* Soy directives that cancel autoescaping (see {@link
* SoyPrintDirective#shouldCancelAutoescape()}).
*/
private final ImmutableSet<String> autoescapeCancellingDirectives;
/** Maps print directive names to the content kinds they consume and produce. */
private final Map<String, SanitizedContent.ContentKind> sanitizedContentOperators;
/** The conclusions drawn by the last {@link #rewrite}. */
private Inferences inferences;
/** Raw text nodes sliced by context. */
private ImmutableList<SlicedRawTextNode> slicedRawTextNodes;
/**
* This injected ctor provides a blank constructor that is filled, in normal compiler operation,
* with the core and basic directives defined in com.google.template.soy.{basic,core}directives,
* and any custom directives supplied on the command line.
*
* @param soyDirectivesMap Map of all SoyPrintDirectives (name to directive) such that {@code
* soyDirectivesMap.get(key).getName().equals(key)} for all key in {@code
* soyDirectivesMap.keySet()}.
*/
@Inject
ContextualAutoescaper(final ImmutableMap<String, ? extends SoyPrintDirective> soyDirectivesMap) {
// Compute the set of directives that are escaping directives.
this(
ImmutableSet.copyOf(
Collections2.filter(
soyDirectivesMap.keySet(),
new Predicate<String>() {
@Override
public boolean apply(String directiveName) {
return soyDirectivesMap.get(directiveName).shouldCancelAutoescape();
}
})),
makeOperatorKindMap(soyDirectivesMap));
}
/**
* @param autoescapeCancellingDirectives The Soy directives that cancel autoescaping (see {@link
* SoyPrintDirective#shouldCancelAutoescape()}).
* @param sanitizedContentOperators Maps print directive names to the content kinds they consume
* and produce.
*/
public ContextualAutoescaper(
Iterable<String> autoescapeCancellingDirectives,
Map<String, SanitizedContent.ContentKind> sanitizedContentOperators) {
this.autoescapeCancellingDirectives = ImmutableSet.copyOf(autoescapeCancellingDirectives);
this.sanitizedContentOperators = ImmutableMap.copyOf(sanitizedContentOperators);
}
/**
* Rewrites the given Soy files so that dynamic output is properly escaped according to the
* context in which it appears.
*
* @param fileSet Modified in place.
* @return Extra templates which were derived from templates under fileSet and which must be
* compiled with fileSet to produce a correct output. See {@link DerivedTemplateUtils} for an
* explanation of these.
*/
public List<TemplateNode> rewrite(
SoyFileSetNode fileSet, TemplateRegistry registry, ErrorReporter errorReporter) {
// Do preliminary sanity checks. Bail if they don't succeed, since errors may void
// the contextual autoescaper's preconditions.
Checkpoint checkpoint = errorReporter.checkpoint();
new CheckEscapingSanityVisitor(registry, errorReporter).exec(fileSet);
if (errorReporter.errorsSince(checkpoint)) {
return ImmutableList.of();
}
// Defensively copy so our loops below hold.
List<SoyFileNode> files = ImmutableList.copyOf(fileSet.getChildren());
Map<String, ImmutableList<TemplateNode>> templatesByName = findTemplates(files);
// Inferences collects all the typing decisions we make, templates we derive, and escaping modes
// we choose.
Inferences inferences =
new Inferences(
autoescapeCancellingDirectives, fileSet.getNodeIdGenerator(), templatesByName);
ImmutableList.Builder<SlicedRawTextNode> slicedRawTextNodesBuilder = ImmutableList.builder();
Collection<TemplateNode> allTemplates = inferences.getAllTemplates();
TemplateCallGraph callGraph = new TemplateCallGraph(templatesByName);
// Generate a call graph, creating a dummy root that calls all non-private template in
// Context.PCDATA, and then type the minimal ancestor set needed to reach all contextual
// templates whether private or not.
// This should have the effect of being a NOP when there are no contextual templates, will type
// all contextual templates, and will not barf on private templates that might be declared
// autoescape="false" because they do funky things that are provably safe by human reason but
// not by this algorithm.
Collection<TemplateNode> thatRequireInference =
Collections2.filter(allTemplates, REQUIRES_INFERENCE);
Set<TemplateNode> templateNodesToType = callGraph.callersOf(thatRequireInference);
templateNodesToType.addAll(thatRequireInference);
Set<SourceLocation> errorLocations = new HashSet<>();
for (TemplateNode templateNode : templateNodesToType) {
try {
// In strict mode, the author specifies the kind of SanitizedContent to produce, and thus
// the context in which to escape.
Context startContext =
(templateNode.getContentKind() != null)
? Context.getStartContextForContentKind(templateNode.getContentKind())
: Context.HTML_PCDATA;
InferenceEngine.inferTemplateEndContext(
templateNode,
startContext,
inferences,
autoescapeCancellingDirectives,
slicedRawTextNodesBuilder,
errorReporter);
} catch (SoyAutoescapeException e) {
reportError(errorReporter, errorLocations, e);
}
}
if (!errorLocations.isEmpty()) {
// Bail out early, since future passes won't succeed and may throw precondition errors.
return ImmutableList.<TemplateNode>of();
}
// Store inferences so that after processing, clients can access the output contexts for
// templates.
this.inferences = inferences;
// Store context boundaries so that later passes can make use of element/attribute boundaries.
this.slicedRawTextNodes = slicedRawTextNodesBuilder.build();
runVisitorOnAllTemplatesIncludingNewOnes(
inferences, new NonContextualTypedRenderUnitNodesVisitor(errorReporter));
// Now that we know we don't fail with exceptions, apply the changes to the given files.
List<TemplateNode> extraTemplates =
new Rewriter(inferences, sanitizedContentOperators, errorReporter).rewrite(fileSet);
runVisitorOnAllTemplatesIncludingNewOnes(
inferences,
new PerformDeprecatedNonContextualAutoescapeVisitor(
autoescapeCancellingDirectives, errorReporter, fileSet.getNodeIdGenerator()));
return extraTemplates;
}
/**
* Runs a visitor on all templates, including newly-generated ones.
*
* <p>After running the inference engine, new re-contextualized templates have been generated, but
* haven't been folded back into the SoyFileSetNode (which happens in the SoyFileSet monster
* class).
*
* <p>Note this is true even for non-contextual templates. If a non-contextual template eventually
* is called by a contextual one, the call subtree will be rewritten for the alternate context
* (even though they remain non-contextually autoescaped).
*/
private void runVisitorOnAllTemplatesIncludingNewOnes(
Inferences inferences, AbstractSoyNodeVisitor<?> visitor) {
List<TemplateNode> allTemplatesIncludingNewOnes = inferences.getAllTemplates();
for (TemplateNode templateNode : allTemplatesIncludingNewOnes) {
visitor.exec(templateNode);
}
}
/**
* Null if no typing has been done for the named template, or otherwise the context after a call
* to the named template. Since we derive templates by start context at the call site, there is no
* start context parameter.
*
* @param templateName A qualified template name.
*/
public Context getTemplateEndContext(String templateName) {
return inferences.getTemplateEndContext(templateName);
}
/**
* Maps ranges of text-nodes to contexts so that later parse passes can add attributes or
* elements.
*/
public ImmutableList<SlicedRawTextNode> getSlicedRawTextNodes() {
return slicedRawTextNodes;
}
/** Reports an autoescape exception. */
private void reportError(
ErrorReporter errorReporter, Set<SourceLocation> errorLocations, SoyAutoescapeException e) {
// First, get to the root cause of the exception, and assemble an error message indicating
// the full call stack that led to the failure.
String message = "- " + e.getMessage();
while (e.getCause() instanceof SoyAutoescapeException) {
e = (SoyAutoescapeException) e.getCause();
message += "\n- " + e.getMessage();
}
// Now that we've gotten to the leaf, let's use its source location as the canonical one for
// reporting and de-duping. (We might otherwise end up reporting a single error multiple times
// because a single template was called by multiple other contextual templates.)
// TODO(gboyer): Delete this logic once deprecated-contextual is removed.
SourceLocation location = Preconditions.checkNotNull(e.getSourceLocation());
if (errorLocations.contains(location)) {
return;
}
errorLocations.add(location);
errorReporter.report(location, AUTOESCAPE_ERROR, message);
}
/**
* Fills in the {@link Inferences} template name to node map.
*
* @param files Modified in place.
*/
private static Map<String, ImmutableList<TemplateNode>> findTemplates(
Iterable<? extends SoyFileNode> files) {
final Map<String, ImmutableList.Builder<TemplateNode>> templatesByName =
Maps.newLinkedHashMap();
for (SoyFileNode file : files) {
for (TemplateNode template : file.getChildren()) {
String templateName;
if (template instanceof TemplateBasicNode) {
templateName = template.getTemplateName();
} else {
templateName = ((TemplateDelegateNode) template).getDelTemplateName();
}
if (!templatesByName.containsKey(templateName)) {
templatesByName.put(templateName, ImmutableList.<TemplateNode>builder());
}
templatesByName.get(templateName).add(template);
}
}
final ImmutableMap.Builder<String, ImmutableList<TemplateNode>> templatesByNameBuilder =
ImmutableMap.builder();
for (Map.Entry<String, ImmutableList.Builder<TemplateNode>> e : templatesByName.entrySet()) {
templatesByNameBuilder.put(e.getKey(), e.getValue().build());
}
return templatesByNameBuilder.build();
}
private static final Predicate<TemplateNode> REQUIRES_INFERENCE =
new Predicate<TemplateNode>() {
@Override
public boolean apply(TemplateNode templateNode) {
// All strict and contextual. With strict, every template establishes its own context.
// With contextual, even if we don't see any callers in the call graph, it still might be
// called from another file. This used to skip private templates, but private supposedly
// only means the template can only be called by other templates, and even then, it is
// not really enforced strongly by the Closure JS Compiler. (Prior to changing this,
// there were a few templates that weren't contextually autoescaped because they were
// private, but were still being called directly from JS.)
return templateNode.getAutoescapeMode() == AutoescapeMode.STRICT
|| templateNode.getAutoescapeMode() == AutoescapeMode.CONTEXTUAL;
}
};
private static Map<String, SanitizedContent.ContentKind> makeOperatorKindMap(
final ImmutableMap<String, ? extends SoyPrintDirective> soyDirectivesMap) {
ImmutableMap.Builder<String, SanitizedContent.ContentKind> operatorKindMapBuilder =
ImmutableMap.builder();
for (SoyPrintDirective directive : soyDirectivesMap.values()) {
if (directive instanceof SanitizedContentOperator) {
operatorKindMapBuilder.put(
directive.getName(), ((SanitizedContentOperator) directive).getContentKind());
}
}
return operatorKindMapBuilder.build();
}
private final class NonContextualTypedRenderUnitNodesVisitor
extends AbstractSoyNodeVisitor<Void> {
final ErrorReporter errorReporter;
NonContextualTypedRenderUnitNodesVisitor(ErrorReporter errorReporter) {
this.errorReporter = errorReporter;
}
@Override
protected void visitTemplateNode(TemplateNode node) {
if (node.getAutoescapeMode() == AutoescapeMode.NONCONTEXTUAL) {
visitChildren(node);
}
}
@Override
protected void visitLetContentNode(LetContentNode node) {
visitRenderUnitNode(node);
}
@Override
protected void visitCallParamContentNode(CallParamContentNode node) {
visitRenderUnitNode(node);
}
protected void visitRenderUnitNode(RenderUnitNode node) {
if (node.getContentKind() != null) {
// Not visiting children in this block.
// In processing a strict block (any block with a kind), contextualAutoescaper will
// automatically go into the children.
// Secondly, CheckEscapingSanityVisitor makes sure that all the children {let} or {param}
// blocks of a strict {let} or {param} block are also strict.
ImmutableList.Builder<SlicedRawTextNode> slicedRawTextNodesBuilder =
ImmutableList.builder();
InferenceEngine.inferStrictRenderUnitNode(
// As this visitor visits only non-contextual templates.
AutoescapeMode.NONCONTEXTUAL,
node,
inferences,
autoescapeCancellingDirectives,
slicedRawTextNodesBuilder,
errorReporter);
} else {
visitChildren(node);
}
}
@Override
protected void visitSoyNode(SoyNode node) {
if (node instanceof ParentSoyNode<?>) {
visitChildren((ParentSoyNode<?>) node);
}
}
}
}