Backed out 4 changesets (bug 1218996) for talos svg failures CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Wed, 23 Dec 2015 11:09:05 -0800
changeset 277486 fa2cfb656c5904938d868192ead19977468a6d2c
parent 277485 24a27da1a369c93df5b446483c8efdeecab96aaf
child 277487 28e9c9db06c4c7f4f8ec5b99f21a3305979f89ea
push id29823
push userryanvm@gmail.com
push dateSat, 26 Dec 2015 01:16:54 +0000
treeherdermozilla-central@691f2e687e46 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1218996
milestone46.0a1
backs outba750628c4f3b9a42f6b6b1d401317caa8f0b31b
2205cce348240acd1f147038a76cebfe5fdec1ca
9410cbc0545e1ec5b6c35b91433056b21d93097a
7839222071ac6d49f7248212d1f946a5e0b4f68c
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 4 changesets (bug 1218996) for talos svg failures CLOSED TREE Backed out changeset ba750628c4f3 (bug 1218996) Backed out changeset 2205cce34824 (bug 1218996) Backed out changeset 9410cbc0545e (bug 1218996) Backed out changeset 7839222071ac (bug 1218996)
browser/app/profile/firefox.js
browser/base/content/browser.js
browser/base/content/remote-newtab/newTab.css
browser/base/content/remote-newtab/newTab.js
browser/base/content/remote-newtab/newTab.xhtml
browser/base/content/test/general/browser_bug763468_perwindowpb.js
browser/base/content/test/general/browser_bug767836_perwindowpb.js
browser/base/content/test/newtab/browser_newtab_external_resource.js
browser/base/content/utilityOverlay.js
browser/base/jar.mn
browser/components/BrowserComponents.manifest
browser/components/about/AboutRedirector.cpp
browser/components/moz.build
browser/components/newtab/NewTabComponents.manifest
browser/components/newtab/NewTabPrefsProvider.jsm
browser/components/newtab/NewTabURL.jsm
browser/components/newtab/RemoteAboutNewTab.jsm
browser/components/newtab/RemoteNewTabLocation.jsm
browser/components/newtab/RemoteNewTabUtils.jsm
browser/components/newtab/aboutNewTabService.js
browser/components/newtab/moz.build
browser/components/newtab/nsIAboutNewTabService.idl
browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js
browser/components/newtab/tests/browser/dummy_page.html
browser/components/newtab/tests/xpcshell/test_AboutNewTabService.js
browser/components/newtab/tests/xpcshell/test_NewTabURL.js
browser/components/newtab/tests/xpcshell/test_RemoteNewTabLocation.js
browser/components/newtab/tests/xpcshell/test_RemoteNewTabUtils.js
browser/components/newtab/tests/xpcshell/xpcshell.ini
browser/components/nsBrowserGlue.js
browser/components/nsIAboutNewTabService.idl
browser/installer/package-manifest.in
browser/modules/DirectoryLinksProvider.jsm
mobile/android/b2gdroid/installer/package-manifest.in
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1343,18 +1343,20 @@ pref("browser.newtabpage.rows", 3);
 pref("browser.newtabpage.columns", 5);
 
 // directory tiles download URL
 pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/v3/links/fetch/%LOCALE%/%CHANNEL%");
 
 // endpoint to send newtab click and view pings
 pref("browser.newtabpage.directory.ping", "https://tiles.services.mozilla.com/v3/links/");
 
-// activates the remote-hosted newtab page
+#ifndef RELEASE_BUILD
+// if true, it activates the remote-hosted newtab page
 pref("browser.newtabpage.remote", false);
+#endif
 
 // Enable the DOM fullscreen API.
 pref("full-screen-api.enabled", true);
 
 // Startup Crash Tracking
 // number of startup crashes that can occur before starting into safe mode automatically
 // (this pref has no effect if more than 6 hours have passed since the last crash)
 pref("toolkit.startup.max_resumed_crashes", 3);
@@ -1631,9 +1633,9 @@ pref("dom.push.enabled", true);
 // If you change this, ENSURE IT IS THE SAME SIZE SET
 // by about:newtab. These values are in CSS pixels.
 pref("toolkit.pageThumbs.minWidth", 280);
 pref("toolkit.pageThumbs.minHeight", 190);
 
 #ifdef NIGHTLY_BUILD
 // Enable speech synthesis, only Nightly for now
 pref("media.webspeech.synth.enabled", true);
-#endif
\ No newline at end of file
+#endif
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6,17 +6,16 @@
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 var Cc = Components.classes;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/NotificationDB.jsm");
 Cu.import("resource:///modules/RecentWindow.jsm");
 
-
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
                                   "resource://gre/modules/Deprecated.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
                                   "resource:///modules/BrowserUITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                   "resource:///modules/E10SUtils.jsm");
@@ -51,19 +50,16 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "@mozilla.org/network/dns-service;1",
                                    "nsIDNSService");
 XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils",
                                    "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Pocket",
                                   "resource:///modules/Pocket.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "gAboutNewTabService",
-                                   "@mozilla.org/browser/aboutnewtab-service;1",
-                                   "nsIAboutNewTabService");
 
 // Can't use XPCOMUtils for these because the scripts try to define the variables
 // on window, and so the defineProperty inside defineLazyGetter fails.
 Object.defineProperty(window, "pktApi", {
   get: function() {
     // Avoid this getter running again:
     delete window.pktApi;
     Services.scriptloader.loadSubScript("chrome://browser/content/pocket/pktApi.js", window);
@@ -2360,21 +2356,18 @@ function URLBarSetURI(aURI) {
   if (value == null) {
     let uri = aURI || gBrowser.currentURI;
     // Strip off "wyciwyg://" and passwords for the location bar
     try {
       uri = Services.uriFixup.createExposableURI(uri);
     } catch (e) {}
 
     // Replace initial page URIs with an empty string
-    // 1. only if there's no opener (bug 370555).
-    // 2. if remote newtab is enabled and it's the default remote newtab page
-    let defaultRemoteURL = gAboutNewTabService.remoteEnabled &&
-                           uri.spec === gAboutNewTabService.newTabURL;
-    if (gInitialPages.includes(uri.spec) || defaultRemoteURL)
+    // only if there's no opener (bug 370555).
+    if (gInitialPages.indexOf(uri.spec) != -1)
       value = gBrowser.selectedBrowser.hasContentOpener ? uri.spec : "";
     else
       value = losslessDecodeURI(uri);
 
     valid = !isBlankPageURL(uri.spec);
   }
 
   gURLBar.value = value;
@@ -3561,19 +3554,18 @@ const BrowserSearch = {
       }
       return;
     }
 
     let openSearchPageIfFieldIsNotActive = function(aSearchBar) {
       if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) {
         let url = gBrowser.currentURI.spec.toLowerCase();
         let mm = gBrowser.selectedBrowser.messageManager;
-        let newTabRemoted = Services.prefs.getBoolPref("browser.newtabpage.remote");
-        let localNewTabEnabled = url === "about:newtab" && !newTabRemoted && NewTabUtils.allPages.enabled;
-        if (url === "about:home" || localNewTabEnabled) {
+        if (url === "about:home" ||
+            (url === "about:newtab" && NewTabUtils.allPages.enabled)) {
           ContentSearch.focusInput(mm);
         } else {
           openUILinkIn("about:home", "current");
         }
       }
     };
 
     let searchBar = this.searchBar;
new file mode 100644
--- /dev/null
+++ b/browser/base/content/remote-newtab/newTab.css
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html {
+  width: 100%;
+  height: 100%;
+}
+
+body {
+  width: 100%;
+  height: 100%;
+  padding: 0;
+  margin: 0;
+  position: relative;
+}
+
+#remotedoc {
+  width: 100%;
+  height: 100%;
+  border: none;
+  position: absolute;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/remote-newtab/newTab.js
@@ -0,0 +1,126 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/*globals XPCOMUtils, Components, sendAsyncMessage, addMessageListener, removeMessageListener,
+          Services, PrivateBrowsingUtils*/
+"use strict";
+
+const {utils: Cu, interfaces: Ci} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+
+(function() {
+  let remoteNewTabLocation;
+  let remoteIFrame;
+
+  /**
+   * Attempts to handle commands sent from the remote IFrame within this content frame.
+   * Expected commands below, with data types explained.
+   *
+   * @returns {Boolean} whether or not the command was handled
+   * @param {String} command
+   *        The command passed from the remote IFrame
+   * @param {Object} data
+   *        Parameters to the command
+   */
+  function handleCommand(command, data) {
+    let commandHandled = true;
+    switch (command) {
+    case "NewTab:UpdateTelemetryProbe":
+      /**
+       * Update a given Telemetry histogram
+       *
+       * @param {String} data.probe
+       *        Probe name to update
+       * @param {Number} data.value
+       *        Value to update histogram by
+       */
+      Services.telemetry.getHistogramById(data.probe).add(data.value);
+      break;
+    case "NewTab:Register":
+      registerEvent(data.type);
+      break;
+    case "NewTab:GetInitialState":
+      getInitialState();
+      break;
+    default:
+      commandHandled = false;
+    }
+    return commandHandled;
+  }
+
+  function initRemotePage(initData) {
+    // Messages that the iframe sends the browser will be passed onto
+    // the privileged parent process
+    remoteNewTabLocation = initData;
+    remoteIFrame = document.querySelector("#remotedoc");
+
+    let loadHandler = () => {
+      if (remoteIFrame.src !== remoteNewTabLocation.href) {
+        return;
+      }
+
+      remoteIFrame.removeEventListener("load", loadHandler);
+
+      remoteIFrame.contentDocument.addEventListener("NewTabCommand", (e) => {
+        // If the commands are not handled within this content frame, the command will be
+        // passed on to main process, in RemoteAboutNewTab.jsm
+        let handled = handleCommand(e.detail.command, e.detail.data);
+        if (!handled) {
+          sendAsyncMessage(e.detail.command, e.detail.data);
+        }
+      });
+      registerEvent("NewTab:Observe");
+      let ev = new CustomEvent("NewTabCommandReady");
+      remoteIFrame.contentDocument.dispatchEvent(ev);
+    };
+
+    remoteIFrame.src = remoteNewTabLocation.href;
+    remoteIFrame.addEventListener("load", loadHandler);
+  }
+
+  /**
+   * Allows the content IFrame to register a listener to an event sent by
+   * the privileged parent process, in RemoteAboutNewTab.jsm
+   *
+   * @param {String} eventName
+   *        Event name to listen to
+   */
+  function registerEvent(eventName) {
+    addMessageListener(eventName, (message) => {
+      remoteIFrame.contentWindow.postMessage(message, remoteNewTabLocation.origin);
+    });
+  }
+
+  /**
+   * Sends the initial data payload to a content IFrame so it can bootstrap
+   */
+  function getInitialState() {
+    let prefs = Services.prefs;
+    let isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
+    let state = {
+      enabled: prefs.getBoolPref("browser.newtabpage.enabled"),
+      enhanced: prefs.getBoolPref("browser.newtabpage.enhanced"),
+      rows: prefs.getIntPref("browser.newtabpage.rows"),
+      columns: prefs.getIntPref("browser.newtabpage.columns"),
+      introShown: prefs.getBoolPref("browser.newtabpage.introShown"),
+      windowID: window.QueryInterface(Ci.nsIInterfaceRequestor)
+        .getInterface(Ci.nsIDOMWindowUtils).outerWindowID,
+      privateBrowsingMode: isPrivate
+    };
+    remoteIFrame.contentWindow.postMessage({
+      name: "NewTab:State",
+      data: state
+    }, remoteNewTabLocation.origin);
+  }
+
+  addMessageListener("NewTabFrame:Init", function loadHandler(message) {
+    // Everything is loaded. Initialize the New Tab Page.
+    removeMessageListener("NewTabFrame:Init", loadHandler);
+    initRemotePage(message.data);
+  });
+  sendAsyncMessage("NewTabFrame:GetInit");
+}());
new file mode 100644
--- /dev/null
+++ b/browser/base/content/remote-newtab/newTab.xhtml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html [
+  <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd">
+  %newTabDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>&newtab.pageTitle;</title>
+  <link rel="stylesheet" href="chrome://browser/content/remote-newtab/newTab.css"/>
+</head>
+<body>
+  <iframe id="remotedoc"/>
+  <script type="text/javascript;version=1.8"
+          src="chrome://browser/content/remote-newtab/newTab.js">
+  </script>
+</body>
+</html>
+
--- a/browser/base/content/test/general/browser_bug763468_perwindowpb.js
+++ b/browser/base/content/test/general/browser_bug763468_perwindowpb.js
@@ -1,57 +1,53 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-"use strict";
-
-/* globals
-  waitForExplicitFinish, whenNewWindowLoaded, whenNewTabLoaded,
-  executeSoon, registerCleanupFunction, finish, is
-*/
-/* exported test */
 
 // This test makes sure that opening a new tab in private browsing mode opens about:privatebrowsing
 function test() {
   // initialization
   waitForExplicitFinish();
+  let aboutNewTabService = Components.classes["@mozilla.org/browser/aboutnewtab-service;1"]
+                                     .getService(Components.interfaces.nsIAboutNewTabService);
 
   let windowsToClose = [];
+  let newTab;
   let newTabURL;
   let mode;
 
   function doTest(aIsPrivateMode, aWindow, aCallback) {
-    whenNewTabLoaded(aWindow, function() {
+    whenNewTabLoaded(aWindow, function () {
       if (aIsPrivateMode) {
         mode = "per window private browsing";
         newTabURL = "about:privatebrowsing";
       } else {
         mode = "normal";
-        newTabURL = "about:newtab";
+        newTabURL = aboutNewTabService.newTabURL;
       }
 
       is(aWindow.gBrowser.currentURI.spec, newTabURL,
         "URL of NewTab should be " + newTabURL + " in " + mode +  " mode");
 
       aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab);
-      aCallback();
+      aCallback()
     });
-  }
+  };
 
   function testOnWindow(aOptions, aCallback) {
     whenNewWindowLoaded(aOptions, function(aWin) {
       windowsToClose.push(aWin);
       // execute should only be called when need, like when you are opening
       // web pages on the test. If calling executeSoon() is not necesary, then
       // call whenNewWindowLoaded() instead of testOnWindow() on your test.
       executeSoon(() => aCallback(aWin));
     });
-  }
+  };
 
-  // this function is called after calling finish() on the test.
+   // this function is called after calling finish() on the test.
   registerCleanupFunction(function() {
     windowsToClose.forEach(function(aWin) {
       aWin.close();
     });
   });
 
   // test first when not on private mode
   testOnWindow({}, function(aWin) {
--- a/browser/base/content/test/general/browser_bug767836_perwindowpb.js
+++ b/browser/base/content/test/general/browser_bug767836_perwindowpb.js
@@ -1,57 +1,51 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
-"use strict";
-/* globals waitForExplicitFinish, executeSoon, finish, whenNewWindowLoaded, ok */
-/* globals is */
-/* exported test */
 
 function test() {
   //initialization
   waitForExplicitFinish();
-
   let aboutNewTabService = Components.classes["@mozilla.org/browser/aboutnewtab-service;1"]
                                      .getService(Components.interfaces.nsIAboutNewTabService);
   let newTabURL;
   let testURL = "http://example.com/";
-  let defaultURL = aboutNewTabService.newTabURL;
   let mode;
 
   function doTest(aIsPrivateMode, aWindow, aCallback) {
-    openNewTab(aWindow, function() {
+    openNewTab(aWindow, function () {
       if (aIsPrivateMode) {
         mode = "per window private browsing";
         newTabURL = "about:privatebrowsing";
       } else {
         mode = "normal";
-        newTabURL = "about:newtab";
+        newTabURL = aboutNewTabService.newTabURL;
       }
 
       // Check the new tab opened while in normal/private mode
       is(aWindow.gBrowser.selectedBrowser.currentURI.spec, newTabURL,
         "URL of NewTab should be " + newTabURL + " in " + mode +  " mode");
       // Set the custom newtab url
       aboutNewTabService.newTabURL = testURL;
       is(aboutNewTabService.newTabURL, testURL, "Custom newtab url is set");
 
       // Open a newtab after setting the custom newtab url
-      openNewTab(aWindow, function() {
+      openNewTab(aWindow, function () {
         is(aWindow.gBrowser.selectedBrowser.currentURI.spec, testURL,
            "URL of NewTab should be the custom url");
 
         // Clear the custom url.
         aboutNewTabService.resetNewTabURL();
-        is(aboutNewTabService.newTabURL, defaultURL, "No custom newtab url is set");
+        is(aboutNewTabService.newTabURL, "about:newtab", "No custom newtab url is set");
 
         aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab);
         aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab);
         aWindow.close();
-        aCallback();
+        aCallback()
       });
     });
   }
 
   function testOnWindow(aIsPrivate, aCallback) {
     whenNewWindowLoaded({private: aIsPrivate}, function(win) {
       executeSoon(() => aCallback(win));
     });
@@ -73,17 +67,17 @@ function test() {
   });
 }
 
 function openNewTab(aWindow, aCallback) {
   // Open a new tab
   aWindow.BrowserOpenTab();
 
   let browser = aWindow.gBrowser.selectedBrowser;
-  if (browser.contentDocument.readyState === "complete") {
+  if (browser.contentDocument.readyState == "complete") {
     executeSoon(aCallback);
     return;
   }
 
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     executeSoon(aCallback);
   }, true);
--- a/browser/base/content/test/newtab/browser_newtab_external_resource.js
+++ b/browser/base/content/test/newtab/browser_newtab_external_resource.js
@@ -4,29 +4,23 @@
  *
  * We perform two tests:
  * (1) We load a new tab (about:newtab) using the default url and make sure that URL
  *     of the doucment matches about:newtab and the principal is the systemPrincipal.
  * (2) We load a new tab (about:newtab) and make sure that document.location as well
  *     as the nodePrincipal match the URL in the URL bar.
  */
 
-/* globals Cc, Ci, ok, is, content, TestRunner, addNewTabPageTab, gWindow, Services, info */
-/* exported runTests */
-
-"use strict";
+const ABOUT_NEWTAB_URI = "about:newtab";
+const PREF_URI = "http://example.com/browser/browser/base/content/test/newtab/external_newtab.html";
 
 var browser = null;
 var aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"]
                            .getService(Ci.nsIAboutNewTabService);
 
-const ABOUT_NEWTAB_URI = "about:newtab";
-const PREF_URI = "http://example.com/browser/browser/base/content/test/newtab/external_newtab.html";
-const DEFAULT_URI = aboutNewTabService.newTabURL;
-
 function testPref() {
   // set the pref for about:newtab to point to an exteranl resource
   aboutNewTabService.newTabURL = PREF_URI;
   ok(aboutNewTabService.overridden,
      "sanity check: default URL for about:newtab should be overriden");
   is(aboutNewTabService.newTabURL, PREF_URI,
      "sanity check: default URL for about:newtab should return the new URL");
 
@@ -35,17 +29,17 @@ function testPref() {
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     is(content.document.location, PREF_URI, "document.location should match the external resource");
     is(content.document.documentURI, PREF_URI, "document.documentURI should match the external resource");
     is(content.document.nodePrincipal.URI.spec, PREF_URI, "nodePrincipal should match the external resource");
 
     // reset to about:newtab and perform sanity check
     aboutNewTabService.resetNewTabURL();
-    is(aboutNewTabService.newTabURL, DEFAULT_URI,
+    is(aboutNewTabService.newTabURL, ABOUT_NEWTAB_URI,
        "sanity check: resetting the URL to about:newtab should return about:newtab");
 
     // remove the tab and move on
     gBrowser.removeCurrentTab();
     TestRunner.next();
   }, true);
 }
 
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -15,17 +15,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "nsIAboutNewTabService");
 
 this.__defineGetter__("BROWSER_NEW_TAB_URL", () => {
   if (PrivateBrowsingUtils.isWindowPrivate(window) &&
       !PrivateBrowsingUtils.permanentPrivateBrowsing &&
       !aboutNewTabService.overridden) {
     return "about:privatebrowsing";
   }
-  return "about:newtab";
+  return aboutNewTabService.newTabURL;
 });
 
 var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
 
 var gBidiUI = false;
 
 /**
  * Determines whether the given url is considered a special URL for new tabs.
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -133,16 +133,19 @@ browser.jar:
         content/browser/defaultthemes/devedition.icon.png     (content/defaultthemes/devedition.icon.png)
         content/browser/gcli_sec_bad.svg              (content/gcli_sec_bad.svg)
         content/browser/gcli_sec_good.svg             (content/gcli_sec_good.svg)
         content/browser/gcli_sec_moderate.svg         (content/gcli_sec_moderate.svg)
         content/browser/newtab/newTab.xhtml           (content/newtab/newTab.xhtml)
 *       content/browser/newtab/newTab.js              (content/newtab/newTab.js)
         content/browser/newtab/newTab.css             (content/newtab/newTab.css)
         content/browser/newtab/newTab.inadjacent.json         (content/newtab/newTab.inadjacent.json)
+        content/browser/remote-newtab/newTab.xhtml    (content/remote-newtab/newTab.xhtml)
+        content/browser/remote-newtab/newTab.js       (content/remote-newtab/newTab.js)
+        content/browser/remote-newtab/newTab.css      (content/remote-newtab/newTab.css)
 *       content/browser/pageinfo/pageInfo.xul         (content/pageinfo/pageInfo.xul)
         content/browser/pageinfo/pageInfo.js          (content/pageinfo/pageInfo.js)
         content/browser/pageinfo/pageInfo.css         (content/pageinfo/pageInfo.css)
         content/browser/pageinfo/pageInfo.xml         (content/pageinfo/pageInfo.xml)
         content/browser/pageinfo/feeds.js             (content/pageinfo/feeds.js)
         content/browser/pageinfo/feeds.xml            (content/pageinfo/feeds.xml)
         content/browser/pageinfo/permissions.js       (content/pageinfo/permissions.js)
         content/browser/pageinfo/security.js          (content/pageinfo/security.js)
--- a/browser/components/BrowserComponents.manifest
+++ b/browser/components/BrowserComponents.manifest
@@ -41,8 +41,10 @@ category command-line-validator b-browse
 
 component {eab9012e-5f74-4cbc-b2b5-a590235513cc} nsBrowserGlue.js
 contract @mozilla.org/browser/browserglue;1 {eab9012e-5f74-4cbc-b2b5-a590235513cc}
 category app-startup nsBrowserGlue service,@mozilla.org/browser/browserglue;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} application={d1bfe7d9-c01e-4237-998b-7b5f960a4314}
 component {d8903bf6-68d5-4e97-bcd1-e4d3012f721a} nsBrowserGlue.js
 #ifndef MOZ_MULET
 contract @mozilla.org/content-permission/prompt;1 {d8903bf6-68d5-4e97-bcd1-e4d3012f721a}
 #endif
+component {97eea4bb-db50-4ae0-9147-1e5ed55b4ed5} nsBrowserGlue.js
+contract @mozilla.org/browser/aboutnewtab-service;1 {97eea4bb-db50-4ae0-9147-1e5ed55b4ed5}
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -81,19 +81,23 @@ static RedirEntry kRedirMap[] = {
     nsIAboutModule::ALLOW_SCRIPT },
   { "sync-tabs", "chrome://browser/content/sync/aboutSyncTabs.xul",
     nsIAboutModule::ALLOW_SCRIPT },
   { "home", "chrome://browser/content/abouthome/aboutHome.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::ENABLE_INDEXED_DB },
-  // the newtab's actual URL will be determined when the channel is created
-  { "newtab", "about:blank",
+  { "newtab", "chrome://browser/content/newtab/newTab.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
+#ifndef RELEASE_BUILD
+  { "remote-newtab", "chrome://browser/content/remote-newtab/newTab.xhtml",
+    nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
+    nsIAboutModule::ALLOW_SCRIPT },
+#endif
   { "preferences", "chrome://browser/content/preferences/in-content/preferences.xul",
     nsIAboutModule::ALLOW_SCRIPT },
   { "downloads", "chrome://browser/content/downloads/contentAreaDownloadsView.xul",
     nsIAboutModule::ALLOW_SCRIPT },
 #ifdef MOZ_SERVICES_HEALTHREPORT
   { "healthreport", "chrome://browser/content/abouthealthreport/abouthealth.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
 #endif
@@ -168,22 +172,28 @@ AboutRedirector::NewChannel(nsIURI* aURI
   nsresult rv;
   nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   for (int i = 0; i < kRedirTotal; i++) {
     if (!strcmp(path.get(), kRedirMap[i].id)) {
       nsAutoCString url;
 
+      // check if about:newtab got overridden
       if (path.EqualsLiteral("newtab")) {
-        // let the aboutNewTabService decide where to redirect
         nsCOMPtr<nsIAboutNewTabService> aboutNewTabService =
           do_GetService("@mozilla.org/browser/aboutnewtab-service;1", &rv);
-        rv = aboutNewTabService->GetNewTabURL(url);
+        NS_ENSURE_SUCCESS(rv, rv);
+        bool overridden = false;
+        rv = aboutNewTabService->GetOverridden(&overridden);
         NS_ENSURE_SUCCESS(rv, rv);
+        if (overridden) {
+          rv = aboutNewTabService->GetNewTabURL(url);
+          NS_ENSURE_SUCCESS(rv, rv);
+        }
       }
       // fall back to the specified url in the map
       if (url.IsEmpty()) {
         url.AssignASCII(kRedirMap[i].url);
       }
 
       nsCOMPtr<nsIChannel> tempChannel;
       nsCOMPtr<nsIURI> tempURI;
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -24,16 +24,17 @@ DIRS += [
     'selfsupport',
     'uitour',
     'translation',
 ]
 
 DIRS += ['build']
 
 XPIDL_SOURCES += [
+    'nsIAboutNewTabService.idl',
     'nsIBrowserGlue.idl',
     'nsIBrowserHandler.idl',
 ]
 
 XPIDL_MODULE = 'browsercompsbase'
 
 EXTRA_PP_COMPONENTS += [
     'BrowserComponents.manifest',
deleted file mode 100644
--- a/browser/components/newtab/NewTabComponents.manifest
+++ /dev/null
@@ -1,2 +0,0 @@
-component {cef25b06-0ef6-4c50-a243-e69f943ef23d} aboutNewTabService.js
-contract @mozilla.org/browser/aboutnewtab-service;1 {cef25b06-0ef6-4c50-a243-e69f943ef23d}
--- a/browser/components/newtab/NewTabPrefsProvider.jsm
+++ b/browser/components/newtab/NewTabPrefsProvider.jsm
@@ -12,17 +12,16 @@ Cu.import("resource://gre/modules/XPCOMU
 
 XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
   const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
   return EventEmitter;
 });
 
 // Supported prefs and data type
 const gPrefsMap = new Map([
-  ["browser.newtabpage.remote", "bool"],
   ["browser.newtabpage.enabled", "bool"],
   ["browser.newtabpage.enhanced", "bool"],
   ["browser.newtabpage.pinned", "str"],
   ["intl.locale.matchOS", "bool"],
   ["general.useragent.locale", "localized"],
 ]);
 
 let PrefsProvider = function PrefsProvider() {
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/NewTabURL.jsm
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "NewTabURL" ];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+                                   "@mozilla.org/browser/aboutnewtab-service;1",
+                                   "nsIAboutNewTabService");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+                                  "resource://gre/modules/Deprecated.jsm");
+
+const DepecationURL = "https://bugzilla.mozilla.org/show_bug.cgi?id=1204983#c89";
+
+this.NewTabURL = {
+
+  get: function() {
+    Deprecated.warning("NewTabURL.get is deprecated, please query aboutNewTabService.newTabURL", DepecationURL);
+    return aboutNewTabService.newTabURL;
+  },
+
+  get overridden() {
+    Deprecated.warning("NewTabURL.overridden is deprecated, please query aboutNewTabService.overridden", DepecationURL);
+    return aboutNewTabService.overridden;
+  },
+
+  override: function(newURL) {
+    Deprecated.warning("NewTabURL.override is deprecated, please set aboutNewTabService.newTabURL", DepecationURL);
+    aboutNewTabService.newTabURL = newURL;
+  },
+
+  reset: function() {
+    Deprecated.warning("NewTabURL.reset is deprecated, please use aboutNewTabService.resetNewTabURL()", DepecationURL);
+    aboutNewTabService.resetNewTabURL();
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/RemoteAboutNewTab.jsm
@@ -0,0 +1,301 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* globals Services, XPCOMUtils, RemotePages, RemoteNewTabLocation, RemoteNewTabUtils, Task  */
+/* globals BackgroundPageThumbs, PageThumbs, DirectoryLinksProvider, PlacesProvider, NewTabPrefsProvider */
+/* exported RemoteAboutNewTab */
+
+"use strict";
+
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
+
+this.EXPORTED_SYMBOLS = ["RemoteAboutNewTab"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.importGlobalProperties(["URL"]);
+
+XPCOMUtils.defineLazyModuleGetter(this, "RemotePages",
+  "resource://gre/modules/RemotePageManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils",
+  "resource:///modules/RemoteNewTabUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BackgroundPageThumbs",
+  "resource://gre/modules/BackgroundPageThumbs.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
+  "resource://gre/modules/PageThumbs.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DirectoryLinksProvider",
+  "resource:///modules/DirectoryLinksProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabLocation",
+  "resource:///modules/RemoteNewTabLocation.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesProvider",
+  "resource:///modules/PlacesProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+  "resource:///modules/NewTabPrefsProvider.jsm");
+
+let RemoteAboutNewTab = {
+
+  pageListener: null,
+
+  /**
+   * Initialize the RemotePageManager and add all message listeners for this page
+   */
+  init: function() {
+    RemoteNewTabLocation.init();
+    this.pageListener = new RemotePages("about:remote-newtab");
+    this.pageListener.addMessageListener("NewTab:InitializeGrid", this.initializeGrid.bind(this));
+    this.pageListener.addMessageListener("NewTab:UpdateGrid", this.updateGrid.bind(this));
+    this.pageListener.addMessageListener("NewTab:Customize", this.customize.bind(this));
+    this.pageListener.addMessageListener("NewTab:CaptureBackgroundPageThumbs",
+        this.captureBackgroundPageThumb.bind(this));
+    this.pageListener.addMessageListener("NewTab:PageThumbs", this.createPageThumb.bind(this));
+    this.pageListener.addMessageListener("NewTabFrame:GetInit", this.initContentFrame.bind(this));
+
+    this._addObservers();
+  },
+
+  customize: function(message) {
+    if (message.data.enabled !== undefined) {
+      Services.prefs.setBoolPref("browser.newtabpage.enabled", message.data.enabled);
+    }
+    if (message.data.enhanced !== undefined) {
+      Services.prefs.setBoolPref("browser.newtabpage.enhanced", message.data.enhanced);
+    }
+  },
+
+  /**
+   * Notifies when history is cleared
+   */
+  placesClearHistory: function() {
+    this.pageListener.sendAsyncMessage("NewTab:PlacesClearHistory");
+  },
+
+  /**
+   * Notifies when a link has changed
+   */
+  placesLinkChanged: function(name, data) { // jshint ignore:line
+    this.pageListener.sendAsyncMessage("NewTab:PlacesLinkChanged", data);
+  },
+
+  /**
+   * Notifies when many links have changed
+   */
+  placesManyLinksChanged: function() {
+    this.pageListener.sendAsyncMessage("NewTab:PlacesManyLinksChanged");
+  },
+
+  /**
+   * Notifies when one URL has been deleted
+   */
+  placesDeleteURI: function(name, data) { // jshint ignore:line
+    this.pageListener.sendAsyncMessage("NewTab:PlacesDeleteURI", data.url);
+  },
+
+  /**
+   * Initializes the grid for the first time when the page loads.
+   * Fetch all the links and send them down to the child to populate
+   * the grid with.
+   *
+   * @param {Object} message
+   *        A RemotePageManager message.
+   */
+  initializeGrid: function(message) {
+    RemoteNewTabUtils.links.populateCache(() => {
+      message.target.sendAsyncMessage("NewTab:InitializeLinks", {
+        links: RemoteNewTabUtils.links.getLinks(),
+        enhancedLinks: this.getEnhancedLinks(),
+      });
+    });
+  },
+
+  /**
+   * Inits the content iframe with the newtab location
+   */
+  initContentFrame: function(message) {
+    message.target.sendAsyncMessage("NewTabFrame:Init", {
+      href: RemoteNewTabLocation.href,
+      origin: RemoteNewTabLocation.origin
+    });
+  },
+
+  /**
+   * Updates the grid by getting a new set of links.
+   *
+   * @param {Object} message
+   *        A RemotePageManager message.
+   */
+  updateGrid: function(message) {
+    message.target.sendAsyncMessage("NewTab:UpdateLinks", {
+      links: RemoteNewTabUtils.links.getLinks(),
+      enhancedLinks: this.getEnhancedLinks(),
+    });
+  },
+
+  /**
+   * Captures the site's thumbnail in the background, then attemps to show the thumbnail.
+   *
+   * @param {Object} message
+   *        A RemotePageManager message with the following data:
+   *
+   *        link (Object):
+   *          A link object that contains:
+   *
+   *          baseDomain (String)
+   *          blockState (Boolean)
+   *          frecency (Integer)
+   *          lastVisiteDate (Integer)
+   *          pinState (Boolean)
+   *          title (String)
+   *          type (String)
+   *          url (String)
+   */
+  captureBackgroundPageThumb: Task.async(function* (message) {
+    try {
+      yield BackgroundPageThumbs.captureIfMissing(message.data.link.url);
+      this.createPageThumb(message);
+    } catch (err) {
+      Cu.reportError("error: " + err);
+    }
+  }),
+
+  /**
+   * Creates the thumbnail to display for each site based on the unique URL
+   * of the site and it's type (regular or enhanced). If the thumbnail is of
+   * type "regular", we create a blob and send that down to the child. If the
+   * thumbnail is of type "enhanced", get the file path for the URL and create
+   * and enhanced URI that will be sent down to the child.
+   *
+   * @param {Object} message
+   *        A RemotePageManager message with the following data:
+   *
+   *        link (Object):
+   *          A link object that contains:
+   *
+   *          baseDomain (String)
+   *          blockState (Boolean)
+   *          frecency (Integer)
+   *          lastVisiteDate (Integer)
+   *          pinState (Boolean)
+   *          title (String)
+   *          type (String)
+   *          url (String)
+   */
+  createPageThumb: function(message) {
+    let imgSrc = PageThumbs.getThumbnailURL(message.data.link.url);
+    let doc = Services.appShell.hiddenDOMWindow.document;
+    let img = doc.createElementNS(XHTML_NAMESPACE, "img");
+    let canvas = doc.createElementNS(XHTML_NAMESPACE, "canvas");
+    let enhanced = Services.prefs.getBoolPref("browser.newtabpage.enhanced");
+
+    img.onload = function(e) { // jshint ignore:line
+      canvas.width = img.naturalWidth;
+      canvas.height = img.naturalHeight;
+      var ctx = canvas.getContext("2d");
+      ctx.drawImage(this, 0, 0, this.naturalWidth, this.naturalHeight);
+      canvas.toBlob(function(blob) {
+        let host = new URL(message.data.link.url).host;
+        RemoteAboutNewTab.pageListener.sendAsyncMessage("NewTab:RegularThumbnailURI", {
+          thumbPath: "/pagethumbs/" + host,
+          enhanced,
+          url: message.data.link.url,
+          blob,
+        });
+      });
+    };
+    img.src = imgSrc;
+  },
+
+  /**
+   * Get the set of enhanced links (if any) from the Directory Links Provider.
+   */
+  getEnhancedLinks: function() {
+    let enhancedLinks = [];
+    for (let link of RemoteNewTabUtils.links.getLinks()) {
+      if (link) {
+        enhancedLinks.push(DirectoryLinksProvider.getEnhancedLink(link));
+      }
+    }
+    return enhancedLinks;
+  },
+
+  /**
+   * Listens for a preference change or session purge for all pages and sends
+   * a message to update the pages that are open. If a session purge occured,
+   * also clear the links cache and update the set of links to display, as they
+   * may have changed, then proceed with the page update.
+   */
+  observe: function(aSubject, aTopic, aData) { // jshint ignore:line
+    let extraData;
+    if (aTopic === "browser:purge-session-history") {
+      RemoteNewTabUtils.links.resetCache();
+      RemoteNewTabUtils.links.populateCache(() => {
+        this.pageListener.sendAsyncMessage("NewTab:UpdateLinks", {
+          links: RemoteNewTabUtils.links.getLinks(),
+          enhancedLinks: this.getEnhancedLinks(),
+        });
+      });
+    }
+
+    if (extraData !== undefined || aTopic === "page-thumbnail:create") {
+      if (aTopic !== "page-thumbnail:create") {
+        // Change the topic for enhanced and enabled observers.
+        aTopic = aData;
+      }
+      this.pageListener.sendAsyncMessage("NewTab:Observe", {topic: aTopic, data: extraData});
+    }
+  },
+
+  setEnabled: function(name, data) { // jshint ignore:line
+    this.pageListener.sendAsyncMessage("NewTab:setEnabled", data);
+  },
+
+  setEnhanced: function(name, data) { // jshint ignore:line
+    this.pageListener.sendAsyncMessage("NewTab:setEnhanced", data);
+  },
+
+  setPinned: function(name, data) { // jshint ignore:line
+    this.pageListener.sendAsyncMessage("NewTab:setPinnedLinks", data);
+  },
+
+  /**
+   * Add all observers that about:newtab page must listen for.
+   */
+  _addObservers: function() {
+    Services.obs.addObserver(this, "page-thumbnail:create", true);
+    Services.obs.addObserver(this, "browser:purge-session-history", true);
+    PlacesProvider.links.on("deleteURI", this.placesDeleteURI.bind(this));
+    PlacesProvider.links.on("clearHistory", this.placesClearHistory.bind(this));
+    PlacesProvider.links.on("linkChanged", this.placesLinkChanged.bind(this));
+    PlacesProvider.links.on("manyLinksChanged", this.placesManyLinksChanged.bind(this));
+    NewTabPrefsProvider.prefs.on("browser.newtabpage.enabled", this.setEnabled.bind(this));
+    NewTabPrefsProvider.prefs.on("browser.newtabpage.enhanced", this.setEnhanced.bind(this));
+    NewTabPrefsProvider.prefs.on("browser.newtabpage.pinned", this.setPinned.bind(this));
+  },
+
+  /**
+   * Remove all observers on the page.
+   */
+  _removeObservers: function() {
+    Services.obs.removeObserver(this, "page-thumbnail:create");
+    Services.obs.removeObserver(this, "browser:purge-session-history");
+    PlacesProvider.links.off("deleteURI", this.placesDeleteURI);
+    PlacesProvider.links.off("clearHistory", this.placesClearHistory);
+    PlacesProvider.links.off("linkChanged", this.placesLinkChanged);
+    PlacesProvider.links.off("manyLinksChanged", this.placesManyLinksChanged);
+    NewTabPrefsProvider.prefs.off("browser.newtabpage.enabled", this.setEnabled.bind(this));
+    NewTabPrefsProvider.prefs.off("browser.newtabpage.enhanced", this.setEnhanced.bind(this));
+    NewTabPrefsProvider.prefs.off("browser.newtabpage.pinned", this.setPinned.bind(this));
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+
+  uninit: function() {
+    RemoteNewTabLocation.uninit();
+    this._removeObservers();
+    this.pageListener.destroy();
+    this.pageListener = null;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/RemoteNewTabLocation.jsm
@@ -0,0 +1,141 @@
+/* globals Services, UpdateUtils, XPCOMUtils, URL, NewTabPrefsProvider, Locale */
+/* exported RemoteNewTabLocation */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["RemoteNewTabLocation"];
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.importGlobalProperties(["URL"]);
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+  "resource://gre/modules/UpdateUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+  "resource:///modules/NewTabPrefsProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Locale",
+  "resource://gre/modules/Locale.jsm");
+
+// The preference that tells whether to match the OS locale
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+
+// The preference that tells what locale the user selected
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+
+const DEFAULT_PAGE_LOCATION = "https://newtab.cdn.mozilla.net/" +
+                              "v%VERSION%/%CHANNEL%/%LOCALE%/index.html";
+
+const VALID_CHANNELS = new Set(["esr", "release", "beta", "aurora", "nightly"]);
+
+const NEWTAB_VERSION = "0";
+
+let RemoteNewTabLocation = {
+  /*
+   * Generate a default url based on locale and update channel
+   */
+  _generateDefaultURL() {
+    let releaseName = this._releaseFromUpdateChannel(UpdateUtils.UpdateChannel);
+    let uri = DEFAULT_PAGE_LOCATION
+      .replace("%VERSION%", this.version)
+      .replace("%LOCALE%", Locale.getLocale())
+      .replace("%CHANNEL%", releaseName);
+    return new URL(uri);
+  },
+
+  _url: null,
+  _overridden: false,
+
+  get href() {
+    return this._url.href;
+  },
+
+  get origin() {
+    return this._url.origin;
+  },
+
+  get overridden() {
+    return this._overridden;
+  },
+
+  get version() {
+    return NEWTAB_VERSION;
+  },
+
+  get channels() {
+    return VALID_CHANNELS;
+  },
+
+  /**
+   * Returns the release name from an Update Channel name
+   *
+   * @return {String} a release name based on the update channel. Defaults to nightly
+   */
+  _releaseFromUpdateChannel(channel) {
+    let result = "nightly";
+    if (VALID_CHANNELS.has(channel)) {
+      result = channel;
+    }
+    return result;
+  },
+
+  /*
+   * Updates the location when the page is not overriden.
+   * Useful when there is a pref change
+   */
+  _updateMaybe() {
+    if (!this.overridden) {
+      let url = this._generateDefaultURL();
+      if (url.href !== this._url.href) {
+        this._url = url;
+        Services.obs.notifyObservers(null, "remote-new-tab-location-changed",
+          this._url.href);
+      }
+    }
+  },
+
+  /*
+   * Override the Remote newtab page location.
+   */
+  override(newURL) {
+    let url = new URL(newURL);
+    if (url.href !== this._url.href) {
+      this._overridden = true;
+      this._url = url;
+      Services.obs.notifyObservers(null, "remote-new-tab-location-changed",
+                                   this._url.href);
+    }
+  },
+
+  /*
+   * Reset the newtab page location to the default value
+   */
+  reset() {
+    let url = this._generateDefaultURL();
+    if (url.href !== this._url.href) {
+      this._url = url;
+      this._overridden = false;
+      Services.obs.notifyObservers(null, "remote-new-tab-location-changed",
+        this._url.href);
+    }
+  },
+
+  init() {
+    NewTabPrefsProvider.prefs.on(
+      PREF_SELECTED_LOCALE,
+      this._updateMaybe.bind(this));
+
+    NewTabPrefsProvider.prefs.on(
+      PREF_MATCH_OS_LOCALE,
+      this._updateMaybe.bind(this));
+
+    this._url = this._generateDefaultURL();
+  },
+
+  uninit() {
+    this._url = null;
+    this._overridden = false;
+    NewTabPrefsProvider.prefs.off(PREF_SELECTED_LOCALE, this._updateMaybe);
+    NewTabPrefsProvider.prefs.off(PREF_MATCH_OS_LOCALE, this._updateMaybe);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/RemoteNewTabUtils.jsm
@@ -0,0 +1,766 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["RemoteNewTabUtils"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+  "resource://gre/modules/PlacesUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
+  "resource://gre/modules/PageThumbs.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch",
+  "resource://gre/modules/BinarySearch.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gPrincipal", function () {
+  let uri = Services.io.newURI("about:newtab", null, null);
+  return Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+});
+
+// The maximum number of results PlacesProvider retrieves from history.
+const HISTORY_RESULTS_LIMIT = 100;
+
+// The maximum number of links Links.getLinks will return.
+const LINKS_GET_LINKS_LIMIT = 100;
+
+/**
+ * Singleton that serves as the default link provider for the grid. It queries
+ * the history to retrieve the most frequently visited sites.
+ */
+let PlacesProvider = {
+  /**
+   * A count of how many batch updates are under way (batches may be nested, so
+   * we keep a counter instead of a simple bool).
+   **/
+  _batchProcessingDepth: 0,
+
+  /**
+   * A flag that tracks whether onFrecencyChanged was notified while a batch
+   * operation was in progress, to tell us whether to take special action after
+   * the batch operation completes.
+   **/
+  _batchCalledFrecencyChanged: false,
+
+  /**
+   * Set this to change the maximum number of links the provider will provide.
+   */
+  maxNumLinks: HISTORY_RESULTS_LIMIT,
+
+  /**
+   * Must be called before the provider is used.
+   */
+  init: function PlacesProvider_init() {
+    PlacesUtils.history.addObserver(this, true);
+  },
+
+  /**
+   * Gets the current set of links delivered by this provider.
+   * @param aCallback The function that the array of links is passed to.
+   */
+  getLinks: function PlacesProvider_getLinks(aCallback) {
+    let options = PlacesUtils.history.getNewQueryOptions();
+    options.maxResults = this.maxNumLinks;
+
+    // Sort by frecency, descending.
+    options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING
+
+    let links = [];
+
+    let callback = {
+      handleResult: function (aResultSet) {
+        let row;
+
+        while ((row = aResultSet.getNextRow())) {
+          let url = row.getResultByIndex(1);
+          if (LinkChecker.checkLoadURI(url)) {
+            let title = row.getResultByIndex(2);
+            let frecency = row.getResultByIndex(12);
+            let lastVisitDate = row.getResultByIndex(5);
+            links.push({
+              url: url,
+              title: title,
+              frecency: frecency,
+              lastVisitDate: lastVisitDate,
+              type: "history",
+            });
+          }
+        }
+      },
+
+      handleError: function (aError) {
+        // Should we somehow handle this error?
+        aCallback([]);
+      },
+
+      handleCompletion: function (aReason) {
+        // The Places query breaks ties in frecency by place ID descending, but
+        // that's different from how Links.compareLinks breaks ties, because
+        // compareLinks doesn't have access to place IDs.  It's very important
+        // that the initial list of links is sorted in the same order imposed by
+        // compareLinks, because Links uses compareLinks to perform binary
+        // searches on the list.  So, ensure the list is so ordered.
+        let i = 1;
+        let outOfOrder = [];
+        while (i < links.length) {
+          if (Links.compareLinks(links[i - 1], links[i]) > 0)
+            outOfOrder.push(links.splice(i, 1)[0]);
+          else
+            i++;
+        }
+        for (let link of outOfOrder) {
+          i = BinarySearch.insertionIndexOf(Links.compareLinks, links, link);
+          links.splice(i, 0, link);
+        }
+
+        aCallback(links);
+      }
+    };
+
+    // Execute the query.
+    let query = PlacesUtils.history.getNewQuery();
+    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase);
+    db.asyncExecuteLegacyQueries([query], 1, options, callback);
+  },
+
+  /**
+   * Registers an object that will be notified when the provider's links change.
+   * @param aObserver An object with the following optional properties:
+   *        * onLinkChanged: A function that's called when a single link
+   *          changes.  It's passed the provider and the link object.  Only the
+   *          link's `url` property is guaranteed to be present.  If its `title`
+   *          property is present, then its title has changed, and the
+   *          property's value is the new title.  If any sort properties are
+   *          present, then its position within the provider's list of links may
+   *          have changed, and the properties' values are the new sort-related
+   *          values.  Note that this link may not necessarily have been present
+   *          in the lists returned from any previous calls to getLinks.
+   *        * onManyLinksChanged: A function that's called when many links
+   *          change at once.  It's passed the provider.  You should call
+   *          getLinks to get the provider's new list of links.
+   */
+  addObserver: function PlacesProvider_addObserver(aObserver) {
+    this._observers.push(aObserver);
+  },
+
+  _observers: [],
+
+  /**
+   * Called by the history service.
+   */
+  onBeginUpdateBatch: function() {
+    this._batchProcessingDepth += 1;
+  },
+
+  onEndUpdateBatch: function() {
+    this._batchProcessingDepth -= 1;
+    if (this._batchProcessingDepth == 0 && this._batchCalledFrecencyChanged) {
+      this.onManyFrecenciesChanged();
+      this._batchCalledFrecencyChanged = false;
+    }
+  },
+
+  onDeleteURI: function PlacesProvider_onDeleteURI(aURI, aGUID, aReason) {
+    // let observers remove sensetive data associated with deleted visit
+    this._callObservers("onDeleteURI", {
+      url: aURI.spec,
+    });
+  },
+
+  onClearHistory: function() {
+    this._callObservers("onClearHistory")
+  },
+
+  /**
+   * Called by the history service.
+   */
+  onFrecencyChanged: function PlacesProvider_onFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden, aLastVisitDate) {
+    // If something is doing a batch update of history entries we don't want
+    // to do lots of work for each record. So we just track the fact we need
+    // to call onManyFrecenciesChanged() once the batch is complete.
+    if (this._batchProcessingDepth > 0) {
+      this._batchCalledFrecencyChanged = true;
+      return;
+    }
+    // The implementation of the query in getLinks excludes hidden and
+    // unvisited pages, so it's important to exclude them here, too.
+    if (!aHidden && aLastVisitDate) {
+      this._callObservers("onLinkChanged", {
+        url: aURI.spec,
+        frecency: aNewFrecency,
+        lastVisitDate: aLastVisitDate,
+        type: "history",
+      });
+    }
+  },
+
+  /**
+   * Called by the history service.
+   */
+  onManyFrecenciesChanged: function PlacesProvider_onManyFrecenciesChanged() {
+    this._callObservers("onManyLinksChanged");
+  },
+
+  /**
+   * Called by the history service.
+   */
+  onTitleChanged: function PlacesProvider_onTitleChanged(aURI, aNewTitle, aGUID) {
+    this._callObservers("onLinkChanged", {
+      url: aURI.spec,
+      title: aNewTitle
+    });
+  },
+
+  _callObservers: function PlacesProvider__callObservers(aMethodName, aArg) {
+    for (let obs of this._observers) {
+      if (obs[aMethodName]) {
+        try {
+          obs[aMethodName](this, aArg);
+        } catch (err) {
+          Cu.reportError(err);
+        }
+      }
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
+                                         Ci.nsISupportsWeakReference]),
+};
+
+/**
+ * Singleton that provides access to all links contained in the grid (including
+ * the ones that don't fit on the grid). A link is a plain object that looks
+ * like this:
+ *
+ * {
+ *   url: "http://www.mozilla.org/",
+ *   title: "Mozilla",
+ *   frecency: 1337,
+ *   lastVisitDate: 1394678824766431,
+ * }
+ */
+let Links = {
+  /**
+   * The maximum number of links returned by getLinks.
+   */
+  maxNumLinks: LINKS_GET_LINKS_LIMIT,
+
+  /**
+   * A mapping from each provider to an object { sortedLinks, siteMap, linkMap }.
+   * sortedLinks is the cached, sorted array of links for the provider.
+   * siteMap is a mapping from base domains to URL count associated with the domain.
+   *         siteMap is used to look up a user's top sites that can be targeted
+   *         with a suggested tile.
+   * linkMap is a Map from link URLs to link objects.
+   */
+  _providers: new Map(),
+
+  /**
+   * The properties of link objects used to sort them.
+   */
+  _sortProperties: [
+    "frecency",
+    "lastVisitDate",
+    "url",
+  ],
+
+  /**
+   * List of callbacks waiting for the cache to be populated.
+   */
+  _populateCallbacks: [],
+
+  /**
+   * A list of objects that are observing links updates.
+   */
+  _observers: [],
+
+  /**
+   * Registers an object that will be notified when links updates.
+   */
+  addObserver: function (aObserver) {
+    this._observers.push(aObserver);
+  },
+
+  /**
+   * Adds a link provider.
+   * @param aProvider The link provider.
+   */
+  addProvider: function Links_addProvider(aProvider) {
+    this._providers.set(aProvider, null);
+    aProvider.addObserver(this);
+  },
+
+  /**
+   * Removes a link provider.
+   * @param aProvider The link provider.
+   */
+  removeProvider: function Links_removeProvider(aProvider) {
+    if (!this._providers.delete(aProvider))
+      throw new Error("Unknown provider");
+  },
+
+  /**
+   * Populates the cache with fresh links from the providers.
+   * @param aCallback The callback to call when finished (optional).
+   * @param aForce When true, populates the cache even when it's already filled.
+   */
+  populateCache: function Links_populateCache(aCallback, aForce) {
+    let callbacks = this._populateCallbacks;
+
+    // Enqueue the current callback.
+    callbacks.push(aCallback);
+
+    // There was a callback waiting already, thus the cache has not yet been
+    // populated.
+    if (callbacks.length > 1)
+      return;
+
+    function executeCallbacks() {
+      while (callbacks.length) {
+        let callback = callbacks.shift();
+        if (callback) {
+          try {
+            callback();
+          } catch (e) {
+            // We want to proceed even if a callback fails.
+          }
+        }
+      }
+    }
+
+    let numProvidersRemaining = this._providers.size;
+    for (let [provider, links] of this._providers) {
+      this._populateProviderCache(provider, () => {
+        if (--numProvidersRemaining == 0)
+          executeCallbacks();
+      }, aForce);
+    }
+  },
+
+  /**
+   * Gets the current set of links contained in the grid.
+   * @return The links in the grid.
+   */
+  getLinks: function Links_getLinks() {
+    let links = this._getMergedProviderLinks();
+
+    let sites = new Set();
+
+    // Filter duplicate base domains.
+    links = links.filter(function (link) {
+      let site = RemoteNewTabUtils.extractSite(link.url);
+      link.baseDomain = site;
+      if (site == null || sites.has(site))
+        return false;
+      sites.add(site);
+
+      return true;
+    });
+
+    return links;
+  },
+
+  /**
+   * Resets the links cache.
+   */
+  resetCache: function Links_resetCache() {
+    for (let provider of this._providers.keys()) {
+      this._providers.set(provider, null);
+    }
+  },
+
+  /**
+   * Compares two links.
+   * @param aLink1 The first link.
+   * @param aLink2 The second link.
+   * @return A negative number if aLink1 is ordered before aLink2, zero if
+   *         aLink1 and aLink2 have the same ordering, or a positive number if
+   *         aLink1 is ordered after aLink2.
+   *
+   * @note compareLinks's this object is bound to Links below.
+   */
+  compareLinks: function Links_compareLinks(aLink1, aLink2) {
+    for (let prop of this._sortProperties) {
+      if (!(prop in aLink1) || !(prop in aLink2))
+        throw new Error("Comparable link missing required property: " + prop);
+    }
+    return aLink2.frecency - aLink1.frecency ||
+           aLink2.lastVisitDate - aLink1.lastVisitDate ||
+           aLink1.url.localeCompare(aLink2.url);
+  },
+
+  _incrementSiteMap: function(map, link) {
+    let site = RemoteNewTabUtils.extractSite(link.url);
+    map.set(site, (map.get(site) || 0) + 1);
+  },
+
+  _decrementSiteMap: function(map, link) {
+    let site = RemoteNewTabUtils.extractSite(link.url);
+    let previousURLCount = map.get(site);
+    if (previousURLCount === 1) {
+      map.delete(site);
+    } else {
+      map.set(site, previousURLCount - 1);
+    }
+  },
+
+  /**
+    * Update the siteMap cache based on the link given and whether we need
+    * to increment or decrement it. We do this by iterating over all stored providers
+    * to find which provider this link already exists in. For providers that
+    * have this link, we will adjust siteMap for them accordingly.
+    *
+    * @param aLink The link that will affect siteMap
+    * @param increment A boolean for whether to increment or decrement siteMap
+    */
+  _adjustSiteMapAndNotify: function(aLink, increment=true) {
+    for (let [provider, cache] of this._providers) {
+      // We only update siteMap if aLink is already stored in linkMap.
+      if (cache.linkMap.get(aLink.url)) {
+        if (increment) {
+          this._incrementSiteMap(cache.siteMap, aLink);
+          continue;
+        }
+        this._decrementSiteMap(cache.siteMap, aLink);
+      }
+    }
+    this._callObservers("onLinkChanged", aLink);
+  },
+
+  populateProviderCache: function(provider, callback) {
+    if (!this._providers.has(provider)) {
+      throw new Error("Can only populate provider cache for existing provider.");
+    }
+
+    return this._populateProviderCache(provider, callback, false);
+  },
+
+  /**
+   * Calls getLinks on the given provider and populates our cache for it.
+   * @param aProvider The provider whose cache will be populated.
+   * @param aCallback The callback to call when finished.
+   * @param aForce When true, populates the provider's cache even when it's
+   *               already filled.
+   */
+  _populateProviderCache: function (aProvider, aCallback, aForce) {
+    let cache = this._providers.get(aProvider);
+    let createCache = !cache;
+    if (createCache) {
+      cache = {
+        // Start with a resolved promise.
+        populatePromise: new Promise(resolve => resolve()),
+      };
+      this._providers.set(aProvider, cache);
+    }
+    // Chain the populatePromise so that calls are effectively queued.
+    cache.populatePromise = cache.populatePromise.then(() => {
+      return new Promise(resolve => {
+        if (!createCache && !aForce) {
+          aCallback();
+          resolve();
+          return;
+        }
+        aProvider.getLinks(links => {
+          // Filter out null and undefined links so we don't have to deal with
+          // them in getLinks when merging links from providers.
+          links = links.filter((link) => !!link);
+          cache.sortedLinks = links;
+          cache.siteMap = links.reduce((map, link) => {
+            this._incrementSiteMap(map, link);
+            return map;
+          }, new Map());
+          cache.linkMap = links.reduce((map, link) => {
+            map.set(link.url, link);
+            return map;
+          }, new Map());
+          aCallback();
+          resolve();
+        });
+      });
+    });
+  },
+
+  /**
+   * Merges the cached lists of links from all providers whose lists are cached.
+   * @return The merged list.
+   */
+  _getMergedProviderLinks: function Links__getMergedProviderLinks() {
+    // Build a list containing a copy of each provider's sortedLinks list.
+    let linkLists = [];
+    for (let provider of this._providers.keys()) {
+      let links = this._providers.get(provider);
+      if (links && links.sortedLinks) {
+        linkLists.push(links.sortedLinks.slice());
+      }
+    }
+
+    function getNextLink() {
+      let minLinks = null;
+      for (let links of linkLists) {
+        if (links.length &&
+            (!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0))
+          minLinks = links;
+      }
+      return minLinks ? minLinks.shift() : null;
+    }
+
+    let finalLinks = [];
+    for (let nextLink = getNextLink();
+         nextLink && finalLinks.length < this.maxNumLinks;
+         nextLink = getNextLink()) {
+      finalLinks.push(nextLink);
+    }
+
+    return finalLinks;
+  },
+
+  /**
+   * Called by a provider to notify us when a single link changes.
+   * @param aProvider The provider whose link changed.
+   * @param aLink The link that changed.  If the link is new, it must have all
+   *              of the _sortProperties.  Otherwise, it may have as few or as
+   *              many as is convenient.
+   * @param aIndex The current index of the changed link in the sortedLinks
+                   cache in _providers. Defaults to -1 if the provider doesn't know the index
+   * @param aDeleted Boolean indicating if the provider has deleted the link.
+   */
+  onLinkChanged: function Links_onLinkChanged(aProvider, aLink, aIndex=-1, aDeleted=false) {
+    if (!("url" in aLink))
+      throw new Error("Changed links must have a url property");
+
+    let links = this._providers.get(aProvider);
+    if (!links)
+      // This is not an error, it just means that between the time the provider
+      // was added and the future time we call getLinks on it, it notified us of
+      // a change.
+      return;
+
+    let { sortedLinks, siteMap, linkMap } = links;
+    let existingLink = linkMap.get(aLink.url);
+    let insertionLink = null;
+
+    if (existingLink) {
+      // Update our copy's position in O(lg n) by first removing it from its
+      // list.  It's important to do this before modifying its properties.
+      if (this._sortProperties.some(prop => prop in aLink)) {
+        let idx = aIndex;
+        if (idx < 0) {
+          idx = this._indexOf(sortedLinks, existingLink);
+        } else if (this.compareLinks(aLink, sortedLinks[idx]) != 0) {
+          throw new Error("aLink should be the same as sortedLinks[idx]");
+        }
+
+        if (idx < 0) {
+          throw new Error("Link should be in _sortedLinks if in _linkMap");
+        }
+        sortedLinks.splice(idx, 1);
+
+        if (aDeleted) {
+          linkMap.delete(existingLink.url);
+          this._decrementSiteMap(siteMap, existingLink);
+        } else {
+          // Update our copy's properties.
+          Object.assign(existingLink, aLink);
+
+          // Finally, reinsert our copy below.
+          insertionLink = existingLink;
+        }
+      }
+      // Update our copy's title in O(1).
+      if ("title" in aLink && aLink.title != existingLink.title) {
+        existingLink.title = aLink.title;
+      }
+    }
+    else if (this._sortProperties.every(prop => prop in aLink)) {
+      // Before doing the O(lg n) insertion below, do an O(1) check for the
+      // common case where the new link is too low-ranked to be in the list.
+      if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) {
+        let lastLink = sortedLinks[sortedLinks.length - 1];
+        if (this.compareLinks(lastLink, aLink) < 0) {
+          return;
+        }
+      }
+      // Copy the link object so that changes later made to it by the caller
+      // don't affect our copy.
+      insertionLink = {};
+      for (let prop in aLink) {
+        insertionLink[prop] = aLink[prop];
+      }
+      linkMap.set(aLink.url, insertionLink);
+      this._incrementSiteMap(siteMap, aLink);
+    }
+
+    if (insertionLink) {
+      let idx = this._insertionIndexOf(sortedLinks, insertionLink);
+      sortedLinks.splice(idx, 0, insertionLink);
+      if (sortedLinks.length > aProvider.maxNumLinks) {
+        let lastLink = sortedLinks.pop();
+        linkMap.delete(lastLink.url);
+        this._decrementSiteMap(siteMap, lastLink);
+      }
+    }
+  },
+
+  /**
+   * Called by a provider to notify us when many links change.
+   */
+  onManyLinksChanged: function Links_onManyLinksChanged(aProvider) {
+    this._populateProviderCache(aProvider, () => {}, true);
+  },
+
+  _indexOf: function Links__indexOf(aArray, aLink) {
+    return this._binsearch(aArray, aLink, "indexOf");
+  },
+
+  _insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) {
+    return this._binsearch(aArray, aLink, "insertionIndexOf");
+  },
+
+  _binsearch: function Links__binsearch(aArray, aLink, aMethod) {
+    return BinarySearch[aMethod](this.compareLinks, aArray, aLink);
+  },
+
+  _callObservers(methodName, ...args) {
+    for (let obs of this._observers) {
+      if (typeof(obs[methodName]) == "function") {
+        try {
+          obs[methodName](this, ...args);
+        } catch (err) {
+          Cu.reportError(err);
+        }
+      }
+    }
+  },
+};
+
+Links.compareLinks = Links.compareLinks.bind(Links);
+
+/**
+ * Singleton that checks if a given link should be displayed on about:newtab
+ * or if we should rather not do it for security reasons. URIs that inherit
+ * their caller's principal will be filtered.
+ */
+let LinkChecker = {
+  _cache: {},
+
+  get flags() {
+    return Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL |
+           Ci.nsIScriptSecurityManager.DONT_REPORT_ERRORS;
+  },
+
+  checkLoadURI: function LinkChecker_checkLoadURI(aURI) {
+    if (!(aURI in this._cache))
+      this._cache[aURI] = this._doCheckLoadURI(aURI);
+
+    return this._cache[aURI];
+  },
+
+  _doCheckLoadURI: function Links_doCheckLoadURI(aURI) {
+    try {
+      Services.scriptSecurityManager.
+        checkLoadURIStrWithPrincipal(gPrincipal, aURI, this.flags);
+      return true;
+    } catch (e) {
+      // We got a weird URI or one that would inherit the caller's principal.
+      return false;
+    }
+  }
+};
+
+let ExpirationFilter = {
+  init: function ExpirationFilter_init() {
+    PageThumbs.addExpirationFilter(this);
+  },
+
+  filterForThumbnailExpiration:
+  function ExpirationFilter_filterForThumbnailExpiration(aCallback) {
+    Links.populateCache(function () {
+      let urls = [];
+
+      // Add all URLs to the list that we want to keep thumbnails for.
+      for (let link of Links.getLinks().slice(0, 25)) {
+        if (link && link.url)
+          urls.push(link.url);
+      }
+
+      aCallback(urls);
+    });
+  }
+};
+
+/**
+ * Singleton that provides the public API of this JSM.
+ */
+this.RemoteNewTabUtils = {
+  _initialized: false,
+
+  /**
+   * Extract a "site" from a url in a way that multiple urls of a "site" returns
+   * the same "site."
+   * @param aUrl Url spec string
+   * @return The "site" string or null
+   */
+  extractSite: function Links_extractSite(url) {
+    let host;
+    try {
+      // Note that nsIURI.asciiHost throws NS_ERROR_FAILURE for some types of
+      // URIs, including jar and moz-icon URIs.
+      host = Services.io.newURI(url, null, null).asciiHost;
+    } catch (ex) {
+      return null;
+    }
+
+    // Strip off common subdomains of the same site (e.g., www, load balancer)
+    return host.replace(/^(m|mobile|www\d*)\./, "");
+  },
+
+  init: function RemoteNewTabUtils_init() {
+    if (this.initWithoutProviders()) {
+      PlacesProvider.init();
+      Links.addProvider(PlacesProvider);
+    }
+  },
+
+  initWithoutProviders: function RemoteNewTabUtils_initWithoutProviders() {
+    if (!this._initialized) {
+      this._initialized = true;
+      ExpirationFilter.init();
+      return true;
+    }
+    return false;
+  },
+
+  getProviderLinks: function(aProvider) {
+    let cache = Links._providers.get(aProvider);
+    if (cache && cache.sortedLinks) {
+      return cache.sortedLinks;
+    }
+    return [];
+  },
+
+  isTopSiteGivenProvider: function(aSite, aProvider) {
+    let cache = Links._providers.get(aProvider);
+    if (cache && cache.siteMap) {
+      return cache.siteMap.has(aSite);
+    }
+    return false;
+  },
+
+  isTopPlacesSite: function(aSite) {
+    return this.isTopSiteGivenProvider(aSite, PlacesProvider);
+  },
+
+  links: Links,
+  linkChecker: LinkChecker,
+  placesProvider: PlacesProvider
+};
deleted file mode 100644
--- a/browser/components/newtab/aboutNewTabService.js
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
-*/
-
-/* globals XPCOMUtils, NewTabPrefsProvider, Services,
-  Locale, UpdateUtils
-*/
-"use strict";
-
-const {utils: Cu, interfaces: Ci} = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
-                                  "resource://gre/modules/UpdateUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
-                                  "resource:///modules/NewTabPrefsProvider.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Locale",
-                                  "resource://gre/modules/Locale.jsm");
-
-const LOCAL_NEWTAB_URL = "chrome://browser/content/newtab/newTab.xhtml";
-
-const REMOTE_NEWTAB_URL = "https://newtab.cdn.mozilla.net/" +
-                              "v%VERSION%/%CHANNEL%/%LOCALE%/index.html";
-
-// Pref that tells if remote newtab is enabled
-const PREF_REMOTE_ENABLED = "browser.newtabpage.remote";
-
-// The preference that tells whether to match the OS locale
-const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
-
-// The preference that tells what locale the user selected
-const PREF_SELECTED_LOCALE = "general.useragent.locale";
-
-const VALID_CHANNELS = new Set(["esr", "release", "beta", "aurora", "nightly"]);
-
-const REMOTE_NEWTAB_VERSION = "0";
-
-function AboutNewTabService() {
-  NewTabPrefsProvider.prefs.on(PREF_REMOTE_ENABLED, this._handleToggleEvent.bind(this));
-
-  // trigger remote change if needed, according to pref
-  this.toggleRemote(Services.prefs.getBoolPref(PREF_REMOTE_ENABLED));
-}
-
-AboutNewTabService.prototype = {
-
-  _newTabURL: LOCAL_NEWTAB_URL,
-  _remoteEnabled: false,
-  _overridden: false,
-
-  classID: Components.ID("{cef25b06-0ef6-4c50-a243-e69f943ef23d}"),
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutNewTabService]),
-  _xpcom_categories: [{
-    service: true
-  }],
-
-  _handleToggleEvent(prefName, stateEnabled, forceState) { //jshint unused:false
-    this.toggleRemote(stateEnabled, forceState);
-  },
-
-  /**
-   * React to changes to the remote newtab pref. Only act
-   * if there is a change of state and if not overridden.
-   *
-   * @returns {Boolean} Returns if there has been a state change
-   *
-   * @param {Boolean}   stateEnabled    remote state to set to
-   * @param {Boolean}   forceState      force state change
-   */
-  toggleRemote(stateEnabled, forceState) {
-
-    if (!forceState && (this._overriden || stateEnabled === this._remoteEnabled)) {
-      // exit there is no change of state
-      return false;
-    }
-
-    if (stateEnabled) {
-      this._newTabURL = this.generateRemoteURL();
-      NewTabPrefsProvider.prefs.on(
-        PREF_SELECTED_LOCALE,
-        this._updateRemoteMaybe.bind(this));
-      NewTabPrefsProvider.prefs.on(
-        PREF_MATCH_OS_LOCALE,
-        this._updateRemoteMaybe.bind(this));
-      this._remoteEnabled = true;
-    } else {
-      this._newTabURL = LOCAL_NEWTAB_URL;
-      NewTabPrefsProvider.prefs.off(PREF_SELECTED_LOCALE, this._updateRemoteMaybe);
-      NewTabPrefsProvider.prefs.off(PREF_MATCH_OS_LOCALE, this._updateRemoteMaybe);
-      this._remoteEnabled = false;
-    }
-    return true;
-  },
-
-  /*
-   * Generate a default url based on locale and update channel
-   */
-  generateRemoteURL() {
-    let releaseName = this.releaseFromUpdateChannel(UpdateUtils.UpdateChannel);
-    let url = REMOTE_NEWTAB_URL
-      .replace("%VERSION%", REMOTE_NEWTAB_VERSION)
-      .replace("%LOCALE%", Locale.getLocale())
-      .replace("%CHANNEL%", releaseName);
-    return url;
-  },
-
-  /*
-   * Updates the remote location when the page is not overriden.
-   *
-   * Useful when there is a dependent pref change
-   */
-  _updateRemoteMaybe() {
-    if (!this._remoteEnabled || this._overridden) {
-      return;
-    }
-
-    let url = this.generateRemoteURL();
-    if (url !== this._newTabURL) {
-      this._newTabURL = url;
-      Services.obs.notifyObservers(null, "newtab-url-changed",
-        this._newTabURL);
-    }
-  },
-
-  /**
-   * Returns the release name from an Update Channel name
-   *
-   * @return {String} a release name based on the update channel. Defaults to nightly
-   */
-  releaseFromUpdateChannel(channelName) {
-    return VALID_CHANNELS.has(channelName) ? channelName : "nightly";
-  },
-
-  get newTabURL() {
-    return this._newTabURL;
-  },
-
-  get remoteVersion() {
-    return REMOTE_NEWTAB_VERSION;
-  },
-
-  get remoteReleaseName() {
-    return this.releaseFromUpdateChannel(UpdateUtils.UpdateChannel);
-  },
-
-  set newTabURL(aNewTabURL) {
-    let remoteURL = this.generateRemoteURL();
-    let prefRemoteEnabled = Services.prefs.getBoolPref(PREF_REMOTE_ENABLED);
-    let isResetLocal = !prefRemoteEnabled && aNewTabURL === LOCAL_NEWTAB_URL;
-    let isResetRemote = prefRemoteEnabled && aNewTabURL === remoteURL;
-
-    if (isResetLocal || isResetRemote) {
-      if (this._overriden) {
-        // only trigger a reset if previously overridden
-        this.resetNewTabURL();
-      }
-      return;
-    }
-    // turn off remote state if needed
-    this.toggleRemote(false);
-    this._newTabURL = aNewTabURL;
-    this._overridden = true;
-    Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
-  },
-
-  get overridden() {
-    return this._overridden;
-  },
-
-  get remoteEnabled() {
-    return this._remoteEnabled;
-  },
-
-  resetNewTabURL() {
-    this._overridden = false;
-    this.toggleRemote(Services.prefs.getBoolPref(PREF_REMOTE_ENABLED), true);
-    Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
-  }
-};
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AboutNewTabService]);
--- a/browser/components/newtab/moz.build
+++ b/browser/components/newtab/moz.build
@@ -1,27 +1,21 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
+if not CONFIG['RELEASE_BUILD']:
+    BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 
-XPCSHELL_TESTS_MANIFESTS += [
-    'tests/xpcshell/xpcshell.ini',
-]
-
-EXTRA_JS_MODULES += [
-    'NewTabPrefsProvider.jsm',
-    'PlacesProvider.jsm',
-]
+    XPCSHELL_TESTS_MANIFESTS += [
+        'tests/xpcshell/xpcshell.ini',
+    ]
 
-XPIDL_SOURCES += [
-    'nsIAboutNewTabService.idl',
-]
-
-XPIDL_MODULE = 'browser-newtab'
-
-EXTRA_COMPONENTS += [
-    'aboutNewTabService.js',
-    'NewTabComponents.manifest',
-]
+    EXTRA_JS_MODULES += [
+        'NewTabPrefsProvider.jsm',
+        'NewTabURL.jsm',
+        'PlacesProvider.jsm',
+        'RemoteAboutNewTab.jsm',
+        'RemoteNewTabLocation.jsm',
+        'RemoteNewTabUtils.jsm',
+    ]
deleted file mode 100644
--- a/browser/components/newtab/nsIAboutNewTabService.idl
+++ /dev/null
@@ -1,58 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#include "nsISupports.idl"
-
-/**
- * Allows to override about:newtab to point to a different location
- * than the one specified within AboutRedirector.cpp
- */
-
-[scriptable, uuid(cef25b06-0ef6-4c50-a243-e69f943ef23d)]
-interface nsIAboutNewTabService : nsISupports
-{
-  /**
-   * Returns the url of the resource for the newtab page if not overridden,
-   * otherwise a string represenation of the new URL.
-   */
-  attribute ACString newTabURL;
-
-  /**
-   * Returns true if the default resource got overridden.
-   */
-  readonly attribute bool overridden;
-
-  /**
-   * Returns true if the default resource is remotely hosted and isn't
-   * overridden
-   */
-  readonly attribute bool remoteEnabled;
-
-
-  /**
-  * Returns the version of the remote newtab page expected
-  */
-  readonly attribute ACString remoteVersion;
-
-  /**
-   * Returns the expected channel for the remote the newtab page
-   */
-  readonly attribute ACString remoteReleaseName;
-
-  /**
-   * Generates and returns the remote newtab page url
-   */
-  ACString generateRemoteURL();
-
-  /**
-   * Returns a remote new tab release name given an update channel name
-   */
-  ACString releaseFromUpdateChannel(in ACString channelName);
-
-  /**
-   * Resets to the default resource and also resets the
-   * overridden attribute to false.
-   */
-  void resetNewTabURL();
-};
--- a/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js
+++ b/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js
@@ -1,57 +1,46 @@
-/* globals XPCOMUtils, aboutNewTabService, Services */
+/* globals XPCOMUtils, Task, RemoteAboutNewTab, RemoteNewTabLocation, ok */
 "use strict";
 
 let Cu = Components.utils;
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "RemotePageManager",
-                                  "resource://gre/modules/RemotePageManager.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
-                                   "@mozilla.org/browser/aboutnewtab-service;1",
-                                   "nsIAboutNewTabService");
+XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabLocation",
+  "resource:///modules/RemoteNewTabLocation.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RemoteAboutNewTab",
+  "resource:///modules/RemoteAboutNewTab.jsm");
 
 const TEST_URL = "https://example.com/browser/browser/components/newtab/tests/browser/dummy_page.html";
+const NEWTAB_URL = "about:remote-newtab";
+
+let tests = [];
+
+/*
+ * Tests that:
+ * 1. overriding the RemoteNewTabPageLocation url causes a remote newtab page
+ *    to load with the new url.
+ * 2. Messages pass between remote page <--> newTab.js <--> RemoteAboutNewTab.js
+ */
+tests.push(Task.spawn(function* testMessage() {
+  yield new Promise(resolve => {
+    RemoteAboutNewTab.pageListener.addMessageListener("NewTab:testMessage", () => {
+      ok(true, "message received");
+      resolve();
+    });
+  });
+}));
 
 add_task(function* open_newtab() {
-  let notificationPromise = nextChangeNotificationPromise(TEST_URL, "newtab page now points to test url");
-  aboutNewTabService.newTabURL = TEST_URL;
-
-  yield notificationPromise;
-  Assert.ok(aboutNewTabService.overridden, "url has been overridden");
-
+  RemoteNewTabLocation.override(TEST_URL);
+  ok(RemoteNewTabLocation.href === TEST_URL, "RemoteNewTabLocation has been overridden");
   let tabOptions = {
     gBrowser,
-    url: "about:newtab",
+    url: NEWTAB_URL,
   };
 
-  yield BrowserTestUtils.withNewTab(tabOptions, function* (browser) {
-    Assert.equal(TEST_URL, browser.contentWindow.location, `New tab should open to ${TEST_URL}`);
-  });
+  for (let test of tests) {
+    yield BrowserTestUtils.withNewTab(tabOptions, function* (browser) { // jshint ignore:line
+      yield test;
+    }); // jshint ignore:line
+  }
 });
-
-add_task(function* emptyURL() {
-  let notificationPromise = nextChangeNotificationPromise("", "newtab service now points to empty url");
-  aboutNewTabService.newTabURL = "";
-  yield notificationPromise;
-
-  let tabOptions = {
-    gBrowser,
-    url: "about:newtab",
-  };
-
-  yield BrowserTestUtils.withNewTab(tabOptions, function* (browser) {
-    Assert.equal("about:blank", browser.contentWindow.location, `New tab should open to ${"about:blank"}`);
-  });
-});
-
-function nextChangeNotificationPromise(aNewURL, testMessage) {
-  return new Promise(resolve => {
-    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {  // jshint unused:false
-      Services.obs.removeObserver(observer, aTopic);
-      Assert.equal(aData, aNewURL, testMessage);
-      resolve();
-    }, "newtab-url-changed", false);
-  });
-}
--- a/browser/components/newtab/tests/browser/dummy_page.html
+++ b/browser/components/newtab/tests/browser/dummy_page.html
@@ -1,10 +1,22 @@
 <!DOCTYPE html>
 
 <html>
 <head>
   <meta charset="utf-8">
 </head>
 <body>
 <p>Dummy Page</p>
+<script type="text/javascript;version=1.8">
+  document.addEventListener("NewTabCommandReady", function readyCmd() {
+    document.removeEventListener("NewTabCommandReady", readyCmd);
+
+    let event = new CustomEvent("NewTabCommand", {
+      detail: {
+        command: "NewTab:testMessage"
+      }
+    });
+    document.dispatchEvent(event);
+  });
+</script>
 </body>
 </html>
--- a/browser/components/newtab/tests/xpcshell/test_AboutNewTabService.js
+++ b/browser/components/newtab/tests/xpcshell/test_AboutNewTabService.js
@@ -1,145 +1,39 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
-
-/* globals Services, XPCOMUtils, NewTabPrefsProvider, Preferences, aboutNewTabService */
-
 "use strict";
 
-const {utils: Cu} = Components;
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
-                                  "resource:///modules/NewTabPrefsProvider.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
-                                   "@mozilla.org/browser/aboutnewtab-service;1",
-                                   "nsIAboutNewTabService");
-
-const DEFAULT_HREF = aboutNewTabService.generateRemoteURL();
+Components.utils.import("resource://gre/modules/Services.jsm");
+let aboutNewTabService = Components.classes["@mozilla.org/browser/aboutnewtab-service;1"]
+                                   .getService(Components.interfaces.nsIAboutNewTabService);
 
-/**
- * Test the overriding of the default URL
- */
 add_task(function* () {
-  NewTabPrefsProvider.prefs.init();
-  let notificationPromise;
-  Services.prefs.setBoolPref("browser.newtabpage.remote", false);
-
-  // tests default is the local newtab resource
-  Assert.equal(aboutNewTabService.newTabURL, "chrome://browser/content/newtab/newTab.xhtml",
-               "Default newtab URL should be chrome://browser/content/newtab/newTab.xhtml");
-
+  Assert.equal(aboutNewTabService.newTabURL, "about:newtab", "Default newtab URL should be about:newtab");
   let url = "http://example.com/";
-  notificationPromise = nextChangeNotificationPromise(url);
+  let notificationPromise = promiseNewtabURLNotification(url);
   aboutNewTabService.newTabURL = url;
   yield notificationPromise;
   Assert.ok(aboutNewTabService.overridden, "Newtab URL should be overridden");
-  Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
   Assert.equal(aboutNewTabService.newTabURL, url, "Newtab URL should be the custom URL");
 
-  notificationPromise = nextChangeNotificationPromise("chrome://browser/content/newtab/newTab.xhtml");
+  notificationPromise = promiseNewtabURLNotification("about:newtab");
   aboutNewTabService.resetNewTabURL();
   yield notificationPromise;
   Assert.ok(!aboutNewTabService.overridden, "Newtab URL should not be overridden");
-  Assert.equal(aboutNewTabService.newTabURL, "chrome://browser/content/newtab/newTab.xhtml",
-               "Newtab URL should be the default");
+  Assert.equal(aboutNewTabService.newTabURL, "about:newtab", "Newtab URL should be the about:newtab");
 
   // change newtab page to remote
   Services.prefs.setBoolPref("browser.newtabpage.remote", true);
-  let remoteHref = aboutNewTabService.generateRemoteURL();
-  Assert.equal(aboutNewTabService.newTabURL, remoteHref, "Newtab URL should be the default remote URL");
+  Assert.equal(aboutNewTabService.newTabURL, "about:remote-newtab", "Newtab URL should be the about:remote-newtab");
   Assert.ok(!aboutNewTabService.overridden, "Newtab URL should not be overridden");
-  Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
-  NewTabPrefsProvider.prefs.uninit();
 });
 
-/**
- * Tests reponse to updates to prefs
- */
-add_task(function* test_updates() {
-  Preferences.set("browser.newtabpage.remote", true);
-  let notificationPromise;
-  let expectedHref = "https://newtab.cdn.mozilla.net" +
-                     `/v${aboutNewTabService.remoteVersion}` +
-                     `/${aboutNewTabService.remoteReleaseName}` +
-                     "/en-GB" +
-                     "/index.html";
-  Preferences.set("intl.locale.matchOS", true);
-  Preferences.set("general.useragent.locale", "en-GB");
-  NewTabPrefsProvider.prefs.init();
-
-  // test update checks for prefs
-  notificationPromise = nextChangeNotificationPromise(
-    expectedHref, "Remote href should be updated");
-  Preferences.set("intl.locale.matchOS", false);
-  yield notificationPromise;
-
-  notificationPromise = nextChangeNotificationPromise(
-    DEFAULT_HREF, "Remote href changes back to default");
-  Preferences.set("general.useragent.locale", "en-US");
-
-  yield notificationPromise;
-
-  // test update fires on override and reset
-  let testURL = "https://example.com/";
-  notificationPromise = nextChangeNotificationPromise(
-    testURL, "a notification occurs on override");
-  aboutNewTabService.newTabURL = testURL;
-  yield notificationPromise;
-
-  // from overridden to default
-  notificationPromise = nextChangeNotificationPromise(
-    DEFAULT_HREF, "a notification occurs on reset");
-  aboutNewTabService.resetNewTabURL();
-  Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
-  yield notificationPromise;
-
-  // override to default URL from default URL
-  notificationPromise = nextChangeNotificationPromise(
-    testURL, "a notification only occurs for a change in overridden urls");
-  aboutNewTabService.newTabURL = aboutNewTabService.generateRemoteURL();
-  Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
-  aboutNewTabService.newTabURL = testURL;
-  Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
-  yield notificationPromise;
-
-  // reset twice, only one notification for default URL
-  notificationPromise = nextChangeNotificationPromise(
-    DEFAULT_HREF, "reset occurs");
-  aboutNewTabService.resetNewTabURL();
-  yield notificationPromise;
-
-  NewTabPrefsProvider.prefs.uninit();
-});
-
-/**
- * Verifies that releaseFromUpdateChannel
- * Returns the correct release names
- */
-add_task(function* test_release_names() {
-  let valid_channels = ["esr", "release", "beta", "aurora", "nightly"];
-  let invalid_channels = new Set(["default", "invalid"]);
-
-  for (let channel of valid_channels) {
-    Assert.equal(channel, aboutNewTabService.releaseFromUpdateChannel(channel),
-          "release == channel name when valid");
-  }
-
-  for (let channel of invalid_channels) {
-    Assert.equal("nightly", aboutNewTabService.releaseFromUpdateChannel(channel),
-          "release == nightly when invalid");
-  }
-});
-
-function nextChangeNotificationPromise(aNewURL, testMessage) {
+function promiseNewtabURLNotification(aNewURL) {
   return new Promise(resolve => {
-    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {  // jshint unused:false
+    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
       Services.obs.removeObserver(observer, aTopic);
-      Assert.equal(aData, aNewURL, testMessage);
+      Assert.equal(aData, aNewURL, "Data for newtab-url-changed notification should be new URL.");
       resolve();
     }, "newtab-url-changed", false);
   });
 }
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/test_NewTabURL.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+Components.utils.import("resource:///modules/NewTabURL.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+add_task(function* () {
+  Assert.equal(NewTabURL.get(), "about:newtab", "Default newtab URL should be about:newtab");
+  let url = "http://example.com/";
+  let notificationPromise = promiseNewtabURLNotification(url);
+  NewTabURL.override(url);
+  yield notificationPromise;
+  Assert.ok(NewTabURL.overridden, "Newtab URL should be overridden");
+  Assert.equal(NewTabURL.get(), url, "Newtab URL should be the custom URL");
+
+  notificationPromise = promiseNewtabURLNotification("about:newtab");
+  NewTabURL.reset();
+  yield notificationPromise;
+  Assert.ok(!NewTabURL.overridden, "Newtab URL should not be overridden");
+  Assert.equal(NewTabURL.get(), "about:newtab", "Newtab URL should be the about:newtab");
+
+  // change newtab page to remote
+  Services.prefs.setBoolPref("browser.newtabpage.remote", true);
+  Assert.equal(NewTabURL.get(), "about:remote-newtab", "Newtab URL should be the about:remote-newtab");
+  Assert.ok(!NewTabURL.overridden, "Newtab URL should not be overridden");
+});
+
+function promiseNewtabURLNotification(aNewURL) {
+  return new Promise(resolve => {
+    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+      Services.obs.removeObserver(observer, aTopic);
+      Assert.equal(aData, aNewURL, "Data for newtab-url-changed notification should be new URL.");
+      resolve();
+    }, "newtab-url-changed", false);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/test_RemoteNewTabLocation.js
@@ -0,0 +1,148 @@
+/* globals ok, equal, RemoteNewTabLocation, NewTabPrefsProvider, Services, Preferences, XPCOMUtils, UpdateUtils */
+/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource:///modules/RemoteNewTabLocation.jsm");
+Cu.import("resource:///modules/NewTabPrefsProvider.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.importGlobalProperties(["URL"]);
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+  "resource://gre/modules/UpdateUtils.jsm");
+
+RemoteNewTabLocation.init();
+const DEFAULT_HREF = RemoteNewTabLocation.href;
+RemoteNewTabLocation.uninit();
+
+add_task(function* test_defaults() {
+  RemoteNewTabLocation.init();
+  ok(RemoteNewTabLocation.href, "Default location has an href");
+  ok(RemoteNewTabLocation.origin, "Default location has an origin");
+  ok(!RemoteNewTabLocation.overridden, "Default location is not overridden");
+  RemoteNewTabLocation.uninit();
+});
+
+/**
+ * Tests the overriding of the default URL
+ */
+add_task(function* test_overrides() {
+  RemoteNewTabLocation.init();
+  let testURL = new URL("https://example.com/");
+  let notificationPromise;
+
+  notificationPromise = nextChangeNotificationPromise(
+    testURL.href, "Remote Location should change");
+  RemoteNewTabLocation.override(testURL.href);
+  yield notificationPromise;
+  ok(RemoteNewTabLocation.overridden, "Remote location should be overridden");
+  equal(RemoteNewTabLocation.href, testURL.href,
+        "Remote href should be the custom URL");
+  equal(RemoteNewTabLocation.origin, testURL.origin,
+        "Remote origin should be the custom URL");
+
+  notificationPromise = nextChangeNotificationPromise(
+    DEFAULT_HREF, "Remote href should be reset");
+  RemoteNewTabLocation.reset();
+  yield notificationPromise;
+  ok(!RemoteNewTabLocation.overridden, "Newtab URL should not be overridden");
+  RemoteNewTabLocation.uninit();
+});
+
+/**
+ * Tests how RemoteNewTabLocation responds to updates to prefs
+ */
+add_task(function* test_updates() {
+  RemoteNewTabLocation.init();
+  let notificationPromise;
+  let release = RemoteNewTabLocation._releaseFromUpdateChannel(
+    UpdateUtils.UpdateChannel);
+  let expectedHref = "https://newtab.cdn.mozilla.net" +
+                     `/v${RemoteNewTabLocation.version}` +
+                     `/${release}` +
+                     "/en-GB" +
+                     "/index.html";
+  Preferences.set("intl.locale.matchOS", true);
+  Preferences.set("general.useragent.locale", "en-GB");
+  NewTabPrefsProvider.prefs.init();
+
+  // test update checks for prefs
+  notificationPromise = nextChangeNotificationPromise(
+    expectedHref, "Remote href should be updated");
+  Preferences.set("intl.locale.matchOS", false);
+  yield notificationPromise;
+
+  notificationPromise = nextChangeNotificationPromise(
+    DEFAULT_HREF, "Remote href changes back to default");
+  Preferences.set("general.useragent.locale", "en-US");
+
+  yield notificationPromise;
+
+  // test update fires on override and reset
+  let testURL = new URL("https://example.com/");
+  notificationPromise = nextChangeNotificationPromise(
+    testURL.href, "a notification occurs on override");
+  RemoteNewTabLocation.override(testURL.href);
+  yield notificationPromise;
+
+  // from overridden to default
+  notificationPromise = nextChangeNotificationPromise(
+    DEFAULT_HREF, "a notification occurs on reset");
+  RemoteNewTabLocation.reset();
+  yield notificationPromise;
+
+  // override to default URL from default URL
+  notificationPromise = nextChangeNotificationPromise(
+    testURL.href, "a notification only occurs for a change in overridden urls");
+  RemoteNewTabLocation.override(DEFAULT_HREF);
+  RemoteNewTabLocation.override(testURL.href);
+  yield notificationPromise;
+
+  // reset twice, only one notification for default URL
+  notificationPromise = nextChangeNotificationPromise(
+    DEFAULT_HREF, "reset occurs");
+  RemoteNewTabLocation.reset();
+  yield notificationPromise;
+
+  notificationPromise = nextChangeNotificationPromise(
+    testURL.href, "a notification only occurs for a change in reset urls");
+  RemoteNewTabLocation.reset();
+  RemoteNewTabLocation.override(testURL.href);
+  yield notificationPromise;
+
+  NewTabPrefsProvider.prefs.uninit();
+  RemoteNewTabLocation.uninit();
+});
+
+/**
+ * Verifies that RemoteNewTabLocation's _releaseFromUpdateChannel
+ * Returns the correct release names
+ */
+add_task(function* test_release_names() {
+  RemoteNewTabLocation.init();
+  let valid_channels = RemoteNewTabLocation.channels;
+  let invalid_channels = new Set(["default", "invalid"]);
+
+  for (let channel of valid_channels) {
+    equal(channel, RemoteNewTabLocation._releaseFromUpdateChannel(channel),
+          "release == channel name when valid");
+  }
+
+  for (let channel of invalid_channels) {
+    equal("nightly", RemoteNewTabLocation._releaseFromUpdateChannel(channel),
+          "release == nightly when invalid");
+  }
+  RemoteNewTabLocation.uninit();
+});
+
+function nextChangeNotificationPromise(aNewURL, testMessage) {
+  return new Promise(resolve => {
+    Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint ignore:line
+      Services.obs.removeObserver(observer, aTopic);
+      equal(aData, aNewURL, testMessage);
+      resolve();
+    }, "remote-new-tab-location-changed", false);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/tests/xpcshell/test_RemoteNewTabUtils.js
@@ -0,0 +1,375 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// See also browser/base/content/test/newtab/.
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+Cu.import("resource:///modules/RemoteNewTabUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function* validCacheMidPopulation() {
+  let expectedLinks = makeLinks(0, 3, 1);
+
+  let provider = new TestProvider(done => done(expectedLinks));
+  provider.maxNumLinks = expectedLinks.length;
+
+  RemoteNewTabUtils.initWithoutProviders();
+  RemoteNewTabUtils.links.addProvider(provider);
+  let promise = new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
+
+  // isTopSiteGivenProvider() and getProviderLinks() should still return results
+  // even when cache is empty or being populated.
+  do_check_false(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider));
+  do_check_links(RemoteNewTabUtils.getProviderLinks(provider), []);
+
+  yield promise;
+
+  // Once the cache is populated, we get the expected results
+  do_check_true(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider));
+  do_check_links(RemoteNewTabUtils.getProviderLinks(provider), expectedLinks);
+  RemoteNewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function* notifyLinkDelete() {
+  let expectedLinks = makeLinks(0, 3, 1);
+
+  let provider = new TestProvider(done => done(expectedLinks));
+  provider.maxNumLinks = expectedLinks.length;
+
+  RemoteNewTabUtils.initWithoutProviders();
+  RemoteNewTabUtils.links.addProvider(provider);
+  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
+
+  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
+
+  // Remove a link.
+  let removedLink = expectedLinks[2];
+  provider.notifyLinkChanged(removedLink, 2, true);
+  let links = RemoteNewTabUtils.links._providers.get(provider);
+
+  // Check that sortedLinks is correctly updated.
+  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks.slice(0, 2));
+
+  // Check that linkMap is accurately updated.
+  do_check_eq(links.linkMap.size, 2);
+  do_check_true(links.linkMap.get(expectedLinks[0].url));
+  do_check_true(links.linkMap.get(expectedLinks[1].url));
+  do_check_false(links.linkMap.get(removedLink.url));
+
+  // Check that siteMap is correctly updated.
+  do_check_eq(links.siteMap.size, 2);
+  do_check_true(links.siteMap.has(RemoteNewTabUtils.extractSite(expectedLinks[0].url)));
+  do_check_true(links.siteMap.has(RemoteNewTabUtils.extractSite(expectedLinks[1].url)));
+  do_check_false(links.siteMap.has(RemoteNewTabUtils.extractSite(removedLink.url)));
+
+  RemoteNewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function populatePromise() {
+  let count = 0;
+  let expectedLinks = makeLinks(0, 10, 2);
+
+  let getLinksFcn = Task.async(function* (callback) {
+    //Should not be calling getLinksFcn twice
+    count++;
+    do_check_eq(count, 1);
+    yield Promise.resolve();
+    callback(expectedLinks);
+  });
+
+  let provider = new TestProvider(getLinksFcn);
+
+  RemoteNewTabUtils.initWithoutProviders();
+  RemoteNewTabUtils.links.addProvider(provider);
+
+  RemoteNewTabUtils.links.populateProviderCache(provider, () => {});
+  RemoteNewTabUtils.links.populateProviderCache(provider, () => {
+    do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
+    RemoteNewTabUtils.links.removeProvider(provider);
+  });
+});
+
+add_task(function* isTopSiteGivenProvider() {
+  let expectedLinks = makeLinks(0, 10, 2);
+
+  // The lowest 2 frecencies have the same base domain.
+  expectedLinks[expectedLinks.length - 2].url = expectedLinks[expectedLinks.length - 1].url + "Test";
+
+  let provider = new TestProvider(done => done(expectedLinks));
+  provider.maxNumLinks = expectedLinks.length;
+
+  RemoteNewTabUtils.initWithoutProviders();
+  RemoteNewTabUtils.links.addProvider(provider);
+  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
+
+  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), true);
+  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider), false);
+
+  // Push out frecency 2 because the maxNumLinks is reached when adding frecency 3
+  let newLink = makeLink(3);
+  provider.notifyLinkChanged(newLink);
+
+  // There is still a frecent url with example2 domain, so it's still frecent.
+  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example3.com", provider), true);
+  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), true);
+
+  // Push out frecency 3
+  newLink = makeLink(5);
+  provider.notifyLinkChanged(newLink);
+
+  // Push out frecency 4
+  newLink = makeLink(9);
+  provider.notifyLinkChanged(newLink);
+
+  // Our count reached 0 for the example2.com domain so it's no longer a frecent site.
+  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example5.com", provider), true);
+  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), false);
+
+  RemoteNewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function* multipleProviders() {
+  // Make each provider generate RemoteNewTabUtils.links.maxNumLinks links to check
+  // that no more than maxNumLinks are actually returned in the merged list.
+  let evenLinks = makeLinks(0, 2 * RemoteNewTabUtils.links.maxNumLinks, 2);
+  let evenProvider = new TestProvider(done => done(evenLinks));
+  let oddLinks = makeLinks(0, 2 * RemoteNewTabUtils.links.maxNumLinks - 1, 2);
+  let oddProvider = new TestProvider(done => done(oddLinks));
+
+  RemoteNewTabUtils.initWithoutProviders();
+  RemoteNewTabUtils.links.addProvider(evenProvider);
+  RemoteNewTabUtils.links.addProvider(oddProvider);
+
+  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
+
+  let links = RemoteNewTabUtils.links.getLinks();
+  let expectedLinks = makeLinks(RemoteNewTabUtils.links.maxNumLinks,
+                                2 * RemoteNewTabUtils.links.maxNumLinks,
+                                1);
+  do_check_eq(links.length, RemoteNewTabUtils.links.maxNumLinks);
+  do_check_links(links, expectedLinks);
+
+  RemoteNewTabUtils.links.removeProvider(evenProvider);
+  RemoteNewTabUtils.links.removeProvider(oddProvider);
+});
+
+add_task(function* changeLinks() {
+  let expectedLinks = makeLinks(0, 20, 2);
+  let provider = new TestProvider(done => done(expectedLinks));
+
+  RemoteNewTabUtils.initWithoutProviders();
+  RemoteNewTabUtils.links.addProvider(provider);
+
+  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
+
+  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
+
+  // Notify of a new link.
+  let newLink = makeLink(19);
+  expectedLinks.splice(1, 0, newLink);
+  provider.notifyLinkChanged(newLink);
+  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
+
+  // Notify of a link that's changed sort criteria.
+  newLink.frecency = 17;
+  expectedLinks.splice(1, 1);
+  expectedLinks.splice(2, 0, newLink);
+  provider.notifyLinkChanged({
+    url: newLink.url,
+    frecency: 17,
+  });
+  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
+
+  // Notify of a link that's changed title.
+  newLink.title = "My frecency is now 17";
+  provider.notifyLinkChanged({
+    url: newLink.url,
+    title: newLink.title,
+  });
+  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
+
+  // Notify of a new link again, but this time make it overflow maxNumLinks.
+  provider.maxNumLinks = expectedLinks.length;
+  newLink = makeLink(21);
+  expectedLinks.unshift(newLink);
+  expectedLinks.pop();
+  do_check_eq(expectedLinks.length, provider.maxNumLinks); // Sanity check.
+  provider.notifyLinkChanged(newLink);
+  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
+
+  // Notify of many links changed.
+  expectedLinks = makeLinks(0, 3, 1);
+  provider.notifyManyLinksChanged();
+
+  // Since _populateProviderCache() is async, we must wait until the provider's
+  // populate promise has been resolved.
+  yield RemoteNewTabUtils.links._providers.get(provider).populatePromise;
+
+  // RemoteNewTabUtils.links will now repopulate its cache
+  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
+
+  RemoteNewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function* oneProviderAlreadyCached() {
+  let links1 = makeLinks(0, 10, 1);
+  let provider1 = new TestProvider(done => done(links1));
+
+  RemoteNewTabUtils.initWithoutProviders();
+  RemoteNewTabUtils.links.addProvider(provider1);
+
+  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
+  do_check_links(RemoteNewTabUtils.links.getLinks(), links1);
+
+  let links2 = makeLinks(10, 20, 1);
+  let provider2 = new TestProvider(done => done(links2));
+  RemoteNewTabUtils.links.addProvider(provider2);
+
+  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
+  do_check_links(RemoteNewTabUtils.links.getLinks(), links2.concat(links1));
+
+  RemoteNewTabUtils.links.removeProvider(provider1);
+  RemoteNewTabUtils.links.removeProvider(provider2);
+});
+
+add_task(function* newLowRankedLink() {
+  // Init a provider with 10 links and make its maximum number also 10.
+  let links = makeLinks(0, 10, 1);
+  let provider = new TestProvider(done => done(links));
+  provider.maxNumLinks = links.length;
+
+  RemoteNewTabUtils.initWithoutProviders();
+  RemoteNewTabUtils.links.addProvider(provider);
+
+  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
+  do_check_links(RemoteNewTabUtils.links.getLinks(), links);
+
+  // Notify of a new link that's low-ranked enough not to make the list.
+  let newLink = makeLink(0);
+  provider.notifyLinkChanged(newLink);
+  do_check_links(RemoteNewTabUtils.links.getLinks(), links);
+
+  // Notify about the new link's title change.
+  provider.notifyLinkChanged({
+    url: newLink.url,
+    title: "a new title",
+  });
+  do_check_links(RemoteNewTabUtils.links.getLinks(), links);
+
+  RemoteNewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function extractSite() {
+  // All these should extract to the same site
+  [ "mozilla.org",
+    "m.mozilla.org",
+    "mobile.mozilla.org",
+    "www.mozilla.org",
+    "www3.mozilla.org",
+  ].forEach(host => {
+    let url = "http://" + host;
+    do_check_eq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted same " + host);
+  });
+
+  // All these should extract to the same subdomain
+  [ "bugzilla.mozilla.org",
+    "www.bugzilla.mozilla.org",
+  ].forEach(host => {
+    let url = "http://" + host;
+    do_check_eq(RemoteNewTabUtils.extractSite(url), "bugzilla.mozilla.org", "extracted eTLD+2 " + host);
+  });
+
+  // All these should not extract to the same site
+  [ "bugzilla.mozilla.org",
+    "bug123.bugzilla.mozilla.org",
+    "too.many.levels.bugzilla.mozilla.org",
+    "m2.mozilla.org",
+    "mobile30.mozilla.org",
+    "ww.mozilla.org",
+    "ww2.mozilla.org",
+    "wwwww.mozilla.org",
+    "wwwww50.mozilla.org",
+    "wwws.mozilla.org",
+    "secure.mozilla.org",
+    "secure10.mozilla.org",
+    "many.levels.deep.mozilla.org",
+    "just.check.in",
+    "192.168.0.1",
+    "localhost",
+  ].forEach(host => {
+    let url = "http://" + host;
+    do_check_neq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted diff " + host);
+  });
+
+  // All these should not extract to the same site
+  [ "about:blank",
+    "file:///Users/user/file",
+    "chrome://browser/something",
+    "ftp://ftp.mozilla.org/",
+  ].forEach(url => {
+    do_check_neq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted diff url " + url);
+  });
+});
+
+function TestProvider(getLinksFn) {
+  this.getLinks = getLinksFn;
+  this._observers = new Set();
+}
+
+TestProvider.prototype = {
+  addObserver: function (observer) {
+    this._observers.add(observer);
+  },
+  notifyLinkChanged: function (link, index=-1, deleted=false) {
+    this._notifyObservers("onLinkChanged", link, index, deleted);
+  },
+  notifyManyLinksChanged: function () {
+    this._notifyObservers("onManyLinksChanged");
+  },
+  _notifyObservers: function () {
+    let observerMethodName = arguments[0];
+    let args = Array.prototype.slice.call(arguments, 1);
+    args.unshift(this);
+    for (let obs of this._observers) {
+      if (obs[observerMethodName])
+        obs[observerMethodName].apply(RemoteNewTabUtils.links, args);
+    }
+  },
+};
+
+function do_check_links(actualLinks, expectedLinks) {
+  do_check_true(Array.isArray(actualLinks));
+  do_check_eq(actualLinks.length, expectedLinks.length);
+  for (let i = 0; i < expectedLinks.length; i++) {
+    let expected = expectedLinks[i];
+    let actual = actualLinks[i];
+    do_check_eq(actual.url, expected.url);
+    do_check_eq(actual.title, expected.title);
+    do_check_eq(actual.frecency, expected.frecency);
+    do_check_eq(actual.lastVisitDate, expected.lastVisitDate);
+  }
+}
+
+function makeLinks(frecRangeStart, frecRangeEnd, step) {
+  let links = [];
+  // Remember, links are ordered by frecency descending.
+  for (let i = frecRangeEnd; i > frecRangeStart; i -= step) {
+    links.push(makeLink(i));
+  }
+  return links;
+}
+
+function makeLink(frecency) {
+  return {
+    url: "http://example" + frecency + ".com/",
+    title: "My frecency is " + frecency,
+    frecency: frecency,
+    lastVisitDate: 0,
+  };
+}
--- a/browser/components/newtab/tests/xpcshell/xpcshell.ini
+++ b/browser/components/newtab/tests/xpcshell/xpcshell.ini
@@ -1,9 +1,12 @@
 [DEFAULT]
 head =
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_AboutNewTabService.js]
 [test_NewTabPrefsProvider.js]
+[test_NewTabURL.js]
 [test_PlacesProvider.js]
+[test_RemoteNewTabLocation.js]
+[test_RemoteNewTabUtils.js]
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -9,30 +9,41 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const POLARIS_ENABLED = "browser.polaris.enabled";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+                                  "resource://gre/modules/AppConstants.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
                                   "resource:///modules/AboutHome.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AboutNewTab",
                                   "resource:///modules/AboutNewTab.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DirectoryLinksProvider",
                                   "resource:///modules/DirectoryLinksProvider.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
                                   "resource://gre/modules/NewTabUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
-                                  "resource:///modules/NewTabPrefsProvider.jsm");
+if(!AppConstants.RELEASE_BUILD) {
+  XPCOMUtils.defineLazyModuleGetter(this, "RemoteAboutNewTab",
+                                    "resource:///modules/RemoteAboutNewTab.jsm");
+
+  XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils",
+                                    "resource:///modules/RemoteNewTabUtils.jsm");
+
+  XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+                                    "resource:///modules/NewTabPrefsProvider.jsm");
+}
 
 XPCOMUtils.defineLazyModuleGetter(this, "UITour",
                                   "resource:///modules/UITour.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentClick",
@@ -173,25 +184,24 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/AddonWatcher.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
                                   "resource://gre/modules/ExtensionManagement.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
-                                  "resource://gre/modules/AppConstants.jsm");
-
 XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils",
                                    "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
 
 XPCOMUtils.defineLazyServiceGetter(this, "AlertsService",
                                    "@mozilla.org/alerts-service;1", "nsIAlertsService");
 
+const ABOUT_NEWTAB = "about:newtab";
+
 const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
 const PREF_PLUGINS_UPDATEURL  = "plugins.update.url";
 
 // Seconds of idle before trying to create a bookmarks backup.
 const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 8 * 60;
 // Minimum interval between backups.  We try to not create more than one backup
 // per interval.
 const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
@@ -816,17 +826,22 @@ BrowserGlue.prototype = {
     webrtcUI.init();
     AboutHome.init();
 
     DirectoryLinksProvider.init();
     NewTabUtils.init();
     NewTabUtils.links.addProvider(DirectoryLinksProvider);
     AboutNewTab.init();
 
-    NewTabPrefsProvider.prefs.init();
+    if(!AppConstants.RELEASE_BUILD) {
+      RemoteNewTabUtils.init();
+      RemoteNewTabUtils.links.addProvider(DirectoryLinksProvider);
+      RemoteAboutNewTab.init();
+      NewTabPrefsProvider.prefs.init();
+    }
 
     SessionStore.init();
     BrowserUITelemetry.init();
     ContentSearch.init();
     FormValidationHandler.init();
 
     ContentClick.init();
     RemotePrompt.init();
@@ -1137,17 +1152,20 @@ BrowserGlue.prototype = {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
     SelfSupportBackend.uninit();
 
     CustomizationTabPreloader.uninit();
     WebappManager.uninit();
 
-    NewTabPrefsProvider.prefs.uninit();
+    if (!AppConstants.RELEASE_BUILD) {
+      RemoteAboutNewTab.uninit();
+      NewTabPrefsProvider.prefs.uninit();
+    }
     AboutNewTab.uninit();
 #ifdef NIGHTLY_BUILD
     if (Services.prefs.getBoolPref("dom.identity.enabled")) {
       SignInToWebsiteUX.uninit();
     }
 #endif
     webrtcUI.uninit();
     FormValidationHandler.uninit();
@@ -2507,16 +2525,60 @@ BrowserGlue.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference,
                                          Ci.nsIBrowserGlue]),
 
   // redefine the default factory for XPCOMUtils
   _xpcom_factory: BrowserGlueServiceFactory,
 }
 
+// ------------------------------------
+// nsIAboutNewTabService implementation
+//-------------------------------------
+
+function AboutNewTabService()
+{
+  this._newTabURL = ABOUT_NEWTAB;
+  this._overridden = false;
+}
+
+AboutNewTabService.prototype = {
+  classID: Components.ID("{97eea4bb-db50-4ae0-9147-1e5ed55b4ed5}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutNewTabService]),
+
+  get newTabURL() {
+
+    if (!AppConstants.RELEASE_BUILD && Services.prefs.getBoolPref("browser.newtabpage.remote")) {
+      return "about:remote-newtab";
+    }
+
+    return this._newTabURL;
+  },
+
+  set newTabURL(aNewTabURL) {
+    if (aNewTabURL === ABOUT_NEWTAB) {
+      this.resetNewTabURL();
+      return;
+    }
+    this._newTabURL = aNewTabURL;
+    this._overridden = true;
+    Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
+  },
+
+  get overridden() {
+    return this._overridden;
+  },
+
+  resetNewTabURL: function() {
+    this._newTabURL = ABOUT_NEWTAB;
+    this._overridden = false;
+    Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
+   }
+};
+
 function ContentPermissionPrompt() {}
 
 ContentPermissionPrompt.prototype = {
   classID:          Components.ID("{d8903bf6-68d5-4e97-bcd1-e4d3012f721a}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
 
   _getBrowserForRequest: function (aRequest) {
@@ -3321,17 +3383,17 @@ var E10SAccessibilityCheck = {
 
     notification =
       win.PopupNotifications.show(browser, "a11y_enabled_with_e10s",
                                   promptMessage, null, mainAction,
                                   secondaryActions, options);
   },
 };
 
-var components = [BrowserGlue, ContentPermissionPrompt];
+var components = [BrowserGlue, ContentPermissionPrompt, AboutNewTabService];
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
 
 
 // Listen for UITour messages.
 // Do it here instead of the UITour module itself so that the UITour module is lazy loaded
 // when the first message is received.
 var globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
 globalMM.addMessageListener("UITour:onPageEvent", function(aMessage) {
new file mode 100644
--- /dev/null
+++ b/browser/components/nsIAboutNewTabService.idl
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Allows to override about:newtab to point to a different location
+ * than the one specified within AboutRedirector.cpp
+ */
+
+[scriptable, uuid(6c66f022-beb1-46ea-8af6-c6c6dd937ea9)]
+interface nsIAboutNewTabService : nsISupports
+{
+  /**
+   * Returns "about:newtab" if not overridden, otherwise
+   * a string represenation of the new URL.
+   */
+  attribute ACString newTabURL;
+
+  /**
+   * Returns true if the default of "about:newtab" got
+   * overridden.
+   */
+  readonly attribute bool overridden;
+
+  /**
+   * Resets "about:newtab" to the default and also resets the
+   * overridden attribute to false.
+   */
+  void resetNewTabURL();
+};
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -383,19 +383,16 @@
 @RESPATH@/browser/components/nsSetDefaultBrowser.manifest
 @RESPATH@/browser/components/nsSetDefaultBrowser.js
 @RESPATH@/browser/components/devtools-clhandler.manifest
 @RESPATH@/browser/components/devtools-clhandler.js
 @RESPATH@/browser/components/webideCli.js
 @RESPATH@/browser/components/webideComponents.manifest
 @RESPATH@/browser/components/Experiments.manifest
 @RESPATH@/browser/components/ExperimentsService.js
-@RESPATH@/browser/components/browser-newtab.xpt
-@RESPATH@/browser/components/aboutNewTabService.js
-@RESPATH@/browser/components/NewTabComponents.manifest
 @RESPATH@/components/Downloads.manifest
 @RESPATH@/components/DownloadLegacy.js
 @RESPATH@/components/BrowserPageThumbs.manifest
 @RESPATH@/components/crashmonitor.manifest
 @RESPATH@/components/nsCrashMonitor.js
 @RESPATH@/components/SiteSpecificUserAgent.js
 @RESPATH@/components/SiteSpecificUserAgent.manifest
 @RESPATH@/components/toolkitsearch.manifest
--- a/browser/modules/DirectoryLinksProvider.jsm
+++ b/browser/modules/DirectoryLinksProvider.jsm
@@ -12,30 +12,38 @@ const Cu = Components.utils;
 const ParserUtils =  Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
 
 Cu.importGlobalProperties(["XMLHttpRequest"]);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm")
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
   "resource://gre/modules/NewTabUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
   "resource://gre/modules/UpdateUtils.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "eTLD",
   "@mozilla.org/network/effective-tld-service;1",
   "nsIEffectiveTLDService");
+
+// ensure remote new tab doesn't go beyond aurora
+if (!AppConstants.RELEASE_BUILD) {
+  XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils",
+    "resource:///modules/RemoteNewTabUtils.jsm");
+}
+
 XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => {
   return new TextDecoder();
 });
 XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function () {
   return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
 });
 XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
@@ -754,17 +762,23 @@ var DirectoryLinksProvider = {
     // setup frequency cap file path
     this._frequencyCapFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, FREQUENCY_CAP_FILE);
     // setup inadjacent sites URL
     this._inadjacentSitesUrl = INADJACENCY_SOURCE;
 
     NewTabUtils.placesProvider.addObserver(this);
     NewTabUtils.links.addObserver(this);
 
-    return Task.spawn(function() {
+    // ensure remote new tab doesn't go beyond aurora
+    if (!AppConstants.RELEASE_BUILD) {
+      RemoteNewTabUtils.placesProvider.addObserver(this);
+      RemoteNewTabUtils.links.addObserver(this);
+    }
+
+    return Task.spawn(function*() {
       // get the last modified time of the links file if it exists
       let doesFileExists = yield OS.File.exists(this._directoryFilePath);
       if (doesFileExists) {
         let fileInfo = yield OS.File.stat(this._directoryFilePath);
         this._lastDownloadMS = Date.parse(fileInfo.lastModificationDate);
       }
       // read frequency cap file
       yield this._readFrequencyCapFile();
--- a/mobile/android/b2gdroid/installer/package-manifest.in
+++ b/mobile/android/b2gdroid/installer/package-manifest.in
@@ -115,17 +115,16 @@
 #endif
 @BINPATH@/components/appshell.xpt
 @BINPATH@/components/appstartup.xpt
 @BINPATH@/components/autocomplete.xpt
 @BINPATH@/components/autoconfig.xpt
 @BINPATH@/components/browsercompsbase.xpt
 @BINPATH@/components/browser-element.xpt
 @BINPATH@/components/browser-feeds.xpt
-@BINPATH@/components/browser-newtab.xpt
 @BINPATH@/components/caps.xpt
 @BINPATH@/components/chardet.xpt
 @BINPATH@/components/chrome.xpt
 @BINPATH@/components/commandhandler.xpt
 @BINPATH@/components/commandlines.xpt
 @BINPATH@/components/composer.xpt
 @BINPATH@/components/content_events.xpt
 @BINPATH@/components/content_geckomediaplugins.xpt