зеркало из https://github.com/mozilla/gecko-dev.git
286 строки
8.9 KiB
JavaScript
286 строки
8.9 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 = ["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,
|
|
]),
|
|
};
|