Revision: 5504
Author:   [email protected]
Date:     Wed Jul 24 15:19:27 2013
Log: [APICHANGE] Tame Window and Document behave correctly as EventTargets.
https://codereview.appspot.com/11775043

For the most part, we use browser native event dispatch for our
virtualized events. load and DOMContentLoaded events have been an
exception for historical reasons (less confidence in the safe use of
initEvent/dispatchEvent) and because there is no host DOM object
corresponding to our TameWindow.

With this change, we now always use native event dispatch (except for
the window.onload _handler_, which is still a specialized kludge).

* Added a third level of wrapper <div> to the guest content, which is
  used to provide a feral EventTarget corresponding to the virtual
  document, whereas the former inner wrapper corresponds to the virtual
  window. The tame window and document are now tame twins of these feral
  nodes. (It would be possible to use only two nodes, but I'm being
  cautious about the interaction of tamed APIs with these new twins.)
  In the case of an iframe, the feral document and window are used
  directly.
* {add,remove}EventListener on tameWindow and tameDocument now install
  listeners on said feral EventTargets rather than implementing their
  own listener tables.
* All tame EventTargets (Node, Document, Window) now implement
  dispatchEvent, uniformly; previously document.dispatchEvent was
  missing and window.dispatchEvent was a no-op.

Supporting changes:
* Domado, rather than caja.js, is now responsible for creating the
  wrapper <div>s.
* TameWindow gets a toString method.
* In Domado, defaultTameNode rather than finishNode is responsible for
  doing taming.tamesTo().

Impact:
* The feral twins of tameWindow and tameDocument are now DOM nodes,
  rather than empty stub objects. (It is uncertain whether this will
  continue to be the case, as it was done solely to support the use of
  the existing event listener wrapper logic, rather than as a desired
  feature.)
* There are now three wrapper divs; if you are correctly using the
  class-name-based interface (caja-vdoc-wrapper etc.) as needed, or are
  not styling or otherwise interacting with the wrapper markup, this
  should make no difference.

[email protected]

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

Modified:
 /trunk/src/com/google/caja/plugin/caja.js
 /trunk/src/com/google/caja/plugin/domado.js
 /trunk/src/com/google/caja/plugin/es53-frame-group.js
 /trunk/src/com/google/caja/plugin/guest-manager.js
 /trunk/src/com/google/caja/plugin/ses-frame-group.js
 /trunk/tests/com/google/caja/plugin/browser-test-case.js
 /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.js
 /trunk/tests/com/google/caja/plugin/es53-test-domado-global.js
 /trunk/tests/com/google/caja/plugin/es53-test-domado-special-guest.html
 /trunk/tests/com/google/caja/plugin/third-party-tests.json

=======================================
--- /trunk/src/com/google/caja/plugin/caja.js   Mon Jul 15 10:46:44 2013
+++ /trunk/src/com/google/caja/plugin/caja.js   Wed Jul 24 15:19:27 2013
@@ -586,48 +586,16 @@
     }
   }

-  function prepareContainerDiv(div, feralWin, domOpts) {
-    if (div && feralWin['document'] !== div.ownerDocument) {
+  function prepareContainerDiv(opt_div, feralWin, domOpts) {
+    if (opt_div && feralWin['document'] !== opt_div.ownerDocument) {
       throw '<div> provided for ES5 frame must be in main document';
     }
     domOpts = domOpts || {};
     var opt_idClass = domOpts ? domOpts['idClass'] : void 0;
     var idClass = opt_idClass || ('caja-guest-' + nextId++ + '___');
-    var inner = null;
-    var outer = null;
-    if (div) {
-      // Class-name hooks: The host page can
-      // * match all elements between its content and the guest content as
-      //   .caja-vdoc-wrapper
-      // * match the outermost such element using .caja-vdoc-outer
-      // * match the innermost such element using .caja-vdoc-inner
- // This scheme has been chosen to be potentially forward-compatible in the
-      // event that we switch to more or less than 2 wrappers.
-
-      inner = div.ownerDocument.createElement('div');
-      inner.className = 'caja-vdoc-inner caja-vdoc-wrapper';
-      inner.style.display = 'block';
-      inner.style.position = 'relative';
-
-      outer = div.ownerDocument.createElement('div');
-      outer.className = 'caja-vdoc-outer caja-vdoc-wrapper';
-      outer.style.position = 'relative';
-      outer.style.overflow = 'hidden';
-      outer.style.display = 'block';
-      outer.style.margin = '0';
-      outer.style.padding = '0';
-      // Move existing children (like static HTML produced by the cajoler)
-      // into the inner container.
-      while (div.firstChild) {
-        inner.appendChild(div.firstChild);
-      }
-      outer.appendChild(inner);
-      div.appendChild(outer);
-    }
     return {
       'idClass': idClass,
-      'inner': inner,
-      'outer': outer
+      'opt_div': opt_div
     };
   }

=======================================
--- /trunk/src/com/google/caja/plugin/domado.js Mon Jul 15 15:56:25 2013
+++ /trunk/src/com/google/caja/plugin/domado.js Wed Jul 24 15:19:27 2013
@@ -1750,9 +1750,6 @@
     /**
      * Add a tamed document implementation to a Gadget's global scope.
      *
-     * Has the side effect of adding the classes "vdoc-container___" and
-     * idSuffix.substring(1) to the containerNode.
-     *
      * @param {string} idSuffix a string suffix appended to all node IDs.
      *     It should begin with "-" and end with "___".
      * @param {Object} uriPolicy an object like <pre>{
@@ -1771,8 +1768,8 @@
      *         If a hint is not present it should not be relied upon.
* The rewrite function should be idempotent to allow rewritten HTML
      *     to be reinjected. The policy must be a tamed object.
- * @param {Node} containerNode an HTML node to contain the children of the
-     *     virtual Document node provided to Cajoled code.
+ * @param {Node} outerContainerNode an HTML node to contain the virtual DOM
+     *     structure, either an Element or Document node.
* @param {Object} optTargetAttributePresets a record containing the presets
      *     (default and whitelist) for the HTML "target" attribute.
      * @param {Object} taming. An interface to a taming membrane.
@@ -1781,7 +1778,7 @@
      *     object is known as a "domicile".
      */
     function attachDocument(
-      idSuffix, naiveUriPolicy, containerNode, optTargetAttributePresets,
+ idSuffix, naiveUriPolicy, outerContainerNode, optTargetAttributePresets,
         taming) {

       if (arguments.length < 3) {
@@ -1816,17 +1813,75 @@

       var vdocContainsForeignNodes = false;

-      containerNode = makeDOMAccessible(containerNode);
-      var document = containerNode.nodeType === 9  // Document node
-          ? containerNode
-          : containerNode.ownerDocument;
+      outerContainerNode = makeDOMAccessible(outerContainerNode);
+      var document = outerContainerNode.nodeType === 9  // Document node
+          ? outerContainerNode
+          : outerContainerNode.ownerDocument;
       document = makeDOMAccessible(document);
       var bridal = bridalMaker(makeDOMAccessible, document);
       var elementForFeatureTests =
           makeDOMAccessible(document.createElement('div'));

-      var window = bridalMaker.getWindow(containerNode, makeDOMAccessible);
+ var window = bridalMaker.getWindow(outerContainerNode, makeDOMAccessible);
       window = makeDOMAccessible(window);
+
+ // Note that feralPseudoWindow may be an Element or a Window depending.
+      var feralPseudoDocument, feralPseudoWindow;
+      if (outerContainerNode.nodeType === 9) { // Document node
+        feralPseudoWindow = window;
+        feralPseudoDocument = outerContainerNode;
+      } else {
+        // Construct wrappers for visual isolation and for feral nodes
+        // corresponding to tame nodes.
+        //
+ // * outerIsolator and feralPseudoWindow (the two outermost wrappers)
+        //   together provide visual isolation.
+        // * feralPseudoWindow is the feral node used for event dispatch on
+        //   tameWindow. feralPseudoDocument is the same for tameDocument.
+        //
+ // The reason we do not use feralPseudoDocument as the inner isolator + // is that feral nodes which are the feral-twin of some tame node are + // at higher risk of being mutated by the guest, or by tamed host APIs
+        // on behalf of the guest. This way, a visual isolation break would
+ // require modifying untame(tameWindow).style, which is less likely to
+        // be accidentally permitted.
+        var outerIsolator = makeDOMAccessible(
+            document.createElement('div'));
+        feralPseudoDocument = makeDOMAccessible(
+            document.createElement('div'));
+ feralPseudoWindow = makeDOMAccessible(document.createElement('div'));
+        outerIsolator.appendChild(feralPseudoWindow);
+        feralPseudoWindow.appendChild(feralPseudoDocument);
+        // Class-name hooks: The host page can
+ // * match all elements between its content and the guest content as
+        //   .caja-vdoc-wrapper
+        // * match the outermost such element using .caja-vdoc-outer
+        // * match the innermost such element using .caja-vdoc-inner
+        // This scheme has been chosen to be forward-compatible in the
+        // event that we change the number of wrappers in use.
+        outerIsolator.className = 'caja-vdoc-wrapper caja-vdoc-outer';
+        feralPseudoWindow.className = 'caja-vdoc-wrapper';
+ feralPseudoDocument.className = 'caja-vdoc-wrapper caja-vdoc-inner ' +
+            'vdoc-container___ ' + idClass;
+        // Visual isolation.
+ // TODO(kpreid): Add explanation of how these style rules produce the
+        // needed effects.
+        makeDOMAccessible(outerIsolator.style);
+        outerIsolator.style.display = 'block';
+        outerIsolator.style.position = 'relative';
+        outerIsolator.style.overflow = 'hidden';
+        outerIsolator.style.margin = '0';
+        outerIsolator.style.padding = '0';
+        makeDOMAccessible(feralPseudoWindow.style);
+        feralPseudoWindow.style.display = 'block';
+        feralPseudoWindow.style.position = 'relative';
+ // Final hookup; move existing children (like static HTML produced by
+        // the cajoler) into the virtual document.
+        while (outerContainerNode.firstChild) {
+          feralPseudoDocument.appendChild(outerContainerNode.firstChild);
+        }
+        outerContainerNode.appendChild(outerIsolator);
+      }

       var elementPolicies = {};
       elementPolicies.form = function (attribs) {
@@ -1862,7 +1917,7 @@

       // On IE, turn <canvas> tags into canvas elements that explorercanvas
       // will recognize
-      bridal.initCanvasElements(containerNode);
+      bridal.initCanvasElements(outerContainerNode);

       var tamingClassTable = new TamingClassTable();
       var inertCtor = tamingClassTable.inertCtor.bind(tamingClassTable);
@@ -1916,27 +1971,6 @@

             node = proxiedNode;
           }
-
-          var feral = privates.feral;
-          if (feral) {
-            if (feral.nodeType === 1) {
-              // Elements must only be tamed once; to do otherwise would be
-              // a bug in Domado.
-              taming.tamesTo(feral, node);
-            } else {
- // Other node types are tamed every time they are encountered;
-              // we simply remember the latest taming here.
-              // TODO(kpreid): Review whether this is still desired and
-              // consistent behavior.
-              taming.reTamesTo(feral, node);
-            }
-          } else {
- // If guest code passes a node of its own with no feral counterpart
-            // to host code, we pass the empty object "{}". This is a safe
-            // behavior until experience determines we need something more
-            // complex.
-            taming.tamesTo({}, node);
-          }

// Require all properties of the private state record to have already // been created (presumably in the constructor). This is so that the
@@ -2071,7 +2105,7 @@
// Class name matching the virtual document container. May be null (not
         // undefined) if we are taming a complete document and there is no
         // container (note: this case is not yet fully implemented).
-        containerClass: containerNode === document ? null : idClass,
+        containerClass: outerContainerNode === document ? null : idClass,

         // Suffix to append to all IDs and ID references.
         idSuffix: idSuffix,
@@ -2781,12 +2815,19 @@
             : makeTameNodeByType(node);
         tamed = finishNode(tamed);

+        taming.tamesTo(node, tamed);
+
         return tamed;
       }

+      /**
+ * Tame a reference to a feral node which might turn out to be outside the
+       * virtual document, inside a foreign node, etc. (in which case it is
+       * replaced with null).
+       */
       function tameRelatedNode(node) {
         if (node === null || node === void 0) { return null; }
-        if (node === containerNode) {
+        if (node === feralPseudoDocument) {
           return tameDocument;
         }

@@ -2811,6 +2852,22 @@
         } catch (e) {}
         return null;
       }
+
+      /**
+ * Like tameRelatedNode but includes the window (which is an EventTarget,
+       * but not a Node).
+       */
+      function tameEventTarget(nodeOrWindow) {
+ if (nodeOrWindow === feralPseudoWindow || nodeOrWindow === window) {
+          return tameWindow;
+        } else if (nodeOrWindow &&
+            makeDOMAccessible(nodeOrWindow).nodeType === 1) {
+          return tameRelatedNode(nodeOrWindow);
+        } else {
+          // Wasn't an element and wasn't the particular window.
+          return null;
+        }
+      }

       /**
        * Is this node a descendant of a foreign node, and therefore to be
@@ -3517,23 +3574,45 @@
         }
         return wrapper;
       }
+
+      /**
+ * Catch all failures and pass to onerror, for when we _aren't_ wrapping
+       * a native event handler/listener and must catch everything.
+       */
+      function callAsEventListener(func, thisArg, tameEventObj) {
+        try {
+          Function.prototype.call.call(func, thisArg, tameEventObj);
+        } catch (e) {
+          try {
+            tameWindow.onerror(
+                e.message,
+ '<' + tameEventObj.type + ' handler>', // better than nothing
+                0);
+          } catch (e2) {
+            console.error('onerror handler failed\n', e, '\n', e2);
+          }
+        }
+      }
+
+      function getFeralEventTarget(privates) {
+        return privates.feralEventTarget || privates.feral;
+      }

       // Implementation of EventTarget::addEventListener
-      var tameAddEventListener =
-          nodeAmp(function(privates, name, listener, useCapture) {
+      var tameAddEventListenerProp =
+          Props.ampMethod(function(privates, name, listener, useCapture) {
         name = String(name);
         useCapture = Boolean(useCapture);
-        var feral = privates.feral;
+        var feral = getFeralEventTarget(privates);
         privates.policy.requireEditable();
         var list = privates.wrappedListeners;
         if (!list) {
           list = privates.wrappedListeners = [];
         }
         if (searchForListener(list, name, listener, useCapture) === null) {
-          var wrappedListener = makeEventHandlerWrapper(
-              privates.feral, listener);
+          var wrappedListener = makeEventHandlerWrapper(feral, listener);
           var remove = bridal.addEventListener(
-              privates.feral, name, wrappedListener, useCapture);
+              feral, name, wrappedListener, useCapture);
           list.push({
             n: name,
             l: listener,
@@ -3544,11 +3623,10 @@
       });

       // Implementation of EventTarget::removeEventListener
-      var tameRemoveEventListener =
-          nodeAmp(function(privates, name, listener, useCapture) {
+      var tameRemoveEventListenerProp =
+          Props.ampMethod(function(privates, name, listener, useCapture) {
         name = String(name);
         useCapture = Boolean(useCapture);
-        var feral = privates.feral;
         privates.policy.requireEditable();
         var list = privates.wrappedListeners;
         if (!list) { return; }
@@ -3571,6 +3649,13 @@
         return null;
       }

+      var tameDispatchEventProp = Props.ampMethod(function(privates, evt) {
+        eventAmplify(evt, function(evtPriv) {
+ bridal.dispatchEvent(getFeralEventTarget(privates), evtPriv.feral);
+        });
+      });
+
+
// We have now set up most of the 'support' facilities and are starting to
       // define node taming classes.

@@ -3708,6 +3793,10 @@
         nodeAmplify(this, function(privates) {
           privates.feral = node;

+          // protocol for EventTarget operations
+          privates.wrappedListeners = [];
+          // privates.feralEventTarget absent as default
+
           if (proxiesAvailable && opt_proxyType) {
             privates.proxyHandler = new opt_proxyType(this);
           }
@@ -3812,11 +3901,9 @@
         }),
// http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget
         // "The EventTarget interface is implemented by all Nodes"
-        dispatchEvent: Props.ampMethod(function(privates, evt) {
-          eventAmplify(evt, function(evtPriv) {
-            bridal.dispatchEvent(privates.feral, evtPriv.feral);
-          });
-        }),
+        addEventListener: tameAddEventListenerProp,
+        removeEventListener: tameRemoveEventListenerProp,
+        dispatchEvent: tameDispatchEventProp,
         /**
* Speced in <a href="http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition";>DOM-Level-3</a>.
          */
@@ -4089,7 +4176,6 @@
         TameBackedNode.call(this, node, opt_policy, opt_proxyType);
         nodeAmplify(this, function(privates) {
           privates.geometryDelegate = node;
-          privates.wrappedListeners = undefined;
         });
       }
       var defaultNodeClassCtor =
@@ -4230,14 +4316,14 @@
             var feralOffsetParent = privates.feral.offsetParent;
             if (!feralOffsetParent) {
               return feralOffsetParent;
-            } else if (feralOffsetParent === containerNode) {
+            } else if (feralOffsetParent === feralPseudoDocument) {
// Return the body if the node is contained in the body. This is // emulating how browsers treat offsetParent and the real <BODY>.
               return nodeAmplify(tameDocument.body, function(bodyPriv) {
                 var feralBody = bodyPriv.feral;
                 for (var ancestor =
                          makeDOMAccessible(privates.feral.parentNode);
-                     ancestor !== containerNode;
+                     ancestor !== feralPseudoDocument;
                      ancestor = makeDOMAccessible(ancestor.parentNode)) {
                   if (ancestor === feralBody) {
                     return defaultTameNode(feralBody);
@@ -4374,9 +4460,7 @@
                       bottom: elRect.bottom - vdocTop
                     });
           });
-        }),
-        addEventListener: Props.overridable(true, tameAddEventListener),
- removeEventListener: Props.overridable(true, tameRemoveEventListener)
+        })
       });
       if ('classList' in elementForFeatureTests) {
         Props.define(TameElement.prototype, TameNodeConf, {
@@ -5906,10 +5990,10 @@
           view: P_e_view(tameEventView),
           target: eventVirtualizingAccessor(function(privates) {
             var event = privates.feral;
-            return tameRelatedNode(event.target || event.srcElement);
+            return tameEventTarget(event.target || event.srcElement);
           }),
           srcElement: P_e_view(tameRelatedNode),
-          currentTarget: P_e_view(tameRelatedNode),
+          currentTarget: P_e_view(tameEventTarget),
           relatedTarget: eventVirtualizingAccessor(function(privates) {
             var e = privates.feral;
             var t = e.relatedTarget;
@@ -5920,7 +6004,7 @@
                 t = e.fromElement;
               }
             }
-            return tameRelatedNode(t);
+            return tameEventTarget(t);
           }),
           fromElement: P_e_view(tameRelatedNode),
           toElement: P_e_view(tameRelatedNode),
@@ -6040,9 +6124,19 @@
           privates.onLoadListeners = [];
           privates.onDCLListeners = [];

+          // protocol for EventTarget operations
+          privates.wrappedListeners = [];
+          privates.feralEventTarget = container;
+          // We use .feralEventTarget rather than .feral here because, even
+ // though the feral twin of the tame document is container, because + // it is not truly a node taming and ordinary node operations should
+          // not be effective on the document's feral node.
+
// Used to implement operations on the document, never exposed to the
-          // guest.
-          privates.tameContainerNode = defaultTameNode(container);
+          // guest. Note in particular that we skip defaultTameNode to skip
+          // registering it in the taming membrane.
+          privates.tameContainerNode =
+            finishNode(makeTameNodeByType(container));
         });

         Props.define(this, TameNodeConf, {
@@ -6050,6 +6144,8 @@
         });

         installLocation(this);
+
+        taming.tamesTo(container, this);
       }
       tamingClassTable.registerSafeCtor('Document',
           inertCtor(TameHTMLDocument, TameNode, 'HTMLDocument'));
@@ -6215,21 +6311,9 @@
return tameQuerySelector(privates.feralContainerNode, selector,
                   true);
             })),
-        addEventListener: Props.ampMethod(
-            function(privates, name, listener, useCapture) {
-          if (name === 'DOMContentLoaded') {
-            ensureValidCallback(listener);
-            privates.onDCLListeners.push(listener);
-          } else {
-            return privates.tameContainerNode.addEventListener(
-                name, listener, useCapture);
-          }
-        }),
-        removeEventListener: Props.ampMethod(
-            function(privates, name, listener, useCapture) {
-          return privates.tameContainerNode.removeEventListener(
-              name, listener, useCapture);
-        }),
+        addEventListener: tameAddEventListenerProp,
+        removeEventListener: tameRemoveEventListenerProp,
+        dispatchEvent: tameDispatchEventProp,
         createComment: Props.ampMethod(function(privates, text) {
           return defaultTameNode(privates.feralDoc.createComment(" "));
         }),
@@ -6347,34 +6431,26 @@
         };
       });

-      function dispatchToListeners(eventType, eventName, listeners) {
+      // TODO(kpreid): reconcile this and fireVirtualEvent
+      function dispatchToListeners(tameNode, eventType, eventName) {
         var event = tameDocument.createEvent(eventType);
-        event.initEvent(eventName, false, false);
-        // In case a listener attempts to append another listener
-        var len = listeners.length;
-        for (var i = 0; i < len; ++i) {
-          tameWindow.setTimeout(
-              Function.prototype.bind.call(
-                listeners[+i], tameWindow, event), 0);
+        event.initEvent(eventName, true, false);
+
+        // TODO(kpreid): onload should be handled generically as an event
+        // handler, not as a special case. But how?
+        if (eventName === 'load') {
+          if (tameWindow.onload) {
+            callAsEventListener(tameWindow.onload, tameNode, event);
+          }
         }
-        listeners.length = 0;
+
+        tameNode.dispatchEvent(event);
       }

// Called by the html-emitter when the virtual document has been loaded.
       domicile.signalLoaded = cajaVM.constFunc(function() {
-        nodeAmplify(tameDocument, function(privates) {
-          dispatchToListeners(
-              'Event',
-              'DOMContentLoaded',
-              privates.onDCLListeners);
-          if (tameWindow.onload) {
-            tameWindow.setTimeout(tameWindow.onload, 0);
-          }
-          dispatchToListeners(
-              'UIEvent',
-              'load',
-              privates.onLoadListeners);
-        });
+        dispatchToListeners(tameDocument, 'Event', 'DOMContentLoaded');
+        dispatchToListeners(tameWindow, 'UIEvent', 'load');
       });

       // Currently used by HtmlEmitter to synthesize script load events.
@@ -6603,7 +6679,7 @@
           function() {
         function isNestedInAnchor(el) {
           for (;
-              el && el != containerNode;
+              el && el !== feralPseudoDocument;
               el = makeDOMAccessible(el.parentNode)) {
             if (el.tagName && el.tagName.toLowerCase() === 'a') {
               return true;
@@ -6641,7 +6717,9 @@
                 return superReadByCanonicalName.call(this, canonName);
               } else {
                 return TameStyleConf.amplify(
- new TameComputedStyle(containerNode, this.pseudoElement), + // TODO(kpreid): Explain why we're using this node's answer
+                    new TameComputedStyle(feralPseudoDocument,
+                        this.pseudoElement),
                     function(p2) {
                   return p2.readByCanonicalName(canonName);
                 });
@@ -6701,7 +6779,7 @@
        * document?
        */
       function isContainerNode(node) {
-        return node === containerNode ||
+        return node === feralPseudoDocument ||
             (node &&
              node.nodeType === 1 &&
              idClassPattern.test(node.className));
@@ -6710,22 +6788,32 @@
       domicile.getIdClass = cajaVM.constFunc(function() {
         return idClass;
       });
-      // enforce id class on container
-      if (containerNode.nodeType !== 9) {  // not a document (top level)
-        bridal.setAttribute(containerNode, 'class',
-            bridal.getAttribute(containerNode, 'class')
-            + ' ' + idClass + ' vdoc-container___');
-      }
+
+      /**
+ * The node whose children correspond to the children of the tameDocument.
+       */
+      domicile.getPseudoDocument = cajaVM.constFunc(function() {
+        return feralPseudoDocument;
+      });

       var TameWindowConf = new Confidence('TameWindow');

       /**
* See http://www.whatwg.org/specs/web-apps/current-work/multipage/browsers.html#window for the full API.
        */
-      function TameWindow(container) {
+      function TameWindow(feralWinNode, feralDocNode) {
         TameWindowConf.confide(this, taming);
         TameWindowConf.amplify(this, function(privates) {
-          privates.feralContainerNode = container;
+          // TODO(kpreid): revise this to make sense
+          privates.feralContainerNode = feralDocNode;
+
+          // needed for EventTarget
+          privates.policy = nodePolicyEditable;
+
+          // protocol for EventTarget operations
+          privates.wrappedListeners = [];
+          privates.feralEventTarget = feralWinNode;
+
           Object.preventExtensions(privates);
         });

@@ -6740,7 +6828,7 @@
           writable: false
         });

-        taming.permitUntaming(this);
+        taming.tamesTo(feralWinNode, this);

         // Attach reflexive properties
         [
@@ -6897,45 +6985,9 @@
       var stubBarPropProp = Props.overridable(true,
           cajaVM.def({visible: false}));
       Props.define(TameWindow.prototype, TameWindowConf, {
-        addEventListener: Props.plainMethod(
-            function(name, listener, useCapture) {
-          if (name === 'load') {
-            ensureValidCallback(listener);
-            nodeAmplify(tameDocument, function(privates) {
-              privates.onLoadListeners.push(listener);
-            });
-          } else if (name === 'DOMContentLoaded') {
-            ensureValidCallback(listener);
-            nodeAmplify(tameDocument, function(privates) {
-              privates.onDCLListeners.push(listener);
-            });
-          } else {
-            // TODO: need a testcase for this
-            tameDocument.addEventListener(name, listener, useCapture);
-          }
-        }),
-        removeEventListener: Props.plainMethod(
-            function (name, listener, useCapture) {
-          if (name === 'load' || name === 'DOMContentLoaded') {
-            var listeners = nodeAmplify(tameDocument, function(p) {
- // exception-safe export from amp - all objects are guest code - return p[name === 'load' ? 'onLoadListeners' : 'onDCLListeners'];
-            });
-            var k = 0;
-            for (var i = 0, n = listeners.length; i < n; ++i) {
-              listeners[i - k] = listeners[+i];
-              if (listeners[+i] === listener) {
-                ++k;
-              }
-            }
-            listeners.length -= k;
-          } else {
-            tameDocument.removeEventListener(name, listener, useCapture);
-          }
-        }),
-        dispatchEvent: Props.plainMethod(function (evt) {
-          // TODO(ihab.awad): Implement
-        }),
+        addEventListener: tameAddEventListenerProp,
+        removeEventListener: tameRemoveEventListenerProp,
+        dispatchEvent: tameDispatchEventProp,
         scrollBy: Props.ampMethod(function(privates, dx, dy) {
// The window is always auto scrollable, so make the apparent window
           // body scrollable if the gadget tries to scroll it.
@@ -7022,14 +7074,14 @@

       var tameDocument = new TameHTMLDocument(
           document,
-          containerNode,
+          feralPseudoDocument,
           // TODO(jasvir): Properly wire up document.domain
           // by untangling the cyclic dependence between
           // TameWindow and TameDocument
           String(undefined || 'nosuchhost.invalid'));
-      domicile.htmlEmitterTarget = containerNode;
+      domicile.htmlEmitterTarget = feralPseudoDocument;

-      var tameWindow = new TameWindow(containerNode);
+ var tameWindow = new TameWindow(feralPseudoWindow, feralPseudoDocument);



=======================================
--- /trunk/src/com/google/caja/plugin/es53-frame-group.js Thu Jul 11 15:50:37 2013 +++ /trunk/src/com/google/caja/plugin/es53-frame-group.js Wed Jul 24 15:19:27 2013
@@ -209,17 +209,17 @@
   //----------------

   function makeES5Frame(div, uriPolicy, es5ready, domOpts) {
-    var divs = cajaInt.prepareContainerDiv(div, feralWin, domOpts);
+    var divInfo = cajaInt.prepareContainerDiv(div, feralWin, domOpts);
     guestMaker.make(function (guestWin) {
       var frameTamingSchema =
           TamingSchema(tamingHelper);
       var frameTamingMembrane =
           TamingMembrane(tamingHelper, frameTamingSchema.control);
       var domicileAndEmitter = makeDomicileAndEmitter(
-          frameTamingMembrane, divs, uriPolicy, guestWin);
+          frameTamingMembrane, divInfo, uriPolicy, guestWin);
       var domicile = domicileAndEmitter && domicileAndEmitter[0];
       var htmlEmitter = domicileAndEmitter && domicileAndEmitter[1];
-      var gman = GuestManager(frameTamingSchema, frameTamingMembrane, divs,
+ var gman = GuestManager(frameTamingSchema, frameTamingMembrane, divInfo,
           cajaInt.documentBaseUrl(), domicile, htmlEmitter, guestWin,
           tamingWin.___.USELESS, uriPolicy, es53run);
       gman._loader = guestWin.loadModuleMaker(
@@ -230,8 +230,8 @@
   }

   function makeDomicileAndEmitter(
-      frameTamingMembrane, divs, uriPolicy, guestWin) {
-    if (!divs.inner) { return null; }
+      frameTamingMembrane, divInfo, uriPolicy, guestWin) {
+    if (!divInfo.opt_div) { return null; }

     // Needs to be accessible by Domado. But markFunction must be done at
     // most once, so markFunction(uriPolicy.rewrite) would only work once,
@@ -285,7 +285,7 @@
     markCallableWithoutMembrane(frameTamingMembrane.hasFeralTwin);

     var domicile = domado.attachDocument(
-      '-' + divs.idClass, uriPolicyWrapper, divs.inner,
+      '-' + divInfo.idClass, uriPolicyWrapper, divInfo.opt_div,
       targetAttributePresets,
       recordWithMethods(
         'permitUntaming', permitUntaming,
=======================================
--- /trunk/src/com/google/caja/plugin/guest-manager.js Thu Jun 27 13:45:40 2013 +++ /trunk/src/com/google/caja/plugin/guest-manager.js Wed Jul 24 15:19:27 2013
@@ -38,8 +38,8 @@
  * semantics that don't matter in practice.  GuestManager combines the two.
  */

-function GuestManager(frameTamingSchema, frameTamingMembrane, divs, hostBaseUrl,
-  domicile, htmlEmitter, guestWin, USELESS, uriPolicy, runImpl) {
+function GuestManager(frameTamingSchema, frameTamingMembrane, divInfo,
+ hostBaseUrl, domicile, htmlEmitter, guestWin, USELESS, uriPolicy, runImpl) {
   // TODO(felix8a): this api needs to be simplified; it's difficult to
   // explain what all the parameters mean in different situations.
   var args = {
@@ -74,8 +74,8 @@

   var self = {
     // Public state
-    div: divs.outer && divs.outer.parentNode,
-    idClass: divs.idClass,
+    div: divInfo.opt_div && divInfo.opt_div.parentNode,
+    idClass: divInfo.idClass,
     getUrl: function() { return args.url; },
     getUriPolicy: function() { return uriPolicy; },

@@ -110,7 +110,7 @@
     // deprecated; idSuffix in domado means '-' + idClass, but idSuffix
     // exposed here is without the leading '-'.  Future code should use the
     // idClass property instead.
-    idSuffix: divs.idClass,
+    idSuffix: divInfo.idClass,

     // TODO(kpreid): rename/move to make sure this is used only for testing
// as SES now doesn't have a distinct guestWin which could cause confusion.
@@ -121,8 +121,8 @@
               : (guestWin.___
? guestWin.___.copy(guestWin.___.sharedImports) // for es53
                  : {})),                                         // for ses
-    innerContainer: divs.inner,
-    outerContainer: divs.outer,
+    innerContainer: domicile && domicile.getPseudoDocument(),
+    outerContainer: divInfo.opt_div,

     // Internal state
     domicile: domicile,      // Currently exposed only for the test suite
=======================================
--- /trunk/src/com/google/caja/plugin/ses-frame-group.js Tue Jun 18 13:18:13 2013 +++ /trunk/src/com/google/caja/plugin/ses-frame-group.js Wed Jul 24 15:19:27 2013
@@ -190,16 +190,16 @@
   //----------------

   function makeES5Frame(div, uriPolicy, es5ready, domOpts) {
-    var divs = cajaInt.prepareContainerDiv(div, feralWin, domOpts);
+    var divInfo = cajaInt.prepareContainerDiv(div, feralWin, domOpts);

     var frameTamingSchema = TamingSchema(tamingHelper);
     var frameTamingMembrane =
         TamingMembrane(tamingHelper, frameTamingSchema.control);
     var domicileAndEmitter = makeDomicileAndEmitter(
-        frameTamingMembrane, divs, uriPolicy);
+        frameTamingMembrane, divInfo, uriPolicy);
     var domicile = domicileAndEmitter && domicileAndEmitter[0];
     var htmlEmitter = domicileAndEmitter && domicileAndEmitter[1];
-    var gman = GuestManager(frameTamingSchema, frameTamingMembrane, divs,
+ var gman = GuestManager(frameTamingSchema, frameTamingMembrane, divInfo,
         cajaInt.documentBaseUrl(), domicile, htmlEmitter, window, USELESS,
         uriPolicy, sesRun);
     es5ready(gman);
@@ -208,8 +208,8 @@
   //----------------

   function makeDomicileAndEmitter(
-      frameTamingMembrane, divs, uriPolicy) {
-    if (!divs.inner) { return null; }
+      frameTamingMembrane, divInfo, uriPolicy) {
+    if (!divInfo.opt_div) { return null; }

     function FeralTwinStub() {}
     FeralTwinStub.prototype.toString = function () {
@@ -255,7 +255,7 @@
     });

     var domicile = domado.attachDocument(
-      '-' + divs.idClass, uriPolicyWrapper, divs.inner,
+      '-' + divInfo.idClass, uriPolicyWrapper, divInfo.opt_div,
       config.targetAttributePresets,
       Object.freeze({
         permitUntaming: permitUntaming,
=======================================
--- /trunk/tests/com/google/caja/plugin/browser-test-case.js Thu Jul 11 15:50:37 2013 +++ /trunk/tests/com/google/caja/plugin/browser-test-case.js Wed Jul 24 15:19:27 2013
@@ -599,11 +599,11 @@
       return frame.domicile.tameNode(
           frame.domicile.feralNode(tameNode).parentNode);
     },
-    getBodyNode: function () {
+    getVdocNode: function () {
       return frame.domicile.tameNode(frame.innerContainer);
     },
     getComputedStyle: function (tameNode, styleProp, opt_pseudoElement) {
-      var node = frame.domicile.feralNode(tameNode);
+      var node = frame.untame(tameNode);
       if (node.currentStyle && !opt_pseudoElement) {
         return node.currentStyle[styleProp.replace(
             /-([a-z])/g,
@@ -651,7 +651,7 @@
   makeCallable(directAccess.getInnerHTML);
   makeCallable(directAccess.getAttribute);
   makeCallable(directAccess.getParentNode);
-  makeCallable(directAccess.getBodyNode);
+  makeCallable(directAccess.getVdocNode);
   makeCallable(directAccess.getComputedStyle);
   makeCallable(directAccess.makeUnattachedScriptNode);
   makeCallable(directAccess.evalInHostFrame);
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-dom-guest.html Mon Jul 15 14:11:53 2013 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-dom-guest.html Wed Jul 24 15:19:27 2013
@@ -3100,6 +3100,9 @@
// Except as noted, all Domado objects we're checking here should toString
     // like the native DOM nodes with the extra marker "domado ".

+    // Global
+    assertEquals('[domado object Window]', window.toString());
+
     // Nodes
     // Printing the nodeName is non-standard
     assertEquals('[domado object HTMLBodyElement BODY]',
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-events-guest.html Wed Jul 17 09:57:52 2013 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-events-guest.html Wed Jul 24 15:19:27 2013
@@ -343,7 +343,7 @@
     var container = document.getElementById('testRepeatedHandlers');

     var events = [];
-    var record = jsunitCallback(function record(event) {
+    var record = jsunitCallback(function(event) {
       events.push([event.type, event.eventPhase]);
     });

@@ -527,76 +527,98 @@
   });
 </script>

-<p class="testcontainer" id="testOnLoadListener">Onload</p>
-<script type="text/javascript">
+<p class="testcontainer" id="testLoadEvents">
+  Load events sequencing and bubbling
+</p>
+<script>
   (function() {
-    var b1 = 0;
-    var b2 = 0;
-    var b3 = 0;
-    var setb1 = function setb1cb() {
-      console.log('set onload b1');
-      b1 ++;
-    };
-    var setb2 = function setb2cb(event) {
-      assertEquals('load', event.type);
-      assertEquals(1, arguments.length);
-      console.log('set onload b2');
-      b2 ++;
-    };
-    var setb3 = function setb3cb() {
-      console.log('set onload b3');
-      b3 ++;
-    };
-    window.addEventListener('load', setb1);
-    window.addEventListener('load', setb2);
-    window.removeEventListener('load', setb1);
-    window.onload = setb3;
+    var events = [];
+    var additionalError;

-    jsunitRegister('testOnLoadListener',
-                 function testOnLoadListener() {
-      if (b1 === 0 && b2 === 1 && b3 === 1) {
-        pass('testOnLoadListener');
-      } else {
-        fail('testOnLoadListener');
-      }
+    jsunitRegister('testLoadEvents', function() {
+      assertEquals(
+        'w,DOMContentLoaded,1,true ' +
+        'd,DOMContentLoaded,2,true ' +
+        'd,DOMContentLoaded,2,false ' +
+        'w,DOMContentLoaded,3,false ' +
+        'w,load,0,handler ' +
+        'w,load,2,true ' +
+        'w,load,2,false',
+        events.join(' '));
+      pass('testLoadEvents');
     });
-  })();
-</script>

-<p class="testcontainer" id="testDOMContentLoadedListener">DOMContentLoaded</p>
-<script>
-  (function () {
-    var b1 = 0;
-    var b2 = 0;
-    var b3 = 0;
-    var setb1 = function setb1cb() {
-      console.log('set dcl b1');
-      b1 ++;
-    };
-    var setb2 = function setb2cb() {
-      console.log('set dcl b2');
-      b2 ++;
-    };
-    var setb3 = function setb3cb() {
-      console.log('set dcl b3');
-      b3 += b2 ? 99 : 1; // DOMContentLoaded must fire before load
-    };
+    function makeListener(node, type, expectTarget, capture) {
+      function inner(thisArg, event) {
+ if (capture && event.target && event.target.nodeName === 'SCRIPT') {
+          // ignore captured script node load events
+          return;
+        }

-    window.addEventListener('DOMContentLoaded', setb1);
-    window.addEventListener('load', setb2);
-    window.addEventListener('DOMContentLoaded', setb3);
-    window.removeEventListener('DOMContentLoaded', setb1);
+        var str = event.type + ' @ ' + node + ' phase ' + event.eventPhase;
+        console.log(str);
+        document.getElementById('testLoadEvents')
+            .appendChild(document.createElement('div'))
+            .appendChild(document.createTextNode(str));
+
+        // done before asserts so overall events check can also run
+ var tshort = thisArg === window ? 'w' : thisArg === document ? 'd' :
+            thisArg;
+        events.push([tshort, event.type, event.eventPhase, capture]);

-    jsunitRegister('testDOMContentLoadedListener',
-                   function testDOMContentLoadedListener() {
-      assertAsynchronousRequirement('loaded',
-          function() { return !!b2; },
-          function() {
-        assertEquals('b1', 0, b1);
-        assertEquals('b2', 1, b2);
-        assertEquals('b3', 1, b3);
+        assertTrue('event phase',
+            capture ? event.eventPhase !== 3 : event.eventPhase !== 1);
+        assertEquals('event type', type, event.type);
+        assertTrue('listener this (' + thisArg + ')', thisArg === node);
+        assertTrue('event target (' + event.target + ')',
+            event.target === expectTarget);
+      }
+      // jsunitCallback doesn't pass 'this' and even if it did it would be
+      // through the membrane
+      var callback = jsunitCallback(inner, 'testLoadEvents');
+      return function(event) { return callback(this, event); };
+    }
+    function record(node, expectTarget, type) {
+      [true, false].forEach(function(capture) {
+        node.addEventListener(
+            type,
+            makeListener(node, type, expectTarget, capture),
+            capture);
       });
-      pass('testDOMContentLoadedListener');
+    }
+
+    window.onload = function(event) {
+      try {
+        events.push([this === window ? 'w' : this, event.type,
+            event.eventPhase, 'handler']);
+      } catch (e) {
+        fail('testLoadEvents', e);
+      }
+    };
+
+    record(window, document, 'DOMContentLoaded');
+    record(document, document, 'DOMContentLoaded');
+    record(window, window, 'load');
+    record(document, window, 'load');
+  })();
+</script>
+
+<p class="testcontainer" id="testRemoveSpecialHandler">
+  testRemoveSpecialHandler
+</p>
+<script type="text/javascript">
+  (function() {
+    var nok = false;
+    function h() {
+      nok = true;
+    }
+    window.addEventListener('load', h);
+    window.removeEventListener('load', h);
+
+    jsunitRegister('testRemoveSpecialHandler',
+                   function testRemoveSpecialHandler() {
+      assertFalse(nok);
+      pass('testRemoveSpecialHandler');
     });
   })();
 </script>
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-foreign.js Fri Jun 21 14:57:00 2013 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-foreign.js Wed Jul 24 15:19:27 2013
@@ -39,7 +39,7 @@

       extraImports.getEmbeddedForeignNode = function() {
         var node = createTestDiv();
-        guestDiv.firstChild.firstChild.appendChild(node);
+        guestDiv.getElementsByTagName('caja-v-body')[0].appendChild(node);
         return frame.domicile.tameNodeAsForeign(node);
       };
       extraImports.getEmbeddedForeignNode.i___ =
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-global.js Fri Jun 21 14:57:00 2013 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-global.js Wed Jul 24 15:19:27 2013
@@ -278,7 +278,7 @@
         '<html><head> <script>;</script></head>'
             + '<body><div>2</div></body></html>',
         function(frame) {
-          var doc = frame.div.firstChild.firstChild;
+          var doc = frame.innerContainer;
           var html = canonInnerHtml(doc.innerHTML);
           assertEquals(
             '<caja-v-html>'
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-domado-special-guest.html Thu Jul 11 15:50:37 2013 +++ /trunk/tests/com/google/caja/plugin/es53-test-domado-special-guest.html Wed Jul 24 15:19:27 2013
@@ -425,7 +425,7 @@
     }

     function windowStyleWidth() {
- var w = parseInt(directAccess.getComputedStyle(directAccess.getBodyNode(), + var w = parseInt(directAccess.getComputedStyle(directAccess.getVdocNode(),
           'width'), 10);

// Firefox and Chrome are inconsistent about whether the computed style
@@ -478,7 +478,7 @@
     // that behavior?
     //assertEquals(
     //    'hidden',
- // directAccess.getComputedStyle(directAccess.getBodyNode(), 'overflow')); + // directAccess.getComputedStyle(directAccess.getVdocNode(), 'overflow'));

     window.scrollBy(0, 0);
     assertEquals(initialViewPort, getViewport());
@@ -486,24 +486,24 @@
     // that behavior?
     //assertEquals(
     //    'hidden',
- // directAccess.getComputedStyle(directAccess.getBodyNode(), 'overflow')); + // directAccess.getComputedStyle(directAccess.getVdocNode(), 'overflow'));

     window.scrollTo(0, 10);
     assertEquals(
         'auto',
- directAccess.getComputedStyle(directAccess.getBodyNode(), 'overflow')); + directAccess.getComputedStyle(directAccess.getVdocNode(), 'overflow'));
     assertEquals('0,10,0,10', getViewport());

     window.scrollBy(10, -5);
     assertEquals(
         'auto',
- directAccess.getComputedStyle(directAccess.getBodyNode(), 'overflow')); + directAccess.getComputedStyle(directAccess.getVdocNode(), 'overflow'));
     assertEquals('10,5,10,5', getViewport());

     document.defaultView.scrollTo(0, 0);
     assertEquals(
         'auto',
- directAccess.getComputedStyle(directAccess.getBodyNode(), 'overflow')); + directAccess.getComputedStyle(directAccess.getVdocNode(), 'overflow'));
     assertEquals('0,0,0,0', getViewport());

     pass('testScrolling');
=======================================
--- /trunk/tests/com/google/caja/plugin/third-party-tests.json Mon Jul 22 14:48:17 2013 +++ /trunk/tests/com/google/caja/plugin/third-party-tests.json Wed Jul 24 15:19:27 2013
@@ -57,7 +57,7 @@
       },
       {
         "guest": "attributes",
-        "expected-pass": 428,
+        "expected-pass": 464,
         "comment": [
           "Current failure categories:",
             "https://code.google.com/p/google-caja/issues/detail?id=1787";,
@@ -74,7 +74,7 @@
       },
       {
         "guest": "event",
-        "expected-pass": 376,
+        "expected-pass": 378,
         "comment": [
           "Current failure categories:",
             "https://code.google.com/p/google-caja/issues/detail?id=1199";,
@@ -105,7 +105,7 @@
       },
       {
         "guest": "manipulation",
-        "expected-pass": { "firefox": 588, "chrome": 559 },
+        "expected-pass": { "firefox": 574, "chrome": 559 },
         "comment": [
           "Current modifications made to test suite:",
"Removed SES-incompatible Array.prototype modification; was only",
@@ -125,7 +125,7 @@
       },
       {
         "guest": "css",
-        "expected-pass": 240,
+        "expected-pass": 241,
         "comment": [
           "Current failure categories:",
             "Not yet examined."
@@ -149,7 +149,7 @@
       },
       {
         "guest": "effects",
-        "expected-pass": 552,
+        "expected-pass": { "firefox": 552, "chrome": 551 },
         "comment": [
           "Current failure categories:",
             "fill-opacity SVG-only CSS property."
@@ -218,7 +218,7 @@
       },
       {
         "guest": "dialog",
-        "expected-pass": { "firefox": 219, "chrome": 226 },
+        "expected-pass": { "firefox": 290, "chrome": 305 },
         "comment": [
           "Current failure categories:",
             "What may be event simulation failures",
@@ -255,7 +255,7 @@
       },
       {
         "guest": "slider",
-        "expected-pass": { "firefox": 92, "chrome": 123 },
+        "expected-pass": { "firefox": 93, "chrome": 124 },
         "comment": [
           "Current failure categories:",
             "Keyboard events not working"

--

--- You received this message because you are subscribed to the Google Groups "Google Caja Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/groups/opt_out.


Reply via email to