0
|
1 const Cc = Components.classes;
|
|
2 const Ci = Components.interfaces;
|
|
3 const Cr = Components.results;
|
|
4
|
|
5 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
6
|
|
7 const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
|
|
8 const PREFIX_ITEM_URI = "urn:mozilla:item:";
|
|
9 const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest";
|
|
10 const FILE_INSTALL_MANIFEST = "install.rdf";
|
|
11 const TOOLKIT_ID = "toolkit@mozilla.org"
|
|
12
|
|
13 var gEM = null;
|
|
14 var gRDF = null;
|
|
15 var gApp = null;
|
|
16 var gVC = null;
|
|
17 var gCheckCompatibility = true;
|
|
18 var gCheckUpdateSecurity = true;
|
|
19 var gPrefs = null;
|
|
20
|
|
21 function EM_NS(property) {
|
|
22 return PREFIX_NS_EM + property;
|
|
23 }
|
|
24
|
|
25 function EM_R(property) {
|
|
26 return gRDF.GetResource(EM_NS(property));
|
|
27 }
|
|
28
|
|
29 function getRDFProperty(ds, source, property) {
|
|
30 var value = ds.GetTarget(source, EM_R(property), true);
|
|
31 if (value && value instanceof Ci.nsIRDFLiteral)
|
|
32 return value.Value;
|
|
33 return null;
|
|
34 }
|
|
35
|
|
36 function removeRDFProperty(ds, source, property) {
|
|
37 var arc = EM_R(property);
|
|
38 var targets = ds.GetTargets(source, arc, true);
|
|
39 while (targets.hasMoreElements())
|
|
40 ds.Unassert(source, arc, targets.getNext());
|
|
41 }
|
|
42
|
|
43 function extractXPI(xpi) {
|
|
44 // XXX For 1.9 final we can switch to just extracting/compressing install.rdf
|
|
45 var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
|
|
46 createInstance(Ci.nsIZipReader);
|
|
47 zipReader.open(xpi);
|
|
48 if (!zipReader.hasEntry(FILE_INSTALL_MANIFEST)) {
|
|
49 zipReader.close();
|
|
50 return null;
|
|
51 }
|
|
52 var dirs = Cc["@mozilla.org/file/directory_service;1"].
|
|
53 getService(Ci.nsIProperties);
|
|
54 var file = dirs.get("TmpD", Ci.nsILocalFile);
|
|
55 file.append("tmpxpi");
|
|
56 file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0755);
|
|
57 var entries = zipReader.findEntries("*");
|
|
58 while (entries.hasMore()) {
|
|
59 var path = entries.getNext();
|
|
60 var entry = zipReader.getEntry(path);
|
|
61 if (path.substring(path.length - 1) == "/")
|
|
62 path = path.substring(0, entry.length - 1);
|
|
63 var parts = path.split("/");
|
|
64 var target = file.clone();
|
|
65 for (var i = 0; i < parts.length; i++)
|
|
66 target.append(parts[i]);
|
|
67 if (entry.isDirectory) {
|
|
68 if (!target.exists())
|
|
69 target.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
|
|
70 }
|
|
71 else {
|
|
72 var parent = target.parent;
|
|
73 if (!parent.exists())
|
|
74 parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
|
|
75 zipReader.extract(path, target);
|
|
76 }
|
|
77 }
|
|
78 zipReader.close();
|
|
79 return file;
|
|
80 }
|
|
81
|
|
82 function loadManifest(file) {
|
|
83 var ioServ = Cc["@mozilla.org/network/io-service;1"].
|
|
84 getService(Ci.nsIIOService);
|
|
85 var fph = ioServ.getProtocolHandler("file")
|
|
86 .QueryInterface(Ci.nsIFileProtocolHandler);
|
|
87 return gRDF.GetDataSourceBlocking(fph.getURLSpecFromFile(file));
|
|
88 }
|
|
89
|
|
90 function recursiveUpdate(zipWriter, path, dir) {
|
|
91 var entries = dir.directoryEntries;
|
|
92 while (entries.hasMoreElements()) {
|
|
93 var entry = entries.getNext().QueryInterface(Ci.nsIFile);
|
|
94 if (entry.isDirectory()) {
|
|
95 var newPath = path + entry.leafName + "/";
|
|
96 zipWriter.addEntryDirectory(newPath, entry.lastModifiedTime, false);
|
|
97 recursiveUpdate(zipWriter, newPath, entry);
|
|
98 }
|
|
99 else {
|
|
100 zipWriter.addEntryFile(path + entry.leafName, Ci.nsIZipWriter.COMPRESSION_NONE,
|
|
101 entry, false);
|
|
102 }
|
|
103 }
|
|
104 }
|
|
105
|
|
106 function updateXPI(xpi, file) {
|
|
107 // XXX For 1.9 final we can switch to just extracting/compressing install.rdf
|
|
108 var zipWriter = Cc["@mozilla.org/zipwriter;1"].
|
|
109 createInstance(Ci.nsIZipWriter);
|
|
110 zipWriter.open(xpi, 0x04 | 0x08 | 0x20);
|
|
111 recursiveUpdate(zipWriter, "", file);
|
|
112 zipWriter.close();
|
|
113 }
|
|
114
|
|
115 function nttAddonUpdateChecker(addon) {
|
|
116 this.addon = addon;
|
|
117 }
|
|
118
|
|
119 nttAddonUpdateChecker.prototype = {
|
|
120 addon: null,
|
|
121 busy: null,
|
|
122
|
|
123 checkForUpdates: function() {
|
|
124 this.busy = true;
|
|
125 LOG("Searching for compatibility information for " + this.addon.id);
|
|
126 gEM.update([this.addon], 1,
|
|
127 Ci.nsIExtensionManager.UPDATE_SYNC_COMPATIBILITY, this);
|
|
128
|
|
129 // Spin an event loop to wait for the update check to complete.
|
|
130 var tm = Cc["@mozilla.org/thread-manager;1"].
|
|
131 getService(Ci.nsIThreadManager);
|
|
132 var thread = tm.currentThread;
|
|
133 while (this.busy)
|
|
134 thread.processNextEvent(true);
|
|
135 },
|
|
136
|
|
137 // nsIAddonUpdateCheckListener implementation
|
|
138 onUpdateStarted: function() {
|
|
139 },
|
|
140
|
|
141 onUpdateEnded: function() {
|
|
142 this.busy = false;
|
|
143 },
|
|
144
|
|
145 onAddonUpdateStarted: function(addon) {
|
|
146 },
|
|
147
|
|
148 onAddonUpdateEnded: function(addon, status) {
|
|
149 if (status & Ci.nsIAddonUpdateCheckListener.STATUS_DATA_FOUND) {
|
|
150 LOG("Found new compatibility information for " + addon.id + ": " + addon.minAppVersion + " " + addon.maxAppVersion);
|
|
151 this.addon.minAppVersion = addon.minAppVersion;
|
|
152 this.addon.maxAppVersion = addon.maxAppVersion;
|
|
153 this.addon.targetAppID = addon.targetAppID;
|
|
154 this.addon.overrideVersions();
|
|
155 }
|
|
156 }
|
|
157 };
|
|
158
|
|
159 function nttAddonDetail() {
|
|
160 }
|
|
161
|
|
162 nttAddonDetail.prototype = {
|
|
163 datasource: null,
|
|
164 root: null,
|
|
165
|
|
166 xpi: null,
|
|
167 file: null,
|
|
168
|
|
169 id: null,
|
|
170 name: null,
|
|
171 version: null,
|
|
172 type: Ci.nsIUpdateItem.TYPE_EXTENSION,
|
|
173 updateRDF: null,
|
|
174 updateKey: null,
|
|
175 iconURL: "chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png",
|
|
176
|
|
177 installLocationKey: null,
|
|
178 xpiURL: null,
|
|
179 xpiHash: null,
|
|
180
|
|
181 appResource: null,
|
|
182 targetAppID: null,
|
|
183 minAppVersion: null,
|
|
184 maxAppVersion: null,
|
|
185
|
|
186 init: function() {
|
|
187 if (!this.id)
|
|
188 this.id = getRDFProperty(this.datasource, this.root, "id");
|
|
189 this.name = getRDFProperty(this.datasource, this.root, "name");
|
|
190 this.version = getRDFProperty(this.datasource, this.root, "version");
|
|
191 this.updateRDF = getRDFProperty(this.datasource, this.root, "updateURL");
|
|
192 this.updateKey = getRDFProperty(this.datasource, this.root, "updateKey");
|
|
193
|
|
194 var apps = this.datasource.GetTargets(this.root, EM_R("targetApplication"), true);
|
|
195 while (apps.hasMoreElements()) {
|
|
196 var app = apps.getNext().QueryInterface(Ci.nsIRDFResource);
|
|
197 var id = getRDFProperty(this.datasource, app, "id");
|
|
198 if (id == gApp.ID || id == TOOLKIT_ID) {
|
|
199 this.minAppVersion = getRDFProperty(this.datasource, app, "minVersion");
|
|
200 this.maxAppVersion = getRDFProperty(this.datasource, app, "maxVersion");
|
|
201 if (this.minAppVersion && this.maxAppVersion) {
|
|
202 this.appResource = app;
|
|
203 this.targetAppID = id;
|
|
204 if (id == gApp.ID)
|
|
205 break;
|
|
206 }
|
|
207 }
|
|
208 }
|
|
209 },
|
|
210
|
|
211 initWithXPI: function(xpi) {
|
|
212 this.xpi = xpi;
|
|
213 this.file = extractXPI(xpi);
|
|
214 var rdf = this.file.clone();
|
|
215 rdf.append(FILE_INSTALL_MANIFEST);
|
|
216 this.datasource = loadManifest(rdf);
|
|
217 this.root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT);
|
|
218 this.init();
|
|
219 },
|
|
220
|
|
221 initWithDataSource: function(ds, root, id) {
|
|
222 this.datasource = ds;
|
|
223 this.root = root;
|
|
224 this.id = id;
|
|
225 this.init();
|
|
226 },
|
|
227
|
|
228 cleanup: function() {
|
|
229 if (this.file && this.file.exists)
|
|
230 this.file.remove(true);
|
|
231 },
|
|
232
|
|
233 overrideVersions: function() {
|
|
234 removeRDFProperty(this.datasource, this.appResource, "minVersion");
|
|
235 this.datasource.Assert(this.appResource, EM_R("minVersion"), gRDF.GetLiteral(this.minAppVersion), true);
|
|
236 removeRDFProperty(this.datasource, this.appResource, "maxVersion");
|
|
237 this.datasource.Assert(this.appResource, EM_R("maxVersion"), gRDF.GetLiteral(this.maxAppVersion), true);
|
|
238 this.datasource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
|
|
239 if (this.xpi && this.file)
|
|
240 updateXPI(this.xpi, this.file);
|
|
241 },
|
|
242
|
|
243 overrideCompatibility: function(ignorePrefs) {
|
|
244 if (!this.isValid())
|
|
245 return;
|
|
246
|
|
247 var changed = false;
|
|
248
|
|
249 if (gCheckCompatibility || ignorePrefs) {
|
|
250 var version = (gApp.ID == this.targetAppID) ? gApp.version : gApp.platformVersion;
|
|
251 if (gVC.compare(version, this.minAppVersion) < 0) {
|
|
252 LOG("minVersion " + this.minAppVersion + " is too high, reducing to " + version);
|
|
253 if (!this.datasource.GetTarget(this.appResource, EM_R("oldMinVersion"), true))
|
|
254 this.datasource.Assert(this.appResource, EM_R("oldMinVersion"), gRDF.GetLiteral(this.minAppVersion), true);
|
|
255 removeRDFProperty(this.datasource, this.appResource, "minVersion");
|
|
256 this.datasource.Assert(this.appResource, EM_R("minVersion"), gRDF.GetLiteral(version), true);
|
|
257 this.minAppVersion = version;
|
|
258 changed = true;
|
|
259 }
|
|
260 else if (gVC.compare(version, this.maxAppVersion) > 0) {
|
|
261 LOG("maxVersion " + this.maxAppVersion + " is too low, increasing to " + version);
|
|
262 if (!this.datasource.GetTarget(this.appResource, EM_R("oldMaxVersion"), true))
|
|
263 this.datasource.Assert(this.appResource, EM_R("oldMaxVersion"), gRDF.GetLiteral(this.maxAppVersion), true);
|
|
264 removeRDFProperty(this.datasource, this.appResource, "maxVersion");
|
|
265 this.datasource.Assert(this.appResource, EM_R("maxVersion"), gRDF.GetLiteral(version), true);
|
|
266 this.maxAppVersion = version;
|
|
267 changed = true;
|
|
268 }
|
|
269
|
|
270 if (changed && !this.xpi) {
|
|
271 // This updates any UI bound to the datasource
|
|
272 var compatprop = EM_R("compatible");
|
|
273 var truth = gRDF.GetLiteral("true");
|
|
274 this.datasource.Assert(this.root, compatprop, truth, true);
|
|
275 this.datasource.Unassert(this.root, compatprop, truth);
|
|
276 }
|
|
277 }
|
|
278
|
|
279 if (!this.isUpdateSecure(ignorePrefs)) {
|
|
280 LOG("Addon is insecure, removing update URL");
|
|
281 removeRDFProperty(this.datasource, this.root, "updateURL");
|
|
282 this.updateRDF = null;
|
|
283 changed = true;
|
|
284
|
|
285 // This updates any UI bound to the datasource
|
|
286 compatprop = EM_R("providesUpdatesSecurely");
|
|
287 truth = gRDF.GetLiteral("true");
|
|
288 this.datasource.Assert(this.root, compatprop, truth, true);
|
|
289 this.datasource.Unassert(this.root, compatprop, truth);
|
|
290 }
|
|
291
|
|
292 if (changed) {
|
|
293 this.datasource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
|
|
294 if (this.xpi && this.file)
|
|
295 updateXPI(this.xpi, this.file);
|
|
296 }
|
|
297 },
|
|
298
|
|
299 isValid: function() {
|
|
300 return !!this.appResource;
|
|
301 },
|
|
302
|
|
303 isCompatible: function(ignorePrefs) {
|
|
304 if (!gCheckCompatibility && !ignorePrefs)
|
|
305 return true;
|
|
306
|
|
307 var version = (gApp.ID == this.targetAppID) ? gApp.version : gApp.platformVersion;
|
|
308 if (gVC.compare(version, this.minAppVersion) < 0)
|
|
309 return false;
|
|
310 if (gVC.compare(version, this.maxAppVersion) > 0)
|
|
311 return false;
|
|
312 return true;
|
|
313 },
|
|
314
|
|
315 isUpdateSecure: function(ignorePrefs) {
|
|
316 if (!gCheckUpdateSecurity && !ignorePrefs)
|
|
317 return true;
|
|
318
|
|
319 if (!this.updateRDF)
|
|
320 return true;
|
|
321 if (this.updateKey)
|
|
322 return true;
|
|
323 return (this.updateRDF.substring(0, 6) == "https:");
|
|
324 },
|
|
325
|
|
326 needsOverride: function(ignorePrefs) {
|
|
327 return (!this.isCompatible(ignorePrefs) || !this.isUpdateSecure(ignorePrefs));
|
|
328 },
|
|
329
|
|
330 QueryInterface: XPCOMUtils.generateQI([Ci.nttIAddon, Ci.nsIUpdateItem]),
|
|
331 };
|
|
332
|
|
333 function nttAddonCompatibilityService() {
|
|
334 }
|
|
335
|
|
336 nttAddonCompatibilityService.prototype = {
|
|
337 id: null,
|
|
338
|
|
339 init: function() {
|
|
340 Components.utils.import("resource://nightly/Logging.jsm");
|
|
341
|
|
342 gEM = Cc["@mozilla.org/extensions/manager;1"].
|
|
343 getService(Ci.nsIExtensionManager);
|
|
344 gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].
|
|
345 getService(Ci.nsIRDFService);
|
|
346 gApp = Cc["@mozilla.org/xre/app-info;1"].
|
|
347 getService(Ci.nsIXULAppInfo).QueryInterface(Ci.nsIXULRuntime);
|
|
348 gVC = Cc["@mozilla.org/xpcom/version-comparator;1"].
|
|
349 getService(Ci.nsIVersionComparator);
|
|
350 if (gVC.compare(gApp.platformVersion, "1.9b5") >= 0)
|
|
351 this.id = gEM.addInstallListener(this);
|
|
352 gPrefs = Components.classes["@mozilla.org/preferences-service;1"]
|
|
353 .getService(Components.interfaces.nsIPrefService)
|
|
354 .getBranch("extensions.")
|
|
355 .QueryInterface(Components.interfaces.nsIPrefBranch2);
|
|
356 try {
|
|
357 gCheckCompatibility = gPrefs.getBoolPref("checkCompatibility");
|
|
358 }
|
|
359 catch (e) { }
|
|
360 try {
|
|
361 gCheckUpdateSecurity = gPrefs.getBoolPref("checkUpdateSecurity");
|
|
362 }
|
|
363 catch (e) { }
|
|
364 gPrefs.addObserver("", this, false);
|
|
365 },
|
|
366
|
|
367 // nsIAddonCompatibilityService implementation
|
|
368 getAddonForID: function(id) {
|
|
369 var addon = new nttAddonDetail();
|
|
370 addon.initWithDataSource(gEM.datasource, gRDF.GetResource(PREFIX_ITEM_URI + id), id);
|
|
371 return addon;
|
|
372 },
|
|
373
|
|
374 confirmOverride: function(addons, count) {
|
|
375 var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
|
|
376 getService(Ci.nsIWindowMediator);
|
|
377 win = wm.getMostRecentWindow("Extension:Manager");
|
|
378 if (win && win.top)
|
|
379 win = win.top;
|
|
380
|
|
381 var params = Cc["@mozilla.org/array;1"].
|
|
382 createInstance(Ci.nsIMutableArray);
|
|
383 for (var i = 0; i < addons.length; i++)
|
|
384 params.appendElement(addons[i], false);
|
|
385 var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
|
|
386 getService(Ci.nsIWindowWatcher);
|
|
387 ww.openWindow(win, "chrome://nightly/content/extensions/incompatible.xul", "",
|
|
388 "chrome,centerscreen,modal,dialog,titlebar", params);
|
|
389 return true;
|
|
390 },
|
|
391
|
|
392 // nsIAddonInstallListener implementation
|
|
393 onDownloadStarted: function(addon) {
|
|
394 },
|
|
395
|
|
396 onDownloadProgress: function(addon, value, maxValue) {
|
|
397 },
|
|
398
|
|
399 onDownloadEnded: function(addon) {
|
|
400 },
|
|
401
|
|
402 onInstallStarted: function(addon) {
|
|
403 LOG("Install Started for " + addon.xpiURL);
|
|
404 var ioServ = Cc["@mozilla.org/network/io-service;1"].
|
|
405 getService(Ci.nsIIOService);
|
|
406 var fph = ioServ.getProtocolHandler("file")
|
|
407 .QueryInterface(Ci.nsIFileProtocolHandler);
|
|
408 var file = fph.getFileFromURLSpec(addon.xpiURL);
|
|
409 if (file.exists()) {
|
|
410 try {
|
|
411 var addon = new nttAddonDetail();
|
|
412 addon.initWithXPI(file);
|
|
413 if (addon.isValid()) {
|
|
414 if (!addon.isCompatible(false)) {
|
|
415 // Check if there are remote updates available
|
|
416 var checker = new nttAddonUpdateChecker(addon);
|
|
417 checker.checkForUpdates();
|
|
418 }
|
|
419
|
|
420 if (addon.needsOverride(false))
|
|
421 this.confirmOverride([addon], 1);
|
|
422 else
|
|
423 LOG("Add-on is already compatible: '" + addon.updateRDF + "' " + addon.minAppVersion + "-" + addon.maxAppVersion);
|
|
424 }
|
|
425 else {
|
|
426 WARN("Add-on seems to be invalid");
|
|
427 }
|
|
428 addon.cleanup();
|
|
429 }
|
|
430 catch (e) {
|
|
431 ERROR("Exception during compatibility check " + e);
|
|
432 }
|
|
433 }
|
|
434 },
|
|
435
|
|
436 onCompatibilityCheckStarted: function(addon) {
|
|
437 },
|
|
438
|
|
439 onCompatibilityCheckEnded: function(addon, status) {
|
|
440 },
|
|
441
|
|
442 onInstallEnded: function(addon, status) {
|
|
443 },
|
|
444
|
|
445 onInstallsCompleted: function() {
|
|
446 },
|
|
447
|
|
448 // nsIObserver implementation
|
|
449 observe: function(subject, topic, data) {
|
|
450 switch (topic) {
|
|
451 case "app-startup":
|
|
452 var os = Cc["@mozilla.org/observer-service;1"].
|
|
453 getService(Ci.nsIObserverService);
|
|
454 os.addObserver(this, "profile-after-change", false);
|
|
455 os.addObserver(this, "quit-application", false);
|
|
456 break;
|
|
457 case "profile-after-change":
|
|
458 this.init();
|
|
459 break;
|
|
460 case "quit-application":
|
|
461 if (this.id)
|
|
462 gEM.removeInstallListenerAt(this.id);
|
|
463 gEM = null;
|
|
464 gRDF = null;
|
|
465 gApp = null;
|
|
466 gVC = null;
|
|
467 gPrefs.removeObserver("", this);
|
|
468 gPrefs = null;
|
|
469 break;
|
|
470 case "nsPref:changed":
|
|
471 switch (data) {
|
|
472 case "checkCompatibility":
|
|
473 try {
|
|
474 gCheckCompatibility = gPrefs.getBoolPref(data);
|
|
475 }
|
|
476 catch (e) {
|
|
477 gCheckCompatibility = true;
|
|
478 }
|
|
479 break;
|
|
480 case "checkUpdateSecurity":
|
|
481 try {
|
|
482 gCheckUpdateSecurity = gPrefs.getBoolPref(data);
|
|
483 }
|
|
484 catch (e) {
|
|
485 gCheckUpdateSecurity = true;
|
|
486 }
|
|
487 break;
|
|
488 }
|
|
489 break;
|
|
490 default:
|
|
491 WARN("Unknown event " + topic);
|
|
492 }
|
|
493 },
|
|
494
|
|
495 classDescription: "Nightly Tester Install Monitor",
|
|
496 contractID: "@oxymoronical.com/nightly/addoncompatibility;1",
|
|
497 classID: Components.ID("{801207d5-037c-4565-80ed-ede8f7a7c100}"),
|
|
498 QueryInterface: XPCOMUtils.generateQI([Ci.nttIAddonCompatibilityService, Ci.nsIAddonInstallListener, Ci.nsIObserver]),
|
|
499 _xpcom_categories: [{
|
|
500 category: "app-startup",
|
|
501 service: true
|
|
502 }]
|
|
503 }
|
|
504
|
|
505 function NSGetModule(compMgr, fileSpec)
|
|
506 XPCOMUtils.generateModule([nttAddonCompatibilityService]);
|