summaryrefslogtreecommitdiff
path: root/application/basilisk/modules/DirectoryLinksProvider.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'application/basilisk/modules/DirectoryLinksProvider.jsm')
-rw-r--r--application/basilisk/modules/DirectoryLinksProvider.jsm1243
1 files changed, 0 insertions, 1243 deletions
diff --git a/application/basilisk/modules/DirectoryLinksProvider.jsm b/application/basilisk/modules/DirectoryLinksProvider.jsm
deleted file mode 100644
index 5fe9f9fce0..0000000000
--- a/application/basilisk/modules/DirectoryLinksProvider.jsm
+++ /dev/null
@@ -1,1243 +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/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["DirectoryLinksProvider"];
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const ParserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
-
-Cu.importGlobalProperties(["XMLHttpRequest"]);
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/Timer.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
- "resource://gre/modules/NetUtil.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
- "resource://gre/modules/NewTabUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OS",
- "resource://gre/modules/osfile.jsm")
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
- "resource://gre/modules/Promise.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
- "resource://gre/modules/UpdateUtils.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "eTLD",
- "@mozilla.org/network/effective-tld-service;1",
- "nsIEffectiveTLDService");
-XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => {
- return new TextDecoder();
-});
-XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function() {
- return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
-});
-XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function() {
- let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
- .createInstance(Ci.nsIScriptableUnicodeConverter);
- converter.charset = "utf8";
- return converter;
-});
-
-
-// The filename where directory links are stored locally
-const DIRECTORY_LINKS_FILE = "directoryLinks.json";
-const DIRECTORY_LINKS_TYPE = "application/json";
-
-// 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
-const PREF_DIRECTORY_SOURCE = "browser.newtabpage.directory.source";
-
-// The preference that tells where to send click/view pings
-const PREF_DIRECTORY_PING = "browser.newtabpage.directory.ping";
-
-// The preference that tells if newtab is enhanced
-const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced";
-
-// 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"]);
-
-// Only allow urls to Mozilla's CDN or empty (for data URIs)
-const ALLOWED_URL_BASE = new Set(["mozilla.net", ""]);
-
-// The frecency of a directory link
-const DIRECTORY_FRECENCY = 1000;
-
-// The frecency of a suggested link
-const SUGGESTED_FRECENCY = Infinity;
-
-// The filename where frequency cap data stored locally
-const FREQUENCY_CAP_FILE = "frequencyCap.json";
-
-// Default settings for daily and total frequency caps
-const DEFAULT_DAILY_FREQUENCY_CAP = 3;
-const DEFAULT_TOTAL_FREQUENCY_CAP = 10;
-
-// Default timeDelta to prune unused frequency cap objects
-// currently set to 10 days in milliseconds
-const DEFAULT_PRUNE_TIME_DELTA = 10 * 24 * 60 * 60 * 1000;
-
-// The min number of visible (not blocked) history tiles to have before showing suggested tiles
-const MIN_VISIBLE_HISTORY_TILES = 8;
-
-// The max number of visible (not blocked) history tiles to test for inadjacency
-const MAX_VISIBLE_HISTORY_TILES = 15;
-
-// Allowed ping actions remotely stored as columns: case-insensitive [a-z0-9_]
-const PING_ACTIONS = ["block", "click", "pin", "sponsored", "sponsored_link", "unpin", "view"];
-
-// Location of inadjacent sites json
-const INADJACENCY_SOURCE = "chrome://browser/content/newtab/newTab.inadjacent.json";
-
-// Fake URL to keep track of last block of a suggested tile in the frequency cap object
-const FAKE_SUGGESTED_BLOCK_URL = "ignore://suggested_block";
-
-// Time before suggested tile is allowed to play again after block - default to 1 day
-const AFTER_SUGGESTED_BLOCK_DECAY_TIME = 24 * 60 * 60 * 1000;
-
-/**
- * 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.
- */
-var DirectoryLinksProvider = {
-
- __linksURL: null,
-
- _observers: new Set(),
-
- // links download deferred, resolved upon download completion
- _downloadDeferred: null,
-
- // download default interval is 24 hours in milliseconds
- _downloadIntervalMS: 86400000,
-
- /**
- * A mapping from eTLD+1 to an enhanced link objects
- */
- _enhancedLinks: new Map(),
-
- /**
- * A mapping from site to a list of suggested link objects
- */
- _suggestedLinks: new Map(),
-
- /**
- * Frequency Cap object - maintains daily and total tile counts, and frequency cap settings
- */
- _frequencyCaps: {},
-
- /**
- * A set of top sites that we can provide suggested links for
- */
- _topSitesWithSuggestedLinks: new Set(),
-
- /**
- * lookup Set of inadjacent domains
- */
- _inadjacentSites: new Set(),
-
- /**
- * This flag is set if there is a suggested tile configured to avoid
- * inadjacent sites in new tab
- */
- _avoidInadjacentSites: false,
-
- /**
- * This flag is set if _avoidInadjacentSites is true and there is
- * an inadjacent site in the new tab
- */
- _newTabHasInadjacentSite: false,
-
- get _observedPrefs() {
- return Object.freeze({
- enhanced: PREF_NEWTAB_ENHANCED,
- linksURL: PREF_DIRECTORY_SOURCE,
- matchOSLocale: PREF_MATCH_OS_LOCALE,
- prefSelectedLocale: PREF_SELECTED_LOCALE,
- });
- },
-
- get _linksURL() {
- if (!this.__linksURL) {
- try {
- this.__linksURL = Services.prefs.getCharPref(this._observedPrefs["linksURL"]);
- this.__linksURLModified = Services.prefs.prefHasUserValue(this._observedPrefs["linksURL"]);
- } catch (e) {
- Cu.reportError("Error fetching directory links url from prefs: " + e);
- }
- }
- return this.__linksURL;
- },
-
- /**
- * 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";
- },
-
- /**
- * Set appropriate default ping behavior controlled by enhanced pref
- */
- _setDefaultEnhanced: function DirectoryLinksProvider_setDefaultEnhanced() {
- if (!Services.prefs.prefHasUserValue(PREF_NEWTAB_ENHANCED)) {
- let enhanced = Services.prefs.getBoolPref(PREF_NEWTAB_ENHANCED);
- try {
- // Default to not enhanced if DNT is set to tell websites to not track
- if (Services.prefs.getBoolPref("privacy.donottrackheader.enabled")) {
- enhanced = false;
- }
- } catch (ex) {}
- Services.prefs.setBoolPref(PREF_NEWTAB_ENHANCED, enhanced);
- }
- },
-
- observe: function DirectoryLinksProvider_observe(aSubject, aTopic, aData) {
- if (aTopic == "nsPref:changed") {
- 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;
- }
- }
- },
-
- _addPrefsObserver: function DirectoryLinksProvider_addObserver() {
- for (let pref in this._observedPrefs) {
- let prefName = this._observedPrefs[pref];
- Services.prefs.addObserver(prefName, this, false);
- }
- },
-
- _removePrefsObserver: function DirectoryLinksProvider_removeObserver() {
- for (let pref in this._observedPrefs) {
- let prefName = this._observedPrefs[pref];
- Services.prefs.removeObserver(prefName, this);
- }
- },
-
- _cacheSuggestedLinks(link) {
- // Don't cache links that don't have the expected 'frecent_sites'
- if (!link.frecent_sites) {
- return;
- }
-
- for (let suggestedSite of link.frecent_sites) {
- let suggestedMap = this._suggestedLinks.get(suggestedSite) || new Map();
- suggestedMap.set(link.url, link);
- this._setupStartEndTime(link);
- this._suggestedLinks.set(suggestedSite, suggestedMap);
- }
- },
-
- _fetchAndCacheLinks: function DirectoryLinksProvider_fetchAndCacheLinks(uri) {
- // Replace with the same display locale used for selecting links data
- uri = uri.replace("%LOCALE%", this.locale);
- uri = uri.replace("%CHANNEL%", UpdateUtils.UpdateChannel);
-
- return this._downloadJsonData(uri).then(json => {
- return OS.File.writeAtomic(this._directoryFilePath, json, {tmpPath: this._directoryFilePath + ".tmp"});
- });
- },
-
- /**
- * Downloads a links with json content
- * @param download uri
- * @return promise resolved to json string, "{}" returned if status != 200
- */
- _downloadJsonData: function DirectoryLinksProvider__downloadJsonData(uri) {
- let deferred = Promise.defer();
- let xmlHttp = this._newXHR();
-
- xmlHttp.onload = function(aResponse) {
- let json = this.responseText;
- if (this.status && this.status != 200) {
- json = "{}";
- }
- deferred.resolve(json);
- };
-
- xmlHttp.onerror = function(e) {
- deferred.reject("Fetching " + uri + " results in error code: " + e.target.status);
- };
-
- try {
- xmlHttp.open("GET", uri);
- // 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);
- xmlHttp.send();
- } catch (e) {
- deferred.reject("Error fetching " + uri);
- Cu.reportError(e);
- }
- return deferred.promise;
- },
-
- /**
- * 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;
- },
-
- /**
- * Create a new XMLHttpRequest that is anonymous, i.e., doesn't send cookies
- */
- _newXHR() {
- return new XMLHttpRequest({mozAnon: true});
- },
-
- /**
- * Reads directory links file and parses its content
- * @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.
- */
- _readDirectoryLinksFile: function DirectoryLinksProvider_readDirectoryLinksFile() {
- let emptyOutput = {directory: [], suggested: [], enhanced: []};
- return OS.File.read(this._directoryFilePath).then(binaryData => {
- let output;
- try {
- let json = gTextDecoder.decode(binaryData);
- let linksObj = JSON.parse(json);
- output = {directory: linksObj.directory || [],
- suggested: linksObj.suggested || [],
- enhanced: linksObj.enhanced || []};
- } catch (e) {
- Cu.reportError(e);
- }
- return output || emptyOutput;
- },
- error => {
- Cu.reportError(error);
- return emptyOutput;
- });
- },
-
- /**
- * Translates link.time_limits to UTC miliseconds and sets
- * link.startTime and link.endTime properties in link object
- */
- _setupStartEndTime: function DirectoryLinksProvider_setupStartEndTime(link) {
- // set start/end limits. Use ISO_8601 format: '2014-01-10T20:20:20.600Z'
- // (details here http://en.wikipedia.org/wiki/ISO_8601)
- // Note that if timezone is missing, FX will interpret as local time
- // meaning that the server can sepecify any time, but if the capmaign
- // needs to start at same time across multiple timezones, the server
- // omits timezone indicator
- if (!link.time_limits) {
- return;
- }
-
- let parsedTime;
- if (link.time_limits.start) {
- parsedTime = Date.parse(link.time_limits.start);
- if (parsedTime && !isNaN(parsedTime)) {
- link.startTime = parsedTime;
- }
- }
- if (link.time_limits.end) {
- parsedTime = Date.parse(link.time_limits.end);
- if (parsedTime && !isNaN(parsedTime)) {
- link.endTime = parsedTime;
- }
- }
- },
-
- /*
- * Handles campaign timeout
- */
- _onCampaignTimeout: function DirectoryLinksProvider_onCampaignTimeout() {
- // _campaignTimeoutID is invalid here, so just set it to null
- this._campaignTimeoutID = null;
- this._updateSuggestedTile();
- },
-
- /*
- * Clears capmpaign timeout
- */
- _clearCampaignTimeout: function DirectoryLinksProvider_clearCampaignTimeout() {
- if (this._campaignTimeoutID) {
- clearTimeout(this._campaignTimeoutID);
- this._campaignTimeoutID = null;
- }
- },
-
- /**
- * Setup capmpaign timeout to recompute suggested tiles upon
- * reaching soonest start or end time for the campaign
- * @param timeout in milliseconds
- */
- _setupCampaignTimeCheck: function DirectoryLinksProvider_setupCampaignTimeCheck(timeout) {
- // sanity check
- if (!timeout || timeout <= 0) {
- return;
- }
- this._clearCampaignTimeout();
- // setup next timeout
- this._campaignTimeoutID = setTimeout(this._onCampaignTimeout.bind(this), timeout);
- },
-
- /**
- * Test link for campaign time limits: checks if link falls within start/end time
- * and returns an object containing a use flag and the timeoutDate milliseconds
- * when the link has to be re-checked for campaign start-ready or end-reach
- * @param link
- * @return object {use: true or false, timeoutDate: milliseconds or null}
- */
- _testLinkForCampaignTimeLimits: function DirectoryLinksProvider_testLinkForCampaignTimeLimits(link) {
- let currentTime = Date.now();
- // test for start time first
- if (link.startTime && link.startTime > currentTime) {
- // not yet ready for start
- return {use: false, timeoutDate: link.startTime};
- }
- // otherwise check for end time
- if (link.endTime) {
- // passed end time
- if (link.endTime <= currentTime) {
- return {use: false};
- }
- // otherwise link is still ok, but we need to set timeoutDate
- return {use: true, timeoutDate: link.endTime};
- }
- // if we are here, the link is ok and no timeoutDate needed
- return {use: true};
- },
-
- /**
- * Handles block on suggested tile: updates fake block url with current timestamp
- */
- handleSuggestedTileBlock: function DirectoryLinksProvider_handleSuggestedTileBlock() {
- this._updateFrequencyCapSettings({url: FAKE_SUGGESTED_BLOCK_URL});
- this._writeFrequencyCapFile();
- this._updateSuggestedTile();
- },
-
- /**
- * Checks if suggested tile is being blocked for the rest of "decay time"
- * @return True if blocked, false otherwise
- */
- _isSuggestedTileBlocked: function DirectoryLinksProvider__isSuggestedTileBlocked() {
- let capObject = this._frequencyCaps[FAKE_SUGGESTED_BLOCK_URL];
- if (!capObject || !capObject.lastUpdated) {
- // user never blocked suggested tile or lastUpdated is missing
- return false;
- }
- // otherwise, make sure that enough time passed after suggested tile was blocked
- return (capObject.lastUpdated + AFTER_SUGGESTED_BLOCK_DECAY_TIME) > Date.now();
- },
-
- /**
- * Report some action on a newtab page (view, click)
- * @param sites Array of sites shown on newtab page
- * @param action String of the behavior to report
- * @param triggeringSiteIndex optional Int index of the site triggering action
- * @return download promise
- */
- reportSitesAction: function DirectoryLinksProvider_reportSitesAction(sites, action, triggeringSiteIndex) {
- // Check if the suggested tile was shown
- if (action == "view") {
- sites.slice(0, triggeringSiteIndex + 1).filter(s => s).forEach(site => {
- let {targetedSite, url} = site.link;
- if (targetedSite) {
- this._addFrequencyCapView(url);
- }
- });
- } else {
- // any click action on a suggested tile should stop that tile suggestion
- // click/block - user either removed a tile or went to a landing page
- // pin - tile turned into history tile, should no longer be suggested
- // unpin - the tile was pinned before, should not matter
- // suggested tile has targetedSite, or frecent_sites if it was pinned
- let {frecent_sites, targetedSite, url} = sites[triggeringSiteIndex].link;
- if (frecent_sites || targetedSite) {
- this._setFrequencyCapClick(url);
- }
- }
-
- let newtabEnhanced = false;
- let pingEndPoint = "";
- try {
- newtabEnhanced = Services.prefs.getBoolPref(PREF_NEWTAB_ENHANCED);
- pingEndPoint = Services.prefs.getCharPref(PREF_DIRECTORY_PING);
- } catch (ex) {}
-
- // Bug 1240245 - We no longer send pings, but frequency capping and fetching
- // tests depend on the following actions, so references to PING remain.
- let invalidAction = PING_ACTIONS.indexOf(action) == -1;
- if (!newtabEnhanced || pingEndPoint == "" || invalidAction) {
- return Promise.resolve();
- }
-
- return Task.spawn(function* () {
- // since we updated views/clicks we need write _frequencyCaps to disk
- yield this._writeFrequencyCapFile();
- // Use this as an opportunity to potentially fetch new links
- yield this._fetchAndCacheLinksIfNecessary();
- }.bind(this));
- },
-
- /**
- * 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
- return link.enhancedImageURI && link ? link :
- this._enhancedLinks.get(NewTabUtils.extractSite(link.url));
- },
-
- /**
- * Check if a url's scheme is in a Set of allowed schemes and if the base
- * domain is allowed.
- * @param url to check
- * @param allowed Set of allowed schemes
- * @param checkBase boolean to check the base domain
- */
- isURLAllowed(url, allowed, checkBase) {
- // Assume no url is an allowed url
- if (!url) {
- return true;
- }
-
- let scheme = "", base = "";
- try {
- // A malformed url will not be allowed
- let uri = Services.io.newURI(url);
- scheme = uri.scheme;
-
- // URIs without base domains will be allowed
- base = Services.eTLD.getBaseDomain(uri);
- } catch (ex) {}
- // Require a scheme match and the base only if desired
- return allowed.has(scheme) && (!checkBase || ALLOWED_URL_BASE.has(base));
- },
-
- _escapeChars(text) {
- let charMap = {
- "&": "&amp;",
- "<": "&lt;",
- ">": "&gt;",
- '"': "&quot;",
- "'": "&#039;"
- };
-
- return text.replace(/[&<>"']/g, (character) => charMap[character]);
- },
-
- /**
- * Gets the current set of directory links.
- * @param aCallback The function that the array of links is passed to.
- */
- getLinks: function DirectoryLinksProvider_getLinks(aCallback) {
- this._readDirectoryLinksFile().then(rawLinks => {
- // Reset the cache of suggested tiles and enhanced images for this new set of links
- this._enhancedLinks.clear();
- this._suggestedLinks.clear();
- this._clearCampaignTimeout();
- this._avoidInadjacentSites = false;
-
- // Only check base domain for images when using the default pref
- let checkBase = !this.__linksURLModified;
- let validityFilter = function(link) {
- // Make sure the link url is allowed and images too if they exist
- return this.isURLAllowed(link.url, ALLOWED_LINK_SCHEMES, false) &&
- (!link.imageURI ||
- this.isURLAllowed(link.imageURI, ALLOWED_IMAGE_SCHEMES, checkBase)) &&
- (!link.enhancedImageURI ||
- this.isURLAllowed(link.enhancedImageURI, ALLOWED_IMAGE_SCHEMES, checkBase));
- }.bind(this);
-
- rawLinks.suggested.filter(validityFilter).forEach((link, position) => {
- // Suggested sites must have an adgroup name.
- if (!link.adgroup_name) {
- return;
- }
-
- let sanitizeFlags = ParserUtils.SanitizerCidEmbedsOnly |
- ParserUtils.SanitizerDropForms |
- ParserUtils.SanitizerDropNonCSSPresentation;
-
- link.explanation = this._escapeChars(link.explanation ? ParserUtils.convertToPlainText(link.explanation, sanitizeFlags, 0) : "");
- link.targetedName = this._escapeChars(ParserUtils.convertToPlainText(link.adgroup_name, sanitizeFlags, 0));
- link.lastVisitDate = rawLinks.suggested.length - position;
- // check if link wants to avoid inadjacent sites
- if (link.check_inadjacency) {
- this._avoidInadjacentSites = true;
- }
-
- // 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);
- this._updateFrequencyCapSettings(link);
- });
-
- 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);
- }
- });
-
- let links = rawLinks.directory.filter(validityFilter).map((link, position) => {
- link.lastVisitDate = rawLinks.directory.length - position;
- link.frecency = DIRECTORY_FRECENCY;
- return link;
- });
-
- // Allow for one link suggestion on top of the default directory links
- this.maxNumLinks = links.length + 1;
-
- // prune frequency caps of outdated urls
- this._pruneFrequencyCapUrls();
- // write frequency caps object to disk asynchronously
- this._writeFrequencyCapFile();
-
- return links;
- }).catch(ex => {
- Cu.reportError(ex);
- return [];
- }).then(links => {
- aCallback(links);
- this._populatePlacesLinks();
- });
- },
-
- init: function DirectoryLinksProvider_init() {
- this._setDefaultEnhanced();
- this._addPrefsObserver();
- // setup directory file path and last download timestamp
- this._directoryFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, DIRECTORY_LINKS_FILE);
- this._lastDownloadMS = 0;
-
- // setup frequency cap file path
- this._frequencyCapFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, FREQUENCY_CAP_FILE);
- // setup inadjacent sites URL
- this._inadjacentSitesUrl = INADJACENCY_SOURCE;
-
- NewTabUtils.placesProvider.addObserver(this);
- NewTabUtils.links.addObserver(this);
-
- return Task.spawn(function*() {
- // get the last modified time of the links file if it exists
- let doesFileExists = yield OS.File.exists(this._directoryFilePath);
- if (doesFileExists) {
- let fileInfo = yield OS.File.stat(this._directoryFilePath);
- this._lastDownloadMS = Date.parse(fileInfo.lastModificationDate);
- }
- // read frequency cap file
- yield this._readFrequencyCapFile();
- // fetch directory on startup without force
- yield this._fetchAndCacheLinksIfNecessary();
- // fecth inadjacent sites on startup
- yield this._loadInadjacentSites();
- }.bind(this));
- },
-
- _handleManyLinksChanged() {
- this._topSitesWithSuggestedLinks.clear();
- this._suggestedLinks.forEach((suggestedLinks, site) => {
- if (NewTabUtils.isTopPlacesSite(site)) {
- this._topSitesWithSuggestedLinks.add(site);
- }
- });
- this._updateSuggestedTile();
- },
-
- /**
- * Updates _topSitesWithSuggestedLinks based on the link that was changed.
- *
- * @return true if _topSitesWithSuggestedLinks was modified, false otherwise.
- */
- _handleLinkChanged(aLink) {
- let changedLinkSite = NewTabUtils.extractSite(aLink.url);
- let linkStored = this._topSitesWithSuggestedLinks.has(changedLinkSite);
-
- if (!NewTabUtils.isTopPlacesSite(changedLinkSite) && linkStored) {
- this._topSitesWithSuggestedLinks.delete(changedLinkSite);
- return true;
- }
-
- if (this._suggestedLinks.has(changedLinkSite) &&
- NewTabUtils.isTopPlacesSite(changedLinkSite) && !linkStored) {
- this._topSitesWithSuggestedLinks.add(changedLinkSite);
- return true;
- }
-
- // always run _updateSuggestedTile if aLink is inadjacent
- // and there are tiles configured to avoid it
- if (this._avoidInadjacentSites && this._isInadjacentLink(aLink)) {
- return true;
- }
-
- return false;
- },
-
- _populatePlacesLinks() {
- NewTabUtils.links.populateProviderCache(NewTabUtils.placesProvider, () => {
- this._handleManyLinksChanged();
- });
- },
-
- onDeleteURI(aProvider, aLink) {
- let {url} = aLink;
- // remove clicked flag for that url and
- // call observer upon disk write completion
- this._removeTileClick(url).then(() => {
- this._callObservers("onDeleteURI", url);
- });
- },
-
- onClearHistory() {
- // remove all clicked flags and call observers upon file write
- this._removeAllTileClicks().then(() => {
- this._callObservers("onClearHistory");
- });
- },
-
- onLinkChanged(aProvider, aLink) {
- // Make sure NewTabUtils.links handles the notification first.
- setTimeout(() => {
- if (this._handleLinkChanged(aLink) || this._shouldUpdateSuggestedTile()) {
- this._updateSuggestedTile();
- }
- }, 0);
- },
-
- onManyLinksChanged() {
- // Make sure NewTabUtils.links handles the notification first.
- setTimeout(() => {
- this._handleManyLinksChanged();
- }, 0);
- },
-
- _getCurrentTopSiteCount() {
- let visibleTopSiteCount = 0;
- let newTabLinks = NewTabUtils.links.getLinks();
- for (let link of newTabLinks.slice(0, MIN_VISIBLE_HISTORY_TILES)) {
- // compute visibleTopSiteCount for suggested tiles
- if (link && (link.type == "history" || link.type == "enhanced")) {
- visibleTopSiteCount++;
- }
- }
- // since newTabLinks are available, set _newTabHasInadjacentSite here
- // note that _shouldUpdateSuggestedTile is called by _updateSuggestedTile
- this._newTabHasInadjacentSite = this._avoidInadjacentSites && this._checkForInadjacentSites(newTabLinks);
-
- return visibleTopSiteCount;
- },
-
- _shouldUpdateSuggestedTile() {
- let sortedLinks = NewTabUtils.getProviderLinks(this);
-
- let mostFrecentLink = {};
- if (sortedLinks && sortedLinks.length) {
- mostFrecentLink = sortedLinks[0]
- }
-
- let currTopSiteCount = this._getCurrentTopSiteCount();
- if ((!mostFrecentLink.targetedSite && currTopSiteCount >= MIN_VISIBLE_HISTORY_TILES) ||
- (mostFrecentLink.targetedSite && currTopSiteCount < MIN_VISIBLE_HISTORY_TILES)) {
- // If mostFrecentLink has a targetedSite then mostFrecentLink is a suggested link.
- // If we have enough history links (8+) to show a suggested tile and we are not
- // already showing one, then we should update (to *attempt* to add a suggested tile).
- // OR if we don't have enough history to show a suggested tile (<8) and we are
- // currently showing one, we should update (to remove it).
- return true;
- }
-
- return false;
- },
-
- /**
- * Chooses and returns a suggested tile based on a user's top sites
- * that we have an available suggested tile for.
- *
- * @return the chosen suggested tile, or undefined if there isn't one
- */
- _updateSuggestedTile() {
- let sortedLinks = NewTabUtils.getProviderLinks(this);
-
- if (!sortedLinks) {
- // If NewTabUtils.links.resetCache() is called before getting here,
- // sortedLinks may be undefined.
- return undefined;
- }
-
- // Delete the current suggested tile, if one exists.
- let initialLength = sortedLinks.length;
- if (initialLength) {
- let mostFrecentLink = sortedLinks[0];
- if (mostFrecentLink.targetedSite) {
- this._callObservers("onLinkChanged", {
- url: mostFrecentLink.url,
- frecency: SUGGESTED_FRECENCY,
- lastVisitDate: mostFrecentLink.lastVisitDate,
- type: mostFrecentLink.type,
- }, 0, true);
- }
- }
-
- if (this._topSitesWithSuggestedLinks.size == 0 ||
- !this._shouldUpdateSuggestedTile() ||
- this._isSuggestedTileBlocked()) {
- // There are no potential suggested links we can show or not
- // enough history for a suggested tile, or suggested tile was
- // recently blocked and wait time interval has not decayed yet
- return undefined;
- }
-
- // 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
- // random from flattenedLinks if it appears only once.
- let nextTimeout;
- let possibleLinks = new Map();
- let targetedSites = new Map();
- this._topSitesWithSuggestedLinks.forEach(topSiteWithSuggestedLink => {
- let suggestedLinksMap = this._suggestedLinks.get(topSiteWithSuggestedLink);
- suggestedLinksMap.forEach((suggestedLink, url) => {
- // Skip this link if we've shown it too many times already
- if (!this._testFrequencyCapLimits(url)) {
- return;
- }
-
- // as we iterate suggestedLinks, check for campaign start/end
- // time limits, and set nextTimeout to the closest timestamp
- let {use, timeoutDate} = this._testLinkForCampaignTimeLimits(suggestedLink);
- // update nextTimeout is necessary
- if (timeoutDate && (!nextTimeout || nextTimeout > timeoutDate)) {
- nextTimeout = timeoutDate;
- }
- // Skip link if it falls outside campaign time limits
- if (!use) {
- return;
- }
-
- // Skip link if it avoids inadjacent sites and newtab has one
- if (suggestedLink.check_inadjacency && this._newTabHasInadjacentSite) {
- return;
- }
-
- possibleLinks.set(url, suggestedLink);
-
- // 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, []);
- }
- targetedSites.get(url).push(topSiteWithSuggestedLink);
- })
- });
-
- // setup timeout check for starting or ending campaigns
- if (nextTimeout) {
- this._setupCampaignTimeCheck(nextTimeout - Date.now());
- }
-
- // We might have run out of possible links to show
- let numLinks = possibleLinks.size;
- if (numLinks == 0) {
- return undefined;
- }
-
- let flattenedLinks = [...possibleLinks.values()];
-
- // Choose our suggested link at random
- let suggestedIndex = Math.floor(Math.random() * numLinks);
- let chosenSuggestedLink = flattenedLinks[suggestedIndex];
-
- // Add the suggested link to the front with some extra values
- this._callObservers("onLinkChanged", Object.assign({
- frecency: SUGGESTED_FRECENCY,
-
- // 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.
- targetedSite: targetedSites.get(chosenSuggestedLink.url).length ?
- targetedSites.get(chosenSuggestedLink.url)[0] : null
- }, chosenSuggestedLink));
- return chosenSuggestedLink;
- },
-
- /**
- * Loads inadjacent sites
- * @return a promise resolved when lookup Set for sites is built
- */
- _loadInadjacentSites: function DirectoryLinksProvider_loadInadjacentSites() {
- return this._downloadJsonData(this._inadjacentSitesUrl).then(jsonString => {
- let jsonObject = {};
- try {
- jsonObject = JSON.parse(jsonString);
- } catch (e) {
- Cu.reportError(e);
- }
-
- this._inadjacentSites = new Set(jsonObject.domains);
- });
- },
-
- /**
- * Genegrates hash suitable for looking up inadjacent site
- * @param value to hsh
- * @return hased value, base64-ed
- */
- _generateHash: function DirectoryLinksProvider_generateHash(value) {
- let byteArr = gUnicodeConverter.convertToByteArray(value);
- gCryptoHash.init(gCryptoHash.MD5);
- gCryptoHash.update(byteArr, byteArr.length);
- return gCryptoHash.finish(true);
- },
-
- /**
- * Checks if link belongs to inadjacent domain
- * @param link to check
- * @return true for inadjacent domains, false otherwise
- */
- _isInadjacentLink: function DirectoryLinksProvider_isInadjacentLink(link) {
- let baseDomain = link.baseDomain || NewTabUtils.extractSite(link.url || "");
- if (!baseDomain) {
- return false;
- }
- // check if hashed domain is inadjacent
- return this._inadjacentSites.has(this._generateHash(baseDomain));
- },
-
- /**
- * Checks if new tab has inadjacent site
- * @param new tab links (or nothing, in which case NewTabUtils.links.getLinks() is called
- * @return true if new tab shows has inadjacent site
- */
- _checkForInadjacentSites: function DirectoryLinksProvider_checkForInadjacentSites(newTabLink) {
- let links = newTabLink || NewTabUtils.links.getLinks();
- for (let link of links.slice(0, MAX_VISIBLE_HISTORY_TILES)) {
- // check links against inadjacent list - specifically include ALL link types
- if (this._isInadjacentLink(link)) {
- return true;
- }
- }
- return false;
- },
-
- /**
- * Reads json file, parses its content, and returns resulting object
- * @param json file path
- * @param json object to return in case file read or parse fails
- * @return a promise resolved to a valid object or undefined upon error
- */
- _readJsonFile: Task.async(function* (filePath, nullObject) {
- let jsonObj;
- try {
- let binaryData = yield OS.File.read(filePath);
- let json = gTextDecoder.decode(binaryData);
- jsonObj = JSON.parse(json);
- } catch (e) {}
- return jsonObj || nullObject;
- }),
-
- /**
- * Loads frequency cap object from file and parses its content
- * @return a promise resolved upon load completion
- * on error or non-exstent file _frequencyCaps is set to empty object
- */
- _readFrequencyCapFile: Task.async(function* () {
- // set _frequencyCaps object to file's content or empty object
- this._frequencyCaps = yield this._readJsonFile(this._frequencyCapFilePath, {});
- }),
-
- /**
- * Saves frequency cap object to file
- * @return a promise resolved upon file i/o completion
- */
- _writeFrequencyCapFile: function DirectoryLinksProvider_writeFrequencyCapFile() {
- let json = JSON.stringify(this._frequencyCaps || {});
- return OS.File.writeAtomic(this._frequencyCapFilePath, json, {tmpPath: this._frequencyCapFilePath + ".tmp"});
- },
-
- /**
- * Clears frequency cap object and writes empty json to file
- * @return a promise resolved upon file i/o completion
- */
- _clearFrequencyCap: function DirectoryLinksProvider_clearFrequencyCap() {
- this._frequencyCaps = {};
- return this._writeFrequencyCapFile();
- },
-
- /**
- * updates frequency cap configuration for a link
- */
- _updateFrequencyCapSettings: function DirectoryLinksProvider_updateFrequencyCapSettings(link) {
- let capsObject = this._frequencyCaps[link.url];
- if (!capsObject) {
- // create an object with empty counts
- capsObject = {
- dailyViews: 0,
- totalViews: 0,
- lastShownDate: 0,
- };
- this._frequencyCaps[link.url] = capsObject;
- }
- // set last updated timestamp
- capsObject.lastUpdated = Date.now();
- // check for link configuration
- if (link.frequency_caps) {
- capsObject.dailyCap = link.frequency_caps.daily || DEFAULT_DAILY_FREQUENCY_CAP;
- capsObject.totalCap = link.frequency_caps.total || DEFAULT_TOTAL_FREQUENCY_CAP;
- } else {
- // fallback to defaults
- capsObject.dailyCap = DEFAULT_DAILY_FREQUENCY_CAP;
- capsObject.totalCap = DEFAULT_TOTAL_FREQUENCY_CAP;
- }
- },
-
- /**
- * Prunes frequency cap objects for outdated links
- * @param timeDetla milliseconds
- * all cap objects with lastUpdated less than (now() - timeDelta)
- * will be removed. This is done to remove frequency cap objects
- * for unused tile urls
- */
- _pruneFrequencyCapUrls: function DirectoryLinksProvider_pruneFrequencyCapUrls(timeDelta = DEFAULT_PRUNE_TIME_DELTA) {
- let timeThreshold = Date.now() - timeDelta;
- Object.keys(this._frequencyCaps).forEach(url => {
- // remove url if it is not ignorable and wasn't updated for a while
- if (!url.startsWith("ignore") && this._frequencyCaps[url].lastUpdated <= timeThreshold) {
- delete this._frequencyCaps[url];
- }
- });
- },
-
- /**
- * Checks if supplied timestamp happened today
- * @param timestamp in milliseconds
- * @return true if the timestamp was made today, false otherwise
- */
- _wasToday: function DirectoryLinksProvider_wasToday(timestamp) {
- let showOn = new Date(timestamp);
- let today = new Date();
- // call timestamps identical if both day and month are same
- return showOn.getDate() == today.getDate() &&
- showOn.getMonth() == today.getMonth() &&
- showOn.getYear() == today.getYear();
- },
-
- /**
- * adds some number of views for a url
- * @param url String url of the suggested link
- */
- _addFrequencyCapView: function DirectoryLinksProvider_addFrequencyCapView(url) {
- let capObject = this._frequencyCaps[url];
- // sanity check
- if (!capObject) {
- return;
- }
-
- // if the day is new: reset the daily counter and lastShownDate
- if (!this._wasToday(capObject.lastShownDate)) {
- capObject.dailyViews = 0;
- // update lastShownDate
- capObject.lastShownDate = Date.now();
- }
-
- // bump both daily and total counters
- capObject.totalViews++;
- capObject.dailyViews++;
-
- // if any of the caps is reached - update suggested tiles
- if (capObject.totalViews >= capObject.totalCap ||
- capObject.dailyViews >= capObject.dailyCap) {
- this._updateSuggestedTile();
- }
- },
-
- /**
- * Sets clicked flag for link url
- * @param url String url of the suggested link
- */
- _setFrequencyCapClick(url) {
- let capObject = this._frequencyCaps[url];
- // sanity check
- if (!capObject) {
- return;
- }
- capObject.clicked = true;
- // and update suggested tiles, since current tile became invalid
- this._updateSuggestedTile();
- },
-
- /**
- * Tests frequency cap limits for link url
- * @param url String url of the suggested link
- * @return true if link is viewable, false otherwise
- */
- _testFrequencyCapLimits: function DirectoryLinksProvider_testFrequencyCapLimits(url) {
- let capObject = this._frequencyCaps[url];
- // sanity check: if url is missing - do not show this tile
- if (!capObject) {
- return false;
- }
-
- // check for clicked set or total views reached
- if (capObject.clicked || capObject.totalViews >= capObject.totalCap) {
- return false;
- }
-
- // otherwise check if link is over daily views limit
- if (this._wasToday(capObject.lastShownDate) &&
- capObject.dailyViews >= capObject.dailyCap) {
- return false;
- }
-
- // we passed all cap tests: return true
- return true;
- },
-
- /**
- * Removes clicked flag from frequency cap entry for tile landing url
- * @param url String url of the suggested link
- * @return promise resolved upon disk write completion
- */
- _removeTileClick: function DirectoryLinksProvider_removeTileClick(url = "") {
- // remove trailing slash, to accomodate Places sending site urls ending with '/'
- let noTrailingSlashUrl = url.replace(/\/$/, "");
- let capObject = this._frequencyCaps[url] || this._frequencyCaps[noTrailingSlashUrl];
- // return resolved promise if capObject is not found
- if (!capObject) {
- return Promise.resolve();
- }
- // otherwise remove clicked flag
- delete capObject.clicked;
- return this._writeFrequencyCapFile();
- },
-
- /**
- * Removes all clicked flags from frequency cap object
- * @return promise resolved upon disk write completion
- */
- _removeAllTileClicks: function DirectoryLinksProvider_removeAllTileClicks() {
- Object.keys(this._frequencyCaps).forEach(url => {
- delete this._frequencyCaps[url].clicked;
- });
- return this._writeFrequencyCapFile();
- },
-
- /**
- * Return the object to its pre-init state
- */
- reset: function DirectoryLinksProvider_reset() {
- delete this.__linksURL;
- this._removePrefsObserver();
- this._removeObservers();
- },
-
- addObserver: function DirectoryLinksProvider_addObserver(aObserver) {
- this._observers.add(aObserver);
- },
-
- removeObserver: function DirectoryLinksProvider_removeObserver(aObserver) {
- this._observers.delete(aObserver);
- },
-
- _callObservers(methodName, ...args) {
- for (let obs of this._observers) {
- if (typeof(obs[methodName]) == "function") {
- try {
- obs[methodName](this, ...args);
- } catch (err) {
- Cu.reportError(err);
- }
- }
- }
- },
-
- _removeObservers() {
- this._observers.clear();
- }
-};