Bug 1284942: Don't fire message listeners for windows hidden in bfcache. r?aswan
MozReview-Commit-ID: KSqLt7Qqdkf
--- a/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js
@@ -107,16 +107,102 @@ add_task(function* tabsSendMessageReply(
yield extension.startup();
yield extension.awaitFinish("sendMessage");
yield extension.unload();
});
+
+add_task(function* tabsSendHidden() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": ["tabs"],
+
+ "content_scripts": [{
+ "matches": ["http://example.com/content*"],
+ "js": ["content-script.js"],
+ "run_at": "document_start",
+ }],
+ },
+
+ background: function() {
+ let resolveContent;
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg[0] == "content-ready") {
+ resolveContent(msg[1]);
+ }
+ });
+
+ let awaitContent = url => {
+ return new Promise(resolve => {
+ resolveContent = resolve;
+ }).then(result => {
+ browser.test.assertEq(url, result, "Expected content script URL");
+ });
+ };
+
+ const URL1 = "http://example.com/content1.html";
+ const URL2 = "http://example.com/content2.html";
+ browser.tabs.create({url: URL1}).then(tab => {
+ return awaitContent(URL1).then(() => {
+ return browser.tabs.sendMessage(tab.id, URL1);
+ }).then(url => {
+ browser.test.assertEq(URL1, url, "Should get response from expected content window");
+
+ return browser.tabs.update(tab.id, {url: URL2});
+ }).then(() => {
+ return awaitContent(URL2);
+ }).then(() => {
+ return browser.tabs.sendMessage(tab.id, URL2);
+ }).then(url => {
+ browser.test.assertEq(URL2, url, "Should get response from expected content window");
+
+ // Repeat once just to be sure the first message was processed by all
+ // listeners before we exit the test.
+ return browser.tabs.sendMessage(tab.id, URL2);
+ }).then(url => {
+ browser.test.assertEq(URL2, url, "Should get response from expected content window");
+
+ return browser.tabs.remove(tab.id);
+ });
+ }).then(() => {
+ browser.test.notifyPass("contentscript-bfcache-window");
+ }).catch(error => {
+ browser.test.fail(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("contentscript-bfcache-window");
+ });
+ },
+
+ files: {
+ "content-script.js": function() {
+ // Store this in a local variable to make sure we don't touch any
+ // properties of the possibly-hidden content window.
+ let href = window.location.href;
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.assertEq(href, msg, "Should be in the expected content window");
+
+ return Promise.resolve(href);
+ });
+
+ browser.runtime.sendMessage(["content-ready", href]);
+ },
+ },
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("contentscript-bfcache-window");
+
+ yield extension.unload();
+});
+
+
add_task(function* tabsSendMessageNoExceptionOnNonExistentTab() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": ["tabs"],
},
background: function() {
let url = "http://example.com/mochitest/browser/browser/components/extensions/test/browser/file_dummy.html";
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -319,16 +319,20 @@ class ExtensionContext extends BaseConte
super(extensionId);
let {isExtensionPage} = contextOptions;
this.isExtensionPage = isExtensionPage;
this.extension = ExtensionManager.get(extensionId);
this.extensionId = extensionId;
this.contentWindow = contentWindow;
+ this.windowId = getInnerWindowID(contentWindow);
+
+ contentWindow.addEventListener("pageshow", this, true);
+ contentWindow.addEventListener("pagehide", this, true);
let frameId = WebNavigationFrames.getFrameId(contentWindow);
this.frameId = frameId;
this.scripts = [];
let mm = getWindowMessageManager(contentWindow);
this.messageManager = mm;
@@ -366,17 +370,17 @@ class ExtensionContext extends BaseConte
wantXrays: false,
isWebExtensionContentScript: true,
});
} else {
// This metadata is required by the Developer Tools, in order for
// the content script to be associated with both the extension and
// the tab holding the content page.
let metadata = {
- "inner-window-id": getInnerWindowID(contentWindow),
+ "inner-window-id": this.windowId,
addonId: attrs.addonId,
};
this.sandbox = Cu.Sandbox(prin, {
metadata,
sandboxPrototype: contentWindow,
wantXrays: true,
isWebExtensionContentScript: true,
@@ -425,16 +429,24 @@ class ExtensionContext extends BaseConte
// This is an iframe with content script API enabled. (See Bug 1214658 for rationale)
if (isExtensionPage) {
Cu.waiveXrays(this.contentWindow).chrome = this.chromeObj;
Cu.waiveXrays(this.contentWindow).browser = this.chromeObj;
}
}
+ handleEvent(event) {
+ if (event.type == "pageshow") {
+ this.active = true;
+ } else if (event.type == "pagehide") {
+ this.active = false;
+ }
+ }
+
get cloneScope() {
return this.sandbox;
}
execute(script, shouldRun) {
script.tryInject(this.contentWindow, this.sandbox, shouldRun);
}
@@ -457,16 +469,21 @@ class ExtensionContext extends BaseConte
// Don't bother saving scripts after document_idle.
this.scripts = this.scripts.filter(script => script.requiresCleanup);
}
}
close() {
super.unload();
+ if (this.windowId === getInnerWindowID(this.contentWindow)) {
+ this.contentWindow.removeEventListener("pageshow", this, true);
+ this.contentWindow.removeEventListener("pagehide", this, true);
+ }
+
for (let script of this.scripts) {
if (script.requiresCleanup) {
script.cleanup(this.contentWindow);
}
}
this.childManager.close();
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -148,16 +148,17 @@ class BaseContext {
constructor(extensionId) {
this.onClose = new Set();
this.checkedLastError = false;
this._lastError = null;
this.contextId = ++gContextId;
this.unloaded = false;
this.extensionId = extensionId;
this.jsonSandbox = null;
+ this.active = true;
}
get cloneScope() {
throw new Error("Not implemented");
}
get principal() {
throw new Error("Not implemented");
@@ -999,17 +1000,20 @@ Port.prototype = {
this.disconnectListeners.add(listener);
return () => {
this.disconnectListeners.delete(listener);
};
}).api(),
onMessage: new EventManager(this.context, "Port.onMessage", fire => {
let listener = ({data}) => {
- if (!this.disconnected) {
+ if (!this.context.active) {
+ // TODO: Send error as a response.
+ Cu.reportError("Message received on port for an inactive content script");
+ } else if (!this.disconnected) {
fire(data);
}
};
this.messageManager.addMessageListener(this.listenerName, listener);
return () => {
this.messageManager.removeMessageListener(this.listenerName, listener);
};
@@ -1114,16 +1118,20 @@ Messenger.prototype = {
},
onMessage(name) {
return new SingletonEventManager(this.context, name, callback => {
let listener = {
messageFilterPermissive: this.filter,
receiveMessage: ({target, data: message, sender, recipient}) => {
+ if (!this.context.active) {
+ return;
+ }
+
if (this.delegate) {
this.delegate.getSender(this.context, target, sender);
}
let sendResponse;
let response = undefined;
let promise = new Promise(resolve => {
sendResponse = value => {