--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -228,23 +228,56 @@ let ProxyMessenger = {
}
// Note: No special handling for sendNativeMessage / connectNative because
// native messaging runs in the chrome process, so it never needs a proxy.
return null;
},
};
+class BrowserDocshellFollower {
+ /**
+ * Follows the <browser> belonging to the `xulBrowser`'s current docshell.
+ *
+ * @param {XULElement} xulBrowser A <browser> tag.
+ * @param {function} onBrowserChange Called when the <browser> changes.
+ */
+ constructor(xulBrowser, onBrowserChange) {
+ this.xulBrowser = xulBrowser;
+ this.onBrowserChange = onBrowserChange;
+
+ xulBrowser.addEventListener("SwapDocShells", this);
+ }
+
+ destroy() {
+ this.xulBrowser.removeEventListener("SwapDocShells", this);
+ this.xulBrowser = null;
+ }
+
+ handleEvent({detail: otherBrowser}) {
+ this.xulBrowser.removeEventListener("SwapDocShells", this);
+ this.xulBrowser = otherBrowser;
+ this.xulBrowser.addEventListener("SwapDocShells", this);
+ this.onBrowserChange(otherBrowser);
+ }
+}
+
class ProxyContext extends BaseContext {
- constructor(envType, extension, params, messageManager, principal) {
+ constructor(envType, extension, params, xulBrowser, principal) {
super(envType, extension);
this.uri = NetUtil.newURI(params.url);
- this.messageManager = messageManager;
+ // This message manager is used by ParentAPIManager to send messages and to
+ // close the ProxyContext if the underlying message manager closes. This
+ // message manager object may change when `xulBrowser` swaps docshells, e.g.
+ // when a tab is moved to a different window.
+ this.currentMessageManager = xulBrowser.messageManager;
+ this._docShellTracker = new BrowserDocshellFollower(xulBrowser,
+ this.onBrowserChange.bind(this));
this.principal_ = principal;
this.apiObj = {};
GlobalManager.injectInObject(this, false, this.apiObj);
this.listenerProxies = new Map();
this.sandbox = Cu.Sandbox(principal, {});
@@ -255,35 +288,49 @@ class ProxyContext extends BaseContext {
get principal() {
return this.principal_;
}
get cloneScope() {
return this.sandbox;
}
+ onBrowserChange(browser) {
+ // Make sure that the message manager is set. Otherwise the ProxyContext may
+ // never be destroyed because the ParentAPIManager would fail to detect that
+ // the message manager is closed.
+ if (!browser.messageManager) {
+ throw new Error("BrowserDocshellFollower: The new browser has no message manager");
+ }
+
+ this.currentMessageManager = browser.messageManager;
+ }
+
shutdown() {
this.unload();
}
unload() {
if (this.unloaded) {
return;
}
+ this._docShellTracker.destroy();
super.unload();
Management.emit("proxy-context-unload", this);
}
}
// The parent ProxyContext of an ExtensionContext in ExtensionChild.jsm.
class ExtensionChildProxyContext extends ProxyContext {
constructor(envType, extension, params, xulBrowser) {
- super(envType, extension, params, xulBrowser.messageManager, extension.principal);
+ super(envType, extension, params, xulBrowser, extension.principal);
this.viewType = params.viewType;
+ // WARNING: The xulBrowser may change when docShells are swapped, e.g. when
+ // the tab moves to a different window.
this.xulBrowser = xulBrowser;
// TODO(robwu): Remove this once all APIs can run in a separate process.
if (params.cloneScopeInProcess) {
this.sandbox = params.cloneScopeInProcess;
}
}
@@ -304,16 +351,21 @@ class ExtensionChildProxyContext extends
if (!Management.global.TabManager) {
return; // Not yet supported on Android.
}
let {gBrowser} = this.xulBrowser.ownerGlobal;
let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
return tab && Management.global.TabManager.getId(tab);
}
+ onBrowserChange(browser) {
+ super.onBrowserChange(browser);
+ this.xulBrowser = browser;
+ }
+
shutdown() {
Management.emit("page-shutdown", this);
super.shutdown();
}
}
function findPathInObject(obj, path, printErrors = true) {
for (let elt of path.split(".")) {
@@ -355,17 +407,17 @@ var ParentAPIManager = {
Services.mm.addMessageListener("API:AddListener", this);
Services.mm.addMessageListener("API:RemoveListener", this);
},
// "message-manager-close" observer.
observe(subject, topic, data) {
let mm = subject;
for (let [childId, context] of this.proxyContexts) {
- if (context.messageManager == mm) {
+ if (context.currentMessageManager == mm) {
this.closeProxyContext(childId);
}
}
},
shutdownExtension(extensionId) {
for (let [childId, context] of this.proxyContexts) {
if (context.extension.id == extensionId) {
@@ -417,17 +469,17 @@ var ParentAPIManager = {
// frame is also the same addon.
if (principal.URI.prePath != extension.baseURI.prePath ||
!target.contentPrincipal.subsumes(principal)) {
Cu.reportError(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
return;
}
context = new ExtensionChildProxyContext(envType, extension, data, target);
} else if (envType == "content_parent") {
- context = new ProxyContext(envType, extension, data, target.messageManager, principal);
+ context = new ProxyContext(envType, extension, data, target, principal);
} else {
Cu.reportError(`Invalid WebExtension context envType: ${envType}`);
return;
}
this.proxyContexts.set(childId, context);
},
closeProxyContext(childId) {
@@ -440,53 +492,60 @@ var ParentAPIManager = {
},
call(data, target) {
let context = this.proxyContexts.get(data.childId);
if (!context) {
Cu.reportError("WebExtension context not found!");
return;
}
+ if (context.currentMessageManager !== target.messageManager) {
+ Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
+ }
+
function callback(...cbArgs) {
let lastError = context.lastError;
- target.messageManager.sendAsyncMessage("API:CallResult", {
+ context.currentMessageManager.sendAsyncMessage("API:CallResult", {
childId: data.childId,
callId: data.callId,
args: cbArgs,
lastError: lastError ? lastError.message : null,
});
}
let args = data.args;
args = Cu.cloneInto(args, context.sandbox);
if (data.callId) {
args = args.concat(callback);
}
try {
findPathInObject(context.apiObj, data.path)(...args);
} catch (e) {
let msg = e.message || "API failed";
- target.messageManager.sendAsyncMessage("API:CallResult", {
+ context.currentMessageManager.sendAsyncMessage("API:CallResult", {
childId: data.childId,
callId: data.callId,
lastError: msg,
});
}
},
addListener(data, target) {
let context = this.proxyContexts.get(data.childId);
if (!context) {
Cu.reportError("WebExtension context not found!");
return;
}
+ if (context.currentMessageManager !== target.messageManager) {
+ Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
+ }
function listener(...listenerArgs) {
- target.messageManager.sendAsyncMessage("API:RunListener", {
+ context.currentMessageManager.sendAsyncMessage("API:RunListener", {
childId: data.childId,
path: data.path,
args: listenerArgs,
});
}
context.listenerProxies.set(data.path, listener);