diff options
Diffstat (limited to 'toolkit/components/addoncompat')
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 0000000000..b32409a46b --- /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 0000000000..2d836690cc --- /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 0000000000..1aacc7f7ab --- /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 0000000000..5cadc2902b --- /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 0000000000..402ab4c322 --- /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 0000000000..fe38f47d80 --- /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 0000000000..a786efed7d --- /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 0000000000..58a26eeba4 --- /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 0000000000..8b252a0c48 --- /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 0000000000..5e69fee22c --- /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 0000000000..602ba3a5dc --- /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 0000000000..90531a4b3e --- /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 0000000000..d59c7b19d1 --- /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 0000000000..7c80211924 --- /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 Binary files differnew file mode 100644 index 0000000000..e6392fb40a --- /dev/null +++ b/toolkit/components/addoncompat/tests/browser/addon.xpi diff --git a/toolkit/components/addoncompat/tests/browser/browser.ini b/toolkit/components/addoncompat/tests/browser/browser.ini new file mode 100644 index 0000000000..7c85475625 --- /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 0000000000..b642eb3cb0 --- /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 0000000000..5a8b34e888 --- /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 0000000000..f644b1129c --- /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 Binary files differnew file mode 100644 index 0000000000..c7ca32cdc6 --- /dev/null +++ b/toolkit/components/addoncompat/tests/browser/compat-addon.xpi 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 0000000000..7c93bad089 --- /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 0000000000..331fd1540b --- /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 0000000000..589eaa8127 --- /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'] |