Reviewers: metaweta,
Description:
When the tagPolicy provided to html-sanitizer modifes the tagName,
rewrite matching end tags as well as start tags.
Add tests for the tagPolicy-using interface.
Please review this at http://codereview.appspot.com/6810096/
Affected files:
M src/com/google/caja/plugin/html-sanitizer.js
M tests/com/google/caja/plugin/html-sanitizer-test.js
Index: tests/com/google/caja/plugin/html-sanitizer-test.js
===================================================================
--- tests/com/google/caja/plugin/html-sanitizer-test.js (revision 5140)
+++ tests/com/google/caja/plugin/html-sanitizer-test.js (working copy)
@@ -307,6 +307,55 @@
jsunit.pass();
});
+jsunitRegister('testTagPolicy',
+ function testTagPolicy() {
+ // NOTE: makeHtmlSanitizer / sanitizeWithPolicy is not documented in the
wiki
+ // JsHtmlSanitizer doc. However, it is used by Caja and other clients.
Changes
+ // to this API should be noted in releases.
+ function checkT(expected, input, tagPolicy) {
+ assertEquals(expected, html.sanitizeWithPolicy(input, tagPolicy));
+ }
+ // pass tag
+ checkT('<a href="http://www.example.com/">hi</a> there',
+ '<a href="http://www.example.com/">hi</a> there',
+ function(name, attribs) {
+ return {attribs: attribs};
+ });
+ // reject tag
+ checkT(' there',
+ '<a href="http://www.example.com/">hi</a> there',
+ function(name, attribs) {
+ return null;
+ });
+ // modify attribs
+ checkT('<a x="y">hi</a> there',
+ '<a href="http://www.example.com/">hi</a> there',
+ function(name, attribs) {
+ return {attribs: ["x", "y"]};
+ });
+ // modify tagName
+ checkT('<xax href="http://www.example.com/">hi</xax> there',
+ '<a href="http://www.example.com/">hi</a> there',
+ function(name, attribs) {
+ return {attribs: attribs, tagName: 'x' + name + 'x'};
+ });
+ // proper end-tag matching w/ rewrite
+ checkT('<span>a<xspanx r="1">b</xspanx>c</span>',
+ '<span>a<span r=1>b</span>c</span>',
+ function(name, attribs) {
+ return {attribs: attribs,
+ tagName: attribs.length ? 'x' + name + 'x' : name};
+ });
+ // proper optional-end-tag handling w/ rewrite
+ checkT('<p>a<xpx r="1">b</xpx>c</p>',
+ '<p>a<p r=1>b</p>c',
+ function(name, attribs) {
+ return {attribs: attribs,
+ tagName: attribs.length ? 'x' + name + 'x' : name};
+ });
+ jsunit.pass();
+});
+
function assertSanitizerMessages(input, expected, messages) {
logMessages = [];
var actual = html.sanitize(input, uriPolicy, nmTokenPolicy, logPolicy);
Index: src/com/google/caja/plugin/html-sanitizer.js
===================================================================
--- src/com/google/caja/plugin/html-sanitizer.js (revision 5140)
+++ src/com/google/caja/plugin/html-sanitizer.js (working copy)
@@ -648,15 +648,15 @@
stack = [];
ignoring = false;
},
- 'startTag': function(tagName, attribs, out) {
+ 'startTag': function(tagNameOrig, attribs, out) {
if (ignoring) { return; }
- if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
- var eflagsOrig = html4.ELEMENTS[tagName];
+ if (!html4.ELEMENTS.hasOwnProperty(tagNameOrig)) { return; }
+ var eflagsOrig = html4.ELEMENTS[tagNameOrig];
if (eflagsOrig & html4.eflags['FOLDABLE']) {
return;
}
- var decision = tagPolicy(tagName, attribs);
+ var decision = tagPolicy(tagNameOrig, attribs);
if (!decision) {
ignoring = !(eflagsOrig & html4.eflags['EMPTY']);
return;
@@ -669,20 +669,22 @@
throw new Error('tagPolicy gave no attribs');
}
var eflagsRep;
+ var tagNameRep;
if ('tagName' in decision) {
- tagName = decision['tagName'];
- eflagsRep = html4.ELEMENTS[tagName];
+ tagNameRep = decision['tagName'];
+ eflagsRep = html4.ELEMENTS[tagNameRep];
} else {
+ tagNameRep = tagNameOrig;
eflagsRep = eflagsOrig;
}
// TODO(mikesamuel): relying on tagPolicy not to insert unsafe
// attribute names.
if (!(eflagsOrig & html4.eflags['EMPTY'])) {
- stack.push(tagName);
+ stack.push({orig: tagNameOrig, rep: tagNameRep});
}
- out.push('<', tagName);
+ out.push('<', tagNameRep);
for (var i = 0, n = attribs.length; i < n; i += 2) {
var attribName = attribs[i],
value = attribs[i + 1];
@@ -695,7 +697,7 @@
if ((eflagsOrig & html4.eflags['EMPTY'])
&& !(eflagsRep & html4.eflags['EMPTY'])) {
// replacement is non-empty, synthesize end tag
- out.push('<\/', tagName, '>');
+ out.push('<\/', tagNameRep, '>');
}
},
'endTag': function(tagName, out) {
@@ -709,9 +711,9 @@
var index;
if (eflags & html4.eflags['OPTIONAL_ENDTAG']) {
for (index = stack.length; --index >= 0;) {
- var stackEl = stack[index];
- if (stackEl === tagName) { break; }
- if (!(html4.ELEMENTS[stackEl] &
+ var stackElOrigTag = stack[index].orig;
+ if (stackElOrigTag === tagName) { break; }
+ if (!(html4.ELEMENTS[stackElOrigTag] &
html4.eflags['OPTIONAL_ENDTAG'])) {
// Don't pop non optional end tags looking for a match.
return;
@@ -719,17 +721,20 @@
}
} else {
for (index = stack.length; --index >= 0;) {
- if (stack[index] === tagName) { break; }
+ if (stack[index].orig === tagName) { break; }
}
}
if (index < 0) { return; } // Not opened.
for (var i = stack.length; --i > index;) {
- var stackEl = stack[i];
- if (!(html4.ELEMENTS[stackEl] &
+ var stackElRepTag = stack[i].rep;
+ if (!(html4.ELEMENTS[stackElRepTag] &
html4.eflags['OPTIONAL_ENDTAG'])) {
- out.push('<\/', stackEl, '>');
+ out.push('<\/', stackElRepTag, '>');
}
}
+ if (index < stack.length) {
+ tagName = stack[index].rep;
+ }
stack.length = index;
out.push('<\/', tagName, '>');
}
@@ -739,7 +744,7 @@
'cdata': emit,
'endDoc': function(out) {
for (; stack.length; stack.length--) {
- out.push('<\/', stack[stack.length - 1], '>');
+ out.push('<\/', stack[stack.length - 1].rep, '>');
}
}
});