toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,738 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +/**
     1.9 + * The AddonUpdateChecker is responsible for retrieving the update information
    1.10 + * from an add-on's remote update manifest.
    1.11 + */
    1.12 +
    1.13 +"use strict";
    1.14 +
    1.15 +const Cc = Components.classes;
    1.16 +const Ci = Components.interfaces;
    1.17 +const Cu = Components.utils;
    1.18 +
    1.19 +this.EXPORTED_SYMBOLS = [ "AddonUpdateChecker" ];
    1.20 +
    1.21 +const TIMEOUT               = 60 * 1000;
    1.22 +const PREFIX_NS_RDF         = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
    1.23 +const PREFIX_NS_EM          = "http://www.mozilla.org/2004/em-rdf#";
    1.24 +const PREFIX_ITEM           = "urn:mozilla:item:";
    1.25 +const PREFIX_EXTENSION      = "urn:mozilla:extension:";
    1.26 +const PREFIX_THEME          = "urn:mozilla:theme:";
    1.27 +const TOOLKIT_ID            = "toolkit@mozilla.org"
    1.28 +const XMLURI_PARSE_ERROR    = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
    1.29 +
    1.30 +const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
    1.31 +
    1.32 +Components.utils.import("resource://gre/modules/Services.jsm");
    1.33 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    1.34 +
    1.35 +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
    1.36 +                                  "resource://gre/modules/AddonManager.jsm");
    1.37 +XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
    1.38 +                                  "resource://gre/modules/addons/AddonRepository.jsm");
    1.39 +
    1.40 +// Shared code for suppressing bad cert dialogs.
    1.41 +XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() {
    1.42 +  let certUtils = {};
    1.43 +  Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
    1.44 +  return certUtils;
    1.45 +});
    1.46 +
    1.47 +var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].
    1.48 +           getService(Ci.nsIRDFService);
    1.49 +
    1.50 +Cu.import("resource://gre/modules/Log.jsm");
    1.51 +const LOGGER_ID = "addons.update-checker";
    1.52 +
    1.53 +// Create a new logger for use by the Addons Update Checker
    1.54 +// (Requires AddonManager.jsm)
    1.55 +let logger = Log.repository.getLogger(LOGGER_ID);
    1.56 +
    1.57 +/**
    1.58 + * A serialisation method for RDF data that produces an identical string
    1.59 + * for matching RDF graphs.
    1.60 + * The serialisation is not complete, only assertions stemming from a given
    1.61 + * resource are included, multiple references to the same resource are not
    1.62 + * permitted, and the RDF prolog and epilog are not included.
    1.63 + * RDF Blob and Date literals are not supported.
    1.64 + */
    1.65 +function RDFSerializer() {
    1.66 +  this.cUtils = Cc["@mozilla.org/rdf/container-utils;1"].
    1.67 +                getService(Ci.nsIRDFContainerUtils);
    1.68 +  this.resources = [];
    1.69 +}
    1.70 +
    1.71 +RDFSerializer.prototype = {
    1.72 +  INDENT: "  ",      // The indent used for pretty-printing
    1.73 +  resources: null,   // Array of the resources that have been found
    1.74 +
    1.75 +  /**
    1.76 +   * Escapes characters from a string that should not appear in XML.
    1.77 +   *
    1.78 +   * @param  aString
    1.79 +   *         The string to be escaped
    1.80 +   * @return a string with all characters invalid in XML character data
    1.81 +   *         converted to entity references.
    1.82 +   */
    1.83 +  escapeEntities: function RDFS_escapeEntities(aString) {
    1.84 +    aString = aString.replace(/&/g, "&");
    1.85 +    aString = aString.replace(/</g, "&lt;");
    1.86 +    aString = aString.replace(/>/g, "&gt;");
    1.87 +    return aString.replace(/"/g, "&quot;");
    1.88 +  },
    1.89 +
    1.90 +  /**
    1.91 +   * Serializes all the elements of an RDF container.
    1.92 +   *
    1.93 +   * @param  aDs
    1.94 +   *         The RDF datasource
    1.95 +   * @param  aContainer
    1.96 +   *         The RDF container to output the child elements of
    1.97 +   * @param  aIndent
    1.98 +   *         The current level of indent for pretty-printing
    1.99 +   * @return a string containing the serialized elements.
   1.100 +   */
   1.101 +  serializeContainerItems: function RDFS_serializeContainerItems(aDs, aContainer,
   1.102 +                                                                 aIndent) {
   1.103 +    var result = "";
   1.104 +    var items = aContainer.GetElements();
   1.105 +    while (items.hasMoreElements()) {
   1.106 +      var item = items.getNext().QueryInterface(Ci.nsIRDFResource);
   1.107 +      result += aIndent + "<RDF:li>\n"
   1.108 +      result += this.serializeResource(aDs, item, aIndent + this.INDENT);
   1.109 +      result += aIndent + "</RDF:li>\n"
   1.110 +    }
   1.111 +    return result;
   1.112 +  },
   1.113 +
   1.114 +  /**
   1.115 +   * Serializes all em:* (see EM_NS) properties of an RDF resource except for
   1.116 +   * the em:signature property. As this serialization is to be compared against
   1.117 +   * the manifest signature it cannot contain the em:signature property itself.
   1.118 +   *
   1.119 +   * @param  aDs
   1.120 +   *         The RDF datasource
   1.121 +   * @param  aResource
   1.122 +   *         The RDF resource that contains the properties to serialize
   1.123 +   * @param  aIndent
   1.124 +   *         The current level of indent for pretty-printing
   1.125 +   * @return a string containing the serialized properties.
   1.126 +   * @throws if the resource contains a property that cannot be serialized
   1.127 +   */
   1.128 +  serializeResourceProperties: function RDFS_serializeResourceProperties(aDs,
   1.129 +                                                                         aResource,
   1.130 +                                                                         aIndent) {
   1.131 +    var result = "";
   1.132 +    var items = [];
   1.133 +    var arcs = aDs.ArcLabelsOut(aResource);
   1.134 +    while (arcs.hasMoreElements()) {
   1.135 +      var arc = arcs.getNext().QueryInterface(Ci.nsIRDFResource);
   1.136 +      if (arc.ValueUTF8.substring(0, PREFIX_NS_EM.length) != PREFIX_NS_EM)
   1.137 +        continue;
   1.138 +      var prop = arc.ValueUTF8.substring(PREFIX_NS_EM.length);
   1.139 +      if (prop == "signature")
   1.140 +        continue;
   1.141 +
   1.142 +      var targets = aDs.GetTargets(aResource, arc, true);
   1.143 +      while (targets.hasMoreElements()) {
   1.144 +        var target = targets.getNext();
   1.145 +        if (target instanceof Ci.nsIRDFResource) {
   1.146 +          var item = aIndent + "<em:" + prop + ">\n";
   1.147 +          item += this.serializeResource(aDs, target, aIndent + this.INDENT);
   1.148 +          item += aIndent + "</em:" + prop + ">\n";
   1.149 +          items.push(item);
   1.150 +        }
   1.151 +        else if (target instanceof Ci.nsIRDFLiteral) {
   1.152 +          items.push(aIndent + "<em:" + prop + ">" +
   1.153 +                     this.escapeEntities(target.Value) + "</em:" + prop + ">\n");
   1.154 +        }
   1.155 +        else if (target instanceof Ci.nsIRDFInt) {
   1.156 +          items.push(aIndent + "<em:" + prop + " NC:parseType=\"Integer\">" +
   1.157 +                     target.Value + "</em:" + prop + ">\n");
   1.158 +        }
   1.159 +        else {
   1.160 +          throw Components.Exception("Cannot serialize unknown literal type");
   1.161 +        }
   1.162 +      }
   1.163 +    }
   1.164 +    items.sort();
   1.165 +    result += items.join("");
   1.166 +    return result;
   1.167 +  },
   1.168 +
   1.169 +  /**
   1.170 +   * Recursively serializes an RDF resource and all resources it links to.
   1.171 +   * This will only output EM_NS properties and will ignore any em:signature
   1.172 +   * property.
   1.173 +   *
   1.174 +   * @param  aDs
   1.175 +   *         The RDF datasource
   1.176 +   * @param  aResource
   1.177 +   *         The RDF resource to serialize
   1.178 +   * @param  aIndent
   1.179 +   *         The current level of indent for pretty-printing. If undefined no
   1.180 +   *         indent will be added
   1.181 +   * @return a string containing the serialized resource.
   1.182 +   * @throws if the RDF data contains multiple references to the same resource.
   1.183 +   */
   1.184 +  serializeResource: function RDFS_serializeResource(aDs, aResource, aIndent) {
   1.185 +    if (this.resources.indexOf(aResource) != -1 ) {
   1.186 +      // We cannot output multiple references to the same resource.
   1.187 +      throw Components.Exception("Cannot serialize multiple references to " + aResource.Value);
   1.188 +    }
   1.189 +    if (aIndent === undefined)
   1.190 +      aIndent = "";
   1.191 +
   1.192 +    this.resources.push(aResource);
   1.193 +    var container = null;
   1.194 +    var type = "Description";
   1.195 +    if (this.cUtils.IsSeq(aDs, aResource)) {
   1.196 +      type = "Seq";
   1.197 +      container = this.cUtils.MakeSeq(aDs, aResource);
   1.198 +    }
   1.199 +    else if (this.cUtils.IsAlt(aDs, aResource)) {
   1.200 +      type = "Alt";
   1.201 +      container = this.cUtils.MakeAlt(aDs, aResource);
   1.202 +    }
   1.203 +    else if (this.cUtils.IsBag(aDs, aResource)) {
   1.204 +      type = "Bag";
   1.205 +      container = this.cUtils.MakeBag(aDs, aResource);
   1.206 +    }
   1.207 +
   1.208 +    var result = aIndent + "<RDF:" + type;
   1.209 +    if (!gRDF.IsAnonymousResource(aResource))
   1.210 +      result += " about=\"" + this.escapeEntities(aResource.ValueUTF8) + "\"";
   1.211 +    result += ">\n";
   1.212 +
   1.213 +    if (container)
   1.214 +      result += this.serializeContainerItems(aDs, container, aIndent + this.INDENT);
   1.215 +
   1.216 +    result += this.serializeResourceProperties(aDs, aResource, aIndent + this.INDENT);
   1.217 +
   1.218 +    result += aIndent + "</RDF:" + type + ">\n";
   1.219 +    return result;
   1.220 +  }
   1.221 +}
   1.222 +
   1.223 +/**
   1.224 + * Parses an RDF style update manifest into an array of update objects.
   1.225 + *
   1.226 + * @param  aId
   1.227 + *         The ID of the add-on being checked for updates
   1.228 + * @param  aUpdateKey
   1.229 + *         An optional update key for the add-on
   1.230 + * @param  aRequest
   1.231 + *         The XMLHttpRequest that has retrieved the update manifest
   1.232 + * @return an array of update objects
   1.233 + * @throws if the update manifest is invalid in any way
   1.234 + */
   1.235 +function parseRDFManifest(aId, aUpdateKey, aRequest) {
   1.236 +  function EM_R(aProp) {
   1.237 +    return gRDF.GetResource(PREFIX_NS_EM + aProp);
   1.238 +  }
   1.239 +
   1.240 +  function getValue(aLiteral) {
   1.241 +    if (aLiteral instanceof Ci.nsIRDFLiteral)
   1.242 +      return aLiteral.Value;
   1.243 +    if (aLiteral instanceof Ci.nsIRDFResource)
   1.244 +      return aLiteral.Value;
   1.245 +    if (aLiteral instanceof Ci.nsIRDFInt)
   1.246 +      return aLiteral.Value;
   1.247 +    return null;
   1.248 +  }
   1.249 +
   1.250 +  function getProperty(aDs, aSource, aProperty) {
   1.251 +    return getValue(aDs.GetTarget(aSource, EM_R(aProperty), true));
   1.252 +  }
   1.253 +
   1.254 +  function getRequiredProperty(aDs, aSource, aProperty) {
   1.255 +    let value = getProperty(aDs, aSource, aProperty);
   1.256 +    if (!value)
   1.257 +      throw Components.Exception("Update manifest is missing a required " + aProperty + " property.");
   1.258 +    return value;
   1.259 +  }
   1.260 +
   1.261 +  let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
   1.262 +                  createInstance(Ci.nsIRDFXMLParser);
   1.263 +  let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
   1.264 +           createInstance(Ci.nsIRDFDataSource);
   1.265 +  rdfParser.parseString(ds, aRequest.channel.URI, aRequest.responseText);
   1.266 +
   1.267 +  // Differentiating between add-on types is deprecated
   1.268 +  let extensionRes = gRDF.GetResource(PREFIX_EXTENSION + aId);
   1.269 +  let themeRes = gRDF.GetResource(PREFIX_THEME + aId);
   1.270 +  let itemRes = gRDF.GetResource(PREFIX_ITEM + aId);
   1.271 +  let addonRes = ds.ArcLabelsOut(extensionRes).hasMoreElements() ? extensionRes
   1.272 +               : ds.ArcLabelsOut(themeRes).hasMoreElements() ? themeRes
   1.273 +               : itemRes;
   1.274 +
   1.275 +  // If we have an update key then the update manifest must be signed
   1.276 +  if (aUpdateKey) {
   1.277 +    let signature = getProperty(ds, addonRes, "signature");
   1.278 +    if (!signature)
   1.279 +      throw Components.Exception("Update manifest for " + aId + " does not contain a required signature");
   1.280 +    let serializer = new RDFSerializer();
   1.281 +    let updateString = null;
   1.282 +
   1.283 +    try {
   1.284 +      updateString = serializer.serializeResource(ds, addonRes);
   1.285 +    }
   1.286 +    catch (e) {
   1.287 +      throw Components.Exception("Failed to generate signed string for " + aId + ". Serializer threw " + e,
   1.288 +                                 e.result);
   1.289 +    }
   1.290 +
   1.291 +    let result = false;
   1.292 +
   1.293 +    try {
   1.294 +      let verifier = Cc["@mozilla.org/security/datasignatureverifier;1"].
   1.295 +                     getService(Ci.nsIDataSignatureVerifier);
   1.296 +      result = verifier.verifyData(updateString, signature, aUpdateKey);
   1.297 +    }
   1.298 +    catch (e) {
   1.299 +      throw Components.Exception("The signature or updateKey for " + aId + " is malformed." +
   1.300 +                                 "Verifier threw " + e, e.result);
   1.301 +    }
   1.302 +
   1.303 +    if (!result)
   1.304 +      throw Components.Exception("The signature for " + aId + " was not created by the add-on's updateKey");
   1.305 +  }
   1.306 +
   1.307 +  let updates = ds.GetTarget(addonRes, EM_R("updates"), true);
   1.308 +
   1.309 +  // A missing updates property doesn't count as a failure, just as no avialable
   1.310 +  // update information
   1.311 +  if (!updates) {
   1.312 +    logger.warn("Update manifest for " + aId + " did not contain an updates property");
   1.313 +    return [];
   1.314 +  }
   1.315 +
   1.316 +  if (!(updates instanceof Ci.nsIRDFResource))
   1.317 +    throw Components.Exception("Missing updates property for " + addonRes.Value);
   1.318 +
   1.319 +  let cu = Cc["@mozilla.org/rdf/container-utils;1"].
   1.320 +           getService(Ci.nsIRDFContainerUtils);
   1.321 +  if (!cu.IsContainer(ds, updates))
   1.322 +    throw Components.Exception("Updates property was not an RDF container");
   1.323 +
   1.324 +  let results = [];
   1.325 +  let ctr = Cc["@mozilla.org/rdf/container;1"].
   1.326 +            createInstance(Ci.nsIRDFContainer);
   1.327 +  ctr.Init(ds, updates);
   1.328 +  let items = ctr.GetElements();
   1.329 +  while (items.hasMoreElements()) {
   1.330 +    let item = items.getNext().QueryInterface(Ci.nsIRDFResource);
   1.331 +    let version = getProperty(ds, item, "version");
   1.332 +    if (!version) {
   1.333 +      logger.warn("Update manifest is missing a required version property.");
   1.334 +      continue;
   1.335 +    }
   1.336 +
   1.337 +    logger.debug("Found an update entry for " + aId + " version " + version);
   1.338 +
   1.339 +    let targetApps = ds.GetTargets(item, EM_R("targetApplication"), true);
   1.340 +    while (targetApps.hasMoreElements()) {
   1.341 +      let targetApp = targetApps.getNext().QueryInterface(Ci.nsIRDFResource);
   1.342 +
   1.343 +      let appEntry = {};
   1.344 +      try {
   1.345 +        appEntry.id = getRequiredProperty(ds, targetApp, "id");
   1.346 +        appEntry.minVersion = getRequiredProperty(ds, targetApp, "minVersion");
   1.347 +        appEntry.maxVersion = getRequiredProperty(ds, targetApp, "maxVersion");
   1.348 +      }
   1.349 +      catch (e) {
   1.350 +        logger.warn(e);
   1.351 +        continue;
   1.352 +      }
   1.353 +
   1.354 +      let result = {
   1.355 +        id: aId,
   1.356 +        version: version,
   1.357 +        updateURL: getProperty(ds, targetApp, "updateLink"),
   1.358 +        updateHash: getProperty(ds, targetApp, "updateHash"),
   1.359 +        updateInfoURL: getProperty(ds, targetApp, "updateInfoURL"),
   1.360 +        strictCompatibility: getProperty(ds, targetApp, "strictCompatibility") == "true",
   1.361 +        targetApplications: [appEntry]
   1.362 +      };
   1.363 +
   1.364 +      if (result.updateURL && AddonManager.checkUpdateSecurity &&
   1.365 +          result.updateURL.substring(0, 6) != "https:" &&
   1.366 +          (!result.updateHash || result.updateHash.substring(0, 3) != "sha")) {
   1.367 +        logger.warn("updateLink " + result.updateURL + " is not secure and is not verified" +
   1.368 +             " by a strong enough hash (needs to be sha1 or stronger).");
   1.369 +        delete result.updateURL;
   1.370 +        delete result.updateHash;
   1.371 +      }
   1.372 +      results.push(result);
   1.373 +    }
   1.374 +  }
   1.375 +  return results;
   1.376 +}
   1.377 +
   1.378 +/**
   1.379 + * Starts downloading an update manifest and then passes it to an appropriate
   1.380 + * parser to convert to an array of update objects
   1.381 + *
   1.382 + * @param  aId
   1.383 + *         The ID of the add-on being checked for updates
   1.384 + * @param  aUpdateKey
   1.385 + *         An optional update key for the add-on
   1.386 + * @param  aUrl
   1.387 + *         The URL of the update manifest
   1.388 + * @param  aObserver
   1.389 + *         An observer to pass results to
   1.390 + */
   1.391 +function UpdateParser(aId, aUpdateKey, aUrl, aObserver) {
   1.392 +  this.id = aId;
   1.393 +  this.updateKey = aUpdateKey;
   1.394 +  this.observer = aObserver;
   1.395 +  this.url = aUrl;
   1.396 +
   1.397 +  let requireBuiltIn = true;
   1.398 +  try {
   1.399 +    requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
   1.400 +  }
   1.401 +  catch (e) {
   1.402 +  }
   1.403 +
   1.404 +  logger.debug("Requesting " + aUrl);
   1.405 +  try {
   1.406 +    this.request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
   1.407 +                   createInstance(Ci.nsIXMLHttpRequest);
   1.408 +    this.request.open("GET", this.url, true);
   1.409 +    this.request.channel.notificationCallbacks = new CertUtils.BadCertHandler(!requireBuiltIn);
   1.410 +    this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
   1.411 +    // Prevent the request from writing to cache.
   1.412 +    this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
   1.413 +    this.request.overrideMimeType("text/xml");
   1.414 +    this.request.timeout = TIMEOUT;
   1.415 +    var self = this;
   1.416 +    this.request.addEventListener("load", function loadEventListener(event) { self.onLoad() }, false);
   1.417 +    this.request.addEventListener("error", function errorEventListener(event) { self.onError() }, false);
   1.418 +    this.request.addEventListener("timeout", function timeoutEventListener(event) { self.onTimeout() }, false);
   1.419 +    this.request.send(null);
   1.420 +  }
   1.421 +  catch (e) {
   1.422 +    logger.error("Failed to request update manifest", e);
   1.423 +  }
   1.424 +}
   1.425 +
   1.426 +UpdateParser.prototype = {
   1.427 +  id: null,
   1.428 +  updateKey: null,
   1.429 +  observer: null,
   1.430 +  request: null,
   1.431 +  url: null,
   1.432 +
   1.433 +  /**
   1.434 +   * Called when the manifest has been successfully loaded.
   1.435 +   */
   1.436 +  onLoad: function UP_onLoad() {
   1.437 +    let request = this.request;
   1.438 +    this.request = null;
   1.439 +
   1.440 +    let requireBuiltIn = true;
   1.441 +    try {
   1.442 +      requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
   1.443 +    }
   1.444 +    catch (e) {
   1.445 +    }
   1.446 +
   1.447 +    try {
   1.448 +      CertUtils.checkCert(request.channel, !requireBuiltIn);
   1.449 +    }
   1.450 +    catch (e) {
   1.451 +      this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
   1.452 +      return;
   1.453 +    }
   1.454 +
   1.455 +    if (!Components.isSuccessCode(request.status)) {
   1.456 +      logger.warn("Request failed: " + this.url + " - " + request.status);
   1.457 +      this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
   1.458 +      return;
   1.459 +    }
   1.460 +
   1.461 +    let channel = request.channel;
   1.462 +    if (channel instanceof Ci.nsIHttpChannel && !channel.requestSucceeded) {
   1.463 +      logger.warn("Request failed: " + this.url + " - " + channel.responseStatus +
   1.464 +           ": " + channel.responseStatusText);
   1.465 +      this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
   1.466 +      return;
   1.467 +    }
   1.468 +
   1.469 +    let xml = request.responseXML;
   1.470 +    if (!xml || xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR) {
   1.471 +      logger.warn("Update manifest was not valid XML");
   1.472 +      this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
   1.473 +      return;
   1.474 +    }
   1.475 +
   1.476 +    // We currently only know about RDF update manifests
   1.477 +    if (xml.documentElement.namespaceURI == PREFIX_NS_RDF) {
   1.478 +      let results = null;
   1.479 +
   1.480 +      try {
   1.481 +        results = parseRDFManifest(this.id, this.updateKey, request);
   1.482 +      }
   1.483 +      catch (e) {
   1.484 +        logger.warn(e);
   1.485 +        this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
   1.486 +        return;
   1.487 +      }
   1.488 +      if ("onUpdateCheckComplete" in this.observer) {
   1.489 +        try {
   1.490 +          this.observer.onUpdateCheckComplete(results);
   1.491 +        }
   1.492 +        catch (e) {
   1.493 +          logger.warn("onUpdateCheckComplete notification failed", e);
   1.494 +        }
   1.495 +      }
   1.496 +      return;
   1.497 +    }
   1.498 +
   1.499 +    logger.warn("Update manifest had an unrecognised namespace: " + xml.documentElement.namespaceURI);
   1.500 +    this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT);
   1.501 +  },
   1.502 +
   1.503 +  /**
   1.504 +   * Called when the request times out
   1.505 +   */
   1.506 +  onTimeout: function() {
   1.507 +    this.request = null;
   1.508 +    logger.warn("Request for " + this.url + " timed out");
   1.509 +    this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
   1.510 +  },
   1.511 +
   1.512 +  /**
   1.513 +   * Called when the manifest failed to load.
   1.514 +   */
   1.515 +  onError: function UP_onError() {
   1.516 +    if (!Components.isSuccessCode(this.request.status)) {
   1.517 +      logger.warn("Request failed: " + this.url + " - " + this.request.status);
   1.518 +    }
   1.519 +    else if (this.request.channel instanceof Ci.nsIHttpChannel) {
   1.520 +      try {
   1.521 +        if (this.request.channel.requestSucceeded) {
   1.522 +          logger.warn("Request failed: " + this.url + " - " +
   1.523 +               this.request.channel.responseStatus + ": " +
   1.524 +               this.request.channel.responseStatusText);
   1.525 +        }
   1.526 +      }
   1.527 +      catch (e) {
   1.528 +        logger.warn("HTTP Request failed for an unknown reason");
   1.529 +      }
   1.530 +    }
   1.531 +    else {
   1.532 +      logger.warn("Request failed for an unknown reason");
   1.533 +    }
   1.534 +
   1.535 +    this.request = null;
   1.536 +
   1.537 +    this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
   1.538 +  },
   1.539 +
   1.540 +  /**
   1.541 +   * Helper method to notify the observer that an error occured.
   1.542 +   */
   1.543 +  notifyError: function UP_notifyError(aStatus) {
   1.544 +    if ("onUpdateCheckError" in this.observer) {
   1.545 +      try {
   1.546 +        this.observer.onUpdateCheckError(aStatus);
   1.547 +      }
   1.548 +      catch (e) {
   1.549 +        logger.warn("onUpdateCheckError notification failed", e);
   1.550 +      }
   1.551 +    }
   1.552 +  },
   1.553 +
   1.554 +  /**
   1.555 +   * Called to cancel an in-progress update check.
   1.556 +   */
   1.557 +  cancel: function UP_cancel() {
   1.558 +    this.request.abort();
   1.559 +    this.request = null;
   1.560 +    this.notifyError(AddonUpdateChecker.ERROR_CANCELLED);
   1.561 +  }
   1.562 +};
   1.563 +
   1.564 +/**
   1.565 + * Tests if an update matches a version of the application or platform
   1.566 + *
   1.567 + * @param  aUpdate
   1.568 + *         The available update
   1.569 + * @param  aAppVersion
   1.570 + *         The application version to use
   1.571 + * @param  aPlatformVersion
   1.572 + *         The platform version to use
   1.573 + * @param  aIgnoreMaxVersion
   1.574 + *         Ignore maxVersion when testing if an update matches. Optional.
   1.575 + * @param  aIgnoreStrictCompat
   1.576 + *         Ignore strictCompatibility when testing if an update matches. Optional.
   1.577 + * @param  aCompatOverrides
   1.578 + *         AddonCompatibilityOverride objects to match against. Optional.
   1.579 + * @return true if the update is compatible with the application/platform
   1.580 + */
   1.581 +function matchesVersions(aUpdate, aAppVersion, aPlatformVersion,
   1.582 +                         aIgnoreMaxVersion, aIgnoreStrictCompat,
   1.583 +                         aCompatOverrides) {
   1.584 +  if (aCompatOverrides) {
   1.585 +    let override = AddonRepository.findMatchingCompatOverride(aUpdate.version,
   1.586 +                                                              aCompatOverrides,
   1.587 +                                                              aAppVersion,
   1.588 +                                                              aPlatformVersion);
   1.589 +    if (override && override.type == "incompatible")
   1.590 +      return false;
   1.591 +  }
   1.592 +
   1.593 +  if (aUpdate.strictCompatibility && !aIgnoreStrictCompat)
   1.594 +    aIgnoreMaxVersion = false;
   1.595 +
   1.596 +  let result = false;
   1.597 +  for (let app of aUpdate.targetApplications) {
   1.598 +    if (app.id == Services.appinfo.ID) {
   1.599 +      return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) &&
   1.600 +             (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0));
   1.601 +    }
   1.602 +    if (app.id == TOOLKIT_ID) {
   1.603 +      result = (Services.vc.compare(aPlatformVersion, app.minVersion) >= 0) &&
   1.604 +               (aIgnoreMaxVersion || (Services.vc.compare(aPlatformVersion, app.maxVersion) <= 0));
   1.605 +    }
   1.606 +  }
   1.607 +  return result;
   1.608 +}
   1.609 +
   1.610 +this.AddonUpdateChecker = {
   1.611 +  // These must be kept in sync with AddonManager
   1.612 +  // The update check timed out
   1.613 +  ERROR_TIMEOUT: -1,
   1.614 +  // There was an error while downloading the update information.
   1.615 +  ERROR_DOWNLOAD_ERROR: -2,
   1.616 +  // The update information was malformed in some way.
   1.617 +  ERROR_PARSE_ERROR: -3,
   1.618 +  // The update information was not in any known format.
   1.619 +  ERROR_UNKNOWN_FORMAT: -4,
   1.620 +  // The update information was not correctly signed or there was an SSL error.
   1.621 +  ERROR_SECURITY_ERROR: -5,
   1.622 +  // The update was cancelled
   1.623 +  ERROR_CANCELLED: -6,
   1.624 +
   1.625 +  /**
   1.626 +   * Retrieves the best matching compatibility update for the application from
   1.627 +   * a list of available update objects.
   1.628 +   *
   1.629 +   * @param  aUpdates
   1.630 +   *         An array of update objects
   1.631 +   * @param  aVersion
   1.632 +   *         The version of the add-on to get new compatibility information for
   1.633 +   * @param  aIgnoreCompatibility
   1.634 +   *         An optional parameter to get the first compatibility update that
   1.635 +   *         is compatible with any version of the application or toolkit
   1.636 +   * @param  aAppVersion
   1.637 +   *         The version of the application or null to use the current version
   1.638 +   * @param  aPlatformVersion
   1.639 +   *         The version of the platform or null to use the current version
   1.640 +   * @param  aIgnoreMaxVersion
   1.641 +   *         Ignore maxVersion when testing if an update matches. Optional.
   1.642 +   * @param  aIgnoreStrictCompat
   1.643 +   *         Ignore strictCompatibility when testing if an update matches. Optional.
   1.644 +   * @return an update object if one matches or null if not
   1.645 +   */
   1.646 +  getCompatibilityUpdate: function AUC_getCompatibilityUpdate(aUpdates, aVersion,
   1.647 +                                                              aIgnoreCompatibility,
   1.648 +                                                              aAppVersion,
   1.649 +                                                              aPlatformVersion,
   1.650 +                                                              aIgnoreMaxVersion,
   1.651 +                                                              aIgnoreStrictCompat) {
   1.652 +    if (!aAppVersion)
   1.653 +      aAppVersion = Services.appinfo.version;
   1.654 +    if (!aPlatformVersion)
   1.655 +      aPlatformVersion = Services.appinfo.platformVersion;
   1.656 +
   1.657 +    for (let update of aUpdates) {
   1.658 +      if (Services.vc.compare(update.version, aVersion) == 0) {
   1.659 +        if (aIgnoreCompatibility) {
   1.660 +          for (let targetApp of update.targetApplications) {
   1.661 +            let id = targetApp.id;
   1.662 +            if (id == Services.appinfo.ID || id == TOOLKIT_ID)
   1.663 +              return update;
   1.664 +          }
   1.665 +        }
   1.666 +        else if (matchesVersions(update, aAppVersion, aPlatformVersion,
   1.667 +                                 aIgnoreMaxVersion, aIgnoreStrictCompat)) {
   1.668 +          return update;
   1.669 +        }
   1.670 +      }
   1.671 +    }
   1.672 +    return null;
   1.673 +  },
   1.674 +
   1.675 +  /**
   1.676 +   * Returns the newest available update from a list of update objects.
   1.677 +   *
   1.678 +   * @param  aUpdates
   1.679 +   *         An array of update objects
   1.680 +   * @param  aAppVersion
   1.681 +   *         The version of the application or null to use the current version
   1.682 +   * @param  aPlatformVersion
   1.683 +   *         The version of the platform or null to use the current version
   1.684 +   * @param  aIgnoreMaxVersion
   1.685 +   *         When determining compatible updates, ignore maxVersion. Optional.
   1.686 +   * @param  aIgnoreStrictCompat
   1.687 +   *         When determining compatible updates, ignore strictCompatibility. Optional.
   1.688 +   * @param  aCompatOverrides
   1.689 +   *         Array of AddonCompatibilityOverride to take into account. Optional.
   1.690 +   * @return an update object if one matches or null if not
   1.691 +   */
   1.692 +  getNewestCompatibleUpdate: function AUC_getNewestCompatibleUpdate(aUpdates,
   1.693 +                                                                    aAppVersion,
   1.694 +                                                                    aPlatformVersion,
   1.695 +                                                                    aIgnoreMaxVersion,
   1.696 +                                                                    aIgnoreStrictCompat,
   1.697 +                                                                    aCompatOverrides) {
   1.698 +    if (!aAppVersion)
   1.699 +      aAppVersion = Services.appinfo.version;
   1.700 +    if (!aPlatformVersion)
   1.701 +      aPlatformVersion = Services.appinfo.platformVersion;
   1.702 +
   1.703 +    let blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
   1.704 +                    getService(Ci.nsIBlocklistService);
   1.705 +
   1.706 +    let newest = null;
   1.707 +    for (let update of aUpdates) {
   1.708 +      if (!update.updateURL)
   1.709 +        continue;
   1.710 +      let state = blocklist.getAddonBlocklistState(update, aAppVersion, aPlatformVersion);
   1.711 +      if (state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
   1.712 +        continue;
   1.713 +      if ((newest == null || (Services.vc.compare(newest.version, update.version) < 0)) &&
   1.714 +          matchesVersions(update, aAppVersion, aPlatformVersion,
   1.715 +                          aIgnoreMaxVersion, aIgnoreStrictCompat,
   1.716 +                          aCompatOverrides)) {
   1.717 +        newest = update;
   1.718 +      }
   1.719 +    }
   1.720 +    return newest;
   1.721 +  },
   1.722 +
   1.723 +  /**
   1.724 +   * Starts an update check.
   1.725 +   *
   1.726 +   * @param  aId
   1.727 +   *         The ID of the add-on being checked for updates
   1.728 +   * @param  aUpdateKey
   1.729 +   *         An optional update key for the add-on
   1.730 +   * @param  aUrl
   1.731 +   *         The URL of the add-on's update manifest
   1.732 +   * @param  aObserver
   1.733 +   *         An observer to notify of results
   1.734 +   * @return UpdateParser so that the caller can use UpdateParser.cancel() to shut
   1.735 +   *         down in-progress update requests
   1.736 +   */
   1.737 +  checkForUpdates: function AUC_checkForUpdates(aId, aUpdateKey, aUrl,
   1.738 +                                                aObserver) {
   1.739 +    return new UpdateParser(aId, aUpdateKey, aUrl, aObserver);
   1.740 +  }
   1.741 +};

mercurial