summaryrefslogtreecommitdiff
path: root/components/addoncompat/RemoteAddonsParent.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'components/addoncompat/RemoteAddonsParent.jsm')
-rw-r--r--components/addoncompat/RemoteAddonsParent.jsm1080
1 files changed, 1080 insertions, 0 deletions
diff --git a/components/addoncompat/RemoteAddonsParent.jsm b/components/addoncompat/RemoteAddonsParent.jsm
new file mode 100644
index 000000000..5cadc2902
--- /dev/null
+++ b/components/addoncompat/RemoteAddonsParent.jsm
@@ -0,0 +1,1080 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+this.EXPORTED_SYMBOLS = ["RemoteAddonsParent"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/RemoteWebProgress.jsm");
+Cu.import('resource://gre/modules/Services.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prefetcher",
+ "resource://gre/modules/Prefetcher.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CompatWarning",
+ "resource://gre/modules/CompatWarning.jsm");
+
+Cu.permitCPOWsInScope(this);
+
+// Similar to Python. Returns dict[key] if it exists. Otherwise,
+// sets dict[key] to default_ and returns default_.
+function setDefault(dict, key, default_)
+{
+ if (key in dict) {
+ return dict[key];
+ }
+ dict[key] = default_;
+ return default_;
+}
+
+// This code keeps track of a set of paths of the form [component_1,
+// ..., component_n]. The components can be strings or booleans. The
+// child is notified whenever a path is added or removed, and new
+// children can request the current set of paths. The purpose is to
+// keep track of all the observers and events that the child should
+// monitor for the parent.
+var NotificationTracker = {
+ // _paths is a multi-level dictionary. Let's add paths [A, B] and
+ // [A, C]. Then _paths will look like this:
+ // { 'A': { 'B': { '_count': 1 }, 'C': { '_count': 1 } } }
+ // Each component in a path will be a key in some dictionary. At the
+ // end, the _count property keeps track of how many instances of the
+ // given path are present in _paths.
+ _paths: {},
+
+ init: function() {
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.initialProcessData.remoteAddonsNotificationPaths = this._paths;
+ },
+
+ add: function(path) {
+ let tracked = this._paths;
+ for (let component of path) {
+ tracked = setDefault(tracked, component, {});
+ }
+ let count = tracked._count || 0;
+ count++;
+ tracked._count = count;
+
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.broadcastAsyncMessage("Addons:ChangeNotification", {path: path, count: count});
+ },
+
+ remove: function(path) {
+ let tracked = this._paths;
+ for (let component of path) {
+ tracked = setDefault(tracked, component, {});
+ }
+ tracked._count--;
+
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.broadcastAsyncMessage("Addons:ChangeNotification", {path: path, count: tracked._count});
+ },
+};
+NotificationTracker.init();
+
+// An interposition is an object with three properties: methods,
+// getters, and setters. See multiprocessShims.js for an explanation
+// of how these are used. The constructor here just allows one
+// interposition to inherit members from another.
+function Interposition(name, base)
+{
+ this.name = name;
+ if (base) {
+ this.methods = Object.create(base.methods);
+ this.getters = Object.create(base.getters);
+ this.setters = Object.create(base.setters);
+ } else {
+ this.methods = Object.create(null);
+ this.getters = Object.create(null);
+ this.setters = Object.create(null);
+ }
+}
+
+// This object is responsible for notifying the child when a new
+// content policy is added or removed. It also runs all the registered
+// add-on content policies when the child asks it to do so.
+var ContentPolicyParent = {
+ init: function() {
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.addMessageListener("Addons:ContentPolicy:Run", this);
+
+ this._policies = new Map();
+ },
+
+ addContentPolicy: function(addon, name, cid) {
+ this._policies.set(name, cid);
+ NotificationTracker.add(["content-policy", addon]);
+ },
+
+ removeContentPolicy: function(addon, name) {
+ this._policies.delete(name);
+ NotificationTracker.remove(["content-policy", addon]);
+ },
+
+ receiveMessage: function (aMessage) {
+ switch (aMessage.name) {
+ case "Addons:ContentPolicy:Run":
+ return this.shouldLoad(aMessage.data, aMessage.objects);
+ }
+ return undefined;
+ },
+
+ shouldLoad: function(aData, aObjects) {
+ for (let policyCID of this._policies.values()) {
+ let policy;
+ try {
+ policy = Cc[policyCID].getService(Ci.nsIContentPolicy);
+ } catch (e) {
+ // Current Gecko behavior is to ignore entries that don't QI.
+ continue;
+ }
+ try {
+ let contentLocation = BrowserUtils.makeURI(aData.contentLocation);
+ let requestOrigin = aData.requestOrigin ? BrowserUtils.makeURI(aData.requestOrigin) : null;
+
+ let result = Prefetcher.withPrefetching(aData.prefetched, aObjects, () => {
+ return policy.shouldLoad(aData.contentType,
+ contentLocation,
+ requestOrigin,
+ aObjects.node,
+ aData.mimeTypeGuess,
+ null,
+ aData.requestPrincipal);
+ });
+ if (result != Ci.nsIContentPolicy.ACCEPT && result != 0)
+ return result;
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+};
+ContentPolicyParent.init();
+
+// This interposition intercepts calls to add or remove new content
+// policies and forwards these requests to ContentPolicyParent.
+var CategoryManagerInterposition = new Interposition("CategoryManagerInterposition");
+
+CategoryManagerInterposition.methods.addCategoryEntry =
+ function(addon, target, category, entry, value, persist, replace) {
+ if (category == "content-policy") {
+ CompatWarning.warn("content-policy should be added from the child process only.",
+ addon, CompatWarning.warnings.nsIContentPolicy);
+ ContentPolicyParent.addContentPolicy(addon, entry, value);
+ }
+
+ target.addCategoryEntry(category, entry, value, persist, replace);
+ };
+
+CategoryManagerInterposition.methods.deleteCategoryEntry =
+ function(addon, target, category, entry, persist) {
+ if (category == "content-policy") {
+ CompatWarning.warn("content-policy should be removed from the child process only.",
+ addon, CompatWarning.warnings.nsIContentPolicy);
+ ContentPolicyParent.removeContentPolicy(addon, entry);
+ }
+
+ target.deleteCategoryEntry(category, entry, persist);
+ };
+
+// This shim handles the case where an add-on registers an about:
+// protocol handler in the parent and we want the child to be able to
+// use it. This code is pretty specific to Adblock's usage.
+var AboutProtocolParent = {
+ init: function() {
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.addMessageListener("Addons:AboutProtocol:GetURIFlags", this);
+ ppmm.addMessageListener("Addons:AboutProtocol:OpenChannel", this);
+ this._protocols = [];
+ },
+
+ registerFactory: function(addon, class_, className, contractID, factory) {
+ this._protocols.push({contractID: contractID, factory: factory});
+ NotificationTracker.add(["about-protocol", contractID, addon]);
+ },
+
+ unregisterFactory: function(addon, class_, factory) {
+ for (let i = 0; i < this._protocols.length; i++) {
+ if (this._protocols[i].factory == factory) {
+ NotificationTracker.remove(["about-protocol", this._protocols[i].contractID, addon]);
+ this._protocols.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ receiveMessage: function (msg) {
+ switch (msg.name) {
+ case "Addons:AboutProtocol:GetURIFlags":
+ return this.getURIFlags(msg);
+ case "Addons:AboutProtocol:OpenChannel":
+ return this.openChannel(msg);
+ }
+ return undefined;
+ },
+
+ getURIFlags: function(msg) {
+ let uri = BrowserUtils.makeURI(msg.data.uri);
+ let contractID = msg.data.contractID;
+ let module = Cc[contractID].getService(Ci.nsIAboutModule);
+ try {
+ return module.getURIFlags(uri);
+ } catch (e) {
+ Cu.reportError(e);
+ return undefined;
+ }
+ },
+
+ // We immediately read all the data out of the channel here and
+ // return it to the child.
+ openChannel: function(msg) {
+ function wrapGetInterface(cpow) {
+ return {
+ getInterface: function(intf) { return cpow.getInterface(intf); }
+ };
+ }
+
+ let uri = BrowserUtils.makeURI(msg.data.uri);
+ let channelParams;
+ if (msg.data.contentPolicyType === Ci.nsIContentPolicy.TYPE_DOCUMENT) {
+ // For TYPE_DOCUMENT loads, we cannot recreate the loadinfo here in the
+ // parent. In that case, treat this as a chrome (addon)-requested
+ // subload. When we use the data in the child, we'll load it into the
+ // correctly-principaled document.
+ channelParams = {
+ uri,
+ contractID: msg.data.contractID,
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
+ };
+ } else {
+ // We can recreate the loadinfo here in the parent for non TYPE_DOCUMENT
+ // loads.
+ channelParams = {
+ uri,
+ contractID: msg.data.contractID,
+ loadingPrincipal: msg.data.loadingPrincipal,
+ securityFlags: msg.data.securityFlags,
+ contentPolicyType: msg.data.contentPolicyType
+ };
+ }
+
+ try {
+ let channel = NetUtil.newChannel(channelParams);
+
+ // We're not allowed to set channel.notificationCallbacks to a
+ // CPOW, since the setter for notificationCallbacks is in C++,
+ // which can't tolerate CPOWs. Instead we just use a JS object
+ // that wraps the CPOW.
+ channel.notificationCallbacks = wrapGetInterface(msg.objects.notificationCallbacks);
+ if (msg.objects.loadGroupNotificationCallbacks) {
+ channel.loadGroup = {notificationCallbacks: msg.objects.loadGroupNotificationCallbacks};
+ } else {
+ channel.loadGroup = null;
+ }
+ let stream = channel.open2();
+ let data = NetUtil.readInputStreamToString(stream, stream.available(), {});
+ return {
+ data: data,
+ contentType: channel.contentType
+ };
+ } catch (e) {
+ Cu.reportError(e);
+ return undefined;
+ }
+ },
+};
+AboutProtocolParent.init();
+
+var ComponentRegistrarInterposition = new Interposition("ComponentRegistrarInterposition");
+
+ComponentRegistrarInterposition.methods.registerFactory =
+ function(addon, target, class_, className, contractID, factory) {
+ if (contractID && contractID.startsWith("@mozilla.org/network/protocol/about;1?")) {
+ CompatWarning.warn("nsIAboutModule should be registered in the content process" +
+ " as well as the chrome process. (If you do that already, ignore" +
+ " this warning.)",
+ addon, CompatWarning.warnings.nsIAboutModule);
+ AboutProtocolParent.registerFactory(addon, class_, className, contractID, factory);
+ }
+
+ target.registerFactory(class_, className, contractID, factory);
+ };
+
+ComponentRegistrarInterposition.methods.unregisterFactory =
+ function(addon, target, class_, factory) {
+ AboutProtocolParent.unregisterFactory(addon, class_, factory);
+ target.unregisterFactory(class_, factory);
+ };
+
+// This object manages add-on observers that might fire in the child
+// process. Rather than managing the observers itself, it uses the
+// parent's observer service. When an add-on listens on topic T,
+// ObserverParent asks the child process to listen on T. It also adds
+// an observer in the parent for the topic e10s-T. When the T observer
+// fires in the child, the parent fires all the e10s-T observers,
+// passing them CPOWs for the subject and data. We don't want to use T
+// in the parent because there might be non-add-on T observers that
+// won't expect to get notified in this case.
+var ObserverParent = {
+ init: function() {
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.addMessageListener("Addons:Observer:Run", this);
+ },
+
+ addObserver: function(addon, observer, topic, ownsWeak) {
+ Services.obs.addObserver(observer, "e10s-" + topic, ownsWeak);
+ NotificationTracker.add(["observer", topic, addon]);
+ },
+
+ removeObserver: function(addon, observer, topic) {
+ Services.obs.removeObserver(observer, "e10s-" + topic);
+ NotificationTracker.remove(["observer", topic, addon]);
+ },
+
+ receiveMessage: function(msg) {
+ switch (msg.name) {
+ case "Addons:Observer:Run":
+ this.notify(msg.objects.subject, msg.objects.topic, msg.objects.data);
+ break;
+ }
+ },
+
+ notify: function(subject, topic, data) {
+ let e = Services.obs.enumerateObservers("e10s-" + topic);
+ while (e.hasMoreElements()) {
+ let obs = e.getNext().QueryInterface(Ci.nsIObserver);
+ try {
+ obs.observe(subject, topic, data);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+};
+ObserverParent.init();
+
+// We only forward observers for these topics.
+var TOPIC_WHITELIST = [
+ "content-document-global-created",
+ "document-element-inserted",
+ "dom-window-destroyed",
+ "inner-window-destroyed",
+ "outer-window-destroyed",
+ "csp-on-violate-policy",
+];
+
+// This interposition listens for
+// nsIObserverService.{add,remove}Observer.
+var ObserverInterposition = new Interposition("ObserverInterposition");
+
+ObserverInterposition.methods.addObserver =
+ function(addon, target, observer, topic, ownsWeak) {
+ if (TOPIC_WHITELIST.indexOf(topic) >= 0) {
+ CompatWarning.warn(`${topic} observer should be added from the child process only.`,
+ addon, CompatWarning.warnings.observers);
+
+ ObserverParent.addObserver(addon, observer, topic);
+ }
+
+ target.addObserver(observer, topic, ownsWeak);
+ };
+
+ObserverInterposition.methods.removeObserver =
+ function(addon, target, observer, topic) {
+ if (TOPIC_WHITELIST.indexOf(topic) >= 0) {
+ ObserverParent.removeObserver(addon, observer, topic);
+ }
+
+ target.removeObserver(observer, topic);
+ };
+
+// This object is responsible for forwarding events from the child to
+// the parent.
+var 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;
+ }
+
+ 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 (window && 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(addon, target, type, listener, useCapture, wantsUntrusted, delayedWarning) {
+ let newTarget = this.redirectEventTarget(target);
+ if (!newTarget) {
+ return;
+ }
+
+ useCapture = useCapture || false;
+ wantsUntrusted = wantsUntrusted || false;
+
+ NotificationTracker.add(["event", type, useCapture, addon]);
+
+ 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].target === target &&
+ forType[i].useCapture === useCapture &&
+ forType[i].wantsUntrusted === wantsUntrusted) {
+ return;
+ }
+ }
+
+ forType.push({listener: listener,
+ target: target,
+ wantsUntrusted: wantsUntrusted,
+ useCapture: useCapture,
+ delayedWarning: delayedWarning});
+ },
+
+ removeEventListener: function(addon, 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].target === target &&
+ forType[i].useCapture === useCapture) {
+ forType.splice(i, 1);
+ NotificationTracker.remove(["event", type, useCapture, addon]);
+ break;
+ }
+ }
+ },
+
+ receiveMessage: function(msg) {
+ switch (msg.name) {
+ case "Addons:Event:Run":
+ this.dispatch(msg.target, msg.data.type, msg.data.capturing,
+ msg.data.isTrusted, msg.data.prefetched, msg.objects);
+ break;
+ }
+ },
+
+ dispatch: function(browser, type, capturing, isTrusted, prefetched, cpows) {
+ let event = cpows.event;
+ let eventTarget = cpows.eventTarget;
+ let targets = this.getTargets(browser);
+ for (let target of targets) {
+ let listeners = this._listeners.get(target);
+ if (!listeners) {
+ continue;
+ }
+ let forType = setDefault(listeners, type, []);
+
+ // Make a copy in case they call removeEventListener in the listener.
+ let handlers = [];
+ for (let {listener, target, wantsUntrusted, useCapture, delayedWarning} of forType) {
+ if ((wantsUntrusted || isTrusted) && useCapture == capturing) {
+ // Issue a warning for this listener.
+ delayedWarning();
+
+ handlers.push([listener, target]);
+ }
+ }
+
+ for (let [handler, target] of handlers) {
+ let EventProxy = {
+ get: function(knownProps, name) {
+ if (knownProps.hasOwnProperty(name))
+ return knownProps[name];
+ return event[name];
+ }
+ }
+ let proxyEvent = new Proxy({
+ currentTarget: target,
+ target: eventTarget,
+ type: type,
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIDOMEventTarget))
+ return proxyEvent;
+ // If event deson't support the interface this will throw. If it
+ // does we want to return the proxy
+ event.QueryInterface(iid);
+ return proxyEvent;
+ }
+ }, EventProxy);
+
+ try {
+ Prefetcher.withPrefetching(prefetched, cpows, () => {
+ if ("handleEvent" in handler) {
+ handler.handleEvent(proxyEvent);
+ } else {
+ handler.call(eventTarget, proxyEvent);
+ }
+ });
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+ }
+};
+EventTargetParent.init();
+
+// This function returns a listener that will not fire on events where
+// the target is a remote xul:browser element itself. We'd rather let
+// the child process handle the event and pass it up via
+// EventTargetParent.
+var filteringListeners = new WeakMap();
+function makeFilteringListener(eventType, listener)
+{
+ // Some events are actually targeted at the <browser> element
+ // itself, so we only handle the ones where know that won't happen.
+ let eventTypes = ["mousedown", "mouseup", "click"];
+ if (!eventTypes.includes(eventType) || !listener ||
+ (typeof listener != "object" && typeof listener != "function")) {
+ return listener;
+ }
+
+ if (filteringListeners.has(listener)) {
+ return filteringListeners.get(listener);
+ }
+
+ function filter(event) {
+ let target = event.originalTarget;
+ if (target instanceof Ci.nsIDOMXULElement &&
+ target.localName == "browser" &&
+ target.isRemoteBrowser) {
+ return;
+ }
+
+ if ("handleEvent" in listener) {
+ listener.handleEvent(event);
+ } else {
+ listener.call(event.target, event);
+ }
+ }
+ filteringListeners.set(listener, filter);
+ return filter;
+}
+
+// This interposition redirects addEventListener and
+// removeEventListener to EventTargetParent.
+var EventTargetInterposition = new Interposition("EventTargetInterposition");
+
+EventTargetInterposition.methods.addEventListener =
+ function(addon, target, type, listener, useCapture, wantsUntrusted) {
+ let delayed = CompatWarning.delayedWarning(
+ `Registering a ${type} event listener on content DOM nodes` +
+ " needs to happen in the content process.",
+ addon, CompatWarning.warnings.DOM_events);
+
+ EventTargetParent.addEventListener(addon, target, type, listener, useCapture, wantsUntrusted, delayed);
+ target.addEventListener(type, makeFilteringListener(type, listener), useCapture, wantsUntrusted);
+ };
+
+EventTargetInterposition.methods.removeEventListener =
+ function(addon, target, type, listener, useCapture) {
+ EventTargetParent.removeEventListener(addon, target, type, listener, useCapture);
+ target.removeEventListener(type, makeFilteringListener(type, listener), useCapture);
+ };
+
+// This interposition intercepts accesses to |rootTreeItem| on a child
+// process docshell. In the child, each docshell is its own
+// root. However, add-ons expect the root to be the chrome docshell,
+// so we make that happen here.
+var ContentDocShellTreeItemInterposition = new Interposition("ContentDocShellTreeItemInterposition");
+
+ContentDocShellTreeItemInterposition.getters.rootTreeItem =
+ function(addon, target) {
+ // The chrome global in the child.
+ let chromeGlobal = target.rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+
+ // Map it to a <browser> element and window.
+ let browser = RemoteAddonsParent.globalToBrowser.get(chromeGlobal);
+ if (!browser) {
+ // Somehow we have a CPOW from the child, but it hasn't sent us
+ // its global yet. That shouldn't happen, but return null just
+ // in case.
+ return null;
+ }
+
+ let chromeWin = browser.ownerDocument.defaultView;
+
+ // Return that window's docshell.
+ return chromeWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem);
+ };
+
+function chromeGlobalForContentWindow(window)
+{
+ return window
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+}
+
+// This object manages sandboxes created with content principals in
+// the parent. We actually create these sandboxes in the child process
+// so that the code loaded into them runs there. The resulting sandbox
+// object is a CPOW. This is primarly useful for Greasemonkey.
+var SandboxParent = {
+ componentsMap: new WeakMap(),
+
+ makeContentSandbox: function(addon, chromeGlobal, principals, ...rest) {
+ CompatWarning.warn("This sandbox should be created from the child process.",
+ addon, CompatWarning.warnings.sandboxes);
+ if (rest.length) {
+ // Do a shallow copy of the options object into the child
+ // process. This way we don't have to access it through a Chrome
+ // object wrapper, which would require __exposedProps__.
+ //
+ // The only object property here is sandboxPrototype. We assume
+ // it's a child process object (since that's what Greasemonkey
+ // does) and leave it alone.
+ let options = rest[0];
+ let optionsCopy = new chromeGlobal.Object();
+ for (let prop in options) {
+ optionsCopy[prop] = options[prop];
+ }
+ rest[0] = optionsCopy;
+ }
+
+ // Make a sandbox in the child.
+ let cu = chromeGlobal.Components.utils;
+ let sandbox = cu.Sandbox(principals, ...rest);
+
+ // We need to save the sandbox in the child so it won't get
+ // GCed. The child will drop this reference at the next
+ // navigation.
+ chromeGlobal.addSandbox(sandbox);
+
+ // The sandbox CPOW will be kept alive by whomever we return it
+ // to. Its lifetime is unrelated to that of the sandbox object in
+ // the child.
+ this.componentsMap.set(sandbox, cu);
+ return sandbox;
+ },
+
+ evalInSandbox: function(code, sandbox, ...rest) {
+ let cu = this.componentsMap.get(sandbox);
+ return cu.evalInSandbox(code, sandbox, ...rest);
+ }
+};
+
+// This interposition redirects calls to Cu.Sandbox and
+// Cu.evalInSandbox to SandboxParent if the principals are content
+// principals.
+var ComponentsUtilsInterposition = new Interposition("ComponentsUtilsInterposition");
+
+ComponentsUtilsInterposition.methods.Sandbox =
+ function(addon, target, principals, ...rest) {
+ // principals can be a window object, a list of window objects, or
+ // something else (a string, for example).
+ if (principals &&
+ typeof(principals) == "object" &&
+ Cu.isCrossProcessWrapper(principals) &&
+ principals instanceof Ci.nsIDOMWindow) {
+ let chromeGlobal = chromeGlobalForContentWindow(principals);
+ return SandboxParent.makeContentSandbox(addon, chromeGlobal, principals, ...rest);
+ } else if (principals &&
+ typeof(principals) == "object" &&
+ "every" in principals &&
+ principals.length &&
+ principals.every(e => e instanceof Ci.nsIDOMWindow && Cu.isCrossProcessWrapper(e))) {
+ let chromeGlobal = chromeGlobalForContentWindow(principals[0]);
+
+ // The principals we pass to the content process must use an
+ // Array object from the content process.
+ let array = new chromeGlobal.Array();
+ for (let i = 0; i < principals.length; i++) {
+ array[i] = principals[i];
+ }
+ return SandboxParent.makeContentSandbox(addon, chromeGlobal, array, ...rest);
+ }
+ return Components.utils.Sandbox(principals, ...rest);
+ };
+
+ComponentsUtilsInterposition.methods.evalInSandbox =
+ function(addon, target, code, sandbox, ...rest) {
+ if (sandbox && Cu.isCrossProcessWrapper(sandbox)) {
+ return SandboxParent.evalInSandbox(code, sandbox, ...rest);
+ }
+ return Components.utils.evalInSandbox(code, sandbox, ...rest);
+ };
+
+// This interposition handles cases where an add-on tries to import a
+// chrome XUL node into a content document. It doesn't actually do the
+// import, which we can't support. It just avoids throwing an
+// exception.
+var ContentDocumentInterposition = new Interposition("ContentDocumentInterposition");
+
+ContentDocumentInterposition.methods.importNode =
+ function(addon, target, node, deep) {
+ if (!Cu.isCrossProcessWrapper(node)) {
+ // Trying to import a node from the parent process into the
+ // child process. We don't support this now. Video Download
+ // Helper does this in domhook-service.js to add a XUL
+ // popupmenu to content.
+ Cu.reportError("Calling contentDocument.importNode on a XUL node is not allowed.");
+ return node;
+ }
+
+ return target.importNode(node, deep);
+ };
+
+// This interposition ensures that calling browser.docShell from an
+// add-on returns a CPOW around the dochell.
+var RemoteBrowserElementInterposition = new Interposition("RemoteBrowserElementInterposition",
+ EventTargetInterposition);
+
+RemoteBrowserElementInterposition.getters.docShell = function(addon, target) {
+ CompatWarning.warn("Direct access to content docshell will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+ let remoteChromeGlobal = RemoteAddonsParent.browserToGlobal.get(target);
+ if (!remoteChromeGlobal) {
+ // We may not have any messages from this tab yet.
+ return null;
+ }
+ return remoteChromeGlobal.docShell;
+};
+
+RemoteBrowserElementInterposition.getters.sessionHistory = function(addon, target) {
+ CompatWarning.warn("Direct access to browser.sessionHistory will no longer " +
+ "work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ return getSessionHistory(target);
+}
+
+// We use this in place of the real browser.contentWindow if we
+// haven't yet received a CPOW for the child process's window. This
+// happens if the tab has just started loading.
+function makeDummyContentWindow(browser) {
+ let dummyContentWindow = {
+ set location(url) {
+ browser.loadURI(url, null, null);
+ },
+ document: {
+ readyState: "loading",
+ location: { href: "about:blank" }
+ },
+ frames: [],
+ };
+ dummyContentWindow.top = dummyContentWindow;
+ dummyContentWindow.document.defaultView = dummyContentWindow;
+ browser._contentWindow = dummyContentWindow;
+ return dummyContentWindow;
+}
+
+RemoteBrowserElementInterposition.getters.contentWindow = function(addon, target) {
+ CompatWarning.warn("Direct access to browser.contentWindow will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ // If we don't have a CPOW yet, just return something we can use for
+ // setting the location. This is useful for tests that create a tab
+ // and immediately set contentWindow.location.
+ if (!target.contentWindowAsCPOW) {
+ CompatWarning.warn("CPOW to the content window does not exist yet, dummy content window is created.");
+ return makeDummyContentWindow(target);
+ }
+ return target.contentWindowAsCPOW;
+};
+
+function getContentDocument(addon, browser)
+{
+ if (!browser.contentWindowAsCPOW) {
+ return makeDummyContentWindow(browser).document;
+ }
+
+ let doc = Prefetcher.lookupInCache(addon, browser.contentWindowAsCPOW, "document");
+ if (doc) {
+ return doc;
+ }
+
+ return browser.contentWindowAsCPOW.document;
+}
+
+function getSessionHistory(browser) {
+ let remoteChromeGlobal = RemoteAddonsParent.browserToGlobal.get(browser);
+ if (!remoteChromeGlobal) {
+ CompatWarning.warn("CPOW for the remote browser docShell hasn't been received yet.");
+ // We may not have any messages from this tab yet.
+ return null;
+ }
+ return remoteChromeGlobal.docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
+}
+
+RemoteBrowserElementInterposition.getters.contentDocument = function(addon, target) {
+ CompatWarning.warn("Direct access to browser.contentDocument will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ return getContentDocument(addon, target);
+};
+
+var TabBrowserElementInterposition = new Interposition("TabBrowserElementInterposition",
+ EventTargetInterposition);
+
+TabBrowserElementInterposition.getters.contentWindow = function(addon, target) {
+ CompatWarning.warn("Direct access to gBrowser.contentWindow will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ if (!target.selectedBrowser.contentWindowAsCPOW) {
+ return makeDummyContentWindow(target.selectedBrowser);
+ }
+ return target.selectedBrowser.contentWindowAsCPOW;
+};
+
+TabBrowserElementInterposition.getters.contentDocument = function(addon, target) {
+ CompatWarning.warn("Direct access to gBrowser.contentDocument will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ let browser = target.selectedBrowser;
+ return getContentDocument(addon, browser);
+};
+
+TabBrowserElementInterposition.getters.sessionHistory = function(addon, target) {
+ CompatWarning.warn("Direct access to gBrowser.sessionHistory will no " +
+ "longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+ let browser = target.selectedBrowser;
+ if (!browser.isRemoteBrowser) {
+ return browser.sessionHistory;
+ }
+ return getSessionHistory(browser);
+};
+
+// This function returns a wrapper around an
+// nsIWebProgressListener. When the wrapper is invoked, it calls the
+// real listener but passes CPOWs for the nsIWebProgress and
+// nsIRequest arguments.
+var progressListeners = {global: new WeakMap(), tabs: new WeakMap()};
+function wrapProgressListener(kind, listener)
+{
+ if (progressListeners[kind].has(listener)) {
+ return progressListeners[kind].get(listener);
+ }
+
+ let ListenerHandler = {
+ get: function(target, name) {
+ if (name.startsWith("on")) {
+ return function(...args) {
+ listener[name].apply(listener, RemoteWebProgressManager.argumentsForAddonListener(kind, args));
+ };
+ }
+
+ return listener[name];
+ }
+ };
+ let listenerProxy = new Proxy(listener, ListenerHandler);
+
+ progressListeners[kind].set(listener, listenerProxy);
+ return listenerProxy;
+}
+
+TabBrowserElementInterposition.methods.addProgressListener = function(addon, target, listener) {
+ if (!target.ownerDocument.defaultView.gMultiProcessBrowser) {
+ return target.addProgressListener(listener);
+ }
+
+ NotificationTracker.add(["web-progress", addon]);
+ return target.addProgressListener(wrapProgressListener("global", listener));
+};
+
+TabBrowserElementInterposition.methods.removeProgressListener = function(addon, target, listener) {
+ if (!target.ownerDocument.defaultView.gMultiProcessBrowser) {
+ return target.removeProgressListener(listener);
+ }
+
+ NotificationTracker.remove(["web-progress", addon]);
+ return target.removeProgressListener(wrapProgressListener("global", listener));
+};
+
+TabBrowserElementInterposition.methods.addTabsProgressListener = function(addon, target, listener) {
+ if (!target.ownerDocument.defaultView.gMultiProcessBrowser) {
+ return target.addTabsProgressListener(listener);
+ }
+
+ NotificationTracker.add(["web-progress", addon]);
+ return target.addTabsProgressListener(wrapProgressListener("tabs", listener));
+};
+
+TabBrowserElementInterposition.methods.removeTabsProgressListener = function(addon, target, listener) {
+ if (!target.ownerDocument.defaultView.gMultiProcessBrowser) {
+ return target.removeTabsProgressListener(listener);
+ }
+
+ NotificationTracker.remove(["web-progress", addon]);
+ return target.removeTabsProgressListener(wrapProgressListener("tabs", listener));
+};
+
+var ChromeWindowInterposition = new Interposition("ChromeWindowInterposition",
+ EventTargetInterposition);
+
+// _content is for older add-ons like pinboard and all-in-one gestures
+// that should be using content instead.
+ChromeWindowInterposition.getters.content =
+ChromeWindowInterposition.getters._content = function(addon, target) {
+ CompatWarning.warn("Direct access to chromeWindow.content will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ let browser = target.gBrowser.selectedBrowser;
+ if (!browser.contentWindowAsCPOW) {
+ return makeDummyContentWindow(browser);
+ }
+ return browser.contentWindowAsCPOW;
+};
+
+var RemoteWebNavigationInterposition = new Interposition("RemoteWebNavigation");
+
+RemoteWebNavigationInterposition.getters.sessionHistory = function(addon, target) {
+ CompatWarning.warn("Direct access to webNavigation.sessionHistory will no longer " +
+ "work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ if (target instanceof Ci.nsIDocShell) {
+ // We must have a non-remote browser, so we can go ahead
+ // and just return the real sessionHistory.
+ return target.sessionHistory;
+ }
+
+ let impl = target.wrappedJSObject;
+ if (!impl) {
+ return null;
+ }
+
+ let browser = impl._browser;
+
+ return getSessionHistory(browser);
+}
+
+var RemoteAddonsParent = {
+ init: function() {
+ let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
+ mm.addMessageListener("Addons:RegisterGlobal", this);
+
+ Services.ppmm.initialProcessData.remoteAddonsParentInitted = true;
+
+ this.globalToBrowser = new WeakMap();
+ this.browserToGlobal = new WeakMap();
+ },
+
+ getInterfaceInterpositions: function() {
+ let result = {};
+
+ function register(intf, interp) {
+ result[intf.number] = interp;
+ }
+
+ register(Ci.nsICategoryManager, CategoryManagerInterposition);
+ register(Ci.nsIComponentRegistrar, ComponentRegistrarInterposition);
+ register(Ci.nsIObserverService, ObserverInterposition);
+ register(Ci.nsIXPCComponents_Utils, ComponentsUtilsInterposition);
+ register(Ci.nsIWebNavigation, RemoteWebNavigationInterposition);
+
+ return result;
+ },
+
+ getTaggedInterpositions: function() {
+ let result = {};
+
+ function register(tag, interp) {
+ result[tag] = interp;
+ }
+
+ register("EventTarget", EventTargetInterposition);
+ register("ContentDocShellTreeItem", ContentDocShellTreeItemInterposition);
+ register("ContentDocument", ContentDocumentInterposition);
+ register("RemoteBrowserElement", RemoteBrowserElementInterposition);
+ register("TabBrowserElement", TabBrowserElementInterposition);
+ register("ChromeWindow", ChromeWindowInterposition);
+
+ return result;
+ },
+
+ receiveMessage: function(msg) {
+ switch (msg.name) {
+ case "Addons:RegisterGlobal":
+ this.browserToGlobal.set(msg.target, msg.objects.global);
+ this.globalToBrowser.set(msg.objects.global, msg.target);
+ break;
+ }
+ }
+};