2014-03-27 08:03:42 +00:00
|
|
|
/* 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 = ["DirectoryLinksProvider"];
|
|
|
|
|
|
|
|
const Ci = Components.interfaces;
|
|
|
|
const Cc = Components.classes;
|
|
|
|
const Cu = Components.utils;
|
2014-05-09 15:24:30 +00:00
|
|
|
const XMLHttpRequest =
|
|
|
|
Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1", "nsIXMLHttpRequest");
|
2014-03-27 08:03:42 +00:00
|
|
|
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
2014-05-27 20:59:33 +00:00
|
|
|
Cu.import("resource://gre/modules/Task.jsm");
|
2015-03-13 15:45:31 +00:00
|
|
|
Cu.import("resource://gre/modules/Timer.jsm");
|
2014-03-27 08:03:42 +00:00
|
|
|
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
|
|
|
"resource://gre/modules/NetUtil.jsm");
|
2014-08-29 20:35:59 +00:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
|
|
|
|
"resource://gre/modules/NewTabUtils.jsm");
|
2014-05-09 15:24:30 +00:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
|
|
|
"resource://gre/modules/osfile.jsm")
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
|
|
|
"resource://gre/modules/Promise.jsm");
|
2015-04-01 21:30:28 +00:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
|
|
|
|
"resource://gre/modules/UpdateChannel.jsm");
|
2014-05-27 20:59:34 +00:00
|
|
|
XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => {
|
|
|
|
return new TextDecoder();
|
|
|
|
});
|
2014-03-27 08:03:42 +00:00
|
|
|
|
2014-05-09 15:24:30 +00:00
|
|
|
// The filename where directory links are stored locally
|
|
|
|
const DIRECTORY_LINKS_FILE = "directoryLinks.json";
|
2014-07-23 21:33:13 +00:00
|
|
|
const DIRECTORY_LINKS_TYPE = "application/json";
|
2014-03-27 08:03:42 +00:00
|
|
|
|
|
|
|
// 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";
|
|
|
|
|
|
|
|
// The preference that tells where to obtain directory links
|
2014-05-27 20:59:33 +00:00
|
|
|
const PREF_DIRECTORY_SOURCE = "browser.newtabpage.directory.source";
|
2014-03-27 08:03:42 +00:00
|
|
|
|
2014-08-08 23:40:40 +00:00
|
|
|
// The preference that tells where to send click/view pings
|
|
|
|
const PREF_DIRECTORY_PING = "browser.newtabpage.directory.ping";
|
2014-06-10 05:03:23 +00:00
|
|
|
|
2014-08-08 23:40:40 +00:00
|
|
|
// The preference that tells if newtab is enhanced
|
|
|
|
const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced";
|
2014-06-10 05:03:23 +00:00
|
|
|
|
2014-10-24 16:33:03 +00:00
|
|
|
// Only allow link urls that are http(s)
|
|
|
|
const ALLOWED_LINK_SCHEMES = new Set(["http", "https"]);
|
|
|
|
|
|
|
|
// Only allow link image urls that are https or data
|
|
|
|
const ALLOWED_IMAGE_SCHEMES = new Set(["https", "data"]);
|
|
|
|
|
2014-03-27 08:03:42 +00:00
|
|
|
// The frecency of a directory link
|
|
|
|
const DIRECTORY_FRECENCY = 1000;
|
|
|
|
|
2015-03-26 21:23:21 +00:00
|
|
|
// The frecency of a suggested link
|
|
|
|
const SUGGESTED_FRECENCY = Infinity;
|
2015-03-13 15:45:34 +00:00
|
|
|
|
2015-03-22 07:46:26 +00:00
|
|
|
// Default number of times to show a link
|
|
|
|
const DEFAULT_FREQUENCY_CAP = 5;
|
|
|
|
|
2014-08-08 23:40:40 +00:00
|
|
|
// Divide frecency by this amount for pings
|
|
|
|
const PING_SCORE_DIVISOR = 10000;
|
|
|
|
|
|
|
|
// Allowed ping actions remotely stored as columns: case-insensitive [a-z0-9_]
|
|
|
|
const PING_ACTIONS = ["block", "click", "pin", "sponsored", "sponsored_link", "unpin", "view"];
|
2014-03-31 08:51:22 +00:00
|
|
|
|
2014-03-27 08:03:42 +00:00
|
|
|
/**
|
|
|
|
* Singleton that serves as the provider of directory links.
|
|
|
|
* Directory links are a hard-coded set of links shown if a user's link
|
|
|
|
* inventory is empty.
|
|
|
|
*/
|
|
|
|
let DirectoryLinksProvider = {
|
|
|
|
|
|
|
|
__linksURL: null,
|
|
|
|
|
2014-05-27 20:59:33 +00:00
|
|
|
_observers: new Set(),
|
|
|
|
|
|
|
|
// links download deferred, resolved upon download completion
|
|
|
|
_downloadDeferred: null,
|
|
|
|
|
|
|
|
// download default interval is 24 hours in milliseconds
|
|
|
|
_downloadIntervalMS: 86400000,
|
2014-03-27 08:03:42 +00:00
|
|
|
|
2014-07-23 18:02:49 +00:00
|
|
|
/**
|
|
|
|
* A mapping from eTLD+1 to an enhanced link objects
|
|
|
|
*/
|
|
|
|
_enhancedLinks: new Map(),
|
|
|
|
|
2015-03-22 07:46:26 +00:00
|
|
|
/**
|
|
|
|
* A mapping from site to remaining number of views
|
|
|
|
*/
|
|
|
|
_frequencyCaps: new Map(),
|
|
|
|
|
2015-03-10 21:08:30 +00:00
|
|
|
/**
|
2015-03-26 21:23:21 +00:00
|
|
|
* A mapping from site to a list of suggested link objects
|
2015-03-10 21:08:30 +00:00
|
|
|
*/
|
2015-03-26 21:23:21 +00:00
|
|
|
_suggestedLinks: new Map(),
|
2015-03-10 21:08:30 +00:00
|
|
|
|
2015-03-13 15:45:31 +00:00
|
|
|
/**
|
2015-03-26 21:23:21 +00:00
|
|
|
* A set of top sites that we can provide suggested links for
|
2015-03-13 15:45:31 +00:00
|
|
|
*/
|
2015-03-26 21:23:21 +00:00
|
|
|
_topSitesWithSuggestedLinks: new Set(),
|
2015-03-13 15:45:31 +00:00
|
|
|
|
2014-05-09 15:24:30 +00:00
|
|
|
get _observedPrefs() Object.freeze({
|
2014-08-08 23:40:40 +00:00
|
|
|
enhanced: PREF_NEWTAB_ENHANCED,
|
2014-03-27 08:03:42 +00:00
|
|
|
linksURL: PREF_DIRECTORY_SOURCE,
|
|
|
|
matchOSLocale: PREF_MATCH_OS_LOCALE,
|
|
|
|
prefSelectedLocale: PREF_SELECTED_LOCALE,
|
|
|
|
}),
|
|
|
|
|
|
|
|
get _linksURL() {
|
|
|
|
if (!this.__linksURL) {
|
|
|
|
try {
|
2014-05-09 15:24:30 +00:00
|
|
|
this.__linksURL = Services.prefs.getCharPref(this._observedPrefs["linksURL"]);
|
2015-03-30 06:43:30 +00:00
|
|
|
|
|
|
|
// Temporarily override the default for en-US until new endpoint is live
|
|
|
|
if (this.locale == "en-US" && !Services.prefs.prefHasUserValue(this._observedPrefs["linksURL"])) {
|
|
|
|
this.__linksURL = "data:text/plain;base64,ewogICAgImRpcmVjdG9yeSI6IFsKICAgICAgICB7CiAgICAgICAgICAgICJiZ0NvbG9yIjogIiIsCiAgICAgICAgICAgICJkaXJlY3RvcnlJZCI6IDQ5OCwKICAgICAgICAgICAgImVuaGFuY2VkSW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy9kMTFiYTBiMzA5NWJiMTlkODA5MmNkMjliZTljYmI5ZTE5NzY3MWVhLjI4MDg4LnBuZyIsCiAgICAgICAgICAgICJpbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzLzEzMzJhNjhiYWRmMTFlM2Y3ZjY5YmY3MzY0ZTc5YzBhN2UyNzUzYmMuNTMxNi5wbmciLAogICAgICAgICAgICAidGl0bGUiOiAiTW96aWxsYSBDb21tdW5pdHkiLAogICAgICAgICAgICAidHlwZSI6ICJhZmZpbGlhdGUiLAogICAgICAgICAgICAidXJsIjogImh0dHA6Ly9jb250cmlidXRlLm1vemlsbGEub3JnLyIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgImJnQ29sb3IiOiAiIiwKICAgICAgICAgICAgImRpcmVjdG9yeUlkIjogNTAwLAogICAgICAgICAgICAiZW5oYW5jZWRJbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzL2NjNjM3NzRiN2E5YWFlMDJmZTM2YmM1Y2FmOTBjMWUyNWU2NmEyYmMuMTM3OTEucG5nIiwKICAgICAgICAgICAgImltYWdlVVJJIjogImh0dHBzOi8vZHRleDRrdmJwcG92dC5jbG91ZGZyb250Lm5ldC9pbWFnZXMvZTgyMmNkNDYyOGM1MTYyMzEzZjQ5ZjVkNDU1NmY4YWFmZGYzODc1MC4xMTUxMy5wbmciLAogICAgICAgICAgICAidGl0bGUiOiAiTW96aWxsYSBNYW5pZmVzdG8iLAogICAgICAgICAgICAidHlwZSI6ICJhZmZpbGlhdGUiLAogICAgICAgICAgICAidXJsIjogImh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2Fib3V0L21hbmlmZXN0by8iCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJiZ0NvbG9yIjogIiIsCiAgICAgICAgICAgICJkaXJlY3RvcnlJZCI6IDUwMiwKICAgICAgICAgICAgImVuaGFuY2VkSW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy80MGU1NjMwNDA1ZDUwMzFjYTczMzkzYmQ3YmMwMDY0MTU2ZjJjYzgyLjEwOTg0LnBuZyIsCiAgICAgICAgICAgICJpbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzLzQ5MGQ0MmQxZjlhNzZjMDc3Mzk2MjZkMWI4YTU2OTE2OWFlYzhmYmUuMTEwMzkucG5nIiwKICAgICAgICAgICAgInRpdGxlIjogIkN1c3RvbWl6ZSBGaXJlZm94IiwKICAgICAgICAgICAgInR5cGUiOiAiYWZmaWxpYXRlIiwKICAgICAgICAgICAgInVybCI6ICJodHRwOi8vZmFzdGVzdGZpcmVmb3guY29tL2ZpcmVmb3gvZGVza3RvcC9jdXN0b21pemUvIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAiYmdDb2xvciI6ICIiLAogICAgICAgICAgICAiZGlyZWN0b3J5SWQiOiA1MDQsCiAgICAgICAgICAgICJlbmhhbmNlZEltYWdlVVJJIjogImh0dHBzOi8vZHRleDRrdmJwcG92dC5jbG91ZGZyb250Lm5ldC9pbWFnZXMvODc3ZjFjNTYxZTczNWY3YjlmNDE5ZmY5YWM3OWViOGM3NDgxMTE5ZC4xNjc0NC5wbmciLAogICAgICAgICAgICAiaW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy8yNWM5ZmJiMDczMDhiODRkMTYwZmMxYjc5NTkzNjRhMmMxOGY5M2I5LjY0MDQucG5nIiwKICAgICAgICAgICAgInRpdGxlIjogIkZpcmVmb3ggTWFya2V0cGxhY2UiLAogICAgICAgICAgICAidHlwZSI6ICJhZmZpbGlhdGUiLAogICAgICAgICAgICAidXJsIjogImh0dHBzOi8vbWFya2V0cGxhY2UuZmlyZWZveC5jb20vIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAiYmdDb2xvciI6ICIjM2ZiNThlIiwKICAgICAgICAgICAgImRpcmVjdG9yeUlkIjogNTA1LAogICAgICAgICAgICAiZW5oYW5jZWRJbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzLzcyMDEyMWU3NDYyZDhjNzg2M2I0ZGQ4ZmE3YjVjMTA4OWI1ZjVmYjIuMzM4NjIucG5nIiwKICAgICAgICAgICAgImltYWdlVVJJIjogImh0dHBzOi8vZHRleDRrdmJwcG92dC5jbG91ZGZyb250Lm5ldC9pbWFnZXMvMGU2MDMxNjc1YTljNDkxZGQwYzY1ZTljNjdjZmJmNTRhNTg4MGYxNy4yMjk1LnN2ZyIsCiAgICAgICAgICAgICJ0aXRsZSI6ICJNb3ppbGxhIFdlYm1ha2VyIiwKICAgICAgICAgICAgInR5cGUiOiAiYWZmaWxpYXRlIiwKICAgICAgICAgICAgInVybCI6ICJodHRwczovL3dlYm1ha2VyLm9yZy8%2FdXRtX3NvdXJjZT1kaXJlY3RvcnktdGlsZXMmdXRtX21lZGl1bT1maXJlZm94LWJyb3dzZXIiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJiZ0NvbG9yIjogIiIsCiAgICAgICAgICAgICJkaXJlY3RvcnlJZCI6IDUwNiwKICAgICAgICAgICAgImVuaGFuY2VkSW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy9kOTcxY2JhZmEwMzA5YTIwMWU1MThhY2RhYzRmMWVlNGRhYmM3ZWFhLjE1MTA5LnBuZyIsCiAgICAgICAgICAgICJpbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzL2I0YWRjNThkZDNjMDJkYTM1NTEwNDk3N2I5MTAyNTUwNjBjZmQ2ZDguMTAzNTAucG5nIiwKICAgICAgICAgICAgInRpdGxlIjogIkZpcmVmb3ggU3luYyIsCiAgICAgICAgICAgICJ0eXBlIjogImFmZmlsaWF0ZSIsCiAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovL21vemlsbGEtZXVyb3BlLm9yZy9maXJlZm94L3N5bmMiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJiZ0NvbG9yIjogIiIsCiAgICAgICAgICAgICJkaXJlY3RvcnlJZCI6IDUwNywKICAgICAgICAgICAgImVuaGFuY2VkSW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy8yMmZiODU2Y2Q1ODM2NTg1NWViNzI1Y
|
|
|
|
}
|
2014-03-27 08:03:42 +00:00
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
Cu.reportError("Error fetching directory links url from prefs: " + e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return this.__linksURL;
|
|
|
|
},
|
|
|
|
|
2014-05-09 15:24:30 +00:00
|
|
|
/**
|
|
|
|
* Gets the currently selected locale for display.
|
|
|
|
* @return the selected locale or "en-US" if none is selected
|
|
|
|
*/
|
|
|
|
get locale() {
|
|
|
|
let matchOS;
|
|
|
|
try {
|
|
|
|
matchOS = Services.prefs.getBoolPref(PREF_MATCH_OS_LOCALE);
|
|
|
|
}
|
|
|
|
catch (e) {}
|
|
|
|
|
|
|
|
if (matchOS) {
|
|
|
|
return Services.locale.getLocaleComponentForUserAgent();
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE,
|
|
|
|
Ci.nsIPrefLocalizedString);
|
|
|
|
if (locale) {
|
|
|
|
return locale.data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (e) {}
|
|
|
|
|
|
|
|
try {
|
|
|
|
return Services.prefs.getCharPref(PREF_SELECTED_LOCALE);
|
|
|
|
}
|
|
|
|
catch (e) {}
|
|
|
|
|
|
|
|
return "en-US";
|
|
|
|
},
|
|
|
|
|
2014-08-08 23:40:40 +00:00
|
|
|
/**
|
|
|
|
* Set appropriate default ping behavior controlled by enhanced pref
|
|
|
|
*/
|
|
|
|
_setDefaultEnhanced: function DirectoryLinksProvider_setDefaultEnhanced() {
|
|
|
|
if (!Services.prefs.prefHasUserValue(PREF_NEWTAB_ENHANCED)) {
|
|
|
|
let enhanced = true;
|
|
|
|
try {
|
|
|
|
// Default to not enhanced if DNT is set to tell websites to not track
|
2014-10-22 17:31:14 +00:00
|
|
|
if (Services.prefs.getBoolPref("privacy.donottrackheader.enabled")) {
|
2014-08-08 23:40:40 +00:00
|
|
|
enhanced = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch(ex) {}
|
|
|
|
Services.prefs.setBoolPref(PREF_NEWTAB_ENHANCED, enhanced);
|
|
|
|
}
|
|
|
|
},
|
2014-03-31 08:51:22 +00:00
|
|
|
|
2014-03-27 08:03:42 +00:00
|
|
|
observe: function DirectoryLinksProvider_observe(aSubject, aTopic, aData) {
|
|
|
|
if (aTopic == "nsPref:changed") {
|
2014-08-08 23:40:40 +00:00
|
|
|
switch (aData) {
|
|
|
|
// Re-set the default in case the user clears the pref
|
|
|
|
case this._observedPrefs.enhanced:
|
|
|
|
this._setDefaultEnhanced();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case this._observedPrefs.linksURL:
|
|
|
|
delete this.__linksURL;
|
|
|
|
// fallthrough
|
|
|
|
|
|
|
|
// Force directory download on changes to fetch related prefs
|
|
|
|
case this._observedPrefs.matchOSLocale:
|
|
|
|
case this._observedPrefs.prefSelectedLocale:
|
|
|
|
this._fetchAndCacheLinksIfNecessary(true);
|
|
|
|
break;
|
2014-03-27 08:03:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_addPrefsObserver: function DirectoryLinksProvider_addObserver() {
|
2014-05-09 15:24:30 +00:00
|
|
|
for (let pref in this._observedPrefs) {
|
|
|
|
let prefName = this._observedPrefs[pref];
|
2014-03-27 08:03:42 +00:00
|
|
|
Services.prefs.addObserver(prefName, this, false);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_removePrefsObserver: function DirectoryLinksProvider_removeObserver() {
|
2014-05-09 15:24:30 +00:00
|
|
|
for (let pref in this._observedPrefs) {
|
|
|
|
let prefName = this._observedPrefs[pref];
|
2014-03-27 08:03:42 +00:00
|
|
|
Services.prefs.removeObserver(prefName, this);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-03-26 21:23:21 +00:00
|
|
|
_cacheSuggestedLinks: function(link) {
|
2015-04-01 21:34:21 +00:00
|
|
|
if (!link.frecent_sites || "sponsored" == link.type) {
|
|
|
|
// Don't cache links that don't have the expected 'frecent_sites' or are sponsored.
|
2015-04-01 21:26:46 +00:00
|
|
|
return;
|
|
|
|
}
|
2015-03-26 21:23:21 +00:00
|
|
|
for (let suggestedSite of link.frecent_sites) {
|
|
|
|
let suggestedMap = this._suggestedLinks.get(suggestedSite) || new Map();
|
|
|
|
suggestedMap.set(link.url, link);
|
|
|
|
this._suggestedLinks.set(suggestedSite, suggestedMap);
|
2015-03-10 21:08:30 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-05-09 15:24:30 +00:00
|
|
|
_fetchAndCacheLinks: function DirectoryLinksProvider_fetchAndCacheLinks(uri) {
|
2014-10-01 16:44:32 +00:00
|
|
|
// Replace with the same display locale used for selecting links data
|
|
|
|
uri = uri.replace("%LOCALE%", this.locale);
|
2015-04-01 21:30:28 +00:00
|
|
|
uri = uri.replace("%CHANNEL%", UpdateChannel.get());
|
2014-10-01 16:44:32 +00:00
|
|
|
|
2014-05-09 15:24:30 +00:00
|
|
|
let deferred = Promise.defer();
|
|
|
|
let xmlHttp = new XMLHttpRequest();
|
|
|
|
|
|
|
|
let self = this;
|
|
|
|
xmlHttp.onload = function(aResponse) {
|
|
|
|
let json = this.responseText;
|
|
|
|
if (this.status && this.status != 200) {
|
|
|
|
json = "{}";
|
|
|
|
}
|
2014-05-27 20:59:33 +00:00
|
|
|
OS.File.writeAtomic(self._directoryFilePath, json, {tmpPath: self._directoryFilePath + ".tmp"})
|
2014-05-09 15:24:30 +00:00
|
|
|
.then(() => {
|
|
|
|
deferred.resolve();
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
deferred.reject("Error writing uri data in profD.");
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
xmlHttp.onerror = function(e) {
|
|
|
|
deferred.reject("Fetching " + uri + " results in error code: " + e.target.status);
|
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
2014-10-01 16:44:32 +00:00
|
|
|
xmlHttp.open("GET", uri);
|
2014-07-23 21:33:13 +00:00
|
|
|
// Override the type so XHR doesn't complain about not well-formed XML
|
|
|
|
xmlHttp.overrideMimeType(DIRECTORY_LINKS_TYPE);
|
|
|
|
// Set the appropriate request type for servers that require correct types
|
|
|
|
xmlHttp.setRequestHeader("Content-Type", DIRECTORY_LINKS_TYPE);
|
2014-10-01 16:44:32 +00:00
|
|
|
xmlHttp.send();
|
2014-05-09 15:24:30 +00:00
|
|
|
} catch (e) {
|
|
|
|
deferred.reject("Error fetching " + uri);
|
|
|
|
Cu.reportError(e);
|
|
|
|
}
|
|
|
|
return deferred.promise;
|
|
|
|
},
|
|
|
|
|
2014-05-27 20:59:33 +00:00
|
|
|
/**
|
|
|
|
* Downloads directory links if needed
|
|
|
|
* @return promise resolved immediately if no download needed, or upon completion
|
|
|
|
*/
|
|
|
|
_fetchAndCacheLinksIfNecessary: function DirectoryLinksProvider_fetchAndCacheLinksIfNecessary(forceDownload=false) {
|
|
|
|
if (this._downloadDeferred) {
|
|
|
|
// fetching links already - just return the promise
|
|
|
|
return this._downloadDeferred.promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (forceDownload || this._needsDownload) {
|
|
|
|
this._downloadDeferred = Promise.defer();
|
|
|
|
this._fetchAndCacheLinks(this._linksURL).then(() => {
|
|
|
|
// the new file was successfully downloaded and cached, so update a timestamp
|
|
|
|
this._lastDownloadMS = Date.now();
|
|
|
|
this._downloadDeferred.resolve();
|
|
|
|
this._downloadDeferred = null;
|
|
|
|
this._callObservers("onManyLinksChanged")
|
|
|
|
},
|
|
|
|
error => {
|
|
|
|
this._downloadDeferred.resolve();
|
|
|
|
this._downloadDeferred = null;
|
|
|
|
this._callObservers("onDownloadFail");
|
|
|
|
});
|
|
|
|
return this._downloadDeferred.promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
// download is not needed
|
|
|
|
return Promise.resolve();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return true if download is needed, false otherwise
|
|
|
|
*/
|
|
|
|
get _needsDownload () {
|
|
|
|
// fail if last download occured less then 24 hours ago
|
|
|
|
if ((Date.now() - this._lastDownloadMS) > this._downloadIntervalMS) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2014-05-27 20:59:34 +00:00
|
|
|
/**
|
|
|
|
* Reads directory links file and parses its content
|
2015-03-26 21:17:59 +00:00
|
|
|
* @return a promise resolved to an object with keys 'directory' and 'suggested',
|
|
|
|
* each containing a valid list of links,
|
|
|
|
* or {'directory': [], 'suggested': []} if read or parse fails.
|
2014-05-27 20:59:34 +00:00
|
|
|
*/
|
|
|
|
_readDirectoryLinksFile: function DirectoryLinksProvider_readDirectoryLinksFile() {
|
2015-04-01 21:26:46 +00:00
|
|
|
let emptyOutput = {directory: [], suggested: [], enhanced: []};
|
2014-05-27 20:59:34 +00:00
|
|
|
return OS.File.read(this._directoryFilePath).then(binaryData => {
|
|
|
|
let output;
|
|
|
|
try {
|
|
|
|
let json = gTextDecoder.decode(binaryData);
|
2015-03-26 21:17:59 +00:00
|
|
|
let linksObj = JSON.parse(json);
|
2015-04-01 21:26:46 +00:00
|
|
|
output = {directory: linksObj.directory || [],
|
|
|
|
suggested: linksObj.suggested || [],
|
|
|
|
enhanced: linksObj.enhanced || []};
|
2014-05-27 20:59:34 +00:00
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
Cu.reportError(e);
|
|
|
|
}
|
2015-03-26 21:17:59 +00:00
|
|
|
return output || emptyOutput;
|
2014-05-27 20:59:34 +00:00
|
|
|
},
|
|
|
|
error => {
|
|
|
|
Cu.reportError(error);
|
2015-03-26 21:17:59 +00:00
|
|
|
return emptyOutput;
|
2014-05-27 20:59:34 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2014-06-10 05:03:23 +00:00
|
|
|
/**
|
2014-08-08 23:40:40 +00:00
|
|
|
* Report some action on a newtab page (view, click)
|
|
|
|
* @param sites Array of sites shown on newtab page
|
2014-06-10 05:03:23 +00:00
|
|
|
* @param action String of the behavior to report
|
2014-08-08 23:40:40 +00:00
|
|
|
* @param triggeringSiteIndex optional Int index of the site triggering action
|
|
|
|
* @return download promise
|
2014-06-10 05:03:23 +00:00
|
|
|
*/
|
2014-08-08 23:40:40 +00:00
|
|
|
reportSitesAction: function DirectoryLinksProvider_reportSitesAction(sites, action, triggeringSiteIndex) {
|
2015-03-22 07:46:26 +00:00
|
|
|
// Check if the suggested tile was shown
|
|
|
|
if (action == "view") {
|
|
|
|
sites.slice(0, triggeringSiteIndex + 1).forEach(site => {
|
|
|
|
let {targetedSite, url} = site.link;
|
|
|
|
if (targetedSite) {
|
|
|
|
this._decreaseFrequencyCap(url, 1);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// Use up all views if the user clicked on a frequency capped tile
|
|
|
|
else if (action == "click") {
|
|
|
|
let {targetedSite, url} = sites[triggeringSiteIndex].link;
|
|
|
|
if (targetedSite) {
|
|
|
|
this._decreaseFrequencyCap(url, DEFAULT_FREQUENCY_CAP);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-08-08 23:40:40 +00:00
|
|
|
let newtabEnhanced = false;
|
|
|
|
let pingEndPoint = "";
|
2014-06-10 05:03:23 +00:00
|
|
|
try {
|
2014-08-08 23:40:40 +00:00
|
|
|
newtabEnhanced = Services.prefs.getBoolPref(PREF_NEWTAB_ENHANCED);
|
|
|
|
pingEndPoint = Services.prefs.getCharPref(PREF_DIRECTORY_PING);
|
2014-06-10 05:03:23 +00:00
|
|
|
}
|
2014-08-08 23:40:40 +00:00
|
|
|
catch (ex) {}
|
|
|
|
|
|
|
|
// Only send pings when enhancing tiles with an endpoint and valid action
|
|
|
|
let invalidAction = PING_ACTIONS.indexOf(action) == -1;
|
|
|
|
if (!newtabEnhanced || pingEndPoint == "" || invalidAction) {
|
|
|
|
return Promise.resolve();
|
2014-06-10 05:03:23 +00:00
|
|
|
}
|
|
|
|
|
2014-08-08 23:40:40 +00:00
|
|
|
let actionIndex;
|
|
|
|
let data = {
|
|
|
|
locale: this.locale,
|
|
|
|
tiles: sites.reduce((tiles, site, pos) => {
|
|
|
|
// Only add data for non-empty tiles
|
|
|
|
if (site) {
|
|
|
|
// Remember which tiles data triggered the action
|
|
|
|
let {link} = site;
|
|
|
|
let tilesIndex = tiles.length;
|
|
|
|
if (triggeringSiteIndex == pos) {
|
|
|
|
actionIndex = tilesIndex;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make the payload in a way so keys can be excluded when stringified
|
|
|
|
let id = link.directoryId;
|
|
|
|
tiles.push({
|
|
|
|
id: id || site.enhancedId,
|
|
|
|
pin: site.isPinned() ? 1 : undefined,
|
|
|
|
pos: pos != tilesIndex ? pos : undefined,
|
|
|
|
score: Math.round(link.frecency / PING_SCORE_DIVISOR) || undefined,
|
2014-09-04 15:54:00 +00:00
|
|
|
url: site.enhancedId && "",
|
2014-08-08 23:40:40 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return tiles;
|
|
|
|
}, []),
|
|
|
|
};
|
|
|
|
|
|
|
|
// Provide a direct index to the tile triggering the action
|
|
|
|
if (actionIndex !== undefined) {
|
|
|
|
data[action] = actionIndex;
|
2014-06-10 05:03:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Package the data to be sent with the ping
|
|
|
|
let ping = new XMLHttpRequest();
|
2014-08-08 23:40:40 +00:00
|
|
|
ping.open("POST", pingEndPoint + (action == "view" ? "view" : "click"));
|
|
|
|
ping.send(JSON.stringify(data));
|
2014-06-10 05:03:23 +00:00
|
|
|
|
2014-08-08 23:40:40 +00:00
|
|
|
// Use this as an opportunity to potentially fetch new links
|
|
|
|
return this._fetchAndCacheLinksIfNecessary();
|
2014-05-27 20:59:33 +00:00
|
|
|
},
|
|
|
|
|
2014-07-23 18:02:49 +00:00
|
|
|
/**
|
|
|
|
* Get the enhanced link object for a link (whether history or directory)
|
|
|
|
*/
|
|
|
|
getEnhancedLink: function DirectoryLinksProvider_getEnhancedLink(link) {
|
|
|
|
// Use the provided link if it's already enhanced
|
2015-04-01 21:26:46 +00:00
|
|
|
return link.enhancedImageURI && link ? link :
|
2014-08-29 20:35:59 +00:00
|
|
|
this._enhancedLinks.get(NewTabUtils.extractSite(link.url));
|
2014-07-23 18:02:49 +00:00
|
|
|
},
|
|
|
|
|
2014-10-24 16:33:03 +00:00
|
|
|
/**
|
|
|
|
* Check if a url's scheme is in a Set of allowed schemes
|
|
|
|
*/
|
|
|
|
isURLAllowed: function DirectoryLinksProvider_isURLAllowed(url, allowed) {
|
|
|
|
// Assume no url is an allowed url
|
|
|
|
if (!url) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
let scheme = "";
|
|
|
|
try {
|
|
|
|
// A malformed url will not be allowed
|
|
|
|
scheme = Services.io.newURI(url, null, null).scheme;
|
|
|
|
}
|
|
|
|
catch(ex) {}
|
|
|
|
return allowed.has(scheme);
|
|
|
|
},
|
|
|
|
|
2014-03-27 08:03:42 +00:00
|
|
|
/**
|
|
|
|
* Gets the current set of directory links.
|
|
|
|
* @param aCallback The function that the array of links is passed to.
|
|
|
|
*/
|
|
|
|
getLinks: function DirectoryLinksProvider_getLinks(aCallback) {
|
2014-05-27 20:59:34 +00:00
|
|
|
this._readDirectoryLinksFile().then(rawLinks => {
|
2015-03-26 21:23:21 +00:00
|
|
|
// Reset the cache of suggested tiles and enhanced images for this new set of links
|
2014-07-23 18:02:49 +00:00
|
|
|
this._enhancedLinks.clear();
|
2015-03-22 07:46:26 +00:00
|
|
|
this._frequencyCaps.clear();
|
2015-03-26 21:23:21 +00:00
|
|
|
this._suggestedLinks.clear();
|
2014-07-23 18:02:49 +00:00
|
|
|
|
2015-03-26 21:17:59 +00:00
|
|
|
let validityFilter = function(link) {
|
2014-10-24 16:33:03 +00:00
|
|
|
// Make sure the link url is allowed and images too if they exist
|
|
|
|
return this.isURLAllowed(link.url, ALLOWED_LINK_SCHEMES) &&
|
|
|
|
this.isURLAllowed(link.imageURI, ALLOWED_IMAGE_SCHEMES) &&
|
|
|
|
this.isURLAllowed(link.enhancedImageURI, ALLOWED_IMAGE_SCHEMES);
|
2015-03-26 21:17:59 +00:00
|
|
|
}.bind(this);
|
|
|
|
|
|
|
|
rawLinks.suggested.filter(validityFilter).forEach((link, position) => {
|
2015-04-01 21:26:46 +00:00
|
|
|
link.lastVisitDate = rawLinks.suggested.length - position;
|
2014-07-23 18:02:49 +00:00
|
|
|
|
2015-03-26 21:23:21 +00:00
|
|
|
// We cache suggested tiles here but do not push any of them in the links list yet.
|
|
|
|
// The decision for which suggested tile to include will be made separately.
|
|
|
|
this._cacheSuggestedLinks(link);
|
2015-03-22 07:46:26 +00:00
|
|
|
this._frequencyCaps.set(link.url, DEFAULT_FREQUENCY_CAP);
|
2015-03-26 21:17:59 +00:00
|
|
|
});
|
|
|
|
|
2015-04-01 21:26:46 +00:00
|
|
|
rawLinks.enhanced.filter(validityFilter).forEach((link, position) => {
|
|
|
|
link.lastVisitDate = rawLinks.enhanced.length - position;
|
|
|
|
|
|
|
|
// Stash the enhanced image for the site
|
|
|
|
if (link.enhancedImageURI) {
|
|
|
|
this._enhancedLinks.set(NewTabUtils.extractSite(link.url), link);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2015-03-22 07:46:26 +00:00
|
|
|
let links = rawLinks.directory.filter(validityFilter).map((link, position) => {
|
2015-04-01 21:26:46 +00:00
|
|
|
link.lastVisitDate = rawLinks.directory.length - position;
|
2014-03-27 08:03:42 +00:00
|
|
|
link.frecency = DIRECTORY_FRECENCY;
|
2015-03-26 21:17:59 +00:00
|
|
|
return link;
|
2014-09-23 22:12:20 +00:00
|
|
|
});
|
2015-03-22 07:46:26 +00:00
|
|
|
|
|
|
|
// Allow for one link suggestion on top of the default directory links
|
|
|
|
this.maxNumLinks = links.length + 1;
|
|
|
|
|
|
|
|
return links;
|
2014-09-23 22:12:20 +00:00
|
|
|
}).catch(ex => {
|
|
|
|
Cu.reportError(ex);
|
|
|
|
return [];
|
2015-03-13 15:45:31 +00:00
|
|
|
}).then(links => {
|
|
|
|
aCallback(links);
|
|
|
|
this._populatePlacesLinks();
|
|
|
|
});
|
2014-03-27 08:03:42 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
init: function DirectoryLinksProvider_init() {
|
2014-08-08 23:40:40 +00:00
|
|
|
this._setDefaultEnhanced();
|
2014-03-27 08:03:42 +00:00
|
|
|
this._addPrefsObserver();
|
2014-05-27 20:59:33 +00:00
|
|
|
// setup directory file path and last download timestamp
|
|
|
|
this._directoryFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, DIRECTORY_LINKS_FILE);
|
|
|
|
this._lastDownloadMS = 0;
|
2015-03-13 15:45:31 +00:00
|
|
|
|
|
|
|
NewTabUtils.placesProvider.addObserver(this);
|
|
|
|
|
2014-05-27 20:59:33 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
// fetch directory on startup without force
|
|
|
|
yield this._fetchAndCacheLinksIfNecessary();
|
|
|
|
}.bind(this));
|
2014-03-27 08:03:42 +00:00
|
|
|
},
|
|
|
|
|
2015-03-13 15:45:31 +00:00
|
|
|
_handleManyLinksChanged: function() {
|
2015-03-26 21:23:21 +00:00
|
|
|
this._topSitesWithSuggestedLinks.clear();
|
|
|
|
this._suggestedLinks.forEach((suggestedLinks, site) => {
|
2015-03-13 15:45:31 +00:00
|
|
|
if (NewTabUtils.isTopPlacesSite(site)) {
|
2015-03-26 21:23:21 +00:00
|
|
|
this._topSitesWithSuggestedLinks.add(site);
|
2015-03-13 15:45:31 +00:00
|
|
|
}
|
|
|
|
});
|
2015-03-26 21:23:21 +00:00
|
|
|
this._updateSuggestedTile();
|
2015-03-13 15:45:31 +00:00
|
|
|
},
|
|
|
|
|
2015-03-13 15:45:34 +00:00
|
|
|
/**
|
2015-03-26 21:23:21 +00:00
|
|
|
* Updates _topSitesWithSuggestedLinks based on the link that was changed.
|
2015-03-13 15:45:34 +00:00
|
|
|
*
|
2015-03-26 21:23:21 +00:00
|
|
|
* @return true if _topSitesWithSuggestedLinks was modified, false otherwise.
|
2015-03-13 15:45:34 +00:00
|
|
|
*/
|
2015-03-13 15:45:31 +00:00
|
|
|
_handleLinkChanged: function(aLink) {
|
|
|
|
let changedLinkSite = NewTabUtils.extractSite(aLink.url);
|
2015-03-26 21:23:21 +00:00
|
|
|
let linkStored = this._topSitesWithSuggestedLinks.has(changedLinkSite);
|
2015-03-13 15:45:34 +00:00
|
|
|
|
|
|
|
if (!NewTabUtils.isTopPlacesSite(changedLinkSite) && linkStored) {
|
2015-03-26 21:23:21 +00:00
|
|
|
this._topSitesWithSuggestedLinks.delete(changedLinkSite);
|
2015-03-13 15:45:34 +00:00
|
|
|
return true;
|
2015-03-13 15:45:31 +00:00
|
|
|
}
|
|
|
|
|
2015-03-26 21:23:21 +00:00
|
|
|
if (this._suggestedLinks.has(changedLinkSite) &&
|
2015-03-13 15:45:34 +00:00
|
|
|
NewTabUtils.isTopPlacesSite(changedLinkSite) && !linkStored) {
|
2015-03-26 21:23:21 +00:00
|
|
|
this._topSitesWithSuggestedLinks.add(changedLinkSite);
|
2015-03-13 15:45:34 +00:00
|
|
|
return true;
|
2015-03-13 15:45:31 +00:00
|
|
|
}
|
2015-03-13 15:45:34 +00:00
|
|
|
return false;
|
2015-03-13 15:45:31 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_populatePlacesLinks: function () {
|
|
|
|
NewTabUtils.links.populateProviderCache(NewTabUtils.placesProvider, () => {
|
|
|
|
this._handleManyLinksChanged();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
onLinkChanged: function (aProvider, aLink) {
|
|
|
|
// Make sure NewTabUtils.links handles the notification first.
|
|
|
|
setTimeout(() => {
|
2015-03-13 15:45:34 +00:00
|
|
|
if (this._handleLinkChanged(aLink)) {
|
2015-03-26 21:23:21 +00:00
|
|
|
this._updateSuggestedTile();
|
2015-03-13 15:45:34 +00:00
|
|
|
}
|
2015-03-13 15:45:31 +00:00
|
|
|
}, 0);
|
|
|
|
},
|
|
|
|
|
|
|
|
onManyLinksChanged: function () {
|
|
|
|
// Make sure NewTabUtils.links handles the notification first.
|
|
|
|
setTimeout(() => {
|
|
|
|
this._handleManyLinksChanged();
|
|
|
|
}, 0);
|
|
|
|
},
|
|
|
|
|
2015-03-22 07:46:26 +00:00
|
|
|
/**
|
|
|
|
* Record for a url that some number of views have been used
|
|
|
|
* @param url String url of the suggested link
|
|
|
|
* @param amount Number of equivalent views to decrease
|
|
|
|
*/
|
|
|
|
_decreaseFrequencyCap(url, amount) {
|
|
|
|
let remainingViews = this._frequencyCaps.get(url) - amount;
|
|
|
|
this._frequencyCaps.set(url, remainingViews);
|
|
|
|
|
|
|
|
// Reached the number of views, so pick a new one.
|
|
|
|
if (remainingViews <= 0) {
|
|
|
|
this._updateSuggestedTile();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-03-13 15:45:34 +00:00
|
|
|
/**
|
2015-03-26 21:23:21 +00:00
|
|
|
* Chooses and returns a suggested tile based on a user's top sites
|
|
|
|
* that we have an available suggested tile for.
|
2015-03-13 15:45:34 +00:00
|
|
|
*
|
2015-03-26 21:23:21 +00:00
|
|
|
* @return the chosen suggested tile, or undefined if there isn't one
|
2015-03-13 15:45:34 +00:00
|
|
|
*/
|
2015-03-26 21:23:21 +00:00
|
|
|
_updateSuggestedTile: function() {
|
2015-03-13 15:45:34 +00:00
|
|
|
let sortedLinks = NewTabUtils.getProviderLinks(this);
|
|
|
|
|
2015-03-13 15:45:38 +00:00
|
|
|
if (!sortedLinks) {
|
|
|
|
// If NewTabUtils.links.resetCache() is called before getting here,
|
|
|
|
// sortedLinks may be undefined.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-03-26 21:23:21 +00:00
|
|
|
// Delete the current suggested tile, if one exists.
|
2015-03-13 15:45:34 +00:00
|
|
|
let initialLength = sortedLinks.length;
|
|
|
|
if (initialLength) {
|
|
|
|
let mostFrecentLink = sortedLinks[0];
|
2015-03-26 21:17:59 +00:00
|
|
|
if (mostFrecentLink.targetedSite) {
|
2015-03-13 15:45:34 +00:00
|
|
|
this._callObservers("onLinkChanged", {
|
|
|
|
url: mostFrecentLink.url,
|
2015-03-22 07:46:26 +00:00
|
|
|
frecency: SUGGESTED_FRECENCY,
|
2015-03-13 15:45:34 +00:00
|
|
|
lastVisitDate: mostFrecentLink.lastVisitDate,
|
2015-03-26 21:17:59 +00:00
|
|
|
type: mostFrecentLink.type,
|
2015-03-13 15:45:34 +00:00
|
|
|
}, 0, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-26 21:23:21 +00:00
|
|
|
if (this._topSitesWithSuggestedLinks.size == 0) {
|
|
|
|
// There are no potential suggested links we can show.
|
2015-03-13 15:45:34 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-03-26 21:23:21 +00:00
|
|
|
// Create a flat list of all possible links we can show as suggested.
|
|
|
|
// Note that many top sites may map to the same suggested links, but we only
|
|
|
|
// want to count each suggested link once (based on url), thus possibleLinks is a map
|
|
|
|
// from url to suggestedLink. Thus, each link has an equal chance of being chosen at
|
2015-03-13 15:45:34 +00:00
|
|
|
// random from flattenedLinks if it appears only once.
|
|
|
|
let possibleLinks = new Map();
|
2015-03-20 20:39:09 +00:00
|
|
|
let targetedSites = new Map();
|
2015-03-26 21:23:21 +00:00
|
|
|
this._topSitesWithSuggestedLinks.forEach(topSiteWithSuggestedLink => {
|
|
|
|
let suggestedLinksMap = this._suggestedLinks.get(topSiteWithSuggestedLink);
|
|
|
|
suggestedLinksMap.forEach((suggestedLink, url) => {
|
2015-03-22 07:46:26 +00:00
|
|
|
// Skip this link if we've shown it too many times already
|
|
|
|
if (this._frequencyCaps.get(url) <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-03-26 21:23:21 +00:00
|
|
|
possibleLinks.set(url, suggestedLink);
|
2015-03-20 20:39:09 +00:00
|
|
|
|
|
|
|
// Keep a map of URL to targeted sites. We later use this to show the user
|
|
|
|
// what site they visited to trigger this suggestion.
|
|
|
|
if (!targetedSites.get(url)) {
|
|
|
|
targetedSites.set(url, []);
|
|
|
|
}
|
2015-03-26 21:23:21 +00:00
|
|
|
targetedSites.get(url).push(topSiteWithSuggestedLink);
|
2015-03-13 15:45:34 +00:00
|
|
|
})
|
|
|
|
});
|
2015-03-22 07:46:26 +00:00
|
|
|
|
|
|
|
// We might have run out of possible links to show
|
|
|
|
let numLinks = possibleLinks.size;
|
|
|
|
if (numLinks == 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-03-13 15:45:34 +00:00
|
|
|
let flattenedLinks = [...possibleLinks.values()];
|
|
|
|
|
2015-03-26 21:23:21 +00:00
|
|
|
// Choose our suggested link at random
|
2015-03-22 07:46:26 +00:00
|
|
|
let suggestedIndex = Math.floor(Math.random() * numLinks);
|
2015-03-26 21:23:21 +00:00
|
|
|
let chosenSuggestedLink = flattenedLinks[suggestedIndex];
|
2015-03-13 15:45:34 +00:00
|
|
|
|
2015-03-30 06:30:54 +00:00
|
|
|
// Add the suggested link to the front with some extra values
|
|
|
|
this._callObservers("onLinkChanged", Object.assign({
|
2015-03-26 21:23:21 +00:00
|
|
|
frecency: SUGGESTED_FRECENCY,
|
2015-03-20 20:39:09 +00:00
|
|
|
|
|
|
|
// Choose the first site a user has visited as the target. In the future,
|
|
|
|
// this should be the site with the highest frecency. However, we currently
|
|
|
|
// store frecency by URL not by site.
|
2015-03-26 21:23:21 +00:00
|
|
|
targetedSite: targetedSites.get(chosenSuggestedLink.url).length ?
|
|
|
|
targetedSites.get(chosenSuggestedLink.url)[0] : null
|
2015-03-30 06:30:54 +00:00
|
|
|
}, chosenSuggestedLink));
|
2015-03-26 21:23:21 +00:00
|
|
|
return chosenSuggestedLink;
|
2015-03-13 15:45:34 +00:00
|
|
|
},
|
|
|
|
|
2014-03-27 08:03:42 +00:00
|
|
|
/**
|
|
|
|
* Return the object to its pre-init state
|
|
|
|
*/
|
|
|
|
reset: function DirectoryLinksProvider_reset() {
|
|
|
|
delete this.__linksURL;
|
|
|
|
this._removePrefsObserver();
|
|
|
|
this._removeObservers();
|
|
|
|
},
|
|
|
|
|
|
|
|
addObserver: function DirectoryLinksProvider_addObserver(aObserver) {
|
2014-05-27 20:59:33 +00:00
|
|
|
this._observers.add(aObserver);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeObserver: function DirectoryLinksProvider_removeObserver(aObserver) {
|
|
|
|
this._observers.delete(aObserver);
|
2014-03-27 08:03:42 +00:00
|
|
|
},
|
|
|
|
|
2015-03-22 07:46:26 +00:00
|
|
|
_callObservers(methodName, ...args) {
|
2014-03-27 08:03:42 +00:00
|
|
|
for (let obs of this._observers) {
|
2015-03-22 07:46:26 +00:00
|
|
|
if (typeof(obs[methodName]) == "function") {
|
2014-03-27 08:03:42 +00:00
|
|
|
try {
|
2015-03-22 07:46:26 +00:00
|
|
|
obs[methodName](this, ...args);
|
2014-03-27 08:03:42 +00:00
|
|
|
} catch (err) {
|
|
|
|
Cu.reportError(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_removeObservers: function() {
|
2014-05-27 20:59:33 +00:00
|
|
|
this._observers.clear();
|
2014-03-27 08:03:42 +00:00
|
|
|
}
|
|
|
|
};
|