diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 4600cfefd3..9c2a8b019b 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -2920,6 +2920,18 @@ // Finish tearing down the tab that's going away. remoteBrowser._endRemoveTab(aOtherTab); + // Notify WebExtensions when one tab is replaced by another. Tab + // adoption uses this same swap internally, but represents a move + // and is marked below so it does not generate a replacement event. + if (!aOurTab._adoptingTab) { + let event = new CustomEvent("TabReplaced", { + bubbles: true, + detail: {addedTab: aOurTab, removedTab: aOtherTab}, + }); + aOurTab.dispatchEvent(event); + } + delete aOurTab._adoptingTab; + if (isBusy) this.setTabTitleLoading(aOurTab); else @@ -3359,6 +3371,7 @@ params.userContextId = aTab.getAttribute("usercontextid"); } let newTab = this.addTab("about:blank", params); + newTab._adoptingTab = aTab; let newBrowser = this.getBrowserForTab(newTab); let newURL = aTab.linkedBrowser.currentURI.spec; diff --git a/browser/components/webextensions/ext-tabs.js b/browser/components/webextensions/ext-tabs.js index 580282b934..cd9f8873ac 100644 --- a/browser/components/webextensions/ext-tabs.js +++ b/browser/components/webextensions/ext-tabs.js @@ -103,6 +103,7 @@ let tabListener = { AllWindowEvents.addListener("TabClose", this); AllWindowEvents.addListener("TabOpen", this); + AllWindowEvents.addListener("TabReplaced", this); WindowListManager.addOpenListener(this.handleWindowOpen); WindowListManager.addCloseListener(this.handleWindowClose); @@ -138,6 +139,10 @@ let tabListener = { this.emitRemoved(tab, false); } break; + + case "TabReplaced": + this.emitReplaced(event.detail.addedTab, event.detail.removedTab); + break; } }, @@ -220,6 +225,13 @@ let tabListener = { }, Ci.nsIThread.DISPATCH_NORMAL); }, + emitReplaced(addedTab, removedTab) { + this.emit("tab-replaced", { + addedTabId: TabManager.getId(addedTab), + removedTabId: TabManager.getId(removedTab), + }); + }, + tabReadyInitialized: false, tabReadyPromises: new WeakMap(), initializingTabs: new WeakSet(), @@ -347,7 +359,15 @@ extensions.registerSchemaAPI("tabs", "addon_parent", context => { }; }).api(), - onReplaced: ignoreEvent(context, "tabs.onReplaced"), + onReplaced: new EventManager(context, "tabs.onReplaced", fire => { + let listener = (eventName, event) => { + fire(event.addedTabId, event.removedTabId); + }; + tabListener.on("tab-replaced", listener); + return () => { + tabListener.off("tab-replaced", listener); + }; + }).api(), onMoved: new EventManager(context, "tabs.onMoved", fire => { // There are certain circumstances where we need to ignore a move event. diff --git a/toolkit/components/webextensions/ExtensionUtils.jsm b/toolkit/components/webextensions/ExtensionUtils.jsm index 2665c1a125..af4df1bb39 100644 --- a/toolkit/components/webextensions/ExtensionUtils.jsm +++ b/toolkit/components/webextensions/ExtensionUtils.jsm @@ -750,9 +750,12 @@ SingletonEventManager.prototype = { }; // Simple API for event listeners where events never fire. -function ignoreEvent(context, name) { +function ignoreEvent(context, name, warn = true) { return { addListener: function(callback) { + if (!warn) { + return; + } let id = context.extension.id; let frame = Components.stack.caller; let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`; @@ -1095,8 +1098,8 @@ class MessageManagerProxy { if (this.messageManager) { return this.messageManager.sendAsyncMessage(...args); } - /* globals uneval */ - Cu.reportError(`Cannot send message: Other side disconnected: ${uneval(args)}`); + // Message senders can outlive their child process during normal teardown. + // Treat this as a dropped message instead of reporting a spurious error. } /** diff --git a/toolkit/mozapps/webextensions/AddonManager.jsm b/toolkit/mozapps/webextensions/AddonManager.jsm index 1203ec0da2..380fcde848 100644 --- a/toolkit/mozapps/webextensions/AddonManager.jsm +++ b/toolkit/mozapps/webextensions/AddonManager.jsm @@ -321,13 +321,60 @@ function webAPIForAddon(addon) { let result = {}; + function cloneable(value, seen = new Set()) { + if (value === null || value === undefined || + typeof value == "string" || typeof value == "number" || + typeof value == "boolean") { + return value; + } + + if (typeof value != "object") { + return undefined; + } + + // Older message managers cannot structured-clone these XPCOM values. + try { + if (value instanceof Ci.nsIURI) { + return value.spec; + } + if (value instanceof Ci.nsIFile) { + return value.path; + } + } catch (e) {} + + if (seen.has(value)) { + return undefined; + } + seen.add(value); + + let className = Cu.getClassName(value, true); + if (className == "Array") { + return value.map(item => cloneable(item, seen)); + } + if (className != "Object") { + return undefined; + } + + let copy = {}; + for (let key of Object.keys(value)) { + let item = cloneable(value[key], seen); + if (item !== undefined) { + copy[key] = item; + } + } + return copy; + } + // By default just pass through any plain property, the webidl will // control access. Also filter out private properties, regular Addon // objects are okay but MockAddon used in tests has non-serializable // private properties. for (let prop in addon) { if (prop[0] != "_" && typeof(addon[prop]) != "function") { - result[prop] = addon[prop]; + let value = cloneable(addon[prop]); + if (value !== undefined) { + result[prop] = value; + } } } diff --git a/toolkit/mozapps/webextensions/addonManager.js b/toolkit/mozapps/webextensions/addonManager.js index d34cbaf624..9113f5eb37 100644 --- a/toolkit/mozapps/webextensions/addonManager.js +++ b/toolkit/mozapps/webextensions/addonManager.js @@ -36,6 +36,20 @@ const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); +function deserializeTriggeringPrincipal(principal) { + if (typeof principal != "string") { + return principal; + } + try { + return Cc["@mozilla.org/network/serialization-helper;1"] + .getService(Ci.nsISerializationHelper) + .deserializeObject(principal) + .QueryInterface(Ci.nsIPrincipal); + } catch (e) { + return null; + } +} + var gSingleton = null; function amManager() { @@ -219,7 +233,7 @@ amManager.prototype = { } return this.installAddonsFromWebpage(payload.mimetype, - aMessage.target, payload.triggeringPrincipal, payload.uris, + aMessage.target, deserializeTriggeringPrincipal(payload.triggeringPrincipal), payload.uris, payload.hashes, payload.names, payload.icons, callback); } diff --git a/toolkit/mozapps/webextensions/amInstallTrigger.js b/toolkit/mozapps/webextensions/amInstallTrigger.js index 382791d326..31e98b9865 100644 --- a/toolkit/mozapps/webextensions/amInstallTrigger.js +++ b/toolkit/mozapps/webextensions/amInstallTrigger.js @@ -76,7 +76,11 @@ RemoteMediator.prototype = { let callbackID = this._addCallback(callback, installs.uris); installs.mimetype = XPINSTALL_MIMETYPE; - installs.triggeringPrincipal = principal; + // nsIPrincipal is an XPCOM object and cannot cross the legacy message + // manager boundary used by content processes. Serialize it explicitly. + installs.triggeringPrincipal = Cc["@mozilla.org/network/serialization-helper;1"] + .getService(Ci.nsISerializationHelper) + .serializeToString(principal); installs.callbackID = callbackID; if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {