1/*
2 * Copyright (C) 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.clearsilver.jsilver.syntax;
18
19import com.google.clearsilver.jsilver.autoescape.AutoEscapeContext;
20import com.google.clearsilver.jsilver.autoescape.EscapeMode;
21import com.google.clearsilver.jsilver.exceptions.JSilverAutoEscapingException;
22import com.google.clearsilver.jsilver.syntax.analysis.DepthFirstAdapter;
23import com.google.clearsilver.jsilver.syntax.node.AAltCommand;
24import com.google.clearsilver.jsilver.syntax.node.AAutoescapeCommand;
25import com.google.clearsilver.jsilver.syntax.node.ACallCommand;
26import com.google.clearsilver.jsilver.syntax.node.AContentTypeCommand;
27import com.google.clearsilver.jsilver.syntax.node.ACsOpenPosition;
28import com.google.clearsilver.jsilver.syntax.node.ADataCommand;
29import com.google.clearsilver.jsilver.syntax.node.ADefCommand;
30import com.google.clearsilver.jsilver.syntax.node.AEscapeCommand;
31import com.google.clearsilver.jsilver.syntax.node.AEvarCommand;
32import com.google.clearsilver.jsilver.syntax.node.AHardIncludeCommand;
33import com.google.clearsilver.jsilver.syntax.node.AHardLincludeCommand;
34import com.google.clearsilver.jsilver.syntax.node.AIfCommand;
35import com.google.clearsilver.jsilver.syntax.node.AIncludeCommand;
36import com.google.clearsilver.jsilver.syntax.node.ALincludeCommand;
37import com.google.clearsilver.jsilver.syntax.node.ALvarCommand;
38import com.google.clearsilver.jsilver.syntax.node.ANameCommand;
39import com.google.clearsilver.jsilver.syntax.node.AStringExpression;
40import com.google.clearsilver.jsilver.syntax.node.AUvarCommand;
41import com.google.clearsilver.jsilver.syntax.node.AVarCommand;
42import com.google.clearsilver.jsilver.syntax.node.Node;
43import com.google.clearsilver.jsilver.syntax.node.PCommand;
44import com.google.clearsilver.jsilver.syntax.node.PPosition;
45import com.google.clearsilver.jsilver.syntax.node.Start;
46import com.google.clearsilver.jsilver.syntax.node.TCsOpen;
47import com.google.clearsilver.jsilver.syntax.node.TString;
48import com.google.clearsilver.jsilver.syntax.node.Token;
49
50/**
51 * Run a context parser (currently only HTML parser) over the AST, determine nodes that need
52 * escaping, and apply the appropriate escaping command to those nodes. The parser is fed literal
53 * data (from DataCommands), which it uses to track the context. When variables (e.g. VarCommand)
54 * are encountered, we query the parser for its current context, and apply the appropriate escaping
55 * command.
56 */
57public class AutoEscaper extends DepthFirstAdapter {
58
59  private AutoEscapeContext autoEscapeContext;
60  private boolean skipAutoEscape;
61  private final EscapeMode escapeMode;
62  private final String templateName;
63  private boolean contentTypeCalled;
64
65  /**
66   * Create an AutoEscaper, which will apply the specified escaping mode. If templateName is
67   * non-null, it will be used while displaying error messages.
68   *
69   * @param mode
70   * @param templateName
71   */
72  public AutoEscaper(EscapeMode mode, String templateName) {
73    this.templateName = templateName;
74    if (mode.equals(EscapeMode.ESCAPE_NONE)) {
75      throw new JSilverAutoEscapingException("AutoEscaper called when no escaping is required",
76          templateName);
77    }
78    escapeMode = mode;
79    if (mode.isAutoEscapingMode()) {
80      autoEscapeContext = new AutoEscapeContext(mode, templateName);
81      skipAutoEscape = false;
82    } else {
83      autoEscapeContext = null;
84    }
85  }
86
87  /**
88   * Create an AutoEscaper, which will apply the specified escaping mode. When possible, use
89   * #AutoEscaper(EscapeMode, String) instead. It specifies the template being auto escaped, which
90   * is useful when displaying error messages.
91   *
92   * @param mode
93   */
94  public AutoEscaper(EscapeMode mode) {
95    this(mode, null);
96  }
97
98  @Override
99  public void caseStart(Start start) {
100    if (!escapeMode.isAutoEscapingMode()) {
101      // For an explicit EscapeMode like {@code EscapeMode.ESCAPE_HTML}, we
102      // do not need to parse the rest of the tree. Instead, we just wrap the
103      // entire tree in a <?cs escape ?> node.
104      handleExplicitEscapeMode(start);
105    } else {
106      AutoEscapeContext.AutoEscapeState startState = autoEscapeContext.getCurrentState();
107      // call super.caseStart, which will make us visit the rest of the tree,
108      // so we can determine the appropriate escaping to apply for each
109      // variable.
110      super.caseStart(start);
111      AutoEscapeContext.AutoEscapeState endState = autoEscapeContext.getCurrentState();
112      if (!autoEscapeContext.isPermittedStateChangeForIncludes(startState, endState)) {
113        // If template contains a content-type command, the escaping context
114        // was intentionally changed. Such a change in context is fine as long
115        // as the current template is not included inside another. There is no
116        // way to verify that the template is not an include template however,
117        // so ignore the error and depend on developers doing the right thing.
118        if (contentTypeCalled) {
119          return;
120        }
121        // We do not permit templates to end in a different context than they start in.
122        // This is so that an included template does not modify the context of
123        // the template that includes it.
124        throw new JSilverAutoEscapingException("Template starts in context " + startState
125            + " but ends in different context " + endState, templateName);
126      }
127    }
128  }
129
130  private void handleExplicitEscapeMode(Start start) {
131    AStringExpression escapeExpr =
132        new AStringExpression(new TString("\"" + escapeMode.getEscapeCommand() + "\""));
133
134    PCommand node = start.getPCommand();
135    AEscapeCommand escape =
136        new AEscapeCommand(new ACsOpenPosition(new TCsOpen("<?cs ", 0, 0)), escapeExpr,
137            (PCommand) node.clone());
138
139    node.replaceBy(escape);
140  }
141
142  @Override
143  public void caseADataCommand(ADataCommand node) {
144    String data = node.getData().getText();
145    autoEscapeContext.setCurrentPosition(node.getData().getLine(), node.getData().getPos());
146    autoEscapeContext.parseData(data);
147  }
148
149  @Override
150  public void caseADefCommand(ADefCommand node) {
151  // Ignore the entire defcommand subtree, don't even parse it.
152  }
153
154  @Override
155  public void caseAIfCommand(AIfCommand node) {
156    setCurrentPosition(node.getPosition());
157
158    /*
159     * Since AutoEscaper is being applied while building the AST, and not during rendering, the html
160     * context of variables is sometimes ambiguous. For instance: <?cs if: X ?><script><?cs /if ?>
161     * <?cs var: MyVar ?>
162     *
163     * Here MyVar may require js escaping or html escaping depending on whether the "if" condition
164     * is true or false.
165     *
166     * To avoid such ambiguity, we require all branches of a conditional statement to end in the
167     * same context. So, <?cs if: X ?><script>X <?cs else ?><script>Y<?cs /if ?> is fine but,
168     *
169     * <?cs if: X ?><script>X <?cs elif: Y ?><script>Y<?cs /if ?> is not.
170     */
171    AutoEscapeContext originalEscapedContext = autoEscapeContext.cloneCurrentEscapeContext();
172    // Save position of the start of if statement.
173    int line = autoEscapeContext.getLineNumber();
174    int column = autoEscapeContext.getColumnNumber();
175
176    if (node.getBlock() != null) {
177      node.getBlock().apply(this);
178    }
179    AutoEscapeContext.AutoEscapeState ifEndState = autoEscapeContext.getCurrentState();
180    // restore original context before executing else block
181    autoEscapeContext = originalEscapedContext;
182
183    // Interestingly, getOtherwise() is not null even when the if command
184    // has no else branch. In such cases, getOtherwise() contains a
185    // Noop command.
186    // In practice this does not matter for the checks being run here.
187    if (node.getOtherwise() != null) {
188      node.getOtherwise().apply(this);
189    }
190    AutoEscapeContext.AutoEscapeState elseEndState = autoEscapeContext.getCurrentState();
191
192    if (!ifEndState.equals(elseEndState)) {
193      throw new JSilverAutoEscapingException("'if/else' branches have different ending contexts "
194          + ifEndState + " and " + elseEndState, templateName, line, column);
195    }
196  }
197
198  @Override
199  public void caseAEscapeCommand(AEscapeCommand node) {
200    boolean saved_skip = skipAutoEscape;
201    skipAutoEscape = true;
202    node.getCommand().apply(this);
203    skipAutoEscape = saved_skip;
204  }
205
206  @Override
207  public void caseACallCommand(ACallCommand node) {
208    saveAutoEscapingContext(node, node.getPosition());
209  }
210
211  @Override
212  public void caseALvarCommand(ALvarCommand node) {
213    saveAutoEscapingContext(node, node.getPosition());
214  }
215
216  @Override
217  public void caseAEvarCommand(AEvarCommand node) {
218    saveAutoEscapingContext(node, node.getPosition());
219  }
220
221  @Override
222  public void caseALincludeCommand(ALincludeCommand node) {
223    saveAutoEscapingContext(node, node.getPosition());
224  }
225
226  @Override
227  public void caseAIncludeCommand(AIncludeCommand node) {
228    saveAutoEscapingContext(node, node.getPosition());
229  }
230
231  @Override
232  public void caseAHardLincludeCommand(AHardLincludeCommand node) {
233    saveAutoEscapingContext(node, node.getPosition());
234  }
235
236  @Override
237  public void caseAHardIncludeCommand(AHardIncludeCommand node) {
238    saveAutoEscapingContext(node, node.getPosition());
239  }
240
241  @Override
242  public void caseAVarCommand(AVarCommand node) {
243    applyAutoEscaping(node, node.getPosition());
244  }
245
246  @Override
247  public void caseAAltCommand(AAltCommand node) {
248    applyAutoEscaping(node, node.getPosition());
249  }
250
251  @Override
252  public void caseANameCommand(ANameCommand node) {
253    applyAutoEscaping(node, node.getPosition());
254  }
255
256  @Override
257  public void caseAUvarCommand(AUvarCommand node) {
258    // Let parser know that was some text that it has not seen
259    setCurrentPosition(node.getPosition());
260    autoEscapeContext.insertText();
261  }
262
263  /**
264   * Handles a &lt;?cs content-type: "content type" ?&gt; command.
265   *
266   * This command is used when the auto escaping context of a template cannot be determined from its
267   * contents - for example, a CSS stylesheet or a javascript source file. Note that &lt;?cs
268   * content-type: ?&gt; command is not required for all javascript and css templates. If the
269   * template contains a &lt;script&gt; or &lt;style&gt; tag (or is included from another template
270   * within the right tag), auto escaping will recognize the tag and switch context accordingly. On
271   * the other hand, if the template serves a resource that is loaded via a &lt;script src= &gt; or
272   * &lt;link rel &gt; command, the explicit &lt;?cs content-type: ?&gt; command would be required.
273   */
274  @Override
275  public void caseAContentTypeCommand(AContentTypeCommand node) {
276    setCurrentPosition(node.getPosition());
277    String contentType = node.getString().getText();
278    // Strip out quotes around the string
279    contentType = contentType.substring(1, contentType.length() - 1);
280    autoEscapeContext.setContentType(contentType);
281    contentTypeCalled = true;
282  }
283
284  private void applyAutoEscaping(PCommand node, PPosition position) {
285    setCurrentPosition(position);
286    if (skipAutoEscape) {
287      return;
288    }
289
290    AStringExpression escapeExpr = new AStringExpression(new TString("\"" + getEscaping() + "\""));
291    AEscapeCommand escape = new AEscapeCommand(position, escapeExpr, (PCommand) node.clone());
292
293    node.replaceBy(escape);
294    // Now that we have determined the correct escaping for this variable,
295    // let parser know that there was some text that it has not seen. The
296    // parser may choose to update its state based on this.
297    autoEscapeContext.insertText();
298
299  }
300
301  private void setCurrentPosition(PPosition position) {
302    // Will eventually call caseACsOpenPosition
303    position.apply(this);
304  }
305
306  @Override
307  public void caseACsOpenPosition(ACsOpenPosition node) {
308    Token token = node.getCsOpen();
309    autoEscapeContext.setCurrentPosition(token.getLine(), token.getPos());
310  }
311
312  private void saveAutoEscapingContext(Node node, PPosition position) {
313    setCurrentPosition(position);
314    if (skipAutoEscape) {
315      return;
316    }
317    EscapeMode mode = autoEscapeContext.getEscapeModeForCurrentState();
318    AStringExpression escapeStrategy =
319        new AStringExpression(new TString("\"" + mode.getEscapeCommand() + "\""));
320    AAutoescapeCommand command =
321        new AAutoescapeCommand(position, escapeStrategy, (PCommand) node.clone());
322    node.replaceBy(command);
323    autoEscapeContext.insertText();
324  }
325
326  private String getEscaping() {
327    return autoEscapeContext.getEscapingFunctionForCurrentState();
328  }
329}
330