summaryrefslogtreecommitdiffstats
path: root/toolkit/components/addoncompat
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/addoncompat')
-rw-r--r--toolkit/components/addoncompat/CompatWarning.jsm107
-rw-r--r--toolkit/components/addoncompat/Prefetcher.jsm557
-rw-r--r--toolkit/components/addoncompat/RemoteAddonsChild.jsm576
-rw-r--r--toolkit/components/addoncompat/RemoteAddonsParent.jsm1080
-rw-r--r--toolkit/components/addoncompat/ShimWaiver.jsm15
-rw-r--r--toolkit/components/addoncompat/addoncompat.manifest4
-rw-r--r--toolkit/components/addoncompat/defaultShims.js39
-rw-r--r--toolkit/components/addoncompat/moz.build21
-rw-r--r--toolkit/components/addoncompat/multiprocessShims.js182
-rw-r--r--toolkit/components/addoncompat/tests/addon/bootstrap.js653
-rw-r--r--toolkit/components/addoncompat/tests/addon/chrome.manifest1
-rw-r--r--toolkit/components/addoncompat/tests/addon/content/page.html2
-rw-r--r--toolkit/components/addoncompat/tests/addon/install.rdf37
-rw-r--r--toolkit/components/addoncompat/tests/browser/.eslintrc.js7
-rw-r--r--toolkit/components/addoncompat/tests/browser/addon.xpibin0 -> 10761 bytes
-rw-r--r--toolkit/components/addoncompat/tests/browser/browser.ini9
-rw-r--r--toolkit/components/addoncompat/tests/browser/browser_addonShims.js67
-rw-r--r--toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage.html17
-rw-r--r--toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage2.html16
-rw-r--r--toolkit/components/addoncompat/tests/browser/compat-addon.xpibin0 -> 5692 bytes
-rw-r--r--toolkit/components/addoncompat/tests/compat-addon/bootstrap.js99
-rw-r--r--toolkit/components/addoncompat/tests/compat-addon/install.rdf37
-rw-r--r--toolkit/components/addoncompat/tests/moz.build7
23 files changed, 3533 insertions, 0 deletions
diff --git a/toolkit/components/addoncompat/CompatWarning.jsm b/toolkit/components/addoncompat/CompatWarning.jsm
new file mode 100644
index 000000000..b32409a46
--- /dev/null
+++ b/toolkit/components/addoncompat/CompatWarning.jsm
@@ -0,0 +1,107 @@
+// 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 = ["CompatWarning"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+
+function section(number, url)
+{
+ const baseURL = "https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Limitations_of_chrome_scripts";
+ return { number, url: baseURL + url };
+}
+
+var CompatWarning = {
+ // Sometimes we want to generate a warning, but put off issuing it
+ // until later. For example, if someone registers a listener, we
+ // might only want to warn about it if the listener actually
+ // fires. However, we want the warning to show a stack for the
+ // registration site.
+ delayedWarning: function(msg, addon, warning) {
+ function isShimLayer(filename) {
+ return filename.indexOf("CompatWarning.jsm") != -1 ||
+ filename.indexOf("RemoteAddonsParent.jsm") != -1 ||
+ filename.indexOf("RemoteAddonsChild.jsm") != -1 ||
+ filename.indexOf("multiprocessShims.js") != -1;
+ }
+
+ let stack = Components.stack;
+ while (stack && isShimLayer(stack.filename))
+ stack = stack.caller;
+
+ let alreadyWarned = false;
+
+ return function() {
+ if (alreadyWarned) {
+ return;
+ }
+ alreadyWarned = true;
+
+ if (addon) {
+ let histogram = Services.telemetry.getKeyedHistogramById("ADDON_SHIM_USAGE");
+ histogram.add(addon, warning ? warning.number : 0);
+ }
+
+ if (!Preferences.get("dom.ipc.shims.enabledWarnings", false))
+ return;
+
+ let error = Cc['@mozilla.org/scripterror;1'].createInstance(Ci.nsIScriptError);
+ if (!error || !Services.console) {
+ // Too late during shutdown to use the nsIConsole
+ return;
+ }
+
+ let message = `Warning: ${msg}`;
+ if (warning)
+ message += `\nMore info at: ${warning.url}`;
+
+ error.init(
+ /* message*/ message,
+ /* sourceName*/ stack ? stack.filename : "",
+ /* sourceLine*/ stack ? stack.sourceLine : "",
+ /* lineNumber*/ stack ? stack.lineNumber : 0,
+ /* columnNumber*/ 0,
+ /* flags*/ Ci.nsIScriptError.warningFlag,
+ /* category*/ "chrome javascript");
+ Services.console.logMessage(error);
+
+ if (Preferences.get("dom.ipc.shims.dumpWarnings", false)) {
+ dump(message + "\n");
+ while (stack) {
+ dump(stack + "\n");
+ stack = stack.caller;
+ }
+ dump("\n");
+ }
+ };
+ },
+
+ warn: function(msg, addon, warning) {
+ let delayed = this.delayedWarning(msg, addon, warning);
+ delayed();
+ },
+
+ warnings: {
+ content: section(1, "#gBrowser.contentWindow.2C_window.content..."),
+ limitations_of_CPOWs: section(2, "#Limitations_of_CPOWs"),
+ nsIContentPolicy: section(3, "#nsIContentPolicy"),
+ nsIWebProgressListener: section(4, "#nsIWebProgressListener"),
+ observers: section(5, "#Observers_in_the_chrome_process"),
+ DOM_events: section(6, "#DOM_Events"),
+ sandboxes: section(7, "#Sandboxes"),
+ JSMs: section(8, "#JavaScript_code_modules_(JSMs)"),
+ nsIAboutModule: section(9, "#nsIAboutModule"),
+ // If more than 14 values appear here, you need to change the
+ // ADDON_SHIM_USAGE histogram definition in Histograms.json.
+ },
+};
diff --git a/toolkit/components/addoncompat/Prefetcher.jsm b/toolkit/components/addoncompat/Prefetcher.jsm
new file mode 100644
index 000000000..2d836690c
--- /dev/null
+++ b/toolkit/components/addoncompat/Prefetcher.jsm
@@ -0,0 +1,557 @@
+// 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 = ["Prefetcher"];
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+
+// Rules are defined at the bottom of this file.
+var PrefetcherRules = {};
+
+/*
+ * When events that trigger in the content process are forwarded to
+ * add-ons in the chrome process, we expect the add-ons to send a lot
+ * of CPOWs to query content nodes while processing the events. To
+ * speed this up, the prefetching system anticipates which properties
+ * will be read and reads them ahead of time. The prefetched
+ * properties are passed to the chrome process along with each
+ * event. A typical scenario might work like this:
+ *
+ * 1. "load" event fires in content
+ * 2. Content process prefetches:
+ * event.target.defaultView = <win 1>
+ * <win 1>.location = <location obj>
+ * event.target.getElementsByTagName("form") = [<elt 1>, <elt 2>]
+ * <elt 1>.id = "login-form"
+ * <elt 2>.id = "subscribe-form"
+ * 3. Content process forwards "load" event to add-on along with
+ * prefetched data
+ * 4. Add-on reads:
+ * event.target.defaultView (already prefetched)
+ * event.target.getElementsByTagName("form") (already prefetched)
+ * <elt 1>.id (already prefetched)
+ * <elt 1>.className (not prefetched; CPOW must be sent)
+ *
+ * The amount of data to prefetch is determined based on the add-on ID
+ * and the event type. The specific data to select is determined using
+ * a set of Datalog-like rules (http://en.wikipedia.org/wiki/Datalog).
+ *
+ * Rules operate on a series of "tables" like in a database. Each
+ * table contains a set of content-process objects. When an event
+ * handler runs, it seeds some initial tables with objects of
+ * interest. For example, the Event table might start out containing
+ * the event that fired.
+ *
+ * Objects are added to tables using a set of rules of the form "if X
+ * is in table A, then add F(X) to table B", where F(X) is typically a
+ * property access or a method call. The most common functions F are:
+ *
+ * PropertyOp(destTable, sourceTable, property):
+ * For each object X in sourceTable, add X.property to destTable.
+ * MethodOp(destTable, sourceTable, method, args):
+ * For each object X in sourceTable, add X.method(args) to destTable.
+ * CollectionOp(destTable, sourceTable):
+ * For each object X in sourceTable, add X[i] to destTable for
+ * all i from 0 to X.length - 1.
+ *
+ * To generate the prefetching in the example above, the following
+ * rules would work:
+ *
+ * 1. PropertyOp("EventTarget", "Event", "target")
+ * 2. PropertyOp("Window", "EventTarget", "defaultView")
+ * 3. MethodOp("FormCollection", "EventTarget", "getElementsByTagName", "form")
+ * 4. CollectionOp("Form", "FormCollection")
+ * 5. PropertyOp(null, "Form", "id")
+ *
+ * Rules are written at the bottom of this file.
+ *
+ * When a rule runs, it will usually generate some cache entries that
+ * will be passed to the chrome process. For example, when PropertyOp
+ * prefetches obj.prop and gets the value X, it caches the value of
+ * obj and X. When the chrome process receives this data, it creates a
+ * two-level map [obj -> [prop -> X]]. When the add-on accesses a
+ * property on obj, the add-on shim code consults this map to see if
+ * the property has already been cached.
+ */
+
+const PREF_PREFETCHING_ENABLED = "extensions.interposition.prefetching";
+
+function isPrimitive(v) {
+ if (!v)
+ return true;
+ let type = typeof(v);
+ return type !== "object" && type !== "function";
+}
+
+function objAddr(obj)
+{
+/*
+ if (!isPrimitive(obj)) {
+ return String(obj) + "[" + Cu.getJSTestingFunctions().objectAddress(obj) + "]";
+ }
+ return String(obj);
+*/
+}
+
+function log(/* ...args*/)
+{
+/*
+ for (let arg of args) {
+ dump(arg);
+ dump(" ");
+ }
+ dump("\n");
+*/
+}
+
+function logPrefetch(/* kind, value1, component, value2*/)
+{
+/*
+ log("prefetching", kind, objAddr(value1) + "." + component, "=", objAddr(value2));
+*/
+}
+
+/*
+ * All the Op classes (representing Datalog rules) have the same interface:
+ * outputTable: Table that objects generated by the rule are added to.
+ * Note that this can be null.
+ * inputTable: Table that the rule draws objects from.
+ * addObject(database, obj): Called when an object is added to inputTable.
+ * This code should take care of adding objects to outputTable.
+ * Data to be cached should be stored by calling database.cache.
+ * makeCacheEntry(item, cache):
+ * Called by the chrome process to create the two-level map of
+ * prefetched objects. |item| holds the cached data
+ * generated by the content process. |cache| is the map to be
+ * generated.
+ */
+
+function PropertyOp(outputTable, inputTable, prop)
+{
+ this.outputTable = outputTable;
+ this.inputTable = inputTable;
+ this.prop = prop;
+}
+
+PropertyOp.prototype.addObject = function(database, obj)
+{
+ let has = false, propValue;
+ try {
+ if (this.prop in obj) {
+ has = true;
+ propValue = obj[this.prop];
+ }
+ } catch (e) {
+ // Don't cache anything if an exception is thrown.
+ return;
+ }
+
+ logPrefetch("prop", obj, this.prop, propValue);
+ database.cache(this.index, obj, has, propValue);
+ if (has && !isPrimitive(propValue) && this.outputTable) {
+ database.add(this.outputTable, propValue);
+ }
+}
+
+PropertyOp.prototype.makeCacheEntry = function(item, cache)
+{
+ let [, obj, , propValue] = item;
+
+ let desc = { configurable: false, enumerable: true, writable: false, value: propValue };
+
+ if (!cache.has(obj)) {
+ cache.set(obj, new Map());
+ }
+ let propMap = cache.get(obj);
+ propMap.set(this.prop, desc);
+}
+
+function MethodOp(outputTable, inputTable, method, ...args)
+{
+ this.outputTable = outputTable;
+ this.inputTable = inputTable;
+ this.method = method;
+ this.args = args;
+}
+
+MethodOp.prototype.addObject = function(database, obj)
+{
+ let result;
+ try {
+ result = obj[this.method].apply(obj, this.args);
+ } catch (e) {
+ // Don't cache anything if an exception is thrown.
+ return;
+ }
+
+ logPrefetch("method", obj, this.method + "(" + this.args + ")", result);
+ database.cache(this.index, obj, result);
+ if (!isPrimitive(result) && this.outputTable) {
+ database.add(this.outputTable, result);
+ }
+}
+
+MethodOp.prototype.makeCacheEntry = function(item, cache)
+{
+ let [, obj, result] = item;
+
+ if (!cache.has(obj)) {
+ cache.set(obj, new Map());
+ }
+ let propMap = cache.get(obj);
+ let fallback = propMap.get(this.method);
+
+ let method = this.method;
+ let selfArgs = this.args;
+ let methodImpl = function(...args) {
+ if (args.length == selfArgs.length && args.every((v, i) => v === selfArgs[i])) {
+ return result;
+ }
+
+ if (fallback) {
+ return fallback.value(...args);
+ }
+ return obj[method](...args);
+ };
+
+ let desc = { configurable: false, enumerable: true, writable: false, value: methodImpl };
+ propMap.set(this.method, desc);
+}
+
+function CollectionOp(outputTable, inputTable)
+{
+ this.outputTable = outputTable;
+ this.inputTable = inputTable;
+}
+
+CollectionOp.prototype.addObject = function(database, obj)
+{
+ let elements = [];
+ try {
+ let len = obj.length;
+ for (let i = 0; i < len; i++) {
+ logPrefetch("index", obj, i, obj[i]);
+ elements.push(obj[i]);
+ }
+ } catch (e) {
+ // Don't cache anything if an exception is thrown.
+ return;
+ }
+
+ database.cache(this.index, obj, ...elements);
+ for (let i = 0; i < elements.length; i++) {
+ if (!isPrimitive(elements[i]) && this.outputTable) {
+ database.add(this.outputTable, elements[i]);
+ }
+ }
+}
+
+CollectionOp.prototype.makeCacheEntry = function(item, cache)
+{
+ let [, obj, ...elements] = item;
+
+ if (!cache.has(obj)) {
+ cache.set(obj, new Map());
+ }
+ let propMap = cache.get(obj);
+
+ let lenDesc = { configurable: false, enumerable: true, writable: false, value: elements.length };
+ propMap.set("length", lenDesc);
+
+ for (let i = 0; i < elements.length; i++) {
+ let desc = { configurable: false, enumerable: true, writable: false, value: elements[i] };
+ propMap.set(i, desc);
+ }
+}
+
+function CopyOp(outputTable, inputTable)
+{
+ this.outputTable = outputTable;
+ this.inputTable = inputTable;
+}
+
+CopyOp.prototype.addObject = function(database, obj)
+{
+ database.add(this.outputTable, obj);
+}
+
+function Database(trigger, addons)
+{
+ // Create a map of rules that apply to this specific trigger and set
+ // of add-ons. The rules are indexed based on their inputTable.
+ this.rules = new Map();
+ for (let addon of addons) {
+ let addonRules = PrefetcherRules[addon] || {};
+ let triggerRules = addonRules[trigger] || [];
+ for (let rule of triggerRules) {
+ let inTable = rule.inputTable;
+ if (!this.rules.has(inTable)) {
+ this.rules.set(inTable, new Set());
+ }
+ let set = this.rules.get(inTable);
+ set.add(rule);
+ }
+ }
+
+ // this.tables maps table names to sets of objects contained in them.
+ this.tables = new Map();
+
+ // todo is a worklist of items added to tables that have not had
+ // rules run on them yet.
+ this.todo = [];
+
+ // Cached data to be sent to the chrome process.
+ this.cached = [];
+}
+
+Database.prototype = {
+ // Add an object to a table.
+ add: function(table, obj) {
+ if (!this.tables.has(table)) {
+ this.tables.set(table, new Set());
+ }
+ let tableSet = this.tables.get(table);
+ if (tableSet.has(obj)) {
+ return;
+ }
+ tableSet.add(obj);
+
+ this.todo.push([table, obj]);
+ },
+
+ cache: function(...args) {
+ this.cached.push(args);
+ },
+
+ // Run a fixed-point iteration that adds objects to table based on
+ // this.rules until there are no more objects to add.
+ process: function() {
+ while (this.todo.length) {
+ let [table, obj] = this.todo.pop();
+ let rules = this.rules.get(table);
+ if (!rules) {
+ continue;
+ }
+ for (let rule of rules) {
+ rule.addObject(this, obj);
+ }
+ }
+ },
+};
+
+var Prefetcher = {
+ init: function() {
+ // Give an index to each rule and store it in this.ruleMap based
+ // on the index. The index is used to serialize and deserialize
+ // data from content to chrome.
+ let counter = 0;
+ this.ruleMap = new Map();
+ for (let addon in PrefetcherRules) {
+ for (let trigger in PrefetcherRules[addon]) {
+ for (let rule of PrefetcherRules[addon][trigger]) {
+ rule.index = counter++;
+ this.ruleMap.set(rule.index, rule);
+ }
+ }
+ }
+
+ this.prefetchingEnabled = Preferences.get(PREF_PREFETCHING_ENABLED, false);
+ Services.prefs.addObserver(PREF_PREFETCHING_ENABLED, this, false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ },
+
+ observe: function(subject, topic, data) {
+ if (topic == "xpcom-shutdown") {
+ Services.prefs.removeObserver(PREF_PREFETCHING_ENABLED, this);
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ } else if (topic == PREF_PREFETCHING_ENABLED) {
+ this.prefetchingEnabled = Preferences.get(PREF_PREFETCHING_ENABLED, false);
+ }
+ },
+
+ // Called when an event occurs in the content process. The event is
+ // described by the trigger string. |addons| is a list of addons
+ // that have listeners installed for the event. |args| is
+ // event-specific data (such as the event object).
+ prefetch: function(trigger, addons, args) {
+ if (!this.prefetchingEnabled) {
+ return [[], []];
+ }
+
+ let db = new Database(trigger, addons);
+ for (let table in args) {
+ log("root", table, "=", objAddr(args[table]));
+ db.add(table, args[table]);
+ }
+
+ // Prefetch objects and add them to tables.
+ db.process();
+
+ // Data passed to sendAsyncMessage must be split into a JSON
+ // portion and a CPOW portion. This code splits apart db.cached
+ // into these two pieces. Any object in db.cache is added to an
+ // array of CPOWs and replaced with {cpow: <index in array>}.
+ let cpowIndexes = new Map();
+ let prefetched = [];
+ let cpows = [];
+ for (let item of db.cached) {
+ item = item.map((elt) => {
+ if (!isPrimitive(elt)) {
+ if (!cpowIndexes.has(elt)) {
+ let index = cpows.length;
+ cpows.push(elt);
+ cpowIndexes.set(elt, index);
+ }
+ return {cpow: cpowIndexes.get(elt)};
+ }
+ return elt;
+ });
+
+ prefetched.push(item);
+ }
+
+ return [prefetched, cpows];
+ },
+
+ cache: null,
+
+ // Generate a two-level mapping based on cached data received from
+ // the content process.
+ generateCache: function(prefetched, cpows) {
+ let cache = new Map();
+ for (let item of prefetched) {
+ // Replace anything of the form {cpow: <index>} with the actual
+ // object in |cpows|.
+ item = item.map((elt) => {
+ if (!isPrimitive(elt)) {
+ return cpows[elt.cpow];
+ }
+ return elt;
+ });
+
+ let index = item[0];
+ let op = this.ruleMap.get(index);
+ op.makeCacheEntry(item, cache);
+ }
+ return cache;
+ },
+
+ // Run |func|, using the prefetched data in |prefetched| and |cpows|
+ // as a cache.
+ withPrefetching: function(prefetched, cpows, func) {
+ if (!this.prefetchingEnabled) {
+ return func();
+ }
+
+ this.cache = this.generateCache(prefetched, cpows);
+
+ try {
+ log("Prefetching on");
+ return func();
+ } finally {
+ // After we return from this event handler, the content process
+ // is free to continue executing, so we invalidate our cache.
+ log("Prefetching off");
+ this.cache = null;
+ }
+ },
+
+ // Called by shim code in the chrome process to check if target.prop
+ // is cached.
+ lookupInCache: function(addon, target, prop) {
+ if (!this.cache || !Cu.isCrossProcessWrapper(target)) {
+ return null;
+ }
+
+ let propMap = this.cache.get(target);
+ if (!propMap) {
+ return null;
+ }
+
+ return propMap.get(prop);
+ },
+};
+
+var AdblockId = "{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}";
+var AdblockRules = {
+ "ContentPolicy.shouldLoad": [
+ new MethodOp("Node", "InitNode", "QueryInterface", Ci.nsISupports),
+ new PropertyOp("Document", "Node", "ownerDocument"),
+ new PropertyOp("Window", "Node", "defaultView"),
+ new PropertyOp("Window", "Document", "defaultView"),
+ new PropertyOp("TopWindow", "Window", "top"),
+ new PropertyOp("WindowLocation", "Window", "location"),
+ new PropertyOp(null, "WindowLocation", "href"),
+ new PropertyOp("Window", "Window", "parent"),
+ new PropertyOp(null, "Window", "name"),
+ new PropertyOp("Document", "Window", "document"),
+ new PropertyOp("TopDocumentElement", "Document", "documentElement"),
+ new MethodOp(null, "TopDocumentElement", "getAttribute", "data-adblockkey"),
+ ]
+};
+PrefetcherRules[AdblockId] = AdblockRules;
+
+var LastpassId = "support@lastpass.com";
+var LastpassRules = {
+ "EventTarget.handleEvent": [
+ new PropertyOp("EventTarget", "Event", "target"),
+ new PropertyOp("EventOriginalTarget", "Event", "originalTarget"),
+ new PropertyOp("Window", "EventOriginalTarget", "defaultView"),
+
+ new CopyOp("Frame", "Window"),
+ new PropertyOp("FrameCollection", "Window", "frames"),
+ new CollectionOp("Frame", "FrameCollection"),
+ new PropertyOp("FrameCollection", "Frame", "frames"),
+ new PropertyOp("FrameDocument", "Frame", "document"),
+ new PropertyOp(null, "Frame", "window"),
+ new PropertyOp(null, "FrameDocument", "defaultView"),
+
+ new PropertyOp("FrameDocumentLocation", "FrameDocument", "location"),
+ new PropertyOp(null, "FrameDocumentLocation", "href"),
+ new PropertyOp("FrameLocation", "Frame", "location"),
+ new PropertyOp(null, "FrameLocation", "href"),
+
+ new MethodOp("FormCollection", "FrameDocument", "getElementsByTagName", "form"),
+ new MethodOp("FormCollection", "FrameDocument", "getElementsByTagName", "FORM"),
+ new CollectionOp("Form", "FormCollection"),
+ new PropertyOp("FormElementCollection", "Form", "elements"),
+ new CollectionOp("FormElement", "FormElementCollection"),
+ new PropertyOp("Style", "Form", "style"),
+
+ new PropertyOp(null, "FormElement", "type"),
+ new PropertyOp(null, "FormElement", "name"),
+ new PropertyOp(null, "FormElement", "value"),
+ new PropertyOp(null, "FormElement", "tagName"),
+ new PropertyOp(null, "FormElement", "id"),
+ new PropertyOp("Style", "FormElement", "style"),
+
+ new PropertyOp(null, "Style", "visibility"),
+
+ new MethodOp("MetaElementsCollection", "EventOriginalTarget", "getElementsByTagName", "meta"),
+ new CollectionOp("MetaElement", "MetaElementsCollection"),
+ new PropertyOp(null, "MetaElement", "httpEquiv"),
+
+ new MethodOp("InputElementCollection", "FrameDocument", "getElementsByTagName", "input"),
+ new MethodOp("InputElementCollection", "FrameDocument", "getElementsByTagName", "INPUT"),
+ new CollectionOp("InputElement", "InputElementCollection"),
+ new PropertyOp(null, "InputElement", "type"),
+ new PropertyOp(null, "InputElement", "name"),
+ new PropertyOp(null, "InputElement", "tagName"),
+ new PropertyOp(null, "InputElement", "form"),
+
+ new PropertyOp("BodyElement", "FrameDocument", "body"),
+ new PropertyOp("BodyInnerText", "BodyElement", "innerText"),
+
+ new PropertyOp("DocumentFormCollection", "FrameDocument", "forms"),
+ new CollectionOp("DocumentForm", "DocumentFormCollection"),
+ ]
+};
+PrefetcherRules[LastpassId] = LastpassRules;
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;
+ },
+};
diff --git a/toolkit/components/addoncompat/RemoteAddonsParent.jsm b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
new file mode 100644
index 000000000..5cadc2902
--- /dev/null
+++ b/toolkit/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;
+ }
+ }
+};
diff --git a/toolkit/components/addoncompat/ShimWaiver.jsm b/toolkit/components/addoncompat/ShimWaiver.jsm
new file mode 100644
index 000000000..402ab4c32
--- /dev/null
+++ b/toolkit/components/addoncompat/ShimWaiver.jsm
@@ -0,0 +1,15 @@
+// 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 = ["ShimWaiver"];
+
+this.ShimWaiver = {
+ getProperty: function(obj, prop) {
+ let rv = obj[prop];
+ if (rv instanceof Function) {
+ rv = rv.bind(obj);
+ }
+ return rv;
+ }
+};
diff --git a/toolkit/components/addoncompat/addoncompat.manifest b/toolkit/components/addoncompat/addoncompat.manifest
new file mode 100644
index 000000000..fe38f47d8
--- /dev/null
+++ b/toolkit/components/addoncompat/addoncompat.manifest
@@ -0,0 +1,4 @@
+component {1363d5f0-d95e-11e3-9c1a-0800200c9a66} multiprocessShims.js
+contract @mozilla.org/addons/multiprocess-shims;1 {1363d5f0-d95e-11e3-9c1a-0800200c9a66}
+component {50bc93ce-602a-4bef-bf3a-61fc749c4caf} defaultShims.js
+contract @mozilla.org/addons/default-addon-shims;1 {50bc93ce-602a-4bef-bf3a-61fc749c4caf}
diff --git a/toolkit/components/addoncompat/defaultShims.js b/toolkit/components/addoncompat/defaultShims.js
new file mode 100644
index 000000000..a786efed7
--- /dev/null
+++ b/toolkit/components/addoncompat/defaultShims.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Using multiprocessShims is optional, and if an add-on is e10s compatible it should not
+ * use it. But in some cases we still want to use the interposition service for various
+ * features so we have a default shim service.
+ */
+
+function DefaultInterpositionService() {
+}
+
+DefaultInterpositionService.prototype = {
+ classID: Components.ID("{50bc93ce-602a-4bef-bf3a-61fc749c4caf}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonInterposition, Ci.nsISupportsWeakReference]),
+
+ getWhitelist: function() {
+ return [];
+ },
+
+ interposeProperty: function(addon, target, iid, prop) {
+ return null;
+ },
+
+ interposeCall: function(addonId, originalFunc, originalThis, args) {
+ args.splice(0, 0, addonId);
+ return originalFunc.apply(originalThis, args);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DefaultInterpositionService]);
diff --git a/toolkit/components/addoncompat/moz.build b/toolkit/components/addoncompat/moz.build
new file mode 100644
index 000000000..58a26eeba
--- /dev/null
+++ b/toolkit/components/addoncompat/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += ['tests']
+
+EXTRA_COMPONENTS += [
+ 'addoncompat.manifest',
+ 'defaultShims.js',
+ 'multiprocessShims.js',
+]
+
+EXTRA_JS_MODULES += [
+ 'CompatWarning.jsm',
+ 'Prefetcher.jsm',
+ 'RemoteAddonsChild.jsm',
+ 'RemoteAddonsParent.jsm',
+ 'ShimWaiver.jsm'
+]
diff --git a/toolkit/components/addoncompat/multiprocessShims.js b/toolkit/components/addoncompat/multiprocessShims.js
new file mode 100644
index 000000000..8b252a0c4
--- /dev/null
+++ b/toolkit/components/addoncompat/multiprocessShims.js
@@ -0,0 +1,182 @@
+/* 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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prefetcher",
+ "resource://gre/modules/Prefetcher.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RemoteAddonsParent",
+ "resource://gre/modules/RemoteAddonsParent.jsm");
+
+/**
+ * This service overlays the API that the browser exposes to
+ * add-ons. The overlay tries to make a multiprocess browser appear as
+ * much as possible like a single process browser. An overlay can
+ * replace methods, getters, and setters of arbitrary browser objects.
+ *
+ * Most of the actual replacement code is implemented in
+ * RemoteAddonsParent. The code in this service simply decides how to
+ * replace code. For a given type of object (say, an
+ * nsIObserverService) the code in RemoteAddonsParent can register a
+ * set of replacement methods. This set is called an
+ * "interposition". The service keeps track of all the different
+ * interpositions. Whenever a method is called on some part of the
+ * browser API, this service gets a chance to replace it. To do so, it
+ * consults its map based on the type of object. If an interposition
+ * is found, the given method is looked up on it and called
+ * instead. If no method (or no interposition) is found, then the
+ * original target method is called as normal.
+ *
+ * For each method call, we need to determine the type of the target
+ * object. If the object is an old-style XPConnect wrapped native,
+ * then the type is simply the interface that the method was called on
+ * (Ci.nsIObserverService, say). For all other objects (WebIDL
+ * objects, CPOWs, and normal JS objects), the type is determined by
+ * calling getObjectTag.
+ *
+ * The interpositions defined in RemoteAddonsParent have three
+ * properties: methods, getters, and setters. When accessing a
+ * property, we first consult methods. If nothing is found, then we
+ * consult getters or setters, depending on whether the access is a
+ * get or a set.
+ *
+ * The methods in |methods| are functions that will be called whenever
+ * the given method is called on the target object. They are passed
+ * the same parameters as the original function except for two
+ * additional ones at the beginning: the add-on ID and the original
+ * target object that the method was called on. Additionally, the
+ * value of |this| is set to the original target object.
+ *
+ * The values in |getters| and |setters| should also be
+ * functions. They are called immediately when the given property is
+ * accessed. The functions in |getters| take two parameters: the
+ * add-on ID and the original target object. The functions in
+ * |setters| take those arguments plus the value that the property is
+ * being set to.
+ */
+
+function AddonInterpositionService()
+{
+ Prefetcher.init();
+ RemoteAddonsParent.init();
+
+ // These maps keep track of the interpositions for all different
+ // kinds of objects.
+ this._interfaceInterpositions = RemoteAddonsParent.getInterfaceInterpositions();
+ this._taggedInterpositions = RemoteAddonsParent.getTaggedInterpositions();
+
+ let wl = [];
+ for (let v in this._interfaceInterpositions) {
+ let interp = this._interfaceInterpositions[v];
+ wl.push(...Object.getOwnPropertyNames(interp.methods));
+ wl.push(...Object.getOwnPropertyNames(interp.getters));
+ wl.push(...Object.getOwnPropertyNames(interp.setters));
+ }
+
+ for (let v in this._taggedInterpositions) {
+ let interp = this._taggedInterpositions[v];
+ wl.push(...Object.getOwnPropertyNames(interp.methods));
+ wl.push(...Object.getOwnPropertyNames(interp.getters));
+ wl.push(...Object.getOwnPropertyNames(interp.setters));
+ }
+
+ let nameSet = new Set();
+ wl = wl.filter(function(item) {
+ if (nameSet.has(item))
+ return true;
+
+ nameSet.add(item);
+ return true;
+ });
+
+ this._whitelist = wl;
+}
+
+AddonInterpositionService.prototype = {
+ classID: Components.ID("{1363d5f0-d95e-11e3-9c1a-0800200c9a66}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonInterposition, Ci.nsISupportsWeakReference]),
+
+ getWhitelist: function() {
+ return this._whitelist;
+ },
+
+ // When the interface is not known for a method call, this code
+ // determines the type of the target object.
+ getObjectTag: function(target) {
+ if (Cu.isCrossProcessWrapper(target)) {
+ return Cu.getCrossProcessWrapperTag(target);
+ }
+
+ if (target instanceof Ci.nsIDOMXULElement) {
+ if (target.localName == "browser" && target.isRemoteBrowser) {
+ return "RemoteBrowserElement";
+ }
+
+ if (target.localName == "tabbrowser") {
+ return "TabBrowserElement";
+ }
+ }
+
+ if (target instanceof Ci.nsIDOMChromeWindow && target.gMultiProcessBrowser) {
+ return "ChromeWindow";
+ }
+
+ if (target instanceof Ci.nsIDOMEventTarget) {
+ return "EventTarget";
+ }
+
+ return "generic";
+ },
+
+ interposeProperty: function(addon, target, iid, prop) {
+ let interp;
+ if (iid) {
+ interp = this._interfaceInterpositions[iid];
+ } else {
+ try {
+ interp = this._taggedInterpositions[this.getObjectTag(target)];
+ }
+ catch (e) {
+ Cu.reportError(new Components.Exception("Failed to interpose object", e.result, Components.stack.caller));
+ }
+ }
+
+ if (!interp) {
+ return Prefetcher.lookupInCache(addon, target, prop);
+ }
+
+ let desc = { configurable: false, enumerable: true };
+
+ if ("methods" in interp && prop in interp.methods) {
+ desc.writable = false;
+ desc.value = function(...args) {
+ return interp.methods[prop](addon, target, ...args);
+ }
+
+ return desc;
+ } else if ("getters" in interp && prop in interp.getters) {
+ desc.get = function() { return interp.getters[prop](addon, target); };
+
+ if ("setters" in interp && prop in interp.setters) {
+ desc.set = function(v) { return interp.setters[prop](addon, target, v); };
+ }
+
+ return desc;
+ }
+
+ return Prefetcher.lookupInCache(addon, target, prop);
+ },
+
+ interposeCall: function(addonId, originalFunc, originalThis, args) {
+ args.splice(0, 0, addonId);
+ return originalFunc.apply(originalThis, args);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonInterpositionService]);
diff --git a/toolkit/components/addoncompat/tests/addon/bootstrap.js b/toolkit/components/addoncompat/tests/addon/bootstrap.js
new file mode 100644
index 000000000..5e69fee22
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/addon/bootstrap.js
@@ -0,0 +1,653 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/BrowserUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const baseURL = "http://mochi.test:8888/browser/" +
+ "toolkit/components/addoncompat/tests/browser/";
+
+var contentSecManager = Cc["@mozilla.org/contentsecuritymanager;1"]
+ .getService(Ci.nsIContentSecurityManager);
+
+function forEachWindow(f)
+{
+ let wins = Services.wm.getEnumerator("navigator:browser");
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext();
+ f(win);
+ }
+}
+
+function addLoadListener(target, listener)
+{
+ target.addEventListener("load", function handler(event) {
+ target.removeEventListener("load", handler, true);
+ return listener(event);
+ }, true);
+}
+
+var gWin;
+var gBrowser;
+var ok, is, info;
+
+function removeTab(tab, done)
+{
+ // Remove the tab in a different turn of the event loop. This way
+ // the nested event loop in removeTab doesn't conflict with the
+ // event listener shims.
+ gWin.setTimeout(() => {
+ gBrowser.removeTab(tab);
+ done();
+ }, 0);
+}
+
+// Make sure that the shims for window.content, browser.contentWindow,
+// and browser.contentDocument are working.
+function testContentWindow()
+{
+ return new Promise(function(resolve, reject) {
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ ok(gWin.content, "content is defined on chrome window");
+ ok(browser.contentWindow, "contentWindow is defined");
+ ok(browser.contentDocument, "contentWindow is defined");
+ is(gWin.content, browser.contentWindow, "content === contentWindow");
+ ok(browser.webNavigation.sessionHistory, "sessionHistory is defined");
+
+ ok(browser.contentDocument.getElementById("link"), "link present in document");
+
+ // FIXME: Waiting on bug 1073631.
+ // is(browser.contentWindow.wrappedJSObject.global, 3, "global available on document");
+
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+// Test for bug 1060046 and bug 1072607. We want to make sure that
+// adding and removing listeners works as expected.
+function testListeners()
+{
+ return new Promise(function(resolve, reject) {
+ const url1 = baseURL + "browser_addonShims_testpage.html";
+ const url2 = baseURL + "browser_addonShims_testpage2.html";
+
+ let tab = gBrowser.addTab(url2);
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ function dummyHandler() {}
+
+ // Test that a removed listener stays removed (bug
+ // 1072607). We're looking to make sure that adding and removing
+ // a listener here doesn't cause later listeners to fire more
+ // than once.
+ for (let i = 0; i < 5; i++) {
+ gBrowser.addEventListener("load", dummyHandler, true);
+ gBrowser.removeEventListener("load", dummyHandler, true);
+ }
+
+ // We also want to make sure that this listener doesn't fire
+ // after it's removed.
+ let loadWithRemoveCount = 0;
+ addLoadListener(browser, function handler1(event) {
+ loadWithRemoveCount++;
+ is(event.target.documentURI, url1, "only fire for first url");
+ });
+
+ // Load url1 and then url2. We want to check that:
+ // 1. handler1 only fires for url1.
+ // 2. handler2 only fires once for url1 (so the second time it
+ // fires should be for url2).
+ let loadCount = 0;
+ browser.addEventListener("load", function handler2(event) {
+ loadCount++;
+ if (loadCount == 1) {
+ is(event.target.documentURI, url1, "first load is for first page loaded");
+ browser.loadURI(url2);
+ } else {
+ gBrowser.removeEventListener("load", handler2, true);
+
+ is(event.target.documentURI, url2, "second load is for second page loaded");
+ is(loadWithRemoveCount, 1, "load handler is only called once");
+
+ removeTab(tab, resolve);
+ }
+ }, true);
+
+ browser.loadURI(url1);
+ });
+ });
+}
+
+// Test for bug 1059207. We want to make sure that adding a capturing
+// listener and a non-capturing listener to the same element works as
+// expected.
+function testCapturing()
+{
+ return new Promise(function(resolve, reject) {
+ let capturingCount = 0;
+ let nonCapturingCount = 0;
+
+ function capturingHandler(event) {
+ is(capturingCount, 0, "capturing handler called once");
+ is(nonCapturingCount, 0, "capturing handler called before bubbling handler");
+ capturingCount++;
+ }
+
+ function nonCapturingHandler(event) {
+ is(capturingCount, 1, "bubbling handler called after capturing handler");
+ is(nonCapturingCount, 0, "bubbling handler called once");
+ nonCapturingCount++;
+ }
+
+ gBrowser.addEventListener("mousedown", capturingHandler, true);
+ gBrowser.addEventListener("mousedown", nonCapturingHandler, false);
+
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ let win = browser.contentWindow;
+ let event = win.document.createEvent("MouseEvents");
+ event.initMouseEvent("mousedown", true, false, win, 1,
+ 1, 0, 0, 0, // screenX, screenY, clientX, clientY
+ false, false, false, false, // ctrlKey, altKey, shiftKey, metaKey
+ 0, null); // buttonCode, relatedTarget
+
+ let element = win.document.getElementById("output");
+ element.dispatchEvent(event);
+
+ is(capturingCount, 1, "capturing handler fired");
+ is(nonCapturingCount, 1, "bubbling handler fired");
+
+ gBrowser.removeEventListener("mousedown", capturingHandler, true);
+ gBrowser.removeEventListener("mousedown", nonCapturingHandler, false);
+
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+// Make sure we get observer notifications that normally fire in the
+// child.
+function testObserver()
+{
+ return new Promise(function(resolve, reject) {
+ let observerFired = 0;
+
+ function observer(subject, topic, data) {
+ Services.obs.removeObserver(observer, "document-element-inserted");
+ observerFired++;
+ }
+ Services.obs.addObserver(observer, "document-element-inserted", false);
+
+ let count = 0;
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+ browser.addEventListener("load", function handler() {
+ count++;
+ if (count == 1) {
+ browser.reload();
+ } else {
+ browser.removeEventListener("load", handler);
+
+ is(observerFired, 1, "got observer notification");
+
+ removeTab(tab, resolve);
+ }
+ }, true);
+ });
+}
+
+// Test for bug 1072472. Make sure that creating a sandbox to run code
+// in the content window works. This is essentially a test for
+// Greasemonkey.
+function testSandbox()
+{
+ return new Promise(function(resolve, reject) {
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+ browser.addEventListener("load", function handler() {
+ browser.removeEventListener("load", handler);
+
+ let sandbox = Cu.Sandbox(browser.contentWindow,
+ {sandboxPrototype: browser.contentWindow,
+ wantXrays: false});
+ Cu.evalInSandbox("const unsafeWindow = window;", sandbox);
+ Cu.evalInSandbox("document.getElementById('output').innerHTML = 'hello';", sandbox);
+
+ is(browser.contentDocument.getElementById("output").innerHTML, "hello",
+ "sandbox code ran successfully");
+
+ // Now try a sandbox with expanded principals.
+ sandbox = Cu.Sandbox([browser.contentWindow],
+ {sandboxPrototype: browser.contentWindow,
+ wantXrays: false});
+ Cu.evalInSandbox("const unsafeWindow = window;", sandbox);
+ Cu.evalInSandbox("document.getElementById('output').innerHTML = 'hello2';", sandbox);
+
+ is(browser.contentDocument.getElementById("output").innerHTML, "hello2",
+ "EP sandbox code ran successfully");
+
+ removeTab(tab, resolve);
+ }, true);
+ });
+}
+
+// Test for bug 1095305. We just want to make sure that loading some
+// unprivileged content from an add-on package doesn't crash.
+function testAddonContent()
+{
+ let chromeRegistry = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIChromeRegistry);
+ let base = chromeRegistry.convertChromeURL(BrowserUtils.makeURI("chrome://addonshim1/content/"));
+
+ let res = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ res.setSubstitution("addonshim1", base);
+
+ return new Promise(function(resolve, reject) {
+ const url = "resource://addonshim1/page.html";
+ let tab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ res.setSubstitution("addonshim1", null);
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+
+// Test for bug 1102410. We check that multiple nsIAboutModule's can be
+// registered in the parent, and that the child can browse to each of
+// the registered about: pages.
+function testAboutModuleRegistration()
+{
+ let Registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+ let modulesToUnregister = new Map();
+
+ function TestChannel(uri, aLoadInfo, aboutName) {
+ this.aboutName = aboutName;
+ this.loadInfo = aLoadInfo;
+ this.URI = this.originalURI = uri;
+ }
+
+ TestChannel.prototype = {
+ asyncOpen: function(listener, context) {
+ let stream = this.open();
+ 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);
+ return this.asyncOpen(outListener, null);
+ },
+
+ open: function() {
+ function getWindow(channel) {
+ try
+ {
+ if (channel.notificationCallbacks)
+ return channel.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
+ } catch (e) {}
+
+ try
+ {
+ if (channel.loadGroup && channel.loadGroup.notificationCallbacks)
+ return channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
+ } catch (e) {}
+
+ return null;
+ }
+
+ let data = `<html><h1>${this.aboutName}</h1></html>`;
+ let wnd = getWindow(this);
+ if (!wnd)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
+ stream.setData(data, data.length);
+ return stream;
+ },
+
+ open2: function() {
+ // throws an error if security checks fail
+ contentSecManager.performSecurityCheck(this, null);
+ return this.open();
+ },
+
+ 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 function creates a new nsIAboutModule and registers it. Callers
+ * should also call unregisterModules after using this function to clean
+ * up the nsIAboutModules at the end of this test.
+ *
+ * @param aboutName
+ * This will be the string after about: used to refer to this module.
+ * For example, if aboutName is foo, you can refer to this module by
+ * browsing to about:foo.
+ *
+ * @param uuid
+ * A unique identifer string for this module. For example,
+ * "5f3a921b-250f-4ac5-a61c-8f79372e6063"
+ */
+ let createAndRegisterAboutModule = function(aboutName, uuid) {
+
+ let AboutModule = function() {};
+
+ AboutModule.prototype = {
+ classID: Components.ID(uuid),
+ classDescription: `Testing About Module for about:${aboutName}`,
+ contractID: `@mozilla.org/network/protocol/about;1?what=${aboutName}`,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+
+ newChannel: (aURI, aLoadInfo) => {
+ return new TestChannel(aURI, aLoadInfo, aboutName);
+ },
+
+ getURIFlags: (aURI) => {
+ return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ Ci.nsIAboutModule.ALLOW_SCRIPT;
+ },
+ };
+
+ let factory = {
+ createInstance: function(outer, iid) {
+ if (outer) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ return new AboutModule();
+ },
+ };
+
+ Registrar.registerFactory(AboutModule.prototype.classID,
+ AboutModule.prototype.classDescription,
+ AboutModule.prototype.contractID,
+ factory);
+
+ modulesToUnregister.set(AboutModule.prototype.classID,
+ factory);
+ };
+
+ /**
+ * Unregisters any nsIAboutModules registered with
+ * createAndRegisterAboutModule.
+ */
+ let unregisterModules = () => {
+ for (let [classID, factory] of modulesToUnregister) {
+ Registrar.unregisterFactory(classID, factory);
+ }
+ };
+
+ /**
+ * Takes a browser, and sends it a framescript to attempt to
+ * load some about: pages. The frame script will send a test:result
+ * message on completion, passing back a data object with:
+ *
+ * {
+ * pass: true
+ * }
+ *
+ * on success, and:
+ *
+ * {
+ * pass: false,
+ * errorMsg: message,
+ * }
+ *
+ * on failure.
+ *
+ * @param browser
+ * The browser to send the framescript to.
+ */
+ let testAboutModulesWork = (browser) => {
+ let testConnection = () => {
+ let request = new content.XMLHttpRequest();
+ try {
+ request.open("GET", "about:test1", false);
+ request.send(null);
+ if (request.status != 200) {
+ throw (`about:test1 response had status ${request.status} - expected 200`);
+ }
+ if (request.responseText.indexOf("test1") == -1) {
+ throw (`about:test1 response had result ${request.responseText}`);
+ }
+
+ request = new content.XMLHttpRequest();
+ request.open("GET", "about:test2", false);
+ request.send(null);
+
+ if (request.status != 200) {
+ throw (`about:test2 response had status ${request.status} - expected 200`);
+ }
+ if (request.responseText.indexOf("test2") == -1) {
+ throw (`about:test2 response had result ${request.responseText}`);
+ }
+
+ sendAsyncMessage("test:result", {
+ pass: true,
+ });
+ } catch (e) {
+ sendAsyncMessage("test:result", {
+ pass: false,
+ errorMsg: e.toString(),
+ });
+ }
+ };
+
+ return new Promise((resolve, reject) => {
+ let mm = browser.messageManager;
+ mm.addMessageListener("test:result", function onTestResult(message) {
+ mm.removeMessageListener("test:result", onTestResult);
+ if (message.data.pass) {
+ ok(true, "Connections to about: pages were successful");
+ } else {
+ ok(false, message.data.errorMsg);
+ }
+ resolve();
+ });
+ mm.loadFrameScript("data:,(" + testConnection.toString() + ")();", false);
+ });
+ }
+
+ // Here's where the actual test is performed.
+ return new Promise((resolve, reject) => {
+ createAndRegisterAboutModule("test1", "5f3a921b-250f-4ac5-a61c-8f79372e6063");
+ createAndRegisterAboutModule("test2", "d7ec0389-1d49-40fa-b55c-a1fc3a6dbf6f");
+
+ // This needs to be a chrome-privileged page that loads in the
+ // content process. It needs chrome privs because otherwise the
+ // XHRs for about:test[12] will fail with a privilege error
+ // despite the presence of URI_SAFE_FOR_UNTRUSTED_CONTENT.
+ let newTab = gBrowser.addTab("chrome://addonshim1/content/page.html");
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+
+ addLoadListener(browser, function() {
+ testAboutModulesWork(browser).then(() => {
+ unregisterModules();
+ removeTab(newTab, resolve);
+ });
+ });
+ });
+}
+
+function testProgressListener()
+{
+ const url = baseURL + "browser_addonShims_testpage.html";
+
+ let sawGlobalLocChange = false;
+ let sawTabsLocChange = false;
+
+ let globalListener = {
+ onLocationChange: function(webProgress, request, uri) {
+ if (uri.spec == url) {
+ sawGlobalLocChange = true;
+ ok(request instanceof Ci.nsIHttpChannel, "Global listener channel is an HTTP channel");
+ }
+ },
+ };
+
+ let tabsListener = {
+ onLocationChange: function(browser, webProgress, request, uri) {
+ if (uri.spec == url) {
+ sawTabsLocChange = true;
+ ok(request instanceof Ci.nsIHttpChannel, "Tab listener channel is an HTTP channel");
+ }
+ },
+ };
+
+ gBrowser.addProgressListener(globalListener);
+ gBrowser.addTabsProgressListener(tabsListener);
+ info("Added progress listeners");
+
+ return new Promise(function(resolve, reject) {
+ let tab = gBrowser.addTab(url);
+ gBrowser.selectedTab = tab;
+ addLoadListener(tab.linkedBrowser, function handler() {
+ ok(sawGlobalLocChange, "Saw global onLocationChange");
+ ok(sawTabsLocChange, "Saw tabs onLocationChange");
+
+ gBrowser.removeProgressListener(globalListener);
+ gBrowser.removeTabsProgressListener(tabsListener);
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+function testRootTreeItem()
+{
+ return new Promise(function(resolve, reject) {
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ let win = browser.contentWindow;
+
+ // Add-ons love this crap.
+ let root = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindow);
+ is(root, gWin, "got correct chrome window");
+
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+function testImportNode()
+{
+ return new Promise(function(resolve, reject) {
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ let node = gWin.document.createElement("div");
+ let doc = browser.contentDocument;
+ let result;
+ try {
+ result = doc.importNode(node, false);
+ } catch (e) {
+ ok(false, "importing threw an exception");
+ }
+ if (browser.isRemoteBrowser) {
+ is(result, node, "got expected import result");
+ }
+
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+function runTests(win, funcs)
+{
+ ok = funcs.ok;
+ is = funcs.is;
+ info = funcs.info;
+
+ gWin = win;
+ gBrowser = win.gBrowser;
+
+ return testContentWindow().
+ then(testListeners).
+ then(testCapturing).
+ then(testObserver).
+ then(testSandbox).
+ then(testAddonContent).
+ then(testAboutModuleRegistration).
+ then(testProgressListener).
+ then(testRootTreeItem).
+ then(testImportNode).
+ then(Promise.resolve());
+}
+
+/*
+ bootstrap.js API
+*/
+
+function startup(aData, aReason)
+{
+ forEachWindow(win => {
+ win.runAddonShimTests = (funcs) => runTests(win, funcs);
+ });
+}
+
+function shutdown(aData, aReason)
+{
+ forEachWindow(win => {
+ delete win.runAddonShimTests;
+ });
+}
+
+function install(aData, aReason)
+{
+}
+
+function uninstall(aData, aReason)
+{
+}
+
diff --git a/toolkit/components/addoncompat/tests/addon/chrome.manifest b/toolkit/components/addoncompat/tests/addon/chrome.manifest
new file mode 100644
index 000000000..602ba3a5d
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/addon/chrome.manifest
@@ -0,0 +1 @@
+content addonshim1 content/
diff --git a/toolkit/components/addoncompat/tests/addon/content/page.html b/toolkit/components/addoncompat/tests/addon/content/page.html
new file mode 100644
index 000000000..90531a4b3
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/addon/content/page.html
@@ -0,0 +1,2 @@
+<html>
+</html>
diff --git a/toolkit/components/addoncompat/tests/addon/install.rdf b/toolkit/components/addoncompat/tests/addon/install.rdf
new file mode 100644
index 000000000..d59c7b19d
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/addon/install.rdf
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-addon-shim-1@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test addon shim 1</em:name>
+ <em:description>Test an add-on that needs multiprocess shims.</em:description>
+ <em:multiprocessCompatible>false</em:multiprocessCompatible>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>10.0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/components/addoncompat/tests/browser/.eslintrc.js b/toolkit/components/addoncompat/tests/browser/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/addoncompat/tests/browser/addon.xpi b/toolkit/components/addoncompat/tests/browser/addon.xpi
new file mode 100644
index 000000000..e6392fb40
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/addon.xpi
Binary files differ
diff --git a/toolkit/components/addoncompat/tests/browser/browser.ini b/toolkit/components/addoncompat/tests/browser/browser.ini
new file mode 100644
index 000000000..7c8547562
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = addons
+support-files =
+ addon.xpi
+ browser_addonShims_testpage.html
+ browser_addonShims_testpage2.html
+ compat-addon.xpi
+
+[browser_addonShims.js]
diff --git a/toolkit/components/addoncompat/tests/browser/browser_addonShims.js b/toolkit/components/addoncompat/tests/browser/browser_addonShims.js
new file mode 100644
index 000000000..b642eb3cb
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/browser_addonShims.js
@@ -0,0 +1,67 @@
+var {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+var {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+const ADDON_URL = "http://example.com/browser/toolkit/components/addoncompat/tests/browser/addon.xpi";
+const COMPAT_ADDON_URL = "http://example.com/browser/toolkit/components/addoncompat/tests/browser/compat-addon.xpi";
+
+// Install a test add-on that will exercise e10s shims.
+// url: Location of the add-on.
+function addAddon(url)
+{
+ info("Installing add-on: " + url);
+
+ return new Promise(function(resolve, reject) {
+ AddonManager.getInstallForURL(url, installer => {
+ installer.install();
+ let listener = {
+ onInstallEnded: function(addon, addonInstall) {
+ installer.removeListener(listener);
+
+ // Wait for add-on's startup scripts to execute. See bug 997408
+ executeSoon(function() {
+ resolve(addonInstall);
+ });
+ }
+ };
+ installer.addListener(listener);
+ }, "application/x-xpinstall");
+ });
+}
+
+// Uninstall a test add-on.
+// addon: The addon reference returned from addAddon.
+function removeAddon(addon)
+{
+ info("Removing addon.");
+
+ return new Promise(function(resolve, reject) {
+ let listener = {
+ onUninstalled: function(uninstalledAddon) {
+ if (uninstalledAddon != addon) {
+ return;
+ }
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ addon.uninstall();
+ });
+}
+
+add_task(function* test_addon_shims() {
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({set: [["dom.ipc.shims.enabledWarnings", true]]},
+ resolve);
+ });
+
+ let addon = yield addAddon(ADDON_URL);
+ yield window.runAddonShimTests({ok: ok, is: is, info: info});
+ yield removeAddon(addon);
+
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ addon = yield addAddon(COMPAT_ADDON_URL);
+ yield window.runAddonTests({ok: ok, is: is, info: info});
+ yield removeAddon(addon);
+ }
+});
diff --git a/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage.html b/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage.html
new file mode 100644
index 000000000..5a8b34e88
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage.html
@@ -0,0 +1,17 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>shim test</title>
+</head>
+
+<body>
+Hello!
+
+<a href="browser_addonShims_testpage2.html" id="link">Link</a>
+<div id="output"></div>
+
+<script type="text/javascript">
+var global = 3;
+</script>
+</body>
+</html>
diff --git a/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage2.html b/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage2.html
new file mode 100644
index 000000000..f644b1129
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage2.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>shim test</title>
+</head>
+
+<body>
+Hello!
+
+<a href="browser_addonShims_testpage.html" id="link">Link</a>
+
+<script type="text/javascript">
+var global = 5;
+</script>
+</body>
+</html>
diff --git a/toolkit/components/addoncompat/tests/browser/compat-addon.xpi b/toolkit/components/addoncompat/tests/browser/compat-addon.xpi
new file mode 100644
index 000000000..c7ca32cdc
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/compat-addon.xpi
Binary files differ
diff --git a/toolkit/components/addoncompat/tests/compat-addon/bootstrap.js b/toolkit/components/addoncompat/tests/compat-addon/bootstrap.js
new file mode 100644
index 000000000..7c93bad08
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/compat-addon/bootstrap.js
@@ -0,0 +1,99 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/BrowserUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const baseURL = "http://mochi.test:8888/browser/" +
+ "toolkit/components/addoncompat/tests/browser/";
+
+function forEachWindow(f)
+{
+ let wins = Services.wm.getEnumerator("navigator:browser");
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext();
+ f(win);
+ }
+}
+
+function addLoadListener(target, listener)
+{
+ function frameScript() {
+ addEventListener("load", function handler(event) {
+ removeEventListener("load", handler, true);
+ sendAsyncMessage("compat-test:loaded");
+ }, true);
+ }
+ target.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")()", false);
+ target.messageManager.addMessageListener("compat-test:loaded", function handler() {
+ target.messageManager.removeMessageListener("compat-test:loaded", handler);
+ listener();
+ });
+}
+
+var gWin;
+var gBrowser;
+var ok, is, info;
+
+// Make sure that the shims for window.content, browser.contentWindow,
+// and browser.contentDocument are working.
+function testContentWindow()
+{
+ return new Promise(function(resolve, reject) {
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab("about:blank");
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ ok(!gWin.content, "content is defined on chrome window");
+ ok(!browser.contentWindow, "contentWindow is defined");
+ ok(!browser.contentDocument, "contentWindow is defined");
+
+ gBrowser.removeTab(tab);
+ resolve();
+ });
+ browser.loadURI(url);
+ });
+}
+
+function runTests(win, funcs)
+{
+ ok = funcs.ok;
+ is = funcs.is;
+ info = funcs.info;
+
+ gWin = win;
+ gBrowser = win.gBrowser;
+
+ return testContentWindow();
+}
+
+/*
+ bootstrap.js API
+*/
+
+function startup(aData, aReason)
+{
+ forEachWindow(win => {
+ win.runAddonTests = (funcs) => runTests(win, funcs);
+ });
+}
+
+function shutdown(aData, aReason)
+{
+ forEachWindow(win => {
+ delete win.runAddonTests;
+ });
+}
+
+function install(aData, aReason)
+{
+}
+
+function uninstall(aData, aReason)
+{
+}
+
diff --git a/toolkit/components/addoncompat/tests/compat-addon/install.rdf b/toolkit/components/addoncompat/tests/compat-addon/install.rdf
new file mode 100644
index 000000000..331fd1540
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/compat-addon/install.rdf
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-addon-shim-2@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test addon shims 2</em:name>
+ <em:description>Test an add-on that doesn't need multiprocess shims.</em:description>
+ <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>10.0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/components/addoncompat/tests/moz.build b/toolkit/components/addoncompat/tests/moz.build
new file mode 100644
index 000000000..589eaa812
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['browser/browser.ini']