Bug 1370368 - Disconnect port upon pagehide
MozReview-Commit-ID: HeBcejthn6p
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -157,16 +157,17 @@ class Port {
this.disconnectHandler = Object.assign({
receiveMessage: ({data}) => this.disconnectByOtherEnd(data),
}, this.handlerBase);
MessageChannel.addListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
this.context.callOnClose(this);
+ this.registerCloseOnPagehide();
}
api() {
let portObj = Cu.createObjectIn(this.context.cloneScope);
let portError = null;
let publicAPI = {
name: this.name,
@@ -265,16 +266,38 @@ class Port {
this.unregisterMessageFuncs.delete(unregister);
MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
};
MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
this.unregisterMessageFuncs.add(unregister);
return unregister;
}
+ registerCloseOnPagehide() {
+ let {contentWindow} = this.context;
+ if (!contentWindow) {
+ return;
+ }
+
+ let onPageHide = (event) => {
+ if (event.persisted) {
+ // If the page is moved to the bfcache, then scripts in the page will be
+ // suspended and the port becomes unreachable from the extension's
+ // perspective. Explicitly disconnect the port when that happens.
+ this.disconnect();
+ }
+ };
+ let unregister = () => {
+ this.unregisterMessageFuncs.delete(unregister);
+ contentWindow.removeEventListener("pagehide", onPageHide, {mozSystemGroup: true});
+ };
+ contentWindow.addEventListener("pagehide", onPageHide, {mozSystemGroup: true, once: true}, false);
+ this.unregisterMessageFuncs.add(unregister);
+ }
+
_sendMessage(message, data) {
let options = {
recipient: Object.assign({}, this.recipient, {portId: this.id}),
responseType: MessageChannel.RESPONSE_NONE,
};
let holder = new StructuredCloneHolder(data);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_port_disconnect_upon_navigation.js
@@ -0,0 +1,100 @@
+"use strict";
+
+const server = createHttpServer({hosts: ["example.com"]});
+
+server.registerPathHandler("/dummy", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.write("<!DOCTYPE html><html></html>");
+});
+
+add_task(async function testConnect_and_navigate_window() {
+ function background() {
+ browser.runtime.onConnect.addListener(port => {
+ let messages = [];
+ port.onMessage.addListener(msg => {
+ messages.push(msg);
+ });
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(null, port.error, "Disconnect without error");
+ browser.test.assertEq("pageshown,pagehidden", messages.join(","), "expected messages");
+ browser.test.sendMessage("port-disconnected");
+ });
+ browser.test.sendMessage("port-connected");
+ });
+ browser.test.sendMessage("bg-ready");
+ }
+ function contentScript() {
+ let port = browser.runtime.connect();
+ port.onDisconnect.addListener(() => {
+ // The background page has an onConnect listener and never disconnects
+ // the port. The port is disconnected when the page unloads, but that
+ // shouldn't trigger the port.onDisconnect event.
+ browser.test.fail("port.onDisconnect should not be fired");
+ });
+
+ // eslint-disable-next-line mozilla/balanced-listeners
+ window.addEventListener("pagehide", () => {
+ browser.test.sendMessage("content-script-hide");
+ try {
+ port.postMessage("pagehidden");
+ } catch (e) {
+ browser.test.sendMessage(`pagehide error: ${e.message}`);
+ }
+ });
+ // eslint-disable-next-line mozilla/balanced-listeners
+ window.addEventListener("pageshow", () => {
+ browser.test.sendMessage("content-script-show");
+ try {
+ port.postMessage("pageshown");
+ } catch (e) {
+ browser.test.sendMessage(`pageshow error: ${e.message}`);
+ }
+ });
+ browser.test.sendMessage("content-script-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ content_scripts: [{
+ "matches": ["http://example.com/dummy"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("bg-ready");
+
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://example.com/dummy");
+ await extension.awaitMessage("content-script-ready");
+ await extension.awaitMessage("port-connected");
+ await extension.awaitMessage("content-script-show");
+ await contentPage.spawn(null, () => {
+ // Navigate so that the content page is hidden in the bfcache.
+ this.content.location = "data:text/plain;charset=utf-8,something-else";
+ });
+ await extension.awaitMessage("content-script-hide");
+ await extension.awaitMessage("port-disconnected");
+
+ await contentPage.spawn(null, () => {
+ // Navigate back so the content page is resurrected from the bfcache.
+ this.content.history.back();
+ });
+
+ await extension.awaitMessage("content-script-show");
+ await extension.awaitMessage("pageshow error: Attempt to postMessage on disconnected port");
+
+ await contentPage.close();
+ await extension.awaitMessage("content-script-hide");
+ await extension.awaitMessage("pagehide error: Attempt to postMessage on disconnected port");
+ await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -57,16 +57,17 @@ skip-if = os == "android" # checking for
[test_ext_localStorage.js]
[test_ext_management.js]
skip-if = (os == "win" && !debug) #Bug 1419183 disable on Windows
[test_ext_management_uninstall_self.js]
[test_ext_onmessage_removelistener.js]
skip-if = true # This test no longer tests what it is meant to test.
[test_ext_permission_xhr.js]
[test_ext_persistent_events.js]
+[test_ext_port_disconnect_upon_navigation.js]
[test_ext_privacy.js]
[test_ext_privacy_disable.js]
[test_ext_privacy_update.js]
[test_ext_proxy_auth.js]
[test_ext_proxy_config.js]
[test_ext_proxy_onauthrequired.js]
[test_ext_proxy_settings.js]
skip-if = os == "android" # proxy settings are not supported on android