tenfourfox/toolkit/components/addoncompat/RemoteAddonsChild.jsm
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

578 lines
17 KiB
JavaScript

// 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);
let [paths] = cpmm.sendSyncMessage("Addons:GetNotifications");
this._paths = paths;
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;
},
};