Bug 1031609 - Event target shims (r=mconley,smaug)
authorBill McCloskey <wmccloskey@mozilla.com>
Mon, 14 Jul 2014 22:10:06 -0700
changeset 214776 8723969ebaec6f57c1ff0d74effbcb40c0b2111d
parent 214775 2edb6251c7952fe7c483d970dcb0fc3da81c6df7
child 214777 1e14af130411bc004e94f2208ba129e38c33946f
push id3857
push userraliiev@mozilla.com
push dateTue, 02 Sep 2014 16:39:23 +0000
treeherdermozilla-beta@5638b907b505 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, smaug
bugs1031609
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1031609 - Event target shims (r=mconley,smaug)
toolkit/components/addoncompat/RemoteAddonsChild.jsm
toolkit/components/addoncompat/RemoteAddonsParent.jsm
--- a/toolkit/components/addoncompat/RemoteAddonsChild.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsChild.jsm
@@ -173,12 +173,41 @@ let ObserverChild = {
       topic: topic,
       subject: subject,
       data: data
     });
   }
 };
 ObserverChild.init();
 
+// There is one of these objects per browser tab in the child. When an
+// add-on in the parent listens for an event, this child object
+// listens for that event in the child.
+function EventTargetChild(childGlobal)
+{
+  this._childGlobal = childGlobal;
+  NotificationTracker.watch("event", (path, count) => this.track(path, count));
+}
+
+EventTargetChild.prototype = {
+  track: function(path, count) {
+    let eventType = path[1];
+    let useCapture = path[2];
+    if (count) {
+      this._childGlobal.addEventListener(eventType, this, useCapture, true);
+    } else {
+      this._childGlobal.removeEventListener(eventType, this, useCapture);
+    }
+  },
+
+  handleEvent: function(event) {
+    this._childGlobal.sendRpcMessage("Addons:Event:Run",
+                                     {type: event.type, isTrusted: event.isTrusted},
+                                     {event: event});
+  }
+};
+
 let RemoteAddonsChild = {
   init: function(global) {
+    // Return this so it gets rooted in the content script.
+    return [new EventTargetChild(global)];
   },
 };
--- a/toolkit/components/addoncompat/RemoteAddonsParent.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
@@ -241,16 +241,174 @@ ObserverInterposition.methods.removeObse
   function(addon, target, observer, topic) {
     if (TOPIC_WHITELIST.indexOf(topic) >= 0) {
       ObserverParent.removeObserver(observer, topic);
     }
 
     target.removeObserver(observer, topic);
   };
 
+// This object is responsible for forwarding events from the child to
+// the parent.
+let EventTargetParent = {
+  init: function() {
+    // The _listeners map goes from targets (either <browser> elements
+    // or windows) to a dictionary from event types to listeners.
+    this._listeners = new WeakMap();
+
+    let mm = Cc["@mozilla.org/globalmessagemanager;1"].
+      getService(Ci.nsIMessageListenerManager);
+    mm.addMessageListener("Addons:Event:Run", this);
+  },
+
+  // If target is not on the path from a <browser> element to the
+  // window root, then we return null here to ignore the
+  // target. Otherwise, if the target is a browser-specific element
+  // (the <browser> or <tab> elements), then we return the
+  // <browser>. If it's some generic element, then we return the
+  // window itself.
+  redirectEventTarget: function(target) {
+    if (Cu.isCrossProcessWrapper(target)) {
+      return null;
+    }
+
+    if (target instanceof Ci.nsIDOMChromeWindow) {
+      return target;
+    }
+
+    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+    if (target instanceof Ci.nsIDOMXULElement) {
+      if (target.localName == "browser") {
+        return target;
+      } else if (target.localName == "tab") {
+        return target.linkedBrowser;
+      }
+
+      // Check if |target| is somewhere on the patch from the
+      // <tabbrowser> up to the root element.
+      let window = target.ownerDocument.defaultView;
+      if (target.contains(window.gBrowser)) {
+        return window;
+      }
+    }
+
+    return null;
+  },
+
+  // When a given event fires in the child, we fire it on the
+  // <browser> element and the window since those are the two possible
+  // results of redirectEventTarget.
+  getTargets: function(browser) {
+    let window = browser.ownerDocument.defaultView;
+    return [browser, window];
+  },
+
+  addEventListener: function(target, type, listener, useCapture, wantsUntrusted) {
+    let newTarget = this.redirectEventTarget(target);
+    if (!newTarget) {
+      return;
+    }
+
+    useCapture = useCapture || false;
+    wantsUntrusted = wantsUntrusted || false;
+
+    NotificationTracker.add(["event", type, useCapture]);
+
+    let listeners = this._listeners.get(newTarget);
+    if (!listeners) {
+      listeners = {};
+      this._listeners.set(newTarget, listeners);
+    }
+    let forType = setDefault(listeners, type, []);
+
+    // If there's already an identical listener, don't do anything.
+    for (let i = 0; i < forType.length; i++) {
+      if (forType[i].listener === listener &&
+          forType[i].useCapture === useCapture &&
+          forType[i].wantsUntrusted === wantsUntrusted) {
+        return;
+      }
+    }
+
+    forType.push({listener: listener, wantsUntrusted: wantsUntrusted, useCapture: useCapture});
+  },
+
+  removeEventListener: function(target, type, listener, useCapture) {
+    let newTarget = this.redirectEventTarget(target);
+    if (!newTarget) {
+      return;
+    }
+
+    useCapture = useCapture || false;
+
+    let listeners = this._listeners.get(newTarget);
+    if (!listeners) {
+      return;
+    }
+    let forType = setDefault(listeners, type, []);
+
+    for (let i = 0; i < forType.length; i++) {
+      if (forType[i].listener === listener && forType[i].useCapture === useCapture) {
+        forType.splice(i, 1);
+        NotificationTracker.remove(["event", type, useCapture]);
+        break;
+      }
+    }
+  },
+
+  receiveMessage: function(msg) {
+    switch (msg.name) {
+      case "Addons:Event:Run":
+        this.dispatch(msg.target, msg.data.type, msg.data.isTrusted, msg.objects.event);
+        break;
+    }
+  },
+
+  dispatch: function(browser, type, isTrusted, event) {
+    let targets = this.getTargets(browser);
+    for (target of targets) {
+      let listeners = this._listeners.get(target);
+      if (!listeners) {
+        continue;
+      }
+      let forType = setDefault(listeners, type, []);
+      for (let {listener, wantsUntrusted} of forType) {
+        if (wantsUntrusted || isTrusted) {
+          try {
+            if ("handleEvent" in listener) {
+              listener.handleEvent(event);
+            } else {
+              listener.call(event.target, event);
+            }
+          } catch (e) {
+            Cu.reportError(e);
+          }
+        }
+      }
+    }
+  }
+};
+EventTargetParent.init();
+
+// This interposition redirects addEventListener and
+// removeEventListener to EventTargetParent.
+let EventTargetInterposition = new Interposition();
+
+EventTargetInterposition.methods.addEventListener =
+  function(addon, target, type, listener, useCapture, wantsUntrusted) {
+    EventTargetParent.addEventListener(target, type, listener, useCapture, wantsUntrusted);
+    target.addEventListener(type, listener, useCapture, wantsUntrusted);
+  };
+
+EventTargetInterposition.methods.removeEventListener =
+  function(addon, target, type, listener, useCapture) {
+    EventTargetParent.removeEventListener(target, type, listener, useCapture);
+    target.removeEventListener(type, listener, useCapture);
+  };
+
 let RemoteAddonsParent = {
   init: function() {
   },
 
   getInterfaceInterpositions: function() {
     let result = {};
 
     function register(intf, interp) {
@@ -259,11 +417,19 @@ let RemoteAddonsParent = {
 
     register(Ci.nsICategoryManager, CategoryManagerInterposition);
     register(Ci.nsIObserverService, ObserverInterposition);
 
     return result;
   },
 
   getTaggedInterpositions: function() {
-    return {};
+    let result = {};
+
+    function register(tag, interp) {
+      result[tag] = interp;
+    }
+
+    register("EventTarget", EventTargetInterposition);
+
+    return result;
   },
 };