Home | History | Annotate | Download | only in syntax
      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 
     17 package com.google.clearsilver.jsilver.syntax;
     18 
     19 import com.google.clearsilver.jsilver.autoescape.AutoEscapeContext;
     20 import com.google.clearsilver.jsilver.autoescape.EscapeMode;
     21 import com.google.clearsilver.jsilver.exceptions.JSilverAutoEscapingException;
     22 import com.google.clearsilver.jsilver.syntax.analysis.DepthFirstAdapter;
     23 import com.google.clearsilver.jsilver.syntax.node.AAltCommand;
     24 import com.google.clearsilver.jsilver.syntax.node.AAutoescapeCommand;
     25 import com.google.clearsilver.jsilver.syntax.node.ACallCommand;
     26 import com.google.clearsilver.jsilver.syntax.node.AContentTypeCommand;
     27 import com.google.clearsilver.jsilver.syntax.node.ACsOpenPosition;
     28 import com.google.clearsilver.jsilver.syntax.node.ADataCommand;
     29 import com.google.clearsilver.jsilver.syntax.node.ADefCommand;
     30 import com.google.clearsilver.jsilver.syntax.node.AEscapeCommand;
     31 import com.google.clearsilver.jsilver.syntax.node.AEvarCommand;
     32 import com.google.clearsilver.jsilver.syntax.node.AHardIncludeCommand;
     33 import com.google.clearsilver.jsilver.syntax.node.AHardLincludeCommand;
     34 import com.google.clearsilver.jsilver.syntax.node.AIfCommand;
     35 import com.google.clearsilver.jsilver.syntax.node.AIncludeCommand;
     36 import com.google.clearsilver.jsilver.syntax.node.ALincludeCommand;
     37 import com.google.clearsilver.jsilver.syntax.node.ALvarCommand;
     38 import com.google.clearsilver.jsilver.syntax.node.ANameCommand;
     39 import com.google.clearsilver.jsilver.syntax.node.AStringExpression;
     40 import com.google.clearsilver.jsilver.syntax.node.AUvarCommand;
     41 import com.google.clearsilver.jsilver.syntax.node.AVarCommand;
     42 import com.google.clearsilver.jsilver.syntax.node.Node;
     43 import com.google.clearsilver.jsilver.syntax.node.PCommand;
     44 import com.google.clearsilver.jsilver.syntax.node.PPosition;
     45 import com.google.clearsilver.jsilver.syntax.node.Start;
     46 import com.google.clearsilver.jsilver.syntax.node.TCsOpen;
     47 import com.google.clearsilver.jsilver.syntax.node.TString;
     48 import 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  */
     57 public 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