/*
* 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();
}
}