michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["UserAgentUpdates"]; michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cc = Components.classes; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter( michael@0: this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter( michael@0: this, "OS", "resource://gre/modules/osfile.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter( michael@0: this, "UpdateChannel", "resource://gre/modules/UpdateChannel.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter( michael@0: this, "gUpdateTimer", "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gApp", michael@0: function() Cc["@mozilla.org/xre/app-info;1"] michael@0: .getService(Ci.nsIXULAppInfo).QueryInterface(Ci.nsIXULRuntime) michael@0: ); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gDecoder", michael@0: function() new TextDecoder() michael@0: ); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "gEncoder", michael@0: function() new TextEncoder() michael@0: ); michael@0: michael@0: const TIMER_ID = "user-agent-updates-timer"; michael@0: michael@0: const PREF_UPDATES = "general.useragent.updates."; michael@0: const PREF_UPDATES_ENABLED = PREF_UPDATES + "enabled"; michael@0: const PREF_UPDATES_URL = PREF_UPDATES + "url"; michael@0: const PREF_UPDATES_INTERVAL = PREF_UPDATES + "interval"; michael@0: const PREF_UPDATES_RETRY = PREF_UPDATES + "retry"; michael@0: const PREF_UPDATES_TIMEOUT = PREF_UPDATES + "timeout"; michael@0: const PREF_UPDATES_LASTUPDATED = PREF_UPDATES + "lastupdated"; michael@0: michael@0: const KEY_PREFDIR = "PrefD"; michael@0: const KEY_APPDIR = "XCurProcD"; michael@0: const FILE_UPDATES = "ua-update.json"; michael@0: michael@0: const PREF_APP_DISTRIBUTION = "distribution.id"; michael@0: const PREF_APP_DISTRIBUTION_VERSION = "distribution.version"; michael@0: michael@0: var gInitialized = false; michael@0: michael@0: this.UserAgentUpdates = { michael@0: init: function(callback) { michael@0: if (gInitialized) { michael@0: return; michael@0: } michael@0: gInitialized = true; michael@0: michael@0: this._callback = callback; michael@0: this._lastUpdated = 0; michael@0: this._applySavedUpdate(); michael@0: michael@0: Services.prefs.addObserver(PREF_UPDATES, this, false); michael@0: }, michael@0: michael@0: uninit: function() { michael@0: if (!gInitialized) { michael@0: return; michael@0: } michael@0: gInitialized = false; michael@0: Services.prefs.removeObserver(PREF_UPDATES, this); michael@0: }, michael@0: michael@0: _applyUpdate: function(update) { michael@0: // Check pref again in case it has changed michael@0: if (update && this._getPref(PREF_UPDATES_ENABLED, false)) { michael@0: this._callback(update); michael@0: } else { michael@0: this._callback(null); michael@0: } michael@0: }, michael@0: michael@0: _applySavedUpdate: function() { michael@0: if (!this._getPref(PREF_UPDATES_ENABLED, false)) { michael@0: // remove previous overrides michael@0: this._applyUpdate(null); michael@0: return; michael@0: } michael@0: // try loading from profile dir, then from app dir michael@0: let dirs = [KEY_PREFDIR, KEY_APPDIR]; michael@0: dirs.reduce((prevLoad, dir) => { michael@0: let file = FileUtils.getFile(dir, [FILE_UPDATES], true).path; michael@0: // tryNext returns promise to read file under dir and parse it michael@0: let tryNext = () => OS.File.read(file).then( michael@0: (bytes) => { michael@0: let update = JSON.parse(gDecoder.decode(bytes)); michael@0: if (!update) { michael@0: throw new Error("invalid update"); michael@0: } michael@0: return update; michael@0: } michael@0: ); michael@0: // try to load next one if the previous load failed michael@0: return prevLoad ? prevLoad.then(null, tryNext) : tryNext(); michael@0: }, null).then( michael@0: // apply update if loading was successful michael@0: (update) => this._applyUpdate(update) michael@0: ); michael@0: this._scheduleUpdate(); michael@0: }, michael@0: michael@0: _saveToFile: function(update) { michael@0: let file = FileUtils.getFile(KEY_PREFDIR, [FILE_UPDATES], true); michael@0: let path = file.path; michael@0: let bytes = gEncoder.encode(JSON.stringify(update)); michael@0: OS.File.writeAtomic(path, bytes, {tmpPath: path + ".tmp"}).then( michael@0: () => { michael@0: this._lastUpdated = Date.now(); michael@0: Services.prefs.setCharPref( michael@0: PREF_UPDATES_LASTUPDATED, this._lastUpdated.toString()); michael@0: }, michael@0: Cu.reportError michael@0: ); michael@0: }, michael@0: michael@0: _getPref: function(name, def) { michael@0: try { michael@0: switch (typeof def) { michael@0: case "number": return Services.prefs.getIntPref(name); michael@0: case "boolean": return Services.prefs.getBoolPref(name); michael@0: } michael@0: return Services.prefs.getCharPref(name); michael@0: } catch (e) { michael@0: return def; michael@0: } michael@0: }, michael@0: michael@0: _getParameters: function() ({ michael@0: "%DATE%": function() Date.now().toString(), michael@0: "%PRODUCT%": function() gApp.name, michael@0: "%APP_ID%": function() gApp.ID, michael@0: "%APP_VERSION%": function() gApp.version, michael@0: "%BUILD_ID%": function() gApp.appBuildID, michael@0: "%OS%": function() gApp.OS, michael@0: "%CHANNEL%": function() UpdateChannel.get(), michael@0: "%DISTRIBUTION%": function() this._getPref(PREF_APP_DISTRIBUTION, ""), michael@0: "%DISTRIBUTION_VERSION%": function() this._getPref(PREF_APP_DISTRIBUTION_VERSION, ""), michael@0: }), michael@0: michael@0: _getUpdateURL: function() { michael@0: let url = this._getPref(PREF_UPDATES_URL, ""); michael@0: let params = this._getParameters(); michael@0: return url.replace(/%[A-Z_]+%/g, function(match) { michael@0: let param = params[match]; michael@0: // preserve the %FOO% string (e.g. as an encoding) if it's not a valid parameter michael@0: return param ? encodeURIComponent(param()) : match; michael@0: }); michael@0: }, michael@0: michael@0: _fetchUpdate: function(url, success, error) { michael@0: let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] michael@0: .createInstance(Ci.nsIXMLHttpRequest); michael@0: request.mozBackgroundRequest = true; michael@0: request.timeout = this._getPref(PREF_UPDATES_TIMEOUT, 60000); michael@0: request.open("GET", url, true); michael@0: request.overrideMimeType("application/json"); michael@0: request.responseType = "json"; michael@0: michael@0: request.addEventListener("load", function() { michael@0: let response = request.response; michael@0: response ? success(response) : error(); michael@0: }); michael@0: request.addEventListener("error", error); michael@0: request.send(); michael@0: }, michael@0: michael@0: _update: function() { michael@0: let url = this._getUpdateURL(); michael@0: url && this._fetchUpdate(url, michael@0: (function(response) { // success michael@0: // apply update and save overrides to profile michael@0: this._applyUpdate(response); michael@0: this._saveToFile(response); michael@0: this._scheduleUpdate(); // cancel any retries michael@0: }).bind(this), michael@0: (function(response) { // error michael@0: this._scheduleUpdate(true /* retry */); michael@0: }).bind(this)); michael@0: }, michael@0: michael@0: _scheduleUpdate: function(retry) { michael@0: // only schedule updates in the main process michael@0: if (gApp.processType !== Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { michael@0: return; michael@0: } michael@0: let interval = this._getPref(PREF_UPDATES_INTERVAL, 604800 /* 1 week */); michael@0: if (retry) { michael@0: interval = this._getPref(PREF_UPDATES_RETRY, interval); michael@0: } michael@0: gUpdateTimer.registerTimer(TIMER_ID, this, Math.max(1, interval)); michael@0: }, michael@0: michael@0: notify: function(timer) { michael@0: // timer notification michael@0: if (this._getPref(PREF_UPDATES_ENABLED, false)) { michael@0: this._update(); michael@0: } michael@0: }, michael@0: michael@0: observe: function(subject, topic, data) { michael@0: switch (topic) { michael@0: case "nsPref:changed": michael@0: if (data === PREF_UPDATES_ENABLED) { michael@0: this._applySavedUpdate(); michael@0: } else if (data === PREF_UPDATES_INTERVAL) { michael@0: this._scheduleUpdate(); michael@0: } else if (data === PREF_UPDATES_LASTUPDATED) { michael@0: // reload from file if there has been an update michael@0: let lastUpdated = parseInt( michael@0: this._getPref(PREF_UPDATES_LASTUPDATED, "0"), 0); michael@0: if (lastUpdated > this._lastUpdated) { michael@0: this._applySavedUpdate(); michael@0: this._lastUpdated = lastUpdated; michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIObserver, michael@0: Ci.nsITimerCallback, michael@0: ]), michael@0: };