Revision: 5143
Author:   [email protected]
Date:     Thu Nov  8 14:37:26 2012
Log: Replace Domado's editable flags with node policy objects determined by parents.
http://codereview.appspot.com/6490106

The opaque and foreign node restrictions as previously implemented are
completely broken, because they attempt to suppress ancestor methods
by overriding them, and the nodes are not even marked editable. To
achieve proper protection, all amplification-capable function objects
(that is, TameNode methods) must instead respect the intended policy.

To achieve this, several new flags are defined to capture the intended
behavior of opaque and foreign nodes. To maintain simplicity, all flags
(including the existing editability flags) are bundled into 'node
policy' objects, of which there are a small set.

Furthermore, the current taming membrane has as a premise that there is
only one tame object per feral object; but this is inconsistent with the
Domado design which determines editability flags according to the way
in which the node being tamed was reached. In order to avoid accidental
mutability of opaque nodes' children, the node policy of a node is now
almost always determined by the policy of its parent node -- with the
exceptions of opaque nodes and foreign nodes. Incidentally, an attempt
to tame a foreign node's descendant (which should never happen) is now
guaranteed to crash, as the foreign node policy refuses to specify a
child policy.

Foreign nodes' descendants are now hidden from NodeLists; this adds
the cost of scanning a prefix of the host NodeList to filter it, but the
results are cached as long as the guest does not modify the DOM.

Node editability and child-relationship editability checks have been
refactored to be implemented in one place each.

Also added a note about our slightly inaccurate implementation of
innerText.

[email protected]

http://code.google.com/p/google-caja/source/detail?r=5143

Modified:
 /trunk/src/com/google/caja/plugin/domado.js
 /trunk/src/com/google/caja/plugin/html-emitter.js
 /trunk/tests/com/google/caja/plugin/browser-test-case.js
 /trunk/tests/com/google/caja/plugin/es53-test-domado-canvas-guest.html
 /trunk/tests/com/google/caja/plugin/es53-test-domado-dom-guest.html
 /trunk/tests/com/google/caja/plugin/es53-test-domado-events-guest.html
 /trunk/tests/com/google/caja/plugin/es53-test-domado-foreign-guest.html
 /trunk/tests/com/google/caja/plugin/es53-test-domado-foreign.js

=======================================
--- /trunk/src/com/google/caja/plugin/domado.js Tue Nov  6 16:00:00 2012
+++ /trunk/src/com/google/caja/plugin/domado.js Thu Nov  8 14:37:26 2012
@@ -508,7 +508,7 @@
         return ProxyHandler.prototype.defineProperty.call(this, name,
             descriptor);
       } else {
-        if (!this.editable) { throw new Error("Not editable"); }
+        if (!this.editable) { throw new Error(NOT_EDITABLE); }
         Object.defineProperty(this.storage, name, descriptor);
         return true;
       }
@@ -521,7 +521,7 @@
         // Forwards everything already defined (not expando).
         return ProxyHandler.prototype['delete'].call(this, name);
       } else {
-        if (!this.editable) { throw new Error("Not editable"); }
+        if (!this.editable) { throw new Error(NOT_EDITABLE); }
         return delete this.storage[name];
       }
       return false;
@@ -909,6 +909,11 @@

   cajaVM.def(domitaModules);

+  var NOT_EDITABLE = "Node not editable.";
+  var UNSAFE_TAGNAME = "Unsafe tag name.";
+  var UNKNOWN_TAGNAME = "Unknown tag name.";
+  var INDEX_SIZE_ERROR = "Index size error.";
+
   /**
    * Authorize the Domado library.
    *
@@ -1369,6 +1374,122 @@
         element.style.width = Math.max(0, w - wError) + 'px';
       }
     }
+
+    /**
+     * Access policies
+     *
+ * Each of these objects is a policy for what type of access (read/write,
+     * read-only, or none) is permitted to a Node or NodeList. Each policy
+ * object determines the access for the associated node and its children. + * The childPolicy may be overridden if the node is an opaque or foreign
+     * node.
+     *
+     * Definitions:
+     *    childrenVisible:
+ * This node appears to have the children it actually does; otherwise,
+     *      appears to have no children.
+     *    attributesVisible:
+     *      This node appears to have the attributes it actually does;
+     *      otherwise, appears to have no attributes.
+     *    editable:
+ * This node's attributes and properties (other than children) may be
+     *      modified.
+     *    childrenEditable:
+ * This node's childNodes list may be modified, and its children are
+     *      both editable and childrenEditable.
+     *
+ * These flags can express several meaningless cases; in particular, the
+     * 'editable but not visible' cases do not occur.
+     */
+    var protoNodePolicy = {
+      requireEditable: function () {
+        if (!this.editable) {
+          throw new Error(NOT_EDITABLE);
+        }
+      },
+      requireChildrenEditable: function () {
+        if (!this.childrenEditable) {
+          throw new Error(NOT_EDITABLE);
+        }
+      },
+      requireUnrestricted: function () {
+        if (!this.unrestricted) {
+          throw new Error("Node is restricted");
+        }
+      },
+      assertRestrictedBy: function (policy) {
+        if (!this.childrenVisible   && policy.childrenVisible ||
+            !this.attributesVisible && policy.attributesVisible ||
+            !this.editable          && policy.editable ||
+            !this.childrenEditable  && policy.childrenEditable ||
+            !this.upwardNavigation  && policy.upwardNavigation ||
+            !this.unrestricted      && policy.unrestricted) {
+ throw new Error("Domado internal error: non-monotonic node policy");
+        }
+      }
+    };
+    // We eagerly await ES6 offering some kind of literal-with-prototype...
+    var nodePolicyEditable = Object.create(protoNodePolicy);
+ nodePolicyEditable.toString = function () { return "nodePolicyEditable"; };
+    nodePolicyEditable.childrenVisible = true;
+    nodePolicyEditable.attributesVisible = true;
+    nodePolicyEditable.editable = true;
+    nodePolicyEditable.childrenEditable = true;
+    nodePolicyEditable.upwardNavigation = true;
+    nodePolicyEditable.unrestricted = true;
+    nodePolicyEditable.childPolicy = nodePolicyEditable;
+
+    var nodePolicyReadOnly = Object.create(protoNodePolicy);
+ nodePolicyReadOnly.toString = function () { return "nodePolicyReadOnly"; };
+    nodePolicyReadOnly.childrenVisible = true;
+    nodePolicyReadOnly.attributesVisible = true;
+    nodePolicyReadOnly.editable = false;
+    nodePolicyReadOnly.childrenEditable = false;
+    nodePolicyReadOnly.upwardNavigation = true;
+    nodePolicyReadOnly.unrestricted = true;
+    nodePolicyReadOnly.childPolicy = nodePolicyReadOnly;
+
+    var nodePolicyReadOnlyChildren = Object.create(protoNodePolicy);
+    nodePolicyReadOnlyChildren.toString =
+        function () { return "nodePolicyReadOnlyChildren"; };
+    nodePolicyReadOnlyChildren.childrenVisible = true;
+    nodePolicyReadOnlyChildren.attributesVisible = true;
+    nodePolicyReadOnlyChildren.editable = true;
+    nodePolicyReadOnlyChildren.childrenEditable = false;
+    nodePolicyReadOnlyChildren.upwardNavigation = true;
+    nodePolicyReadOnlyChildren.unrestricted = true;
+    nodePolicyReadOnlyChildren.childPolicy = nodePolicyReadOnly;
+
+    var nodePolicyOpaque = Object.create(protoNodePolicy);
+    nodePolicyOpaque.toString = function () { return "nodePolicyOpaque"; };
+    nodePolicyOpaque.childrenVisible = true;
+    nodePolicyOpaque.attributesVisible = false;
+    nodePolicyOpaque.editable = false;
+    nodePolicyOpaque.childrenEditable = false;
+    nodePolicyOpaque.upwardNavigation = true;
+    nodePolicyOpaque.unrestricted = false;
+    nodePolicyOpaque.childPolicy = nodePolicyReadOnly;
+
+    var nodePolicyForeign = Object.create(protoNodePolicy);
+ nodePolicyForeign.toString = function () { return "nodePolicyForeign"; };
+    nodePolicyForeign.childrenVisible = false;
+    nodePolicyForeign.attributesVisible = false;
+    nodePolicyForeign.editable = false;
+    nodePolicyForeign.childrenEditable = false;
+    nodePolicyForeign.upwardNavigation = false;
+    nodePolicyForeign.unrestricted = false;
+    Object.defineProperty(nodePolicyForeign, "childPolicy", {
+      get: function () {
+ throw new Error("Foreign node childPolicy should never be consulted");
+      }
+    });
+    cajaVM.def([
+      nodePolicyEditable,
+      nodePolicyReadOnly,
+      nodePolicyReadOnlyChildren,
+      nodePolicyOpaque,
+      nodePolicyForeign
+    ]);

     /**
      * Add a tamed document implementation to a Gadget's global scope.
@@ -1422,6 +1543,8 @@
       };
       var pluginId;

+      var vdocContainsForeignNodes = false;
+
       containerNode = makeDOMAccessible(containerNode);
       var document = containerNode.ownerDocument;
       document = makeDOMAccessible(document);
@@ -1469,8 +1592,7 @@

       // The private properties used in TameNodeConf are:
       //    feral (feral node)
-      //    editable (this node editable)
-      //    childrenEditable (this node editable)
+      //    policy (access policy)
       //    Several specifically for TameHTMLDocument.
       // Furthermore, by virtual of being scoped inside attachDocument,
       // TameNodeT also indicates that the object is a node from the *same*
@@ -1520,7 +1642,7 @@
         }

         if (feral) {
-          if (node.nodeType === 1) {
+          if (feral.nodeType === 1) {
             // Elements must only be tamed once; to do otherwise would be
             // a bug in Domado.
             taming.tamesTo(feral, node);
@@ -1930,8 +2052,8 @@
       /**
* Construct property descriptors suitable for taming objects which use
        * the specified confidence, such that confidence.p(obj).feral is the
-       * feral object to forward to and confidence.p(obj).editable is an
-       * editable/readonly flag.
+       * feral object to forward to and confidence.p(obj).policy is a node
+       * policy object for writability decisions.
        *
        * Lowercase properties are property descriptors; uppercase ones are
        * constructors for parameterized property descriptors.
@@ -1964,8 +2086,9 @@
               return p(this).feral[prop];
             }),
             set: method(function (value, prop) {
-              if (!p(this).editable) { throw new Error(NOT_EDITABLE); }
-              p(this).feral[prop] = value;
+              var privates = p(this);
+              privates.policy.requireEditable();
+              privates.feral[prop] = value;
             })
           },

@@ -1984,7 +2107,7 @@
               }),
               set: method(function (value, prop) {
                 var privates = p(this);
-                if (!privates.editable) { throw new Error(NOT_EDITABLE); }
+                privates.policy.requireEditable();
                 if (predicate(value)) {
                   privates.feral[prop] = value;
                 }
@@ -2020,14 +2143,13 @@
             enumerable: true,
             extendedAccessors: true,
             get: method(function (prop) {
-              if (!('editable' in p(this))) {
-                throw new Error(
-                    "Internal error: related property tamer can only"
-                    + " be applied to objects with an editable flag");
+              var privates = p(this);
+              if (privates.policy.upwardNavigation) {
+ // TODO(kpreid): Can we move this check *into* tameRelatedNode? + return tameRelatedNode(privates.feral[prop], defaultTameNode);
+              } else {
+                return null;
               }
-              return tameRelatedNode(p(this).feral[prop],
-                                     p(this).editable,
-                                     defaultTameNode);
             })
           },

@@ -2073,8 +2195,9 @@
                       this.setAttribute(name, fromValue.call(this, value));
                     })
                   : method(function (value, name) {
- if (!p(this).editable) { throw new Error(NOT_EDITABLE); }
-                      p(this).feral[name] = fromValue.call(this, value);
+                      var privates = p(this);
+                      privates.policy.requireEditable();
+                      privates.feral[name] = fromValue.call(this, value);
                     });
             }
             return desc;
@@ -2101,8 +2224,12 @@
         enumerable: true,
         extendedAccessors: true,
         get: nodeMethod(function (prop) {
-          return defaultTameNode(np(this).feral[prop],
-                                 np(this).childrenEditable);
+          var privates = np(this);
+          if (privates.policy.childrenVisible) {
+            return defaultTameNode(np(this).feral[prop]);
+          } else {
+            return null;
+          }
         })
       };

@@ -2119,7 +2246,7 @@
         return s;
       }

-      function makeTameNodeByType(node, editable) {
+      function makeTameNodeByType(node) {
         switch (node.nodeType) {
           case 1:  // Element
             var tagName = node.tagName.toLowerCase();
@@ -2128,21 +2255,20 @@
// href property). This is deliberately before the unsafe test;
               // for example, <script> has its own class even though it is
               // unsafe.
-              return new (tamingClassesByElement[tagName + '$'])(
-                  node, editable);
-            }
+              return new (tamingClassesByElement[tagName + '$'])(node);
+            }
             var schemaElem = htmlSchema.element(tagName);
             if (schemaElem.isVirtualizedElementName) {
               // Virtualized unrecognized elements are generic
-              return new TameElement(node, editable, editable);
+              return new TameElement(node);
             } else if (schemaElem.allowed) {
-              return new TameElement(node, editable, editable);
+              return new TameElement(node);
             } else {
               // If an unrecognized or unsafe node, return a
               // placeholder that doesn't prevent tree navigation,
               // but that doesn't allow mutation or leak attribute
               // information.
-              return new TameOpaqueNode(node, editable);
+              return new TameOpaqueNode(node);
             }
           case 2:  // Attr
             // Cannot generically wrap since we must have access to the
@@ -2150,24 +2276,24 @@
             throw 'Internal: Attr nodes cannot be generically wrapped';
           case 3:  // Text
           case 4:  // CDATA Section Node
-            return new TameTextNode(node, editable);
+            return new TameTextNode(node);
           case 8:  // Comment
-            return new TameCommentNode(node, editable);
+            return new TameCommentNode(node);
           case 11: // Document Fragment
-            return new TameBackedNode(node, editable, editable);
+            return new TameBackedNode(node);
           default:
-            return new TameOpaqueNode(node, editable);
+            return new TameOpaqueNode(node);
         }
       }

       /**
        * returns a tame DOM node.
        * @param {Node} node
-       * @param {boolean} editable
+       * @param {boolean} foreign
        * @see <a href="http://www.w3.org/TR/DOM-Level-2-HTML/html.html";
        *       >DOM Level 2</a>
        */
-      function defaultTameNode(node, editable, foreign) {
+      function defaultTameNode(node, foreign) {
         if (node === null || node === void 0) { return null; }
         node = makeDOMAccessible(node);
         // TODO(mikesamuel): make sure it really is a DOM node
@@ -2175,22 +2301,22 @@
         if (taming.hasTameTwin(node)) {
           return taming.tame(node);
         }
+
+        if (foreign) {
+          vdocContainsForeignNodes = true;
+        }

         var tamed = foreign
-            ? new TameForeignNode(node, editable)
-            : makeTameNodeByType(node, editable);
+            ? new TameForeignNode(node)
+            : makeTameNodeByType(node);
         tamed = finishNode(tamed);

         return tamed;
       }

-      function tameRelatedNode(node, editable, tameNodeCtor) {
+      function tameRelatedNode(node, tameNodeCtor) {
         if (node === null || node === void 0) { return null; }
         if (node === np(tameDocument).feralContainerNode) {
-          if (np(tameDocument).editable && !editable) {
-            // FIXME: return a non-editable version instead
-            throw new Error(NOT_EDITABLE);
-          }
           return tameDocument;
         }

@@ -2203,18 +2329,18 @@
               ancestor;
               ancestor = makeDOMAccessible(ancestor.parentNode)) {
             if (idClassPattern.test(ancestor.className)) {
-              return tameNodeCtor(node, editable);
+              return tameNodeCtor(node);
             } else if (ancestor === docElem) {
               return null;
             }
           }
-          return tameNodeCtor(node, editable);
+          return tameNodeCtor(node);
         } catch (e) {}
         return null;
       }

       domicile.tameNodeAsForeign = function(node) {
-        return defaultTameNode(node, true, true);
+        return defaultTameNode(node, true);
       };

       /**
@@ -2232,6 +2358,120 @@
         return limit;
       }

+      function nodeListEqualsArray(nodeList, array) {
+        var nll = getNodeListLength(nodeList);
+        if (nll !== array.length) {
+          return false;
+        } else {
+          for (var i = 0; i < nll; i++) {
+            if (nodeList[i] !== array[i]) {
+              return false;
+            }
+          }
+          return true;
+        }
+      }
+
+      // Commentary on foreign node children in NodeLists:
+      //
+      // The children of a foreign node are an implementation detail which
+ // guest code should not be permitted to see. Therefore, we must hide them + // from appearing in NodeLists. This would be a straightforward matter of + // filtering, except that NodeLists are "live", reflecting DOM changes + // immediately; and DOM changes change the membership and numeric indexes
+      // of the NodeList.
+      //
+ // One could imagine caching the outcomes: given an index, scan the host
+      // list until the required number of visible-to-guest nodes have been
+ // found, cache the indexes and node, and then validate the cache entry + // later by comparing indexes, but that is not sufficient; consider if a + // foreign child is deleted, and at the same time a guest-visible node + // is added in a similar document position; then the index of a guest node + // which is after that position *should* increase, but this cache cannot
+      // tell.
+      //
+      // Therefore, we do cache the list, but we must re-validate the cache
+      // from 0 up to the desired index on every access.
+      /**
+       * This is NOT a node list taming. This is a component for performing
+       * foreign node filtering only.
+       */
+      function NodeListFilter(feralNodeList) {
+        feralNodeList = makeDOMAccessible(feralNodeList);
+        var expectation = [];
+        var filteredCache = [];
+
+        function calcUpTo(index) {
+          var feralLength = getNodeListLength(feralNodeList);
+          var feralIndex = 0;
+
+          // Validate cache
+          if (feralLength < expectation.length) {
+            expectation = [];
+            filteredCache = [];
+            feralIndex = 0;
+          } else {
+            for (
+                ;
+ feralIndex < expectation.length && feralIndex < feralLength;
+                feralIndex++) {
+              if (feralNodeList[feralIndex] !== expectation[feralIndex]) {
+                expectation = [];
+                filteredCache = [];
+                feralIndex = 0;
+                break;
+              }
+            }
+          }
+
+          // Extend cache
+          nodeListScan: for (
+              ;
+              feralIndex < feralLength && filteredCache.length <= index;
+              feralIndex++) {
+            var node = feralNodeList[feralIndex];
+            expectation.push(node);
+            makeDOMAccessible(node);
+            // Filter out foreign nodes' descendants
+            walkUp: for (
+                var ancestor = makeDOMAccessible(node.parentNode);
+                ancestor !== null;
+                ancestor = makeDOMAccessible(ancestor.parentNode)) {
+              if (taming.hasTameTwin(ancestor)) {
+                if (taming.tame(ancestor) instanceof TameForeignNode) {
+                  // Every foreign node is already tamed as foreign, by
+                  // definition.
+                  continue nodeListScan;
+                } else {
+                  // Reached a node known to be non-foreign.
+                  break walkUp;
+                }
+              }
+            }
+            // Not a foreign node descendant, so include it in the list.
+            filteredCache.push(node);
+          }
+        }
+        return cajaVM.def({
+          getLength: function() {
+            if (vdocContainsForeignNodes) {
+              calcUpTo(Infinity);
+              return filteredCache.length;
+            } else {
+              return getNodeListLength(feralNodeList);
+            }
+          },
+          item: function(i) {
+            if (vdocContainsForeignNodes) {
+              calcUpTo(i);
+              return filteredCache[i];
+            } else {
+              return feralNodeList[i];
+            }
+          }
+        });
+      }
+
       /**
        * Constructs a NodeList-like object.
        *
@@ -2240,27 +2480,31 @@
        *     precede the actual NodeList elements.
* @param nodeList an array-like object supporting a "length" property
        *     and "[]" numeric indexing, or a raw DOM NodeList;
-       * @param editable whether the tame nodes wrapped by this object
-       *     should permit editing.
        * @param opt_tameNodeCtor a function for constructing tame nodes
        *     out of raw DOM nodes.
        */
-      function mixinNodeList(tamed, nodeList, editable, opt_tameNodeCtor) {
+      function mixinNodeList(tamed, nodeList, opt_tameNodeCtor) {
         // TODO(kpreid): Under a true ES5 environment, node lists should be
         // proxies so that they preserve liveness of the original lists.
         // This should be controlled by an option.
+ // UPDATE: We have live NodeLists as TameNodeList and TameOptionsList.
+        // This is not live, but is used in less-mainstream cases.

-        var limit = getNodeListLength(nodeList);
+        var visibleList = new NodeListFilter(nodeList);
+
+        var limit = visibleList.getLength();
         if (limit > 0 && !opt_tameNodeCtor) {
throw 'Internal: Nonempty mixinNodeList() without a tameNodeCtor';
         }

- for (var i = tamed.length, j = 0; j < limit && nodeList[+j]; ++i, ++j) {
-          tamed[+i] = opt_tameNodeCtor(nodeList[+j], editable);
+        for (var i = tamed.length, j = 0;
+             j < limit && visibleList.item(j);
+             ++i, ++j) {
+          tamed[+i] = opt_tameNodeCtor(visibleList.item(+j));
         }

         // Guard against accidental leakage of untamed nodes
-        nodeList = null;
+        nodeList = visibleList = null;

         tamed.item = cajaVM.def(function (k) {
           k &= 0x7fffffff;
@@ -2287,16 +2531,14 @@
       }

       function makeTameNodeList() {
-        return function TNL(nodeList, editable, tameNodeCtor) {
-            nodeList = makeDOMAccessible(nodeList);
+        return function TNL(nodeList, tameNodeCtor) {
+            var visibleList = new NodeListFilter(nodeList);
             function getItem(i) {
               i = +i;
-              if (i >= nodeList.length) { return void 0; }
-              return tameNodeCtor(nodeList[i], editable);
+              if (i >= visibleList.getLength()) { return void 0; }
+              return tameNodeCtor(visibleList.item(i));
             }
-            function getLength() {
-              return nodeList.length;
-            }
+            var getLength = visibleList.getLength.bind(visibleList);
             var len = +getLength();
             var ArrayLike = cajaVM.makeArrayLike(len);
             if (!(TameNodeList.prototype instanceof ArrayLike)) {
@@ -2312,13 +2554,13 @@
       var TameNodeList = Object.freeze(makeTameNodeList());

       function makeTameOptionsList() {
-        return function TOL(nodeList, editable, opt_tameNodeCtor) {
-            nodeList = makeDOMAccessible(nodeList);
+        return function TOL(nodeList, opt_tameNodeCtor) {
+            var visibleList = new NodeListFilter(nodeList);
             function getItem(i) {
               i = +i;
-              return opt_tameNodeCtor(nodeList[i], editable);
+              return opt_tameNodeCtor(visibleList.item(i));
             }
-            function getLength() { return nodeList.length; }
+            var getLength = visibleList.getLength.bind(visibleList);
             var len = +getLength();
             var ArrayLike = cajaVM.makeArrayLike(len);
             if (!(TameOptionsList.prototype instanceof ArrayLike)) {
@@ -2353,8 +2595,6 @@
        *     with the DOM HTMLCollection API.
* @param nodeList an array-like object supporting a "length" property
        *     and "[]" numeric indexing.
-       * @param editable whether the tame nodes wrapped by this object
-       *     should permit editing.
        * @param opt_tameNodeCtor a function for constructing tame nodes
        *     out of raw DOM nodes.
        *
@@ -2363,9 +2603,8 @@
        * this should be looking up ids as well as names. (And not returning
        * nodelists, but is that for compatibility?)
        */
-      function mixinHTMLCollection(tamed, nodeList, editable,
-          opt_tameNodeCtor) {
-        mixinNodeList(tamed, nodeList, editable, opt_tameNodeCtor);
+      function mixinHTMLCollection(tamed, nodeList, opt_tameNodeCtor) {
+        mixinNodeList(tamed, nodeList, opt_tameNodeCtor);

         var tameNodesByName = {};
         var tameNode;
@@ -2404,12 +2643,12 @@
         return tamed;
       }

-      function tameHTMLCollection(nodeList, editable, opt_tameNodeCtor) {
+      function tameHTMLCollection(nodeList, opt_tameNodeCtor) {
         return Object.freeze(
-            mixinHTMLCollection([], nodeList, editable, opt_tameNodeCtor));
+            mixinHTMLCollection([], nodeList, opt_tameNodeCtor));
       }

-      function tameGetElementsByTagName(rootNode, tagName, editable) {
+      function tameGetElementsByTagName(rootNode, tagName) {
         tagName = String(tagName);
         var eflags = 0;
         if (tagName !== '*') {
@@ -2417,14 +2656,14 @@
           tagName = virtualToRealElementName(tagName);
         }
         return new TameNodeList(rootNode.getElementsByTagName(tagName),
-            editable, defaultTameNode);
+            defaultTameNode);
       }

       /**
* Implements http://www.whatwg.org/specs/web-apps/current-work/#dom-document-getelementsbyclassname
        * using an existing implementation on browsers that have one.
        */
-      function tameGetElementsByClassName(rootNode, className, editable) {
+      function tameGetElementsByClassName(rootNode, className) {
         className = String(className);

// The quotes below are taken from the HTML5 draft referenced above.
@@ -2457,7 +2696,7 @@
         if (typeof rootNode.getElementsByClassName === 'function') {
           return new TameNodeList(
               rootNode.getElementsByClassName(
-                  classes.join(' ')), editable, defaultTameNode);
+                  classes.join(' ')), defaultTameNode);
         } else {
// Add spaces around each class so that we can use indexOf later to
           // find a match.
@@ -2495,7 +2734,7 @@
                 continue candidate_loop;
               }
             }
-            var tamed = defaultTameNode(candidate, editable);
+            var tamed = defaultTameNode(candidate);
             if (tamed) {
               matches[++k] = tamed;
             }
@@ -2513,14 +2752,11 @@
         }
         return wrapper;
       }
-
-      var NOT_EDITABLE = "Node not editable.";
-      var INDEX_SIZE_ERROR = "Index size error.";

       // Implementation of EventTarget::addEventListener
       function tameAddEventListener(name, listener, useCapture) {
         var feral = np(this).feral;
-        if (!np(this).editable) { throw new Error(NOT_EDITABLE); }
+        np(this).policy.requireEditable();
         if (!np(this).wrappedListeners) { np(this).wrappedListeners = []; }
         useCapture = Boolean(useCapture);
var wrappedListener = makeEventHandlerWrapper(np(this).feral, listener);
@@ -2534,7 +2770,7 @@
       function tameRemoveEventListener(name, listener, useCapture) {
         var self = TameNodeT.coerce(this);
         var feral = np(self).feral;
-        if (!np(self).editable) { throw new Error(NOT_EDITABLE); }
+        np(this).policy.requireEditable();
         if (!np(this).wrappedListeners) { return; }
         var wrappedListener = null;
         for (var i = np(this).wrappedListeners.length; --i >= 0;) {
@@ -2594,14 +2830,15 @@
* not applied here since that freezes the object, and also because of the
        * forwarding proxies used for catching expando properties.
        *
-       * @param {boolean} editable true if the node's value, attributes,
-       *     children,
-       *     or custom properties are mutable.
+       * @param {policy} Mutability policy to apply.
        * @constructor
        */
-      function TameNode(editable) {
+      function TameNode(policy) {
         TameNodeConf.confide(this);
-        np(this).editable = editable;
+        if (!policy || !policy.requireEditable) {
+ throw new Error("Domado internal error: Policy missing or invalid");
+        }
+        np(this).policy = policy;
         return this;
       }
       inertCtor(TameNode, Object, 'Node');
@@ -2657,11 +2894,6 @@
       // abstract TameNode.prototype.getElementsByClassName
       // abstract TameNode.prototype.childNodes
       // abstract TameNode.prototype.attributes
-      var tameNodePublicMembers = [
-          'cloneNode',
-          'appendChild', 'insertBefore', 'removeChild', 'replaceChild',
-          'dispatchEvent', 'hasChildNodes'
-          ];
       traceStartup("DT: about to defend TameNode");
       cajaVM.def(TameNode);  // and its prototype

@@ -2673,26 +2905,60 @@
* Note that the constructor returns a proxy which delegates to 'this';
        * subclasses should apply properties to 'this' but return the proxy.
        *
- * @param {boolean} childrenEditable true iff the child list is mutable. * @param {Function} opt_proxyType The constructor of the proxy handler
        *     to use, defaulting to ExpandoProxyHandler.
        * @constructor
        */
- function TameBackedNode(node, editable, childrenEditable, opt_proxyType) {
+      function TameBackedNode(node, opt_policy, opt_proxyType) {
         node = makeDOMAccessible(node);

         if (!node) {
throw new Error('Creating tame node with undefined native delegate');
         }

-        TameNode.call(this, editable);
+        // Determine access policy
+        var parent = makeDOMAccessible(node.parentNode);
+        var parentPolicy;
+        if (!parent ||
+            idClassPattern.test(parent.className) ||
+            idClassPattern.test(node.className)) {
+          parentPolicy = null;
+        } else {
+          // Parent is inside the vdoc.
+          parentPolicy = np(defaultTameNode(parent)).policy;
+        }
+        var policy;
+        if (opt_policy) {
+          if (parentPolicy) {
+            parentPolicy.childPolicy.assertRestrictedBy(opt_policy);
+          }
+          policy = opt_policy;
+          //console.log("", parent, "->", node, "policy explicit",
+          //    policy.toString());
+        } else if (idClassPattern.test(node.className)) {
+ // Virtual document root -- stop implicit recursion and define the
+          // root policy. If we wanted to be able to define a "entire DOM
+          // read-only" policy, this is where to hook it in.
+          policy = nodePolicyEditable;
+          //console.log("", parent, "->", node, "root policy",
+          //    policy.toString());
+        } else if (parentPolicy) {
+          policy = parentPolicy.childPolicy;
+          //console.log("", parent, "->", node, "policy via parent",
+          //    parentPolicy.toString(), policy.toString());
+        } else {
+          policy = nodePolicyEditable;
+          //console.log("", parent, "->", node, "policy isolated",
+          //    policy.toString());
+        }
+
+        TameNode.call(this, policy);

         np(this).feral = node;
-        np(this).childrenEditable = editable && childrenEditable;

         if (domitaModules.proxiesAvailable) {
np(this).proxyHandler = new (opt_proxyType || ExpandoProxyHandler)(
-              this, editable, getNodeExpandoStorage(node));
+              this, policy.editable, getNodeExpandoStorage(node));
         }
       }
       inertCtor(TameBackedNode, TameNode);
@@ -2700,7 +2966,7 @@
         nodeType: NP.ro,
         nodeName: NP.ro,
         nodeValue: NP.ro,
-        firstChild: NP_tameDescendant,
+        firstChild: NP_tameDescendant, // TODO(kpreid): Must be disableable
         lastChild: NP_tameDescendant,
         nextSibling: NP.related,
         previousSibling: NP.related,
@@ -2708,36 +2974,56 @@
         childNodes: {
           enumerable: true,
           get: cajaVM.def(function () {
-            return new TameNodeList(np(this).feral.childNodes,
- np(this).childrenEditable, defaultTameNode);
+            var privates = np(this);
+            if (privates.policy.childrenVisible) {
+              return new TameNodeList(np(this).feral.childNodes,
+                  defaultTameNode);
+            } else {
+              return fakeNodeList([]);
+            }
           })
         },
         attributes: {
           enumerable: true,
           get: cajaVM.def(function () {
-            var thisNode = np(this).feral;
-            var tameNodeCtor = function(node, editable) {
-              return new TameBackedAttributeNode(node, editable, thisNode);
-            };
-            return new TameNodeList(
-                thisNode.attributes, thisNode, tameNodeCtor);
+            var privates = np(this);
+            if (privates.policy.attributesVisible) {
+              var thisNode = privates.feral;
+              var tameNodeCtor = function(node) {
+                return new TameBackedAttributeNode(node, thisNode);
+              };
+              // TODO(kpreid): There is no test which caught a previous
+              // editability policy failure here
+              return new TameNodeList(thisNode.attributes, tameNodeCtor);
+            } else {
+              return fakeNodeList([]);
+            }
           })
         }
       });
       TameBackedNode.prototype.cloneNode = nodeMethod(function (deep) {
+        np(this).policy.requireUnrestricted();
         var clone = bridal.cloneNode(np(this).feral, Boolean(deep));
         // From http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-3A0ED0A4
// "Note that cloning an immutable subtree results in a mutable copy"
-        return defaultTameNode(clone, true);
+        return defaultTameNode(clone);
       });
-      TameBackedNode.prototype.appendChild = nodeMethod(function (child) {
-        child = child || {};
+      /** Is it OK to make 'child' a child of 'parent'? */
+      function checkAdoption(parent, child) {
         // Child must be editable since appendChild can remove it from its
         // parent.
+        np(parent).policy.requireChildrenEditable();
+        np(child).policy.requireEditable();
+ // Sanity check: this cannot currently happen but if it does then we
+        // need to rethink the calculation of policies.
+        np(parent).policy.childPolicy.assertRestrictedBy(np(child).policy);
+      }
+      TameBackedNode.prototype.appendChild = nodeMethod(function (child) {
+        child = child || {};
         child = TameNodeT.coerce(child);
-        if (!np(this).childrenEditable || !np(child).editable) {
-          throw new Error(NOT_EDITABLE);
-        }
+
+        checkAdoption(this, child);
+
         np(this).feral.appendChild(np(child).feral);
         return child;
       });
@@ -2745,24 +3031,23 @@
           function(toInsert, child) {
         toInsert = TameNodeT.coerce(toInsert);
         if (child === void 0) { child = null; }
+
         if (child !== null) {
           child = TameNodeT.coerce(child);
-          if (!np(child).editable) {
-            throw new Error(NOT_EDITABLE);
-          }
+          // TODO(kpreid): This child is not being mutated except for its
+          // previousSibling, so why are we rejecting here?
+          np(child).policy.requireEditable();
         }
-        if (!np(this).childrenEditable || !np(toInsert).editable) {
-          throw new Error(NOT_EDITABLE);
-        }
+        checkAdoption(this, toInsert);
+
         np(this).feral.insertBefore(
             np(toInsert).feral, child !== null ? np(child).feral : null);
         return toInsert;
       });
       TameBackedNode.prototype.removeChild = nodeMethod(function(child) {
         child = TameNodeT.coerce(child);
-        if (!np(this).childrenEditable || !np(child).editable) {
-          throw new Error(NOT_EDITABLE);
-        }
+        np(this).policy.requireChildrenEditable();
+        np(child).policy.requireEditable();
         np(this).feral.removeChild(np(child).feral);
         return child;
       });
@@ -2770,15 +3055,19 @@
           function(newChild, oldChild) {
         newChild = TameNodeT.coerce(newChild);
         oldChild = TameNodeT.coerce(oldChild);
-        if (!np(this).childrenEditable || !np(newChild).editable
-            || !np(oldChild).editable) {
-          throw new Error(NOT_EDITABLE);
-        }
+
+        checkAdoption(this, newChild);
+        np(oldChild).policy.requireEditable();
+
np(this).feral.replaceChild(np(newChild).feral, np(oldChild).feral);
         return oldChild;
       });
       TameBackedNode.prototype.hasChildNodes = nodeMethod(function() {
-        return !!np(this).feral.hasChildNodes();
+        if (np(this).policy.childrenVisible) {
+          return !!np(this).feral.hasChildNodes();
+        } else {
+          return false;
+        }
       });
// http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget
       // "The EventTarget interface is implemented by all Nodes"
@@ -2847,12 +3136,14 @@
        * A fake node that is not backed by a real DOM node.
        * @constructor
        */
-      function TamePseudoNode(editable) {
-        TameNode.call(this, editable);
+      function TamePseudoNode() {
+ // Note inconsistency: we have an editable policy, for the sake of our
+        // children, but don't actually allow direct mutation.
+        TameNode.call(this, nodePolicyEditable);

         if (domitaModules.proxiesAvailable) {
           // finishNode will wrap 'this' with an actual proxy later.
- np(this).proxyHandler = new ExpandoProxyHandler(this, editable, {});
+          np(this).proxyHandler = new ExpandoProxyHandler(this, true, {});
         }
       }
       inertCtor(TamePseudoNode, TameNode);
@@ -2901,121 +3192,40 @@
       cajaVM.def(TamePseudoNode);  // and its prototype

       traceStartup("DT: done fundamental nodes");
-      traceStartup("DT: about to define makeRestrictedNodeType");
-
-      function makeRestrictedNodeType(whitelist) {
-        function ForeignOrOpaqueNode(node, editable) {
-          TameBackedNode.call(this, node, editable, editable);
-        }
- var nodeType = ForeignOrOpaqueNode; // other name is for debug hint
-        inherit(nodeType, TameBackedNode);
-        for (var safe in whitelist) {
-          // Any non-own property is overridden to be opaque below.
-          var descriptor = (whitelist[safe] === 0)
-              ? domitaModules.getPropertyDescriptor(
-                    TameBackedNode.prototype, safe)
-              : {
-                  value: whitelist[safe],
-                  writable: false,
-                  configurable: false,
-                  enumerable: true
-              };
-          Object.defineProperty(nodeType.prototype, safe, descriptor);
-        }
-        definePropertiesAwesomely(nodeType.prototype, {
-          attributes: {
-            enumerable: canHaveEnumerableAccessors,
-            get: nodeMethod(function () {
-              return new TameNodeList([], false, undefined);
-            })
-          }
-        });
-        function throwRestricted() {
-          throw new Error('Node is restricted');
-        }
-        cajaVM.def(throwRestricted);
-        for (var i = tameNodePublicMembers.length; --i >= 0;) {
-          var k = tameNodePublicMembers[+i];
-          if (!nodeType.prototype.hasOwnProperty(k)) {
-            if (typeof TameBackedNode.prototype[k] === 'Function') {
-              nodeType.prototype[k] = throwRestricted;
-            } else {
-              Object.defineProperty(nodeType.prototype, k, {
-                enumerable: canHaveEnumerableAccessors,
-                get: throwRestricted
-              });
-            }
-          }
-        }
-        return cajaVM.def(nodeType);  // and its prototype
-      }

       traceStartup("DT: about to make TameOpaqueNode");

// An opaque node is traversible but not manipulable by guest code. This // is the default taming for unrecognized nodes or nodes not explicitly
       // whitelisted.
-      var TameOpaqueNode = makeRestrictedNodeType({
-        nodeValue: 0,
-        nodeType: 0,
-        nodeName: 0,
-        nextSibling: 0,
-        previousSibling: 0,
-        firstChild: 0,
-        lastChild: 0,
-        parentNode: 0,
***The diff for this file has been truncated for email.***
=======================================
--- /trunk/src/com/google/caja/plugin/html-emitter.js Tue Oct 23 10:12:27 2012 +++ /trunk/src/com/google/caja/plugin/html-emitter.js Thu Nov 8 14:37:26 2012
@@ -67,12 +67,12 @@
   var detached = null;
   /** Makes sure IDs are accessible within removed detached nodes. */
   var idMap = null;
-
+
   /** Hook from attach/detach to document.write logic. */
   var updateInsertionMode;

   var arraySplice = Array.prototype.splice;
-
+
   var HTML5_WHITESPACE_RE = /^[\u0009\u000a\u000c\u000d\u0020]*$/;

   function buildIdMap() {
@@ -445,7 +445,7 @@
     function defineUntrustedStylesheet(styleBaseUri, cssText) {
       if (domicile && domicile.emitCss) {
         domicile.emitCss(sanitizeStylesheet(styleBaseUri,
-            cssText, domicile.suffixStr.replace(/^-/, ''),
+            cssText, domicile.suffixStr.replace(/^-/, ''),
             makeCssUriSanitizer(styleBaseUri),
             domicile.tagPolicy));
       }
@@ -564,9 +564,9 @@
       }
       insertionPoint.appendChild(el);
       if (!vSchemaEl.empty) { insertionPoint = el; }
-
+
       for (var i = slowPathAttribs.length - 2; i >= 0; i -= 2) {
-        opt_domicile.tameNode(el, true).setAttribute(
+        opt_domicile.tameNode(el).setAttribute(
           slowPathAttribs[i], slowPathAttribs[i+1]);
       }
     }
@@ -756,7 +756,7 @@
             //  corresponding value to that element."
           } else if (tagName === 'base' || tagName === 'basefont' ||
               tagName === 'bgsound'     || tagName === 'command' ||
-              tagName === 'link'        || tagName === 'meta' ||
+              tagName === 'link'        || tagName === 'meta' ||
               tagName === 'noframes'    || tagName === 'script' ||
               tagName === 'style'       || tagName === 'title') {
             insertionModes.inHead.startTag.apply(undefined, arguments);
=======================================
--- /trunk/tests/com/google/caja/plugin/browser-test-case.js Tue Oct 23 16:53:57 2012 +++ /trunk/tests/com/google/caja/plugin/browser-test-case.js Thu Nov 8 14:37:26 2012
@@ -362,14 +362,18 @@
     frame.tame(frame.markFunction(assertStringDoesNotContain));

   if (frame.div) {
- // Create a readonly mirror of document so that we can test that mutations
-    // fail when they should.
-    standardImports.documentRO =
-      new frame.domicile.TameHTMLDocument(
-          document,          // Document of host frame
-          frame.div,         // Containing div in host frame
-          'nosuchhost.fake', // Fake domain name
-          false);            // Not writeable
+    // Create a node which is in a context such that it must be read-only.
+    // (Note taming membrane is in use here, so we get/return feral nodes.)
+    standardImports.makeReadOnly = frame.tame(frame.markFunction(
+        function (node) {
+      // Must clone to throw out the cached policy decision
+      var clone = node.cloneNode(true);
+      var container = document.createElement("anUnknownElement");
+      container.appendChild(clone);
+      node.parentNode.replaceChild(container, node);
+      frame.domicile.tameNode(clone); // cause registration as Domado node
+      return clone;
+    }));
   }

   var fakeConsole = {
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-canvas-guest.html Sat Sep 15 07:44:14 2012 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-canvas-guest.html Thu Nov 8 14:37:26 2012
@@ -569,7 +569,10 @@
 <script type="text/javascript">
   jsunitRegister('testCanvasNotEditable',
                  function testCanvasNotEditable() {
-    var canvas = documentRO.getElementById('testCanvasNotEditable-canvas');
+    var canvas = makeReadOnly(
+        document.getElementById('testCanvasNotEditable-canvas'));
+    assertEquals('CANVAS', canvas.tagName);
+ // verify the node is otherwise functional, not just a broken object
     expectFailure(function () {
       canvas.getContext('2d');
     }, "can't get context for a RO canvas ref");
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-dom-guest.html Tue Oct 30 17:04:11 2012 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-dom-guest.html Thu Nov 8 14:37:26 2012
@@ -859,14 +859,18 @@
 <script type="text/javascript">
   jsunitRegister('testReadOnly',
                  function testReadOnly() {
-    function $(id) { return documentRO.getElementById(id); }
+    makeReadOnly(document.getElementById('testReadOnly'));
+    function $(id) { return document.getElementById(id); }
     assertEquals("I am indelible", $('indelible').innerHTML);
         // test we can access it and aren't just failing unconditionally
-    expectFailure(
-        function () {
-          documentRO.createElement('SPAN');
-        },
-        'successfully created element');
+    // TODO(kpreid): It is no longer possible to create an independent RO
+ // Document in the same Domado vdoc. Test this in a context where we can
+    // create a whole document.
+    //expectFailure(
+    //    function () {
+    //      documentRO.createElement('SPAN');
+    //    },
+    //    'successfully created element');
     expectFailure(
         function () {
           var el = document.createElement('SPAN');
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-events-guest.html Thu Sep 20 20:58:16 2012 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-events-guest.html Thu Nov 8 14:37:26 2012
@@ -61,6 +61,9 @@
   window.doIt = function doIt(event, node) {
     if ((event instanceof Event) && (node instanceof HTMLElement)) {
       pass('testFunctionCallsInAttributes');
+    } else {
+ console.error('testFunctionCallsInAttributes: wrong args event=', event,
+          'node=', node);
     }
   };
   jsunitRegister('testFunctionCallsInAttributes',
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-foreign-guest.html Sat Sep 15 07:44:14 2012 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-foreign-guest.html Thu Nov 8 14:37:26 2012
@@ -17,13 +17,21 @@
 <script type="text/javascript">
   function assertBottom(msg, o) {
     if (!(o === null || o === undefined)) {
-      console.log("Expected bottom value: " + msg);
+      console.log("Expected bottom value: " + msg + "; but was", o);
       fail("Expected bottom value: " + msg);
     }
   }
   window.assertBottom = assertBottom; // SES scope workaround
 </script>

+<div>
+  <span class="innertest" id="innersafe">
+     For testEmbeddedForeign; must be early in document.
+     There should be exactly two spans visible in the
+     document, this and the added one, not foreign ones.
+  </span>
+</div>
+
 <p class="testcontainer" id="testNotNull">
   test-not-null
 </p>
@@ -46,7 +54,7 @@
       fail(name + ' assertThrowsRestricted');
     } catch (e) {
       e = '' + e;
-      if (!/Node is restricted/.test(e)) {
+ if (!(/Node is restricted/.test(e) || /Node not editable\./.test(e))) {
         fail(name + ' assertThrowsRestricted failed with ' + e);
       }
     }
@@ -69,6 +77,15 @@
     // assertFalse(name + 'hasAttributes()', node.hasAttributes());
     assertEquals(name + 'attributes', 0, node.attributes.length);
     assertBottom(name + 'attributes[0]', node.attributes[0]);
+    // There is no getAttribute method but a client could try this:
+    assertBottom(name + 'getAttribute unknown',
+      Element.prototype.getAttribute.call(node, 'testattr'));
+    assertBottom(name + 'getAttribute known',
+      Element.prototype.getAttribute.call(node, 'title'));
+    assertBottom(name + 'getAttributeNode unknown',
+      Element.prototype.getAttributeNode.call(node, 'testattr'));
+    assertBottom(name + 'getAttributeNode known',
+      Element.prototype.getAttributeNode.call(node, 'title'));

     // assertFalse(name + 'isSupported()', node.isSupported('', ''));

@@ -85,11 +102,13 @@
     // });

     assertThrowsRestricted(name + 'replaceChild(Node,Node)', function() {
-      node.replaceChild(undefined /* ?? */, document.createElement('div'));
+      node.replaceChild(document.createElement('div'),
+        document.createElement('div'));
     });

     assertThrowsRestricted(name + 'insertBefore(Node,Node)', function() {
-      node.insertBefore(undefined /* ?? */, document.createElement('div'));
+      node.insertBefore(document.createElement('div'),
+        document.createElement('div'));
     });

     assertThrowsRestricted(name + 'removeChild(Node)', function() {
@@ -100,7 +119,7 @@
       node.appendChild(document.createElement('div') /* ?? */);
     });

-    assertBottom(name + 'ownerDocument', node.ownerDocument);
+    assertEquals(name + 'ownerDocument', node.ownerDocument, document);
     assertBottom(name + 'parentNode', node.parentNode);

     assertFalse(name + 'hasChildNodes()', node.hasChildNodes());
@@ -145,9 +164,53 @@
   testEmbeddedForeign
 </p>
 <script type="text/javascript">
+  // TODO(kpreid): Does this already exist? jsUnit.js assertArrayEquals is
+  // a deep-compare which is not good for this case.
+  function assertIdenticalElements(note, array1, array2) {
+    if (array1.length === array2.length) {
+      var ok = true;
+      for (var i = array1.length - 1; i >= 0; i--) {
+        if (array1[i] !== array2[i]) {
+          ok = false;
+          break;
+        }
+      }
+      if (ok) { return; }
+    }
+    // found a mismatch, generate default error
+    assertEquals(note, array1, array2);
+  }
+
   jsunitRegister('testEmbeddedForeign',
                  function testEmbeddedForeign() {
     testForeign('embedded-foreign', getEmbeddedForeignNode());
+
+    // Test that getElementsBy* skip foreign children.
+    // goodNode1 is at the beginning of the list in document order and
+ // goodNode2 is at the end; they test that foreign children are omitted.
+    var goodNode1 = document.getElementById('innersafe');
+    var goodNode2 = document.createElement('span');
+    goodNode2.className = 'innertest';
+    document.body.appendChild(goodNode2);
+
+    var classNameList = document.getElementsByClassName('innertest');
+    var tagNameList = document.getElementsByTagName('span');
+
+    assertIdenticalElements('getElementsByClassName',
+        [goodNode1, goodNode2], classNameList);
+    assertIdenticalElements('getElementsByTagName',
+        [goodNode1, goodNode2], tagNameList);
+
+    // Test that node lists remain live-updating in the case where foreign
+    // nodes exist.
+    var goodNode1b = document.createElement('span');
+    goodNode1.parentNode.appendChild(goodNode1b);
+    assertIdenticalElements('getElementsByClassName after',
+        [goodNode1, goodNode1b, goodNode2], tagNameList);
+    goodNode1b.className = 'innertest';
+    assertIdenticalElements('getElementsByTagName after',
+        [goodNode1, goodNode1b, goodNode2], classNameList);
+
     pass('testEmbeddedForeign');
   });
 </script>
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-foreign.js Thu Aug 16 10:30:36 2012 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-foreign.js Thu Nov 8 14:37:26 2012
@@ -22,11 +22,13 @@

 function createTestDiv() {
   var d = createDiv();
-  d.setAttribute('testattr', 'testattrvalue');
+  d.setAttribute('testattr', 'testattrvaluereal');
+  d.setAttribute('data-caja-testattr', 'testattrvaluevirt');
+  d.setAttribute('title', 'testknownattrvalue');
   for (var i = 0; i < 2; i++) {
     var x = createDiv();
     x.setAttribute('class', 'testclass');
-    x.innerHTML = "Div number " + i;
+    x.innerHTML = "Div number " + i + " <span class='innertest'></span>";
     d.appendChild(x);
   }
   document.body.appendChild(d);

Reply via email to