/* * Copyright (C) 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.clearsilver.jsilver.syntax; import com.google.clearsilver.jsilver.autoescape.AutoEscapeContext; import com.google.clearsilver.jsilver.autoescape.EscapeMode; import com.google.clearsilver.jsilver.exceptions.JSilverAutoEscapingException; import com.google.clearsilver.jsilver.syntax.analysis.DepthFirstAdapter; import com.google.clearsilver.jsilver.syntax.node.AAltCommand; import com.google.clearsilver.jsilver.syntax.node.AAutoescapeCommand; import com.google.clearsilver.jsilver.syntax.node.ACallCommand; import com.google.clearsilver.jsilver.syntax.node.AContentTypeCommand; import com.google.clearsilver.jsilver.syntax.node.ACsOpenPosition; import com.google.clearsilver.jsilver.syntax.node.ADataCommand; import com.google.clearsilver.jsilver.syntax.node.ADefCommand; import com.google.clearsilver.jsilver.syntax.node.AEscapeCommand; import com.google.clearsilver.jsilver.syntax.node.AEvarCommand; import com.google.clearsilver.jsilver.syntax.node.AHardIncludeCommand; import com.google.clearsilver.jsilver.syntax.node.AHardLincludeCommand; import com.google.clearsilver.jsilver.syntax.node.AIfCommand; import com.google.clearsilver.jsilver.syntax.node.AIncludeCommand; import com.google.clearsilver.jsilver.syntax.node.ALincludeCommand; import com.google.clearsilver.jsilver.syntax.node.ALvarCommand; import com.google.clearsilver.jsilver.syntax.node.ANameCommand; import com.google.clearsilver.jsilver.syntax.node.AStringExpression; import com.google.clearsilver.jsilver.syntax.node.AUvarCommand; import com.google.clearsilver.jsilver.syntax.node.AVarCommand; import com.google.clearsilver.jsilver.syntax.node.Node; import com.google.clearsilver.jsilver.syntax.node.PCommand; import com.google.clearsilver.jsilver.syntax.node.PPosition; import com.google.clearsilver.jsilver.syntax.node.Start; import com.google.clearsilver.jsilver.syntax.node.TCsOpen; import com.google.clearsilver.jsilver.syntax.node.TString; import com.google.clearsilver.jsilver.syntax.node.Token; /** * Run a context parser (currently only HTML parser) over the AST, determine nodes that need * escaping, and apply the appropriate escaping command to those nodes. The parser is fed literal * data (from DataCommands), which it uses to track the context. When variables (e.g. VarCommand) * are encountered, we query the parser for its current context, and apply the appropriate escaping * command. */ public class AutoEscaper extends DepthFirstAdapter { private AutoEscapeContext autoEscapeContext; private boolean skipAutoEscape; private final EscapeMode escapeMode; private final String templateName; private boolean contentTypeCalled; /** * Create an AutoEscaper, which will apply the specified escaping mode. If templateName is * non-null, it will be used while displaying error messages. * * @param mode * @param templateName */ public AutoEscaper(EscapeMode mode, String templateName) { this.templateName = templateName; if (mode.equals(EscapeMode.ESCAPE_NONE)) { throw new JSilverAutoEscapingException("AutoEscaper called when no escaping is required", templateName); } escapeMode = mode; if (mode.isAutoEscapingMode()) { autoEscapeContext = new AutoEscapeContext(mode, templateName); skipAutoEscape = false; } else { autoEscapeContext = null; } } /** * Create an AutoEscaper, which will apply the specified escaping mode. When possible, use * #AutoEscaper(EscapeMode, String) instead. It specifies the template being auto escaped, which * is useful when displaying error messages. * * @param mode */ public AutoEscaper(EscapeMode mode) { this(mode, null); } @Override public void caseStart(Start start) { if (!escapeMode.isAutoEscapingMode()) { // For an explicit EscapeMode like {@code EscapeMode.ESCAPE_HTML}, we // do not need to parse the rest of the tree. Instead, we just wrap the // entire tree in a <?cs escape ?> node. handleExplicitEscapeMode(start); } else { AutoEscapeContext.AutoEscapeState startState = autoEscapeContext.getCurrentState(); // call super.caseStart, which will make us visit the rest of the tree, // so we can determine the appropriate escaping to apply for each // variable. super.caseStart(start); AutoEscapeContext.AutoEscapeState endState = autoEscapeContext.getCurrentState(); if (!autoEscapeContext.isPermittedStateChangeForIncludes(startState, endState)) { // If template contains a content-type command, the escaping context // was intentionally changed. Such a change in context is fine as long // as the current template is not included inside another. There is no // way to verify that the template is not an include template however, // so ignore the error and depend on developers doing the right thing. if (contentTypeCalled) { return; } // We do not permit templates to end in a different context than they start in. // This is so that an included template does not modify the context of // the template that includes it. throw new JSilverAutoEscapingException("Template starts in context " + startState + " but ends in different context " + endState, templateName); } } } private void handleExplicitEscapeMode(Start start) { AStringExpression escapeExpr = new AStringExpression(new TString("\"" + escapeMode.getEscapeCommand() + "\"")); PCommand node = start.getPCommand(); AEscapeCommand escape = new AEscapeCommand(new ACsOpenPosition(new TCsOpen("<?cs ", 0, 0)), escapeExpr, (PCommand) node.clone()); node.replaceBy(escape); } @Override public void caseADataCommand(ADataCommand node) { String data = node.getData().getText(); autoEscapeContext.setCurrentPosition(node.getData().getLine(), node.getData().getPos()); autoEscapeContext.parseData(data); } @Override public void caseADefCommand(ADefCommand node) { // Ignore the entire defcommand subtree, don't even parse it. } @Override public void caseAIfCommand(AIfCommand node) { setCurrentPosition(node.getPosition()); /* * Since AutoEscaper is being applied while building the AST, and not during rendering, the html * context of variables is sometimes ambiguous. For instance: <?cs if: X ?><script><?cs /if ?> * <?cs var: MyVar ?> * * Here MyVar may require js escaping or html escaping depending on whether the "if" condition * is true or false. * * To avoid such ambiguity, we require all branches of a conditional statement to end in the * same context. So, <?cs if: X ?><script>X <?cs else ?><script>Y<?cs /if ?> is fine but, * * <?cs if: X ?><script>X <?cs elif: Y ?><script>Y<?cs /if ?> is not. */ AutoEscapeContext originalEscapedContext = autoEscapeContext.cloneCurrentEscapeContext(); // Save position of the start of if statement. int line = autoEscapeContext.getLineNumber(); int column = autoEscapeContext.getColumnNumber(); if (node.getBlock() != null) { node.getBlock().apply(this); } AutoEscapeContext.AutoEscapeState ifEndState = autoEscapeContext.getCurrentState(); // restore original context before executing else block autoEscapeContext = originalEscapedContext; // Interestingly, getOtherwise() is not null even when the if command // has no else branch. In such cases, getOtherwise() contains a // Noop command. // In practice this does not matter for the checks being run here. if (node.getOtherwise() != null) { node.getOtherwise().apply(this); } AutoEscapeContext.AutoEscapeState elseEndState = autoEscapeContext.getCurrentState(); if (!ifEndState.equals(elseEndState)) { throw new JSilverAutoEscapingException("'if/else' branches have different ending contexts " + ifEndState + " and " + elseEndState, templateName, line, column); } } @Override public void caseAEscapeCommand(AEscapeCommand node) { boolean saved_skip = skipAutoEscape; skipAutoEscape = true; node.getCommand().apply(this); skipAutoEscape = saved_skip; } @Override public void caseACallCommand(ACallCommand node) { saveAutoEscapingContext(node, node.getPosition()); } @Override public void caseALvarCommand(ALvarCommand node) { saveAutoEscapingContext(node, node.getPosition()); } @Override public void caseAEvarCommand(AEvarCommand node) { saveAutoEscapingContext(node, node.getPosition()); } @Override public void caseALincludeCommand(ALincludeCommand node) { saveAutoEscapingContext(node, node.getPosition()); } @Override public void caseAIncludeCommand(AIncludeCommand node) { saveAutoEscapingContext(node, node.getPosition()); } @Override public void caseAHardLincludeCommand(AHardLincludeCommand node) { saveAutoEscapingContext(node, node.getPosition()); } @Override public void caseAHardIncludeCommand(AHardIncludeCommand node) { saveAutoEscapingContext(node, node.getPosition()); } @Override public void caseAVarCommand(AVarCommand node) { applyAutoEscaping(node, node.getPosition()); } @Override public void caseAAltCommand(AAltCommand node) { applyAutoEscaping(node, node.getPosition()); } @Override public void caseANameCommand(ANameCommand node) { applyAutoEscaping(node, node.getPosition()); } @Override public void caseAUvarCommand(AUvarCommand node) { // Let parser know that was some text that it has not seen setCurrentPosition(node.getPosition()); autoEscapeContext.insertText(); } /** * Handles a <?cs content-type: "content type" ?> command. * * This command is used when the auto escaping context of a template cannot be determined from its * contents - for example, a CSS stylesheet or a javascript source file. Note that <?cs * content-type: ?> command is not required for all javascript and css templates. If the * template contains a <script> or <style> tag (or is included from another template * within the right tag), auto escaping will recognize the tag and switch context accordingly. On * the other hand, if the template serves a resource that is loaded via a <script src= > or * <link rel > command, the explicit <?cs content-type: ?> command would be required. */ @Override public void caseAContentTypeCommand(AContentTypeCommand node) { setCurrentPosition(node.getPosition()); String contentType = node.getString().getText(); // Strip out quotes around the string contentType = contentType.substring(1, contentType.length() - 1); autoEscapeContext.setContentType(contentType); contentTypeCalled = true; } private void applyAutoEscaping(PCommand node, PPosition position) { setCurrentPosition(position); if (skipAutoEscape) { return; } AStringExpression escapeExpr = new AStringExpression(new TString("\"" + getEscaping() + "\"")); AEscapeCommand escape = new AEscapeCommand(position, escapeExpr, (PCommand) node.clone()); node.replaceBy(escape); // Now that we have determined the correct escaping for this variable, // let parser know that there was some text that it has not seen. The // parser may choose to update its state based on this. autoEscapeContext.insertText(); } private void setCurrentPosition(PPosition position) { // Will eventually call caseACsOpenPosition position.apply(this); } @Override public void caseACsOpenPosition(ACsOpenPosition node) { Token token = node.getCsOpen(); autoEscapeContext.setCurrentPosition(token.getLine(), token.getPos()); } private void saveAutoEscapingContext(Node node, PPosition position) { setCurrentPosition(position); if (skipAutoEscape) { return; } EscapeMode mode = autoEscapeContext.getEscapeModeForCurrentState(); AStringExpression escapeStrategy = new AStringExpression(new TString("\"" + mode.getEscapeCommand() + "\"")); AAutoescapeCommand command = new AAutoescapeCommand(position, escapeStrategy, (PCommand) node.clone()); node.replaceBy(command); autoEscapeContext.insertText(); } private String getEscaping() { return autoEscapeContext.getEscapingFunctionForCurrentState(); } }