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, '>');
         }
       }
     });


Reply via email to