Revision: 4809
Author: [email protected]
Date: Fri Mar 16 09:15:11 2012
Log: Integrate CSS rewriter tests with client side CSS rewriter
http://codereview.appspot.com/5836053
This fixes a number of bugs in the client side CSS rewriter and
wires up tests.
Following CLs will:
1. add another parameter to sanitizeStylesheet to take a URL policy
and re-enable the background-image tests for the client side rewriter
2. remove trivial differences in the output between the two rewriters
to deal with ordering of split rulesets, choice of quotes,
whether font-names are case-preserved
3. implement case-sensitive rule bodies
[email protected]
http://code.google.com/p/google-caja/source/detail?r=4809
Added:
/trunk/tests/com/google/caja/plugin/CssStylesheetTest.java
/trunk/tests/com/google/caja/plugin/css-stylesheet-test.html
/trunk/tests/com/google/caja/plugin/css-stylesheet-test.js
Modified:
/trunk/src/com/google/caja/plugin/cssparser.js
/trunk/src/com/google/caja/plugin/sanitizecss.js
/trunk/tests/com/google/caja/plugin/CssRewriterTest.java
/trunk/tests/com/google/caja/plugin/css-stylesheet-tests.js
=======================================
--- /dev/null
+++ /trunk/tests/com/google/caja/plugin/CssStylesheetTest.java Fri Mar 16
09:15:11 2012
@@ -0,0 +1,30 @@
+// Copyright (C) 2012 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.caja.plugin;
+
+import com.google.caja.util.CajaTestCase;
+import com.google.caja.util.RhinoTestBed;
+
+/**
+ * JUnit wrapper for CSS stylesheet JSUnit unittests.
+ *
+ * @author [email protected]
+ */
+public final class CssStylesheetTest extends CajaTestCase {
+ public final void testHtmlSanitizer() throws Exception {
+ RhinoTestBed.runJsUnittestFromHtml(
+ html(fromResource("css-stylesheet-test.html")));
+ }
+}
=======================================
--- /dev/null
+++ /trunk/tests/com/google/caja/plugin/css-stylesheet-test.html Fri Mar 16
09:15:11 2012
@@ -0,0 +1,32 @@
+<!doctype html>
+<!--
+ - Copyright (C) 2012 Google Inc.
+ -
+ - Licensed under the Apache License, Version 2.0 (the "License");
+ - you may not use this file except in compliance with the License.
+ - You may obtain a copy of the License at
+ -
+ - http://www.apache.org/licenses/LICENSE-2.0
+ -
+ - Unless required by applicable law or agreed to in writing, software
+ - distributed under the License is distributed on an "AS IS" BASIS,
+ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ - See the License for the specific language governing permissions and
+ - limitations under the License.
+-->
+<title>CSS Lexer Test</title>
+
+<style>
+p { font-family: monospace; white-space: pre-wrap }
+</style>
+<script type="text/javascript"
src="../../../../js/jsunit/2.2/jsUnitCore.js"
+ ></script>
+<script type="text/javascript" src="html4-defs.js"></script>
+<script type="text/javascript" src="css-defs.js"></script>
+<script type="text/javascript" src="jsunit.js"></script>
+<script type="text/javascript" src="csslexer.js"></script>
+<script type="text/javascript" src="cssparser.js"></script>
+<script type="text/javascript" src="sanitizecss.js"></script>
+<script type="text/javascript" src="css-stylesheet-test.js"></script>
+<script type="text/javascript" src="css-stylesheet-tests.js"></script>
+<script>jsunitRun()</script>
=======================================
--- /dev/null
+++ /trunk/tests/com/google/caja/plugin/css-stylesheet-test.js Fri Mar 16
09:15:11 2012
@@ -0,0 +1,47 @@
+// Copyright (C) 2012 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Called from css-stylesheet-tests.js in a JSONP style.
+function runCssSelectorTests(testGroups) {
+ function testCssStylesheets() {
+ for (var j = 0, m = testGroups.length; j < m; ++j) {
+ var testGroup = testGroups[j];
+ var name = testGroup.test_name;
+ assertEquals('testGroups[' + j + '].name', 'string', typeof name);
+ var tests = testGroup.tests;
+ for (var i = 0, n = tests.length; i < n; ++i) {
+ var test = tests[i];
+ var input = test.cssText;
+ var golden = test.golden;
+ assertEquals(
+ name + ' tests[' + i + '].cssText', 'string', typeof input);
+ assertEquals(
+ name + ' tests[' + i + '].golden', 'string', typeof golden);
+
+ var actual = sanitizeStylesheet(test.cssText);
+ // The Java version produces property groups without a trailing
+ // ';' since the semicolon is technically a separator in CSS.
+ // This JavaScript version does not because it is simpler to
+ // just treat it as a terminator.
+ actual = actual.replace(/;\}/g, '}');
+ if (golden !== actual && 'string' === typeof test.altGolden) {
+ golden = test.altGolden;
+ }
+ assertEquals('stylesheet test ' + i + ': ' + input, golden,
actual);
+ }
+ }
+ }
+ // Create a test method that will be called by jsUnit.
+ jsunitRegister('testCssStylesheets', testCssStylesheets);
+}
=======================================
--- /trunk/src/com/google/caja/plugin/cssparser.js Thu Jan 19 09:04:11 2012
+++ /trunk/src/com/google/caja/plugin/cssparser.js Fri Mar 16 09:15:11 2012
@@ -173,9 +173,10 @@
// Don't include any trailing space in the selector slice.
if (e > s && toks[e-1] === ' ') { --e; }
var tok = toks[i];
- if (tok != '{') {
- // Skip one token on a malformed declaration group.
- return i+1;
+ ++i; // Skip over '{'
+ if (tok !== '{') {
+ // Skips past the '{' when there is a malformed input.
+ return i;
}
if (handler.startRuleset) {
handler.startRuleset(toks.slice(s, e));
=======================================
--- /trunk/src/com/google/caja/plugin/sanitizecss.js Thu Mar 15 12:38:53
2012
+++ /trunk/src/com/google/caja/plugin/sanitizecss.js Fri Mar 16 09:15:11
2012
@@ -308,7 +308,7 @@
while (start < end) {
tok = selectors[start];
if (tok.charAt(0) === '#') {
- if (/^#_|__$/.test(tok)) { return null; }
+ if (/^#_|__$|[^#0-9A-Za-z:_\-]/.test(tok)) { return null; }
classId += tok;
} else if (tok === '.') {
if (++start < end
@@ -449,7 +449,7 @@
selector = 'head > html';
removeHistoryInsensitiveSelectors = true;
}
- safeCss.push(selector);
+ safeCss.push(selector, '{');
}
}
blockStack.push(
@@ -466,7 +466,7 @@
// history sensitive case.
: {
historySensitiveSelectors: historySensitiveSelectors,
- endOfSelecctors: safeCss.length,
+ endOfSelectors: safeCss.length - 1, // 1 is open curly
removeHistoryInsensitiveSelectors:
removeHistoryInsensitiveSelectors
});
@@ -474,17 +474,21 @@
endRuleset: function () {
var rules = blockStack.pop();
var propertiesEnd = safeCss.length;
- if (!elide && rules) {
- var extraSelectors = rules.historySensitiveSelectors;
- if (extraSelectors.length) {
- var propertyGroupTokens =
safeCss.slice(rules.endOfSelectors);
- safeCss.push(extraSelectors.join(', '));
- safeCss.push.apply(
- safeCss,
sanitizeHistorySensitive(propertyGroupTokens));
+ if (!elide) {
+ safeCss.push('}');
+ if (rules) {
+ var extraSelectors = rules.historySensitiveSelectors;
+ if (extraSelectors.length) {
+ var propertyGroupTokens =
safeCss.slice(rules.endOfSelectors);
+ safeCss.push(extraSelectors.join(', '),
+
sanitizeHistorySensitive(propertyGroupTokens));
+ }
}
}
if (rules && rules.removeHistoryInsensitiveSelectors) {
- safeCss.splice(rules.endOfSelectors - 1, propertiesEnd);
+ safeCss.splice(
+ // -1 and +1 account for curly braces.
+ rules.endOfSelectors - 1, propertiesEnd + 1);
}
checkElide();
},
@@ -502,8 +506,8 @@
}
});
function checkElide() {
- elide = blockStack.length === 0
- || blockStack[blockStack.length-1] !== null;
+ elide = blockStack.length !== 0
+ && blockStack[blockStack.length-1] !== null;
}
return safeCss.join('');
};
=======================================
--- /trunk/tests/com/google/caja/plugin/CssRewriterTest.java Thu Mar 15
12:38:53 2012
+++ /trunk/tests/com/google/caja/plugin/CssRewriterTest.java Fri Mar 16
09:15:11 2012
@@ -121,6 +121,8 @@
.getUnquotedValue();
} else if ("messages".equals(pname)) {
messages = (ArrayConstructor) prop.getValueExpr();
+ } else if ("altGolden".equals(pname)) {
+ // OK.
} else {
fail(
"Unrecognized testcase property " + pname + " in "
@@ -133,10 +135,13 @@
throw ex;
}
}
+
+ String normalizedGolden = "".equals(golden)
+ ? "" : render(css(fromString(golden)));
mq.getMessages().clear();
try {
- runTest(cssText, golden);
+ runTest(cssText, normalizedGolden);
if (messages != null) {
for (Expression message : messages.children()) {
ObjectConstructor messageObj = (ObjectConstructor) message;
=======================================
--- /trunk/tests/com/google/caja/plugin/css-stylesheet-tests.js Thu Mar 15
12:38:53 2012
+++ /trunk/tests/com/google/caja/plugin/css-stylesheet-tests.js Fri Mar 16
09:15:11 2012
@@ -20,7 +20,7 @@
* runtest call, you can parse the rest as a JSON object.
*/
-runTests([
+runCssSelectorTests([
{
"test_name": "UnknownTagsRemoved",
"tests": [
@@ -30,7 +30,7 @@
},
{
"cssText": "a, bogus, i { display: none }",
- "golden": "a, i {\n display: none\n}"
+ "golden": "a, i{display:none}"
}
]
},
@@ -50,7 +50,7 @@
},
{
"cssText": "strike, script, strong { display: none }",
- "golden": "strike, strong {\n display: none\n}",
+ "golden": "strike, strong{display:none}",
"messages": [
{
"type": "UNSAFE_TAG",
@@ -76,33 +76,37 @@
// visibility takes "hidden", not "none"
{
"cssText": "a { visibility: none }",
- "golden": ""
+ "golden": "",
+ // The JS side emits an empty property group while the Java version
+ // does not.
+ "altGolden": "a{}"
},
{
"cssText": "a { visibility: hidden; }",
- "golden": "a {\n visibility: hidden\n}"
+ "golden": "a{visibility:hidden}"
},
// no such property
{
"cssText": "a { bogus: bogus }",
- "golden": ""
+ "golden": "",
+ "altGolden": "a{}"
},
// make sure it doesn't interfere with others
{
"cssText": "a { visibility: none; font-weight: bold }",
- "golden": "a {\n font-weight: bold\n}"
+ "golden": "a{font-weight:bold}"
},
{
"cssText": "a { font-weight: bold; visibility: none }",
- "golden": "a {\n font-weight: bold\n}"
+ "golden": "a{font-weight:bold}"
},
{
"cssText": "a { bogus: bogus; font-weight: bold }",
- "golden": "a {\n font-weight: bold\n}"
+ "golden": "a{font-weight:bold}"
},
{
"cssText": "a { font-weight: bold; bogus: bogus }",
- "golden": "a {\n font-weight: bold\n}"
+ "golden": "a{font-weight:bold}"
}
]
},
@@ -112,7 +116,7 @@
{
"cssText":
"a { color: blue; content: 'booyah'; text-decoration: underline;
}",
- "golden": "a {\n color: blue;\n text-decoration: underline\n}"
+ "golden": "a{color:blue;text-decoration:underline}"
}
]
},
@@ -125,7 +129,7 @@
},
{
"cssText": "a:attr(href) { color: blue } b { font-weight: bolder
}",
- "golden": "b {\n font-weight: bolder\n}"
+ "golden": "b{font-weight:bolder}"
}
]
},
@@ -135,12 +139,15 @@
{
"cssText":
"a { font:12pt Times New Roman, Times,\"Times Old Roman\",serif
}",
- "golden": "a {\n font: 12pt 'Times New Roman', 'Times',"
- + " 'Times Old Roman', serif\n}"
+ "golden": "a{font:12pt 'Times New Roman', 'Times',"
+ + " 'Times Old Roman', serif}",
+ "altGolden": 'a{font:12pt "times new roman" , "times" ,'
+ + ' "times old roman" , serif}'
},
{
"cssText": "a { font:bold 12pt Arial Black }",
- "golden": "a {\n font: bold 12pt 'Arial Black'\n}"
+ "golden": "a{font:bold 12pt 'Arial Black'}",
+ "altGolden": 'a{font:bold 12pt "arial black"}'
}
]
},
@@ -149,15 +156,15 @@
"tests": [
{
"cssText": "a.foo { color:blue }",
- "golden": "a.foo {\n color: blue\n}"
+ "golden": "a.foo{color:blue}"
},
{
"cssText": "#foo { color: blue }",
- "golden": "#foo {\n color: blue\n}"
+ "golden": "#foo{color:blue}"
},
{
"cssText": "body.ie6 p { color: blue }",
- "golden": "body.ie6 p {\n color: blue\n}"
+ "golden": "body.ie6 p{color:blue}"
},
{
"cssText": "body { margin: 0; }",
@@ -169,7 +176,7 @@
}, // Not allowed
{
"cssText": "* html p { margin: 0; }",
- "golden": "* html p {\n margin: 0\n}"
+ "golden": "* html p{margin:0}"
},
{
"cssText": "* html { margin: 0; }",
@@ -181,11 +188,11 @@
}, // Not allowed
{
"cssText": "#foo > #bar { color: blue }",
- "golden": "#foo > #bar {\n color: blue\n}"
+ "golden": "#foo > #bar{color:blue}"
},
{
"cssText": "#foo .bar { color: blue }",
- "golden": "#foo .bar {\n color: blue\n}"
+ "golden": "#foo .bar{color:blue}"
}
]
},
@@ -194,15 +201,15 @@
"tests": [
{
"cssText": "a.foo, b#c\\2c d, .e { color:blue }", // "\\2c "
-> ","
- "golden": "a.foo, .e {\n color: blue\n}"
+ "golden": "a.foo, .e{color:blue}"
},
{
"cssText": "a.foo, .b_c {color: blue}",
- "golden": "a.foo, .b_c {\n color: blue\n}"
+ "golden": "a.foo, .b_c{color:blue}"
},
{
"cssText": "a.foo, ._c {color: blue}",
- "golden": "a.foo {\n color: blue\n}"
+ "golden": "a.foo{color:blue}"
},
{
"cssText": "a._c {_color: blue; margin:0;}",
@@ -227,11 +234,13 @@
"tests": [
{
"cssText": "a:link, a:badness { color:blue }",
- "golden": "a:link {\n color: blue\n}"
+ "golden": "a:link{color:blue}",
+ "altGolden": "a:link{}" // TODO: Allow history sensitive in JS.
},
{
"cssText": "a:visited { color:blue }",
- "golden": "a:visited {\n color: blue\n}",
+ "golden": "a:visited{color:blue}",
+ "altGolden": "a:visited{}", // TODO: Allow history sensitive in
JS.
"messages": []
},
@@ -242,7 +251,8 @@
{
"cssText":
"a:visited { color:blue; float:left; _float:left; *float:left }",
- "golden": "a:visited {\n color: blue\n}",
+ "golden": "a:visited{color:blue}",
+ "altGolden": "a:visited{}",
"messages": [
{
"type": "DISALLOWED_CSS_PROPERTY_IN_SELECTOR",
@@ -276,20 +286,24 @@
{
"cssText":
"a:visited { COLOR:blue; FLOAT:left; _FLOAT:left; *FLOAT:left }",
- "golden": "a:visited {\n color: blue\n}"
+ "golden": "a:visited{color:blue}",
+ "altGolden": "a:visited{}" // TODO
},
{
"cssText": "*:visited { color: blue; }",
- "golden": "a:visited {\n color: blue\n}"
+ "golden": "a:visited{color:blue}",
+ "altGolden": "a:visited{}" // TODO
},
{
"cssText": "#foo:visited { color: blue; }",
- "golden": "a#foo:visited {\n color: blue\n}"
+ "golden": "a#foo:visited{color:blue}",
+ "altGolden": "a#foo:visited{}" // TODO
},
{
"cssText": ".foo:link { color: blue; }",
- "golden": "a.foo:link {\n color: blue\n}"
+ "golden": "a.foo:link{color:blue}",
+ "altGolden": "a.foo:link{}" // TODO
},
{
@@ -299,12 +313,20 @@
+ " color: blue;\n"
+ "}",
"golden": ""
- + "a#foo:visited, a.bar:link {\n"
- + " color: blue\n"
- + "}\n"
- + "div, p {\n"
- + " padding: 1px;\n"
- + " color: blue\n"
+ + "a#foo:visited, a.bar:link{"
+ + "color:blue\n"
+ + "}"
+ + "div, p{"
+ + "padding:1px;"
+ + "color:blue"
+ + "}",
+ "altGolden": "" // TODO: Fix difference in order in Java.
+ + "div, p{"
+ + "padding:1px;"
+ + "color:blue"
+ + "}"
+ + "a#foo:visited, a.bar:link{"
+ //+ "color:blue\n" // TODO
+ "}"
},
@@ -315,10 +337,12 @@
+ " color: purple"
+ "}",
"golden": ""
- + "a#foo-bank {\n"
- + " background:
url('http://whitelisted-host.com/?bank=X&u=Al');\n"
- + " color: purple\n"
+ + "a#foo-bank{"
+ + "background:url('http://whitelisted-host.com/?bank=X&u=Al');"
+ + "color:purple"
+ "}",
+ // TODO: integrate URL policy into CSS sanitizer.
+ "altGolden": "a#foo-bank{color:purple}",
"messages": []
},
// Differs from the previous only in that it has the :visited pseudo
@@ -331,10 +355,8 @@
+ " background-image: 'http://whitelisted-host.com/?bank=X&u=Al';"
+ " color: purple"
+ "}",
- "golden": ""
- + "a#foo-bank:visited {\n"
- + " color: purple\n"
- + "}"
+ "golden": "a#foo-bank:visited{color:purple}",
+ "altGolden": "a#foo-bank:visited{}" // TODO
}
]
},
@@ -344,31 +366,42 @@
// ok
{
"cssText": "#foo { background: url(/bar.png) }",
- "golden": "#foo {\n background: url('/foo/bar.png')\n}"
+ "golden": "#foo{background:url('/foo/bar.png')}",
+ //"altGolden": '#foo{backgroud:url("/foo/bar.png")}' TODO
+ "altGolden": '#foo{}'
},
{
"cssText": "#foo { background: url('/bar.png') }",
- "golden": "#foo {\n background: url('/foo/bar.png')\n}"
+ "golden": "#foo{background:url('/foo/bar.png')}",
+ //"altGolden": '#foo{background:url("/foo/bar.png")}' TODO
+ "altGolden": '#foo{}'
},
{
"cssText": "#foo { background: '/bar.png' }",
- "golden": "#foo {\n background: url('/foo/bar.png')\n}"
+ "golden": "#foo{background:url('/foo/bar.png')}",
+ //"altGolden": '#foo{background:url("/foo/bar.png")}' TODO
+ "altGolden": '#foo{}'
},
{
"cssText":
"#foo { background: 'http://whitelisted-host.com/blinky.gif' }",
"golden":
- "#foo {\n background:
url('http://whitelisted-host.com/blinky.gif')\n}"
+ "#foo{background:url('http://whitelisted-host.com/blinky.gif')}",
+ "altGolden":
+// '#foo{background:url("http://whitelisted-host.com/blinky.gif")}'
+ '#foo{}'
},
// disallowed
{
"cssText": "#foo { background: url('http://cnn.com/bar.png') }",
- "golden": ""
+ "golden": "",
+ "altGolden": "#foo{}"
},
{
"cssText": "#foo { background: 'http://cnn.com/bar.png' }",
- "golden": ""
+ "golden": "",
+ "altGolden": "#foo{}"
}
]
},
@@ -379,7 +412,7 @@
"tests": [
{
"cssText": "div * { margin: 0; }",
- "golden": "div * {\n margin: 0\n}"
+ "golden": "div *{margin:0}"
}
]
},
@@ -388,11 +421,13 @@
"tests": [
{
"cssText": "div { padding: 10 0 5.0 4 }",
- "golden": "div {\n padding: 10px 0 5.0px 4px\n}"
+ "golden": "div{padding:10px 0 5.0px 4px}",
+ "altGolden": "div{padding:10 0 5.0 4}"
},
{
"cssText": "div { margin: -5 5; z-index: 2 }",
- "golden": "div {\n margin: -5px 5px;\n z-index: 2\n}"
+ "golden": "div{margin:-5px 5px;z-index:2}",
+ "altGolden": "div{margin:-5 5;z-index:2}"
}
]
},
@@ -409,12 +444,21 @@
+ " font-weight: bold\n"
+ "}",
"golden": ""
- + "p {\n"
- + " color: blue;\n"
- + " *color: red;\n" // Good user agent hack
- + " background-color: green;\n"
+ + "p{"
+ + "color:blue;"
+ + "*color:red;" // Good user agent hack
+ + "background-color:green;"
// Bad user-agent hack removed.
- + " font-weight: bold\n"
+ + "font-weight:bold"
+ + "}",
+ "altGolden": ""
+ + "p{"
+ + "color:blue;"
+ // TODO: Implement support for user-agent hacks.
+ //+ "*color:red;" // Good user agent hack
+ + "background-color:green;"
+ // Bad user-agent hack removed.
+ + "font-weight:bold"
+ "}",
"messages": [
{
@@ -429,7 +473,9 @@
},
{
"cssText": "a.c {_color: blue; margin:0;}",
- "golden": "a.c {\n _color: blue;\n margin: 0\n}",
+ "golden": "a.c{_color:blue;margin:0}",
+ // TODO: implement user agent hacks
+ "altGolden": "a.c{margin:0}",
"messages": []
}
]
@@ -440,6 +486,8 @@
{
"cssText": "a.c { color: LightSlateGray; background: ivory; }",
"golden": "a.c {\n color: #789;\n background: #fffff0\n}",
+ // TODO: see if special color names work when quoted.
+ "altGolden": "a.c{color:lightslategray;background:ivory}",
"messages": [
{
"type": "NON_STANDARD_COLOR",
@@ -466,12 +514,12 @@
"tests": [
{
"cssText": "#foo { position: absolute; left: 0px; top: 0px }",
- "golden": "#foo {\n position: absolute;\n left: 0px;\n top:
0px\n}",
+ "golden": "#foo{position:absolute;left:0px;top:0px}",
"messages": []
},
{
"cssText": "#foo { position: fixed; left: 0px; top: 0px }",
- "golden": "#foo {\n left: 0px;\n top: 0px\n}",
+ "golden": "#foo{left:0px;top:0px}",
"messages": [
// TODO(mikesamuel): fix message.
// "fixed" is well-formed but disallowed.