diff options
Diffstat (limited to 'browser/components/newtab/NewTabWebChannel.jsm')
-rw-r--r-- | browser/components/newtab/NewTabWebChannel.jsm | 299 |
1 files changed, 299 insertions, 0 deletions
diff --git a/browser/components/newtab/NewTabWebChannel.jsm b/browser/components/newtab/NewTabWebChannel.jsm new file mode 100644 index 000000000..40ee73684 --- /dev/null +++ b/browser/components/newtab/NewTabWebChannel.jsm @@ -0,0 +1,299 @@ +/* global + NewTabPrefsProvider, + Services, + EventEmitter, + Preferences, + XPCOMUtils, + WebChannel, + NewTabRemoteResources +*/ +/* exported NewTabWebChannel */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["NewTabWebChannel"]; + +const {utils: Cu} = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider", + "resource:///modules/NewTabPrefsProvider.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NewTabRemoteResources", + "resource:///modules/NewTabRemoteResources.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WebChannel", + "resource://gre/modules/WebChannel.jsm"); +XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() { + const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {}); + return EventEmitter; +}); + +const CHAN_ID = "newtab"; +const PREF_ENABLED = "browser.newtabpage.remote"; +const PREF_MODE = "browser.newtabpage.remote.mode"; + +/** + * NewTabWebChannel is the conduit for all communication with unprivileged newtab instances. + * + * It allows for the ability to broadcast to all newtab browsers. + * If the browser.newtab.remote pref is false, the object will be in an uninitialized state. + * + * Mode choices: + * 'production': pages from our production CDN + * 'staging': pages from our staging CDN + * 'test': intended for tests + * 'test2': intended for tests + * 'dev': intended for development + * + * An unknown mode will result in 'production' mode, which is the default + * + * Incoming messages are expected to be JSON-serialized and in the format: + * + * { + * type: "REQUEST_SCREENSHOT", + * data: { + * url: "https://example.com" + * } + * } + * + * Or: + * + * { + * type: "REQUEST_SCREENSHOT", + * } + * + * Outgoing messages are expected to be objects serializable by structured cloning, in a similar format: + * { + * type: "RECEIVE_SCREENSHOT", + * data: { + * "url": "https://example.com", + * "image": "dataURi:....." + * } + * } + */ +let NewTabWebChannelImpl = function NewTabWebChannelImpl() { + EventEmitter.decorate(this); + this._handlePrefChange = this._handlePrefChange.bind(this); + this._incomingMessage = this._incomingMessage.bind(this); +}; + +NewTabWebChannelImpl.prototype = { + _prefs: {}, + _channel: null, + + // a WeakMap containing browsers as keys and a weak ref to their principal + // as value + _principals: null, + + // a Set containing weak refs to browsers + _browsers: null, + + /* + * Returns current channel's ID + */ + get chanId() { + return CHAN_ID; + }, + + /* + * Returns the number of browsers currently tracking + */ + get numBrowsers() { + return this._getBrowserRefs().length; + }, + + /* + * Returns current channel's origin + */ + get origin() { + if (!(this._prefs.mode in NewTabRemoteResources.MODE_CHANNEL_MAP)) { + this._prefs.mode = "production"; + } + return NewTabRemoteResources.MODE_CHANNEL_MAP[this._prefs.mode].origin; + }, + + /* + * Unloads all browsers and principals + */ + _unloadAll() { + if (this._principals != null) { + this._principals = new WeakMap(); + } + this._browsers = new Set(); + this.emit("targetUnloadAll"); + }, + + /* + * Checks if a browser is known + * + * This will cause an iteration through all known browsers. + * That's ok, we don't expect a lot of browsers + */ + _isBrowserKnown(browser) { + for (let bRef of this._getBrowserRefs()) { + let b = bRef.get(); + if (b && b.permanentKey === browser.permanentKey) { + return true; + } + } + + return false; + }, + + /* + * Obtains all known browser refs + */ + _getBrowserRefs() { + // Some code may try to emit messages after teardown. + if (!this._browsers) { + return []; + } + let refs = []; + for (let bRef of this._browsers) { + /* + * even though we hold a weak ref to browser, it seems that browser + * objects aren't gc'd immediately after a tab closes. They stick around + * in memory, but thankfully they don't have a documentURI in that case + */ + let browser = bRef.get(); + if (browser && browser.documentURI) { + refs.push(bRef); + } else { + // need to clean up principals because the browser object is not gc'ed + // immediately + this._principals.delete(browser); + this._browsers.delete(bRef); + this.emit("targetUnload"); + } + } + return refs; + }, + + /* + * Receives a message from content. + * + * Keeps track of browsers for broadcast, relays messages to listeners. + */ + _incomingMessage(id, message, target) { + if (this.chanId !== id) { + Cu.reportError(new Error("NewTabWebChannel unexpected message destination")); + } + + /* + * need to differentiate by browser, because event targets are created each + * time a message is sent. + */ + if (!this._isBrowserKnown(target.browser)) { + this._browsers.add(Cu.getWeakReference(target.browser)); + this._principals.set(target.browser, Cu.getWeakReference(target.principal)); + this.emit("targetAdd"); + } + + try { + let msg = JSON.parse(message); + this.emit(msg.type, {data: msg.data, target: target}); + } catch (err) { + Cu.reportError(err); + } + }, + + /* + * Sends a message to all known browsers + */ + broadcast(actionType, message) { + for (let bRef of this._getBrowserRefs()) { + let browser = bRef.get(); + try { + let principal = this._principals.get(browser).get(); + if (principal && browser && browser.documentURI) { + this._channel.send({type: actionType, data: message}, {browser, principal}); + } + } catch (e) { + Cu.reportError(new Error("NewTabWebChannel WeakRef is dead")); + this._principals.delete(browser); + } + } + }, + + /* + * Sends a message to a specific target + */ + send(actionType, message, target) { + try { + this._channel.send({type: actionType, data: message}, target); + } catch (e) { + // Web Channel might be dead + Cu.reportError(e); + } + }, + + /* + * Pref change observer callback + */ + _handlePrefChange(prefName, newState, forceState) { // eslint-disable-line no-unused-vars + switch (prefName) { + case PREF_ENABLED: + if (!this._prefs.enabled && newState) { + // changing state from disabled to enabled + this.setupState(); + } else if (this._prefs.enabled && !newState) { + // changing state from enabled to disabled + this.tearDownState(); + } + break; + case PREF_MODE: + if (this._prefs.mode !== newState) { + // changing modes + this.tearDownState(); + this.setupState(); + } + break; + } + }, + + /* + * Sets up the internal state + */ + setupState() { + this._prefs.enabled = Preferences.get(PREF_ENABLED, false); + + let mode = Preferences.get(PREF_MODE, "production"); + if (!(mode in NewTabRemoteResources.MODE_CHANNEL_MAP)) { + mode = "production"; + } + this._prefs.mode = mode; + this._principals = new WeakMap(); + this._browsers = new Set(); + + if (this._prefs.enabled) { + this._channel = new WebChannel(this.chanId, Services.io.newURI(this.origin, null, null)); + this._channel.listen(this._incomingMessage); + } + }, + + tearDownState() { + if (this._channel) { + this._channel.stopListening(); + } + this._prefs = {}; + this._unloadAll(); + this._channel = null; + this._principals = null; + this._browsers = null; + }, + + init() { + this.setupState(); + NewTabPrefsProvider.prefs.on(PREF_ENABLED, this._handlePrefChange); + NewTabPrefsProvider.prefs.on(PREF_MODE, this._handlePrefChange); + }, + + uninit() { + this.tearDownState(); + NewTabPrefsProvider.prefs.off(PREF_ENABLED, this._handlePrefChange); + NewTabPrefsProvider.prefs.off(PREF_MODE, this._handlePrefChange); + } +}; + +let NewTabWebChannel = new NewTabWebChannelImpl(); |