summaryrefslogtreecommitdiffstats
path: root/toolkit/components/addoncompat/RemoteAddonsChild.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/addoncompat/RemoteAddonsChild.jsm')
-rw-r--r--toolkit/components/addoncompat/RemoteAddonsChild.jsm576
1 files changed, 576 insertions, 0 deletions
diff --git a/toolkit/components/addoncompat/RemoteAddonsChild.jsm b/toolkit/components/addoncompat/RemoteAddonsChild.jsm
new file mode 100644
index 000000000..1aacc7f7a
--- /dev/null
+++ b/toolkit/components/addoncompat/RemoteAddonsChild.jsm
@@ -0,0 +1,576 @@
+// 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 = ["RemoteAddonsChild"];
+
+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/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prefetcher",
+ "resource://gre/modules/Prefetcher.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "SystemPrincipal",
+ "@mozilla.org/systemprincipal;1", "nsIPrincipal");
+
+XPCOMUtils.defineLazyServiceGetter(this, "contentSecManager",
+ "@mozilla.org/contentsecuritymanager;1",
+ "nsIContentSecurityManager");
+
+// 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.
+//
+// In the child, clients can watch for changes to all paths that start
+// with a given component.
+var NotificationTracker = {
+ init: function() {
+ let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ cpmm.addMessageListener("Addons:ChangeNotification", this);
+ this._paths = cpmm.initialProcessData.remoteAddonsNotificationPaths;
+ this._registered = new Map();
+ this._watchers = {};
+ },
+
+ receiveMessage: function(msg) {
+ let path = msg.data.path;
+ let count = msg.data.count;
+
+ let tracked = this._paths;
+ for (let component of path) {
+ tracked = setDefault(tracked, component, {});
+ }
+
+ tracked._count = count;
+
+ if (this._watchers[path[0]]) {
+ for (let watcher of this._watchers[path[0]]) {
+ this.runCallback(watcher, path, count);
+ }
+ }
+ },
+
+ runCallback: function(watcher, path, count) {
+ let pathString = path.join("/");
+ let registeredSet = this._registered.get(watcher);
+ let registered = registeredSet.has(pathString);
+ if (count && !registered) {
+ watcher.track(path, true);
+ registeredSet.add(pathString);
+ } else if (!count && registered) {
+ watcher.track(path, false);
+ registeredSet.delete(pathString);
+ }
+ },
+
+ findPaths: function(prefix) {
+ if (!this._paths) {
+ return [];
+ }
+
+ let tracked = this._paths;
+ for (let component of prefix) {
+ tracked = setDefault(tracked, component, {});
+ }
+
+ let result = [];
+ let enumerate = (tracked, curPath) => {
+ for (let component in tracked) {
+ if (component == "_count") {
+ result.push([curPath, tracked._count]);
+ } else {
+ let path = curPath.slice();
+ if (component === "true") {
+ component = true;
+ } else if (component === "false") {
+ component = false;
+ }
+ path.push(component);
+ enumerate(tracked[component], path);
+ }
+ }
+ }
+ enumerate(tracked, prefix);
+
+ return result;
+ },
+
+ findSuffixes: function(prefix) {
+ let paths = this.findPaths(prefix);
+ return paths.map(([path, count]) => path[path.length - 1]);
+ },
+
+ watch: function(component1, watcher) {
+ setDefault(this._watchers, component1, []).push(watcher);
+ this._registered.set(watcher, new Set());
+
+ let paths = this.findPaths([component1]);
+ for (let [path, count] of paths) {
+ this.runCallback(watcher, path, count);
+ }
+ },
+
+ unwatch: function(component1, watcher) {
+ let watchers = this._watchers[component1];
+ let index = watchers.lastIndexOf(watcher);
+ if (index > -1) {
+ watchers.splice(index, 1);
+ }
+
+ this._registered.delete(watcher);
+ },
+
+ getCount(component1) {
+ return this.findPaths([component1]).length;
+ },
+};
+
+// This code registers an nsIContentPolicy in the child process. When
+// it runs, it notifies the parent that it needs to run its own
+// nsIContentPolicy list. If any policy in the parent rejects a
+// resource load, that answer is returned to the child.
+var ContentPolicyChild = {
+ _classDescription: "Addon shim content policy",
+ _classID: Components.ID("6e869130-635c-11e2-bcfd-0800200c9a66"),
+ _contractID: "@mozilla.org/addon-child/policy;1",
+
+ init: function() {
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(this._classID, this._classDescription, this._contractID, this);
+
+ NotificationTracker.watch("content-policy", this);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver,
+ Ci.nsIChannelEventSink, Ci.nsIFactory,
+ Ci.nsISupportsWeakReference]),
+
+ track: function(path, register) {
+ let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
+ if (register) {
+ catMan.addCategoryEntry("content-policy", this._contractID, this._contractID, false, true);
+ } else {
+ catMan.deleteCategoryEntry("content-policy", this._contractID, false);
+ }
+ },
+
+ shouldLoad: function(contentType, contentLocation, requestOrigin,
+ node, mimeTypeGuess, extra, requestPrincipal) {
+ let addons = NotificationTracker.findSuffixes(["content-policy"]);
+ let [prefetched, cpows] = Prefetcher.prefetch("ContentPolicy.shouldLoad",
+ addons, {InitNode: node});
+ cpows.node = node;
+
+ let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ let rval = cpmm.sendRpcMessage("Addons:ContentPolicy:Run", {
+ contentType: contentType,
+ contentLocation: contentLocation.spec,
+ requestOrigin: requestOrigin ? requestOrigin.spec : null,
+ mimeTypeGuess: mimeTypeGuess,
+ requestPrincipal: requestPrincipal,
+ prefetched: prefetched,
+ }, cpows);
+ if (rval.length != 1) {
+ return Ci.nsIContentPolicy.ACCEPT;
+ }
+
+ return rval[0];
+ },
+
+ shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra) {
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+
+ createInstance: function(outer, iid) {
+ if (outer) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ return this.QueryInterface(iid);
+ },
+};
+
+// This is a shim channel whose only purpose is to return some string
+// data from an about: protocol handler.
+function AboutProtocolChannel(uri, contractID, loadInfo)
+{
+ this.URI = uri;
+ this.originalURI = uri;
+ this._contractID = contractID;
+ this._loadingPrincipal = loadInfo.loadingPrincipal;
+ this._securityFlags = loadInfo.securityFlags;
+ this._contentPolicyType = loadInfo.externalContentPolicyType;
+}
+
+AboutProtocolChannel.prototype = {
+ contentCharset: "utf-8",
+ contentLength: 0,
+ owner: SystemPrincipal,
+ securityInfo: null,
+ notificationCallbacks: null,
+ loadFlags: 0,
+ loadGroup: null,
+ name: null,
+ status: Cr.NS_OK,
+
+ asyncOpen: function(listener, context) {
+ // Ask the parent to synchronously read all the data from the channel.
+ let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ let rval = cpmm.sendRpcMessage("Addons:AboutProtocol:OpenChannel", {
+ uri: this.URI.spec,
+ contractID: this._contractID,
+ loadingPrincipal: this._loadingPrincipal,
+ securityFlags: this._securityFlags,
+ contentPolicyType: this._contentPolicyType
+ }, {
+ notificationCallbacks: this.notificationCallbacks,
+ loadGroupNotificationCallbacks: this.loadGroup ? this.loadGroup.notificationCallbacks : null,
+ });
+
+ if (rval.length != 1) {
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ let {data, contentType} = rval[0];
+ this.contentType = contentType;
+
+ // Return the data via an nsIStringInputStream.
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
+ stream.setData(data, data.length);
+
+ let runnable = {
+ run: () => {
+ try {
+ listener.onStartRequest(this, context);
+ } catch (e) {}
+ try {
+ listener.onDataAvailable(this, context, stream, 0, stream.available());
+ } catch (e) {}
+ try {
+ listener.onStopRequest(this, context, Cr.NS_OK);
+ } catch (e) {}
+ }
+ };
+ Services.tm.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
+ },
+
+ asyncOpen2: function(listener) {
+ // throws an error if security checks fail
+ var outListener = contentSecManager.performSecurityCheck(this, listener);
+ this.asyncOpen(outListener, null);
+ },
+
+ open: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ open2: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ isPending: function() {
+ return false;
+ },
+
+ cancel: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ suspend: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ resume: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
+};
+
+// This shim protocol handler is used when content fetches an about: URL.
+function AboutProtocolInstance(contractID)
+{
+ this._contractID = contractID;
+ this._uriFlags = undefined;
+}
+
+AboutProtocolInstance.prototype = {
+ createInstance: function(outer, iid) {
+ if (outer != null) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+
+ return this.QueryInterface(iid);
+ },
+
+ getURIFlags: function(uri) {
+ // Cache the result to avoid the extra IPC.
+ if (this._uriFlags !== undefined) {
+ return this._uriFlags;
+ }
+
+ let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+
+ let rval = cpmm.sendRpcMessage("Addons:AboutProtocol:GetURIFlags", {
+ uri: uri.spec,
+ contractID: this._contractID
+ });
+
+ if (rval.length != 1) {
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ this._uriFlags = rval[0];
+ return this._uriFlags;
+ },
+
+ // We take some shortcuts here. Ideally, we would return a CPOW that
+ // wraps the add-on's nsIChannel. However, many of the methods
+ // related to nsIChannel are marked [noscript], so they're not
+ // available to CPOWs. Consequently, we return a shim channel that,
+ // when opened, asks the parent to open the channel and read out all
+ // the data.
+ newChannel: function(uri, loadInfo) {
+ return new AboutProtocolChannel(uri, this._contractID, loadInfo);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule])
+};
+
+var AboutProtocolChild = {
+ _classDescription: "Addon shim about: protocol handler",
+
+ init: function() {
+ // Maps contractIDs to instances
+ this._instances = new Map();
+ // Maps contractIDs to classIDs
+ this._classIDs = new Map();
+ NotificationTracker.watch("about-protocol", this);
+ },
+
+ track: function(path, register) {
+ let contractID = path[1];
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ if (register) {
+ let instance = new AboutProtocolInstance(contractID);
+ let classID = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator)
+ .generateUUID();
+
+ this._instances.set(contractID, instance);
+ this._classIDs.set(contractID, classID);
+ registrar.registerFactory(classID, this._classDescription, contractID, instance);
+ } else {
+ let instance = this._instances.get(contractID);
+ let classID = this._classIDs.get(contractID);
+ registrar.unregisterFactory(classID, instance);
+ this._instances.delete(contractID);
+ this._classIDs.delete(contractID);
+ }
+ },
+};
+
+// This code registers observers in the child whenever an add-on in
+// the parent asks for notifications on the given topic.
+var ObserverChild = {
+ init: function() {
+ NotificationTracker.watch("observer", this);
+ },
+
+ track: function(path, register) {
+ let topic = path[1];
+ if (register) {
+ Services.obs.addObserver(this, topic, false);
+ } else {
+ Services.obs.removeObserver(this, topic);
+ }
+ },
+
+ observe: function(subject, topic, data) {
+ let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ cpmm.sendRpcMessage("Addons:Observer:Run", {}, {
+ topic: topic,
+ subject: subject,
+ data: data
+ });
+ }
+};
+
+// 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;
+ this.capturingHandler = (event) => this.handleEvent(true, event);
+ this.nonCapturingHandler = (event) => this.handleEvent(false, event);
+ NotificationTracker.watch("event", this);
+}
+
+EventTargetChild.prototype = {
+ uninit: function() {
+ NotificationTracker.unwatch("event", this);
+ },
+
+ track: function(path, register) {
+ let eventType = path[1];
+ let useCapture = path[2];
+ let listener = useCapture ? this.capturingHandler : this.nonCapturingHandler;
+ if (register) {
+ this._childGlobal.addEventListener(eventType, listener, useCapture, true);
+ } else {
+ this._childGlobal.removeEventListener(eventType, listener, useCapture);
+ }
+ },
+
+ handleEvent: function(capturing, event) {
+ let addons = NotificationTracker.findSuffixes(["event", event.type, capturing]);
+ let [prefetched, cpows] = Prefetcher.prefetch("EventTarget.handleEvent",
+ addons,
+ {Event: event,
+ Window: this._childGlobal.content});
+ cpows.event = event;
+ cpows.eventTarget = event.target;
+
+ this._childGlobal.sendRpcMessage("Addons:Event:Run",
+ {type: event.type,
+ capturing: capturing,
+ isTrusted: event.isTrusted,
+ prefetched: prefetched},
+ cpows);
+ }
+};
+
+// The parent can create a sandbox to run code in the child
+// process. We actually create the sandbox in the child so that the
+// code runs there. However, managing the lifetime of these sandboxes
+// can be tricky. The parent references these sandboxes using CPOWs,
+// which only keep weak references. So we need to create a strong
+// reference in the child. For simplicity, we kill off these strong
+// references whenever we navigate away from the page for which the
+// sandbox was created.
+function SandboxChild(chromeGlobal)
+{
+ this.chromeGlobal = chromeGlobal;
+ this.sandboxes = [];
+}
+
+SandboxChild.prototype = {
+ uninit: function() {
+ this.clearSandboxes();
+ },
+
+ addListener: function() {
+ let webProgress = this.chromeGlobal.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+ },
+
+ removeListener: function() {
+ let webProgress = this.chromeGlobal.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.removeProgressListener(this);
+ },
+
+ onLocationChange: function(webProgress, request, location, flags) {
+ this.clearSandboxes();
+ },
+
+ addSandbox: function(sandbox) {
+ if (this.sandboxes.length == 0) {
+ this.addListener();
+ }
+ this.sandboxes.push(sandbox);
+ },
+
+ clearSandboxes: function() {
+ if (this.sandboxes.length) {
+ this.removeListener();
+ }
+ this.sandboxes = [];
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference])
+};
+
+var RemoteAddonsChild = {
+ _ready: false,
+
+ makeReady: function() {
+ let shims = [
+ Prefetcher,
+ NotificationTracker,
+ ContentPolicyChild,
+ AboutProtocolChild,
+ ObserverChild,
+ ];
+
+ for (let shim of shims) {
+ try {
+ shim.init();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ init: function(global) {
+
+ if (!this._ready) {
+ if (!Services.cpmm.initialProcessData.remoteAddonsParentInitted) {
+ return null;
+ }
+
+ this.makeReady();
+ this._ready = true;
+ }
+
+ global.sendAsyncMessage("Addons:RegisterGlobal", {}, {global: global});
+
+ let sandboxChild = new SandboxChild(global);
+ global.addSandbox = sandboxChild.addSandbox.bind(sandboxChild);
+
+ // Return this so it gets rooted in the content script.
+ return [new EventTargetChild(global), sandboxChild];
+ },
+
+ uninit: function(perTabShims) {
+ for (let shim of perTabShims) {
+ try {
+ shim.uninit();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ get useSyncWebProgress() {
+ return NotificationTracker.getCount("web-progress") > 0;
+ },
+};