diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 63b5c1138788..f05671b7507b 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1348,8 +1348,10 @@ pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/
// 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);
@@ -1636,4 +1638,4 @@ 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
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index b4ed41d3e8db..cb60461a04c8 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -11,7 +11,6 @@ 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",
@@ -56,9 +55,6 @@ 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.
@@ -2365,11 +2361,8 @@ function URLBarSetURI(aURI) {
} 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);
@@ -3566,9 +3559,8 @@ const BrowserSearch = {
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");
diff --git a/browser/base/content/remote-newtab/newTab.css b/browser/base/content/remote-newtab/newTab.css
new file mode 100644
index 000000000000..303cafddc5e2
--- /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;
+}
diff --git a/browser/base/content/remote-newtab/newTab.js b/browser/base/content/remote-newtab/newTab.js
new file mode 100644
index 000000000000..920830c96767
--- /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");
+}());
diff --git a/browser/base/content/remote-newtab/newTab.xhtml b/browser/base/content/remote-newtab/newTab.xhtml
new file mode 100644
index 000000000000..1ae754eedbfd
--- /dev/null
+++ b/browser/base/content/remote-newtab/newTab.xhtml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ %newTabDTD;
+]>
+
+
+
+ &newtab.pageTitle;
+
+
+
+
+
+
+
+
diff --git a/browser/base/content/test/general/browser_bug763468_perwindowpb.js b/browser/base/content/test/general/browser_bug763468_perwindowpb.js
index f4124d9ef3ae..b5f1c2bb698b 100644
--- a/browser/base/content/test/general/browser_bug763468_perwindowpb.js
+++ b/browser/base/content/test/general/browser_bug763468_perwindowpb.js
@@ -1,40 +1,36 @@
/* 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) {
@@ -44,9 +40,9 @@ function test() {
// 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();
diff --git a/browser/base/content/test/general/browser_bug767836_perwindowpb.js b/browser/base/content/test/general/browser_bug767836_perwindowpb.js
index 90215806ffa0..d49660e2aa7a 100644
--- a/browser/base/content/test/general/browser_bug767836_perwindowpb.js
+++ b/browser/base/content/test/general/browser_bug767836_perwindowpb.js
@@ -1,30 +1,24 @@
/* 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
@@ -35,18 +29,18 @@ function test() {
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()
});
});
}
@@ -78,7 +72,7 @@ function openNewTab(aWindow, aCallback) {
aWindow.BrowserOpenTab();
let browser = aWindow.gBrowser.selectedBrowser;
- if (browser.contentDocument.readyState === "complete") {
+ if (browser.contentDocument.readyState == "complete") {
executeSoon(aCallback);
return;
}
diff --git a/browser/base/content/test/newtab/browser_newtab_external_resource.js b/browser/base/content/test/newtab/browser_newtab_external_resource.js
index 0d4a625bcc92..295b08646b15 100644
--- a/browser/base/content/test/newtab/browser_newtab_external_resource.js
+++ b/browser/base/content/test/newtab/browser_newtab_external_resource.js
@@ -9,19 +9,13 @@
* 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;
@@ -40,7 +34,7 @@ function testPref() {
// 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
diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js
index 4a5268b1db11..936dc4caed92 100644
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -20,7 +20,7 @@ this.__defineGetter__("BROWSER_NEW_TAB_URL", () => {
!aboutNewTabService.overridden) {
return "about:privatebrowsing";
}
- return "about:newtab";
+ return aboutNewTabService.newTabURL;
});
var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
diff --git a/browser/base/jar.mn b/browser/base/jar.mn
index 5fedd203ddb6..862f6da9b2eb 100644
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -138,6 +138,9 @@ browser.jar:
* 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)
diff --git a/browser/components/BrowserComponents.manifest b/browser/components/BrowserComponents.manifest
index 433a1be2983c..0fffa476e23a 100644
--- a/browser/components/BrowserComponents.manifest
+++ b/browser/components/BrowserComponents.manifest
@@ -46,3 +46,5 @@ 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}
diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp
index d9cd075603f2..78f55c9b64c1 100644
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -86,9 +86,13 @@ static RedirEntry kRedirMap[] = {
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",
@@ -173,12 +177,18 @@ AboutRedirector::NewChannel(nsIURI* aURI,
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 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()) {
diff --git a/browser/components/moz.build b/browser/components/moz.build
index debdfe93fde0..431d505a040b 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -29,6 +29,7 @@ DIRS += [
DIRS += ['build']
XPIDL_SOURCES += [
+ 'nsIAboutNewTabService.idl',
'nsIBrowserGlue.idl',
'nsIBrowserHandler.idl',
]
diff --git a/browser/components/newtab/NewTabComponents.manifest b/browser/components/newtab/NewTabComponents.manifest
deleted file mode 100644
index c2f329ac163e..000000000000
--- 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}
diff --git a/browser/components/newtab/NewTabPrefsProvider.jsm b/browser/components/newtab/NewTabPrefsProvider.jsm
index ab1b8e72e9b9..86ce80033a6d 100644
--- a/browser/components/newtab/NewTabPrefsProvider.jsm
+++ b/browser/components/newtab/NewTabPrefsProvider.jsm
@@ -17,7 +17,6 @@ XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
// 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"],
diff --git a/browser/components/newtab/NewTabURL.jsm b/browser/components/newtab/NewTabURL.jsm
new file mode 100644
index 000000000000..e75873955c50
--- /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();
+ }
+};
diff --git a/browser/components/newtab/RemoteAboutNewTab.jsm b/browser/components/newtab/RemoteAboutNewTab.jsm
new file mode 100644
index 000000000000..4ef509633eaf
--- /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;
+ },
+};
diff --git a/browser/components/newtab/RemoteNewTabLocation.jsm b/browser/components/newtab/RemoteNewTabLocation.jsm
new file mode 100644
index 000000000000..1c958a275f83
--- /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);
+ }
+};
diff --git a/browser/components/newtab/RemoteNewTabUtils.jsm b/browser/components/newtab/RemoteNewTabUtils.jsm
new file mode 100644
index 000000000000..8baa63c3341d
--- /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
+};
diff --git a/browser/components/newtab/aboutNewTabService.js b/browser/components/newtab/aboutNewTabService.js
deleted file mode 100644
index b96ab0d56416..000000000000
--- 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]);
diff --git a/browser/components/newtab/moz.build b/browser/components/newtab/moz.build
index dae55d1ac537..d032271c84f2 100644
--- a/browser/components/newtab/moz.build
+++ b/browser/components/newtab/moz.build
@@ -4,24 +4,18 @@
# 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',
-]
+ XPCSHELL_TESTS_MANIFESTS += [
+ 'tests/xpcshell/xpcshell.ini',
+ ]
-EXTRA_JS_MODULES += [
- 'NewTabPrefsProvider.jsm',
- 'PlacesProvider.jsm',
-]
-
-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',
+ ]
diff --git a/browser/components/newtab/nsIAboutNewTabService.idl b/browser/components/newtab/nsIAboutNewTabService.idl
deleted file mode 100644
index 5bfc9ab34db7..000000000000
--- 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();
-};
diff --git a/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js b/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js
index 1b397b3afe64..153e7fdbb182 100644
--- 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);
- });
-}
diff --git a/browser/components/newtab/tests/browser/dummy_page.html b/browser/components/newtab/tests/browser/dummy_page.html
index 4b0689bde7ed..664d33738670 100644
--- a/browser/components/newtab/tests/browser/dummy_page.html
+++ b/browser/components/newtab/tests/browser/dummy_page.html
@@ -6,5 +6,17 @@