1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/netwerk/protocol/http/UserAgentUpdates.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,242 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["UserAgentUpdates"]; 1.11 + 1.12 +const Ci = Components.interfaces; 1.13 +const Cc = Components.classes; 1.14 +const Cu = Components.utils; 1.15 + 1.16 +Cu.import("resource://gre/modules/Services.jsm"); 1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 + 1.19 +XPCOMUtils.defineLazyModuleGetter( 1.20 + this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); 1.21 + 1.22 +XPCOMUtils.defineLazyModuleGetter( 1.23 + this, "OS", "resource://gre/modules/osfile.jsm"); 1.24 + 1.25 +XPCOMUtils.defineLazyModuleGetter( 1.26 + this, "UpdateChannel", "resource://gre/modules/UpdateChannel.jsm"); 1.27 + 1.28 +XPCOMUtils.defineLazyServiceGetter( 1.29 + this, "gUpdateTimer", "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager"); 1.30 + 1.31 +XPCOMUtils.defineLazyGetter(this, "gApp", 1.32 + function() Cc["@mozilla.org/xre/app-info;1"] 1.33 + .getService(Ci.nsIXULAppInfo).QueryInterface(Ci.nsIXULRuntime) 1.34 +); 1.35 + 1.36 +XPCOMUtils.defineLazyGetter(this, "gDecoder", 1.37 + function() new TextDecoder() 1.38 +); 1.39 + 1.40 +XPCOMUtils.defineLazyGetter(this, "gEncoder", 1.41 + function() new TextEncoder() 1.42 +); 1.43 + 1.44 +const TIMER_ID = "user-agent-updates-timer"; 1.45 + 1.46 +const PREF_UPDATES = "general.useragent.updates."; 1.47 +const PREF_UPDATES_ENABLED = PREF_UPDATES + "enabled"; 1.48 +const PREF_UPDATES_URL = PREF_UPDATES + "url"; 1.49 +const PREF_UPDATES_INTERVAL = PREF_UPDATES + "interval"; 1.50 +const PREF_UPDATES_RETRY = PREF_UPDATES + "retry"; 1.51 +const PREF_UPDATES_TIMEOUT = PREF_UPDATES + "timeout"; 1.52 +const PREF_UPDATES_LASTUPDATED = PREF_UPDATES + "lastupdated"; 1.53 + 1.54 +const KEY_PREFDIR = "PrefD"; 1.55 +const KEY_APPDIR = "XCurProcD"; 1.56 +const FILE_UPDATES = "ua-update.json"; 1.57 + 1.58 +const PREF_APP_DISTRIBUTION = "distribution.id"; 1.59 +const PREF_APP_DISTRIBUTION_VERSION = "distribution.version"; 1.60 + 1.61 +var gInitialized = false; 1.62 + 1.63 +this.UserAgentUpdates = { 1.64 + init: function(callback) { 1.65 + if (gInitialized) { 1.66 + return; 1.67 + } 1.68 + gInitialized = true; 1.69 + 1.70 + this._callback = callback; 1.71 + this._lastUpdated = 0; 1.72 + this._applySavedUpdate(); 1.73 + 1.74 + Services.prefs.addObserver(PREF_UPDATES, this, false); 1.75 + }, 1.76 + 1.77 + uninit: function() { 1.78 + if (!gInitialized) { 1.79 + return; 1.80 + } 1.81 + gInitialized = false; 1.82 + Services.prefs.removeObserver(PREF_UPDATES, this); 1.83 + }, 1.84 + 1.85 + _applyUpdate: function(update) { 1.86 + // Check pref again in case it has changed 1.87 + if (update && this._getPref(PREF_UPDATES_ENABLED, false)) { 1.88 + this._callback(update); 1.89 + } else { 1.90 + this._callback(null); 1.91 + } 1.92 + }, 1.93 + 1.94 + _applySavedUpdate: function() { 1.95 + if (!this._getPref(PREF_UPDATES_ENABLED, false)) { 1.96 + // remove previous overrides 1.97 + this._applyUpdate(null); 1.98 + return; 1.99 + } 1.100 + // try loading from profile dir, then from app dir 1.101 + let dirs = [KEY_PREFDIR, KEY_APPDIR]; 1.102 + dirs.reduce((prevLoad, dir) => { 1.103 + let file = FileUtils.getFile(dir, [FILE_UPDATES], true).path; 1.104 + // tryNext returns promise to read file under dir and parse it 1.105 + let tryNext = () => OS.File.read(file).then( 1.106 + (bytes) => { 1.107 + let update = JSON.parse(gDecoder.decode(bytes)); 1.108 + if (!update) { 1.109 + throw new Error("invalid update"); 1.110 + } 1.111 + return update; 1.112 + } 1.113 + ); 1.114 + // try to load next one if the previous load failed 1.115 + return prevLoad ? prevLoad.then(null, tryNext) : tryNext(); 1.116 + }, null).then( 1.117 + // apply update if loading was successful 1.118 + (update) => this._applyUpdate(update) 1.119 + ); 1.120 + this._scheduleUpdate(); 1.121 + }, 1.122 + 1.123 + _saveToFile: function(update) { 1.124 + let file = FileUtils.getFile(KEY_PREFDIR, [FILE_UPDATES], true); 1.125 + let path = file.path; 1.126 + let bytes = gEncoder.encode(JSON.stringify(update)); 1.127 + OS.File.writeAtomic(path, bytes, {tmpPath: path + ".tmp"}).then( 1.128 + () => { 1.129 + this._lastUpdated = Date.now(); 1.130 + Services.prefs.setCharPref( 1.131 + PREF_UPDATES_LASTUPDATED, this._lastUpdated.toString()); 1.132 + }, 1.133 + Cu.reportError 1.134 + ); 1.135 + }, 1.136 + 1.137 + _getPref: function(name, def) { 1.138 + try { 1.139 + switch (typeof def) { 1.140 + case "number": return Services.prefs.getIntPref(name); 1.141 + case "boolean": return Services.prefs.getBoolPref(name); 1.142 + } 1.143 + return Services.prefs.getCharPref(name); 1.144 + } catch (e) { 1.145 + return def; 1.146 + } 1.147 + }, 1.148 + 1.149 + _getParameters: function() ({ 1.150 + "%DATE%": function() Date.now().toString(), 1.151 + "%PRODUCT%": function() gApp.name, 1.152 + "%APP_ID%": function() gApp.ID, 1.153 + "%APP_VERSION%": function() gApp.version, 1.154 + "%BUILD_ID%": function() gApp.appBuildID, 1.155 + "%OS%": function() gApp.OS, 1.156 + "%CHANNEL%": function() UpdateChannel.get(), 1.157 + "%DISTRIBUTION%": function() this._getPref(PREF_APP_DISTRIBUTION, ""), 1.158 + "%DISTRIBUTION_VERSION%": function() this._getPref(PREF_APP_DISTRIBUTION_VERSION, ""), 1.159 + }), 1.160 + 1.161 + _getUpdateURL: function() { 1.162 + let url = this._getPref(PREF_UPDATES_URL, ""); 1.163 + let params = this._getParameters(); 1.164 + return url.replace(/%[A-Z_]+%/g, function(match) { 1.165 + let param = params[match]; 1.166 + // preserve the %FOO% string (e.g. as an encoding) if it's not a valid parameter 1.167 + return param ? encodeURIComponent(param()) : match; 1.168 + }); 1.169 + }, 1.170 + 1.171 + _fetchUpdate: function(url, success, error) { 1.172 + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] 1.173 + .createInstance(Ci.nsIXMLHttpRequest); 1.174 + request.mozBackgroundRequest = true; 1.175 + request.timeout = this._getPref(PREF_UPDATES_TIMEOUT, 60000); 1.176 + request.open("GET", url, true); 1.177 + request.overrideMimeType("application/json"); 1.178 + request.responseType = "json"; 1.179 + 1.180 + request.addEventListener("load", function() { 1.181 + let response = request.response; 1.182 + response ? success(response) : error(); 1.183 + }); 1.184 + request.addEventListener("error", error); 1.185 + request.send(); 1.186 + }, 1.187 + 1.188 + _update: function() { 1.189 + let url = this._getUpdateURL(); 1.190 + url && this._fetchUpdate(url, 1.191 + (function(response) { // success 1.192 + // apply update and save overrides to profile 1.193 + this._applyUpdate(response); 1.194 + this._saveToFile(response); 1.195 + this._scheduleUpdate(); // cancel any retries 1.196 + }).bind(this), 1.197 + (function(response) { // error 1.198 + this._scheduleUpdate(true /* retry */); 1.199 + }).bind(this)); 1.200 + }, 1.201 + 1.202 + _scheduleUpdate: function(retry) { 1.203 + // only schedule updates in the main process 1.204 + if (gApp.processType !== Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { 1.205 + return; 1.206 + } 1.207 + let interval = this._getPref(PREF_UPDATES_INTERVAL, 604800 /* 1 week */); 1.208 + if (retry) { 1.209 + interval = this._getPref(PREF_UPDATES_RETRY, interval); 1.210 + } 1.211 + gUpdateTimer.registerTimer(TIMER_ID, this, Math.max(1, interval)); 1.212 + }, 1.213 + 1.214 + notify: function(timer) { 1.215 + // timer notification 1.216 + if (this._getPref(PREF_UPDATES_ENABLED, false)) { 1.217 + this._update(); 1.218 + } 1.219 + }, 1.220 + 1.221 + observe: function(subject, topic, data) { 1.222 + switch (topic) { 1.223 + case "nsPref:changed": 1.224 + if (data === PREF_UPDATES_ENABLED) { 1.225 + this._applySavedUpdate(); 1.226 + } else if (data === PREF_UPDATES_INTERVAL) { 1.227 + this._scheduleUpdate(); 1.228 + } else if (data === PREF_UPDATES_LASTUPDATED) { 1.229 + // reload from file if there has been an update 1.230 + let lastUpdated = parseInt( 1.231 + this._getPref(PREF_UPDATES_LASTUPDATED, "0"), 0); 1.232 + if (lastUpdated > this._lastUpdated) { 1.233 + this._applySavedUpdate(); 1.234 + this._lastUpdated = lastUpdated; 1.235 + } 1.236 + } 1.237 + break; 1.238 + } 1.239 + }, 1.240 + 1.241 + QueryInterface: XPCOMUtils.generateQI([ 1.242 + Ci.nsIObserver, 1.243 + Ci.nsITimerCallback, 1.244 + ]), 1.245 +};