Thu, 15 Jan 2015 21:03:48 +0100
Integrate friendly tips from Tor colleagues to make (or not) 4.5 alpha 3;
This includes removal of overloaded (but unused) methods, and addition of
a overlooked call to DataStruct::SetData(nsISupports, uint32_t, bool.)
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["UserAgentUpdates"];
9 const Ci = Components.interfaces;
10 const Cc = Components.classes;
11 const Cu = Components.utils;
13 Cu.import("resource://gre/modules/Services.jsm");
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
16 XPCOMUtils.defineLazyModuleGetter(
17 this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
19 XPCOMUtils.defineLazyModuleGetter(
20 this, "OS", "resource://gre/modules/osfile.jsm");
22 XPCOMUtils.defineLazyModuleGetter(
23 this, "UpdateChannel", "resource://gre/modules/UpdateChannel.jsm");
25 XPCOMUtils.defineLazyServiceGetter(
26 this, "gUpdateTimer", "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
28 XPCOMUtils.defineLazyGetter(this, "gApp",
29 function() Cc["@mozilla.org/xre/app-info;1"]
30 .getService(Ci.nsIXULAppInfo).QueryInterface(Ci.nsIXULRuntime)
31 );
33 XPCOMUtils.defineLazyGetter(this, "gDecoder",
34 function() new TextDecoder()
35 );
37 XPCOMUtils.defineLazyGetter(this, "gEncoder",
38 function() new TextEncoder()
39 );
41 const TIMER_ID = "user-agent-updates-timer";
43 const PREF_UPDATES = "general.useragent.updates.";
44 const PREF_UPDATES_ENABLED = PREF_UPDATES + "enabled";
45 const PREF_UPDATES_URL = PREF_UPDATES + "url";
46 const PREF_UPDATES_INTERVAL = PREF_UPDATES + "interval";
47 const PREF_UPDATES_RETRY = PREF_UPDATES + "retry";
48 const PREF_UPDATES_TIMEOUT = PREF_UPDATES + "timeout";
49 const PREF_UPDATES_LASTUPDATED = PREF_UPDATES + "lastupdated";
51 const KEY_PREFDIR = "PrefD";
52 const KEY_APPDIR = "XCurProcD";
53 const FILE_UPDATES = "ua-update.json";
55 const PREF_APP_DISTRIBUTION = "distribution.id";
56 const PREF_APP_DISTRIBUTION_VERSION = "distribution.version";
58 var gInitialized = false;
60 this.UserAgentUpdates = {
61 init: function(callback) {
62 if (gInitialized) {
63 return;
64 }
65 gInitialized = true;
67 this._callback = callback;
68 this._lastUpdated = 0;
69 this._applySavedUpdate();
71 Services.prefs.addObserver(PREF_UPDATES, this, false);
72 },
74 uninit: function() {
75 if (!gInitialized) {
76 return;
77 }
78 gInitialized = false;
79 Services.prefs.removeObserver(PREF_UPDATES, this);
80 },
82 _applyUpdate: function(update) {
83 // Check pref again in case it has changed
84 if (update && this._getPref(PREF_UPDATES_ENABLED, false)) {
85 this._callback(update);
86 } else {
87 this._callback(null);
88 }
89 },
91 _applySavedUpdate: function() {
92 if (!this._getPref(PREF_UPDATES_ENABLED, false)) {
93 // remove previous overrides
94 this._applyUpdate(null);
95 return;
96 }
97 // try loading from profile dir, then from app dir
98 let dirs = [KEY_PREFDIR, KEY_APPDIR];
99 dirs.reduce((prevLoad, dir) => {
100 let file = FileUtils.getFile(dir, [FILE_UPDATES], true).path;
101 // tryNext returns promise to read file under dir and parse it
102 let tryNext = () => OS.File.read(file).then(
103 (bytes) => {
104 let update = JSON.parse(gDecoder.decode(bytes));
105 if (!update) {
106 throw new Error("invalid update");
107 }
108 return update;
109 }
110 );
111 // try to load next one if the previous load failed
112 return prevLoad ? prevLoad.then(null, tryNext) : tryNext();
113 }, null).then(
114 // apply update if loading was successful
115 (update) => this._applyUpdate(update)
116 );
117 this._scheduleUpdate();
118 },
120 _saveToFile: function(update) {
121 let file = FileUtils.getFile(KEY_PREFDIR, [FILE_UPDATES], true);
122 let path = file.path;
123 let bytes = gEncoder.encode(JSON.stringify(update));
124 OS.File.writeAtomic(path, bytes, {tmpPath: path + ".tmp"}).then(
125 () => {
126 this._lastUpdated = Date.now();
127 Services.prefs.setCharPref(
128 PREF_UPDATES_LASTUPDATED, this._lastUpdated.toString());
129 },
130 Cu.reportError
131 );
132 },
134 _getPref: function(name, def) {
135 try {
136 switch (typeof def) {
137 case "number": return Services.prefs.getIntPref(name);
138 case "boolean": return Services.prefs.getBoolPref(name);
139 }
140 return Services.prefs.getCharPref(name);
141 } catch (e) {
142 return def;
143 }
144 },
146 _getParameters: function() ({
147 "%DATE%": function() Date.now().toString(),
148 "%PRODUCT%": function() gApp.name,
149 "%APP_ID%": function() gApp.ID,
150 "%APP_VERSION%": function() gApp.version,
151 "%BUILD_ID%": function() gApp.appBuildID,
152 "%OS%": function() gApp.OS,
153 "%CHANNEL%": function() UpdateChannel.get(),
154 "%DISTRIBUTION%": function() this._getPref(PREF_APP_DISTRIBUTION, ""),
155 "%DISTRIBUTION_VERSION%": function() this._getPref(PREF_APP_DISTRIBUTION_VERSION, ""),
156 }),
158 _getUpdateURL: function() {
159 let url = this._getPref(PREF_UPDATES_URL, "");
160 let params = this._getParameters();
161 return url.replace(/%[A-Z_]+%/g, function(match) {
162 let param = params[match];
163 // preserve the %FOO% string (e.g. as an encoding) if it's not a valid parameter
164 return param ? encodeURIComponent(param()) : match;
165 });
166 },
168 _fetchUpdate: function(url, success, error) {
169 let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
170 .createInstance(Ci.nsIXMLHttpRequest);
171 request.mozBackgroundRequest = true;
172 request.timeout = this._getPref(PREF_UPDATES_TIMEOUT, 60000);
173 request.open("GET", url, true);
174 request.overrideMimeType("application/json");
175 request.responseType = "json";
177 request.addEventListener("load", function() {
178 let response = request.response;
179 response ? success(response) : error();
180 });
181 request.addEventListener("error", error);
182 request.send();
183 },
185 _update: function() {
186 let url = this._getUpdateURL();
187 url && this._fetchUpdate(url,
188 (function(response) { // success
189 // apply update and save overrides to profile
190 this._applyUpdate(response);
191 this._saveToFile(response);
192 this._scheduleUpdate(); // cancel any retries
193 }).bind(this),
194 (function(response) { // error
195 this._scheduleUpdate(true /* retry */);
196 }).bind(this));
197 },
199 _scheduleUpdate: function(retry) {
200 // only schedule updates in the main process
201 if (gApp.processType !== Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
202 return;
203 }
204 let interval = this._getPref(PREF_UPDATES_INTERVAL, 604800 /* 1 week */);
205 if (retry) {
206 interval = this._getPref(PREF_UPDATES_RETRY, interval);
207 }
208 gUpdateTimer.registerTimer(TIMER_ID, this, Math.max(1, interval));
209 },
211 notify: function(timer) {
212 // timer notification
213 if (this._getPref(PREF_UPDATES_ENABLED, false)) {
214 this._update();
215 }
216 },
218 observe: function(subject, topic, data) {
219 switch (topic) {
220 case "nsPref:changed":
221 if (data === PREF_UPDATES_ENABLED) {
222 this._applySavedUpdate();
223 } else if (data === PREF_UPDATES_INTERVAL) {
224 this._scheduleUpdate();
225 } else if (data === PREF_UPDATES_LASTUPDATED) {
226 // reload from file if there has been an update
227 let lastUpdated = parseInt(
228 this._getPref(PREF_UPDATES_LASTUPDATED, "0"), 0);
229 if (lastUpdated > this._lastUpdated) {
230 this._applySavedUpdate();
231 this._lastUpdated = lastUpdated;
232 }
233 }
234 break;
235 }
236 },
238 QueryInterface: XPCOMUtils.generateQI([
239 Ci.nsIObserver,
240 Ci.nsITimerCallback,
241 ]),
242 };