/* * 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.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.template.soy.data.SanitizedContent.ContentKind; import com.google.template.soy.data.internalutils.NodeContentKinds; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.internal.base.Pair; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; import com.google.template.soy.soytree.AutoescapeMode; import com.google.template.soy.soytree.CallBasicNode; import com.google.template.soy.soytree.CallDelegateNode; import com.google.template.soy.soytree.CallNode; import com.google.template.soy.soytree.CallParamContentNode; import com.google.template.soy.soytree.CssNode; import com.google.template.soy.soytree.EscapingMode; import com.google.template.soy.soytree.ForNode; import com.google.template.soy.soytree.ForeachIfemptyNode; import com.google.template.soy.soytree.ForeachNode; import com.google.template.soy.soytree.ForeachNonemptyNode; import com.google.template.soy.soytree.HtmlContext; import com.google.template.soy.soytree.IfElseNode; import com.google.template.soy.soytree.IfNode; import com.google.template.soy.soytree.LetContentNode; import com.google.template.soy.soytree.MsgFallbackGroupNode; import com.google.template.soy.soytree.PrintDirectiveNode; import com.google.template.soy.soytree.PrintNode; import com.google.template.soy.soytree.RawTextNode; import com.google.template.soy.soytree.SoyNode; import com.google.template.soy.soytree.SoyNode.BlockNode; import com.google.template.soy.soytree.SoyNode.CommandNode; import com.google.template.soy.soytree.SoyNode.ParentSoyNode; import com.google.template.soy.soytree.SoyNode.RenderUnitNode; import com.google.template.soy.soytree.SwitchDefaultNode; import com.google.template.soy.soytree.SwitchNode; import com.google.template.soy.soytree.TemplateNode; import com.google.template.soy.soytree.XidNode; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Chooses appropriate escaping modes for <code>{print}</code> commands and derives templates as * necessary. * * <p>For each template with {@code autoescape="contextual"}, assume that the template is used to * produce an HTML fragment. Start walking the body with the {@link Context context} provided by the * caller (typically {@link HtmlContext#HTML_PCDATA}). * * <ul> * <li>For RawTextNodes, update the context based on the fragment, so seeing "<script>" will * move us into a JavaScript context while "<!--" would move us into an HTML comment * context. * <li>For {@link PrintNode}s, choose an escaping convention appropriate to the current context. * <li>For {@link IfNode}s, {@link SwitchNode}s, and looping constructs, propagate context * separately along each path, and make sure they converge on a consistent context. * <li>For {@link CallBasicNode}s, maybe derive the target based on current context, recursively * propagate contexts through the derived template to compute an end context for the template. * See fixed-point typing below for a discussion of reentrant templates and templates used in * different contexts. * </ul> * */ final class InferenceEngine { /** * Infer an end context for the given template and, if requested, choose escaping directives for * any <code>{print}</code>. * * @param templateNode A template that is visited in {@code startContext} and no other. If a * template can be reached from multiple contexts, then it should be cloned. This class * automatically does that for called templates. * @param inferences Receives all suggested changes and inferences to tn. * @param autoescapeCancellingDirectives Soy directives that cancel autoescaping (see {@link * com.google.template.soy.shared.restricted.SoyPrintDirective#shouldCancelAutoescape()}). * @return The end context when the given template is reached from {@code startContext}. */ public static Context inferTemplateEndContext( TemplateNode templateNode, Context startContext, Inferences inferences, Set<String> autoescapeCancellingDirectives, ImmutableList.Builder<SlicedRawTextNode> slicedRawTextNodesBuilder, ErrorReporter errorReporter) { Context endContext; try { AutoescapeMode autoescapeMode = templateNode.getAutoescapeMode(); InferenceEngine inferenceEngine = new InferenceEngine( autoescapeMode, autoescapeMode, inferences, autoescapeCancellingDirectives, slicedRawTextNodesBuilder, errorReporter); // Context started off as startContext and we have propagated context through all of // template's children, so now context is the template's end context. endContext = inferenceEngine.infer(templateNode, startContext); inferences.recordTemplateEndContext(templateNode.getTemplateName(), endContext); } catch (SoyAutoescapeException e) { throw e.maybeAssociateNode(templateNode); } return endContext; } /** * Checks that the end context of a strict block is compatible with its start context. * * @throws SoyAutoescapeException if they mismatch. */ private static void checkStrictBlockEndContext(RenderUnitNode node, Context endContext) { if (!endContext.isValidEndContextForContentKind(node.getContentKind())) { throw SoyAutoescapeException.createWithNode( "A strict block of kind=\"" + NodeContentKinds.toAttributeValue(node.getContentKind()) + "\" cannot end in context " + endContext + ". Likely cause is " + endContext.getLikelyEndContextMismatchCause(node.getContentKind()) + ": " + node.getTagString(), node); } } /** * Applies strict contextual autoescaping to the given node's children. * * <p>The start context is the given node's declared {@link ContentKind}, and it is enforced that * the block's inferred end context matches the start context. * * <p>This method is used to visit the content of {let} and {param} nodes with a {@code kind} * attribute. */ static void inferStrictRenderUnitNode( AutoescapeMode templateAutoescapeMode, RenderUnitNode node, Inferences inferences, Set<String> autoescapeCancellingDirectives, ImmutableList.Builder<SlicedRawTextNode> slicedRawTextNodesBuilder, ErrorReporter errorReporter) { InferenceEngine inferenceEngine = new InferenceEngine( AutoescapeMode.STRICT, templateAutoescapeMode, inferences, autoescapeCancellingDirectives, slicedRawTextNodesBuilder, errorReporter); // Context started off as startContext and we have propagated context through all of // node's children, so now context is the node's end context. Context endContext = inferenceEngine.inferChildren( node, Context.getStartContextForContentKind(node.getContentKind())); // Checking that start and end context is same. checkStrictBlockEndContext(node, endContext); } /** The autoescaping mode in this current context. */ private final AutoescapeMode autoescapeMode; /** The autoescape mode of the surrounding {template}. */ private final AutoescapeMode templateAutoescapeMode; /** Receives modifications and typing inferences. */ private final Inferences inferences; /** The escaping mode to assume when none is specified. */ private final EscapingMode defaultEscapingMode; /** * Soy directives that cancel autoescaping (see {@link * com.google.template.soy.shared.restricted.SoyPrintDirective#shouldCancelAutoescape()}). */ private final Set<String> autoescapeCancellingDirectives; /** Records context transitions found by the raw text node escaper. */ private final ImmutableList.Builder<SlicedRawTextNode> slicedRawTextNodesBuilder; /** For reporting errors. */ private final ErrorReporter errorReporter; private InferenceEngine( AutoescapeMode autoescapeMode, AutoescapeMode templateAutoescapeMode, Inferences inferences, Set<String> autoescapeCancellingDirectives, ImmutableList.Builder<SlicedRawTextNode> slicedRawTextNodesBuilder, ErrorReporter errorReporter) { this.autoescapeMode = autoescapeMode; this.templateAutoescapeMode = templateAutoescapeMode; this.inferences = inferences; this.autoescapeCancellingDirectives = autoescapeCancellingDirectives; this.slicedRawTextNodesBuilder = slicedRawTextNodesBuilder; this.defaultEscapingMode = EscapingMode.ESCAPE_HTML; this.errorReporter = errorReporter; } private Context infer(SoyNode node, Context context) { return new ContextPropagatingVisitor(context).exec(node); } private Context inferChildren(SoyNode node, Context context) { ContextPropagatingVisitor contextPropagatingVisitor = new ContextPropagatingVisitor(context); return contextPropagatingVisitor.execChildren(node); } /** * A visitor that propagates context across a Soy AST to determine its end context. The end * context of an AST is the one that would be reached by applying the {@link * RawTextContextUpdater}'s HTML/CSS/JS grammar to any output of the template (where print * commands produce innocuous strings). An innocuous string is one that is non-empty and that * contains no special characters in HTML/CSS/JS. The string 'z' is a good example of an innocuous * string. */ private final class ContextPropagatingVisitor extends AbstractSoyNodeVisitor<Context> { private Context context; public ContextPropagatingVisitor(Context context) { this.context = context; } @Override public Context exec(SoyNode node) { visit(node); return context; } /** Like {@link #exec(SoyNode)}, but only visits the current node's children, if any. */ public Context execChildren(SoyNode node) { if (node instanceof ParentSoyNode<?>) { visitChildren((ParentSoyNode<?>) node); } return context; } @Override protected void visitTemplateNode(TemplateNode templateNode) { Preconditions.checkState( templateNode.getAutoescapeMode() == autoescapeMode, "Same ContextPropagatingVisitor cannot be reused for multiple escaping modes."); if (autoescapeMode == AutoescapeMode.STRICT) { Preconditions.checkState( context.isValidStartContextForContentKind(templateNode.getContentKind()), "Strict templates may only be visited in the context for their declared content kind."); // Normalize to the canonical context, even if we started in a similar but allowable // context (e.g. single versus double quotes). context = Context.getStartContextForContentKind(templateNode.getContentKind()); } visitChildren(templateNode); if (autoescapeMode == AutoescapeMode.STRICT) { checkStrictBlockEndContext(templateNode, context); } } /** Propagates context across raw chunks of HTML text. */ @Override protected void visitRawTextNode(RawTextNode rawTextNode) { Context newContext; try { SlicedRawTextNode sliced = RawTextContextUpdater.processRawText(rawTextNode, context); newContext = sliced.getEndContext(); slicedRawTextNodesBuilder.add(sliced); } catch (SoyAutoescapeException ex) { throw ex.maybeAssociateNode(rawTextNode); } context = newContext; } @Override protected void visitMsgFallbackGroupNode(MsgFallbackGroupNode node) { if (autoescapeMode == AutoescapeMode.STRICT || autoescapeMode == AutoescapeMode.CONTEXTUAL) { // (1) Determine the escaping we should do on the node itself, and the context we should // parse the children in. Optional<Context.MsgEscapingStrategy> maybeStrategy = context.getMsgEscapingStrategy(); if (!maybeStrategy.isPresent()) { throw SoyAutoescapeException.createWithNode( "Messages are not supported in this context, because it would mean asking " + "translators to write source code; if this is desired, try factoring the " + "message into a {let} block: " + context, node); } Context.MsgEscapingStrategy strategy = maybeStrategy.get(); inferences.setEscapingDirectives(node, context, strategy.escapingModesForFullMessage); // (2) Run the inference engine on the parts of the message in that context. Context msgEndContext = new InferenceEngine( autoescapeMode, templateAutoescapeMode, inferences, autoescapeCancellingDirectives, slicedRawTextNodesBuilder, errorReporter) .inferChildren(node, strategy.childContext); // (3) Make sure the message didn't itself change context. if (!msgEndContext.equals(strategy.childContext)) { throw SoyAutoescapeException.createWithNode( "Message text should not alter the escaping context. " + context + " != " + strategy.childContext, node); } } else { // In a non-contextual mode, we just descend into the children. visitChildren(node); } } // TODO: Reorder visitCall* methods in AbstractSoyNodeVisitor order. /** * {@link DerivedTemplateUtils Derive} a template from the given call's target if necessary, and * figure out the template's end context. */ @Override protected void visitCallNode(CallNode callNode) { try { String calleeName; if (callNode instanceof CallBasicNode) { calleeName = ((CallBasicNode) callNode).getCalleeName(); } else { calleeName = ((CallDelegateNode) callNode).getDelCalleeName(); } Pair<String, Context> derivedNameAndContext = inferCallSite(callNode, context, calleeName, inferences); String derivedCalleeName = derivedNameAndContext.first; if (!calleeName.equals(derivedCalleeName)) { inferences.retargetCall(callNode, derivedCalleeName); } context = derivedNameAndContext.second; } catch (SoyAutoescapeException ex) { throw ex.maybeAssociateNode(callNode); } visitChildren(callNode); } /** * For param content nodes with a {@code kind} attribute, visit the node's content with the * strict contextual escaper in the start context indicated by the {@code kind} attribute. * * <p>If the param content nodes with a {@code kind} attribute is in non-contextual template it * is handled by another visitor ({@link * ContextualAutoescaper.NonContextualTypedRenderUnitNodesVisitor}) called from {@link * ContextualAutoescaper}. Here only nodes in strict or contextual templates are handled. */ @Override protected void visitCallParamContentNode(CallParamContentNode node) { if (node.getContentKind() != null && (autoescapeMode == AutoescapeMode.CONTEXTUAL || autoescapeMode == AutoescapeMode.STRICT)) { inferInStrictMode(node); } else if (autoescapeMode == AutoescapeMode.CONTEXTUAL) { inferInContextualModeForHtml(node); } else { // No contextual inference. We should never reach this in strict mode, since all param // blocks must have an explicit kind, checked in CheckEscapingSanityVisitor. Preconditions.checkState(autoescapeMode != AutoescapeMode.STRICT); } } /** Pass over 'xid' nodes. */ @Override protected void visitXidNode(XidNode node) { context = context.getContextBeforeDynamicValue(); // TODO: Maybe check that we're in a non-string CSS context, a JS string or value context, or // an attribute value context like a class, id, or for. } /** Pass over CSS nodes. */ @Override protected void visitCssNode(CssNode node) { context = context.getContextBeforeDynamicValue(); // TODO: Maybe check that we're in a non-string CSS context, a JS string or value context, or // an attribute value context like a class, id, or for. } /** * For let content nodes with a {@code kind} attribute, visit the node's content with the strict * contextual escaper in the start context indicated by the {@code kind} attribute. * * <p>If the let content nodes with a {@code kind} attribute is in non-contextual template it is * handled by another visitor ({@link * ContextualAutoescaper.NonContextualTypedRenderUnitNodesVisitor}) called from {@link * ContextualAutoescaper}. Here only nodes in strict or contextual templates are handled. */ @Override protected void visitLetContentNode(LetContentNode node) { if (node.getContentKind() == null) { // Nodes without kind attribute are treated by the contextual autoescaper as before (i.e. // visted in whatever context the let node appears. // TODO: Consider unconditionally visiting as HTML_PCDATA to be consistent with {param}. super.visitLetContentNode(node); } else { if (autoescapeMode == AutoescapeMode.CONTEXTUAL || autoescapeMode == AutoescapeMode.STRICT) { inferInStrictMode(node); } } } @Override protected void visitIfNode(IfNode ifNode) { propagateAcrossDisjunction(ifNode); } @Override protected void visitSwitchNode(SwitchNode switchNode) { propagateAcrossDisjunction(switchNode); } /** * Do multiple inferences so we can make sure we get to a consistent context regardless of how * many times the loop is entered. */ @Override protected void visitForNode(ForNode forNode) { // Strictly speaking, if a for loop is guaranteed to execute once, then the result of // rewrite(loopBody, context) must be the same as rewrite(loopBody, result). // But where we cannot prove that the loop is executed at least once, the result must be the // same as context. // Even more strictly speaking, if there exists an arbitrary positive integer P such that the // loop is guaranteed to execute N*P times for some arbitrary non-negative integer N then // we can follow the loop body P times to compute the end context, and where N is positive, // we can ignore the context before the loop. // For simplicity, we just enforce the property that the loop body cannot change context. try { Context afterBody = context; for (SoyNode child : forNode.getChildren()) { afterBody = infer(child, afterBody); } Optional<Context> combined = Context.union(context, afterBody); if (!combined.isPresent()) { throw SoyAutoescapeException.createWithNode( "{for} command changes context so it cannot be reentered : " + forNode.toSourceString(), forNode); } context = combined.get(); } catch (SoyAutoescapeException ex) { throw ex.maybeAssociateNode(forNode); } } /** * Do multiple inferences so we can make sure we get to a consistent context regardless of how * many times the loop is entered. */ @Override protected void visitForeachNode(ForeachNode foreachNode) { List<BlockNode> foreachChildren = foreachNode.getChildren(); ForeachNonemptyNode neNode = (ForeachNonemptyNode) foreachChildren.get(0); ForeachIfemptyNode ieNode; if (foreachChildren.size() == 2) { ieNode = (ForeachIfemptyNode) foreachChildren.get(1); } else if (foreachChildren.size() == 1) { ieNode = null; } else { throw new AssertionError(); } try { Context afterBody = context; if (neNode != null) { afterBody = infer(neNode, context); // Make sure that repeated invocations of the body end up in the same state. Context elseContext = infer(neNode, afterBody); Optional<Context> combined = Context.union(elseContext, afterBody); if (!combined.isPresent()) { throw SoyAutoescapeException.createWithNode( "{foreach} body does not end in the same context after repeated entries : " + neNode.toSourceString(), neNode); } afterBody = combined.get(); } Context ifemptyContext; if (ieNode != null) { ifemptyContext = infer(ieNode, context); } else { ifemptyContext = context; } Optional<Context> combined = Context.union(ifemptyContext, afterBody); if (!combined.isPresent()) { throw SoyAutoescapeException.createWithNode( (ieNode == null ? "{foreach} body changes context : " : "{foreach} body does not end in the same context as {ifempty} : ") + foreachNode.toSourceString(), ieNode == null ? foreachNode : ieNode); } context = combined.get(); } catch (SoyAutoescapeException ex) { throw ex.maybeAssociateNode(foreachNode); } } /** * Pick an escaping mode for the print node if this is in an {@code autoescape="contextual"} * template. */ @Override protected void visitPrintNode(PrintNode printNode) { try { // It is an error to use autoescape-canceling print directives in strict mode unless in a // block of kind text. if (autoescapeMode == AutoescapeMode.STRICT && context.state != HtmlContext.TEXT) { for (PrintDirectiveNode printDirective : printNode.getChildren()) { if (printDirective.getName().equals("|noAutoescape")) { // Treat noAutoescape specially: // - It is allowed in strict sub-contexts if the surrounding template is non-strict, // to help with migration. This does not apply to other escaping directives since // they are just as dangerous, but less obvious to auditors. // - It deserves a more useful error message. if (templateAutoescapeMode == AutoescapeMode.STRICT) { // Help the user figure out the best content kind to use, using existing heuristics. ContentKind recommendedKind = context.getMostAppropriateContentKind(); String recommendedKindStr = (recommendedKind == ContentKind.TEXT) ? "appropriate kind=\"...\"" : ("kind=\"" + NodeContentKinds.toAttributeValue(recommendedKind) + "\""); throw SoyAutoescapeException.createWithNode( "noAutoescape is not allowed in strict autoescaping mode. Instead, pass in a " + "{param} with " + recommendedKindStr + " or SanitizedContent.", printNode); } } else if (autoescapeCancellingDirectives.contains(printDirective.getName())) { throw SoyAutoescapeException.createWithNode( "Autoescape-cancelling print directives like " + printDirective.getName() + " are only allowed in kind=\"text\" blocks. If you really want to " + "over-escape, try using a let block: " + "{let $foo kind=\"text\"}" + printNode.toSourceString() + "{/let}{$foo}.", printNode); } } } List<EscapingMode> escapingModes = inferences.getEscapingMode(printNode); context = context.getContextBeforeDynamicValue(); if (escapingModes.isEmpty()) { // None specified. // The inferences set below specify which nodes to change. In the non-contextual modes, // we leave escapingModesToSet null since no changes are to be made to this print node. List<EscapingMode> escapingModesToSet = null; switch (autoescapeMode) { case STRICT: case CONTEXTUAL: // Infer one. escapingModes = escapingModesToSet = context.getEscapingModes(printNode.getChildren()); break; case NONCONTEXTUAL: escapingModes = ImmutableList.of(defaultEscapingMode); break; } inferences.setEscapingDirectives(printNode, context, escapingModesToSet); } else if (!context.isCompatibleWith(escapingModes.get(0))) { throw SoyAutoescapeException.createWithNode( "Escaping modes " + escapingModes + " not compatible with " + context + " : " + printNode.toSourceString(), printNode); } // Figure out the context at the end. if (!escapingModes.isEmpty() || autoescapeMode == AutoescapeMode.CONTEXTUAL || autoescapeMode == AutoescapeMode.STRICT) { // If we know the escaping mode or we're supposed to choose one, then use that. context = getContextAfterEscaping(printNode, context); } else { // If we are not in an autoescaping template, assume that the author knows what they're // doing and simulate an innocuous value. context = RawTextContextUpdater.processRawText( new RawTextNode(-1, "z", printNode.getSourceLocation()), context) .getEndContext(); } } catch (SoyAutoescapeException ex) { throw ex.maybeAssociateNode(printNode); } } /** Handle conjunction nodes. */ @Override protected void visitSoyNode(SoyNode node) { if (node instanceof ParentSoyNode<?>) { visitChildren((ParentSoyNode<?>) node); } } // // Helper methods. /** * Determines the content kind of the templates. * * <p>This relies on CheckDelegatesVisitor to print friendly messages if the deltemplates differ * in content kind. */ private ContentKind getCommonContentKindIfStrict(List<TemplateNode> templates) { if (templates == null || templates.isEmpty()) { return null; } ContentKind contentKind = templates.get(0).getContentKind(); for (TemplateNode template : templates) { Preconditions.checkArgument(template.getContentKind() == contentKind); } return contentKind; } /** * Derives a template if necessary to compute a consistent end context for a call to the named * template. * * @param callNode The call node. * @param startContext The context before the call. * @param templateName The name of the template being called. * @param inferences Contains a mapping of templates visible to the call site, prior typing * decisions, and derived templates. Will receive any templates successfully derived as a * side-effect of this call. * @return The name of the template to call (possibly derived from templateName) and the context * after the call ends. */ private Pair<String, Context> inferCallSite( CallNode callNode, Context startContext, String templateName, Inferences inferences) throws SoyAutoescapeException { inferences.recordTemplateChecked(templateName); List<TemplateNode> targets = inferences.lookupTemplates(templateName); ContentKind calleeStrictContentKind = getCommonContentKindIfStrict(targets); if (autoescapeMode == AutoescapeMode.STRICT) { // We're currently in a strict mode template. Check what kind of template is being called. if (calleeStrictContentKind != null && startContext.isValidStartContextForContentKind(calleeStrictContentKind)) { // As an optimization, don't escape the call site if the callee has the right content // kind. Since all deltemplates with the same name must be of the same kind (checked // elsewhere), we can make this optimization even if we can't see all the deltemplates. return Pair.of(templateName, getContextAfterDynamicValue(callNode, startContext)); } else if (calleeStrictContentKind != null || targets == null || targets.isEmpty()) { Context callContext = startContext.getContextBeforeDynamicValue(); // If a strict template calls another strict template (or an unknown extern), the result // will be escaped, so the call statement behaves effectively like a print statement. // No re-contextualization of the callee is done. // TODO(gboyer): Throw an exception if the list of escaping modes is empty, which // indicates that there's no valid escaper for this context. My plan is to actually have // getEscapingModes() itself throw the exception, but this requires some weeding out of // bad existing templates. inferences.setEscapingDirectives( callNode, callContext, callContext.getEscapingModes(ImmutableList.<PrintDirectiveNode>of())); return Pair.of(templateName, getContextAfterDynamicValue(callNode, startContext)); } else if (startContext.state == HtmlContext.TEXT) { // Contextualize the callee in TEXT mode. It's okay to call any template from TEXT mode // since TEXT doesn't make any safety guarantees. return contextualizeCallee(callNode, startContext, templateName, inferences); } else { // TODO: We could easily allow this in a future release. We can contextualize the callee // and re-escape its output. There are two options. TEXT is nicer because there's no // re-escaping in most cases. Markup won't be preserved, but at least there will be zero // double-escaping. HTML is more consistent because externs behave the same as interns. throw SoyAutoescapeException.createWithNode( "Soy strict autoescaping currently forbids calls to non-strict templates, unless " + "the context is kind=\"text\", since there's no guarantee the callee is safe: " + callNode.getTagString(), callNode); } } else { // In a non-strict mode template. if (targets == null || targets.isEmpty()) { // External template not visible to compiler -- let's pray for the best! We might end up // calling a Javascript-escaping template from HTML or vice versa. return Pair.of(templateName, startContext); } else if (calleeStrictContentKind != null) { // Non-strict templates may call strict templates, but only if the context is a match. // NOTE: While contextual templates *might* do escaping like strict in this context, it // would silently break if the template is compiled as an extern. By having this check, // teams can do a single monolithic compilation for error checking to prevent this. // We're a little loose in this check to allow calling URI templates within URI // attributes, even though it's not technically valid HTML, in order to help migration. if (!startContext.isValidStartContextForContentKindLoose(calleeStrictContentKind)) { throw SoyAutoescapeException.createWithNode( "Cannot call strictly autoescaped template " + templateName + " of kind=\"" + NodeContentKinds.toAttributeValue(calleeStrictContentKind) + "\" from incompatible context " + startContext + ". Strict templates " + "generate extra code to safely call templates of other content kinds, but " + "non-strict templates do not: " + callNode.getTagString(), callNode); } return Pair.of(templateName, startContext); } else { // Normal contextual-to-contextual propagation. return contextualizeCallee(callNode, startContext, templateName, inferences); } } } /** * Creates a contextual derivative of the specified template and infers the end context. * * @param callNode The call site. * @param startContext The known context to start at. * @param calleeName The non-contextualized callee name. * @param inferences The inferences to write to. * @return A pairing of the new derived name and the end context. */ private Pair<String, Context> contextualizeCallee( CallNode callNode, Context startContext, String calleeName, Inferences inferences) { // Propgate the context into the callee contextual template. String suffix = DerivedTemplateUtils.getSuffix(startContext); String baseName = DerivedTemplateUtils.getBaseName(calleeName); // The derived template name. String newCalleeName = baseName + suffix; // Clone the templates for this new context if needed. if (inferences.lookupTemplates(newCalleeName) == null) { inferences.cloneTemplates(baseName, newCalleeName); } try { Context endContext = determineContextualization(startContext, newCalleeName, inferences); return Pair.of(newCalleeName, endContext); } catch (SoyAutoescapeException e) { throw SoyAutoescapeException.createCausedWithNode( "Error while re-contextualizing template " + calleeName + " in context " + startContext + ":", e, callNode); } } /** * Determines the end context and a set of inferences for a template in a particular context. * * <p>This does not create new cloned templates, but just computes contextualization on existing * ones. * * @param startContext The start context we're calling these templates in. * @param calleeName The callee's name, already modified for context. * @param inferences The inferences to modify. */ private Context determineContextualization( Context startContext, String calleeName, Inferences inferences) { Context endContext = inferences.getTemplateEndContext(calleeName); if (endContext != null) { // We've already computed this; return early. return endContext; } List<TemplateNode> templateNodes = inferences.lookupTemplates(calleeName); // Optimistically assume the new callee ends with the same context as it starts, and then // verify that's the case. Pair<Inferences, Context> hypothesis = hypothesizeContextualization( startContext, startContext, calleeName, templateNodes, inferences); endContext = hypothesis.second; Inferences subInferences = hypothesis.first; if (!endContext.equals(startContext) && subInferences.wasTemplateChecked(calleeName)) { // Try assuming endContext as the endContext and see if that is a fixed point. If so, it // is a valid endContext context since its output is the same regardless of whether // recursive calls are properly typed. This allows us to gloss over minor differences in // startContexts, e.g. JsFollowingSlash. Pair<Inferences, Context> secondHypothesis = hypothesizeContextualization( startContext, endContext, calleeName, templateNodes, inferences); Optional<Context> combined = Context.union(secondHypothesis.second, endContext); // See if the first and second hypothesis result in a compatible end context. if (!combined.isPresent()) { // Cannot identify an end context. Bail. throw SoyAutoescapeException.createWithNode( "Cannot determine end context for recursive template " + calleeName, templateNodes.get(0)); } endContext = combined.get(); } subInferences.recordTemplateEndContext(calleeName, endContext); subInferences.foldIntoParent(); return endContext; } /** * Hypothesizes a particular end context and determines a potential end context, if any. * * <p>This returns the *actual* end context determined from this hypothesis. Hypotheses are * needed to handle recursive templates, where the output context is needed to compute the * context within the template. * * @param startContext The known context to start at. * @param hypotheticalEndContext The end context to test. * @param calleeName Name of the callee. * @param templateNodes The templates and deltemplates of the same name. * @param parentInferences The inferences to work from. * @return A combination of the end context determined and the inferences that go along with * them. */ private Pair<Inferences, Context> hypothesizeContextualization( Context startContext, Context hypotheticalEndContext, String calleeName, List<TemplateNode> templateNodes, Inferences parentInferences) { // Create a hypothetical world of inferences based on this hypothesis. It is up to the caller // to fold these into the parent inferences if it chooses to use these. Inferences inferences = new Inferences(parentInferences); List<Context> endContexts = new ArrayList<Context>(); inferences.recordTemplateEndContext(calleeName, hypotheticalEndContext); for (TemplateNode templateNode : templateNodes) { endContexts.add( inferTemplateEndContext( templateNode, startContext, inferences, autoescapeCancellingDirectives, slicedRawTextNodesBuilder, errorReporter)); } Optional<Context> combined = Context.union(endContexts); if (!combined.isPresent()) { throw SoyAutoescapeException.createWithNode( "Deltemplates diverge when used with deprecated-contextual autoescaping." + " Based on the call site, assuming these templates all start in " + startContext + ", the different deltemplates end in incompatible contexts: " + Joiner.on(", ").join(endContexts), templateNodes.get(0)); } return Pair.of(inferences, combined.get()); } /** Consider the various branches separately and compute a union context for each branch. */ private void propagateAcrossDisjunction(ParentSoyNode<?> node) { try { // All the branches of an {if} or {switch} should return compatible contexts, so that we can // figure out the end context of the branch as a whole. Iterator<? extends SoyNode> childIt = node.getChildren().iterator(); SoyNode firstBranch = childIt.next(); Context out = infer(firstBranch, context); boolean sawElseOrDefault = false; while (childIt.hasNext()) { SoyNode branch = childIt.next(); Context brOut = infer(branch, context); Optional<Context> combined = Context.union(out, brOut); if (!combined.isPresent()) { throw SoyAutoescapeException.createWithNode( (node instanceof IfNode ? "{if} command branch ends in a different context than preceding branches:" : "{switch} command case ends in a different context than preceding cases:") + " " + branch.toSourceString(), branch); } out = combined.get(); if (branch instanceof IfElseNode || branch instanceof SwitchDefaultNode) { sawElseOrDefault = true; } } // If there is no else or default, then the end context has to be the compatible with the // start context. if (!sawElseOrDefault) { Optional<Context> combined = Context.union(context, out); if (!combined.isPresent()) { throw SoyAutoescapeException.createWithNode( (node instanceof IfNode ? "{if} command without {else} changes context : " : "{switch} command without {default} changes context : ") + node.toSourceString(), node); } out = combined.get(); } context = out; } catch (SoyAutoescapeException ex) { throw ex.maybeAssociateNode(node); } } private void inferInStrictMode(RenderUnitNode node) { inferStrictRenderUnitNode( templateAutoescapeMode, node, inferences, autoescapeCancellingDirectives, slicedRawTextNodesBuilder, errorReporter); } /** Applies HTML contextual autoescaping on a legacy contextual parameter block. */ private void inferInContextualModeForHtml(CommandNode node) { // NOTE: Previously this wouldn't do any contextual analysis, which resulted in subtle bugs // such as the contextual autoescaper not seeing typed parameters in nested calls. final Context paramContentNodeEndContext = new InferenceEngine( AutoescapeMode.CONTEXTUAL, templateAutoescapeMode, inferences, autoescapeCancellingDirectives, slicedRawTextNodesBuilder, errorReporter) .inferChildren(node, Context.HTML_PCDATA); if (!paramContentNodeEndContext.equals(Context.HTML_PCDATA)) { throw SoyAutoescapeException.createWithNode( "Blocks should start and end in HTML context: " + node.getTagString(), node); } } } // // Static helper methods (cannot be part of inner class). /** * Returns the end context after a properly escaped dynamic value was inserted. * * @param node Node to print out in case of an error. * @param startContext The context after which a dynamic value is inserted. */ private static Context getContextAfterDynamicValue(SoyNode node, Context startContext) { // TODO: If the context is JS, perhaps this should return JsFollowingSlash.UNKNOWN. Right now // we assume that the dynamic value is also an expression, but JsFollowingSlash.UNKNOWN would // account for things that end in semicolons (since the next slash could be either a regex OR a // division op). return getContextAfterEscaping(node, startContext); } /** * Returns the end context after a dynamic value was inserted with specific escaping modes. * * @param node The node to print in case of an error. * @param startContext The start context -- must be a "context before dynamic value". * @param escapingModes The escaping sequence being used. */ private static Context getContextAfterEscaping(SoyNode node, Context startContext) { try { return startContext.getContextAfterDynamicValue(); } catch (SoyAutoescapeException e) { throw e.maybeAssociateNode(node); } } }