summaryrefslogtreecommitdiffstats
path: root/netwerk/protocol/http/UserAgentUpdates.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/protocol/http/UserAgentUpdates.jsm')
-rw-r--r--netwerk/protocol/http/UserAgentUpdates.jsm285
1 files changed, 285 insertions, 0 deletions
diff --git a/netwerk/protocol/http/UserAgentUpdates.jsm b/netwerk/protocol/http/UserAgentUpdates.jsm
new file mode 100644
index 000000000..602705ebe
--- /dev/null
+++ b/netwerk/protocol/http/UserAgentUpdates.jsm
@@ -0,0 +1,285 @@
+/* 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 = ["UserAgentUpdates"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(
+ this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(
+ this, "NetUtil", "resource://gre/modules/NetUtil.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, "gUpdateTimer", "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
+
+XPCOMUtils.defineLazyGetter(this, "gApp",
+ function() {
+ return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo)
+ .QueryInterface(Ci.nsIXULRuntime);
+ });
+
+XPCOMUtils.defineLazyGetter(this, "gDecoder",
+ function() { return new TextDecoder(); }
+);
+
+XPCOMUtils.defineLazyGetter(this, "gEncoder",
+ function() { return new TextEncoder(); }
+);
+
+const TIMER_ID = "user-agent-updates-timer";
+
+const PREF_UPDATES = "general.useragent.updates.";
+const PREF_UPDATES_ENABLED = PREF_UPDATES + "enabled";
+const PREF_UPDATES_URL = PREF_UPDATES + "url";
+const PREF_UPDATES_INTERVAL = PREF_UPDATES + "interval";
+const PREF_UPDATES_RETRY = PREF_UPDATES + "retry";
+const PREF_UPDATES_TIMEOUT = PREF_UPDATES + "timeout";
+const PREF_UPDATES_LASTUPDATED = PREF_UPDATES + "lastupdated";
+
+const KEY_PREFDIR = "PrefD";
+const KEY_APPDIR = "XCurProcD";
+const FILE_UPDATES = "ua-update.json";
+
+const PREF_APP_DISTRIBUTION = "distribution.id";
+const PREF_APP_DISTRIBUTION_VERSION = "distribution.version";
+
+var gInitialized = false;
+
+function readChannel(url) {
+ return new Promise((resolve, reject) => {
+ try {
+ let channel = NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true});
+ channel.contentType = "application/json";
+
+ NetUtil.asyncFetch(channel, (inputStream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ reject();
+ return;
+ }
+
+ let data = JSON.parse(
+ NetUtil.readInputStreamToString(inputStream, inputStream.available())
+ );
+ resolve(data);
+ });
+ } catch (ex) {
+ reject(new Error("UserAgentUpdates: Could not fetch " + url + " " +
+ ex + "\n" + ex.stack));
+ }
+ });
+}
+
+this.UserAgentUpdates = {
+ init: function(callback) {
+ if (gInitialized) {
+ return;
+ }
+ gInitialized = true;
+
+ this._callback = callback;
+ this._lastUpdated = 0;
+ this._applySavedUpdate();
+
+ Services.prefs.addObserver(PREF_UPDATES, this, false);
+ },
+
+ uninit: function() {
+ if (!gInitialized) {
+ return;
+ }
+ gInitialized = false;
+ Services.prefs.removeObserver(PREF_UPDATES, this);
+ },
+
+ _applyUpdate: function(update) {
+ // Check pref again in case it has changed
+ if (update && this._getPref(PREF_UPDATES_ENABLED, false)) {
+ this._callback(update);
+ } else {
+ this._callback(null);
+ }
+ },
+
+ _applySavedUpdate: function() {
+ if (!this._getPref(PREF_UPDATES_ENABLED, false)) {
+ // remove previous overrides
+ this._applyUpdate(null);
+ return;
+ }
+ // try loading from profile dir, then from app dir
+ let dirs = [KEY_PREFDIR, KEY_APPDIR];
+
+ dirs.reduce((prevLoad, dir) => {
+ let file = FileUtils.getFile(dir, [FILE_UPDATES], true).path;
+ // tryNext returns promise to read file under dir and parse it
+ let tryNext = () => OS.File.read(file).then(
+ (bytes) => {
+ let update = JSON.parse(gDecoder.decode(bytes));
+ if (!update) {
+ throw new Error("invalid update");
+ }
+ return update;
+ }
+ );
+ // try to load next one if the previous load failed
+ return prevLoad ? prevLoad.then(null, tryNext) : tryNext();
+ }, null).then(null, (ex) => {
+ if (AppConstants.platform !== "android") {
+ // All previous (non-Android) load attempts have failed, so we bail.
+ throw new Error("UserAgentUpdates: Failed to load " + FILE_UPDATES +
+ ex + "\n" + ex.stack);
+ }
+ // Make one last attempt to read from the Fennec APK root.
+ return readChannel("resource://android/" + FILE_UPDATES);
+ }).then((update) => {
+ // Apply update if loading was successful
+ this._applyUpdate(update);
+ }).catch(Cu.reportError);
+ this._scheduleUpdate();
+ },
+
+ _saveToFile: function(update) {
+ let file = FileUtils.getFile(KEY_PREFDIR, [FILE_UPDATES], true);
+ let path = file.path;
+ let bytes = gEncoder.encode(JSON.stringify(update));
+ OS.File.writeAtomic(path, bytes, {tmpPath: path + ".tmp"}).then(
+ () => {
+ this._lastUpdated = Date.now();
+ Services.prefs.setCharPref(
+ PREF_UPDATES_LASTUPDATED, this._lastUpdated.toString());
+ },
+ Cu.reportError
+ );
+ },
+
+ _getPref: function(name, def) {
+ try {
+ switch (typeof def) {
+ case "number": return Services.prefs.getIntPref(name);
+ case "boolean": return Services.prefs.getBoolPref(name);
+ }
+ return Services.prefs.getCharPref(name);
+ } catch (e) {
+ return def;
+ }
+ },
+
+ _getParameters() {
+ return {
+ "%DATE%": function() { return Date.now().toString(); },
+ "%PRODUCT%": function() { return gApp.name; },
+ "%APP_ID%": function() { return gApp.ID; },
+ "%APP_VERSION%": function() { return gApp.version; },
+ "%BUILD_ID%": function() { return gApp.appBuildID; },
+ "%OS%": function() { return gApp.OS; },
+ "%CHANNEL%": function() { return UpdateUtils.UpdateChannel; },
+ "%DISTRIBUTION%": function() { return this._getPref(PREF_APP_DISTRIBUTION, ""); },
+ "%DISTRIBUTION_VERSION%": function() { return this._getPref(PREF_APP_DISTRIBUTION_VERSION, ""); },
+ };
+ },
+
+ _getUpdateURL: function() {
+ let url = this._getPref(PREF_UPDATES_URL, "");
+ let params = this._getParameters();
+ return url.replace(/%[A-Z_]+%/g, function(match) {
+ let param = params[match];
+ // preserve the %FOO% string (e.g. as an encoding) if it's not a valid parameter
+ return param ? encodeURIComponent(param()) : match;
+ });
+ },
+
+ _fetchUpdate: function(url, success, error) {
+ let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+ request.mozBackgroundRequest = true;
+ request.timeout = this._getPref(PREF_UPDATES_TIMEOUT, 60000);
+ request.open("GET", url, true);
+ request.overrideMimeType("application/json");
+ request.responseType = "json";
+
+ request.addEventListener("load", function() {
+ let response = request.response;
+ response ? success(response) : error();
+ });
+ request.addEventListener("error", error);
+ request.send();
+ },
+
+ _update: function() {
+ let url = this._getUpdateURL();
+ url && this._fetchUpdate(url,
+ (function(response) { // success
+ // apply update and save overrides to profile
+ this._applyUpdate(response);
+ this._saveToFile(response);
+ this._scheduleUpdate(); // cancel any retries
+ }).bind(this),
+ (function(response) { // error
+ this._scheduleUpdate(true /* retry */);
+ }).bind(this));
+ },
+
+ _scheduleUpdate: function(retry) {
+ // only schedule updates in the main process
+ if (gApp.processType !== Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ return;
+ }
+ let interval = this._getPref(PREF_UPDATES_INTERVAL, 604800 /* 1 week */);
+ if (retry) {
+ interval = this._getPref(PREF_UPDATES_RETRY, interval);
+ }
+ gUpdateTimer.registerTimer(TIMER_ID, this, Math.max(1, interval));
+ },
+
+ notify: function(timer) {
+ // timer notification
+ if (this._getPref(PREF_UPDATES_ENABLED, false)) {
+ this._update();
+ }
+ },
+
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ case "nsPref:changed":
+ if (data === PREF_UPDATES_ENABLED) {
+ this._applySavedUpdate();
+ } else if (data === PREF_UPDATES_INTERVAL) {
+ this._scheduleUpdate();
+ } else if (data === PREF_UPDATES_LASTUPDATED) {
+ // reload from file if there has been an update
+ let lastUpdated = parseInt(
+ this._getPref(PREF_UPDATES_LASTUPDATED, "0"), 0);
+ if (lastUpdated > this._lastUpdated) {
+ this._applySavedUpdate();
+ this._lastUpdated = lastUpdated;
+ }
+ }
+ break;
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsITimerCallback,
+ ]),
+};