Revision: 4807
Author: [email protected]
Date: Thu Mar 15 12:38:53 2012
Log: Move CSS Stylesheet Rewriter tests into a JavaScript file
http://codereview.appspot.com/5784076
This reworks CssRewriterTest so that we can use tests specified in
JavaScript
both in the Java version and to test the JavaScript version.
A later CL will wire the tests into sanitize-css-test.html.
This change also factors out sanitizeCss from html-emitter, moving it to
sanitize-css.js.
[email protected]
http://code.google.com/p/google-caja/source/detail?r=4807
Added:
/trunk/tests/com/google/caja/plugin/css-stylesheet-tests.js
Modified:
/trunk/src/com/google/caja/plugin/html-emitter.js
/trunk/src/com/google/caja/plugin/sanitizecss.js
/trunk/tests/com/google/caja/plugin/CssRewriterTest.java
/trunk/tests/com/google/caja/util/CajaTestCase.java
=======================================
--- /dev/null
+++ /trunk/tests/com/google/caja/plugin/css-stylesheet-tests.js Thu Mar 15
12:38:53 2012
@@ -0,0 +1,490 @@
+// 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.
+
+/*
+ * @fileoverview
+ * These test cases are run in JS against santizeCss.js and against the
+ * CssRewriter in Java.
+ * This file is meant to be in JSONP. If you strip comments and the
+ * runtest call, you can parse the rest as a JSON object.
+ */
+
+runTests([
+ {
+ "test_name": "UnknownTagsRemoved",
+ "tests": [
+ {
+ "cssText": "bogus { display: none }",
+ "golden": ""
+ },
+ {
+ "cssText": "a, bogus, i { display: none }",
+ "golden": "a, i {\n display: none\n}"
+ }
+ ]
+ },
+ {
+ "test_name": "BadTagsRemoved",
+ "tests": [
+ {
+ "cssText": "script { display: none }",
+ "golden": "",
+ "messages": [
+ {
+ "type": "UNSAFE_TAG",
+ "level": "ERROR",
+ "args": ["script"]
+ }
+ ]
+ },
+ {
+ "cssText": "strike, script, strong { display: none }",
+ "golden": "strike, strong {\n display: none\n}",
+ "messages": [
+ {
+ "type": "UNSAFE_TAG",
+ "level": "ERROR",
+ "args": ["script"]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "test_name": "BadAttribsRemoved",
+ "tests": [
+ {
+ "cssText": "div[zwop] { color: blue }",
+ "golden": ""
+ },
+ ]
+ },
+ {
+ "test_name": "InvalidPropertiesRemoved",
+ "tests": [
+ // visibility takes "hidden", not "none"
+ {
+ "cssText": "a { visibility: none }",
+ "golden": ""
+ },
+ {
+ "cssText": "a { visibility: hidden; }",
+ "golden": "a {\n visibility: hidden\n}"
+ },
+ // no such property
+ {
+ "cssText": "a { bogus: bogus }",
+ "golden": ""
+ },
+ // make sure it doesn't interfere with others
+ {
+ "cssText": "a { visibility: none; font-weight: bold }",
+ "golden": "a {\n font-weight: bold\n}"
+ },
+ {
+ "cssText": "a { font-weight: bold; visibility: none }",
+ "golden": "a {\n font-weight: bold\n}"
+ },
+ {
+ "cssText": "a { bogus: bogus; font-weight: bold }",
+ "golden": "a {\n font-weight: bold\n}"
+ },
+ {
+ "cssText": "a { font-weight: bold; bogus: bogus }",
+ "golden": "a {\n font-weight: bold\n}"
+ }
+ ]
+ },
+ {
+ "test_name": "ContentRemoved",
+ "tests": [
+ {
+ "cssText":
+ "a { color: blue; content: 'booyah'; text-decoration: underline;
}",
+ "golden": "a {\n color: blue;\n text-decoration: underline\n}"
+ }
+ ]
+ },
+ {
+ "test_name": "AttrRemoved",
+ "tests": [
+ {
+ "cssText": "a:attr(href) { color: blue }",
+ "golden": ""
+ },
+ {
+ "cssText": "a:attr(href) { color: blue } b { font-weight: bolder
}",
+ "golden": "b {\n font-weight: bolder\n}"
+ }
+ ]
+ },
+ {
+ "test_name": "FontNamesQuoted",
+ "tests": [
+ {
+ "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}"
+ },
+ {
+ "cssText": "a { font:bold 12pt Arial Black }",
+ "golden": "a {\n font: bold 12pt 'Arial Black'\n}"
+ }
+ ]
+ },
+ {
+ "test_name": "Namespacing",
+ "tests": [
+ {
+ "cssText": "a.foo { color:blue }",
+ "golden": "a.foo {\n color: blue\n}"
+ },
+ {
+ "cssText": "#foo { color: blue }",
+ "golden": "#foo {\n color: blue\n}"
+ },
+ {
+ "cssText": "body.ie6 p { color: blue }",
+ "golden": "body.ie6 p {\n color: blue\n}"
+ },
+ {
+ "cssText": "body { margin: 0; }",
+ "golden": ""
+ }, // Not allowed
+ {
+ "cssText": "body.ie6 { margin: 0; }",
+ "golden": ""
+ }, // Not allowed
+ {
+ "cssText": "* html p { margin: 0; }",
+ "golden": "* html p {\n margin: 0\n}"
+ },
+ {
+ "cssText": "* html { margin: 0; }",
+ "golden": ""
+ }, // Not allowed
+ {
+ "cssText": "* html > * > p { margin: 0; }",
+ "golden": ""
+ }, // Not allowed
+ {
+ "cssText": "#foo > #bar { color: blue }",
+ "golden": "#foo > #bar {\n color: blue\n}"
+ },
+ {
+ "cssText": "#foo .bar { color: blue }",
+ "golden": "#foo .bar {\n color: blue\n}"
+ }
+ ]
+ },
+ {
+ "test_name": "UnsafeIdentifiers",
+ "tests": [
+ {
+ "cssText": "a.foo, b#c\\2c d, .e { color:blue }", // "\\2c "
-> ","
+ "golden": "a.foo, .e {\n color: blue\n}"
+ },
+ {
+ "cssText": "a.foo, .b_c {color: blue}",
+ "golden": "a.foo, .b_c {\n color: blue\n}"
+ },
+ {
+ "cssText": "a.foo, ._c {color: blue}",
+ "golden": "a.foo {\n color: blue\n}"
+ },
+ {
+ "cssText": "a._c {_color: blue; margin:0;}",
+ "golden": ""
+ },
+ {
+ "cssText": "a#_c {_color: blue; margin:0;}",
+ "golden": ""
+ },
+ {
+ "cssText": ".c__ {_color: blue; margin:0;}",
+ "golden": ""
+ },
+ {
+ "cssText": "#c__ {_color: blue; margin:0;}",
+ "golden": ""
+ }
+ ]
+ },
+ {
+ "test_name": "PseudosWhitelisted",
+ "tests": [
+ {
+ "cssText": "a:link, a:badness { color:blue }",
+ "golden": "a:link {\n color: blue\n}"
+ },
+ {
+ "cssText": "a:visited { color:blue }",
+ "golden": "a:visited {\n color: blue\n}",
+ "messages": []
+ },
+
+ // Properties that are on DOMita's HISTORY_INSENSITIVE_STYLE_WHITELIST
+ // should not be allowed in any rule that correlates with the :visited
+ // pseudo selector.
+ // TODO: How is this a whitelist then?
+ {
+ "cssText":
+ "a:visited { color:blue; float:left; _float:left; *float:left }",
+ "golden": "a:visited {\n color: blue\n}",
+ "messages": [
+ {
+ "type": "DISALLOWED_CSS_PROPERTY_IN_SELECTOR",
+ "level": "ERROR",
+ "args": [
+ "test:1+25@25 - 30@30",
+ "float",
+ "test:1+1@1 - 10@10"
+ ]
+ },
+ {
+ "type": "DISALLOWED_CSS_PROPERTY_IN_SELECTOR",
+ "level": "ERROR",
+ "args": [
+ "test:1+37@37 - 43@43",
+ "_float",
+ "test:1+1@1 - 10@10"
+ ]
+ },
+ {
+ "type": "DISALLOWED_CSS_PROPERTY_IN_SELECTOR",
+ "level": "ERROR",
+ "args": [
+ "test:1+51@51 - 56@56",
+ "float",
+ "test:1+1@1 - 10@10"
+ ]
+ }
+ ]
+ },
+ {
+ "cssText":
+ "a:visited { COLOR:blue; FLOAT:left; _FLOAT:left; *FLOAT:left }",
+ "golden": "a:visited {\n color: blue\n}"
+ },
+
+ {
+ "cssText": "*:visited { color: blue; }",
+ "golden": "a:visited {\n color: blue\n}"
+ },
+ {
+ "cssText": "#foo:visited { color: blue; }",
+ "golden": "a#foo:visited {\n color: blue\n}"
+ },
+ {
+ "cssText": ".foo:link { color: blue; }",
+ "golden": "a.foo:link {\n color: blue\n}"
+ },
+
+ {
+ "cssText": ""
+ + "#foo:visited, div, .bar:link, p {\n"
+ + " padding: 1px;\n"
+ + " color: blue;\n"
+ + "}",
+ "golden": ""
+ + "a#foo:visited, a.bar:link {\n"
+ + " color: blue\n"
+ + "}\n"
+ + "div, p {\n"
+ + " padding: 1px;\n"
+ + " color: blue\n"
+ + "}"
+ },
+
+ {
+ "cssText": ""
+ + "a#foo-bank {"
+ + " background: 'http://whitelisted-host.com/?bank=X&u=Al';"
+ + " color: purple"
+ + "}",
+ "golden": ""
+ + "a#foo-bank {\n"
+ + " background:
url('http://whitelisted-host.com/?bank=X&u=Al');\n"
+ + " color: purple\n"
+ + "}",
+ "messages": []
+ },
+ // Differs from the previous only in that it has the :visited pseudo
+ // selector which means we can't allow it to cause a network fetch
because
+ // that could leak user history state.
+
+ {
+ "cssText": ""
+ + "a#foo-bank:visited {"
+ + " background-image: 'http://whitelisted-host.com/?bank=X&u=Al';"
+ + " color: purple"
+ + "}",
+ "golden": ""
+ + "a#foo-bank:visited {\n"
+ + " color: purple\n"
+ + "}"
+ }
+ ]
+ },
+ {
+ "test_name": "NoBadUrls",
+ "tests": [
+ // ok
+ {
+ "cssText": "#foo { background: url(/bar.png) }",
+ "golden": "#foo {\n background: url('/foo/bar.png')\n}"
+ },
+ {
+ "cssText": "#foo { background: url('/bar.png') }",
+ "golden": "#foo {\n background: url('/foo/bar.png')\n}"
+ },
+ {
+ "cssText": "#foo { background: '/bar.png' }",
+ "golden": "#foo {\n background: url('/foo/bar.png')\n}"
+ },
+ {
+ "cssText":
+ "#foo { background: 'http://whitelisted-host.com/blinky.gif' }",
+ "golden":
+ "#foo {\n background:
url('http://whitelisted-host.com/blinky.gif')\n}"
+ },
+
+ // disallowed
+ {
+ "cssText": "#foo { background: url('http://cnn.com/bar.png') }",
+ "golden": ""
+ },
+ {
+ "cssText": "#foo { background: 'http://cnn.com/bar.png' }",
+ "golden": ""
+ }
+ ]
+ },
+ {
+ // "*" selectors should rewrite properly.
+ // http://code.google.com/p/google-caja/issues/detail?id=57
+ "test_name": "WildcardSelectors",
+ "tests": [
+ {
+ "cssText": "div * { margin: 0; }",
+ "golden": "div * {\n margin: 0\n}"
+ }
+ ]
+ },
+ {
+ "test_name": "UnitlessLengths",
+ "tests": [
+ {
+ "cssText": "div { padding: 10 0 5.0 4 }",
+ "golden": "div {\n padding: 10px 0 5.0px 4px\n}"
+ },
+ {
+ "cssText": "div { margin: -5 5; z-index: 2 }",
+ "golden": "div {\n margin: -5px 5px;\n z-index: 2\n}"
+ }
+ ]
+ },
+ {
+ "test_name": "UserAgentHacks",
+ "tests": [
+ {
+ "cssText": ""
+ + "p {\n"
+ + " color: blue;\n"
+ + " *color: red;\n"
+ + " background-color: green;\n"
+ + " *background-color: yelow;\n" // misspelled
+ + " font-weight: bold\n"
+ + "}",
+ "golden": ""
+ + "p {\n"
+ + " color: blue;\n"
+ + " *color: red;\n" // Good user agent hack
+ + " background-color: green;\n"
+ // Bad user-agent hack removed.
+ + " font-weight: bold\n"
+ + "}",
+ "messages": [
+ {
+ "type": "MALFORMED_CSS_PROPERTY_VALUE",
+ "level": "WARNING",
+ "args": [
+ "background-color",
+ "==>yelow<=="
+ ]
+ }
+ ]
+ },
+ {
+ "cssText": "a.c {_color: blue; margin:0;}",
+ "golden": "a.c {\n _color: blue;\n margin: 0\n}",
+ "messages": []
+ }
+ ]
+ },
+ {
+ "test_name": "NonStandardColors",
+ "tests": [
+ {
+ "cssText": "a.c { color: LightSlateGray; background: ivory; }",
+ "golden": "a.c {\n color: #789;\n background: #fffff0\n}",
+ "messages": [
+ {
+ "type": "NON_STANDARD_COLOR",
+ "level": "LINT",
+ "args": [
+ "lightslategray",
+ "#789"
+ ]
+ },
+ {
+ "type": "NON_STANDARD_COLOR",
+ "level": "LINT",
+ "args": [
+ "ivory",
+ "#fffff0"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "test_name": "FixedPositioning",
+ "tests": [
+ {
+ "cssText": "#foo { position: absolute; left: 0px; top: 0px }",
+ "golden": "#foo {\n position: absolute;\n left: 0px;\n top:
0px\n}",
+ "messages": []
+ },
+ {
+ "cssText": "#foo { position: fixed; left: 0px; top: 0px }",
+ "golden": "#foo {\n left: 0px;\n top: 0px\n}",
+ "messages": [
+ // TODO(mikesamuel): fix message.
+ // "fixed" is well-formed but disallowed.
+ {
+ "type": "MALFORMED_CSS_PROPERTY_VALUE",
+ "level": "WARNING",
+ "args": [
+ "position",
+ "==>fixed<=="
+ ]
+ }
+ ]
+ }
+ ]
+ }]
+)
=======================================
--- /trunk/src/com/google/caja/plugin/html-emitter.js Tue Feb 28 23:15:18
2012
+++ /trunk/src/com/google/caja/plugin/html-emitter.js Thu Mar 15 12:38:53
2012
@@ -24,8 +24,8 @@
*
* @author [email protected]
* @provides HtmlEmitter
- * @requires bridalMaker html html4 cajaVM parseCssStylesheet console
- * @requires cssSchema sanitizeCssProperty sanitizeCssSelectors
+ * @requires bridalMaker html html4 cajaVM
+ * @requires sanitizeStylesheet
*/
/**
@@ -370,164 +370,10 @@
// Ignore problems dispatching error.
}
}
-
- var allowed = {};
- var cssMediaTypeWhitelist = {
- 'braille': allowed,
- 'embossed': allowed,
- 'handheld': allowed,
- 'print': allowed,
- 'projection': allowed,
- 'screen': allowed,
- 'speech': allowed,
- 'tty': allowed,
- 'tv': allowed
- };
-
- function sanitizeHistorySensitive(blockOfProperties) {
- return '{}'; // TODO: implement me.
- }
function defineUntrustedStylesheet(cssText) {
- var safeCss = void 0;
- // A stack describing the { ... } regions.
- // Null elements indicate blocks that should not be emitted.
- var blockStack = [];
- // True when the content of the current block should be left off
safeCss.
- // If we don't have a domicile then we don't have a way to sanitize
CSS
- // properties.
- var elide = !domicile;
- parseCssStylesheet(
- cssText,
- {
- startStylesheet: function () {
- safeCss = [];
- },
- endStylesheet: function () {
- },
- startAtrule: function (atIdent, headerArray) {
- if (elide) {
- atIdent = null;
- } else if (atIdent === '@media') {
- headerArray = headerArray.filter(
- function (mediaType) {
- return cssMediaTypeWhitelist[mediaType] == allowed;
- });
- if (headerArray.length) {
- safeCss.push(atIdent, headerArray.join(','), '{');
- } else {
- atIdent = null;
- }
- } else {
- if (atIdent === '@import') {
- if ('undefined' !== typeof console) {
- console.log('@import ' + headerArray.join(' ') + '
elided');
- }
- }
- atIdent = null; // Elide the block.
- }
- elide = !atIdent;
- blockStack.push(atIdent);
- },
- endAtrule: function () {
- var atIdent = blockStack.pop();
- if (!elide) {
- safeCss.push(';');
- }
- checkElide();
- },
- startBlock: function () {
- // There are no bare blocks in CSS, so we do not change the
- // block stack here, but instead in the events that bracket
- // blocks.
- if (!elide) {
- safeCss.push('{');
- }
- },
- endBlock: function () {
- if (!elide) {
- safeCss.push('}');
- elide = true; // skip any semicolon from endAtRule.
- }
- },
- startRuleset: function (selectorArray) {
- var historySensitiveSelectors = void 0;
- var removeHistoryInsensitiveSelectors = false;
- if (!elide) {
- var selectors = sanitizeCssSelectors(selectorArray);
- var historyInsensitiveSelectors = selectors[0];
- historySensitiveSelectors = selectors[1];
- if (!historyInsensitiveSelectors.length
- && !historySensitiveSelectors.length) {
- elide = true;
- } else {
- var selector = historyInsensitiveSelectors.join(', ');
- if (!selector) {
- // If we have only history sensitive selectors,
- // use an impossible rule so that we can capture the
content
- // for later processing by
- // history insenstive content for use below.
- selector = 'head > html';
- removeHistoryInsensitiveSelectors = true;
- }
- safeCss.push(selector);
- }
- }
- blockStack.push(
- elide
- ? null
- // Sometimes a single list of selectors is split in two,
- // div, a:visited
- // because we want to allow some properties for DIV that
- // we don't want to allow for A:VISITED to avoid leaking
- // user history.
- // Store the history sensitive selectors and the position
- // where the block starts so we can later create a copy
- // of the permissive tokens, and filter it to handle the
- // history sensitive case.
- : {
- historySensitiveSelectors: historySensitiveSelectors,
- endOfSelecctors: safeCss.length,
- removeHistoryInsensitiveSelectors:
- removeHistoryInsensitiveSelectors
- });
- },
- 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 (rules && rules.removeHistoryInsensitiveSelectors) {
- safeCss.splice(rules.endOfSelectors - 1, propertiesEnd);
- }
- checkElide();
- },
- declaration: function (property, valueArray) {
- if (!elide && domicile) {
- var schema = cssSchema[property];
- var sanitizeUri = void 0; // TODO
- if (schema) {
- sanitizeCssProperty(property, valueArray, sanitizeUri);
- if (valueArray.length) {
- safeCss.push(property, ':', valueArray.join(' '), ';');
- }
- }
- }
- }
- });
- function checkElide() {
- elide = blockStack.length === 0
- || blockStack[blockStack.length-1] !== null;
- }
+ var safeCssText = sanitizeStylesheet(cssText);
var document = insertionPoint.ownerDocument;
- var safeCssText = safeCss.join('');
document.getElementsByTagName('head')[0].appendChild(
bridal.createStylesheet(document, safeCssText));
}
=======================================
--- /trunk/src/com/google/caja/plugin/sanitizecss.js Mon Feb 27 09:52:24
2012
+++ /trunk/src/com/google/caja/plugin/sanitizecss.js Thu Mar 15 12:38:53
2012
@@ -24,10 +24,14 @@
* @requires CSS_PROP_BIT_QSTRING_CONTENT
* @requires CSS_PROP_BIT_QSTRING_URL
* @requires CSS_PROP_BIT_QUANTITY
+ * @requires console
+ * @requires cssSchema
* @requires decodeCss
* @requires html4
+ * @requires parseCssStylesheet
* @provides sanitizeCssProperty
* @provides sanitizeCssSelectors
+ * @provides sanitizeStylesheet
*/
/**
@@ -345,3 +349,162 @@
return [historyInsensitiveSelectors, historySensitiveSelectors];
}
+
+var sanitizeStylesheet = (function () {
+ var allowed = {};
+ var cssMediaTypeWhitelist = {
+ 'braille': allowed,
+ 'embossed': allowed,
+ 'handheld': allowed,
+ 'print': allowed,
+ 'projection': allowed,
+ 'screen': allowed,
+ 'speech': allowed,
+ 'tty': allowed,
+ 'tv': allowed
+ };
+
+ function sanitizeHistorySensitive(blockOfProperties) {
+ return '{}'; // TODO: implement me.
+ }
+
+ return function /*sanitizeStylesheet*/(cssText) {
+ var safeCss = void 0;
+ // A stack describing the { ... } regions.
+ // Null elements indicate blocks that should not be emitted.
+ var blockStack = [];
+ // True when the content of the current block should be left off
safeCss.
+ var elide = false;
+ parseCssStylesheet(
+ cssText,
+ {
+ startStylesheet: function () {
+ safeCss = [];
+ },
+ endStylesheet: function () {
+ },
+ startAtrule: function (atIdent, headerArray) {
+ if (elide) {
+ atIdent = null;
+ } else if (atIdent === '@media') {
+ headerArray = headerArray.filter(
+ function (mediaType) {
+ return cssMediaTypeWhitelist[mediaType] == allowed;
+ });
+ if (headerArray.length) {
+ safeCss.push(atIdent, headerArray.join(','), '{');
+ } else {
+ atIdent = null;
+ }
+ } else {
+ if (atIdent === '@import') {
+ if ('undefined' !== typeof console) {
+ // TODO: Use a logger instead.
+ console.log('@import ' + headerArray.join(' ') + '
elided');
+ }
+ }
+ atIdent = null; // Elide the block.
+ }
+ elide = !atIdent;
+ blockStack.push(atIdent);
+ },
+ endAtrule: function () {
+ var atIdent = blockStack.pop();
+ if (!elide) {
+ safeCss.push(';');
+ }
+ checkElide();
+ },
+ startBlock: function () {
+ // There are no bare blocks in CSS, so we do not change the
+ // block stack here, but instead in the events that bracket
+ // blocks.
+ if (!elide) {
+ safeCss.push('{');
+ }
+ },
+ endBlock: function () {
+ if (!elide) {
+ safeCss.push('}');
+ elide = true; // skip any semicolon from endAtRule.
+ }
+ },
+ startRuleset: function (selectorArray) {
+ var historySensitiveSelectors = void 0;
+ var removeHistoryInsensitiveSelectors = false;
+ if (!elide) {
+ var selectors = sanitizeCssSelectors(selectorArray);
+ var historyInsensitiveSelectors = selectors[0];
+ historySensitiveSelectors = selectors[1];
+ if (!historyInsensitiveSelectors.length
+ && !historySensitiveSelectors.length) {
+ elide = true;
+ } else {
+ var selector = historyInsensitiveSelectors.join(', ');
+ if (!selector) {
+ // If we have only history sensitive selectors,
+ // use an impossible rule so that we can capture the
content
+ // for later processing by
+ // history insenstive content for use below.
+ selector = 'head > html';
+ removeHistoryInsensitiveSelectors = true;
+ }
+ safeCss.push(selector);
+ }
+ }
+ blockStack.push(
+ elide
+ ? null
+ // Sometimes a single list of selectors is split in two,
+ // div, a:visited
+ // because we want to allow some properties for DIV that
+ // we don't want to allow for A:VISITED to avoid leaking
+ // user history.
+ // Store the history sensitive selectors and the position
+ // where the block starts so we can later create a copy
+ // of the permissive tokens, and filter it to handle the
+ // history sensitive case.
+ : {
+ historySensitiveSelectors: historySensitiveSelectors,
+ endOfSelecctors: safeCss.length,
+ removeHistoryInsensitiveSelectors:
+ removeHistoryInsensitiveSelectors
+ });
+ },
+ 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 (rules && rules.removeHistoryInsensitiveSelectors) {
+ safeCss.splice(rules.endOfSelectors - 1, propertiesEnd);
+ }
+ checkElide();
+ },
+ declaration: function (property, valueArray) {
+ if (!elide) {
+ var schema = cssSchema[property];
+ var sanitizeUri = void 0; // TODO
+ if (schema) {
+ sanitizeCssProperty(schema, valueArray, sanitizeUri);
+ if (valueArray.length) {
+ safeCss.push(property, ':', valueArray.join(' '), ';');
+ }
+ }
+ }
+ }
+ });
+ function checkElide() {
+ elide = blockStack.length === 0
+ || blockStack[blockStack.length-1] !== null;
+ }
+ return safeCss.join('');
+ };
+})();
=======================================
--- /trunk/tests/com/google/caja/plugin/CssRewriterTest.java Thu Jan 19
09:04:11 2012
+++ /trunk/tests/com/google/caja/plugin/CssRewriterTest.java Thu Mar 15
12:38:53 2012
@@ -18,25 +18,28 @@
import com.google.caja.lang.html.HtmlSchema;
import com.google.caja.lexer.ExternalReference;
import com.google.caja.lexer.FilePosition;
+import com.google.caja.lexer.InputSource;
import com.google.caja.lexer.ParseException;
import com.google.caja.parser.AncestorChain;
+import com.google.caja.parser.MutableParseTreeNode;
import com.google.caja.parser.ParseTreeNode;
import com.google.caja.parser.Visitor;
import com.google.caja.parser.css.CssTree;
-import com.google.caja.parser.html.ElKey;
-import com.google.caja.parser.html.Namespaces;
import com.google.caja.parser.js.ArrayConstructor;
+import com.google.caja.parser.js.Expression;
+import com.google.caja.parser.js.ObjProperty;
+import com.google.caja.parser.js.ObjectConstructor;
+import com.google.caja.parser.js.Operation;
+import com.google.caja.parser.js.Operator;
import com.google.caja.parser.js.StringLiteral;
-import com.google.caja.parser.quasiliteral.QuasiBuilder;
-import com.google.caja.reporting.MessageLevel;
+import com.google.caja.parser.js.ValueProperty;
+import com.google.caja.reporting.Message;
import com.google.caja.reporting.MessagePart;
import com.google.caja.util.CajaTestCase;
-import com.google.caja.util.Executor;
-import com.google.caja.util.Join;
+import com.google.caja.util.Function;
import com.google.caja.util.Lists;
import com.google.caja.util.MoreAsserts;
import com.google.caja.util.Name;
-import com.google.caja.util.RhinoTestBed;
import com.google.caja.util.Sets;
import java.net.URI;
@@ -47,206 +50,128 @@
import java.util.Map;
import java.util.Set;
+import junit.framework.AssertionFailedError;
+
/**
*
* @author [email protected]
*/
public class CssRewriterTest extends CajaTestCase {
- public final void testUnknownTagsRemoved() throws Exception {
- runTest("bogus { display: none }", "");
- runTest("a, bogus, i { display: none }",
- "a, i {\n display: none\n}");
- }
-
- public final void testBadTagsRemoved() throws Exception {
- runTest("script { display: none }", "");
- assertMessage(
- true, PluginMessageType.UNSAFE_TAG, MessageLevel.ERROR,
- ElKey.forElement(Namespaces.HTML_DEFAULT, "script"));
- assertNoErrors();
- runTest("strike, script, strong { display: none }",
- "strike, strong {\n display: none\n}"); // See error
- assertMessage(
- true, PluginMessageType.UNSAFE_TAG, MessageLevel.ERROR,
- ElKey.forElement(Namespaces.HTML_DEFAULT, "script"));
- assertNoErrors();
- }
-
- public final void testBadAttribsRemoved() throws Exception {
- runTest("div[zwop] { color: blue }", "");
- }
-
- public final void testInvalidPropertiesRemoved() throws Exception {
- // visibility takes "hidden", not "none"
- runTest("a { visibility: none }", "");
- runTest("a { visibility: hidden; }", "a {\n visibility: hidden\n}");
- // no such property
- runTest("a { bogus: bogus }", "");
- // make sure it doesn't interfere with others
- runTest("a { visibility: none; font-weight: bold }",
- "a {\n font-weight: bold\n}");
- runTest("a { font-weight: bold; visibility: none }",
- "a {\n font-weight: bold\n}");
- runTest("a { bogus: bogus; font-weight: bold }",
- "a {\n font-weight: bold\n}");
- runTest("a { font-weight: bold; bogus: bogus }",
- "a {\n font-weight: bold\n}");
- }
-
- public final void testContentRemoved() throws Exception {
- runTest("a { color: blue; content: 'booyah'; text-decoration:
underline; }",
- "a {\n color: blue;\n text-decoration: underline\n}");
- }
-
- public final void testAttrRemoved() throws Exception {
- runTest("a:attr(href) { color: blue }", "");
- runTest("a:attr(href) { color: blue } b { font-weight: bolder }",
- "b {\n font-weight: bolder\n}");
- }
-
- public final void testFontNamesQuoted() throws Exception {
- runTest("a { font:12pt Times New Roman, Times,\"Times Old
Roman\",serif }",
- "a {\n font: 12pt 'Times New Roman', 'Times',"
- + " 'Times Old Roman', serif\n}");
- runTest("a { font:bold 12pt Arial Black }",
- "a {\n font: bold 12pt 'Arial Black'\n}");
- }
-
- public final void testNamespacing() throws Exception {
- runTest("a.foo { color:blue }", "a.foo {\n color: blue\n}");
- runTest("#foo { color: blue }", "#foo {\n color: blue\n}");
- runTest("body.ie6 p { color: blue }",
- "body.ie6 p {\n color: blue\n}");
- runTest("body { margin: 0; }", ""); // Not allowed
- runTest("body.ie6 { margin: 0; }", ""); // Not allowed
- runTest("* html p { margin: 0; }", "* html p {\n margin: 0\n}");
- runTest("* html { margin: 0; }", ""); // Not allowed
- runTest("* html > * > p { margin: 0; }", ""); // Not allowed
- runTest("#foo > #bar { color: blue }",
- "#foo > #bar {\n color: blue\n}");
- runTest("#foo .bar { color: blue }",
- "#foo .bar {\n color: blue\n}");
- }
-
- public final void testUnsafeIdentifiers() throws Exception {
- runTest("a.foo, b#c\\2c d, .e { color:blue }", // "\\2c " -> ","
- "a.foo, .e {\n color: blue\n}");
- runTest("a.foo, .b_c {color: blue}",
- "a.foo, .b_c {\n color: blue\n}");
- runTest("a.foo, ._c {color: blue}",
- "a.foo {\n color: blue\n}");
- runTest("a._c {_color: blue; margin:0;}", "");
- runTest("a#_c {_color: blue; margin:0;}", "");
- runTest(".c__ {_color: blue; margin:0;}", "");
- runTest("#c__ {_color: blue; margin:0;}", "");
- }
-
- public final void testPseudosWhitelisted() throws Exception {
- runTest("a:link, a:badness { color:blue }",
- "a:link {\n color: blue\n}");
- mq.getMessages().clear();
- runTest("a:visited { color:blue }",
- "a:visited {\n color: blue\n}");
- assertNoErrors();
-
- // Properties that are on DOMita's HISTORY_INSENSITIVE_STYLE_WHITELIST
- // should not be allowed in any rule that correlates with the :visited
- // pseudo selector.
- // TODO: How is this a whitelist then?
- mq.getMessages().clear();
- runTest(
- "a:visited { color:blue; float:left; _float:left; *float:left }",
- "a:visited {\n color: blue\n}");
- assertMessage(
- PluginMessageType.DISALLOWED_CSS_PROPERTY_IN_SELECTOR,
- MessageLevel.ERROR,
- FilePosition.instance(is, 1, 25, 25, 5), Name.css("float"),
- FilePosition.instance(is, 1, 1, 1, 9));
- assertMessage(
- PluginMessageType.DISALLOWED_CSS_PROPERTY_IN_SELECTOR,
- MessageLevel.ERROR,
- FilePosition.instance(is, 1, 37, 37, 6), Name.css("_float"),
- FilePosition.instance(is, 1, 1, 1, 9));
- assertMessage(
- PluginMessageType.DISALLOWED_CSS_PROPERTY_IN_SELECTOR,
- MessageLevel.ERROR,
- FilePosition.instance(is, 1, 51, 51, 5), Name.css("float"),
- FilePosition.instance(is, 1, 1, 1, 9));
-
- runTest(
- "a:visited { COLOR:blue; FLOAT:left; _FLOAT:left; *FLOAT:left }",
- "a:visited {\n color: blue\n}");
-
- runTest(
- "*:visited { color: blue; }",
- "a:visited {\n color: blue\n}");
- runTest(
- "#foo:visited { color: blue; }",
- "a#foo:visited {\n color: blue\n}");
- runTest(
- ".foo:link { color: blue; }",
- "a.foo:link {\n color: blue\n}");
-
- runTest(
- ""
- + "#foo:visited, div, .bar:link, p {\n"
- + " padding: 1px;\n"
- + " color: blue;\n"
- + "}",
- ""
- + "a#foo:visited, a.bar:link {\n"
- + " color: blue\n"
- + "}\n"
- + "div, p {\n"
- + " padding: 1px;\n"
- + " color: blue\n"
- + "}");
-
- runTest(
- ""
- + "a#foo-bank {"
- + " background: 'http://whitelisted-host.com/?bank=X&u=Al';"
- + " color: purple"
- + "}",
- ""
- + "a#foo-bank {\n"
- + " background:
url('http://whitelisted-host.com/?bank=X&u=Al');\n"
- + " color: purple\n"
- + "}");
- // Differs from the previous only in that it has the :visited pseudo
- // selector which means we can't allow it to cause a network fetch
because
- // that could leak user history state.
- mq.getMessages().clear();
- runTest(
- ""
- + "a#foo-bank:visited {"
- + " background-image: 'http://whitelisted-host.com/?bank=X&u=Al';"
- + " color: purple"
- + "}",
- ""
- + "a#foo-bank:visited {\n"
- + " color: purple\n"
- + "}");
- }
-
- public final void testNoBadUrls() throws Exception {
- // ok
- runTest("#foo { background: url(/bar.png) }",
- "#foo {\n background: url('/foo/bar.png')\n}");
- runTest("#foo { background: url('/bar.png') }",
- "#foo {\n background: url('/foo/bar.png')\n}");
- runTest("#foo { background: '/bar.png' }",
- "#foo {\n background: url('/foo/bar.png')\n}");
- runTest(
- "#foo { background: 'http://whitelisted-host.com/blinky.gif' }",
- "#foo {\n background:
url('http://whitelisted-host.com/blinky.gif')\n}"
- );
-
- // disallowed
- runTest("#foo { background: url('http://cnn.com/bar.png') }",
- "");
- runTest("#foo { background: 'http://cnn.com/bar.png' }",
- "");
+
+ public final void testCssRewriterEquivalence() throws Exception {
+ Expression tests = jsExpr(fromResource("css-stylesheet-tests.js"));
+ // tests is a JSONP style JavaScript expression.
+ // Normalize "foo" + "bar" -> "foo bar"
+ tests.acceptPostOrder(new Visitor() {
+ @Override
+ public boolean visit(AncestorChain<?> chain) {
+ if (Operation.is(chain.node, Operator.ADDITION)) {
+ Operation op = chain.cast(Operation.class).node;
+ Expression left = op.children().get(0);
+ Expression right = op.children().get(1);
+ if (left instanceof StringLiteral && right instanceof
StringLiteral) {
+ StringLiteral concatenation = StringLiteral.valueOf(
+ FilePosition.span(
+ left.getFilePosition(), right.getFilePosition()),
+ ((StringLiteral) left).getUnquotedValue()
+ + ((StringLiteral) right).getUnquotedValue());
+
chain.parent.cast(MutableParseTreeNode.class).node.replaceChild(
+ concatenation, op);
+ }
+ }
+ return true;
+ }
+ }, null);
+
+ AssertionFailedError failure = null;
+
+ // InputSource for file positions in error message goldens.
+ is = new InputSource(new URI("test://example.org/test"));
+
+ // Extract the JSON style-object from the call.
+ assertTrue(render(tests), Operation.is(tests, Operator.FUNCTION_CALL));
+ Operation call = (Operation) tests;
+ assertEquals(2, call.children().size());
+ Expression testArray = call.children().get(1);
+ // testArray is an array like
+ // [{ test_name: ..., tests: [] }]
+ for (Expression test : ((ArrayConstructor) testArray).children()) {
+ ObjectConstructor obj = (ObjectConstructor) test;
+ String name = (String)
+ ((ValueProperty) obj.propertyWithName("test_name"))
+ .getValueExpr().getValue();
+ ValueProperty testcases = (ValueProperty)
obj.propertyWithName("tests");
+ // testcases is an object like
+ // [{ cssText: ..., golden: ..., messages: ... }]
+ for (Expression testCase
+ : ((ArrayConstructor) testcases.getValueExpr()).children()) {
+ ObjectConstructor testCaseObj = (ObjectConstructor) testCase;
+ String cssText = null;
+ String golden = null;
+ ArrayConstructor messages = null;
+ for (ObjProperty oprop : testCaseObj.children()) {
+ ValueProperty prop = (ValueProperty) oprop;
+ String pname = prop.getPropertyName();
+ try {
+ if ("cssText".equals(pname)) {
+ cssText = ((StringLiteral) prop.getValueExpr())
+ .getUnquotedValue();
+ } else if ("golden".equals(pname)) {
+ golden = ((StringLiteral) prop.getValueExpr())
+ .getUnquotedValue();
+ } else if ("messages".equals(pname)) {
+ messages = (ArrayConstructor) prop.getValueExpr();
+ } else {
+ fail(
+ "Unrecognized testcase property " + pname + " in "
+ + render(testCase) + " at " +
testCase.getFilePosition());
+ }
+ } catch (RuntimeException ex) {
+ System.err.println(
+ "Type mismatch in " + name
+ + " at " + testCase.getFilePosition());
+ throw ex;
+ }
+ }
+
+ mq.getMessages().clear();
+ try {
+ runTest(cssText, golden);
+ if (messages != null) {
+ for (Expression message : messages.children()) {
+ ObjectConstructor messageObj = (ObjectConstructor) message;
+ String type = ((StringLiteral)
+ ((ValueProperty) messageObj.propertyWithName("type"))
+ .getValueExpr())
+ .getUnquotedValue();
+ String level = ((StringLiteral)
+ ((ValueProperty) messageObj.propertyWithName("level"))
+ .getValueExpr())
+ .getUnquotedValue();
+ List<String> args = Lists.newArrayList();
+ ArrayConstructor argsArray = (ArrayConstructor)
+ ((ValueProperty) messageObj.propertyWithName("args"))
+ .getValueExpr();
+ for (Expression argExpr : argsArray.children()) {
+ args.add(((StringLiteral) argExpr).getUnquotedValue());
+ }
+ consumeMessage(message.getFilePosition(), type, level, args);
+ }
+ assertNoErrors();
+ }
+ } catch (Exception ex) {
+ System.err.println("Test " + name + "\n" + render(testCase));
+ throw ex;
+ } catch (AssertionFailedError ex) {
+ System.err.println("Test " + name + "\n" + render(testCase));
+ ex.printStackTrace();
+ if (failure == null) {
+ failure = ex;
+ }
+ }
+ }
+ }
+ if (failure != null) { throw failure; }
}
public final void testSubstitutions() throws Exception {
@@ -261,61 +186,8 @@
"#foo {\n left: ${x * 4}px;\n top: ${y * 4}px\n}",
true);
}
-
- /**
- * "*" selectors should rewrite properly.
- * <a
href="http://code.google.com/p/google-caja/issues/detail?id=57">bug</a>
- */
- public final void testWildcardSelectors() throws Exception {
- runTest("div * { margin: 0; }", "div * {\n margin: 0\n}", false);
- }
-
- public final void testUnitlessLengths() throws Exception {
- runTest("div { padding: 10 0 5.0 4 }",
- "div {\n padding: 10px 0 5.0px 4px\n}", false);
- runTest("div { margin: -5 5; z-index: 2 }",
- "div {\n margin: -5px 5px;\n z-index: 2\n}", false);
- }
-
- public final void testUserAgentHacks() throws Exception {
- runTest(
- ""
- + "p {\n"
- + " color: blue;\n"
- + " *color: red;\n"
- + " background-color: green;\n"
- + " *background-color: yelow;\n" // misspelled
- + " font-weight: bold\n"
- + "}",
- ""
- + "p {\n"
- + " color: blue;\n"
- + " *color: red;\n" // Good user agent hack
- + " background-color: green;\n"
- // Bad user-agent hack removed.
- + " font-weight: bold\n"
- + "}"
- );
- assertMessage(PluginMessageType.MALFORMED_CSS_PROPERTY_VALUE,
- MessageLevel.WARNING,
- Name.css("background-color"),
- MessagePart.Factory.valueOf("==>yelow<=="));
- runTest("a.c {_color: blue; margin:0;}",
- "a.c {\n _color: blue;\n margin: 0\n}");
- assertNoErrors();
- }
public final void testNonStandardColors() throws Exception {
- runTest("a.c { color: LightSlateGray; background: ivory; }",
- "a.c {\n color: #789;\n background: #fffff0\n}");
- assertMessage(PluginMessageType.NON_STANDARD_COLOR,
- MessageLevel.LINT, Name.css("lightslategray"),
- MessagePart.Factory.valueOf("#789"));
- assertMessage(PluginMessageType.NON_STANDARD_COLOR,
- MessageLevel.LINT, Name.css("ivory"),
- MessagePart.Factory.valueOf("#fffff0"));
- assertNoErrors();
-
FilePosition u = FilePosition.UNKNOWN;
assertNull(CssRewriter.colorHash(u, Name.css("invisible")));
// Can get color hashes even for standard colors.
@@ -335,19 +207,6 @@
assertEquals(
"#112220", CssRewriter.colorHash(u, 0x112233 ^
0x000013).getValue());
}
-
- public final void testFixedPositioning() throws Exception {
- runTest("#foo { position: absolute; left: 0px; top: 0px }",
- "#foo {\n position: absolute;\n left: 0px;\n top: 0px\n}");
- assertNoErrors();
- runTest("#foo { position: fixed; left: 0px; top: 0px }",
- "#foo {\n left: 0px;\n top: 0px\n}");
- // TODO(mikesamuel): fix message. "fixed" is well-formed but
disallowed.
- assertMessage(true, PluginMessageType.MALFORMED_CSS_PROPERTY_VALUE,
- MessageLevel.WARNING, Name.css("position"),
- MessagePart.Factory.valueOf("==>fixed<=="));
- assertNoErrors();
- }
public final void testUrisCalledWithProperPropertyPart() throws
Exception {
// The CssRewriter needs to rewrite URIs.
@@ -489,86 +348,6 @@
}
assertEquals(msg, golden, render(t));
-
- // Check that the server side rewriter is consistent with the client
side
- // rewriter.
- // TODO: rewrite rules as well.
- if (!allowSubstitutions) {
- ArrayConstructor[] propArrays = new ArrayConstructor[2];
- int i = 0;
- for (CssTree tree : new CssTree[] { css(fromString(css)), t }) {
- final List<StringLiteral> namesAndValues = Lists.newArrayList();
- tree.acceptPreOrder(new Visitor() {
- @Override
- public boolean visit(AncestorChain<?> ac) {
- String namePrefix;
- CssTree.PropertyDeclaration d;
- if (ac.node instanceof CssTree.UserAgentHack) {
- namePrefix = "*";
- d =
ac.cast(CssTree.UserAgentHack.class).node.getDeclaration();
- } else if (ac.node instanceof CssTree.PropertyDeclaration) {
- namePrefix = "";
- d = ac.cast(CssTree.PropertyDeclaration.class).node;
- } else {
- return true;
- }
- namesAndValues.add(StringLiteral.valueOf(
- FilePosition.UNKNOWN,
- namePrefix
- + d.getProperty().getPropertyName().getCanonicalForm()));
- namesAndValues.add(StringLiteral.valueOf(
- FilePosition.UNKNOWN, render(d.getExpr())));
- return false;
- }
- }, null);
- propArrays[i++] = new ArrayConstructor(
- FilePosition.UNKNOWN, namesAndValues);
- }
- ArrayConstructor input = propArrays[0];
- ArrayConstructor want = propArrays[1];
- String testJs = render(QuasiBuilder.substV(
- Join.join("\n",
- "(function () {",
- " var input = @input;",
- " var want = @want;",
- " var urlRewriter = function (url) { return null; };",
- " var actual = {}, golden = {};",
- " for (var i = 0; i < input.length; i += 2) {",
- " var tokens = lexCss(input[i+1]), name = input[i];",
- " sanitizeCssProperty(",
- // Handle user agent hacks and undefined properties.
- " cssSchema[name.replace(/^[_*]/, '')] || {},",
- " tokens, urlRewriter);",
- " golden[name] = '';",
- " actual[name] = tokens.join(' ');",
- " }",
- " for (var i = 0; i < want.length; i += 2) {",
- " golden[want[i]] = want[i + 1];",
- " }",
- " golden = JSON.stringify(golden);",
- " actual = JSON.stringify(actual);",
- " if (golden !== actual) {",
- " throw new Error(golden + '\\n\\t!=\\n' + actual);",
- " }",
- "})();"),
- "input", input,
- "want", want));
- try {
- RhinoTestBed.runJs(
- new Executor.Input(getClass(), "css-defs.js"),
- new Executor.Input(getClass(), "csslexer.js"),
- new Executor.Input(getClass(), "sanitizecss.js"),
- new Executor.Input(testJs, getName()));
- } catch (RuntimeException ex) {
- if (ex.getMessage().contains("}\n\t!=\n{")) {
- // JavaScript inconsistencies are just advisory for now.
- // TODO: start enforcing these.
- System.err.println("WARNING:" + getName() + ":" +
ex.getMessage());
- } else {
- throw ex;
- }
- }
- }
}
private void assertCallsUriRewriterWithPropertyPart(
@@ -600,4 +379,39 @@
Arrays.asList(expectedParts),
Lists.newArrayList(propertyParts));
}
-}
+
+ private void consumeMessage(
+ FilePosition pos, final String type, final String level,
+ final List<String> parts) {
+ try {
+ assertMessage(
+ true,
+ new Function<Message, Integer>() {
+ @Override
+ public Integer apply(Message msg) {
+ int score = 0;
+ if (msg.getMessageType().name().equals(type)) { ++score; }
+ if (msg.getMessageLevel().name().equals(level)) { ++score; }
+ score -= partsMissing(msg, parts);
+ return (score == 2) ? Integer.MAX_VALUE : score;
+ }
+ }, "type=" + type + ", level=" + level);
+ } catch (AssertionFailedError err) {
+ System.err.println("Message specified at " + pos + " was not found");
+ throw err;
+ }
+ }
+
+ private static int partsMissing(Message msg, List<? extends String>
parts) {
+ int missing = 0;
+ outerLoop:
+ for (String expectedPart : parts) {
+ for (MessagePart candidate : msg.getMessageParts()) {
+ String candidatePart = candidate.toString();
+ if (candidatePart.equals(expectedPart)) { continue outerLoop; }
+ }
+ ++missing;
+ }
+ return missing;
+ }
+}
=======================================
--- /trunk/tests/com/google/caja/util/CajaTestCase.java Fri Mar 2 15:59:21
2012
+++ /trunk/tests/com/google/caja/util/CajaTestCase.java Thu Mar 15 12:38:53
2012
@@ -390,16 +390,29 @@
}
protected void assertMessage(
- boolean consume, MessageTypeInt type, MessageLevel level,
- MessagePart... expectedParts) {
+ boolean consume, final MessageTypeInt type, final MessageLevel level,
+ final MessagePart... expectedParts) {
+ assertMessage(
+ consume,
+ new Function<Message, Integer>() {
+ public Integer apply(Message msg) {
+ int score = 0;
+ if (msg.getMessageType() == type) { ++score; }
+ if (msg.getMessageLevel() == level) { ++score; }
+ score -= partsMissing(msg, expectedParts);
+ return score == 2 ? Integer.MAX_VALUE : score;
+ }
+ },
+ "type " + type + " and level " + level);
+ }
+
+ protected void assertMessage(
+ boolean consume, Function<Message, Integer> scorer, String
description) {
Message closest = null;
int closestScore = Integer.MIN_VALUE;
for (Message msg : mq.getMessages()) {
- int score = 0;
- if (msg.getMessageType() == type) { ++score; }
- if (msg.getMessageLevel() == level) { ++score; }
- score -= partsMissing(msg, expectedParts);
- if (score == 2) {
+ final int score = scorer.apply(msg);
+ if (score == Integer.MAX_VALUE) {
if (consume) {
mq.getMessages().remove(msg);
}
@@ -411,7 +424,7 @@
}
}
if (closest == null) {
- fail("No message found of type " + type + " and level " + level);
+ fail("No message found like " + description);
} else {
fail("Failed to find message. Closest match was " +
closest.format(mc)
+ " with parts " + closest.getMessageParts());