1 | // Copyright (C) 2006 Google Inc. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (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 |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | package com.google.caja.plugin; |
16 | |
17 | import com.google.caja.SomethingWidgyHappenedError; |
18 | import com.google.caja.lang.css.CssPropertyPatterns; |
19 | import com.google.caja.lang.css.CssSchema; |
20 | import com.google.caja.lang.css.CssSchema.SymbolInfo; |
21 | import com.google.caja.lexer.ExternalReference; |
22 | import com.google.caja.lexer.FilePosition; |
23 | import com.google.caja.lexer.TokenConsumer; |
24 | import com.google.caja.parser.AncestorChain; |
25 | import com.google.caja.parser.MutableParseTreeNode; |
26 | import com.google.caja.parser.ParseTreeNode; |
27 | import com.google.caja.parser.Visitor; |
28 | import com.google.caja.parser.css.CssTree; |
29 | import com.google.caja.parser.html.ElKey; |
30 | import com.google.caja.parser.html.Namespaces; |
31 | import com.google.caja.render.Concatenator; |
32 | import com.google.caja.render.CssPrettyPrinter; |
33 | import com.google.caja.reporting.Message; |
34 | import com.google.caja.reporting.MessageLevel; |
35 | import com.google.caja.reporting.MessagePart; |
36 | import com.google.caja.reporting.MessageQueue; |
37 | import com.google.caja.reporting.RenderContext; |
38 | import com.google.caja.util.Lists; |
39 | import com.google.caja.util.Maps; |
40 | import com.google.caja.util.Name; |
41 | import com.google.caja.util.Pair; |
42 | import com.google.caja.util.Sets; |
43 | |
44 | import java.net.URI; |
45 | import java.net.URISyntaxException; |
46 | import java.util.Collections; |
47 | import java.util.List; |
48 | import java.util.Map; |
49 | import java.util.Set; |
50 | import java.util.regex.Pattern; |
51 | |
52 | /** |
53 | * Rewrites CSS to be safer and shorter. |
54 | * Excises disallowed constructs, removes extraneous nodes, and collapses |
55 | * duplicate rule-set selectors. |
56 | * <p> |
57 | * Does not separate rules into separate name-spaces. |
58 | * |
59 | * @author mikesamuel@gmail.com |
60 | */ |
61 | public final class CssRewriter { |
62 | private final UriPolicy uriPolicy; |
63 | private final CssSchema schema; |
64 | private final MessageQueue mq; |
65 | private MessageLevel invalidNodeMessageLevel = MessageLevel.ERROR; |
66 | |
67 | public CssRewriter(UriPolicy uriPolicy, CssSchema schema, MessageQueue mq) { |
68 | assert null != mq; |
69 | assert null != uriPolicy; |
70 | this.uriPolicy = uriPolicy; |
71 | this.schema = schema; |
72 | this.mq = mq; |
73 | } |
74 | |
75 | /** |
76 | * Specifies the level of messages issued when nodes are marked |
77 | * {@link CssValidator#INVALID}. |
78 | * If you are dealing with noisy CSS and later remove invalid nodes, then |
79 | * this can be set to {@link MessageLevel#WARNING}. |
80 | * @return this |
81 | */ |
82 | public CssRewriter withInvalidNodeMessageLevel(MessageLevel messageLevel) { |
83 | this.invalidNodeMessageLevel = messageLevel; |
84 | return this; |
85 | } |
86 | |
87 | /** |
88 | * Rewrite the given CSS tree to be safer and shorter. |
89 | * |
90 | * If the tree could not be made safe, then there will be |
91 | * {@link MessageLevel#ERROR error}s on the {@link MessageQueue} passed |
92 | * to the constructor. |
93 | * |
94 | * @param t non null. modified in place. |
95 | */ |
96 | public void rewrite(AncestorChain<? extends CssTree> t) { |
97 | rewriteHistorySensitiveRulesets(t); |
98 | quoteLooseWords(t); |
99 | fixTerms(t); |
100 | // Once at the beginning, and again at the end. |
101 | removeUnsafeConstructs(t); |
102 | removeEmptyDeclarationsAndSelectors(t); |
103 | // After we remove declarations, we may have some rulesets without any |
104 | // declarations which is technically illegal, so we remove rulesets without |
105 | // declarations. |
106 | removeEmptyRuleSets(t); |
107 | // Disallow classes and IDs that end in double underscore. |
108 | removeForbiddenIdents(t); |
109 | // Do this again to make sure no earlier changes introduce unsafe constructs |
110 | removeUnsafeConstructs(t); |
111 | |
112 | translateUrls(t); |
113 | } |
114 | |
115 | /** |
116 | * A set of pseudo classes that are allowed in restricted context because they |
117 | * can leak user history information. |
118 | * <p> |
119 | * From http://www.w3.org/TR/css3-selectors/#dynamic-pseudos : <blockquote> |
120 | * <h3>6.6.1. Dynamic pseudo-classes</h3> |
121 | * The link pseudo-classes: :link and :visited<br> |
122 | * <br> |
123 | * User agents commonly display unvisited links differently from previously |
124 | * visited ones. Selectors provides the pseudo-classes :link and :visited to |
125 | * distinguish them:<ul> |
126 | * <li>The :link pseudo-class applies to links that have not yet been |
127 | * visited. |
128 | * <li>The :visited pseudo-class applies once the link has been visited by |
129 | * the user. |
130 | * </ul> |
131 | * </blockquote> |
132 | */ |
133 | private static final Set<Name> LINK_PSEUDO_CLASSES = Sets.immutableSet( |
134 | Name.css("link"), Name.css("visited")); |
135 | |
136 | /** |
137 | * Split any ruleset containing :link or :visited pseudoclasses into two |
138 | * rulesets: one with these pseudoclasses in the selector, and one without. |
139 | * (One of these resulting rulesets may be empty and thus not emitted.) So |
140 | * for example, the stylesheet: |
141 | * |
142 | * <pre> |
143 | * :visited, a:link, p, div { color: blue } |
144 | * </pre> |
145 | * |
146 | * <p>becomes: |
147 | * |
148 | * <pre> |
149 | * :visited, a:link { color: blue } |
150 | * p, div { color: blue } |
151 | * </pre> |
152 | * |
153 | * <p>We do this because, downstream, we are going to cull away declarations |
154 | * for properties which are not permitted to depend on the :link or :visisted |
155 | * pseudoclasses. We do this, in turn, to prevent history mining attacks. |
156 | * |
157 | * <p>Furthermore, scope any selectors containing linkey pseudo classes to |
158 | * operate only on anchor (<A>) elements. Modify it if necessary, or record |
159 | * an error if the selector is already scoped to some element that is not an |
160 | * anchor. For example: |
161 | * |
162 | * <pre> |
163 | * div#foo --> div#foo (unmodified) |
164 | * :visited --> a:visited |
165 | * :link --> a:link |
166 | * *:visited --> a:visited |
167 | * p:visited --> ERROR |
168 | * </pre> |
169 | * |
170 | * <p>We do this to ensure the most predictable possible browser behavior |
171 | * around this sensitive and exploitable issue. |
172 | */ |
173 | private void rewriteHistorySensitiveRulesets( |
174 | final AncestorChain<? extends CssTree> t) { |
175 | t.node.acceptPreOrder(new Visitor() { |
176 | public boolean visit(AncestorChain<?> ancestors) { |
177 | if (!(ancestors.node instanceof CssTree.RuleSet)) { return true; } |
178 | Pair<CssTree.RuleSet, CssTree.RuleSet> rewritten = |
179 | rewriteHistorySensitiveRuleset((CssTree.RuleSet) ancestors.node); |
180 | if (rewritten != null) { |
181 | t.node.insertBefore(rewritten.a, ancestors.node); |
182 | t.node.insertBefore(rewritten.b, ancestors.node); |
183 | t.node.removeChild(ancestors.node); |
184 | } |
185 | return false; |
186 | } |
187 | }, t.parent); |
188 | } |
189 | |
190 | private Pair<CssTree.RuleSet, CssTree.RuleSet> rewriteHistorySensitiveRuleset( |
191 | CssTree.RuleSet ruleSet) { |
192 | List<CssTree> linkeyChildren = Lists.newArrayList(); |
193 | List<CssTree> nonLinkeyChildren = Lists.newArrayList(); |
194 | |
195 | for (CssTree child : ruleSet.children()) { |
196 | if (child instanceof CssTree.Selector) { |
197 | CssTree.Selector selector = (CssTree.Selector) child; |
198 | if (vetLinkToHistorySensitiveSelector(selector)) { |
199 | linkeyChildren.add(selector); |
200 | } else { |
201 | nonLinkeyChildren.add(selector); |
202 | } |
203 | } else { |
204 | // All the selectors come first, so now we know whether we need to split |
205 | // the child lists in two. |
206 | if (linkeyChildren.isEmpty() || nonLinkeyChildren.isEmpty()) { |
207 | return null; |
208 | } else { |
209 | linkeyChildren.add(child); |
210 | nonLinkeyChildren.add((CssTree) child.clone()); |
211 | } |
212 | } |
213 | } |
214 | |
215 | return Pair.pair( |
216 | new CssTree.RuleSet(ruleSet.getFilePosition(), linkeyChildren), |
217 | new CssTree.RuleSet(ruleSet.getFilePosition(), nonLinkeyChildren)); |
218 | } |
219 | |
220 | /** |
221 | * Rewrites any visited or link pseudo class elements to have element name A. |
222 | * @return true if argument is a compound selector like |
223 | * {@code div#foo > p > *:visited}. |
224 | */ |
225 | private boolean vetLinkToHistorySensitiveSelector(CssTree.Selector selector) { |
226 | boolean modified = false; |
227 | for (CssTree child : selector.children()) { |
228 | if (child instanceof CssTree.SimpleSelector) { |
229 | modified |= vetLinkToHistorySensitiveSimpleSelector( |
230 | (CssTree.SimpleSelector) child); |
231 | } |
232 | } |
233 | return modified; |
234 | } |
235 | |
236 | /** The name of an anchor {@code <A>} HTML tag. */ |
237 | private static final ElKey HTML_ANCHOR = ElKey.forHtmlElement("a"); |
238 | |
239 | /** |
240 | * Rewrites any visited or link pseudo class elements to have element name A. |
241 | * @return true iff argument is a simple selector like {@code *:visited}. |
242 | */ |
243 | private boolean vetLinkToHistorySensitiveSimpleSelector( |
244 | CssTree.SimpleSelector selector) { |
245 | if (selector.children().isEmpty()) { return false; } |
246 | if (!containsLinkPseudoClass(selector)) { return false; } |
247 | CssTree firstChild = selector.children().get(0); |
248 | if (firstChild instanceof CssTree.WildcardElement) { |
249 | // "*#foo:visited" --> "a#foo:visited" |
250 | selector.replaceChild( |
251 | new CssTree.IdentLiteral( |
252 | firstChild.getFilePosition(), HTML_ANCHOR.toString()), |
253 | firstChild); |
254 | return true; |
255 | } else if (firstChild instanceof CssTree.IdentLiteral) { |
256 | // "a#foo:visited" is legal; "p#foo:visited" is not |
257 | String value = ((CssTree.IdentLiteral) firstChild).getValue(); |
258 | if (!HTML_ANCHOR.equals( |
259 | ElKey.forElement(Namespaces.HTML_DEFAULT, value))) { |
260 | mq.addMessage( |
261 | PluginMessageType.CSS_LINK_PSEUDO_SELECTOR_NOT_ALLOWED_ON_NONANCHOR, |
262 | firstChild.getFilePosition()); |
263 | } |
264 | return false; |
265 | } else { |
266 | // "#foo:visited" --> "a#foo:visited" |
267 | selector.insertBefore( |
268 | new CssTree.IdentLiteral( |
269 | firstChild.getFilePosition(), HTML_ANCHOR.toString()), |
270 | firstChild); |
271 | return true; |
272 | } |
273 | } |
274 | |
275 | private boolean containsLinkPseudoClass(CssTree.SimpleSelector selector) { |
276 | final boolean[] result = new boolean[1]; |
277 | selector.acceptPreOrder(new Visitor() { |
278 | public boolean visit(AncestorChain<?> chain) { |
279 | if (chain.node instanceof CssTree.Pseudo) { |
280 | CssTree firstChild = (CssTree) chain.node.children().get(0); |
281 | if (firstChild instanceof CssTree.IdentLiteral) { |
282 | CssTree.IdentLiteral ident = (CssTree.IdentLiteral) firstChild; |
283 | if (LINK_PSEUDO_CLASSES.contains(Name.css(ident.getValue()))) { |
284 | result[0] = true; |
285 | return false; |
286 | } |
287 | } |
288 | } |
289 | return true; |
290 | } |
291 | }, null); |
292 | return result[0]; |
293 | } |
294 | |
295 | /** |
296 | * Turn a run of unquoted identifiers into a single string, where the property |
297 | * description says "Names containing space *should* be quoted", but does not |
298 | * require it. |
299 | * <p> |
300 | * This is important for font {@code family-name}s where |
301 | * {@code font: Times New Roman} should be written as |
302 | * {@code font: "Times New Roman"} to avoid any possible ambiguity between |
303 | * the individual terms and special values such as {@code serif}. |
304 | * |
305 | * @see CssPropertyPartType#LOOSE_WORD |
306 | */ |
307 | private void quoteLooseWords(AncestorChain<? extends CssTree> t) { |
308 | if (t.node instanceof CssTree.Expr) { |
309 | combineLooseWords(t.cast(CssTree.Expr.class).node); |
310 | } |
311 | for (CssTree child : t.node.children()) { |
312 | quoteLooseWords(AncestorChain.instance(t, child)); |
313 | } |
314 | } |
315 | |
316 | private void combineLooseWords(CssTree.Expr e) { |
317 | for (int i = 0, n = e.getNTerms(); i < n; ++i) { |
318 | CssTree.Term t = e.getNthTerm(i); |
319 | if (!isLooseWord(t)) { continue; } |
320 | |
321 | Name propertyPart = propertyPart(t); |
322 | StringBuilder sb = new StringBuilder(); |
323 | sb.append(t.getExprAtom().getValue()); |
324 | |
325 | // Compile a mutation that removes all the extraneous terms and that |
326 | // replaces t with a string literal. |
327 | MutableParseTreeNode.Mutation mut = e.createMutation(); |
328 | |
329 | // Compute end, the term index after the last of the run of loose terms |
330 | // for t's property part. |
331 | int start = i; |
332 | int end = i + 1; |
333 | while (end < n) { |
334 | CssTree.Operation op = e.getNthOperation(end - 1); |
335 | CssTree.Term t2 = e.getNthTerm(end); |
336 | if (!(CssTree.Operator.NONE == op.getOperator() && isLooseWord(t2) |
337 | && propertyPart.equals(propertyPart(t2)))) { |
338 | break; |
339 | } |
340 | mut.removeChild(op); |
341 | mut.removeChild(t2); |
342 | sb.append(' ').append(e.getNthTerm(end).getExprAtom().getValue()); |
343 | ++end; |
344 | } |
345 | |
346 | // Create a string literal to replace all the terms [start:end-1]. |
347 | // Make sure it has the same synthetic attributes and file position. |
348 | String text = sb.toString(); |
349 | FilePosition pos = FilePosition.span( |
350 | t.getFilePosition(), e.getNthTerm(end - 1).getFilePosition()); |
351 | CssTree.StringLiteral quotedWords = new CssTree.StringLiteral(pos, text); |
352 | CssTree.Term quotedTerm = new CssTree.Term(pos, null, quotedWords); |
353 | quotedTerm.getAttributes().putAll(t.getAttributes()); |
354 | quotedTerm.getAttributes().set(CssValidator.CSS_PROPERTY_PART_TYPE, |
355 | CssPropertyPartType.STRING); |
356 | |
357 | mut.replaceChild(quotedTerm, t); |
358 | mut.execute(); |
359 | |
360 | // If we made a substantive change, combining multiple terms into one, |
361 | // then issue a line message. We don't need to issue a warning on all |
362 | // changes, since we only reach this code if we passed validation. |
363 | if (end - start > 1) { |
364 | mq.addMessage(PluginMessageType.QUOTED_CSS_VALUE, |
365 | pos, MessagePart.Factory.valueOf(text)); |
366 | } |
367 | |
368 | n = e.getNTerms(); |
369 | } |
370 | } |
371 | |
372 | /** @see CssPropertyPartType#LOOSE_WORD */ |
373 | private static boolean isLooseWord(CssTree.Term t) { |
374 | return t.getOperator() == null |
375 | && t.getExprAtom() instanceof CssTree.IdentLiteral |
376 | && propertyPartType(t) == CssPropertyPartType.LOOSE_WORD; |
377 | } |
378 | |
379 | /** |
380 | * Make sure that unitless lengths have units, and convert non-standard |
381 | * colors to hex constants. |
382 | * <a href="http://www.w3.org/TR/CSS21/syndata.html#length-units">Lengths</a> |
383 | * require units unless the value is zero. All browsers assume px if the |
384 | * suffix is missing. |
385 | */ |
386 | private void fixTerms(AncestorChain<? extends CssTree> t) { |
387 | SymbolInfo stdColors = schema.getSymbol(Name.css("color-standard")); |
388 | final Pattern stdColorMatcher; |
389 | if (stdColors != null) { |
390 | stdColorMatcher = new CssPropertyPatterns(schema) |
391 | .cssPropertyToJavaRegex(stdColors.sig); |
392 | } else { |
393 | stdColorMatcher = null; |
394 | } |
395 | t.node.acceptPreOrder(new Visitor() { |
396 | public boolean visit(AncestorChain<?> ancestors) { |
397 | if (!(ancestors.node instanceof CssTree.Term)) { |
398 | return true; |
399 | } |
400 | CssTree.Term term = (CssTree.Term) ancestors.node; |
401 | CssPropertyPartType partType = propertyPartType(term); |
402 | if (CssPropertyPartType.LENGTH == partType |
403 | && term.getExprAtom() instanceof CssTree.QuantityLiteral) { |
404 | CssTree.QuantityLiteral quantity = (CssTree.QuantityLiteral) |
405 | term.getExprAtom(); |
406 | String value = quantity.getValue(); |
407 | if (!isZeroOrHasUnits(value)) { |
408 | // Missing units. |
409 | CssTree.QuantityLiteral withUnits = new CssTree.QuantityLiteral( |
410 | quantity.getFilePosition(), value + "px"); |
411 | withUnits.getAttributes().putAll(quantity.getAttributes()); |
412 | term.replaceChild(withUnits, quantity); |
413 | mq.addMessage(PluginMessageType.ASSUMING_PIXELS_FOR_LENGTH, |
414 | quantity.getFilePosition(), |
415 | MessagePart.Factory.valueOf(value)); |
416 | } |
417 | return false; |
418 | } else if (stdColorMatcher != null |
419 | && CssPropertyPartType.IDENT == partType |
420 | && (propertyPart(term).getCanonicalForm() |
421 | .endsWith("::color"))) { |
422 | Name colorName = Name.css( |
423 | ((CssTree.IdentLiteral) term.getExprAtom()).getValue()); |
424 | if (!stdColorMatcher.matcher(colorName.getCanonicalForm() + " ") |
425 | .matches()) { |
426 | FilePosition pos = term.getExprAtom().getFilePosition(); |
427 | CssTree.HashLiteral replacement = colorHash(pos, colorName); |
428 | MessageLevel lvl = MessageLevel.LINT; |
429 | if (replacement == null) { |
430 | lvl = MessageLevel.ERROR; |
431 | replacement = CssTree.HashLiteral.hex(pos, 0, 3); |
432 | } |
433 | term.replaceChild(replacement, term.getExprAtom()); |
434 | mq.addMessage( |
435 | PluginMessageType.NON_STANDARD_COLOR, lvl, pos, colorName, |
436 | MessagePart.Factory.valueOf(replacement.getValue())); |
437 | } |
438 | return false; |
439 | } |
440 | return true; |
441 | } |
442 | }, t.parent); |
443 | } |
444 | private static boolean isZeroOrHasUnits(String value) { |
445 | int len = value.length(); |
446 | char ch = value.charAt(len - 1); |
447 | if (ch == '.' || ('0' <= ch && ch <= '9')) { // Missing units |
448 | for (int i = len; --i >= 0;) { |
449 | ch = value.charAt(i); |
450 | if ('1' <= ch && ch <= '9') { return false; } |
451 | } |
452 | } |
453 | return true; |
454 | } |
455 | |
456 | /** Get rid of rules like <code>p { }</code>. */ |
457 | private void removeEmptyDeclarationsAndSelectors( |
458 | AncestorChain<? extends CssTree> t) { |
459 | t.node.acceptPreOrder(new Visitor() { |
460 | public boolean visit(AncestorChain<?> ancestors) { |
461 | ParseTreeNode node = ancestors.node; |
462 | if (node instanceof CssTree.EmptyDeclaration) { |
463 | ParseTreeNode parent = ancestors.getParentNode(); |
464 | if (parent instanceof MutableParseTreeNode) { |
465 | ((MutableParseTreeNode) parent).removeChild(node); |
466 | } |
467 | return false; |
468 | } else if (node instanceof CssTree.Selector) { |
469 | CssTree.Selector sel = (CssTree.Selector) node; |
470 | if (sel.children().isEmpty() |
471 | || !(sel.children().get(0) instanceof CssTree.SimpleSelector)) { |
472 | // Remove from parent |
473 | ParseTreeNode parent = ancestors.getParentNode(); |
474 | if (parent instanceof MutableParseTreeNode) { |
475 | ((MutableParseTreeNode) parent).removeChild(sel); |
476 | } |
477 | } |
478 | return false; |
479 | } |
480 | return true; |
481 | } |
482 | }, t.parent); |
483 | } |
484 | private void removeEmptyRuleSets(AncestorChain<? extends CssTree> t) { |
485 | t.node.acceptPreOrder(new Visitor() { |
486 | public boolean visit(AncestorChain<?> ancestors) { |
487 | ParseTreeNode node = ancestors.node; |
488 | if (!(node instanceof CssTree.RuleSet)) { return true; } |
489 | CssTree.RuleSet rset = (CssTree.RuleSet) node; |
490 | List<? extends CssTree> children = rset.children(); |
491 | if (children.isEmpty() |
492 | || (children.get(children.size() - 1) |
493 | instanceof CssTree.Selector) |
494 | || !(children.get(0) instanceof CssTree.Selector)) { |
495 | // No declarations or no selectors, so either the properties apply |
496 | // to nothing or there are no properties to apply. |
497 | ParseTreeNode parent = ancestors.getParentNode(); |
498 | if (parent instanceof MutableParseTreeNode) { |
499 | ((MutableParseTreeNode) parent).removeChild(rset); |
500 | } |
501 | } |
502 | return false; |
503 | } |
504 | }, t.parent); |
505 | } |
506 | private void removeForbiddenIdents(AncestorChain<? extends CssTree> t) { |
507 | t.node.acceptPreOrder(new Visitor() { |
508 | public boolean visit(AncestorChain<?> ac) { |
509 | if (!(ac.node instanceof CssTree.SimpleSelector)) { return true; } |
510 | CssTree.SimpleSelector ss = (CssTree.SimpleSelector) ac.node; |
511 | boolean ok = false; |
512 | for (CssTree child : ss.children()) { |
513 | if (child instanceof CssTree.ClassLiteral |
514 | || child instanceof CssTree.IdLiteral) { |
515 | String literal = (String) child.getValue(); |
516 | if (literal.endsWith("__")) { |
517 | mq.addMessage(PluginMessageType.UNSAFE_CSS_IDENTIFIER, |
518 | child.getFilePosition(), |
519 | MessagePart.Factory.valueOf(literal)); |
520 | ac.parent.node.getAttributes().set(CssValidator.INVALID, true); |
521 | ok = false; |
522 | } |
523 | } |
524 | } |
525 | return ok; |
526 | } |
527 | }, t.parent); |
528 | } |
529 | |
530 | private static final Set<Name> ALLOWED_PSEUDO_CLASSES = Sets.immutableSet( |
531 | Name.css("active"), Name.css("after"), Name.css("before"), |
532 | Name.css("first-child"), Name.css("first-letter"), Name.css("focus"), |
533 | Name.css("link"), Name.css("hover")); |
534 | void removeUnsafeConstructs(AncestorChain<? extends CssTree> t) { |
535 | |
536 | // 1) Check that all classes, ids, property names, etc. are valid |
537 | // css identifiers. |
538 | t.node.acceptPreOrder(new Visitor() { |
539 | public boolean visit(AncestorChain<?> ancestors) { |
540 | ParseTreeNode node = ancestors.node; |
541 | if (node instanceof CssTree.SimpleSelector) { |
542 | for (CssTree child : ((CssTree.SimpleSelector) node).children()) { |
543 | if (child instanceof CssTree.Pseudo) { |
544 | child = child.children().get(0); |
545 | // TODO(mikesamuel): check argument if child now a FunctionCall |
546 | } |
547 | Object value = child.getValue(); |
548 | if (value != null && !isSafeSelectorPart(value.toString())) { |
549 | mq.addMessage(PluginMessageType.UNSAFE_CSS_IDENTIFIER, |
550 | child.getFilePosition(), |
551 | MessagePart.Factory.valueOf(value.toString())); |
552 | // Will be deleted by a later pass after all messages have been |
553 | // generated |
554 | node.getAttributes().set(CssValidator.INVALID, Boolean.TRUE); |
555 | return false; |
556 | } |
557 | } |
558 | } |
559 | // The CssValidator checks the safety of CSS property names. |
560 | return true; |
561 | } |
562 | }, t.parent); |
563 | |
564 | // 2) Ban content properties, and attr pseudo classes, and any other |
565 | // pseudo selectors that don't match the whitelist |
566 | t.node.acceptPreOrder(new Visitor() { |
567 | public boolean visit(AncestorChain<?> ancestors) { |
568 | ParseTreeNode node = ancestors.node; |
569 | if (node instanceof CssTree.Pseudo) { |
570 | boolean remove = false; |
571 | CssTree child = ((CssTree.Pseudo) node).children().get(0); |
572 | if (child instanceof CssTree.IdentLiteral) { |
573 | Name pseudoName = Name.css( |
574 | ((CssTree.IdentLiteral) child).getValue()); |
575 | if (!ALLOWED_PSEUDO_CLASSES.contains(pseudoName)) { |
576 | // Allow the visited pseudo selector but not with any styles |
577 | // that can be fetched via getComputedStyle in DOMita's |
578 | // COMPUTED_STYLE_WHITELIST. |
579 | if (!(LINK_PSEUDO_CLASSES.contains(pseudoName) |
580 | && strippedPropertiesBannedInLinkClasses( |
581 | ancestors.parent.parent.cast(CssTree.Selector.class) |
582 | ))) { |
583 | mq.addMessage(PluginMessageType.UNSAFE_CSS_PSEUDO_SELECTOR, |
584 | invalidNodeMessageLevel, node.getFilePosition(), |
585 | node); |
586 | remove = true; |
587 | } |
588 | } |
589 | } else { |
590 | StringBuilder rendered = new StringBuilder(); |
591 | TokenConsumer tc = new CssPrettyPrinter( |
592 | new Concatenator(rendered)); |
593 | node.render(new RenderContext(tc)); |
594 | tc.noMoreTokens(); |
595 | mq.addMessage(PluginMessageType.UNSAFE_CSS_PSEUDO_SELECTOR, |
596 | invalidNodeMessageLevel, node.getFilePosition(), |
597 | MessagePart.Factory.valueOf(rendered.toString())); |
598 | remove = true; |
599 | } |
600 | if (remove) { |
601 | // Delete the containing selector, since otherwise we'd broaden |
602 | // the rule. |
603 | selectorFor(ancestors).getAttributes().set( |
604 | CssValidator.INVALID, Boolean.TRUE); |
605 | } |
606 | } |
607 | return true; |
608 | } |
609 | }, t.parent); |
610 | // 3) Remove any properties and attributes that didn't validate |
611 | t.node.acceptPreOrder(new Visitor() { |
612 | public boolean visit(AncestorChain<?> ancestors) { |
613 | ParseTreeNode node = ancestors.node; |
614 | if (node instanceof CssTree.Property) { |
615 | if (node.getAttributes().is(CssValidator.INVALID)) { |
616 | declarationFor(ancestors).getAttributes().set( |
617 | CssValidator.INVALID, Boolean.TRUE); |
618 | } |
619 | } else if (node instanceof CssTree.Attrib) { |
620 | if (node.getAttributes().is(CssValidator.INVALID)) { |
621 | simpleSelectorFor(ancestors).getAttributes().set( |
622 | CssValidator.INVALID, Boolean.TRUE); |
623 | } |
624 | } else if (node instanceof CssTree.Term |
625 | && (CssPropertyPartType.URI == propertyPartType(node))) { |
626 | |
627 | boolean remove = false; |
628 | Message removeMsg = null; |
629 | |
630 | CssTree term = (CssTree.Term) node; |
631 | CssTree.CssLiteral content = |
632 | (CssTree.CssLiteral) term.children().get(0); |
633 | |
634 | if (content instanceof CssTree.Substitution) { |
635 | return true; // Handled by later pass. |
636 | } |
637 | |
638 | String uriStr = content.getValue(); |
639 | try { |
640 | URI baseUri = content.getFilePosition().source().getUri(); |
641 | URI uri = baseUri.resolve(new URI(uriStr)); |
642 | ExternalReference ref = new ExternalReference( |
643 | uri, content.getFilePosition()); |
644 | Name propertyPart = propertyPart(node); // TODO |
645 | if (uriPolicy.rewriteUri( |
646 | ref, UriEffect.SAME_DOCUMENT, LoaderType.SANDBOXED, |
647 | Collections.singletonMap( |
648 | UriPolicyHintKey.CSS_PROP.key, propertyPart)) |
649 | == null) { |
650 | removeMsg = new Message( |
651 | PluginMessageType.DISALLOWED_URI, |
652 | node.getFilePosition(), |
653 | MessagePart.Factory.valueOf(uriStr)); |
654 | remove = true; |
655 | } |
656 | } catch (URISyntaxException ex) { |
657 | removeMsg = new Message( |
658 | PluginMessageType.DISALLOWED_URI, |
659 | node.getFilePosition(), MessagePart.Factory.valueOf(uriStr)); |
660 | remove = true; |
661 | } |
662 | |
663 | if (remove) { |
664 | // condemn the containing declaration |
665 | CssTree.Declaration decl = declarationFor(ancestors); |
666 | if (null != decl) { |
667 | if (!decl.getAttributes().is(CssValidator.INVALID)) { |
668 | if (null != removeMsg) { mq.getMessages().add(removeMsg); } |
669 | decl.getAttributes().set(CssValidator.INVALID, Boolean.TRUE); |
670 | } |
671 | } |
672 | } |
673 | } |
674 | return true; |
675 | } |
676 | }, t.parent); |
677 | |
678 | // 4) Remove invalid nodes |
679 | removeInvalidNodes(t); |
680 | |
681 | // 5) Cleanup. Remove any rulesets with empty selectors |
682 | // Since this is a post order traversal, we will first remove empty |
683 | // selectors, and then consider any rulesets that have become empty due to |
684 | // a lack of selectors. |
685 | t.node.acceptPreOrder(new Visitor() { |
686 | public boolean visit(AncestorChain<?> ancestors) { |
687 | ParseTreeNode node = ancestors.node; |
688 | if ((node instanceof CssTree.Selector && node.children().isEmpty()) |
689 | || (node instanceof CssTree.RuleSet |
690 | && (node.children().isEmpty() |
691 | || node.children().get(0) instanceof CssTree.Declaration)) |
692 | ) { |
693 | ((MutableParseTreeNode) ancestors.parent.node).removeChild(node); |
694 | return false; |
695 | } |
696 | return true; |
697 | } |
698 | }, t.parent); |
699 | } |
700 | |
701 | private void removeInvalidNodes(AncestorChain<? extends CssTree> t) { |
702 | if (t.node.getAttributes().is(CssValidator.INVALID)) { |
703 | ((MutableParseTreeNode) t.parent.node).removeChild(t.node); |
704 | return; |
705 | } |
706 | |
707 | // Use a mutation to remove invalid nodes so that the sanity checks in |
708 | // childrenChanged sees all removals at once. |
709 | MutableParseTreeNode.Mutation mut = null; |
710 | for (CssTree child : t.node.children()) { |
711 | if (child.getAttributes().is(CssValidator.INVALID)) { |
712 | if (mut == null) { mut = t.node.createMutation(); } |
713 | mut.removeChild(child); |
714 | } else { |
715 | removeInvalidNodes(AncestorChain.instance(t, child)); |
716 | } |
717 | } |
718 | if (mut != null) { mut.execute(); } |
719 | } |
720 | |
721 | private void translateUrls(AncestorChain<? extends CssTree> t) { |
722 | t.node.acceptPreOrder(new Visitor() { |
723 | public boolean visit(AncestorChain<?> ancestors) { |
724 | ParseTreeNode node = ancestors.node; |
725 | if (node instanceof CssTree.Term |
726 | && CssPropertyPartType.URI == propertyPartType(node)) { |
727 | CssTree term = (CssTree.Term) node; |
728 | |
729 | CssTree.CssLiteral content = |
730 | (CssTree.CssLiteral) term.children().get(0); |
731 | if (content instanceof CssTree.Substitution) { |
732 | return true; // Handled by later pass. |
733 | } |
734 | |
735 | Name propertyPart = propertyPart(node); |
736 | String uriStr = content.getValue(); |
737 | try { |
738 | URI baseUri = content.getFilePosition().source().getUri(); |
739 | URI uri = baseUri.resolve(new URI(uriStr)); |
740 | // Rewrite the URI. |
741 | // TODO(mikesamuel): for content: and other URI types, use |
742 | // mime-type of text/*. |
743 | ExternalReference ref = new ExternalReference( |
744 | uri, content.getFilePosition()); |
745 | String rewrittenUri = uriPolicy.rewriteUri( |
746 | ref, UriEffect.SAME_DOCUMENT, LoaderType.SANDBOXED, |
747 | Collections.singletonMap( |
748 | UriPolicyHintKey.CSS_PROP.key, propertyPart)); |
749 | CssTree.UriLiteral replacement = new CssTree.UriLiteral( |
750 | content.getFilePosition(), URI.create(rewrittenUri)); |
751 | replacement.getAttributes().putAll(content.getAttributes()); |
752 | term.replaceChild(replacement, content); |
753 | } catch (URISyntaxException ex) { |
754 | // Should've been checked in removeUnsafeConstructs. |
755 | throw new SomethingWidgyHappenedError(ex); |
756 | } |
757 | } |
758 | return true; |
759 | } |
760 | }, t.parent); |
761 | } |
762 | |
763 | private static CssTree.Declaration declarationFor(AncestorChain<?> chain) { |
764 | for (AncestorChain<?> c = chain; null != c; c = c.parent) { |
765 | if (c.node instanceof CssTree.Declaration) { |
766 | return (CssTree.Declaration) c.node; |
767 | } |
768 | } |
769 | return null; |
770 | } |
771 | |
772 | private static CssTree.SimpleSelector simpleSelectorFor( |
773 | AncestorChain<?> chain) { |
774 | for (AncestorChain<?> c = chain; null != c; c = c.parent) { |
775 | if (c.node instanceof CssTree.SimpleSelector) { |
776 | return (CssTree.SimpleSelector) c.node; |
777 | } |
778 | } |
779 | return null; |
780 | } |
781 | |
782 | private static CssTree.Selector selectorFor(AncestorChain<?> chain) { |
783 | for (AncestorChain<?> c = chain; null != c; c = c.parent) { |
784 | if (c.node instanceof CssTree.Selector) { |
785 | return (CssTree.Selector) c.node; |
786 | } |
787 | } |
788 | return null; |
789 | } |
790 | |
791 | private static final Set<Name> PROPERTIES_ALLOWED_IN_LINK_CLASSES; |
792 | static { |
793 | Set<Name> propNames = Sets.newHashSet( |
794 | Name.css("background-color"), Name.css("color"), Name.css("cursor")); |
795 | // Rules limited to link and visited styles cannot allow properties that |
796 | // can be tested by DOMita's getComputedStyle since it would allow history |
797 | // mining. |
798 | // Do not inline the below. The removeAll relies on the input being a set |
799 | // of names, but since removeAll takes a Collection<?> it would fail |
800 | // silently if the whitelist were changed to a Collection<String>. |
801 | // Assigning to a local does type-check though. |
802 | Set<Name> computedStyleNames |
803 | = CssPropertyPatterns.HISTORY_INSENSITIVE_STYLE_WHITELIST; |
804 | propNames.removeAll(computedStyleNames); |
805 | PROPERTIES_ALLOWED_IN_LINK_CLASSES = Sets.immutableSet(propNames); |
806 | } |
807 | private boolean strippedPropertiesBannedInLinkClasses( |
808 | AncestorChain<CssTree.Selector> sel) { |
809 | if (!(sel.parent.node instanceof CssTree.RuleSet)) { return false; } |
810 | Set<Name> propertyNames = PROPERTIES_ALLOWED_IN_LINK_CLASSES; |
811 | CssTree.RuleSet rs = sel.parent.cast(CssTree.RuleSet.class).node; |
812 | MutableParseTreeNode.Mutation mut = rs.createMutation(); |
813 | for (CssTree child : rs.children()) { |
814 | if (child instanceof CssTree.Selector |
815 | || child instanceof CssTree.EmptyDeclaration) { |
816 | continue; |
817 | } |
818 | CssTree.PropertyDeclaration pd; |
819 | if (child instanceof CssTree.PropertyDeclaration) { |
820 | pd = (CssTree.PropertyDeclaration) child; |
821 | } else { |
822 | pd = ((CssTree.UserAgentHack) child).getDeclaration(); |
823 | } |
824 | CssTree.Property p = pd.getProperty(); |
825 | Name propName = p.getPropertyName(); |
826 | boolean allowedInLinkClass = propertyNames.contains(propName); |
827 | if (!allowedInLinkClass && propName.getCanonicalForm().startsWith("_")) { |
828 | allowedInLinkClass = propertyNames.contains(Name.css( |
829 | propName.getCanonicalForm().substring(1))); |
830 | } |
831 | if (!allowedInLinkClass || mightContainUrl(pd.getExpr())) { |
832 | mq.getMessages().add(new Message( |
833 | PluginMessageType.DISALLOWED_CSS_PROPERTY_IN_SELECTOR, |
834 | this.invalidNodeMessageLevel, |
835 | p.getFilePosition(), p.getPropertyName(), |
836 | sel.node.getFilePosition())); |
837 | mut.removeChild(child); |
838 | } |
839 | } |
840 | mut.execute(); |
841 | return true; |
842 | } |
843 | |
844 | private boolean mightContainUrl(CssTree.Expr expr) { |
845 | for (int n = expr.getNTerms(), i = 0; i < n; ++i) { |
846 | CssTree.CssExprAtom atom = expr.getNthTerm(i).getExprAtom(); |
847 | if (!(atom instanceof CssTree.IdentLiteral |
848 | || atom instanceof CssTree.QuantityLiteral |
849 | || atom instanceof CssTree.HashLiteral)) { |
850 | return true; |
851 | } |
852 | } |
853 | return false; |
854 | } |
855 | |
856 | private static final Pattern SAFE_SELECTOR_PART |
857 | = Pattern.compile("^[#!\\.]?[a-zA-Z][_a-zA-Z0-9\\-]*$"); |
858 | /** |
859 | * Restrict selectors to ascii characters until we can test browser handling |
860 | * of escape sequences. |
861 | */ |
862 | private static boolean isSafeSelectorPart(String s) { |
863 | return SAFE_SELECTOR_PART.matcher(s).matches(); |
864 | } |
865 | |
866 | private static Name propertyPart(ParseTreeNode node) { |
867 | return node.getAttributes().get(CssValidator.CSS_PROPERTY_PART); |
868 | } |
869 | |
870 | private static CssPropertyPartType propertyPartType(ParseTreeNode node) { |
871 | return node.getAttributes().get(CssValidator.CSS_PROPERTY_PART_TYPE); |
872 | } |
873 | |
874 | public static CssTree.HashLiteral colorHash(FilePosition pos, Name color) { |
875 | Integer hexI = CSS3_COLORS.get(color.getCanonicalForm()); |
876 | return hexI != null ? colorHash(pos, hexI) : null; |
877 | } |
878 | |
879 | public static CssTree.HashLiteral colorHash(FilePosition pos, int hex) { |
880 | if ((hex & 0x0f0f0f) == ((hex >>> 4) & 0x0f0f0f)) { // #rgb |
881 | return CssTree.HashLiteral.hex( |
882 | pos, ((hex >>> 8) & 0xf00) | ((hex >>> 4) & 0xf0) | (hex & 0xf), 3); |
883 | } else { // #rrggbb |
884 | return CssTree.HashLiteral.hex(pos, hex, 6); |
885 | } |
886 | } |
887 | |
888 | // http://www.w3.org/TR/css3-iccprof#x11-color |
889 | private static final Map<String, Integer> CSS3_COLORS |
890 | = Maps.<String, Integer>immutableMap() |
891 | .put("aliceblue", 0xF0F8FF) |
892 | .put("antiquewhite", 0xFAEBD7) |
893 | .put("aqua", 0x00FFFF) |
894 | .put("aquamarine", 0x7FFFD4) |
895 | .put("azure", 0xF0FFFF) |
896 | .put("beige", 0xF5F5DC) |
897 | .put("bisque", 0xFFE4C4) |
898 | .put("black", 0x000000) |
899 | .put("blanchedalmond", 0xFFEBCD) |
900 | .put("blue", 0x0000FF) |
901 | .put("blueviolet", 0x8A2BE2) |
902 | .put("brown", 0xA52A2A) |
903 | .put("burlywood", 0xDEB887) |
904 | .put("cadetblue", 0x5F9EA0) |
905 | .put("chartreuse", 0x7FFF00) |
906 | .put("chocolate", 0xD2691E) |
907 | .put("coral", 0xFF7F50) |
908 | .put("cornflowerblue", 0x6495ED) |
909 | .put("cornsilk", 0xFFF8DC) |
910 | .put("crimson", 0xDC143C) |
911 | .put("cyan", 0x00FFFF) |
912 | .put("darkblue", 0x00008B) |
913 | .put("darkcyan", 0x008B8B) |
914 | .put("darkgoldenrod", 0xB8860B) |
915 | .put("darkgray", 0xA9A9A9) |
916 | .put("darkgreen", 0x006400) |
917 | .put("darkkhaki", 0xBDB76B) |
918 | .put("darkmagenta", 0x8B008B) |
919 | .put("darkolivegreen", 0x556B2F) |
920 | .put("darkorange", 0xFF8C00) |
921 | .put("darkorchid", 0x9932CC) |
922 | .put("darkred", 0x8B0000) |
923 | .put("darksalmon", 0xE9967A) |
924 | .put("darkseagreen", 0x8FBC8F) |
925 | .put("darkslateblue", 0x483D8B) |
926 | .put("darkslategray", 0x2F4F4F) |
927 | .put("darkturquoise", 0x00CED1) |
928 | .put("darkviolet", 0x9400D3) |
929 | .put("deeppink", 0xFF1493) |
930 | .put("deepskyblue", 0x00BFFF) |
931 | .put("dimgray", 0x696969) |
932 | .put("dodgerblue", 0x1E90FF) |
933 | .put("firebrick", 0xB22222) |
934 | .put("floralwhite", 0xFFFAF0) |
935 | .put("forestgreen", 0x228B22) |
936 | .put("fuchsia", 0xFF00FF) |
937 | .put("gainsboro", 0xDCDCDC) |
938 | .put("ghostwhite", 0xF8F8FF) |
939 | .put("gold", 0xFFD700) |
940 | .put("goldenrod", 0xDAA520) |
941 | .put("gray", 0x808080) |
942 | .put("green", 0x008000) |
943 | .put("greenyellow", 0xADFF2F) |
944 | .put("honeydew", 0xF0FFF0) |
945 | .put("hotpink", 0xFF69B4) |
946 | .put("indianred", 0xCD5C5C) |
947 | .put("indigo", 0x4B0082) |
948 | .put("ivory", 0xFFFFF0) |
949 | .put("khaki", 0xF0E68C) |
950 | .put("lavender", 0xE6E6FA) |
951 | .put("lavenderblush", 0xFFF0F5) |
952 | .put("lawngreen", 0x7CFC00) |
953 | .put("lemonchiffon", 0xFFFACD) |
954 | .put("lightblue", 0xADD8E6) |
955 | .put("lightcoral", 0xF08080) |
956 | .put("lightcyan", 0xE0FFFF) |
957 | .put("lightgoldenrodyellow", 0xFAFAD2) |
958 | .put("lightgreen", 0x90EE90) |
959 | .put("lightgrey", 0xD3D3D3) |
960 | .put("lightpink", 0xFFB6C1) |
961 | .put("lightsalmon", 0xFFA07A) |
962 | .put("lightseagreen", 0x20B2AA) |
963 | .put("lightskyblue", 0x87CEFA) |
964 | .put("lightslategray", 0x778899) |
965 | .put("lightsteelblue", 0xB0C4DE) |
966 | .put("lightyellow", 0xFFFFE0) |
967 | .put("lime", 0x00FF00) |
968 | .put("limegreen", 0x32CD32) |
969 | .put("linen", 0xFAF0E6) |
970 | .put("magenta", 0xFF00FF) |
971 | .put("maroon", 0x800000) |
972 | .put("mediumaquamarine", 0x66CDAA) |
973 | .put("mediumblue", 0x0000CD) |
974 | .put("mediumorchid", 0xBA55D3) |
975 | .put("mediumpurple", 0x9370DB) |
976 | .put("mediumseagreen", 0x3CB371) |
977 | .put("mediumslateblue", 0x7B68EE) |
978 | .put("mediumspringgreen", 0x00FA9A) |
979 | .put("mediumturquoise", 0x48D1CC) |
980 | .put("mediumvioletred", 0xC71585) |
981 | .put("midnightblue", 0x191970) |
982 | .put("mintcream", 0xF5FFFA) |
983 | .put("mistyrose", 0xFFE4E1) |
984 | .put("moccasin", 0xFFE4B5) |
985 | .put("navajowhite", 0xFFDEAD) |
986 | .put("navy", 0x000080) |
987 | .put("oldlace", 0xFDF5E6) |
988 | .put("olive", 0x808000) |
989 | .put("olivedrab", 0x6B8E23) |
990 | .put("orange", 0xFFA500) |
991 | .put("orangered", 0xFF4500) |
992 | .put("orchid", 0xDA70D6) |
993 | .put("palegoldenrod", 0xEEE8AA) |
994 | .put("palegreen", 0x98FB98) |
995 | .put("paleturquoise", 0xAFEEEE) |
996 | .put("palevioletred", 0xDB7093) |
997 | .put("papayawhip", 0xFFEFD5) |
998 | .put("peachpuff", 0xFFDAB9) |
999 | .put("peru", 0xCD853F) |
1000 | .put("pink", 0xFFC0CB) |
1001 | .put("plum", 0xDDA0DD) |
1002 | .put("powderblue", 0xB0E0E6) |
1003 | .put("purple", 0x800080) |
1004 | .put("red", 0xFF0000) |
1005 | .put("rosybrown", 0xBC8F8F) |
1006 | .put("royalblue", 0x4169E1) |
1007 | .put("saddlebrown", 0x8B4513) |
1008 | .put("salmon", 0xFA8072) |
1009 | .put("sandybrown", 0xF4A460) |
1010 | .put("seagreen", 0x2E8B57) |
1011 | .put("seashell", 0xFFF5EE) |
1012 | .put("sienna", 0xA0522D) |
1013 | .put("silver", 0xC0C0C0) |
1014 | .put("skyblue", 0x87CEEB) |
1015 | .put("slateblue", 0x6A5ACD) |
1016 | .put("slategray", 0x708090) |
1017 | .put("snow", 0xFFFAFA) |
1018 | .put("springgreen", 0x00FF7F) |
1019 | .put("steelblue", 0x4682B4) |
1020 | .put("tan", 0xD2B48C) |
1021 | .put("teal", 0x008080) |
1022 | .put("thistle", 0xD8BFD8) |
1023 | .put("tomato", 0xFF6347) |
1024 | .put("turquoise", 0x40E0D0) |
1025 | .put("violet", 0xEE82EE) |
1026 | .put("wheat", 0xF5DEB3) |
1027 | .put("white", 0xFFFFFF) |
1028 | .put("whitesmoke", 0xF5F5F5) |
1029 | .put("yellow", 0xFFFF00) |
1030 | .put("yellowgreen", 0x9ACD32) |
1031 | .create(); |
1032 | } |