зеркало из https://github.com/mozilla/gecko-dev.git
379 строки
12 KiB
JavaScript
379 строки
12 KiB
JavaScript
/* 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 Cu = Components.utils;
|
|
Cu.importGlobalProperties(["XMLHttpRequest"]);
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
|
"resource://gre/modules/osfile.jsm")
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
|
"resource://gre/modules/PromiseUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
|
|
"resource://gre/modules/UpdateUtils.jsm");
|
|
XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => {
|
|
return new TextDecoder();
|
|
});
|
|
|
|
|
|
// The filename where directory links are stored locally
|
|
const DIRECTORY_LINKS_FILE = "directoryLinks.json";
|
|
const DIRECTORY_LINKS_TYPE = "application/json";
|
|
|
|
// The preference that tells where to obtain directory links
|
|
const PREF_DIRECTORY_SOURCE = "browser.newtabpage.directory.source";
|
|
|
|
// 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;
|
|
|
|
/**
|
|
* 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,
|
|
|
|
get _observedPrefs() {
|
|
return Object.freeze({
|
|
enhanced: PREF_NEWTAB_ENHANCED,
|
|
linksURL: PREF_DIRECTORY_SOURCE,
|
|
});
|
|
},
|
|
|
|
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() {
|
|
return Services.locale.getRequestedLocale() || "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;
|
|
this._fetchAndCacheLinksIfNecessary(true);
|
|
break;
|
|
}
|
|
} else if (aTopic === "intl:requested-locales-changed") {
|
|
this._fetchAndCacheLinksIfNecessary(true);
|
|
}
|
|
},
|
|
|
|
_addPrefsObserver: function DirectoryLinksProvider_addObserver() {
|
|
for (let pref in this._observedPrefs) {
|
|
let prefName = this._observedPrefs[pref];
|
|
Services.prefs.addObserver(prefName, this);
|
|
}
|
|
},
|
|
|
|
_removePrefsObserver: function DirectoryLinksProvider_removeObserver() {
|
|
for (let pref in this._observedPrefs) {
|
|
let prefName = this._observedPrefs[pref];
|
|
Services.prefs.removeObserver(prefName, this);
|
|
}
|
|
},
|
|
|
|
_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) {
|
|
return new Promise((resolve, reject) => {
|
|
let xmlHttp = this._newXHR();
|
|
|
|
xmlHttp.onload = function(aResponse) {
|
|
let json = this.responseText;
|
|
if (this.status && this.status != 200) {
|
|
json = "{}";
|
|
}
|
|
resolve(json);
|
|
};
|
|
|
|
xmlHttp.onerror = function(e) {
|
|
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) {
|
|
reject("Error fetching " + uri);
|
|
Cu.reportError(e);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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 = PromiseUtils.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: []};
|
|
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 || []};
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
}
|
|
return output || emptyOutput;
|
|
},
|
|
error => {
|
|
Cu.reportError(error);
|
|
return emptyOutput;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* 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));
|
|
},
|
|
|
|
/**
|
|
* 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 => {
|
|
// Only check base domain for images when using the default pref
|
|
let checkBase = !this.__linksURLModified;
|
|
let validityFilter = 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));
|
|
};
|
|
|
|
let links = rawLinks.directory.filter(validityFilter).map((link, position) => {
|
|
link.lastVisitDate = rawLinks.directory.length - position;
|
|
link.frecency = DIRECTORY_FRECENCY;
|
|
return link;
|
|
});
|
|
|
|
return links;
|
|
}).catch(ex => {
|
|
Cu.reportError(ex);
|
|
return [];
|
|
}).then(links => {
|
|
aCallback(links);
|
|
});
|
|
},
|
|
|
|
init: function DirectoryLinksProvider_init() {
|
|
this._setDefaultEnhanced();
|
|
this._addPrefsObserver();
|
|
Services.obs.addObserver(this, "intl:requested-locales-changed");
|
|
// setup directory file path and last download timestamp
|
|
this._directoryFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, DIRECTORY_LINKS_FILE);
|
|
this._lastDownloadMS = 0;
|
|
|
|
return (async () => {
|
|
// get the last modified time of the links file if it exists
|
|
let doesFileExists = await OS.File.exists(this._directoryFilePath);
|
|
if (doesFileExists) {
|
|
let fileInfo = await OS.File.stat(this._directoryFilePath);
|
|
this._lastDownloadMS = Date.parse(fileInfo.lastModificationDate);
|
|
}
|
|
// fetch directory on startup without force
|
|
await this._fetchAndCacheLinksIfNecessary();
|
|
})();
|
|
},
|
|
|
|
/**
|
|
* Return the object to its pre-init state
|
|
*/
|
|
reset: function DirectoryLinksProvider_reset() {
|
|
delete this.__linksURL;
|
|
this._removePrefsObserver();
|
|
this._removeObservers();
|
|
Services.obs.removeObserver(this, "intl:requested-locales-changed");
|
|
},
|
|
|
|
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();
|
|
}
|
|
};
|