michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: /**
michael@0: * The AddonUpdateChecker is responsible for retrieving the update information
michael@0: * from an add-on's remote update manifest.
michael@0: */
michael@0:
michael@0: "use strict";
michael@0:
michael@0: const Cc = Components.classes;
michael@0: const Ci = Components.interfaces;
michael@0: const Cu = Components.utils;
michael@0:
michael@0: this.EXPORTED_SYMBOLS = [ "AddonUpdateChecker" ];
michael@0:
michael@0: const TIMEOUT = 60 * 1000;
michael@0: const PREFIX_NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
michael@0: const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
michael@0: const PREFIX_ITEM = "urn:mozilla:item:";
michael@0: const PREFIX_EXTENSION = "urn:mozilla:extension:";
michael@0: const PREFIX_THEME = "urn:mozilla:theme:";
michael@0: const TOOLKIT_ID = "toolkit@mozilla.org"
michael@0: const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
michael@0:
michael@0: const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
michael@0:
michael@0: Components.utils.import("resource://gre/modules/Services.jsm");
michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0:
michael@0: XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
michael@0: "resource://gre/modules/AddonManager.jsm");
michael@0: XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
michael@0: "resource://gre/modules/addons/AddonRepository.jsm");
michael@0:
michael@0: // Shared code for suppressing bad cert dialogs.
michael@0: XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() {
michael@0: let certUtils = {};
michael@0: Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
michael@0: return certUtils;
michael@0: });
michael@0:
michael@0: var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].
michael@0: getService(Ci.nsIRDFService);
michael@0:
michael@0: Cu.import("resource://gre/modules/Log.jsm");
michael@0: const LOGGER_ID = "addons.update-checker";
michael@0:
michael@0: // Create a new logger for use by the Addons Update Checker
michael@0: // (Requires AddonManager.jsm)
michael@0: let logger = Log.repository.getLogger(LOGGER_ID);
michael@0:
michael@0: /**
michael@0: * A serialisation method for RDF data that produces an identical string
michael@0: * for matching RDF graphs.
michael@0: * The serialisation is not complete, only assertions stemming from a given
michael@0: * resource are included, multiple references to the same resource are not
michael@0: * permitted, and the RDF prolog and epilog are not included.
michael@0: * RDF Blob and Date literals are not supported.
michael@0: */
michael@0: function RDFSerializer() {
michael@0: this.cUtils = Cc["@mozilla.org/rdf/container-utils;1"].
michael@0: getService(Ci.nsIRDFContainerUtils);
michael@0: this.resources = [];
michael@0: }
michael@0:
michael@0: RDFSerializer.prototype = {
michael@0: INDENT: " ", // The indent used for pretty-printing
michael@0: resources: null, // Array of the resources that have been found
michael@0:
michael@0: /**
michael@0: * Escapes characters from a string that should not appear in XML.
michael@0: *
michael@0: * @param aString
michael@0: * The string to be escaped
michael@0: * @return a string with all characters invalid in XML character data
michael@0: * converted to entity references.
michael@0: */
michael@0: escapeEntities: function RDFS_escapeEntities(aString) {
michael@0: aString = aString.replace(/&/g, "&");
michael@0: aString = aString.replace(//g, ">");
michael@0: return aString.replace(/"/g, """);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Serializes all the elements of an RDF container.
michael@0: *
michael@0: * @param aDs
michael@0: * The RDF datasource
michael@0: * @param aContainer
michael@0: * The RDF container to output the child elements of
michael@0: * @param aIndent
michael@0: * The current level of indent for pretty-printing
michael@0: * @return a string containing the serialized elements.
michael@0: */
michael@0: serializeContainerItems: function RDFS_serializeContainerItems(aDs, aContainer,
michael@0: aIndent) {
michael@0: var result = "";
michael@0: var items = aContainer.GetElements();
michael@0: while (items.hasMoreElements()) {
michael@0: var item = items.getNext().QueryInterface(Ci.nsIRDFResource);
michael@0: result += aIndent + "\n"
michael@0: result += this.serializeResource(aDs, item, aIndent + this.INDENT);
michael@0: result += aIndent + "\n"
michael@0: }
michael@0: return result;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Serializes all em:* (see EM_NS) properties of an RDF resource except for
michael@0: * the em:signature property. As this serialization is to be compared against
michael@0: * the manifest signature it cannot contain the em:signature property itself.
michael@0: *
michael@0: * @param aDs
michael@0: * The RDF datasource
michael@0: * @param aResource
michael@0: * The RDF resource that contains the properties to serialize
michael@0: * @param aIndent
michael@0: * The current level of indent for pretty-printing
michael@0: * @return a string containing the serialized properties.
michael@0: * @throws if the resource contains a property that cannot be serialized
michael@0: */
michael@0: serializeResourceProperties: function RDFS_serializeResourceProperties(aDs,
michael@0: aResource,
michael@0: aIndent) {
michael@0: var result = "";
michael@0: var items = [];
michael@0: var arcs = aDs.ArcLabelsOut(aResource);
michael@0: while (arcs.hasMoreElements()) {
michael@0: var arc = arcs.getNext().QueryInterface(Ci.nsIRDFResource);
michael@0: if (arc.ValueUTF8.substring(0, PREFIX_NS_EM.length) != PREFIX_NS_EM)
michael@0: continue;
michael@0: var prop = arc.ValueUTF8.substring(PREFIX_NS_EM.length);
michael@0: if (prop == "signature")
michael@0: continue;
michael@0:
michael@0: var targets = aDs.GetTargets(aResource, arc, true);
michael@0: while (targets.hasMoreElements()) {
michael@0: var target = targets.getNext();
michael@0: if (target instanceof Ci.nsIRDFResource) {
michael@0: var item = aIndent + "\n";
michael@0: item += this.serializeResource(aDs, target, aIndent + this.INDENT);
michael@0: item += aIndent + "\n";
michael@0: items.push(item);
michael@0: }
michael@0: else if (target instanceof Ci.nsIRDFLiteral) {
michael@0: items.push(aIndent + "" +
michael@0: this.escapeEntities(target.Value) + "\n");
michael@0: }
michael@0: else if (target instanceof Ci.nsIRDFInt) {
michael@0: items.push(aIndent + "" +
michael@0: target.Value + "\n");
michael@0: }
michael@0: else {
michael@0: throw Components.Exception("Cannot serialize unknown literal type");
michael@0: }
michael@0: }
michael@0: }
michael@0: items.sort();
michael@0: result += items.join("");
michael@0: return result;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Recursively serializes an RDF resource and all resources it links to.
michael@0: * This will only output EM_NS properties and will ignore any em:signature
michael@0: * property.
michael@0: *
michael@0: * @param aDs
michael@0: * The RDF datasource
michael@0: * @param aResource
michael@0: * The RDF resource to serialize
michael@0: * @param aIndent
michael@0: * The current level of indent for pretty-printing. If undefined no
michael@0: * indent will be added
michael@0: * @return a string containing the serialized resource.
michael@0: * @throws if the RDF data contains multiple references to the same resource.
michael@0: */
michael@0: serializeResource: function RDFS_serializeResource(aDs, aResource, aIndent) {
michael@0: if (this.resources.indexOf(aResource) != -1 ) {
michael@0: // We cannot output multiple references to the same resource.
michael@0: throw Components.Exception("Cannot serialize multiple references to " + aResource.Value);
michael@0: }
michael@0: if (aIndent === undefined)
michael@0: aIndent = "";
michael@0:
michael@0: this.resources.push(aResource);
michael@0: var container = null;
michael@0: var type = "Description";
michael@0: if (this.cUtils.IsSeq(aDs, aResource)) {
michael@0: type = "Seq";
michael@0: container = this.cUtils.MakeSeq(aDs, aResource);
michael@0: }
michael@0: else if (this.cUtils.IsAlt(aDs, aResource)) {
michael@0: type = "Alt";
michael@0: container = this.cUtils.MakeAlt(aDs, aResource);
michael@0: }
michael@0: else if (this.cUtils.IsBag(aDs, aResource)) {
michael@0: type = "Bag";
michael@0: container = this.cUtils.MakeBag(aDs, aResource);
michael@0: }
michael@0:
michael@0: var result = aIndent + "\n";
michael@0:
michael@0: if (container)
michael@0: result += this.serializeContainerItems(aDs, container, aIndent + this.INDENT);
michael@0:
michael@0: result += this.serializeResourceProperties(aDs, aResource, aIndent + this.INDENT);
michael@0:
michael@0: result += aIndent + "\n";
michael@0: return result;
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Parses an RDF style update manifest into an array of update objects.
michael@0: *
michael@0: * @param aId
michael@0: * The ID of the add-on being checked for updates
michael@0: * @param aUpdateKey
michael@0: * An optional update key for the add-on
michael@0: * @param aRequest
michael@0: * The XMLHttpRequest that has retrieved the update manifest
michael@0: * @return an array of update objects
michael@0: * @throws if the update manifest is invalid in any way
michael@0: */
michael@0: function parseRDFManifest(aId, aUpdateKey, aRequest) {
michael@0: function EM_R(aProp) {
michael@0: return gRDF.GetResource(PREFIX_NS_EM + aProp);
michael@0: }
michael@0:
michael@0: function getValue(aLiteral) {
michael@0: if (aLiteral instanceof Ci.nsIRDFLiteral)
michael@0: return aLiteral.Value;
michael@0: if (aLiteral instanceof Ci.nsIRDFResource)
michael@0: return aLiteral.Value;
michael@0: if (aLiteral instanceof Ci.nsIRDFInt)
michael@0: return aLiteral.Value;
michael@0: return null;
michael@0: }
michael@0:
michael@0: function getProperty(aDs, aSource, aProperty) {
michael@0: return getValue(aDs.GetTarget(aSource, EM_R(aProperty), true));
michael@0: }
michael@0:
michael@0: function getRequiredProperty(aDs, aSource, aProperty) {
michael@0: let value = getProperty(aDs, aSource, aProperty);
michael@0: if (!value)
michael@0: throw Components.Exception("Update manifest is missing a required " + aProperty + " property.");
michael@0: return value;
michael@0: }
michael@0:
michael@0: let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
michael@0: createInstance(Ci.nsIRDFXMLParser);
michael@0: let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
michael@0: createInstance(Ci.nsIRDFDataSource);
michael@0: rdfParser.parseString(ds, aRequest.channel.URI, aRequest.responseText);
michael@0:
michael@0: // Differentiating between add-on types is deprecated
michael@0: let extensionRes = gRDF.GetResource(PREFIX_EXTENSION + aId);
michael@0: let themeRes = gRDF.GetResource(PREFIX_THEME + aId);
michael@0: let itemRes = gRDF.GetResource(PREFIX_ITEM + aId);
michael@0: let addonRes = ds.ArcLabelsOut(extensionRes).hasMoreElements() ? extensionRes
michael@0: : ds.ArcLabelsOut(themeRes).hasMoreElements() ? themeRes
michael@0: : itemRes;
michael@0:
michael@0: // If we have an update key then the update manifest must be signed
michael@0: if (aUpdateKey) {
michael@0: let signature = getProperty(ds, addonRes, "signature");
michael@0: if (!signature)
michael@0: throw Components.Exception("Update manifest for " + aId + " does not contain a required signature");
michael@0: let serializer = new RDFSerializer();
michael@0: let updateString = null;
michael@0:
michael@0: try {
michael@0: updateString = serializer.serializeResource(ds, addonRes);
michael@0: }
michael@0: catch (e) {
michael@0: throw Components.Exception("Failed to generate signed string for " + aId + ". Serializer threw " + e,
michael@0: e.result);
michael@0: }
michael@0:
michael@0: let result = false;
michael@0:
michael@0: try {
michael@0: let verifier = Cc["@mozilla.org/security/datasignatureverifier;1"].
michael@0: getService(Ci.nsIDataSignatureVerifier);
michael@0: result = verifier.verifyData(updateString, signature, aUpdateKey);
michael@0: }
michael@0: catch (e) {
michael@0: throw Components.Exception("The signature or updateKey for " + aId + " is malformed." +
michael@0: "Verifier threw " + e, e.result);
michael@0: }
michael@0:
michael@0: if (!result)
michael@0: throw Components.Exception("The signature for " + aId + " was not created by the add-on's updateKey");
michael@0: }
michael@0:
michael@0: let updates = ds.GetTarget(addonRes, EM_R("updates"), true);
michael@0:
michael@0: // A missing updates property doesn't count as a failure, just as no avialable
michael@0: // update information
michael@0: if (!updates) {
michael@0: logger.warn("Update manifest for " + aId + " did not contain an updates property");
michael@0: return [];
michael@0: }
michael@0:
michael@0: if (!(updates instanceof Ci.nsIRDFResource))
michael@0: throw Components.Exception("Missing updates property for " + addonRes.Value);
michael@0:
michael@0: let cu = Cc["@mozilla.org/rdf/container-utils;1"].
michael@0: getService(Ci.nsIRDFContainerUtils);
michael@0: if (!cu.IsContainer(ds, updates))
michael@0: throw Components.Exception("Updates property was not an RDF container");
michael@0:
michael@0: let results = [];
michael@0: let ctr = Cc["@mozilla.org/rdf/container;1"].
michael@0: createInstance(Ci.nsIRDFContainer);
michael@0: ctr.Init(ds, updates);
michael@0: let items = ctr.GetElements();
michael@0: while (items.hasMoreElements()) {
michael@0: let item = items.getNext().QueryInterface(Ci.nsIRDFResource);
michael@0: let version = getProperty(ds, item, "version");
michael@0: if (!version) {
michael@0: logger.warn("Update manifest is missing a required version property.");
michael@0: continue;
michael@0: }
michael@0:
michael@0: logger.debug("Found an update entry for " + aId + " version " + version);
michael@0:
michael@0: let targetApps = ds.GetTargets(item, EM_R("targetApplication"), true);
michael@0: while (targetApps.hasMoreElements()) {
michael@0: let targetApp = targetApps.getNext().QueryInterface(Ci.nsIRDFResource);
michael@0:
michael@0: let appEntry = {};
michael@0: try {
michael@0: appEntry.id = getRequiredProperty(ds, targetApp, "id");
michael@0: appEntry.minVersion = getRequiredProperty(ds, targetApp, "minVersion");
michael@0: appEntry.maxVersion = getRequiredProperty(ds, targetApp, "maxVersion");
michael@0: }
michael@0: catch (e) {
michael@0: logger.warn(e);
michael@0: continue;
michael@0: }
michael@0:
michael@0: let result = {
michael@0: id: aId,
michael@0: version: version,
michael@0: updateURL: getProperty(ds, targetApp, "updateLink"),
michael@0: updateHash: getProperty(ds, targetApp, "updateHash"),
michael@0: updateInfoURL: getProperty(ds, targetApp, "updateInfoURL"),
michael@0: strictCompatibility: getProperty(ds, targetApp, "strictCompatibility") == "true",
michael@0: targetApplications: [appEntry]
michael@0: };
michael@0:
michael@0: if (result.updateURL && AddonManager.checkUpdateSecurity &&
michael@0: result.updateURL.substring(0, 6) != "https:" &&
michael@0: (!result.updateHash || result.updateHash.substring(0, 3) != "sha")) {
michael@0: logger.warn("updateLink " + result.updateURL + " is not secure and is not verified" +
michael@0: " by a strong enough hash (needs to be sha1 or stronger).");
michael@0: delete result.updateURL;
michael@0: delete result.updateHash;
michael@0: }
michael@0: results.push(result);
michael@0: }
michael@0: }
michael@0: return results;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Starts downloading an update manifest and then passes it to an appropriate
michael@0: * parser to convert to an array of update objects
michael@0: *
michael@0: * @param aId
michael@0: * The ID of the add-on being checked for updates
michael@0: * @param aUpdateKey
michael@0: * An optional update key for the add-on
michael@0: * @param aUrl
michael@0: * The URL of the update manifest
michael@0: * @param aObserver
michael@0: * An observer to pass results to
michael@0: */
michael@0: function UpdateParser(aId, aUpdateKey, aUrl, aObserver) {
michael@0: this.id = aId;
michael@0: this.updateKey = aUpdateKey;
michael@0: this.observer = aObserver;
michael@0: this.url = aUrl;
michael@0:
michael@0: let requireBuiltIn = true;
michael@0: try {
michael@0: requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
michael@0: }
michael@0: catch (e) {
michael@0: }
michael@0:
michael@0: logger.debug("Requesting " + aUrl);
michael@0: try {
michael@0: this.request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
michael@0: createInstance(Ci.nsIXMLHttpRequest);
michael@0: this.request.open("GET", this.url, true);
michael@0: this.request.channel.notificationCallbacks = new CertUtils.BadCertHandler(!requireBuiltIn);
michael@0: this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
michael@0: // Prevent the request from writing to cache.
michael@0: this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
michael@0: this.request.overrideMimeType("text/xml");
michael@0: this.request.timeout = TIMEOUT;
michael@0: var self = this;
michael@0: this.request.addEventListener("load", function loadEventListener(event) { self.onLoad() }, false);
michael@0: this.request.addEventListener("error", function errorEventListener(event) { self.onError() }, false);
michael@0: this.request.addEventListener("timeout", function timeoutEventListener(event) { self.onTimeout() }, false);
michael@0: this.request.send(null);
michael@0: }
michael@0: catch (e) {
michael@0: logger.error("Failed to request update manifest", e);
michael@0: }
michael@0: }
michael@0:
michael@0: UpdateParser.prototype = {
michael@0: id: null,
michael@0: updateKey: null,
michael@0: observer: null,
michael@0: request: null,
michael@0: url: null,
michael@0:
michael@0: /**
michael@0: * Called when the manifest has been successfully loaded.
michael@0: */
michael@0: onLoad: function UP_onLoad() {
michael@0: let request = this.request;
michael@0: this.request = null;
michael@0:
michael@0: let requireBuiltIn = true;
michael@0: try {
michael@0: requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
michael@0: }
michael@0: catch (e) {
michael@0: }
michael@0:
michael@0: try {
michael@0: CertUtils.checkCert(request.channel, !requireBuiltIn);
michael@0: }
michael@0: catch (e) {
michael@0: this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
michael@0: return;
michael@0: }
michael@0:
michael@0: if (!Components.isSuccessCode(request.status)) {
michael@0: logger.warn("Request failed: " + this.url + " - " + request.status);
michael@0: this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
michael@0: return;
michael@0: }
michael@0:
michael@0: let channel = request.channel;
michael@0: if (channel instanceof Ci.nsIHttpChannel && !channel.requestSucceeded) {
michael@0: logger.warn("Request failed: " + this.url + " - " + channel.responseStatus +
michael@0: ": " + channel.responseStatusText);
michael@0: this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
michael@0: return;
michael@0: }
michael@0:
michael@0: let xml = request.responseXML;
michael@0: if (!xml || xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR) {
michael@0: logger.warn("Update manifest was not valid XML");
michael@0: this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
michael@0: return;
michael@0: }
michael@0:
michael@0: // We currently only know about RDF update manifests
michael@0: if (xml.documentElement.namespaceURI == PREFIX_NS_RDF) {
michael@0: let results = null;
michael@0:
michael@0: try {
michael@0: results = parseRDFManifest(this.id, this.updateKey, request);
michael@0: }
michael@0: catch (e) {
michael@0: logger.warn(e);
michael@0: this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
michael@0: return;
michael@0: }
michael@0: if ("onUpdateCheckComplete" in this.observer) {
michael@0: try {
michael@0: this.observer.onUpdateCheckComplete(results);
michael@0: }
michael@0: catch (e) {
michael@0: logger.warn("onUpdateCheckComplete notification failed", e);
michael@0: }
michael@0: }
michael@0: return;
michael@0: }
michael@0:
michael@0: logger.warn("Update manifest had an unrecognised namespace: " + xml.documentElement.namespaceURI);
michael@0: this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Called when the request times out
michael@0: */
michael@0: onTimeout: function() {
michael@0: this.request = null;
michael@0: logger.warn("Request for " + this.url + " timed out");
michael@0: this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Called when the manifest failed to load.
michael@0: */
michael@0: onError: function UP_onError() {
michael@0: if (!Components.isSuccessCode(this.request.status)) {
michael@0: logger.warn("Request failed: " + this.url + " - " + this.request.status);
michael@0: }
michael@0: else if (this.request.channel instanceof Ci.nsIHttpChannel) {
michael@0: try {
michael@0: if (this.request.channel.requestSucceeded) {
michael@0: logger.warn("Request failed: " + this.url + " - " +
michael@0: this.request.channel.responseStatus + ": " +
michael@0: this.request.channel.responseStatusText);
michael@0: }
michael@0: }
michael@0: catch (e) {
michael@0: logger.warn("HTTP Request failed for an unknown reason");
michael@0: }
michael@0: }
michael@0: else {
michael@0: logger.warn("Request failed for an unknown reason");
michael@0: }
michael@0:
michael@0: this.request = null;
michael@0:
michael@0: this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Helper method to notify the observer that an error occured.
michael@0: */
michael@0: notifyError: function UP_notifyError(aStatus) {
michael@0: if ("onUpdateCheckError" in this.observer) {
michael@0: try {
michael@0: this.observer.onUpdateCheckError(aStatus);
michael@0: }
michael@0: catch (e) {
michael@0: logger.warn("onUpdateCheckError notification failed", e);
michael@0: }
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Called to cancel an in-progress update check.
michael@0: */
michael@0: cancel: function UP_cancel() {
michael@0: this.request.abort();
michael@0: this.request = null;
michael@0: this.notifyError(AddonUpdateChecker.ERROR_CANCELLED);
michael@0: }
michael@0: };
michael@0:
michael@0: /**
michael@0: * Tests if an update matches a version of the application or platform
michael@0: *
michael@0: * @param aUpdate
michael@0: * The available update
michael@0: * @param aAppVersion
michael@0: * The application version to use
michael@0: * @param aPlatformVersion
michael@0: * The platform version to use
michael@0: * @param aIgnoreMaxVersion
michael@0: * Ignore maxVersion when testing if an update matches. Optional.
michael@0: * @param aIgnoreStrictCompat
michael@0: * Ignore strictCompatibility when testing if an update matches. Optional.
michael@0: * @param aCompatOverrides
michael@0: * AddonCompatibilityOverride objects to match against. Optional.
michael@0: * @return true if the update is compatible with the application/platform
michael@0: */
michael@0: function matchesVersions(aUpdate, aAppVersion, aPlatformVersion,
michael@0: aIgnoreMaxVersion, aIgnoreStrictCompat,
michael@0: aCompatOverrides) {
michael@0: if (aCompatOverrides) {
michael@0: let override = AddonRepository.findMatchingCompatOverride(aUpdate.version,
michael@0: aCompatOverrides,
michael@0: aAppVersion,
michael@0: aPlatformVersion);
michael@0: if (override && override.type == "incompatible")
michael@0: return false;
michael@0: }
michael@0:
michael@0: if (aUpdate.strictCompatibility && !aIgnoreStrictCompat)
michael@0: aIgnoreMaxVersion = false;
michael@0:
michael@0: let result = false;
michael@0: for (let app of aUpdate.targetApplications) {
michael@0: if (app.id == Services.appinfo.ID) {
michael@0: return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) &&
michael@0: (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0));
michael@0: }
michael@0: if (app.id == TOOLKIT_ID) {
michael@0: result = (Services.vc.compare(aPlatformVersion, app.minVersion) >= 0) &&
michael@0: (aIgnoreMaxVersion || (Services.vc.compare(aPlatformVersion, app.maxVersion) <= 0));
michael@0: }
michael@0: }
michael@0: return result;
michael@0: }
michael@0:
michael@0: this.AddonUpdateChecker = {
michael@0: // These must be kept in sync with AddonManager
michael@0: // The update check timed out
michael@0: ERROR_TIMEOUT: -1,
michael@0: // There was an error while downloading the update information.
michael@0: ERROR_DOWNLOAD_ERROR: -2,
michael@0: // The update information was malformed in some way.
michael@0: ERROR_PARSE_ERROR: -3,
michael@0: // The update information was not in any known format.
michael@0: ERROR_UNKNOWN_FORMAT: -4,
michael@0: // The update information was not correctly signed or there was an SSL error.
michael@0: ERROR_SECURITY_ERROR: -5,
michael@0: // The update was cancelled
michael@0: ERROR_CANCELLED: -6,
michael@0:
michael@0: /**
michael@0: * Retrieves the best matching compatibility update for the application from
michael@0: * a list of available update objects.
michael@0: *
michael@0: * @param aUpdates
michael@0: * An array of update objects
michael@0: * @param aVersion
michael@0: * The version of the add-on to get new compatibility information for
michael@0: * @param aIgnoreCompatibility
michael@0: * An optional parameter to get the first compatibility update that
michael@0: * is compatible with any version of the application or toolkit
michael@0: * @param aAppVersion
michael@0: * The version of the application or null to use the current version
michael@0: * @param aPlatformVersion
michael@0: * The version of the platform or null to use the current version
michael@0: * @param aIgnoreMaxVersion
michael@0: * Ignore maxVersion when testing if an update matches. Optional.
michael@0: * @param aIgnoreStrictCompat
michael@0: * Ignore strictCompatibility when testing if an update matches. Optional.
michael@0: * @return an update object if one matches or null if not
michael@0: */
michael@0: getCompatibilityUpdate: function AUC_getCompatibilityUpdate(aUpdates, aVersion,
michael@0: aIgnoreCompatibility,
michael@0: aAppVersion,
michael@0: aPlatformVersion,
michael@0: aIgnoreMaxVersion,
michael@0: aIgnoreStrictCompat) {
michael@0: if (!aAppVersion)
michael@0: aAppVersion = Services.appinfo.version;
michael@0: if (!aPlatformVersion)
michael@0: aPlatformVersion = Services.appinfo.platformVersion;
michael@0:
michael@0: for (let update of aUpdates) {
michael@0: if (Services.vc.compare(update.version, aVersion) == 0) {
michael@0: if (aIgnoreCompatibility) {
michael@0: for (let targetApp of update.targetApplications) {
michael@0: let id = targetApp.id;
michael@0: if (id == Services.appinfo.ID || id == TOOLKIT_ID)
michael@0: return update;
michael@0: }
michael@0: }
michael@0: else if (matchesVersions(update, aAppVersion, aPlatformVersion,
michael@0: aIgnoreMaxVersion, aIgnoreStrictCompat)) {
michael@0: return update;
michael@0: }
michael@0: }
michael@0: }
michael@0: return null;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Returns the newest available update from a list of update objects.
michael@0: *
michael@0: * @param aUpdates
michael@0: * An array of update objects
michael@0: * @param aAppVersion
michael@0: * The version of the application or null to use the current version
michael@0: * @param aPlatformVersion
michael@0: * The version of the platform or null to use the current version
michael@0: * @param aIgnoreMaxVersion
michael@0: * When determining compatible updates, ignore maxVersion. Optional.
michael@0: * @param aIgnoreStrictCompat
michael@0: * When determining compatible updates, ignore strictCompatibility. Optional.
michael@0: * @param aCompatOverrides
michael@0: * Array of AddonCompatibilityOverride to take into account. Optional.
michael@0: * @return an update object if one matches or null if not
michael@0: */
michael@0: getNewestCompatibleUpdate: function AUC_getNewestCompatibleUpdate(aUpdates,
michael@0: aAppVersion,
michael@0: aPlatformVersion,
michael@0: aIgnoreMaxVersion,
michael@0: aIgnoreStrictCompat,
michael@0: aCompatOverrides) {
michael@0: if (!aAppVersion)
michael@0: aAppVersion = Services.appinfo.version;
michael@0: if (!aPlatformVersion)
michael@0: aPlatformVersion = Services.appinfo.platformVersion;
michael@0:
michael@0: let blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
michael@0: getService(Ci.nsIBlocklistService);
michael@0:
michael@0: let newest = null;
michael@0: for (let update of aUpdates) {
michael@0: if (!update.updateURL)
michael@0: continue;
michael@0: let state = blocklist.getAddonBlocklistState(update, aAppVersion, aPlatformVersion);
michael@0: if (state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
michael@0: continue;
michael@0: if ((newest == null || (Services.vc.compare(newest.version, update.version) < 0)) &&
michael@0: matchesVersions(update, aAppVersion, aPlatformVersion,
michael@0: aIgnoreMaxVersion, aIgnoreStrictCompat,
michael@0: aCompatOverrides)) {
michael@0: newest = update;
michael@0: }
michael@0: }
michael@0: return newest;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Starts an update check.
michael@0: *
michael@0: * @param aId
michael@0: * The ID of the add-on being checked for updates
michael@0: * @param aUpdateKey
michael@0: * An optional update key for the add-on
michael@0: * @param aUrl
michael@0: * The URL of the add-on's update manifest
michael@0: * @param aObserver
michael@0: * An observer to notify of results
michael@0: * @return UpdateParser so that the caller can use UpdateParser.cancel() to shut
michael@0: * down in-progress update requests
michael@0: */
michael@0: checkForUpdates: function AUC_checkForUpdates(aId, aUpdateKey, aUrl,
michael@0: aObserver) {
michael@0: return new UpdateParser(aId, aUpdateKey, aUrl, aObserver);
michael@0: }
michael@0: };