// 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; } } };