Mercurial > nightly_tester_tools
diff components/nttAddonCompatibilityService.js @ 0:dada0ac40a8f
initial import
author | Yoshiki Yazawa <yaz@honeyplanet.jp> |
---|---|
date | Tue, 02 Dec 2008 20:31:01 +0900 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/components/nttAddonCompatibilityService.js Tue Dec 02 20:31:01 2008 +0900 @@ -0,0 +1,506 @@ +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; +const PREFIX_ITEM_URI = "urn:mozilla:item:"; +const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest"; +const FILE_INSTALL_MANIFEST = "install.rdf"; +const TOOLKIT_ID = "toolkit@mozilla.org" + +var gEM = null; +var gRDF = null; +var gApp = null; +var gVC = null; +var gCheckCompatibility = true; +var gCheckUpdateSecurity = true; +var gPrefs = null; + +function EM_NS(property) { + return PREFIX_NS_EM + property; +} + +function EM_R(property) { + return gRDF.GetResource(EM_NS(property)); +} + +function getRDFProperty(ds, source, property) { + var value = ds.GetTarget(source, EM_R(property), true); + if (value && value instanceof Ci.nsIRDFLiteral) + return value.Value; + return null; +} + +function removeRDFProperty(ds, source, property) { + var arc = EM_R(property); + var targets = ds.GetTargets(source, arc, true); + while (targets.hasMoreElements()) + ds.Unassert(source, arc, targets.getNext()); +} + +function extractXPI(xpi) { + // XXX For 1.9 final we can switch to just extracting/compressing install.rdf + var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. + createInstance(Ci.nsIZipReader); + zipReader.open(xpi); + if (!zipReader.hasEntry(FILE_INSTALL_MANIFEST)) { + zipReader.close(); + return null; + } + var dirs = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + var file = dirs.get("TmpD", Ci.nsILocalFile); + file.append("tmpxpi"); + file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0755); + var entries = zipReader.findEntries("*"); + while (entries.hasMore()) { + var path = entries.getNext(); + var entry = zipReader.getEntry(path); + if (path.substring(path.length - 1) == "/") + path = path.substring(0, entry.length - 1); + var parts = path.split("/"); + var target = file.clone(); + for (var i = 0; i < parts.length; i++) + target.append(parts[i]); + if (entry.isDirectory) { + if (!target.exists()) + target.create(Ci.nsIFile.DIRECTORY_TYPE, 0755); + } + else { + var parent = target.parent; + if (!parent.exists()) + parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0755); + zipReader.extract(path, target); + } + } + zipReader.close(); + return file; +} + +function loadManifest(file) { + var ioServ = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var fph = ioServ.getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + return gRDF.GetDataSourceBlocking(fph.getURLSpecFromFile(file)); +} + +function recursiveUpdate(zipWriter, path, dir) { + var entries = dir.directoryEntries; + while (entries.hasMoreElements()) { + var entry = entries.getNext().QueryInterface(Ci.nsIFile); + if (entry.isDirectory()) { + var newPath = path + entry.leafName + "/"; + zipWriter.addEntryDirectory(newPath, entry.lastModifiedTime, false); + recursiveUpdate(zipWriter, newPath, entry); + } + else { + zipWriter.addEntryFile(path + entry.leafName, Ci.nsIZipWriter.COMPRESSION_NONE, + entry, false); + } + } +} + +function updateXPI(xpi, file) { + // XXX For 1.9 final we can switch to just extracting/compressing install.rdf + var zipWriter = Cc["@mozilla.org/zipwriter;1"]. + createInstance(Ci.nsIZipWriter); + zipWriter.open(xpi, 0x04 | 0x08 | 0x20); + recursiveUpdate(zipWriter, "", file); + zipWriter.close(); +} + +function nttAddonUpdateChecker(addon) { + this.addon = addon; +} + +nttAddonUpdateChecker.prototype = { + addon: null, + busy: null, + + checkForUpdates: function() { + this.busy = true; + LOG("Searching for compatibility information for " + this.addon.id); + gEM.update([this.addon], 1, + Ci.nsIExtensionManager.UPDATE_SYNC_COMPATIBILITY, this); + + // Spin an event loop to wait for the update check to complete. + var tm = Cc["@mozilla.org/thread-manager;1"]. + getService(Ci.nsIThreadManager); + var thread = tm.currentThread; + while (this.busy) + thread.processNextEvent(true); + }, + + // nsIAddonUpdateCheckListener implementation + onUpdateStarted: function() { + }, + + onUpdateEnded: function() { + this.busy = false; + }, + + onAddonUpdateStarted: function(addon) { + }, + + onAddonUpdateEnded: function(addon, status) { + if (status & Ci.nsIAddonUpdateCheckListener.STATUS_DATA_FOUND) { + LOG("Found new compatibility information for " + addon.id + ": " + addon.minAppVersion + " " + addon.maxAppVersion); + this.addon.minAppVersion = addon.minAppVersion; + this.addon.maxAppVersion = addon.maxAppVersion; + this.addon.targetAppID = addon.targetAppID; + this.addon.overrideVersions(); + } + } +}; + +function nttAddonDetail() { +} + +nttAddonDetail.prototype = { + datasource: null, + root: null, + + xpi: null, + file: null, + + id: null, + name: null, + version: null, + type: Ci.nsIUpdateItem.TYPE_EXTENSION, + updateRDF: null, + updateKey: null, + iconURL: "chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png", + + installLocationKey: null, + xpiURL: null, + xpiHash: null, + + appResource: null, + targetAppID: null, + minAppVersion: null, + maxAppVersion: null, + + init: function() { + if (!this.id) + this.id = getRDFProperty(this.datasource, this.root, "id"); + this.name = getRDFProperty(this.datasource, this.root, "name"); + this.version = getRDFProperty(this.datasource, this.root, "version"); + this.updateRDF = getRDFProperty(this.datasource, this.root, "updateURL"); + this.updateKey = getRDFProperty(this.datasource, this.root, "updateKey"); + + var apps = this.datasource.GetTargets(this.root, EM_R("targetApplication"), true); + while (apps.hasMoreElements()) { + var app = apps.getNext().QueryInterface(Ci.nsIRDFResource); + var id = getRDFProperty(this.datasource, app, "id"); + if (id == gApp.ID || id == TOOLKIT_ID) { + this.minAppVersion = getRDFProperty(this.datasource, app, "minVersion"); + this.maxAppVersion = getRDFProperty(this.datasource, app, "maxVersion"); + if (this.minAppVersion && this.maxAppVersion) { + this.appResource = app; + this.targetAppID = id; + if (id == gApp.ID) + break; + } + } + } + }, + + initWithXPI: function(xpi) { + this.xpi = xpi; + this.file = extractXPI(xpi); + var rdf = this.file.clone(); + rdf.append(FILE_INSTALL_MANIFEST); + this.datasource = loadManifest(rdf); + this.root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT); + this.init(); + }, + + initWithDataSource: function(ds, root, id) { + this.datasource = ds; + this.root = root; + this.id = id; + this.init(); + }, + + cleanup: function() { + if (this.file && this.file.exists) + this.file.remove(true); + }, + + overrideVersions: function() { + removeRDFProperty(this.datasource, this.appResource, "minVersion"); + this.datasource.Assert(this.appResource, EM_R("minVersion"), gRDF.GetLiteral(this.minAppVersion), true); + removeRDFProperty(this.datasource, this.appResource, "maxVersion"); + this.datasource.Assert(this.appResource, EM_R("maxVersion"), gRDF.GetLiteral(this.maxAppVersion), true); + this.datasource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush(); + if (this.xpi && this.file) + updateXPI(this.xpi, this.file); + }, + + overrideCompatibility: function(ignorePrefs) { + if (!this.isValid()) + return; + + var changed = false; + + if (gCheckCompatibility || ignorePrefs) { + var version = (gApp.ID == this.targetAppID) ? gApp.version : gApp.platformVersion; + if (gVC.compare(version, this.minAppVersion) < 0) { + LOG("minVersion " + this.minAppVersion + " is too high, reducing to " + version); + if (!this.datasource.GetTarget(this.appResource, EM_R("oldMinVersion"), true)) + this.datasource.Assert(this.appResource, EM_R("oldMinVersion"), gRDF.GetLiteral(this.minAppVersion), true); + removeRDFProperty(this.datasource, this.appResource, "minVersion"); + this.datasource.Assert(this.appResource, EM_R("minVersion"), gRDF.GetLiteral(version), true); + this.minAppVersion = version; + changed = true; + } + else if (gVC.compare(version, this.maxAppVersion) > 0) { + LOG("maxVersion " + this.maxAppVersion + " is too low, increasing to " + version); + if (!this.datasource.GetTarget(this.appResource, EM_R("oldMaxVersion"), true)) + this.datasource.Assert(this.appResource, EM_R("oldMaxVersion"), gRDF.GetLiteral(this.maxAppVersion), true); + removeRDFProperty(this.datasource, this.appResource, "maxVersion"); + this.datasource.Assert(this.appResource, EM_R("maxVersion"), gRDF.GetLiteral(version), true); + this.maxAppVersion = version; + changed = true; + } + + if (changed && !this.xpi) { + // This updates any UI bound to the datasource + var compatprop = EM_R("compatible"); + var truth = gRDF.GetLiteral("true"); + this.datasource.Assert(this.root, compatprop, truth, true); + this.datasource.Unassert(this.root, compatprop, truth); + } + } + + if (!this.isUpdateSecure(ignorePrefs)) { + LOG("Addon is insecure, removing update URL"); + removeRDFProperty(this.datasource, this.root, "updateURL"); + this.updateRDF = null; + changed = true; + + // This updates any UI bound to the datasource + compatprop = EM_R("providesUpdatesSecurely"); + truth = gRDF.GetLiteral("true"); + this.datasource.Assert(this.root, compatprop, truth, true); + this.datasource.Unassert(this.root, compatprop, truth); + } + + if (changed) { + this.datasource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush(); + if (this.xpi && this.file) + updateXPI(this.xpi, this.file); + } + }, + + isValid: function() { + return !!this.appResource; + }, + + isCompatible: function(ignorePrefs) { + if (!gCheckCompatibility && !ignorePrefs) + return true; + + var version = (gApp.ID == this.targetAppID) ? gApp.version : gApp.platformVersion; + if (gVC.compare(version, this.minAppVersion) < 0) + return false; + if (gVC.compare(version, this.maxAppVersion) > 0) + return false; + return true; + }, + + isUpdateSecure: function(ignorePrefs) { + if (!gCheckUpdateSecurity && !ignorePrefs) + return true; + + if (!this.updateRDF) + return true; + if (this.updateKey) + return true; + return (this.updateRDF.substring(0, 6) == "https:"); + }, + + needsOverride: function(ignorePrefs) { + return (!this.isCompatible(ignorePrefs) || !this.isUpdateSecure(ignorePrefs)); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nttIAddon, Ci.nsIUpdateItem]), +}; + +function nttAddonCompatibilityService() { +} + +nttAddonCompatibilityService.prototype = { + id: null, + + init: function() { + Components.utils.import("resource://nightly/Logging.jsm"); + + gEM = Cc["@mozilla.org/extensions/manager;1"]. + getService(Ci.nsIExtensionManager); + gRDF = Cc["@mozilla.org/rdf/rdf-service;1"]. + getService(Ci.nsIRDFService); + gApp = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULAppInfo).QueryInterface(Ci.nsIXULRuntime); + gVC = Cc["@mozilla.org/xpcom/version-comparator;1"]. + getService(Ci.nsIVersionComparator); + if (gVC.compare(gApp.platformVersion, "1.9b5") >= 0) + this.id = gEM.addInstallListener(this); + gPrefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService) + .getBranch("extensions.") + .QueryInterface(Components.interfaces.nsIPrefBranch2); + try { + gCheckCompatibility = gPrefs.getBoolPref("checkCompatibility"); + } + catch (e) { } + try { + gCheckUpdateSecurity = gPrefs.getBoolPref("checkUpdateSecurity"); + } + catch (e) { } + gPrefs.addObserver("", this, false); + }, + + // nsIAddonCompatibilityService implementation + getAddonForID: function(id) { + var addon = new nttAddonDetail(); + addon.initWithDataSource(gEM.datasource, gRDF.GetResource(PREFIX_ITEM_URI + id), id); + return addon; + }, + + confirmOverride: function(addons, count) { + var wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + win = wm.getMostRecentWindow("Extension:Manager"); + if (win && win.top) + win = win.top; + + var params = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + for (var i = 0; i < addons.length; i++) + params.appendElement(addons[i], false); + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + ww.openWindow(win, "chrome://nightly/content/extensions/incompatible.xul", "", + "chrome,centerscreen,modal,dialog,titlebar", params); + return true; + }, + + // nsIAddonInstallListener implementation + onDownloadStarted: function(addon) { + }, + + onDownloadProgress: function(addon, value, maxValue) { + }, + + onDownloadEnded: function(addon) { + }, + + onInstallStarted: function(addon) { + LOG("Install Started for " + addon.xpiURL); + var ioServ = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var fph = ioServ.getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + var file = fph.getFileFromURLSpec(addon.xpiURL); + if (file.exists()) { + try { + var addon = new nttAddonDetail(); + addon.initWithXPI(file); + if (addon.isValid()) { + if (!addon.isCompatible(false)) { + // Check if there are remote updates available + var checker = new nttAddonUpdateChecker(addon); + checker.checkForUpdates(); + } + + if (addon.needsOverride(false)) + this.confirmOverride([addon], 1); + else + LOG("Add-on is already compatible: '" + addon.updateRDF + "' " + addon.minAppVersion + "-" + addon.maxAppVersion); + } + else { + WARN("Add-on seems to be invalid"); + } + addon.cleanup(); + } + catch (e) { + ERROR("Exception during compatibility check " + e); + } + } + }, + + onCompatibilityCheckStarted: function(addon) { + }, + + onCompatibilityCheckEnded: function(addon, status) { + }, + + onInstallEnded: function(addon, status) { + }, + + onInstallsCompleted: function() { + }, + + // nsIObserver implementation + observe: function(subject, topic, data) { + switch (topic) { + case "app-startup": + var os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + os.addObserver(this, "profile-after-change", false); + os.addObserver(this, "quit-application", false); + break; + case "profile-after-change": + this.init(); + break; + case "quit-application": + if (this.id) + gEM.removeInstallListenerAt(this.id); + gEM = null; + gRDF = null; + gApp = null; + gVC = null; + gPrefs.removeObserver("", this); + gPrefs = null; + break; + case "nsPref:changed": + switch (data) { + case "checkCompatibility": + try { + gCheckCompatibility = gPrefs.getBoolPref(data); + } + catch (e) { + gCheckCompatibility = true; + } + break; + case "checkUpdateSecurity": + try { + gCheckUpdateSecurity = gPrefs.getBoolPref(data); + } + catch (e) { + gCheckUpdateSecurity = true; + } + break; + } + break; + default: + WARN("Unknown event " + topic); + } + }, + + classDescription: "Nightly Tester Install Monitor", + contractID: "@oxymoronical.com/nightly/addoncompatibility;1", + classID: Components.ID("{801207d5-037c-4565-80ed-ede8f7a7c100}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nttIAddonCompatibilityService, Ci.nsIAddonInstallListener, Ci.nsIObserver]), + _xpcom_categories: [{ + category: "app-startup", + service: true + }] +} + +function NSGetModule(compMgr, fileSpec) + XPCOMUtils.generateModule([nttAddonCompatibilityService]);