comparison components/nttAddonCompatibilityService.js @ 0:dada0ac40a8f

initial import
author Yoshiki Yazawa <yaz@honeyplanet.jp>
date Tue, 02 Dec 2008 20:31:01 +0900
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:dada0ac40a8f
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]);