Bug 1239118 - Send prefs to Remote Newtab page using WebChannel r?ursula draft
authorOlivier Yiptong <olivier@olivieryiptong.com>
Thu, 03 Mar 2016 22:20:23 -0500
changeset 338191 c73060a32dd2cf1a17dec421a76e0919637a0052
parent 337003 04a4f82f4b74c72e52e2aa2afbc1684822707401
child 338193 d1e711283895a5a0ad88689af9852202b897efcd
push id12462
push userolivier@olivieryiptong.com
push dateTue, 08 Mar 2016 19:09:39 +0000
reviewersursula
bugs1239118
milestone47.0a1
Bug 1239118 - Send prefs to Remote Newtab page using WebChannel r?ursula MozReview-Commit-ID: CQQQmgrXSDt
browser/components/newtab/NewTabMessages.jsm
browser/components/newtab/NewTabPrefsProvider.jsm
browser/components/newtab/NewTabWebChannel.jsm
browser/components/newtab/moz.build
browser/components/newtab/tests/browser/browser.ini
browser/components/newtab/tests/browser/browser_newtabmessages.js
browser/components/newtab/tests/browser/browser_newtabwebchannel.js
browser/components/newtab/tests/browser/newtabmessages_prefs.html
browser/components/newtab/tests/browser/newtabwebchannel_basic.html
browser/components/nsBrowserGlue.js
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/NewTabMessages.jsm
@@ -0,0 +1,95 @@
+/*global
+  NewTabWebChannel,
+  NewTabPrefsProvider,
+  Preferences,
+  XPCOMUtils
+*/
+
+/* exported NewTabMessages */
+
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+                                  "resource:///modules/NewTabPrefsProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
+                                  "resource:///modules/NewTabWebChannel.jsm");
+
+this.EXPORTED_SYMBOLS = ["NewTabMessages"];
+
+const PREF_ENABLED = "browser.newtabpage.remote";
+
+// Action names are from the content's perspective. in from chrome == out from content
+// Maybe replace the ACTION objects by a bi-directional Map a bit later?
+const ACTIONS = {
+  prefs: {
+    inPrefs: "REQUEST_PREFS",
+    outPrefs: "RECEIVE_PREFS",
+    action_types: new Set(["REQUEST_PREFS", "RECEIVE_PREFS"]),
+  }
+};
+
+let NewTabMessages = {
+
+  _prefs: {},
+
+  /** NEWTAB EVENT HANDLERS **/
+
+  /*
+   * Return to the originator all newtabpage prefs. A point-to-point request.
+   */
+  handlePrefRequest(actionName, {target}) {
+    if (ACTIONS.prefs.action_types.has(actionName)) {
+      let results = NewTabPrefsProvider.prefs.newtabPagePrefs;
+      NewTabWebChannel.send(ACTIONS.prefs.outPrefs, results, target);
+    }
+  },
+
+  /*
+   * Broadcast preference changes to all open newtab pages
+   */
+  handlePrefChange(actionName, value) {
+    let prefChange = {};
+    prefChange[actionName] = value;
+    NewTabWebChannel.broadcast(ACTIONS.prefs.outPrefs, prefChange);
+  },
+
+  _handleEnabledChange(prefName, value) {
+    if (prefName === PREF_ENABLED) {
+      if (this._prefs.enabled && !value) {
+        this.uninit();
+      } else if (!this._prefs.enabled && value) {
+        this.init();
+      }
+    }
+  },
+
+  init() {
+    this._prefs.enabled = Preferences.get(PREF_ENABLED, false);
+
+    if (this._prefs.enabled) {
+      NewTabWebChannel.on(ACTIONS.prefs.inPrefs, this.handlePrefRequest.bind(this));
+      NewTabPrefsProvider.prefs.on(PREF_ENABLED, this._handleEnabledChange.bind(this));
+
+      for (let pref of NewTabPrefsProvider.newtabPagePrefSet) {
+        NewTabPrefsProvider.prefs.on(pref, this.handlePrefChange.bind(this));
+      }
+    }
+  },
+
+  uninit() {
+    this._prefs.enabled = Preferences.get(PREF_ENABLED, false);
+
+    if (this._prefs.enabled) {
+      NewTabPrefsProvider.prefs.off(PREF_ENABLED, this._handleEnabledChange);
+
+      NewTabWebChannel.off(ACTIONS.prefs.inPrefs, this.handlePrefRequest);
+      for (let pref of NewTabPrefsProvider.newtabPagePrefSet) {
+        NewTabPrefsProvider.prefs.off(pref, this.handlePrefChange);
+      }
+    }
+  }
+};
--- a/browser/components/newtab/NewTabPrefsProvider.jsm
+++ b/browser/components/newtab/NewTabPrefsProvider.jsm
@@ -16,21 +16,34 @@ XPCOMUtils.defineLazyGetter(this, "Event
 });
 
 // Supported prefs and data type
 const gPrefsMap = new Map([
   ["browser.newtabpage.remote", "bool"],
   ["browser.newtabpage.remote.mode", "str"],
   ["browser.newtabpage.enabled", "bool"],
   ["browser.newtabpage.enhanced", "bool"],
+  ["browser.newtabpage.introShown", "bool"],
+  ["browser.newtabpage.updateIntroShown", "bool"],
   ["browser.newtabpage.pinned", "str"],
+  ["browser.newtabpage.blocked", "str"],
   ["intl.locale.matchOS", "bool"],
   ["general.useragent.locale", "localized"],
 ]);
 
+// prefs that are important for the newtab page
+const gNewtabPagePrefs = new Set([
+  "browser.newtabpage.enabled",
+  "browser.newtabpage.enhanced",
+  "browser.newtabpage.pinned",
+  "browser.newtabpage.blocked",
+  "browser.newtabpage.introShown",
+  "browser.newtabpage.updateIntroShown"
+]);
+
 let PrefsProvider = function PrefsProvider() {
   EventEmitter.decorate(this);
 };
 
 PrefsProvider.prototype = {
 
   observe(subject, topic, data) { // jshint ignore:line
     if (topic === "nsPref:changed") {
@@ -54,16 +67,27 @@ PrefsProvider.prototype = {
             break;
         }
       }
     } else {
       Cu.reportError(new Error("NewTabPrefsProvider observing unknown topic"));
     }
   },
 
+  /*
+   * Return the preferences that are important to the newtab page
+   */
+  get newtabPagePrefs() {
+    let results = {};
+    for (let pref of gNewtabPagePrefs) {
+      results[pref] = Preferences.get(pref, null);
+    }
+    return results;
+  },
+
   get prefsMap() {
     return gPrefsMap;
   },
 
   init() {
     for (let pref of gPrefsMap.keys()) {
       Services.prefs.addObserver(pref, this, false);
     }
@@ -78,9 +102,10 @@ PrefsProvider.prototype = {
 
 /**
  * Singleton that serves as the default new tab pref provider for the grid.
  */
 const gPrefs = new PrefsProvider();
 
 let NewTabPrefsProvider = {
   prefs: gPrefs,
+  newtabPagePrefSet: gNewtabPagePrefs,
 };
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/NewTabWebChannel.jsm
@@ -0,0 +1,295 @@
+/* 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://gre/modules/devtools/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() {
+    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();
--- a/browser/components/newtab/moz.build
+++ b/browser/components/newtab/moz.build
@@ -6,19 +6,21 @@
 
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 
 XPCSHELL_TESTS_MANIFESTS += [
     'tests/xpcshell/xpcshell.ini',
 ]
 
 EXTRA_JS_MODULES += [
+    'NewTabMessages.jsm',
     'NewTabPrefsProvider.jsm',
     'NewTabRemoteResources.jsm',
     'NewTabURL.jsm',
+    'NewTabWebChannel.jsm',
     'PlacesProvider.jsm'
 ]
 
 XPIDL_SOURCES += [
     'nsIAboutNewTabService.idl',
 ]
 
 XPIDL_MODULE = 'browser-newtab'
--- a/browser/components/newtab/tests/browser/browser.ini
+++ b/browser/components/newtab/tests/browser/browser.ini
@@ -1,6 +1,10 @@
 [DEFAULT]
 support-files =
   dummy_page.html
+  newtabwebchannel_basic.html
+  newtabmessages_prefs.html
 
 [browser_remotenewtab_pageloads.js]
 [browser_newtab_overrides.js]
+[browser_newtabmessages.js]
+[browser_newtabwebchannel.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/browser/browser_newtabmessages.js
@@ -0,0 +1,57 @@
+/* globals Cu, XPCOMUtils, Preferences, is, registerCleanupFunction, NewTabWebChannel */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
+                                  "resource:///modules/NewTabWebChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabMessages",
+                                  "resource:///modules/NewTabMessages.jsm");
+
+function setup() {
+  Preferences.set("browser.newtabpage.enhanced", true);
+  Preferences.set("browser.newtabpage.remote.mode", "test");
+  Preferences.set("browser.newtabpage.remote", true);
+  NewTabMessages.init();
+}
+
+function cleanup() {
+  NewTabMessages.uninit();
+  NewTabWebChannel.tearDownState();
+  Preferences.set("browser.newtabpage.remote", false);
+  Preferences.set("browser.newtabpage.remote.mode", "production");
+}
+registerCleanupFunction(cleanup);
+
+/*
+ * Sanity tests for pref messages
+ */
+add_task(function* prefMessages_request() {
+  setup();
+  let testURL = "https://example.com/browser/browser/components/newtab/tests/browser/newtabmessages_prefs.html";
+
+  let tabOptions = {
+    gBrowser,
+    url: testURL
+  };
+
+  let prefResponseAck = new Promise(resolve => {
+    NewTabWebChannel.once("responseAck", () => {
+      ok(true, "a request response has been received");
+      resolve();
+    });
+  });
+
+  yield BrowserTestUtils.withNewTab(tabOptions, function*() {
+    yield prefResponseAck;
+    let prefChangeAck = new Promise(resolve => {
+      NewTabWebChannel.once("responseAck", () => {
+        ok(true, "a change response has been received");
+        resolve();
+      });
+    });
+    Preferences.set("browser.newtabpage.enhanced", false);
+    yield prefChangeAck;
+  });
+  cleanup();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/browser/browser_newtabwebchannel.js
@@ -0,0 +1,232 @@
+/* globals XPCOMUtils, Cu, Preferences, NewTabWebChannel, is, registerCleanupFunction */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
+                                  "resource:///modules/NewTabWebChannel.jsm");
+
+const TEST_URL = "https://example.com/browser/browser/components/newtab/tests/browser/newtabwebchannel_basic.html";
+const TEST_URL_2 = "http://mochi.test:8888/browser/browser/components/newtab/tests/browser/newtabwebchannel_basic.html";
+
+function cleanup() {
+  NewTabWebChannel.tearDownState();
+  Preferences.set("browser.newtabpage.remote", false);
+  Preferences.set("browser.newtabpage.remote.mode", "production");
+}
+registerCleanupFunction(cleanup);
+
+/*
+ * Tests flow of messages from newtab to chrome and chrome to newtab
+ */
+add_task(function* open_webchannel_basic() {
+  Preferences.set("browser.newtabpage.remote.mode", "test");
+  Preferences.set("browser.newtabpage.remote", true);
+
+  let tabOptions = {
+    gBrowser,
+    url: TEST_URL
+  };
+
+  let messagePromise = new Promise(resolve => {
+    NewTabWebChannel.once("foo", function(name, msg) {
+      is(name, "foo", "Correct message type sent: foo");
+      is(msg.data, "bar", "Correct data sent: bar");
+      resolve(msg.target);
+    });
+  });
+
+  let replyPromise = new Promise(resolve => {
+    NewTabWebChannel.once("reply", function(name, msg) {
+      is(name, "reply", "Correct message type sent: reply");
+      is(msg.data, "quuz", "Correct data sent: quuz");
+      resolve(msg.target);
+    });
+  });
+
+  let unloadPromise = new Promise(resolve => {
+    NewTabWebChannel.once("targetUnload", function(name) {
+      is(name, "targetUnload", "Correct message type sent: targetUnload");
+      resolve();
+    });
+  });
+
+  is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+  yield BrowserTestUtils.withNewTab(tabOptions, function*(browser) {
+    let target = yield messagePromise;
+    is(NewTabWebChannel.numBrowsers, 1, "One target expected");
+    is(target.browser, browser, "Same browser");
+    NewTabWebChannel.send("respond", null, target);
+    yield replyPromise;
+  });
+
+  Cu.forceGC();
+  is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+  yield unloadPromise;
+  cleanup();
+});
+
+/*
+ * Tests message broadcast reaches all open newtab pages
+ */
+add_task(function* webchannel_broadcast() {
+  Preferences.set("browser.newtabpage.remote.mode", "test");
+  Preferences.set("browser.newtabpage.remote", true);
+
+  let countingMessagePromise = new Promise(resolve => {
+    let count = 0;
+    NewTabWebChannel.on("foo", function test_message(name, msg) {
+      count += 1;
+      if (count === 2) {
+        NewTabWebChannel.off("foo", test_message);
+        resolve(msg.target);
+      }
+    }.bind(this));
+  });
+
+  let countingReplyPromise = new Promise(resolve => {
+    let count = 0;
+    NewTabWebChannel.on("reply", function test_message(name, msg) {
+      count += 1;
+      if (count === 2) {
+        NewTabWebChannel.off("reply", test_message);
+        resolve(msg.target);
+      }
+    }.bind(this));
+  });
+
+  let countingUnloadPromise = new Promise(resolve => {
+    let count = 0;
+    NewTabWebChannel.on("targetUnload", function test_message() {
+      count += 1;
+      if (count === 2) {
+        NewTabWebChannel.off("targetUnload", test_message);
+        resolve();
+      }
+    });
+  });
+
+  let tabs = [];
+  is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+  tabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL));
+  tabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL));
+
+  yield countingMessagePromise;
+  is(NewTabWebChannel.numBrowsers, 2, "Two targets expected");
+
+  NewTabWebChannel.broadcast("respond", null);
+  yield countingReplyPromise;
+
+  for (let tab of tabs) {
+    yield BrowserTestUtils.removeTab(tab);
+  }
+  Cu.forceGC();
+
+  is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+  yield countingUnloadPromise;
+  cleanup();
+});
+
+/*
+ * Tests switching modes
+ */
+add_task(function* webchannel_switch() {
+  Preferences.set("browser.newtabpage.remote.mode", "test");
+  Preferences.set("browser.newtabpage.remote", true);
+
+  function newMessagePromise() {
+    return new Promise(resolve => {
+      NewTabWebChannel.once("foo", function(name, msg) {
+        resolve(msg.target);
+      }.bind(this));
+    });
+  }
+
+  let replyCount = 0;
+  let replyPromise = new Promise(resolve => {
+    NewTabWebChannel.on("reply", function() {
+      replyCount += 1;
+      resolve();
+    }.bind(this));
+  });
+
+  let unloadPromise = new Promise(resolve => {
+    NewTabWebChannel.once("targetUnload", function() {
+      resolve();
+    });
+  });
+
+  let unloadAllPromise = new Promise(resolve => {
+    NewTabWebChannel.once("targetUnloadAll", function() {
+      resolve();
+    });
+  });
+
+  let tabs = [];
+  let messagePromise;
+  is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+
+  messagePromise = newMessagePromise();
+  tabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL));
+  yield messagePromise;
+  is(NewTabWebChannel.numBrowsers, 1, "Correct number of targets");
+
+  messagePromise = newMessagePromise();
+  Preferences.set("browser.newtabpage.remote.mode", "test2");
+  tabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL_2));
+  yield unloadAllPromise;
+  yield messagePromise;
+  is(NewTabWebChannel.numBrowsers, 1, "Correct number of targets");
+
+  NewTabWebChannel.broadcast("respond", null);
+  yield replyPromise;
+  is(replyCount, 1, "only current channel is listened to for replies");
+
+  for (let tab of tabs) {
+    yield BrowserTestUtils.removeTab(tab);
+  }
+
+  Cu.forceGC();
+  is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+  yield unloadPromise;
+  cleanup();
+});
+
+add_task(function* open_webchannel_reload() {
+  Preferences.set("browser.newtabpage.remote.mode", "test");
+  Preferences.set("browser.newtabpage.remote", true);
+
+  let tabOptions = {
+    gBrowser,
+    url: TEST_URL
+  };
+
+  let messagePromise = new Promise(resolve => {
+    NewTabWebChannel.once("foo", function(name, msg) {
+      is(name, "foo", "Correct message type sent: foo");
+      is(msg.data, "bar", "Correct data sent: bar");
+      resolve(msg.target);
+    });
+  });
+  let unloadPromise = new Promise(resolve => {
+    NewTabWebChannel.once("targetUnload", function() {
+      resolve();
+    });
+  });
+
+  is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+  yield BrowserTestUtils.withNewTab(tabOptions, function*(browser) {
+    let target = yield messagePromise;
+    is(NewTabWebChannel.numBrowsers, 1, "One target expected");
+    is(target.browser, browser, "Same browser");
+
+    browser.contentWindow.location.reload();
+  });
+
+  Cu.forceGC();
+  is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
+  yield unloadPromise;
+  cleanup();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/browser/newtabmessages_prefs.html
@@ -0,0 +1,32 @@
+<html>
+    <head>
+        <meta charset="utf8">
+        <title>Newtab WebChannel test</title>
+    </head>
+    <body>
+        <script>
+            window.addEventListener("WebChannelMessageToContent", function(e) {
+                if (e.detail.message && e.detail.message.type === "RECEIVE_PREFS") {
+                    let reply = new window.CustomEvent("WebChannelMessageToChrome", {
+                        detail: {
+                            id: "newtab",
+                            message: JSON.stringify({type: "responseAck"}),
+                        }
+                    });
+                    window.dispatchEvent(reply);
+                }
+            }, true);
+
+            document.onreadystatechange = function () {
+                let msg = new window.CustomEvent("WebChannelMessageToChrome", {
+                    detail: {
+                        id: "newtab",
+                        message: JSON.stringify({type: "REQUEST_PREFS"}),
+                    }
+                });
+                window.dispatchEvent(msg);
+            };
+
+        </script>
+    </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/browser/newtabwebchannel_basic.html
@@ -0,0 +1,32 @@
+<html>
+    <head>
+        <meta charset="utf8">
+        <title>Newtab WebChannel test</title>
+    </head>
+    <body>
+        <script>
+            document.onreadystatechange = function () {
+                let msg = new window.CustomEvent("WebChannelMessageToChrome", {
+                    detail: {
+                        id: "newtab",
+                        message: JSON.stringify({type: "foo", data: "bar"}),
+                    }
+                });
+                window.dispatchEvent(msg);
+            };
+
+            window.addEventListener("WebChannelMessageToContent", function(e) {
+                if (e.detail.message && e.detail.message.type === "respond") {
+                    let reply = new window.CustomEvent("WebChannelMessageToChrome", {
+                        detail: {
+                            id: "newtab",
+                            message: JSON.stringify({type: "reply", data: "quuz"}),
+                        }
+                    });
+                    window.dispatchEvent(reply);
+                }
+            }, true);
+            
+        </script>
+    </body>
+</html>
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -23,16 +23,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource:///modules/DirectoryLinksProvider.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
                                   "resource://gre/modules/NewTabUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
                                   "resource:///modules/NewTabPrefsProvider.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
+                                  "resource:///modules/NewTabWebChannel.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabMessages",
+                                  "resource:///modules/NewTabMessages.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "UITour",
                                   "resource:///modules/UITour.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentClick",
                                   "resource:///modules/ContentClick.jsm");
@@ -748,16 +754,18 @@ BrowserGlue.prototype = {
     AboutHome.init();
 
     DirectoryLinksProvider.init();
     NewTabUtils.init();
     NewTabUtils.links.addProvider(DirectoryLinksProvider);
     AboutNewTab.init();
 
     NewTabPrefsProvider.prefs.init();
+    NewTabWebChannel.init();
+    NewTabMessages.init();
 
     SessionStore.init();
     BrowserUITelemetry.init();
     ContentSearch.init();
     FormValidationHandler.init();
 
     ContentClick.init();
     RemotePrompt.init();
@@ -1056,16 +1064,19 @@ BrowserGlue.prototype = {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
     SelfSupportBackend.uninit();
 
     WebappManager.uninit();
 
     NewTabPrefsProvider.prefs.uninit();
+    NewTabWebChannel.uninit();
+    NewTabMessages.uninit();
+
     AboutNewTab.uninit();
     webrtcUI.uninit();
     FormValidationHandler.uninit();
     if (AppConstants.NIGHTLY_BUILD) {
       AddonWatcher.uninit();
     }
   },