Bug 937820 - Create Snippets XPCOM component. r=bnicholson

This commit is contained in:
Margaret Leibovic 2013-11-22 16:55:33 -08:00
parent 93cc55206d
commit 7c0a8fca8c
5 changed files with 255 additions and 0 deletions

View File

@ -788,3 +788,17 @@ pref("browser.ui.linkify.phone", false);
// Enables/disables Spatial Navigation
pref("snav.enabled", true);
// This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
// this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
// repackager of this code using an alternate snippet url, please keep your users safe
pref("browser.snippets.updateUrl", "https://snippets.mozilla.com/json/%SNIPPETS_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
// How frequently we check for new snippets, in seconds (1 day)
pref("browser.snippets.updateInterval", 86400);
// URL used to check for user's country code
pref("browser.snippets.geoUrl", "https://geo.mozilla.org/country.json");
// This pref requires a restart to take effect.
pref("browser.snippets.enabled", false);

View File

@ -99,3 +99,9 @@ contract @mozilla.org/payment/ui-glue;1 {3c6c9575-f57e-427b-a8aa-57bc3cbff48f}
# FilePicker.js
component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js
contract @mozilla.org/filepicker;1 {18a4e042-7c7c-424b-a583-354e68553a7f}
# Snippets.js
component {a78d7e59-b558-4321-a3d6-dffe2f1e76dd} Snippets.js
contract @mozilla.org/snippets;1 {a78d7e59-b558-4321-a3d6-dffe2f1e76dd}
category profile-after-change Snippets @mozilla.org/snippets;1
category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400

View File

@ -0,0 +1,233 @@
/* 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/. */
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Home", "resource://gre/modules/Home.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { return new gChromeWin.TextEncoder(); });
XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { return new gChromeWin.TextDecoder(); });
const SNIPPETS_ENABLED = Services.prefs.getBoolPref("browser.snippets.enabled");
// URL to fetch snippets, in the urlFormatter service format.
const SNIPPETS_UPDATE_URL_PREF = "browser.snippets.updateUrl";
// URL to fetch country code, a value that's cached and refreshed once per month.
const SNIPPETS_GEO_URL_PREF = "browser.snippets.geoUrl";
// Timestamp when we last updated the user's country code.
const SNIPPETS_GEO_LAST_UPDATE_PREF = "browser.snippets.geoLastUpdate";
// Pref where we'll cache the user's country.
const SNIPPETS_COUNTRY_CODE_PREF = "browser.snippets.countryCode";
// How frequently we update the user's country code from the server (30 days).
const SNIPPETS_GEO_UPDATE_INTERVAL_MS = 86400000*30;
// Should be bumped up if the snippets content format changes.
const SNIPPETS_VERSION = 1;
XPCOMUtils.defineLazyGetter(this, "gSnippetsURL", function() {
let updateURL = Services.prefs.getCharPref(SNIPPETS_UPDATE_URL_PREF).replace("%SNIPPETS_VERSION%", SNIPPETS_VERSION);
return Services.urlFormatter.formatURL(updateURL);
});
XPCOMUtils.defineLazyGetter(this, "gGeoURL", function() {
return Services.prefs.getCharPref(SNIPPETS_GEO_URL_PREF);
});
XPCOMUtils.defineLazyGetter(this, "gCountryCode", function() {
try {
return Services.prefs.getCharPref(SNIPPETS_COUNTRY_CODE_PREF);
} catch (e) {
// Return an empty string if the country code pref isn't set yet.
return "";
}
});
XPCOMUtils.defineLazyGetter(this, "gChromeWin", function() {
return Services.wm.getMostRecentWindow("navigator:browser");
});
/**
* Updates snippet data and country code (if necessary).
*/
function update() {
// Check to see if we should update the user's country code from the geo server.
let lastUpdate = 0;
try {
lastUpdate = parseFloat(Services.prefs.getCharPref(SNIPPETS_GEO_LAST_UPDATE_PREF));
} catch (e) {}
if (Date.now() - lastUpdate > SNIPPETS_GEO_UPDATE_INTERVAL_MS) {
// We should update the snippets after updating the country code,
// so that we can filter snippets to add to the banner.
updateCountryCode(updateSnippets);
} else {
updateSnippets();
}
}
/**
* Fetches the user's country code from the geo server and stores the value in a pref.
*
* @param callback function called once country code is updated
*/
function updateCountryCode(callback) {
_httpGetRequest(gGeoURL, function(responseText) {
// Store the country code in a pref.
let data = JSON.parse(responseText);
Services.prefs.setCharPref(SNIPPETS_COUNTRY_CODE_PREF, data.country_code);
// Set last update time.
Services.prefs.setCharPref(SNIPPETS_GEO_LAST_UPDATE_PREF, Date.now());
callback();
});
}
/**
* Loads snippets from snippets server, caches the response, and
* updates the home banner with the new set of snippets.
*/
function updateSnippets() {
_httpGetRequest(gSnippetsURL, function(responseText) {
cacheSnippets(responseText);
updateBanner(responseText);
});
}
/**
* Caches snippets server response text to `snippets.json` in profile directory.
*
* @param response responseText returned from snippets server
*/
function cacheSnippets(response) {
let path = OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
let data = gEncoder.encode(response);
let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp" });
promise.then(null, e => Cu.reportError("Error caching snippets: " + e));
}
/**
* Loads snippets from cached `snippets.json`.
*/
function loadSnippetsFromCache() {
let path = OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
let promise = OS.File.read(path);
promise.then(array => updateBanner(gDecoder.decode(array)), e => {
// If snippets.json doesn't exist, update data from the server.
if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
update();
} else {
Cu.reportError("Error loading snippets from cache: " + e);
}
});
}
// Array of the message ids added to the home banner, used to remove
// older set of snippets when new ones are available.
var gMessageIds = [];
/**
* Updates set of snippets in the home banner message rotation.
*
* @param response responseText returned from snippets server.
* This should be a JSON array of message data JSON objects.
* Each message object should have the following properties:
* - id (?): Unique identifier for this snippets message
* - text (string): Text to show as banner message
* - url (string): URL to open when banner is clicked
* - icon (data URI): Icon to appear in banner
* - target_geo (string): Country code for where this message should be shown (e.g. "US")
*/
function updateBanner(response) {
// Remove the current messages, if there are any.
gMessageIds.forEach(function(id) {
Home.banner.remove(id);
})
gMessageIds = [];
let messages = JSON.parse(response);
messages.forEach(function(message) {
// Don't add this message to the banner if it's not supposed to be shown in this country.
if ("target_geo" in message && message.target_geo != gCountryCode) {
return;
}
let id = Home.banner.add({
text: message.text,
icon: message.icon,
onclick: function() {
gChromeWin.BrowserApp.addTab(message.url);
},
onshown: function() {
// XXX: 10% of the time, let the metrics server know which message was shown (bug 937373)
}
});
// Keep track of the message we added so that we can remove it later.
gMessageIds.push(id);
});
}
/**
* Helper function to make HTTP GET requests.
*
* @param url where we send the request
* @param callback function that is called with the xhr responseText
*/
function _httpGetRequest(url, callback) {
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
try {
xhr.open("GET", url, true);
} catch (e) {
Cu.reportError("Error opening request to " + url + ": " + e);
return;
}
xhr.onerror = function onerror(e) {
Cu.reportError("Error making request to " + url + ": " + e.error);
}
xhr.onload = function onload(event) {
if (xhr.status !== 200) {
Cu.reportError("Request to " + url + " returned status " + xhr.status);
return;
}
if (callback) {
callback(xhr.responseText);
}
}
xhr.send(null);
}
function Snippets() {}
Snippets.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsITimerCallback]),
classID: Components.ID("{a78d7e59-b558-4321-a3d6-dffe2f1e76dd}"),
observe: function(subject, topic, data) {
if (!SNIPPETS_ENABLED) {
return;
}
switch(topic) {
case "profile-after-change":
loadSnippetsFromCache();
break;
}
},
// By default, this timer fires once every 24 hours. See the "browser.snippets.updateInterval" pref.
notify: function(timer) {
if (!SNIPPETS_ENABLED) {
return;
}
update();
}
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Snippets]);

View File

@ -22,6 +22,7 @@ EXTRA_COMPONENTS += [
'PaymentsUI.js',
'PromptService.js',
'SiteSpecificUserAgent.js',
'Snippets.js',
'XPIDialogService.js',
]

View File

@ -571,6 +571,7 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DLL_SUFFIX@
@BINPATH@/components/PromptService.js
@BINPATH@/components/SessionStore.js
@BINPATH@/components/Sidebar.js
@BINPATH@/components/Snippets.js
@BINPATH@/components/Payment.js
@BINPATH@/components/PaymentFlowInfo.js