OLD | NEW |
1 // Copyright (C) 2007 Google Inc. | 1 // Copyright (C) 2009 Google Inc. |
2 // | 2 // |
3 // Licensed under the Apache License, Version 2.0 (the "License"); | 3 // Licensed under the Apache License, Version 2.0 (the "License"); |
4 // you may not use this file except in compliance with the License. | 4 // you may not use this file except in compliance with the License. |
5 // You may obtain a copy of the License at | 5 // You may obtain a copy of the License at |
6 // | 6 // |
7 // http://www.apache.org/licenses/LICENSE-2.0 | 7 // http://www.apache.org/licenses/LICENSE-2.0 |
8 // | 8 // |
9 // Unless required by applicable law or agreed to in writing, software | 9 // Unless required by applicable law or agreed to in writing, software |
10 // distributed under the License is distributed on an "AS IS" BASIS, | 10 // distributed under the License is distributed on an "AS IS" BASIS, |
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 // See the License for the specific language governing permissions and | 12 // See the License for the specific language governing permissions and |
13 // limitations under the License. | 13 // limitations under the License. |
14 | 14 |
15 package com.google.caja.plugin; | 15 package com.google.caja.plugin.templates; |
16 | 16 |
17 import com.google.caja.CajaException; | |
18 import com.google.caja.lang.css.CssSchema; | 17 import com.google.caja.lang.css.CssSchema; |
19 import com.google.caja.lang.html.HTML; | 18 import com.google.caja.lang.html.HTML; |
20 import com.google.caja.lang.html.HtmlSchema; | 19 import com.google.caja.lang.html.HtmlSchema; |
21 import com.google.caja.lexer.CharProducer; | 20 import com.google.caja.lexer.CharProducer; |
22 import com.google.caja.lexer.CssTokenType; | 21 import com.google.caja.lexer.CssTokenType; |
23 import com.google.caja.lexer.ExternalReference; | 22 import com.google.caja.lexer.ExternalReference; |
24 import com.google.caja.lexer.FilePosition; | 23 import com.google.caja.lexer.FilePosition; |
25 import com.google.caja.lexer.JsLexer; | 24 import com.google.caja.lexer.JsLexer; |
26 import com.google.caja.lexer.JsTokenQueue; | 25 import com.google.caja.lexer.JsTokenQueue; |
27 import com.google.caja.lexer.Keyword; | 26 import com.google.caja.lexer.Keyword; |
28 import com.google.caja.lexer.ParseException; | 27 import com.google.caja.lexer.ParseException; |
29 import com.google.caja.lexer.TokenQueue; | 28 import com.google.caja.lexer.TokenQueue; |
30 import com.google.caja.lexer.TokenConsumer; | 29 import com.google.caja.lexer.escaping.UriUtil; |
31 import com.google.caja.parser.AncestorChain; | 30 import com.google.caja.parser.AncestorChain; |
32 import com.google.caja.parser.ParseTreeNode; | 31 import com.google.caja.parser.ParseTreeNode; |
| 32 import com.google.caja.parser.ParseTreeNodeContainer; |
33 import com.google.caja.parser.Visitor; | 33 import com.google.caja.parser.Visitor; |
34 import com.google.caja.parser.css.CssParser; | 34 import com.google.caja.parser.css.CssParser; |
35 import com.google.caja.parser.css.CssTree; | 35 import com.google.caja.parser.css.CssTree; |
36 import com.google.caja.parser.html.Nodes; | 36 import com.google.caja.parser.html.Nodes; |
37 import com.google.caja.parser.js.Block; | 37 import com.google.caja.parser.js.Block; |
38 import com.google.caja.parser.js.Expression; | 38 import com.google.caja.parser.js.Expression; |
39 import com.google.caja.parser.js.ExpressionStmt; | |
40 import com.google.caja.parser.js.FormalParam; | |
41 import com.google.caja.parser.js.FunctionConstructor; | 39 import com.google.caja.parser.js.FunctionConstructor; |
42 import com.google.caja.parser.js.Identifier; | 40 import com.google.caja.parser.js.Identifier; |
43 import com.google.caja.parser.js.Operation; | |
44 import com.google.caja.parser.js.Operator; | |
45 import com.google.caja.parser.js.Parser; | 41 import com.google.caja.parser.js.Parser; |
46 import com.google.caja.parser.js.Reference; | 42 import com.google.caja.parser.js.Reference; |
47 import com.google.caja.parser.js.Statement; | 43 import com.google.caja.parser.js.Statement; |
48 import com.google.caja.parser.js.StringLiteral; | 44 import com.google.caja.parser.js.StringLiteral; |
49 import com.google.caja.parser.js.TryStmt; | 45 import com.google.caja.parser.js.SyntheticNodes; |
| 46 import com.google.caja.parser.js.UncajoledModule; |
50 import com.google.caja.parser.quasiliteral.QuasiBuilder; | 47 import com.google.caja.parser.quasiliteral.QuasiBuilder; |
51 import com.google.caja.parser.quasiliteral.ReservedNames; | 48 import com.google.caja.parser.quasiliteral.ReservedNames; |
52 import com.google.caja.plugin.stages.RewriteHtmlStage; | 49 import com.google.caja.plugin.CssRewriter; |
53 import com.google.caja.reporting.Message; | 50 import com.google.caja.plugin.CssValidator; |
| 51 import com.google.caja.plugin.ExtractedHtmlContent; |
| 52 import com.google.caja.plugin.PluginMeta; |
54 import com.google.caja.reporting.MessageContext; | 53 import com.google.caja.reporting.MessageContext; |
55 import com.google.caja.reporting.MessageLevel; | 54 import com.google.caja.reporting.MessageLevel; |
56 import com.google.caja.reporting.MessagePart; | 55 import com.google.caja.reporting.MessagePart; |
57 import com.google.caja.reporting.MessageQueue; | 56 import com.google.caja.reporting.MessageQueue; |
58 import com.google.caja.reporting.MessageType; | |
59 import com.google.caja.reporting.RenderContext; | 57 import com.google.caja.reporting.RenderContext; |
60 import com.google.caja.util.Name; | 58 import com.google.caja.util.Name; |
61 import com.google.caja.util.Pair; | 59 import com.google.caja.util.Pair; |
62 import static com.google.caja.parser.js.SyntheticNodes.s; | |
63 | 60 |
64 import java.io.StringReader; | 61 import java.io.StringReader; |
65 import java.net.URI; | 62 import java.net.URI; |
66 import java.net.URISyntaxException; | 63 import java.net.URISyntaxException; |
| 64 |
67 import java.util.ArrayList; | 65 import java.util.ArrayList; |
68 import java.util.Arrays; | |
69 import java.util.Collection; | |
70 import java.util.Collections; | 66 import java.util.Collections; |
71 import java.util.LinkedHashMap; | 67 import java.util.HashMap; |
| 68 import java.util.IdentityHashMap; |
72 import java.util.List; | 69 import java.util.List; |
73 import java.util.Map; | 70 import java.util.Map; |
74 import java.util.regex.Pattern; | 71 import java.util.regex.Pattern; |
75 | 72 |
76 import org.w3c.dom.Attr; | 73 import org.w3c.dom.Attr; |
| 74 import org.w3c.dom.Document; |
| 75 import org.w3c.dom.DocumentFragment; |
77 import org.w3c.dom.Element; | 76 import org.w3c.dom.Element; |
78 import org.w3c.dom.Node; | 77 import org.w3c.dom.Node; |
| 78 import org.w3c.dom.Text; |
79 | 79 |
80 /** | 80 /** |
81 * Compiles HTML containing CSS and JavaScript to Javascript + safe CSS. | 81 * Compiles an HTML document to a chunk of safe static HTML, and a bit of |
82 * This takes in a DOM, and outputs javascript that will render the DOM. | 82 * javascript which attaches event handlers and other dynamic attributes, and |
83 * The resulting javascript requires "html-emitter.js" which builds the DOM | 83 * executes inline scripts. |
84 * client side. | 84 * |
| 85 * <p> |
| 86 * Requires that CSS be rewritten, that inline scripts have been |
| 87 * {@link ExtractedHtmlContent extracted}, and that the output JS be run through |
| 88 * the CajitaRewriter. |
85 * | 89 * |
86 * @author mikesamuel@gmail.com | 90 * @author mikesamuel@gmail.com |
87 */ | 91 */ |
88 public class HtmlCompiler { | 92 public class TemplateCompiler { |
89 public static final class BadContentException extends CajaException { | 93 private final List<Node> ihtmlRoots; |
90 private static final long serialVersionUID = -5317800396186044550L; | 94 private final List<CssTree.StyleSheet> safeStylesheets; |
91 BadContentException(Message m) { super(m); } | |
92 BadContentException(Message m, Throwable th) { super(m, th); } | |
93 } | |
94 | |
95 private final CssSchema cssSchema; | 95 private final CssSchema cssSchema; |
96 private final HtmlSchema htmlSchema; | 96 private final HtmlSchema htmlSchema; |
| 97 private final PluginMeta meta; |
97 private final MessageContext mc; | 98 private final MessageContext mc; |
98 private final MessageQueue mq; | 99 private final MessageQueue mq; |
99 private final PluginMeta meta; | 100 /** |
100 private Map<String, Statement> eventHandlers = | 101 * Maps {@link Node}s to JS parse trees. |
101 new LinkedHashMap<String, Statement>(); | 102 * If the value is null, then the literal value is fine. |
102 | 103 * If the node is an Element, then the value is an expression that returns |
103 public HtmlCompiler(CssSchema cssSchema, HtmlSchema htmlSchema, | 104 * a tag name. If the node is an attribute, then the value is an expression |
104 MessageContext mc, MessageQueue mq, PluginMeta meta) { | 105 * that returns a key, value pair. If the node is a text node inside a |
105 if (null == cssSchema || null == htmlSchema || null == mq || null == meta) { | 106 * script block, then the value is an {@link UncajoledModule}, but otherwise |
106 throw new NullPointerException(); | 107 * the value is an expression specify the dynamic text value. |
107 } | 108 */ |
| 109 private final Map<Node, ParseTreeNode> scriptsPerNode |
| 110 = new IdentityHashMap<Node, ParseTreeNode>(); |
| 111 /** Extracted event handler functions. */ |
| 112 private final List<Statement> handlers = new ArrayList<Statement>(); |
| 113 /** Maps handler attribute source to handler names. */ |
| 114 private final Map<String, String> handlerCache |
| 115 = new HashMap<String, String>(); |
| 116 |
| 117 /** |
| 118 * @param ihtmlRoots roots of trees to process. |
| 119 * @param safeStylesheets CSS style-sheets that have had unsafe |
| 120 * constructs removed and had rules rewritten. |
| 121 * @param meta specifies how URLs and other attributes are rewritten. |
| 122 * @param cssSchema specifies how STYLE attributes are rewritten. |
| 123 * @param htmlSchema specifies how elements and attributes are handled. |
| 124 * @param mq receives messages about invalid attribute values. |
| 125 */ |
| 126 public TemplateCompiler( |
| 127 List<? extends Node> ihtmlRoots, |
| 128 List<? extends CssTree.StyleSheet> safeStylesheets, |
| 129 CssSchema cssSchema, HtmlSchema htmlSchema, |
| 130 PluginMeta meta, MessageContext mc, MessageQueue mq) { |
| 131 this.ihtmlRoots = new ArrayList<Node>(ihtmlRoots); |
| 132 this.safeStylesheets = new ArrayList<CssTree.StyleSheet>(safeStylesheets); |
108 this.cssSchema = cssSchema; | 133 this.cssSchema = cssSchema; |
109 this.htmlSchema = htmlSchema; | 134 this.htmlSchema = htmlSchema; |
| 135 this.meta = meta; |
110 this.mc = mc; | 136 this.mc = mc; |
111 this.mq = mq; | 137 this.mq = mq; |
112 this.meta = meta; | 138 } |
113 } | 139 |
114 | 140 /** |
115 /** | 141 * Examines the HTML document and writes messages about problematic portions |
116 * Compiles a document to a javascript function. | 142 * to the message queue passed to the constructor. |
117 * | 143 */ |
118 * <p>This method extracts embedded javascript but performs no validation on | 144 public void inspect() { |
119 * it.</p> | 145 if (!mq.hasMessageAtLevel(MessageLevel.FATAL_ERROR)) { |
120 */ | 146 for (Node ihtmlRoot : ihtmlRoots) { |
121 public Block compileDocument(Node doc) throws BadContentException { | 147 inspect(ihtmlRoot, Name.html("div")); |
122 // Produce calls to IMPORTS___.htmlEmitter___(...) | 148 } |
123 // with interleaved script bodies. | 149 } |
124 DomProcessingEvents cdom = new DomProcessingEvents(); | 150 } |
125 compileDom(doc, cdom); | 151 |
126 | 152 private void inspect(Node n, Name containingHtmlElement) { |
127 Block body = new Block( | 153 switch (n.getNodeType()) { |
128 Nodes.getFilePositionFor(doc), Collections.<Statement>emptyList()); | 154 case Node.ELEMENT_NODE: |
129 cdom.toJavascript(body); | 155 inspectElement((Element) n, containingHtmlElement); |
130 return body; | 156 break; |
131 } | 157 case Node.TEXT_NODE: case Node.CDATA_SECTION_NODE: |
132 | 158 inspectText((Text) n, containingHtmlElement); |
133 public Collection<? extends Statement> getEventHandlers() { | 159 break; |
134 return eventHandlers.values(); | |
135 } | |
136 | |
137 /** | |
138 * Appends to block, statements that will send events to out that can be used | |
139 * to reproduce t on the browser. | |
140 * | |
141 * @param t the tree to render | |
142 */ | |
143 private void compileDom(Node t, DomProcessingEvents out) | |
144 throws BadContentException { | |
145 switch (t.getNodeType()) { | |
146 case Node.DOCUMENT_FRAGMENT_NODE: | 160 case Node.DOCUMENT_FRAGMENT_NODE: |
147 for (Node c = t.getFirstChild(); c != null; c = c.getNextSibling()) { | 161 inspectFragment((DocumentFragment) n, containingHtmlElement); |
148 compileDom(c, out); | 162 break; |
149 } | 163 default: |
150 break; | 164 // Since they don't show in the scriptsPerNode map, they won't appear in |
151 case Node.TEXT_NODE: | 165 // any output trees. |
152 case Node.CDATA_SECTION_NODE: | 166 break; |
153 out.pcdata(Nodes.getFilePositionFor(t), t.getNodeValue()); | 167 } |
154 break; | 168 } |
155 case Node.ELEMENT_NODE: | 169 |
156 Element el = (Element) t; | 170 /** |
157 Name tagName = Name.xml(el.getTagName()); | 171 * @param containingHtmlElement the name of the HTML element containing el. |
158 | 172 * If the HTML element is contained inside a template construct then this |
159 if ("span".equals(tagName.getCanonicalForm())) { | 173 * name may differ from el's immediate parent. |
160 Block extractedScriptBody = RewriteHtmlStage.extractedScriptFor(el); | 174 */ |
161 if (extractedScriptBody != null) { | 175 private void inspectElement(Element el, Name containingHtmlElement) { |
162 out.script(scriptBodyEnvelope(extractedScriptBody)); | 176 Name elName = Name.html(el.getTagName()); |
| 177 |
| 178 // Recurse early so that ihtml:dynamic elements have been parsed before we |
| 179 // process the attributes element list. |
| 180 for (Node child : Nodes.childrenOf(el)) { |
| 181 inspect(child, elName); |
| 182 } |
| 183 |
| 184 // For each attribute allowed on this element type, ensure that |
| 185 // (1) If it is not specified, and its default value is not allowed, then |
| 186 // it is added with a known safe value. |
| 187 // (2) Its value is rewritten as appropriate. |
| 188 // We don't have to worry about disallowed attributes since those will |
| 189 // not be present in scriptsPerNode. The TemplateSanitizer should have |
| 190 // stripped those out. |
| 191 HtmlSchema schema = htmlSchema; |
| 192 if (!schema.isElementAllowed(elName)) { return; } |
| 193 |
| 194 HTML.Element elInfo = schema.lookupElement(elName); |
| 195 List<HTML.Attribute> attrs = elInfo.getAttributes(); |
| 196 if (attrs != null) { |
| 197 for (HTML.Attribute a : attrs) { |
| 198 Name attrName = a.getAttributeName(); |
| 199 if (!schema.isAttributeAllowed(elName, attrName)) { continue; } |
| 200 HTML.Attribute attrInfo = schema.lookupAttribute(elName, attrName); |
| 201 String attrNameStr = attrName.getCanonicalForm(); |
| 202 Attr attr = null; |
| 203 if (el.hasAttribute(attrNameStr) |
| 204 && attrInfo.getValueCriterion().accept( |
| 205 el.getAttribute(attrNameStr))) { |
| 206 attr = el.getAttributeNode(attrNameStr); |
| 207 } else if ((a.getDefaultValue() != null |
| 208 && !a.getValueCriterion().accept(a.getDefaultValue())) |
| 209 || !a.isOptional()) { |
| 210 attr = el.getOwnerDocument().createAttribute(attrNameStr); |
| 211 String safeValue; |
| 212 if (a.getType() == HTML.Attribute.Type.URI) { |
| 213 safeValue = Nodes.getFilePositionFor(el) |
| 214 .source().getUri().toString(); |
| 215 } else { |
| 216 safeValue = a.getSafeValue(); |
| 217 } |
| 218 attr.setNodeValue(safeValue); |
| 219 el.setAttributeNode(attr); |
| 220 } |
| 221 if (attr != null) { |
| 222 inspectHtmlAttribute(attr, attrInfo); |
| 223 } |
| 224 } |
| 225 } |
| 226 scriptsPerNode.put(el, null); |
| 227 } |
| 228 |
| 229 private void inspectText(Text t, Name containingHtmlElement) { |
| 230 if (!htmlSchema.isElementAllowed(containingHtmlElement)) { return; } |
| 231 scriptsPerNode.put(t, null); |
| 232 } |
| 233 |
| 234 private void inspectFragment(DocumentFragment f, Name containingHtmlElement) { |
| 235 scriptsPerNode.put(f, null); |
| 236 for (Node child : Nodes.childrenOf(f)) { |
| 237 // We know that top level text nodes in a document fragment |
| 238 // are not significant if they are just newlines and indentation. |
| 239 // This decreases output size significantly. |
| 240 if (isWhitespaceOnlyTextNode(child)) { continue; } |
| 241 inspect(child, containingHtmlElement); |
| 242 } |
| 243 } |
| 244 private static boolean isWhitespaceOnlyTextNode(Node child) { |
| 245 return child.getNodeType() == Node.TEXT_NODE // excludes CDATA sections |
| 246 && "".equals(child.getNodeValue().replaceAll("[\r\n]+[ \t]*", "")); |
| 247 } |
| 248 |
| 249 /** |
| 250 * For an HTML attribute, decides whether the value is valid according to the |
| 251 * schema and if it is valid, sets a value into {@link #scriptsPerNode}. |
| 252 * The expression is null if the current value is fine, or a StringLiteral |
| 253 * if it can be statically rewritten. |
| 254 */ |
| 255 private void inspectHtmlAttribute(Attr attr, HTML.Attribute info) { |
| 256 FilePosition pos = Nodes.getFilePositionForValue(attr); |
| 257 String value = attr.getValue(); |
| 258 |
| 259 Expression dynamicValue; |
| 260 switch (info.getType()) { |
| 261 case CLASSES: |
| 262 if (!checkRestrictedNames(value, pos)) { return; } |
| 263 dynamicValue = null; |
| 264 break; |
| 265 case FRAME_TARGET: |
| 266 case LOCAL_NAME: |
| 267 if (!checkRestrictedName(value, pos)) { return; } |
| 268 dynamicValue = null; |
| 269 break; |
| 270 case GLOBAL_NAME: |
| 271 case ID: |
| 272 case IDREF: |
| 273 if (!checkRestrictedName(value, pos)) { return; } |
| 274 dynamicValue = rewriteIdentifiers(pos, value); |
| 275 break; |
| 276 case IDREFS: |
| 277 if (!checkRestrictedNames(value, pos)) { return; } |
| 278 dynamicValue = rewriteIdentifiers(pos, value); |
| 279 break; |
| 280 case NONE: |
| 281 dynamicValue = null; |
| 282 break; |
| 283 case SCRIPT: |
| 284 String handlerFnName = handlerCache.get(attr.getValue()); |
| 285 if (handlerFnName == null) { |
| 286 Block b; |
| 287 try { |
| 288 b = parseJsFromAttrValue(attr); |
| 289 } catch (ParseException ex) { |
| 290 ex.toMessageQueue(mq); |
163 return; | 291 return; |
164 } | 292 } |
165 } else if ("style".equals(tagName.getCanonicalForm())) { | 293 if (b.children().isEmpty()) { return; } |
166 // nothing to do. Style tags get combined into one and output as | 294 rewriteEventHandlerReferences(b); |
167 // CSS, not written via javascript. | 295 |
| 296 handlerFnName = meta.generateUniqueName("c"); |
| 297 handlers.add(QuasiUtil.quasiStmt( |
| 298 "" |
| 299 + "IMPORTS___.@handlerName = function (" |
| 300 + " event, " + ReservedNames.THIS_NODE + ") { @body*; };", |
| 301 "handlerName", new Reference(SyntheticNodes.s( |
| 302 new Identifier(FilePosition.UNKNOWN, handlerFnName))), |
| 303 "body", new ParseTreeNodeContainer(b.children()))); |
| 304 handlerCache.put(attr.getValue(), handlerFnName); |
| 305 } |
| 306 |
| 307 dynamicValue = (Expression) QuasiBuilder.substV( |
| 308 "'return plugin_dispatchEvent___(" |
| 309 + "this, event, ' + ___./*@synthetic*/getId(IMPORTS___) + @tail", |
| 310 "tail", StringLiteral.valueOf( |
| 311 pos, ", " + StringLiteral.toQuotedValue(handlerFnName) + ");")); |
| 312 break; |
| 313 case STYLE: |
| 314 CssTree.DeclarationGroup decls; |
| 315 try { |
| 316 decls = parseStyleAttrib(attr); |
| 317 if (decls == null) { return; } |
| 318 } catch (ParseException ex) { |
| 319 ex.toMessageQueue(mq); |
168 return; | 320 return; |
169 } | 321 } |
170 | 322 |
171 assertNotBlacklistedTag(el); | 323 // The validator will check that property values are well-formed, |
172 | 324 // marking those that aren't, and identifies all URLs. |
173 DomAttributeConstraint constraint = | 325 CssValidator v = new CssValidator(cssSchema, htmlSchema, mq) |
174 DomAttributeConstraint.Factory.forTag(tagName); | 326 .withInvalidNodeMessageLevel(MessageLevel.WARNING); |
175 | 327 v.validateCss(AncestorChain.instance(decls)); |
176 tagName = assertHtmlIdentifier(tagName, el); | 328 // The rewriter will remove any unsafe constructs. |
177 boolean requiresCloseTag = requiresCloseTag(tagName); | 329 // and put URLs in the proper filename namespace |
178 constraint.startTag(el); | 330 new CssRewriter(meta, mq) |
179 | 331 .withInvalidNodeMessageLevel(MessageLevel.WARNING) |
180 out.begin(Nodes.getFilePositionFor(el), tagName); | 332 .rewrite(AncestorChain.instance(decls)); |
181 | 333 |
182 // output attributes | 334 StringBuilder css = new StringBuilder(); |
183 for (Attr attrib : Nodes.attributesOf(el)) { | 335 RenderContext rc = new RenderContext(decls.makeRenderer(css, null)); |
184 Name name = Name.xml(attrib.getNodeName()); | 336 decls.render(rc); |
185 | 337 rc.getOut().noMoreTokens(); |
186 assertNotBlacklistedAttrib(tagName, attrib); | 338 |
187 | 339 dynamicValue = StringLiteral.valueOf(pos, css); |
188 name = assertHtmlIdentifier(name, attrib); | 340 break; |
189 | 341 case URI: |
190 Pair<String, String> wrapper = constraint.attributeValueHtml(name); | 342 try { |
191 if (null == wrapper) { continue; } | 343 URI uri = new URI(value); |
192 | 344 ExternalReference ref = new ExternalReference( |
193 if ("style".equals(name.getCanonicalForm())) { | 345 pos.source().getUri().resolve(uri), pos); |
194 compileStyleAttrib(attrib, out); | 346 String rewrittenUri = meta.getPluginEnvironment() |
195 } else { | 347 .rewriteUri(ref, info.getMimeTypes()); |
196 AttributeXform xform = xformForAttribute(tagName, name); | 348 if (rewrittenUri == null) { |
197 | 349 mq.addMessage( |
198 Attr temp = el.getOwnerDocument().createAttribute( | 350 IhtmlMessageType.MALFORMED_URI, |
199 name.getCanonicalForm()); | 351 MessagePart.Factory.valueOf(uri.toString())); |
200 temp.setNodeValue(wrapper.a + attrib.getNodeValue() + wrapper.b); | 352 return; |
201 Nodes.setFilePositionFor(temp, Nodes.getFilePositionFor(attrib)); | |
202 Nodes.setFilePositionForValue( | |
203 temp, Nodes.getFilePositionForValue(attrib)); | |
204 | |
205 if (null == xform) { | |
206 out.attr( | |
207 Nodes.getFilePositionFor(temp), name, temp.getNodeValue()); | |
208 } else { | |
209 xform.apply(tagName, temp, this, out); | |
210 } | |
211 } | 353 } |
212 constraint.attributeDone(name); | 354 rewrittenUri = UriUtil.normalizeUri(rewrittenUri); |
213 } | 355 dynamicValue = StringLiteral.valueOf( |
214 | 356 ref.getReferencePosition(), rewrittenUri); |
215 for (Pair<Name, String> extra : constraint.tagDone(el)) { | 357 } catch (URISyntaxException ex) { |
216 out.attr(FilePosition.UNKNOWN, extra.a, extra.b); | 358 mq.addMessage( |
217 } | 359 IhtmlMessageType.MALFORMED_URI, pos, |
218 | 360 MessagePart.Factory.valueOf(value)); |
219 boolean tagAllowsContent = tagAllowsContent(tagName); | 361 return; |
220 out.finishAttrs(!(requiresCloseTag || tagAllowsContent)); | 362 } |
221 | 363 break; |
222 Iterable<? extends Node> children = Nodes.childrenOf(el); | 364 default: |
223 | 365 throw new RuntimeException(info.getType().name()); |
224 // recurse to contents | 366 } |
225 boolean wroteChildElement = false; | 367 scriptsPerNode.put(attr, dynamicValue); |
226 | 368 } |
227 if (tagAllowsContent) { | 369 |
228 for (Node child : children) { | 370 private static final Pattern IDENTIFIER_SEPARATOR = Pattern.compile("\\s+"); |
229 compileDom(child, out); | 371 private static final Pattern ALLOWED_NAME = Pattern.compile( |
230 wroteChildElement = true; | 372 "^[\\p{Alpha}_:][\\p{Alnum}.\\-_:]*$"); |
231 } | 373 /** True if value is a valid XML names outside the restricted namespace. */ |
| 374 private boolean checkRestrictedName(String value, FilePosition pos) { |
| 375 assert "".equals(value) || !IDENTIFIER_SEPARATOR.matcher(value).find(); |
| 376 if (ALLOWED_NAME.matcher(value).find()) { return true; } |
| 377 System.err.println("rejected ident `" + value + "`"); |
| 378 if (!"".equals(value)) { |
| 379 mq.addMessage( |
| 380 IhtmlMessageType.ILLEGAL_NAME, pos, |
| 381 MessagePart.Factory.valueOf(value)); |
| 382 } |
| 383 return false; |
| 384 } |
| 385 /** |
| 386 * True iff value is a space separated group of XML names outside the |
| 387 * restricted namespace. |
| 388 */ |
| 389 private boolean checkRestrictedNames(String value, FilePosition pos) { |
| 390 if ("".equals(value)) { return true; } |
| 391 boolean ok = true; |
| 392 for (String ident : IDENTIFIER_SEPARATOR.split(value)) { |
| 393 if ("".equals(ident)) { continue; } |
| 394 if (!ALLOWED_NAME.matcher(ident).matches()) { |
| 395 mq.addMessage( |
| 396 IhtmlMessageType.ILLEGAL_NAME, pos, |
| 397 MessagePart.Factory.valueOf(ident)); |
| 398 ok = false; |
| 399 } |
| 400 } |
| 401 return ok; |
| 402 } |
| 403 |
| 404 /** "foo bar baz" -> "foo-suffix___ bar-suffix___ baz-suffix___". */ |
| 405 private Expression rewriteIdentifiers(FilePosition pos, String names) { |
| 406 if ("".equals(names)) { return null; } |
| 407 String[] idents = IDENTIFIER_SEPARATOR.split(names); |
| 408 String idClass = meta.getIdClass(); |
| 409 if (idClass != null) { |
| 410 StringBuilder result = new StringBuilder(names.length()); |
| 411 for (String ident : idents) { |
| 412 if ("".equals(ident)) { continue; } |
| 413 if (result.length() != 0) { result.append(' '); } |
| 414 result.append(ident).append('-').append(idClass); |
| 415 } |
| 416 return StringLiteral.valueOf(pos, result.toString()); |
| 417 } else { |
| 418 Expression result = null; |
| 419 for (String ident : idents) { |
| 420 if ("".equals(ident)) { continue; } |
| 421 Expression oneRewritten = (Expression) QuasiBuilder.substV( |
| 422 "@ident + IMPORTS___.getIdClass___()", |
| 423 "ident", StringLiteral.valueOf( |
| 424 pos, (result != null ? " " : "") + ident + "-")); |
| 425 if (result != null) { |
| 426 result = QuasiUtil.concat(result, oneRewritten); |
232 } else { | 427 } else { |
233 for (Node child : children) { | 428 result = oneRewritten; |
234 if (!isWhitespaceTextNode(child)) { | 429 } |
235 mq.addMessage(MessageType.MALFORMED_XHTML, | |
236 Nodes.getFilePositionFor(child), | |
237 MessagePart.Factory.valueOf("<" + tagName + ">")); | |
238 } | |
239 } | |
240 } | |
241 | |
242 if (wroteChildElement || requiresCloseTag) { | |
243 out.end(FilePosition.endOf(Nodes.getFilePositionFor(el)), tagName); | |
244 } | |
245 break; | |
246 default: | |
247 throw new AssertionError(t.getNodeName()); | |
248 } | |
249 } | |
250 | |
251 private static final Pattern HTML_ID = Pattern.compile( | |
252 "^[a-z][a-z0-9-]*$", Pattern.CASE_INSENSITIVE); | |
253 private static Name assertHtmlIdentifier(Name id, Node node) | |
254 throws BadContentException { | |
255 if (!HTML_ID.matcher(id.getCanonicalForm()).matches()) { | |
256 throw new BadContentException(new Message( | |
257 PluginMessageType.BAD_IDENTIFIER, | |
258 Nodes.getFilePositionFor(node), id)); | |
259 } | |
260 return id; | |
261 } | |
262 | |
263 private void assertNotBlacklistedTag(Element el) throws BadContentException { | |
264 Name tagName = Name.xml(el.getTagName()); | |
265 if (!htmlSchema.isElementAllowed(tagName)) { | |
266 throw new BadContentException( | |
267 new Message(PluginMessageType.UNSAFE_TAG, | |
268 Nodes.getFilePositionFor(el), tagName)); | |
269 } | |
270 } | |
271 | |
272 private void assertNotBlacklistedAttrib(Name tagName, Attr attr) | |
273 throws BadContentException { | |
274 Name attrName = Name.xml(attr.getNodeName()); | |
275 if (!htmlSchema.isAttributeAllowed(tagName, attrName)) { | |
276 throw new BadContentException(new Message( | |
277 PluginMessageType.UNSAFE_ATTRIBUTE, Nodes.getFilePositionFor(attr), | |
278 attrName, tagName)); | |
279 } | |
280 } | |
281 | |
282 /** | |
283 * True if the given name requires a close tag. | |
284 * "TABLE" -> true, "BR" -> false. | |
285 */ | |
286 private boolean requiresCloseTag(Name tagName) { | |
287 HTML.Element e = htmlSchema.lookupElement(tagName); | |
288 return null == e || !e.isEmpty(); | |
289 } | |
290 | |
291 /** | |
292 * True if the tag can have content. False for unitary tags like | |
293 * {@code INPUT} and {@code BR}. | |
294 */ | |
295 private boolean tagAllowsContent(Name tagName) { | |
296 HTML.Element e = htmlSchema.lookupElement(tagName); | |
297 return null == e || !e.isEmpty(); | |
298 } | |
299 | |
300 /** | |
301 * Invokes the CSS validator to rewrite style attributes. | |
302 * @param attrib an attribute with name {@code "style"}. | |
303 */ | |
304 private void compileStyleAttrib(Attr attrib, DomProcessingEvents out) | |
305 throws BadContentException { | |
306 CssTree.DeclarationGroup decls; | |
307 try { | |
308 decls = parseStyleAttrib(attrib); | |
309 if (decls == null) { return; } | |
310 } catch (ParseException ex) { | |
311 throw new BadContentException(ex.getCajaMessage(), ex); | |
312 } | |
313 | |
314 // The validator will check that property values are well-formed, | |
315 // marking those that aren't, and identifies all urls. | |
316 CssValidator v = new CssValidator(cssSchema, htmlSchema, mq) | |
317 .withInvalidNodeMessageLevel(MessageLevel.WARNING); | |
318 v.validateCss(new AncestorChain<CssTree>(decls)); | |
319 // The rewriter will remove any unsafe constructs. | |
320 // and put urls in the proper filename namespace | |
321 new CssRewriter(meta, mq).withInvalidNodeMessageLevel(MessageLevel.WARNING) | |
322 .rewrite(new AncestorChain<CssTree>(decls)); | |
323 | |
324 Block cssBlock = new Block( | |
325 FilePosition.UNKNOWN, Collections.<Statement>emptyList()); | |
326 // Produces a call to cat(bits, of, css); | |
327 declGroupToStyleValue( | |
328 decls, Arrays.asList("cat"), cssBlock, JsWriter.Esc.NONE); | |
329 if (cssBlock.children().isEmpty()) { return; } | |
330 if (cssBlock.children().size() != 1) { | |
331 throw new IllegalStateException(attrib.getNodeValue()); | |
332 } | |
333 Expression css = ((ExpressionStmt) cssBlock.children().get(0)) | |
334 .getExpression(); | |
335 // Convert cat(a, b, c) to (a + b) + c | |
336 List<? extends Expression> operands = ((Operation) css).children(); | |
337 Expression cssOp = operands.get(1); | |
338 for (Expression e : operands.subList(2, operands.size())) { | |
339 cssOp = Operation.createInfix(Operator.ADDITION, cssOp, e); | |
340 } | |
341 out.attr(Name.html("style"), cssOp); | |
342 } | |
343 | |
344 /** | |
345 * Parses a style attribute's value as a CSS declaration group. | |
346 */ | |
347 private CssTree.DeclarationGroup parseStyleAttrib(Attr a) | |
348 throws ParseException { | |
349 CharProducer cp = fromAttrValue(a); | |
350 // Parse the css as a set of declarations separated by semicolons. | |
351 TokenQueue<CssTokenType> tq = CssParser.makeTokenQueue(cp, mq, false); | |
352 if (tq.isEmpty()) { return null; } | |
353 tq.setInputRange(Nodes.getFilePositionForValue(a)); | |
354 CssParser p = new CssParser(tq, mq, MessageLevel.WARNING); | |
355 CssTree.DeclarationGroup decls = p.parseDeclarationGroup(); | |
356 tq.expectEmpty(); | |
357 return decls; | |
358 } | |
359 | |
360 private static CharProducer fromAttrValue(Attr a) { | |
361 String value = a.getNodeValue(); | |
362 FilePosition pos = Nodes.getFilePositionForValue(a); | |
363 String rawValue = Nodes.getRawValue(a); | |
364 // Use the raw value so that the file positions come out right in | |
365 // error messages. | |
366 if (rawValue != null) { | |
367 // The raw value is html so we wrap it in an html unescaper. | |
368 CharProducer cp = CharProducer.Factory.fromHtmlAttribute( | |
369 CharProducer.Factory.create( | |
370 new StringReader(deQuote(rawValue)), pos)); | |
371 // Check if the attribute value has been set since parsing. | |
372 if (String.valueOf(cp.getBuffer(), cp.getOffset(), cp.getLength()) | |
373 .equals(value)) { | |
374 return cp; | |
375 } | 430 } |
376 } | 431 return result; |
377 // Reached if no raw value stored or if the raw value is out of sync. | 432 } |
378 return CharProducer.Factory.create(new StringReader(value), pos); | 433 } |
379 } | 434 |
380 | 435 /** |
381 /** | |
382 * Strip quotes from an attribute value if there are any. | |
383 */ | |
384 private static String deQuote(String s) { | |
385 int len = s.length(); | |
386 if (len < 2) { return s; } | |
387 char ch0 = s.charAt(0); | |
388 return (('"' == ch0 || '\'' == ch0) && ch0 == s.charAt(len - 1)) | |
389 ? " " + s.substring(1, len - 1) + " " | |
390 : s; | |
391 } | |
392 | |
393 /** | |
394 * Parses an {@code onclick} handler's or other handler's attribute value | |
395 * as a javascript statement. | |
396 */ | |
397 private Block asBlock(Attr jsAttr) { | |
398 // parse as a javascript expression. | |
399 FilePosition pos = Nodes.getFilePositionForValue(jsAttr); | |
400 CharProducer cp = fromAttrValue(jsAttr); | |
401 JsLexer lexer = new JsLexer(cp); | |
402 JsTokenQueue tq = new JsTokenQueue(lexer, pos.source()); | |
403 tq.setInputRange(pos); | |
404 Parser p = new Parser(tq, mq); | |
405 List<Statement> statements = new ArrayList<Statement>(); | |
406 try { | |
407 while (!tq.isEmpty()) { | |
408 statements.add(p.parseStatement()); | |
409 } | |
410 } catch (ParseException ex) { | |
411 ex.toMessageQueue(mq); | |
412 statements.clear(); | |
413 } | |
414 | |
415 // expression will be sanitized in a later pass | |
416 return new Block(pos, statements); | |
417 } | |
418 | |
419 /** | |
420 * Wrap the extracted script block in an exception handler so that | |
421 * exceptions thrown in script blocks do not interfere with subsequent | |
422 * execution. | |
423 * @see <a href= | |
424 * "http://code.google.com/p/google-caja/wiki/UncaughtExceptionHandling" | |
425 * >UncaughtExceptionHandling</a> | |
426 */ | |
427 private Statement scriptBodyEnvelope(Block scriptBody) { | |
428 FilePosition pos = scriptBody.getFilePosition(); | |
429 String sourcePath = mc.abbreviate(pos.source()); | |
430 TryStmt envelope = (TryStmt) QuasiBuilder.substV( | |
431 "" | |
432 + "try {" | |
433 + " @scriptBody;" | |
434 + "} catch (ex___) {" | |
435 + " ___./*@synthetic*/ getNewModuleHandler()" | |
436 + " ./*@synthetic*/ handleUncaughtException(" | |
437 + " ex___, onerror, @sourceFile, @line);" | |
438 + "}", | |
439 | |
440 "scriptBody", scriptBody, | |
441 "sourceFile", StringLiteral.valueOf(FilePosition.UNKNOWN, sourcePath), | |
442 "line", StringLiteral.valueOf( | |
443 FilePosition.UNKNOWN, String.valueOf(pos.startLineNo()))); | |
444 envelope.setFilePosition(pos); | |
445 return envelope; | |
446 } | |
447 | |
448 /** | |
449 * produces an identifier that will not collide with any previously generated | |
450 * identifier. | |
451 */ | |
452 private String syntheticId() { | |
453 return meta.generateUniqueName("c"); | |
454 } | |
455 | |
456 /** is the given node a text node that consists only of whitespace? */ | |
457 private static boolean isWhitespaceTextNode(Node t) { | |
458 switch (t.getNodeType()) { | |
459 case Node.TEXT_NODE: case Node.CDATA_SECTION_NODE: | |
460 return "".equals(t.getNodeValue().trim()); | |
461 default: | |
462 return false; | |
463 } | |
464 } | |
465 | |
466 /** | |
467 * for a given html attribute, what kind of transformation do we have to | |
468 * perform on the value? | |
469 */ | |
470 private AttributeXform xformForAttribute( | |
471 Name tagName, Name attribute) { | |
472 HTML.Attribute a = htmlSchema.lookupAttribute(tagName, attribute); | |
473 if (null != a) { | |
474 switch (a.getType()) { | |
475 case ID: | |
476 case IDREF: | |
477 case GLOBAL_NAME: | |
478 return AttributeXform.NAMES; | |
479 case CLASSES: | |
480 return AttributeXform.CLASSES; | |
481 case STYLE: | |
482 return AttributeXform.STYLE; | |
483 case SCRIPT: | |
484 return AttributeXform.SCRIPT; | |
485 case URI: | |
486 return AttributeXform.URI; | |
487 default: break; | |
488 } | |
489 } | |
490 return null; | |
491 } | |
492 | |
493 private String guessMimeType(Name tagName, Name attribName) { | |
494 HTML.Attribute type = htmlSchema.lookupAttribute(tagName, attribName); | |
495 String mimeType = type.getMimeTypes(); | |
496 return mimeType != null ? mimeType : "*/*"; | |
497 } | |
498 | |
499 /** | |
500 * encapsulates a transformation on an html attribute value. | |
501 * Transformations are performed at compile time. | |
502 */ | |
503 private static enum AttributeXform { | |
504 /** Applied to {@code name} and {@code id} attributes. */ | |
505 NAMES { | |
506 @Override | |
507 void apply( | |
508 Name elName, Attr a, HtmlCompiler htmlc, DomProcessingEvents out) { | |
509 IdentifierWriter.ConcatenationEmitter emitter | |
510 = new IdentifierWriter.ConcatenationEmitter(); | |
511 FilePosition valuePos = Nodes.getFilePositionForValue(a); | |
512 new IdentifierWriter(htmlc.mq, true) | |
513 .toJavascript(valuePos, a.getNodeValue(), emitter); | |
514 Expression value = emitter.getExpression(); | |
515 if (value != null) { | |
516 out.attr(Name.xml(a.getNodeName()), value); | |
517 } | |
518 } | |
519 }, | |
520 CLASSES { | |
521 @Override | |
522 void apply( | |
523 Name elName, Attr a, HtmlCompiler htmlc, DomProcessingEvents out) { | |
524 IdentifierWriter.ConcatenationEmitter emitter | |
525 = new IdentifierWriter.ConcatenationEmitter(); | |
526 FilePosition valuePos = Nodes.getFilePositionForValue(a); | |
527 new IdentifierWriter(htmlc.mq, false) | |
528 .toJavascript(valuePos, a.getNodeValue(), emitter); | |
529 Expression value = emitter.getExpression(); | |
530 if (value != null) { | |
531 out.attr(Name.xml(a.getNodeName()), value); | |
532 } | |
533 } | |
534 }, | |
535 /** Applied to CSS such as {@code style} attributes. */ | |
536 STYLE { | |
537 @Override | |
538 void apply( | |
539 Name elName, Attr a, HtmlCompiler htmlc, DomProcessingEvents out) { | |
540 // should be handled in compileDOM | |
541 throw new AssertionError(); | |
542 } | |
543 }, | |
544 /** Applied to javascript such as {@code onclick} attributes. */ | |
545 SCRIPT { | |
546 @Override | |
547 void apply( | |
548 Name elName, Attr a, HtmlCompiler htmlc, DomProcessingEvents out) { | |
549 // Extract the handler into a function so that it can be analyzed. | |
550 Block handler = htmlc.asBlock(a); | |
551 if (handler.children().isEmpty()) { return; } | |
552 rewriteEventHandlerReferences(handler); | |
553 | |
554 // This function must not be synthetic. If it were, the rewriter would | |
555 // not treat its formals as affecting scope. | |
556 FunctionConstructor handlerFn = new FunctionConstructor( | |
557 Nodes.getFilePositionForValue(a), | |
558 new Identifier(FilePosition.UNKNOWN, null), | |
559 Arrays.asList( | |
560 new FormalParam(s( | |
561 new Identifier(FilePosition.UNKNOWN, "event"))), | |
562 new FormalParam(s( | |
563 new Identifier( | |
564 FilePosition.UNKNOWN, ReservedNames.THIS_NODE)))), | |
565 handler); | |
566 | |
567 String handlerFnName = htmlc.syntheticId(); | |
568 htmlc.eventHandlers.put( | |
569 handlerFnName, | |
570 new ExpressionStmt( | |
571 Nodes.getFilePositionForValue(a), | |
572 (Expression) QuasiBuilder.substV( | |
573 "IMPORTS___.@handlerFnName = @handlerFn;", | |
574 "handlerFnName", TreeConstruction.ref(handlerFnName), | |
575 "handlerFn", handlerFn))); | |
576 | |
577 String handlerFnNameLit = StringLiteral.toQuotedValue(handlerFnName); | |
578 | |
579 Operation dispatcher = Operation.createInfix( | |
580 Operator.ADDITION, | |
581 Operation.createInfix( | |
582 Operator.ADDITION, | |
583 StringLiteral.valueOf( | |
584 Nodes.getFilePositionForValue(a), | |
585 "return plugin_dispatchEvent___(this, event, "), | |
586 TreeConstruction.call( | |
587 TreeConstruction.memberAccess("___", "getId"), | |
588 TreeConstruction.ref(ReservedNames.IMPORTS))), | |
589 StringLiteral.valueOf( | |
590 FilePosition.UNKNOWN, ", " + handlerFnNameLit + ")")); | |
591 out.handler(Name.xml(a.getNodeName()), dispatcher); | |
592 } | |
593 }, | |
594 /** Applied to URIs such as {@code href} and {@code src} attributes. */ | |
595 URI { | |
596 @Override | |
597 void apply( | |
598 Name elName, Attr a, HtmlCompiler htmlc, DomProcessingEvents out) { | |
599 URI uri; | |
600 try { | |
601 uri = new URI(a.getNodeValue()); | |
602 } catch (URISyntaxException ex) { | |
603 htmlc.mq.addMessage( | |
604 PluginMessageType.MALFORMED_URL, | |
605 Nodes.getFilePositionForValue(a), | |
606 MessagePart.Factory.valueOf(a.getNodeValue())); | |
607 return; | |
608 } | |
609 String mimeType = htmlc.guessMimeType( | |
610 elName, Name.xml(a.getNodeName())); | |
611 String rewrittenUri = htmlc.meta.getPluginEnvironment().rewriteUri( | |
612 new ExternalReference(uri, Nodes.getFilePositionForValue(a)), | |
613 mimeType); | |
614 if (rewrittenUri != null) { | |
615 out.attr(Nodes.getFilePositionFor(a), Name.xml(a.getNodeName()), | |
616 rewrittenUri); | |
617 } else { | |
618 htmlc.mq.addMessage( | |
619 PluginMessageType.DISALLOWED_URI, | |
620 Nodes.getFilePositionForValue(a), | |
621 MessagePart.Factory.valueOf(uri.toString())); | |
622 } | |
623 } | |
624 }, | |
625 ; | |
626 | |
627 /** | |
628 * apply, at compile time, any preprocessing steps to the given attributes | |
629 * value. | |
630 */ | |
631 abstract void apply( | |
632 Name elName, Attr a, HtmlCompiler htmlc, DomProcessingEvents out) | |
633 throws BadContentException; | |
634 } | |
635 | |
636 /** | |
637 * Convert "this" -> "thisNode___" in event handlers. Event handlers are | 436 * Convert "this" -> "thisNode___" in event handlers. Event handlers are |
638 * run in a context where this points to the current node. | 437 * run in a context where this points to the current node. |
639 * We need to emulate that but still allow the event handlers to be simple | 438 * We need to emulate that but still allow the event handlers to be simple |
640 * functions, so we pass in the tamed node as the first parameter. | 439 * functions, so we pass in the tamed node as the first parameter. |
641 * | 440 * |
642 * The event handler goes from:<br> | 441 * The event handler goes from:<br> |
643 * {@code if (this.type === 'text') alert(this.value); } | 442 * {@code if (this.type === 'text') alert(this.value); } |
644 * to a function like:<pre> | 443 * to a function like:<pre> |
645 * function (thisNode___, event) { | 444 * function (thisNode___, event) { |
646 * if (thisNode___.type === 'text') { | 445 * if (thisNode___.type === 'text') { |
647 * alert(thisNode___.value); | 446 * alert(thisNode___.value); |
648 * } | 447 * } |
649 * }</pre> | 448 * }</pre> |
650 * <p> | 449 * <p> |
651 * And the resulting function is called via a handler attribute like | 450 * And the resulting function is called via a handler attribute like |
652 * {@code onchange="plugin_dispatchEvent___(this, node, 1234, 'handlerName')"} | 451 * {@code onchange="plugin_dispatchEvent___(this, node, 1234, 'handlerName')"} |
653 */ | 452 */ |
654 static void rewriteEventHandlerReferences(Block block) { | 453 private static void rewriteEventHandlerReferences(Block block) { |
655 block.acceptPreOrder( | 454 block.acceptPreOrder( |
656 new Visitor() { | 455 new Visitor() { |
657 public boolean visit(AncestorChain<?> ancestors) { | 456 public boolean visit(AncestorChain<?> ancestors) { |
658 ParseTreeNode node = ancestors.node; | 457 ParseTreeNode node = ancestors.node; |
659 // Do not recurse into closures. | 458 // Do not recurse into closures. |
660 if (node instanceof FunctionConstructor) { return false; } | 459 if (node instanceof FunctionConstructor) { return false; } |
661 if (node instanceof Reference) { | 460 if (node instanceof Reference) { |
662 Reference r = (Reference) node; | 461 Reference r = (Reference) node; |
663 if (Keyword.THIS.toString().equals(r.getIdentifierName())) { | 462 if (Keyword.THIS.toString().equals(r.getIdentifierName())) { |
664 Identifier oldRef = r.getIdentifier(); | 463 Identifier oldRef = r.getIdentifier(); |
665 Identifier thisNode = new Identifier( | 464 Identifier thisNode = new Identifier( |
666 oldRef.getFilePosition(), ReservedNames.THIS_NODE); | 465 oldRef.getFilePosition(), ReservedNames.THIS_NODE); |
667 r.replaceChild(s(thisNode), oldRef); | 466 r.replaceChild(SyntheticNodes.s(thisNode), oldRef); |
668 } | 467 } |
669 return false; | 468 return false; |
670 } | 469 } |
671 return true; | 470 return true; |
672 } | 471 } |
673 }, null); | 472 }, null); |
674 } | 473 } |
675 | 474 |
676 static void declGroupToStyleValue( | 475 /** |
677 CssTree.DeclarationGroup cssTree, final List<String> tgtChain, | 476 * Builds a tree of only the safe HTML parts ignoring IHTML elements. |
678 final Block b, final JsWriter.Esc esc) { | 477 * If there are embedded script elements, then these will be removed, and |
679 | 478 * nodes may have {@code afterScript0___} classes added so that a client |
680 declarationsToJavascript(cssTree, esc, new DynamicCssReceiver() { | 479 * can split them into the elements present when each script is executed. |
681 boolean first = true; | 480 * |
682 | 481 * The output will not will not contain SCRIPT nodes corresponding to inline |
683 public void property(CssTree.Property p) { | 482 * HTML. This could be repaired by changing {@link SafeHtmlMaker} to include |
684 StringBuilder out = new StringBuilder(); | 483 * empty SCRIPT nodes but makes the output larger. |
685 TokenConsumer tc = p.makeRenderer(out, null); | 484 */ |
686 if (first) { | 485 public Pair<Node, List<Block>> getSafeHtml(Document doc) { |
687 first = false; | 486 // Emit safe HTML with JS which attaches dynamic attributes. |
688 } else { | 487 SafeHtmlMaker htmlMaker = new SafeHtmlMaker(meta, mc, doc, scriptsPerNode); |
689 tc.consume(";"); | 488 Pair<Node, List<Block>> htmlAndJs = htmlMaker.make(ihtmlRoots, handlers); |
690 } | 489 Node html = htmlAndJs.a; |
691 p.render(new RenderContext(tc)); | 490 List<Block> js = htmlAndJs.b; |
692 tc.consume(":"); | 491 Block firstJs; |
693 tc.noMoreTokens(); | 492 if (js.isEmpty()) { |
694 out.append(" "); | 493 js.add(firstJs = new Block()); |
695 rawCss(p.getFilePosition(), out.toString()); | 494 } else { |
696 } | 495 firstJs = js.get(0); |
697 | 496 } |
698 public void rawCss(FilePosition pos, String rawCss) { | 497 // Compile CSS to HTML when appropriate or to JS where not. |
699 JsWriter.appendText(pos, rawCss, esc, tgtChain, b); | 498 // It always ends up at the top either way. |
700 } | 499 new SafeCssMaker(html, firstJs, safeStylesheets).make(); |
701 | 500 if (firstJs.children().isEmpty()) { |
702 public void priority(CssTree.Prio p) { | 501 js.remove(firstJs); |
703 StringBuilder out = new StringBuilder(); | 502 } |
704 out.append(" "); | 503 return Pair.pair(html, js); |
705 TokenConsumer tc = p.makeRenderer(out, null); | |
706 p.render(new RenderContext(tc)); | |
707 tc.noMoreTokens(); | |
708 rawCss(p.getFilePosition(), out.toString()); | |
709 } | |
710 }); | |
711 } | 504 } |
712 | 505 |
713 private static interface DynamicCssReceiver { | 506 /** |
714 void property(CssTree.Property p); | 507 * Parses an {@code onclick} handler's or other handler's attribute value |
715 | 508 * as a javascript statement. |
716 void rawCss(FilePosition pos, String rawCss); | 509 */ |
717 | 510 private Block parseJsFromAttrValue(Attr attr) throws ParseException { |
718 void priority(CssTree.Prio p); | 511 FilePosition pos = Nodes.getFilePositionForValue(attr); |
| 512 CharProducer cp = fromAttrValue(attr); |
| 513 JsTokenQueue tq = new JsTokenQueue(new JsLexer(cp, false), pos.source()); |
| 514 tq.setInputRange(pos); |
| 515 if (tq.isEmpty()) { |
| 516 return new Block(pos, Collections.<Statement>emptyList()); |
| 517 } |
| 518 // Parse as a javascript block. |
| 519 Block b = new Parser(tq, mq).parse(); |
| 520 // Block will be sanitized in a later pass. |
| 521 b.setFilePosition(pos); |
| 522 return b; |
719 } | 523 } |
720 | 524 |
721 private static void declarationsToJavascript( | 525 /** |
722 CssTree.DeclarationGroup decls, JsWriter.Esc esc, | 526 * Parses a style attribute's value as a CSS declaration group. |
723 DynamicCssReceiver out) { | 527 */ |
724 assert esc == JsWriter.Esc.NONE || esc == JsWriter.Esc.HTML_ATTRIB : esc; | 528 private CssTree.DeclarationGroup parseStyleAttrib(Attr a) |
| 529 throws ParseException { |
| 530 CharProducer cp = fromAttrValue(a); |
| 531 // Parse the CSS as a set of declarations separated by semicolons. |
| 532 TokenQueue<CssTokenType> tq = CssParser.makeTokenQueue(cp, mq, false); |
| 533 if (tq.isEmpty()) { return null; } |
| 534 tq.setInputRange(Nodes.getFilePositionForValue(a)); |
| 535 CssParser p = new CssParser(tq, mq, MessageLevel.WARNING); |
| 536 CssTree.DeclarationGroup decls = p.parseDeclarationGroup(); |
| 537 tq.expectEmpty(); |
| 538 return decls; |
| 539 } |
725 | 540 |
726 for (CssTree.Declaration decl : decls.children()) { | 541 private static CharProducer fromAttrValue(Attr a) { |
727 if (decl instanceof CssTree.PropertyDeclaration) { | 542 String value = a.getNodeValue(); |
728 CssTree.PropertyDeclaration pdecl = (CssTree.PropertyDeclaration) decl; | 543 FilePosition pos = Nodes.getFilePositionForValue(a); |
729 // Render the style to a canonical form with consistent escaping | 544 String rawValue = Nodes.getRawValue(a); |
730 // conventions, so that we can avoid browser bugs. | 545 // Use the raw value so that the file positions come out right in |
731 String css; | 546 // error messages. |
732 { | 547 if (rawValue != null) { |
733 StringBuilder cssBuf = new StringBuilder(); | 548 // The raw value is HTML so we wrap it in an HTML decoder. |
734 TokenConsumer tc = decl.makeRenderer(cssBuf, null); | 549 CharProducer cp = CharProducer.Factory.fromHtmlAttribute( |
735 pdecl.getExpr().render(new RenderContext(tc)); | 550 CharProducer.Factory.create( |
736 tc.noMoreTokens(); | 551 new StringReader(deQuote(rawValue)), pos)); |
737 | 552 // Check if the attribute value has been set since parsing. |
738 // Contains the rendered CSS with ${\0###\0} placeholders. | 553 if (String.valueOf(cp.getBuffer(), cp.getOffset(), cp.getLength()) |
739 // Split around the placeholders, parse the javascript, escape the | 554 .equals(value)) { |
740 // literal text, and emit the appropriate javascript. | 555 return cp; |
741 css = cssBuf.toString(); | |
742 } | |
743 | |
744 out.property(pdecl.getProperty()); | |
745 out.rawCss(pdecl.getFilePosition(), css); | |
746 if (pdecl.getPrio() != null) { out.priority(pdecl.getPrio()); } | |
747 } | 556 } |
748 } | 557 } |
| 558 // Reached if no raw value stored or if the raw value is out of sync. |
| 559 return CharProducer.Factory.create(new StringReader(value), pos); |
| 560 } |
| 561 |
| 562 /** Strip quotes from an attribute value if there are any. */ |
| 563 private static String deQuote(String s) { |
| 564 int len = s.length(); |
| 565 if (len < 2) { return s; } |
| 566 char ch0 = s.charAt(0); |
| 567 return (('"' == ch0 || '\'' == ch0) && ch0 == s.charAt(len - 1)) |
| 568 ? " " + s.substring(1, len - 1) + " " |
| 569 : s; |
749 } | 570 } |
750 } | 571 } |
OLD | NEW |