diff options
Diffstat (limited to 'toolkit/components/addoncompat/RemoteAddonsChild.jsm')
-rw-r--r-- | toolkit/components/addoncompat/RemoteAddonsChild.jsm | 576 |
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; + }, +}; |