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 */ AutoEscaper(EscapeMode mode, String templateName)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 */ AutoEscaper(EscapeMode mode)94 public AutoEscaper(EscapeMode mode) { 95 this(mode, null); 96 } 97 98 @Override caseStart(Start start)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 handleExplicitEscapeMode(Start start)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 caseADataCommand(ADataCommand node)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 caseADefCommand(ADefCommand node)150 public void caseADefCommand(ADefCommand node) { 151 // Ignore the entire defcommand subtree, don't even parse it. 152 } 153 154 @Override caseAIfCommand(AIfCommand node)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 caseAEscapeCommand(AEscapeCommand node)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 caseACallCommand(ACallCommand node)207 public void caseACallCommand(ACallCommand node) { 208 saveAutoEscapingContext(node, node.getPosition()); 209 } 210 211 @Override caseALvarCommand(ALvarCommand node)212 public void caseALvarCommand(ALvarCommand node) { 213 saveAutoEscapingContext(node, node.getPosition()); 214 } 215 216 @Override caseAEvarCommand(AEvarCommand node)217 public void caseAEvarCommand(AEvarCommand node) { 218 saveAutoEscapingContext(node, node.getPosition()); 219 } 220 221 @Override caseALincludeCommand(ALincludeCommand node)222 public void caseALincludeCommand(ALincludeCommand node) { 223 saveAutoEscapingContext(node, node.getPosition()); 224 } 225 226 @Override caseAIncludeCommand(AIncludeCommand node)227 public void caseAIncludeCommand(AIncludeCommand node) { 228 saveAutoEscapingContext(node, node.getPosition()); 229 } 230 231 @Override caseAHardLincludeCommand(AHardLincludeCommand node)232 public void caseAHardLincludeCommand(AHardLincludeCommand node) { 233 saveAutoEscapingContext(node, node.getPosition()); 234 } 235 236 @Override caseAHardIncludeCommand(AHardIncludeCommand node)237 public void caseAHardIncludeCommand(AHardIncludeCommand node) { 238 saveAutoEscapingContext(node, node.getPosition()); 239 } 240 241 @Override caseAVarCommand(AVarCommand node)242 public void caseAVarCommand(AVarCommand node) { 243 applyAutoEscaping(node, node.getPosition()); 244 } 245 246 @Override caseAAltCommand(AAltCommand node)247 public void caseAAltCommand(AAltCommand node) { 248 applyAutoEscaping(node, node.getPosition()); 249 } 250 251 @Override caseANameCommand(ANameCommand node)252 public void caseANameCommand(ANameCommand node) { 253 applyAutoEscaping(node, node.getPosition()); 254 } 255 256 @Override caseAUvarCommand(AUvarCommand node)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 <?cs content-type: "content type" ?> 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 <?cs 268 * content-type: ?> command is not required for all javascript and css templates. If the 269 * template contains a <script> or <style> 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 <script src= > or 272 * <link rel > command, the explicit <?cs content-type: ?> command would be required. 273 */ 274 @Override caseAContentTypeCommand(AContentTypeCommand node)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 applyAutoEscaping(PCommand node, PPosition position)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 setCurrentPosition(PPosition position)301 private void setCurrentPosition(PPosition position) { 302 // Will eventually call caseACsOpenPosition 303 position.apply(this); 304 } 305 306 @Override caseACsOpenPosition(ACsOpenPosition node)307 public void caseACsOpenPosition(ACsOpenPosition node) { 308 Token token = node.getCsOpen(); 309 autoEscapeContext.setCurrentPosition(token.getLine(), token.getPos()); 310 } 311 saveAutoEscapingContext(Node node, PPosition position)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 getEscaping()326 private String getEscaping() { 327 return autoEscapeContext.getEscapingFunctionForCurrentState(); 328 } 329 } 330