forked from mozilla/testpilot
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use BroadcastChannel to allow WebExtensions to communicate with Test …
…Pilot - New lib/webextension-channels module to manage channels for every enabled experiment - Each "channel" creates a background page in the WebExtension's origin and listens for BroadcastChannel messages from the WebExtension - Example WebExtension that uses the BroadcastChannel Fixes mozilla#433
- Loading branch information
Showing
4 changed files
with
211 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
/* | ||
* This Source Code is subject to the terms of the Mozilla Public License | ||
* version 2.0 (the 'License'). You can obtain a copy of the License at | ||
* http://mozilla.org/MPL/2.0/. | ||
*/ | ||
|
||
/* global XPCOMUtils, Services */ | ||
|
||
const { Ci, Cu } = require('chrome'); | ||
const { Class } = require('sdk/core/heritage'); | ||
const { Disposable } = require('sdk/core/disposable'); | ||
|
||
const store = require('sdk/simple-storage').storage; | ||
|
||
const Metrics = require('./metrics'); | ||
|
||
const TESTPILOT_TELEMETRY_CHANNEL = 'testpilot-telemetry'; | ||
|
||
Cu.import('resource://gre/modules/XPCOMUtils.jsm'); | ||
|
||
XPCOMUtils.defineLazyModuleGetter(this, 'Services', | ||
'resource://gre/modules/Services.jsm'); | ||
|
||
const {getExtensionUUID} = Cu.import('resource://gre/modules/Extension.jsm', {}); | ||
|
||
function createChannelForAddonId(name, addonId) { | ||
// The BroadcastChannel API allows messaging between different windows that | ||
// share the same origin. Bug 1186732 extended this to WebExtensions (which | ||
// may not have an origin) by adding a special URL that loads an about:blank | ||
// page at the (generalized) "origin" of the extension. | ||
// | ||
// Load that about:blank page, and use its `window` to get a BroadcastChannel | ||
// that allows two-way communication between the main Test Pilot extension and | ||
// individual experiment extensions. | ||
|
||
// Note: the `targetExtensionUUID` is different for each copy of Firefox, | ||
// so that malicious websites can't guess the special URL associated with | ||
// an extension. | ||
const targetExtensionUUID = getExtensionUUID(addonId); | ||
|
||
// Create the special about:blank URL for the extension. | ||
const baseURI = Services.io | ||
.newURI(`moz-extension://${targetExtensionUUID}/_blank.html`, null, null); | ||
|
||
// Create a principal (security context) for the generalized origin given | ||
// by the extension's special URL and its `addonId`. | ||
const principal = Services.scriptSecurityManager | ||
.createCodebasePrincipal(baseURI, { addonId }); | ||
|
||
// Create a hidden window and open the special about:blank page for the | ||
// extension. | ||
const addonChromeWebNav = Services.appShell.createWindowlessBrowser(true); | ||
const docShell = addonChromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor) // eslint-disable-line new-cap | ||
.getInterface(Ci.nsIDocShell); | ||
docShell.createAboutBlankContentViewer(principal); | ||
const window = docShell.contentViewer.DOMDocument.defaultView; | ||
|
||
// Finally, get the BroadcastChannel associated with the extension. | ||
const addonBroadcastChannel = new window.BroadcastChannel(name); | ||
|
||
// Callers need to keep the pointer to the window, otherwise the window's | ||
// BroadcastChannel will get garbage collected. | ||
return { | ||
addonChromeWebNav, | ||
addonBroadcastChannel | ||
}; | ||
} | ||
|
||
const WebExtensionChannel = Class({ // eslint-disable-line new-cap | ||
implements: [Disposable], | ||
|
||
initialize(targetAddonId) { | ||
this.pingListeners = new Set(); | ||
|
||
this.targetAddonId = targetAddonId; | ||
|
||
const { | ||
addonChromeWebNav, | ||
addonBroadcastChannel | ||
} = createChannelForAddonId(TESTPILOT_TELEMETRY_CHANNEL, targetAddonId); | ||
|
||
// NOTE: Keep a ref to prevent it from going away during garbage collection | ||
// (or the BroadcastChannel will stop working). | ||
this.addonChromeWebNav = addonChromeWebNav; | ||
this.addonBroadcastChannel = addonBroadcastChannel; | ||
|
||
this.handleEventBound = ev => this.handleEvent(ev); | ||
}, | ||
|
||
dispose() { | ||
this.addonBroadcastChannel.removeEventListener('message', this.handleEventBound); | ||
this.addonBroadcastChannel.close(); | ||
this.addonChromeWebNav.close(); | ||
this.pingListeners.clear(); | ||
|
||
this.addonBroadcastChannel = null; | ||
this.addonChromeWebNav = null; | ||
}, | ||
|
||
registerPingListener(callback) { | ||
this.pingListeners.add(callback); | ||
|
||
if (this.pingListeners.size >= 0) { | ||
this.addonBroadcastChannel.addEventListener('message', this.handleEventBound); | ||
} | ||
}, | ||
|
||
unregisterPingListener(callback) { | ||
this.pingListeners.delete(callback); | ||
|
||
if (this.pingListeners.size === 0) { | ||
this.addonBroadcastChannel.removeEventListener('message', this.handleEventBound); | ||
} | ||
}, | ||
|
||
handleEvent(event) { | ||
if (event.data) { | ||
this.notifyPing(event.data, {addonId: this.targetAddonId}); | ||
} | ||
}, | ||
|
||
notifyPing(data, sender) { | ||
for (let pingListener of this.pingListeners) { // eslint-disable-line prefer-const | ||
try { | ||
pingListener({ | ||
senderAddonId: sender.addonId, | ||
testpilotPingData: data | ||
}); | ||
} catch (err) { | ||
console.error('Error executing pingListener', err); // eslint-disable-line no-console | ||
} | ||
} | ||
} | ||
}); | ||
|
||
let channels = {}; | ||
|
||
module.exports = { | ||
WebExtensionChannel, | ||
|
||
// Update all the channels on init. | ||
init() { | ||
this.updateExperimentChannels(); | ||
}, | ||
|
||
// Drop refs to channels for garbage collection | ||
destroy() { | ||
channels = {}; | ||
}, | ||
|
||
// Rebuild channels for all known experiments | ||
updateExperimentChannels() { | ||
channels = {}; | ||
if (store.installedAddons) { | ||
Object.keys(store.installedAddons).forEach(id => { | ||
const channel = new WebExtensionChannel(id); | ||
channels[id] = channel; | ||
channel.registerPingListener(data => | ||
this.handleWebExtensionPing(id, data)); | ||
}); | ||
} | ||
}, | ||
|
||
// Pass a ping message along to Telemetry via Metrics | ||
handleWebExtensionPing(id, data) { | ||
Metrics.onExperimentPing({ | ||
subject: id, | ||
data: JSON.stringify(data) | ||
}); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
var TESTPILOT_TELEMETRY_CHANNEL = 'testpilot-telemetry'; | ||
var testpilotPingChannel = new BroadcastChannel(TESTPILOT_TELEMETRY_CHANNEL); | ||
setInterval(function () { | ||
testpilotPingChannel.postMessage({ | ||
boolData: true, | ||
arrayOfData: ["one", "two", "three"], | ||
nestedData: { | ||
intData: 10, | ||
}, | ||
}); | ||
console.log("TEST PILOT PING SENT", Date.now()); | ||
}, 5000); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"manifest_version": 2, | ||
"name": "Test Pilot WebExtension Example", | ||
"version": "1.0", | ||
"description": "This is a WebExtension built as an example Test Pilot experiment", | ||
"icons": { | ||
"32": "icons/icon-32.png" | ||
}, | ||
"permissions": ["background"], | ||
"applications": { | ||
"gecko": { | ||
"id": "[email protected]", | ||
"strict_min_version": "45.0" | ||
} | ||
}, | ||
"background": { | ||
"scripts": ["background.js"] | ||
} | ||
} |