summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/webextensions/internal/AddonUpdateChecker.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/webextensions/internal/AddonUpdateChecker.jsm')
-rw-r--r--toolkit/mozapps/webextensions/internal/AddonUpdateChecker.jsm936
1 files changed, 936 insertions, 0 deletions
diff --git a/toolkit/mozapps/webextensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/webextensions/internal/AddonUpdateChecker.jsm
new file mode 100644
index 000000000..918ba5328
--- /dev/null
+++ b/toolkit/mozapps/webextensions/internal/AddonUpdateChecker.jsm
@@ -0,0 +1,936 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * The AddonUpdateChecker is responsible for retrieving the update information
+ * from an add-on's remote update manifest.
+ */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "AddonUpdateChecker" ];
+
+const TIMEOUT = 60 * 1000;
+const PREFIX_NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
+const PREFIX_ITEM = "urn:mozilla:item:";
+const PREFIX_EXTENSION = "urn:mozilla:extension:";
+const PREFIX_THEME = "urn:mozilla:theme:";
+const TOOLKIT_ID = "toolkit@mozilla.org"
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
+
+const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest",
+ "resource://gre/modules/ServiceRequest.jsm");
+
+
+// Shared code for suppressing bad cert dialogs.
+XPCOMUtils.defineLazyGetter(this, "CertUtils", function() {
+ let certUtils = {};
+ Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
+ return certUtils;
+});
+
+var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].
+ getService(Ci.nsIRDFService);
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.update-checker";
+
+// Create a new logger for use by the Addons Update Checker
+// (Requires AddonManager.jsm)
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+/**
+ * A serialisation method for RDF data that produces an identical string
+ * for matching RDF graphs.
+ * The serialisation is not complete, only assertions stemming from a given
+ * resource are included, multiple references to the same resource are not
+ * permitted, and the RDF prolog and epilog are not included.
+ * RDF Blob and Date literals are not supported.
+ */
+function RDFSerializer() {
+ this.cUtils = Cc["@mozilla.org/rdf/container-utils;1"].
+ getService(Ci.nsIRDFContainerUtils);
+ this.resources = [];
+}
+
+RDFSerializer.prototype = {
+ INDENT: " ", // The indent used for pretty-printing
+ resources: null, // Array of the resources that have been found
+
+ /**
+ * Escapes characters from a string that should not appear in XML.
+ *
+ * @param aString
+ * The string to be escaped
+ * @return a string with all characters invalid in XML character data
+ * converted to entity references.
+ */
+ escapeEntities: function(aString) {
+ aString = aString.replace(/&/g, "&");
+ aString = aString.replace(/</g, "&lt;");
+ aString = aString.replace(/>/g, "&gt;");
+ return aString.replace(/"/g, "&quot;");
+ },
+
+ /**
+ * Serializes all the elements of an RDF container.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aContainer
+ * The RDF container to output the child elements of
+ * @param aIndent
+ * The current level of indent for pretty-printing
+ * @return a string containing the serialized elements.
+ */
+ serializeContainerItems: function(aDs, aContainer, aIndent) {
+ var result = "";
+ var items = aContainer.GetElements();
+ while (items.hasMoreElements()) {
+ var item = items.getNext().QueryInterface(Ci.nsIRDFResource);
+ result += aIndent + "<RDF:li>\n"
+ result += this.serializeResource(aDs, item, aIndent + this.INDENT);
+ result += aIndent + "</RDF:li>\n"
+ }
+ return result;
+ },
+
+ /**
+ * Serializes all em:* (see EM_NS) properties of an RDF resource except for
+ * the em:signature property. As this serialization is to be compared against
+ * the manifest signature it cannot contain the em:signature property itself.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aResource
+ * The RDF resource that contains the properties to serialize
+ * @param aIndent
+ * The current level of indent for pretty-printing
+ * @return a string containing the serialized properties.
+ * @throws if the resource contains a property that cannot be serialized
+ */
+ serializeResourceProperties: function(aDs, aResource, aIndent) {
+ var result = "";
+ var items = [];
+ var arcs = aDs.ArcLabelsOut(aResource);
+ while (arcs.hasMoreElements()) {
+ var arc = arcs.getNext().QueryInterface(Ci.nsIRDFResource);
+ if (arc.ValueUTF8.substring(0, PREFIX_NS_EM.length) != PREFIX_NS_EM)
+ continue;
+ var prop = arc.ValueUTF8.substring(PREFIX_NS_EM.length);
+ if (prop == "signature")
+ continue;
+
+ var targets = aDs.GetTargets(aResource, arc, true);
+ while (targets.hasMoreElements()) {
+ var target = targets.getNext();
+ if (target instanceof Ci.nsIRDFResource) {
+ var item = aIndent + "<em:" + prop + ">\n";
+ item += this.serializeResource(aDs, target, aIndent + this.INDENT);
+ item += aIndent + "</em:" + prop + ">\n";
+ items.push(item);
+ }
+ else if (target instanceof Ci.nsIRDFLiteral) {
+ items.push(aIndent + "<em:" + prop + ">" +
+ this.escapeEntities(target.Value) + "</em:" + prop + ">\n");
+ }
+ else if (target instanceof Ci.nsIRDFInt) {
+ items.push(aIndent + "<em:" + prop + " NC:parseType=\"Integer\">" +
+ target.Value + "</em:" + prop + ">\n");
+ }
+ else {
+ throw Components.Exception("Cannot serialize unknown literal type");
+ }
+ }
+ }
+ items.sort();
+ result += items.join("");
+ return result;
+ },
+
+ /**
+ * Recursively serializes an RDF resource and all resources it links to.
+ * This will only output EM_NS properties and will ignore any em:signature
+ * property.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aResource
+ * The RDF resource to serialize
+ * @param aIndent
+ * The current level of indent for pretty-printing. If undefined no
+ * indent will be added
+ * @return a string containing the serialized resource.
+ * @throws if the RDF data contains multiple references to the same resource.
+ */
+ serializeResource: function(aDs, aResource, aIndent) {
+ if (this.resources.indexOf(aResource) != -1 ) {
+ // We cannot output multiple references to the same resource.
+ throw Components.Exception("Cannot serialize multiple references to " + aResource.Value);
+ }
+ if (aIndent === undefined)
+ aIndent = "";
+
+ this.resources.push(aResource);
+ var container = null;
+ var type = "Description";
+ if (this.cUtils.IsSeq(aDs, aResource)) {
+ type = "Seq";
+ container = this.cUtils.MakeSeq(aDs, aResource);
+ }
+ else if (this.cUtils.IsAlt(aDs, aResource)) {
+ type = "Alt";
+ container = this.cUtils.MakeAlt(aDs, aResource);
+ }
+ else if (this.cUtils.IsBag(aDs, aResource)) {
+ type = "Bag";
+ container = this.cUtils.MakeBag(aDs, aResource);
+ }
+
+ var result = aIndent + "<RDF:" + type;
+ if (!gRDF.IsAnonymousResource(aResource))
+ result += " about=\"" + this.escapeEntities(aResource.ValueUTF8) + "\"";
+ result += ">\n";
+
+ if (container)
+ result += this.serializeContainerItems(aDs, container, aIndent + this.INDENT);
+
+ result += this.serializeResourceProperties(aDs, aResource, aIndent + this.INDENT);
+
+ result += aIndent + "</RDF:" + type + ">\n";
+ return result;
+ }
+}
+
+/**
+ * Sanitizes the update URL in an update item, as returned by
+ * parseRDFManifest and parseJSONManifest. Ensures that:
+ *
+ * - The URL is secure, or secured by a strong enough hash.
+ * - The security principal of the update manifest has permission to
+ * load the URL.
+ *
+ * @param aUpdate
+ * The update item to sanitize.
+ * @param aRequest
+ * The XMLHttpRequest used to load the manifest.
+ * @param aHashPattern
+ * The regular expression used to validate the update hash.
+ * @param aHashString
+ * The human-readable string specifying which hash functions
+ * are accepted.
+ */
+function sanitizeUpdateURL(aUpdate, aRequest, aHashPattern, aHashString) {
+ if (aUpdate.updateURL) {
+ let scriptSecurity = Services.scriptSecurityManager;
+ let principal = scriptSecurity.getChannelURIPrincipal(aRequest.channel);
+ try {
+ // This logs an error on failure, so no need to log it a second time
+ scriptSecurity.checkLoadURIStrWithPrincipal(principal, aUpdate.updateURL,
+ scriptSecurity.DISALLOW_SCRIPT);
+ } catch (e) {
+ delete aUpdate.updateURL;
+ return;
+ }
+
+ if (AddonManager.checkUpdateSecurity &&
+ !aUpdate.updateURL.startsWith("https:") &&
+ !aHashPattern.test(aUpdate.updateHash)) {
+ logger.warn(`Update link ${aUpdate.updateURL} is not secure and is not verified ` +
+ `by a strong enough hash (needs to be ${aHashString}).`);
+ delete aUpdate.updateURL;
+ delete aUpdate.updateHash;
+ }
+ }
+}
+
+/**
+ * Parses an RDF style update manifest into an array of update objects.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aRequest
+ * The XMLHttpRequest that has retrieved the update manifest
+ * @param aManifestData
+ * The pre-parsed manifest, as a bare XML DOM document
+ * @return an array of update objects
+ * @throws if the update manifest is invalid in any way
+ */
+function parseRDFManifest(aId, aUpdateKey, aRequest, aManifestData) {
+ if (aManifestData.documentElement.namespaceURI != PREFIX_NS_RDF) {
+ throw Components.Exception("Update manifest had an unrecognised namespace: " +
+ aManifestData.documentElement.namespaceURI);
+ }
+
+ function EM_R(aProp) {
+ return gRDF.GetResource(PREFIX_NS_EM + aProp);
+ }
+
+ function getValue(aLiteral) {
+ if (aLiteral instanceof Ci.nsIRDFLiteral)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFResource)
+ return aLiteral.Value;
+ if (aLiteral instanceof Ci.nsIRDFInt)
+ return aLiteral.Value;
+ return null;
+ }
+
+ function getProperty(aDs, aSource, aProperty) {
+ return getValue(aDs.GetTarget(aSource, EM_R(aProperty), true));
+ }
+
+ function getBooleanProperty(aDs, aSource, aProperty) {
+ let propValue = aDs.GetTarget(aSource, EM_R(aProperty), true);
+ if (!propValue)
+ return undefined;
+ return getValue(propValue) == "true";
+ }
+
+ function getRequiredProperty(aDs, aSource, aProperty) {
+ let value = getProperty(aDs, aSource, aProperty);
+ if (!value)
+ throw Components.Exception("Update manifest is missing a required " + aProperty + " property.");
+ return value;
+ }
+
+ let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
+ createInstance(Ci.nsIRDFXMLParser);
+ let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
+ createInstance(Ci.nsIRDFDataSource);
+ rdfParser.parseString(ds, aRequest.channel.URI, aRequest.responseText);
+
+ // Differentiating between add-on types is deprecated
+ let extensionRes = gRDF.GetResource(PREFIX_EXTENSION + aId);
+ let themeRes = gRDF.GetResource(PREFIX_THEME + aId);
+ let itemRes = gRDF.GetResource(PREFIX_ITEM + aId);
+ let addonRes;
+ if (ds.ArcLabelsOut(extensionRes).hasMoreElements())
+ addonRes = extensionRes;
+ else if (ds.ArcLabelsOut(themeRes).hasMoreElements())
+ addonRes = themeRes;
+ else
+ addonRes = itemRes;
+
+ // If we have an update key then the update manifest must be signed
+ if (aUpdateKey) {
+ let signature = getProperty(ds, addonRes, "signature");
+ if (!signature)
+ throw Components.Exception("Update manifest for " + aId + " does not contain a required signature");
+ let serializer = new RDFSerializer();
+ let updateString = null;
+
+ try {
+ updateString = serializer.serializeResource(ds, addonRes);
+ }
+ catch (e) {
+ throw Components.Exception("Failed to generate signed string for " + aId + ". Serializer threw " + e,
+ e.result);
+ }
+
+ let result = false;
+
+ try {
+ let verifier = Cc["@mozilla.org/security/datasignatureverifier;1"].
+ getService(Ci.nsIDataSignatureVerifier);
+ result = verifier.verifyData(updateString, signature, aUpdateKey);
+ }
+ catch (e) {
+ throw Components.Exception("The signature or updateKey for " + aId + " is malformed." +
+ "Verifier threw " + e, e.result);
+ }
+
+ if (!result)
+ throw Components.Exception("The signature for " + aId + " was not created by the add-on's updateKey");
+ }
+
+ let updates = ds.GetTarget(addonRes, EM_R("updates"), true);
+
+ // A missing updates property doesn't count as a failure, just as no avialable
+ // update information
+ if (!updates) {
+ logger.warn("Update manifest for " + aId + " did not contain an updates property");
+ return [];
+ }
+
+ if (!(updates instanceof Ci.nsIRDFResource))
+ throw Components.Exception("Missing updates property for " + addonRes.Value);
+
+ let cu = Cc["@mozilla.org/rdf/container-utils;1"].
+ getService(Ci.nsIRDFContainerUtils);
+ if (!cu.IsContainer(ds, updates))
+ throw Components.Exception("Updates property was not an RDF container");
+
+ let results = [];
+ let ctr = Cc["@mozilla.org/rdf/container;1"].
+ createInstance(Ci.nsIRDFContainer);
+ ctr.Init(ds, updates);
+ let items = ctr.GetElements();
+ while (items.hasMoreElements()) {
+ let item = items.getNext().QueryInterface(Ci.nsIRDFResource);
+ let version = getProperty(ds, item, "version");
+ if (!version) {
+ logger.warn("Update manifest is missing a required version property.");
+ continue;
+ }
+
+ logger.debug("Found an update entry for " + aId + " version " + version);
+
+ let targetApps = ds.GetTargets(item, EM_R("targetApplication"), true);
+ while (targetApps.hasMoreElements()) {
+ let targetApp = targetApps.getNext().QueryInterface(Ci.nsIRDFResource);
+
+ let appEntry = {};
+ try {
+ appEntry.id = getRequiredProperty(ds, targetApp, "id");
+ appEntry.minVersion = getRequiredProperty(ds, targetApp, "minVersion");
+ appEntry.maxVersion = getRequiredProperty(ds, targetApp, "maxVersion");
+ }
+ catch (e) {
+ logger.warn(e);
+ continue;
+ }
+
+ let result = {
+ id: aId,
+ version: version,
+ multiprocessCompatible: getBooleanProperty(ds, item, "multiprocessCompatible"),
+ updateURL: getProperty(ds, targetApp, "updateLink"),
+ updateHash: getProperty(ds, targetApp, "updateHash"),
+ updateInfoURL: getProperty(ds, targetApp, "updateInfoURL"),
+ strictCompatibility: !!getBooleanProperty(ds, targetApp, "strictCompatibility"),
+ targetApplications: [appEntry]
+ };
+
+ // The JSON update protocol requires an SHA-2 hash. RDF still
+ // supports SHA-1, for compatibility reasons.
+ sanitizeUpdateURL(result, aRequest, /^sha/, "sha1 or stronger");
+
+ results.push(result);
+ }
+ }
+ return results;
+}
+
+/**
+ * Parses an JSON update manifest into an array of update objects.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aRequest
+ * The XMLHttpRequest that has retrieved the update manifest
+ * @param aManifestData
+ * The pre-parsed manifest, as a JSON object tree
+ * @return an array of update objects
+ * @throws if the update manifest is invalid in any way
+ */
+function parseJSONManifest(aId, aUpdateKey, aRequest, aManifestData) {
+ if (aUpdateKey)
+ throw Components.Exception("Update keys are not supported for JSON update manifests");
+
+ let TYPE_CHECK = {
+ "array": val => Array.isArray(val),
+ "object": val => val && typeof val == "object" && !Array.isArray(val),
+ };
+
+ function getProperty(aObj, aProperty, aType, aDefault = undefined) {
+ if (!(aProperty in aObj))
+ return aDefault;
+
+ let value = aObj[aProperty];
+
+ let matchesType = aType in TYPE_CHECK ? TYPE_CHECK[aType](value) : typeof value == aType;
+ if (!matchesType)
+ throw Components.Exception(`Update manifest property '${aProperty}' has incorrect type (expected ${aType})`);
+
+ return value;
+ }
+
+ function getRequiredProperty(aObj, aProperty, aType) {
+ let value = getProperty(aObj, aProperty, aType);
+ if (value === undefined)
+ throw Components.Exception(`Update manifest is missing a required ${aProperty} property.`);
+ return value;
+ }
+
+ let manifest = aManifestData;
+
+ if (!TYPE_CHECK["object"](manifest))
+ throw Components.Exception("Root element of update manifest must be a JSON object literal");
+
+ // The set of add-ons this manifest has updates for
+ let addons = getRequiredProperty(manifest, "addons", "object");
+
+ // The entry for this particular add-on
+ let addon = getProperty(addons, aId, "object");
+
+ // A missing entry doesn't count as a failure, just as no avialable update
+ // information
+ if (!addon) {
+ logger.warn("Update manifest did not contain an entry for " + aId);
+ return [];
+ }
+
+ // The list of available updates
+ let updates = getProperty(addon, "updates", "array", []);
+
+ let results = [];
+
+ for (let update of updates) {
+ let version = getRequiredProperty(update, "version", "string");
+
+ logger.debug(`Found an update entry for ${aId} version ${version}`);
+
+ let applications = getProperty(update, "applications", "object",
+ { gecko: {} });
+
+ // "gecko" is currently the only supported application entry. If
+ // it's missing, skip this update.
+ if (!("gecko" in applications)) {
+ logger.debug("gecko not in application entry, skipping update of ${addon}")
+ continue;
+ }
+
+ let app = getProperty(applications, "gecko", "object");
+
+ let appEntry = {
+ id: TOOLKIT_ID,
+ minVersion: getProperty(app, "strict_min_version", "string",
+ AddonManagerPrivate.webExtensionsMinPlatformVersion),
+ maxVersion: "*",
+ };
+
+ let result = {
+ id: aId,
+ version: version,
+ multiprocessCompatible: getProperty(update, "multiprocess_compatible", "boolean", true),
+ updateURL: getProperty(update, "update_link", "string"),
+ updateHash: getProperty(update, "update_hash", "string"),
+ updateInfoURL: getProperty(update, "update_info_url", "string"),
+ strictCompatibility: false,
+ targetApplications: [appEntry],
+ };
+
+ if ("strict_max_version" in app) {
+ if ("advisory_max_version" in app) {
+ logger.warn("Ignoring 'advisory_max_version' update manifest property for " +
+ aId + " property since 'strict_max_version' also present");
+ }
+
+ appEntry.maxVersion = getProperty(app, "strict_max_version", "string");
+ result.strictCompatibility = appEntry.maxVersion != "*";
+ } else if ("advisory_max_version" in app) {
+ appEntry.maxVersion = getProperty(app, "advisory_max_version", "string");
+ }
+
+ // The JSON update protocol requires an SHA-2 hash. RDF still
+ // supports SHA-1, for compatibility reasons.
+ sanitizeUpdateURL(result, aRequest, /^sha(256|512):/, "sha256 or sha512");
+
+ results.push(result);
+ }
+ return results;
+}
+
+/**
+ * Starts downloading an update manifest and then passes it to an appropriate
+ * parser to convert to an array of update objects
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aUrl
+ * The URL of the update manifest
+ * @param aObserver
+ * An observer to pass results to
+ */
+function UpdateParser(aId, aUpdateKey, aUrl, aObserver) {
+ this.id = aId;
+ this.updateKey = aUpdateKey;
+ this.observer = aObserver;
+ this.url = aUrl;
+
+ let requireBuiltIn = true;
+ try {
+ requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
+ }
+ catch (e) {
+ }
+
+ logger.debug("Requesting " + aUrl);
+ try {
+ this.request = new ServiceRequest();
+ this.request.open("GET", this.url, true);
+ this.request.channel.notificationCallbacks = new CertUtils.BadCertHandler(!requireBuiltIn);
+ this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to cache.
+ this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ this.request.overrideMimeType("text/plain");
+ this.request.setRequestHeader("Moz-XPI-Update", "1", true);
+ this.request.timeout = TIMEOUT;
+ this.request.addEventListener("load", () => this.onLoad(), false);
+ this.request.addEventListener("error", () => this.onError(), false);
+ this.request.addEventListener("timeout", () => this.onTimeout(), false);
+ this.request.send(null);
+ }
+ catch (e) {
+ logger.error("Failed to request update manifest", e);
+ }
+}
+
+UpdateParser.prototype = {
+ id: null,
+ updateKey: null,
+ observer: null,
+ request: null,
+ url: null,
+
+ /**
+ * Called when the manifest has been successfully loaded.
+ */
+ onLoad: function() {
+ let request = this.request;
+ this.request = null;
+ this._doneAt = new Error("place holder");
+
+ let requireBuiltIn = true;
+ try {
+ requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
+ }
+ catch (e) {
+ }
+
+ try {
+ CertUtils.checkCert(request.channel, !requireBuiltIn);
+ }
+ catch (e) {
+ logger.warn("Request failed: " + this.url + " - " + e);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ if (!Components.isSuccessCode(request.status)) {
+ logger.warn("Request failed: " + this.url + " - " + request.status);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ let channel = request.channel;
+ if (channel instanceof Ci.nsIHttpChannel && !channel.requestSucceeded) {
+ logger.warn("Request failed: " + this.url + " - " + channel.responseStatus +
+ ": " + channel.responseStatusText);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ // Detect the manifest type by first attempting to parse it as
+ // JSON, and falling back to parsing it as XML if that fails.
+ let parser;
+ try {
+ try {
+ let json = JSON.parse(request.responseText);
+
+ parser = () => parseJSONManifest(this.id, this.updateKey, request, json);
+ } catch (e) {
+ if (!(e instanceof SyntaxError))
+ throw e;
+ let domParser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
+ let xml = domParser.parseFromString(request.responseText, "text/xml");
+
+ if (xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR)
+ throw new Error("Update manifest was not valid XML or JSON");
+
+ parser = () => parseRDFManifest(this.id, this.updateKey, request, xml);
+ }
+ } catch (e) {
+ logger.warn("onUpdateCheckComplete failed to determine manifest type");
+ this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT);
+ return;
+ }
+
+ let results;
+ try {
+ results = parser();
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckComplete failed to parse update manifest", e);
+ this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
+ return;
+ }
+
+ if ("onUpdateCheckComplete" in this.observer) {
+ try {
+ this.observer.onUpdateCheckComplete(results);
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckComplete notification failed", e);
+ }
+ }
+ else {
+ logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker"));
+ }
+ },
+
+ /**
+ * Called when the request times out
+ */
+ onTimeout: function() {
+ this.request = null;
+ this._doneAt = new Error("Timed out");
+ logger.warn("Request for " + this.url + " timed out");
+ this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
+ },
+
+ /**
+ * Called when the manifest failed to load.
+ */
+ onError: function() {
+ if (!Components.isSuccessCode(this.request.status)) {
+ logger.warn("Request failed: " + this.url + " - " + this.request.status);
+ }
+ else if (this.request.channel instanceof Ci.nsIHttpChannel) {
+ try {
+ if (this.request.channel.requestSucceeded) {
+ logger.warn("Request failed: " + this.url + " - " +
+ this.request.channel.responseStatus + ": " +
+ this.request.channel.responseStatusText);
+ }
+ }
+ catch (e) {
+ logger.warn("HTTP Request failed for an unknown reason");
+ }
+ }
+ else {
+ logger.warn("Request failed for an unknown reason");
+ }
+
+ this.request = null;
+ this._doneAt = new Error("UP_onError");
+
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ },
+
+ /**
+ * Helper method to notify the observer that an error occured.
+ */
+ notifyError: function(aStatus) {
+ if ("onUpdateCheckError" in this.observer) {
+ try {
+ this.observer.onUpdateCheckError(aStatus);
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckError notification failed", e);
+ }
+ }
+ },
+
+ /**
+ * Called to cancel an in-progress update check.
+ */
+ cancel: function() {
+ if (!this.request) {
+ logger.error("Trying to cancel already-complete request", this._doneAt);
+ return;
+ }
+ this.request.abort();
+ this.request = null;
+ this._doneAt = new Error("UP_cancel");
+ this.notifyError(AddonUpdateChecker.ERROR_CANCELLED);
+ }
+};
+
+/**
+ * Tests if an update matches a version of the application or platform
+ *
+ * @param aUpdate
+ * The available update
+ * @param aAppVersion
+ * The application version to use
+ * @param aPlatformVersion
+ * The platform version to use
+ * @param aIgnoreMaxVersion
+ * Ignore maxVersion when testing if an update matches. Optional.
+ * @param aIgnoreStrictCompat
+ * Ignore strictCompatibility when testing if an update matches. Optional.
+ * @param aCompatOverrides
+ * AddonCompatibilityOverride objects to match against. Optional.
+ * @return true if the update is compatible with the application/platform
+ */
+function matchesVersions(aUpdate, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides) {
+ if (aCompatOverrides) {
+ let override = AddonRepository.findMatchingCompatOverride(aUpdate.version,
+ aCompatOverrides,
+ aAppVersion,
+ aPlatformVersion);
+ if (override && override.type == "incompatible")
+ return false;
+ }
+
+ if (aUpdate.strictCompatibility && !aIgnoreStrictCompat)
+ aIgnoreMaxVersion = false;
+
+ let result = false;
+ for (let app of aUpdate.targetApplications) {
+ if (app.id == Services.appinfo.ID) {
+ return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) &&
+ (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0));
+ }
+ if (app.id == TOOLKIT_ID) {
+ result = (Services.vc.compare(aPlatformVersion, app.minVersion) >= 0) &&
+ (aIgnoreMaxVersion || (Services.vc.compare(aPlatformVersion, app.maxVersion) <= 0));
+ }
+ }
+ return result;
+}
+
+this.AddonUpdateChecker = {
+ // These must be kept in sync with AddonManager
+ // The update check timed out
+ ERROR_TIMEOUT: -1,
+ // There was an error while downloading the update information.
+ ERROR_DOWNLOAD_ERROR: -2,
+ // The update information was malformed in some way.
+ ERROR_PARSE_ERROR: -3,
+ // The update information was not in any known format.
+ ERROR_UNKNOWN_FORMAT: -4,
+ // The update information was not correctly signed or there was an SSL error.
+ ERROR_SECURITY_ERROR: -5,
+ // The update was cancelled
+ ERROR_CANCELLED: -6,
+
+ /**
+ * Retrieves the best matching compatibility update for the application from
+ * a list of available update objects.
+ *
+ * @param aUpdates
+ * An array of update objects
+ * @param aVersion
+ * The version of the add-on to get new compatibility information for
+ * @param aIgnoreCompatibility
+ * An optional parameter to get the first compatibility update that
+ * is compatible with any version of the application or toolkit
+ * @param aAppVersion
+ * The version of the application or null to use the current version
+ * @param aPlatformVersion
+ * The version of the platform or null to use the current version
+ * @param aIgnoreMaxVersion
+ * Ignore maxVersion when testing if an update matches. Optional.
+ * @param aIgnoreStrictCompat
+ * Ignore strictCompatibility when testing if an update matches. Optional.
+ * @return an update object if one matches or null if not
+ */
+ getCompatibilityUpdate: function(aUpdates, aVersion, aIgnoreCompatibility,
+ aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat) {
+ if (!aAppVersion)
+ aAppVersion = Services.appinfo.version;
+ if (!aPlatformVersion)
+ aPlatformVersion = Services.appinfo.platformVersion;
+
+ for (let update of aUpdates) {
+ if (Services.vc.compare(update.version, aVersion) == 0) {
+ if (aIgnoreCompatibility) {
+ for (let targetApp of update.targetApplications) {
+ let id = targetApp.id;
+ if (id == Services.appinfo.ID || id == TOOLKIT_ID)
+ return update;
+ }
+ }
+ else if (matchesVersions(update, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat)) {
+ return update;
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns the newest available update from a list of update objects.
+ *
+ * @param aUpdates
+ * An array of update objects
+ * @param aAppVersion
+ * The version of the application or null to use the current version
+ * @param aPlatformVersion
+ * The version of the platform or null to use the current version
+ * @param aIgnoreMaxVersion
+ * When determining compatible updates, ignore maxVersion. Optional.
+ * @param aIgnoreStrictCompat
+ * When determining compatible updates, ignore strictCompatibility. Optional.
+ * @param aCompatOverrides
+ * Array of AddonCompatibilityOverride to take into account. Optional.
+ * @return an update object if one matches or null if not
+ */
+ getNewestCompatibleUpdate: function(aUpdates, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides) {
+ if (!aAppVersion)
+ aAppVersion = Services.appinfo.version;
+ if (!aPlatformVersion)
+ aPlatformVersion = Services.appinfo.platformVersion;
+
+ let blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsIBlocklistService);
+
+ let newest = null;
+ for (let update of aUpdates) {
+ if (!update.updateURL)
+ continue;
+ let state = blocklist.getAddonBlocklistState(update, aAppVersion, aPlatformVersion);
+ if (state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
+ continue;
+ if ((newest == null || (Services.vc.compare(newest.version, update.version) < 0)) &&
+ matchesVersions(update, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides)) {
+ newest = update;
+ }
+ }
+ return newest;
+ },
+
+ /**
+ * Starts an update check.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aUrl
+ * The URL of the add-on's update manifest
+ * @param aObserver
+ * An observer to notify of results
+ * @return UpdateParser so that the caller can use UpdateParser.cancel() to shut
+ * down in-progress update requests
+ */
+ checkForUpdates: function(aId, aUpdateKey, aUrl, aObserver) {
+ // Exclude default theme
+ if (aId != "{972ce4c6-7e08-4474-a285-3208198ce6fd}")
+ return new UpdateParser(aId, aUpdateKey, aUrl, aObserver);
+ }
+};