Revision: 4783
Author: metaweta
Date: Wed Feb 22 10:19:25 2012
Log: Allows arbitrary attributes in static HTML.
http://codereview.appspot.com/5672083
This change gets rid of the _d_attributes map and uses the
data-caja-* attribute namespace instead. TemplateCompiler now renames
attributes rather than stripping them out, and the *Attribute()
methods in Domado prefix attributes if they're not otherwise
whitelisted.
R=jasvir
http://code.google.com/p/google-caja/source/detail?r=4783
Modified:
/trunk/src/com/google/caja/lang/html/HtmlSchema.java
/trunk/src/com/google/caja/plugin/domado.js
/trunk/src/com/google/caja/plugin/templates/TemplateCompiler.java
/trunk/src/com/google/caja/plugin/templates/TemplateSanitizer.java
/trunk/tests/com/google/caja/plugin/templates/TemplateSanitizerTest.java
/trunk/tests/com/google/caja/service/HtmlHandlerTest.java
=======================================
--- /trunk/src/com/google/caja/lang/html/HtmlSchema.java Tue Aug 24
13:37:35 2010
+++ /trunk/src/com/google/caja/lang/html/HtmlSchema.java Wed Feb 22
10:19:25 2012
@@ -229,7 +229,9 @@
public boolean isAttributeAllowed(AttribKey k) {
HTML.Attribute a = lookupAttribute(k);
- return a != null && allowedAttributes.contains(a.getKey());
+ if (a != null && allowedAttributes.contains(a.getKey())) { return
true; }
+ String keyName = k.localName;
+ return keyName.startsWith("data-caja-") && !keyName.endsWith("___");
}
public HTML.Attribute lookupAttribute(AttribKey k) {
=======================================
--- /trunk/src/com/google/caja/plugin/domado.js Thu Feb 9 21:05:25 2012
+++ /trunk/src/com/google/caja/plugin/domado.js Wed Feb 22 10:19:25 2012
@@ -33,7 +33,7 @@
* TODO(ihab.awad): Our implementation of getAttribute (and friends)
* is such that standard DOM attributes which we disallow for security
* reasons (like 'form:enctype') are placed in the "virtual" attributes
- * map (node._d_attributes). They appear to be settable and gettable,
+ * map (the data-caja-* namespace). They appear to be settable and
gettable,
* but their values are ignored and do not have the expected semantics
* per the DOM API. This is because we do not have a column in
* html4-defs.js stating that an attribute is valid but explicitly
@@ -59,6 +59,9 @@
var domitaModules;
var Domado = (function() {
'use strict';
+ var cajaPrefix = 'data-caja-';
+ var cajaPrefRe = new RegExp('^' + cajaPrefix);
+
if (!domitaModules) { domitaModules = {}; }
domitaModules.proxiesAvailable = typeof Proxy !== 'undefined';
@@ -1467,6 +1470,8 @@
html4.ATTRIBS.hasOwnProperty(attribKey))) {
atype = html4.ATTRIBS[attribKey];
value = rewriteAttribute(tagName, attribName, atype, value);
+ } else if (!/__$/.test(attribKey)) {
+ attribName = attribs[+i] = cajaPrefix + attribs[+i];
} else {
value = null;
}
@@ -1547,7 +1552,7 @@
startTag: function (tagName, attribs, out) {
out.push('<', tagName);
for (var i = 0; i < attribs.length; i += 2) {
- var aname = attribs[+i];
+ var aname = '' + attribs[+i];
var atype = getAttributeType(tagName, aname);
var value = attribs[i + 1];
if (aname !== 'target' && atype !== void 0) {
@@ -1555,6 +1560,9 @@
if (typeof value === 'string') {
out.push(' ', aname, '="',
html.escapeAttrib(value), '"');
}
+ } else if (cajaPrefRe.test(aname)) {
+ out.push(' ', aname.substring(10), '="',
+ html.escapeAttrib(value), '"');
}
}
out.push('>');
@@ -3092,14 +3100,13 @@
TameElement.prototype.getAttribute = nodeMethod(function
(attribName) {
var feral = np(this).feral;
attribName = String(attribName).toLowerCase();
+ if (/__$/.test(attribName)) {
+ throw new TypeError('Attributes may not end with __');
+ }
var tagName = feral.tagName.toLowerCase();
var atype = getAttributeType(tagName, attribName);
if (atype === void 0) {
- // Unrecognized attribute; use virtual map
- if (feral._d_attributes) {
- return feral._d_attributes[attribName] || null;
- }
- return null;
+ return feral.getAttribute(cajaPrefix + attribName);
}
var value = bridal.getAttribute(feral, attribName);
if ('string' !== typeof value) { return value; }
@@ -3118,11 +3125,7 @@
var tagName = feral.tagName.toLowerCase();
var atype = getAttributeType(tagName, attribName);
if (atype === void 0) {
- // Unrecognized attribute; use virtual map
- return !!(
- feral._d_attributes &&
- Object.prototype.hasOwnProperty.call(
- feral._d_attributes, attribName));
+ return bridal.hasAttribute(feral, cajaPrefix + attribName);
} else {
return bridal.hasAttribute(feral, attribName);
}
@@ -3133,12 +3136,13 @@
var feral = np(this).feral;
if (!np(this).editable) { throw new Error(NOT_EDITABLE); }
attribName = String(attribName).toLowerCase();
+ if (/__$/.test(attribName)) {
+ throw new TypeError('Attributes may not end with __');
+ }
var tagName = feral.tagName.toLowerCase();
var atype = getAttributeType(tagName, attribName);
if (atype === void 0) {
- // Unrecognized attribute; use virtual map
- if (!feral._d_attributes) { feral._d_attributes = {}; }
- feral._d_attributes[attribName] = String(value);
+ bridal.setAttribute(feral, cajaPrefix + attribName, value);
} else {
var sanitizedValue = rewriteAttribute(
tagName, attribName, atype, value);
@@ -3152,13 +3156,13 @@
var feral = np(this).feral;
if (!np(this).editable) { throw new Error(NOT_EDITABLE); }
attribName = String(attribName).toLowerCase();
+ if (/__$/.test(attribName)) {
+ throw new TypeError('Attributes may not end with __');
+ }
var tagName = feral.tagName.toLowerCase();
var atype = getAttributeType(tagName, attribName);
if (atype === void 0) {
- // Unrecognized attribute; use virtual map
- if (feral._d_attributes) {
- delete feral._d_attributes[attribName];
- }
+ feral.removeAttribute(cajaPrefix + attribName);
} else {
feral.removeAttribute(attribName);
}
=======================================
--- /trunk/src/com/google/caja/plugin/templates/TemplateCompiler.java Tue
Jan 24 11:04:08 2012
+++ /trunk/src/com/google/caja/plugin/templates/TemplateCompiler.java Wed
Feb 22 10:19:25 2012
@@ -21,6 +21,7 @@
import com.google.caja.parser.html.AttribKey;
import com.google.caja.parser.html.ElKey;
import com.google.caja.parser.html.Nodes;
+import com.google.caja.parser.js.StringLiteral;
import com.google.caja.parser.js.UncajoledModule;
import com.google.caja.plugin.JobEnvelope;
import com.google.caja.plugin.Placeholder;
@@ -41,6 +42,7 @@
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
@@ -236,6 +238,17 @@
}
}
}
+ // Iterate over all attributes on el and allow any starting with
data-caja-
+ NamedNodeMap attrMap = el.getAttributes();
+ int attrLen = attrMap.getLength();
+ for (int i = 0; i < attrLen; ++i) {
+ Attr attr = (Attr) attrMap.item(i);
+ if (attr.getLocalName().startsWith("data-caja-")) {
+ scriptsPerNode.put(attr, new StringLiteral(
+ Nodes.getFilePositionForValue(attr),
+ attr.getValue()));
+ }
+ }
scriptsPerNode.put(el, null);
}
=======================================
--- /trunk/src/com/google/caja/plugin/templates/TemplateSanitizer.java Tue
Jan 24 11:04:08 2012
+++ /trunk/src/com/google/caja/plugin/templates/TemplateSanitizer.java Wed
Feb 22 10:19:25 2012
@@ -45,6 +45,7 @@
public final class TemplateSanitizer {
private final HtmlSchema schema;
private final MessageQueue mq;
+ private static String cajaPrefix = "data-caja-";
/**
* @param schema specifies which tags and attributes are allowed, and
which
@@ -143,12 +144,18 @@
HTML.Attribute a = schema.lookupAttribute(attrKey);
if (null == a) {
if (!Placeholder.ID_ATTR.is(attrib)) {
- if (!ignore) {
- mq.getMessages().add(new Message(
- PluginMessageType.UNKNOWN_ATTRIBUTE, MessageLevel.WARNING,
- Nodes.getFilePositionFor(attrib), attrKey, elKey));
- }
- valid &= removeBadAttribute(el, attrKey);
+ if (attrKey.localName.endsWith("__")) { valid = false; }
+ else if (!attrKey.localName.startsWith(cajaPrefix)) {
+ // Remove this attribute and create another that
+ // starts with "data-caja-".
+ String localName = attrKey.localName;
+ String value = attrib.getValue();
+ removeBadAttribute(el, attrKey);
+ el.setAttributeNS(
+ attrKey.ns.uri,
+ cajaPrefix + localName,
+ value == null ? localName : value);
+ }
}
} else if ("target".equals(attrKey.localName)) {
if (!"_self".equals(attrib.getNodeValue())) {
@@ -214,9 +221,13 @@
for (Node child : Nodes.childrenOf(el)) { valid &= sanitize(child); }
for (Attr a : Nodes.attributesOf(el)) {
+ String attrName = a.getNodeName();
+ if (attrName.startsWith(cajaPrefix)) {
+ attrName = attrName.substring(10);
+ }
mq.addMessage(
PluginMessageType.CANNOT_FOLD_ATTRIBUTE,
Nodes.getFilePositionFor(a),
- MessagePart.Factory.valueOf(a.getNodeName()),
+ MessagePart.Factory.valueOf(attrName),
MessagePart.Factory.valueOf(el.getLocalName()));
}
=======================================
---
/trunk/tests/com/google/caja/plugin/templates/TemplateSanitizerTest.java
Tue Sep 28 09:41:05 2010
+++
/trunk/tests/com/google/caja/plugin/templates/TemplateSanitizerTest.java
Wed Feb 22 10:19:25 2012
@@ -41,8 +41,7 @@
public final void testUnknownAttribute() throws Exception {
assertValid(
htmlFragment(fromString("<b unknown=\"bogus\">Hello</b>")),
- "<b>Hello</b>",
- "WARNING: removing unknown attribute unknown on b");
+ "<b data-caja-unknown=\"bogus\">Hello</b>");
}
public final void testKnownAttribute() throws Exception {
assertValid(htmlFragment(fromString("<b id=\"bold\">Hello</b>")),
@@ -59,8 +58,8 @@
assertValid(
htmlFragment(fromString("<bogus unknown=\"bogus\">Hello</bogus>")),
"Hello",
- "WARNING: removing unknown tag bogus"
- // No need to warn about unknown since we have warned about tag
+ "WARNING: removing unknown tag bogus",
+ "WARNING: removing attribute unknown when folding bogus into
parent"
);
}
public final void testDisallowedScriptElement() throws Exception {
@@ -251,12 +250,12 @@
+ "<x>Four</x>"
+ "</body>"
+ "</html>")),
- "<p>Foo</p><p>One</p><p>Two</p>ThreeFour",
+ "<p>Foo</p><p>One</p><p data-caja-styleo=\"color: red\">Two" +
+ "</p>ThreeFour",
"WARNING: folding element html into parent",
"WARNING: folding element head into parent",
"WARNING: removing disallowed tag title",
"WARNING: folding element body into parent",
- "WARNING: removing unknown attribute styleo on p",
"WARNING: removing unknown tag x");
}
public final void testElementFolding4() throws Exception {
=======================================
--- /trunk/tests/com/google/caja/service/HtmlHandlerTest.java Tue Jan 24
11:04:08 2012
+++ /trunk/tests/com/google/caja/service/HtmlHandlerTest.java Wed Feb 22
10:19:25 2012
@@ -151,7 +151,8 @@
private void assertHtml2Json(String inputMimeType)
throws Exception {
registerUri(
- "http://foo/bar.html", "<p>hi</p><script>42;</script><p>bye</p>",
+ "http://foo/bar.html",
+ "<p foo align=center>hi</p><script>42;</script><p>bye</p>",
"text/html");
Object result = json((String) requestGet(
@@ -162,8 +163,10 @@
JSONObject json = (JSONObject) result;
// Check html generation is correct
- assertEquals("<p>hi<span id=\"id_2___\"></span></p><p>bye</p>",
- (String) json.get("html"));
+ assertEquals(
+ "<p align=\"center\" data-caja-foo=\"foo\">" +
+ "hi<span id=\"id_2___\"></span></p><p>bye</p>",
+ (String) json.get("html"));
assertEquals("{"
+ "___.loadModule({"
+ "'instantiate':function(___,IMPORTS___){"