toolkit/mozapps/extensions/internal/AddonRepository.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,2003 @@
     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 +"use strict";
     1.9 +
    1.10 +const Cc = Components.classes;
    1.11 +const Ci = Components.interfaces;
    1.12 +const Cu = Components.utils;
    1.13 +const Cr = Components.results;
    1.14 +
    1.15 +Components.utils.import("resource://gre/modules/Services.jsm");
    1.16 +Components.utils.import("resource://gre/modules/AddonManager.jsm");
    1.17 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    1.18 +
    1.19 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
    1.20 +                                  "resource://gre/modules/FileUtils.jsm");
    1.21 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    1.22 +                                  "resource://gre/modules/NetUtil.jsm");
    1.23 +XPCOMUtils.defineLazyModuleGetter(this, "OS",
    1.24 +                                  "resource://gre/modules/osfile.jsm");
    1.25 +XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave",
    1.26 +                                  "resource://gre/modules/DeferredSave.jsm");
    1.27 +XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository_SQLiteMigrator",
    1.28 +                                  "resource://gre/modules/addons/AddonRepository_SQLiteMigrator.jsm");
    1.29 +XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    1.30 +                                  "resource://gre/modules/Promise.jsm");
    1.31 +
    1.32 +this.EXPORTED_SYMBOLS = [ "AddonRepository" ];
    1.33 +
    1.34 +const PREF_GETADDONS_CACHE_ENABLED       = "extensions.getAddons.cache.enabled";
    1.35 +const PREF_GETADDONS_CACHE_TYPES         = "extensions.getAddons.cache.types";
    1.36 +const PREF_GETADDONS_CACHE_ID_ENABLED    = "extensions.%ID%.getAddons.cache.enabled"
    1.37 +const PREF_GETADDONS_BROWSEADDONS        = "extensions.getAddons.browseAddons";
    1.38 +const PREF_GETADDONS_BYIDS               = "extensions.getAddons.get.url";
    1.39 +const PREF_GETADDONS_BYIDS_PERFORMANCE   = "extensions.getAddons.getWithPerformance.url";
    1.40 +const PREF_GETADDONS_BROWSERECOMMENDED   = "extensions.getAddons.recommended.browseURL";
    1.41 +const PREF_GETADDONS_GETRECOMMENDED      = "extensions.getAddons.recommended.url";
    1.42 +const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL";
    1.43 +const PREF_GETADDONS_GETSEARCHRESULTS    = "extensions.getAddons.search.url";
    1.44 +const PREF_GETADDONS_DB_SCHEMA           = "extensions.getAddons.databaseSchema"
    1.45 +
    1.46 +const XMLURI_PARSE_ERROR  = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
    1.47 +
    1.48 +const API_VERSION = "1.5";
    1.49 +const DEFAULT_CACHE_TYPES = "extension,theme,locale,dictionary";
    1.50 +
    1.51 +const KEY_PROFILEDIR        = "ProfD";
    1.52 +const FILE_DATABASE         = "addons.json";
    1.53 +const DB_SCHEMA             = 5;
    1.54 +const DB_MIN_JSON_SCHEMA    = 5;
    1.55 +const DB_BATCH_TIMEOUT_MS   = 50;
    1.56 +
    1.57 +const BLANK_DB = function() {
    1.58 +  return {
    1.59 +    addons: new Map(),
    1.60 +    schema: DB_SCHEMA
    1.61 +  };
    1.62 +}
    1.63 +
    1.64 +const TOOLKIT_ID     = "toolkit@mozilla.org";
    1.65 +
    1.66 +Cu.import("resource://gre/modules/Log.jsm");
    1.67 +const LOGGER_ID = "addons.repository";
    1.68 +
    1.69 +// Create a new logger for use by the Addons Repository
    1.70 +// (Requires AddonManager.jsm)
    1.71 +let logger = Log.repository.getLogger(LOGGER_ID);
    1.72 +
    1.73 +// A map between XML keys to AddonSearchResult keys for string values
    1.74 +// that require no extra parsing from XML
    1.75 +const STRING_KEY_MAP = {
    1.76 +  name:               "name",
    1.77 +  version:            "version",
    1.78 +  homepage:           "homepageURL",
    1.79 +  support:            "supportURL"
    1.80 +};
    1.81 +
    1.82 +// A map between XML keys to AddonSearchResult keys for string values
    1.83 +// that require parsing from HTML
    1.84 +const HTML_KEY_MAP = {
    1.85 +  summary:            "description",
    1.86 +  description:        "fullDescription",
    1.87 +  developer_comments: "developerComments",
    1.88 +  eula:               "eula"
    1.89 +};
    1.90 +
    1.91 +// A map between XML keys to AddonSearchResult keys for integer values
    1.92 +// that require no extra parsing from XML
    1.93 +const INTEGER_KEY_MAP = {
    1.94 +  total_downloads:  "totalDownloads",
    1.95 +  weekly_downloads: "weeklyDownloads",
    1.96 +  daily_users:      "dailyUsers"
    1.97 +};
    1.98 +
    1.99 +// Wrap the XHR factory so that tests can override with a mock
   1.100 +let XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
   1.101 +                                       "nsIXMLHttpRequest");
   1.102 +
   1.103 +function convertHTMLToPlainText(html) {
   1.104 +  if (!html)
   1.105 +    return html;
   1.106 +  var converter = Cc["@mozilla.org/widget/htmlformatconverter;1"].
   1.107 +                  createInstance(Ci.nsIFormatConverter);
   1.108 +
   1.109 +  var input = Cc["@mozilla.org/supports-string;1"].
   1.110 +              createInstance(Ci.nsISupportsString);
   1.111 +  input.data = html.replace(/\n/g, "<br>");
   1.112 +
   1.113 +  var output = {};
   1.114 +  converter.convert("text/html", input, input.data.length, "text/unicode",
   1.115 +                    output, {});
   1.116 +
   1.117 +  if (output.value instanceof Ci.nsISupportsString)
   1.118 +    return output.value.data.replace(/\r\n/g, "\n");
   1.119 +  return html;
   1.120 +}
   1.121 +
   1.122 +function getAddonsToCache(aIds, aCallback) {
   1.123 +  try {
   1.124 +    var types = Services.prefs.getCharPref(PREF_GETADDONS_CACHE_TYPES);
   1.125 +  }
   1.126 +  catch (e) { }
   1.127 +  if (!types)
   1.128 +    types = DEFAULT_CACHE_TYPES;
   1.129 +
   1.130 +  types = types.split(",");
   1.131 +
   1.132 +  AddonManager.getAddonsByIDs(aIds, function getAddonsToCache_getAddonsByIDs(aAddons) {
   1.133 +    let enabledIds = [];
   1.134 +    for (var i = 0; i < aIds.length; i++) {
   1.135 +      var preference = PREF_GETADDONS_CACHE_ID_ENABLED.replace("%ID%", aIds[i]);
   1.136 +      try {
   1.137 +        if (!Services.prefs.getBoolPref(preference))
   1.138 +          continue;
   1.139 +      } catch(e) {
   1.140 +        // If the preference doesn't exist caching is enabled by default
   1.141 +      }
   1.142 +
   1.143 +      // The add-ons manager may not know about this ID yet if it is a pending
   1.144 +      // install. In that case we'll just cache it regardless
   1.145 +      if (aAddons[i] && (types.indexOf(aAddons[i].type) == -1))
   1.146 +        continue;
   1.147 +
   1.148 +      enabledIds.push(aIds[i]);
   1.149 +    }
   1.150 +
   1.151 +    aCallback(enabledIds);
   1.152 +  });
   1.153 +}
   1.154 +
   1.155 +function AddonSearchResult(aId) {
   1.156 +  this.id = aId;
   1.157 +  this.icons = {};
   1.158 +  this._unsupportedProperties = {};
   1.159 +}
   1.160 +
   1.161 +AddonSearchResult.prototype = {
   1.162 +  /**
   1.163 +   * The ID of the add-on
   1.164 +   */
   1.165 +  id: null,
   1.166 +
   1.167 +  /**
   1.168 +   * The add-on type (e.g. "extension" or "theme")
   1.169 +   */
   1.170 +  type: null,
   1.171 +
   1.172 +  /**
   1.173 +   * The name of the add-on
   1.174 +   */
   1.175 +  name: null,
   1.176 +
   1.177 +  /**
   1.178 +   * The version of the add-on
   1.179 +   */
   1.180 +  version: null,
   1.181 +
   1.182 +  /**
   1.183 +   * The creator of the add-on
   1.184 +   */
   1.185 +  creator: null,
   1.186 +
   1.187 +  /**
   1.188 +   * The developers of the add-on
   1.189 +   */
   1.190 +  developers: null,
   1.191 +
   1.192 +  /**
   1.193 +   * A short description of the add-on
   1.194 +   */
   1.195 +  description: null,
   1.196 +
   1.197 +  /**
   1.198 +   * The full description of the add-on
   1.199 +   */
   1.200 +  fullDescription: null,
   1.201 +
   1.202 +  /**
   1.203 +   * The developer comments for the add-on. This includes any information
   1.204 +   * that may be helpful to end users that isn't necessarily applicable to
   1.205 +   * the add-on description (e.g. known major bugs)
   1.206 +   */
   1.207 +  developerComments: null,
   1.208 +
   1.209 +  /**
   1.210 +   * The end-user licensing agreement (EULA) of the add-on
   1.211 +   */
   1.212 +  eula: null,
   1.213 +
   1.214 +  /**
   1.215 +   * The url of the add-on's icon
   1.216 +   */
   1.217 +  get iconURL() {
   1.218 +    return this.icons && this.icons[32];
   1.219 +  },
   1.220 +
   1.221 +   /**
   1.222 +   * The URLs of the add-on's icons, as an object with icon size as key
   1.223 +   */
   1.224 +  icons: null,
   1.225 +
   1.226 +  /**
   1.227 +   * An array of screenshot urls for the add-on
   1.228 +   */
   1.229 +  screenshots: null,
   1.230 +
   1.231 +  /**
   1.232 +   * The homepage for the add-on
   1.233 +   */
   1.234 +  homepageURL: null,
   1.235 +
   1.236 +  /**
   1.237 +   * The homepage for the add-on
   1.238 +   */
   1.239 +  learnmoreURL: null,
   1.240 +
   1.241 +  /**
   1.242 +   * The support URL for the add-on
   1.243 +   */
   1.244 +  supportURL: null,
   1.245 +
   1.246 +  /**
   1.247 +   * The contribution url of the add-on
   1.248 +   */
   1.249 +  contributionURL: null,
   1.250 +
   1.251 +  /**
   1.252 +   * The suggested contribution amount
   1.253 +   */
   1.254 +  contributionAmount: null,
   1.255 +
   1.256 +  /**
   1.257 +   * The URL to visit in order to purchase the add-on
   1.258 +   */
   1.259 +  purchaseURL: null,
   1.260 +
   1.261 +  /**
   1.262 +   * The numerical cost of the add-on in some currency, for sorting purposes
   1.263 +   * only
   1.264 +   */
   1.265 +  purchaseAmount: null,
   1.266 +
   1.267 +  /**
   1.268 +   * The display cost of the add-on, for display purposes only
   1.269 +   */
   1.270 +  purchaseDisplayAmount: null,
   1.271 +
   1.272 +  /**
   1.273 +   * The rating of the add-on, 0-5
   1.274 +   */
   1.275 +  averageRating: null,
   1.276 +
   1.277 +  /**
   1.278 +   * The number of reviews for this add-on
   1.279 +   */
   1.280 +  reviewCount: null,
   1.281 +
   1.282 +  /**
   1.283 +   * The URL to the list of reviews for this add-on
   1.284 +   */
   1.285 +  reviewURL: null,
   1.286 +
   1.287 +  /**
   1.288 +   * The total number of times the add-on was downloaded
   1.289 +   */
   1.290 +  totalDownloads: null,
   1.291 +
   1.292 +  /**
   1.293 +   * The number of times the add-on was downloaded the current week
   1.294 +   */
   1.295 +  weeklyDownloads: null,
   1.296 +
   1.297 +  /**
   1.298 +   * The number of daily users for the add-on
   1.299 +   */
   1.300 +  dailyUsers: null,
   1.301 +
   1.302 +  /**
   1.303 +   * AddonInstall object generated from the add-on XPI url
   1.304 +   */
   1.305 +  install: null,
   1.306 +
   1.307 +  /**
   1.308 +   * nsIURI storing where this add-on was installed from
   1.309 +   */
   1.310 +  sourceURI: null,
   1.311 +
   1.312 +  /**
   1.313 +   * The status of the add-on in the repository (e.g. 4 = "Public")
   1.314 +   */
   1.315 +  repositoryStatus: null,
   1.316 +
   1.317 +  /**
   1.318 +   * The size of the add-on's files in bytes. For an add-on that have not yet
   1.319 +   * been downloaded this may be an estimated value.
   1.320 +   */
   1.321 +  size: null,
   1.322 +
   1.323 +  /**
   1.324 +   * The Date that the add-on was most recently updated
   1.325 +   */
   1.326 +  updateDate: null,
   1.327 +
   1.328 +  /**
   1.329 +   * True or false depending on whether the add-on is compatible with the
   1.330 +   * current version of the application
   1.331 +   */
   1.332 +  isCompatible: true,
   1.333 +
   1.334 +  /**
   1.335 +   * True or false depending on whether the add-on is compatible with the
   1.336 +   * current platform
   1.337 +   */
   1.338 +  isPlatformCompatible: true,
   1.339 +
   1.340 +  /**
   1.341 +   * Array of AddonCompatibilityOverride objects, that describe overrides for
   1.342 +   * compatibility with an application versions.
   1.343 +   **/
   1.344 +  compatibilityOverrides: null,
   1.345 +
   1.346 +  /**
   1.347 +   * True if the add-on has a secure means of updating
   1.348 +   */
   1.349 +  providesUpdatesSecurely: true,
   1.350 +
   1.351 +  /**
   1.352 +   * The current blocklist state of the add-on
   1.353 +   */
   1.354 +  blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
   1.355 +
   1.356 +  /**
   1.357 +   * True if this add-on cannot be used in the application based on version
   1.358 +   * compatibility, dependencies and blocklisting
   1.359 +   */
   1.360 +  appDisabled: false,
   1.361 +
   1.362 +  /**
   1.363 +   * True if the user wants this add-on to be disabled
   1.364 +   */
   1.365 +  userDisabled: false,
   1.366 +
   1.367 +  /**
   1.368 +   * Indicates what scope the add-on is installed in, per profile, user,
   1.369 +   * system or application
   1.370 +   */
   1.371 +  scope: AddonManager.SCOPE_PROFILE,
   1.372 +
   1.373 +  /**
   1.374 +   * True if the add-on is currently functional
   1.375 +   */
   1.376 +  isActive: true,
   1.377 +
   1.378 +  /**
   1.379 +   * A bitfield holding all of the current operations that are waiting to be
   1.380 +   * performed for this add-on
   1.381 +   */
   1.382 +  pendingOperations: AddonManager.PENDING_NONE,
   1.383 +
   1.384 +  /**
   1.385 +   * A bitfield holding all the the operations that can be performed on
   1.386 +   * this add-on
   1.387 +   */
   1.388 +  permissions: 0,
   1.389 +
   1.390 +  /**
   1.391 +   * Tests whether this add-on is known to be compatible with a
   1.392 +   * particular application and platform version.
   1.393 +   *
   1.394 +   * @param  appVersion
   1.395 +   *         An application version to test against
   1.396 +   * @param  platformVersion
   1.397 +   *         A platform version to test against
   1.398 +   * @return Boolean representing if the add-on is compatible
   1.399 +   */
   1.400 +  isCompatibleWith: function ASR_isCompatibleWith(aAppVerison, aPlatformVersion) {
   1.401 +    return true;
   1.402 +  },
   1.403 +
   1.404 +  /**
   1.405 +   * Starts an update check for this add-on. This will perform
   1.406 +   * asynchronously and deliver results to the given listener.
   1.407 +   *
   1.408 +   * @param  aListener
   1.409 +   *         An UpdateListener for the update process
   1.410 +   * @param  aReason
   1.411 +   *         A reason code for performing the update
   1.412 +   * @param  aAppVersion
   1.413 +   *         An application version to check for updates for
   1.414 +   * @param  aPlatformVersion
   1.415 +   *         A platform version to check for updates for
   1.416 +   */
   1.417 +  findUpdates: function ASR_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
   1.418 +    if ("onNoCompatibilityUpdateAvailable" in aListener)
   1.419 +      aListener.onNoCompatibilityUpdateAvailable(this);
   1.420 +    if ("onNoUpdateAvailable" in aListener)
   1.421 +      aListener.onNoUpdateAvailable(this);
   1.422 +    if ("onUpdateFinished" in aListener)
   1.423 +      aListener.onUpdateFinished(this);
   1.424 +  },
   1.425 +
   1.426 +  toJSON: function() {
   1.427 +    let json = {};
   1.428 +
   1.429 +    for (let [property, value] of Iterator(this)) {
   1.430 +      if (property.startsWith("_") ||
   1.431 +          typeof(value) === "function")
   1.432 +        continue;
   1.433 +
   1.434 +      try {
   1.435 +        switch (property) {
   1.436 +          case "sourceURI":
   1.437 +            json.sourceURI = value ? value.spec : "";
   1.438 +            break;
   1.439 +
   1.440 +          case "updateDate":
   1.441 +            json.updateDate = value ? value.getTime() : "";
   1.442 +            break;
   1.443 +
   1.444 +          default:
   1.445 +            json[property] = value;
   1.446 +        }
   1.447 +      } catch (ex) {
   1.448 +        logger.warn("Error writing property value for " + property);
   1.449 +      }
   1.450 +    }
   1.451 +
   1.452 +    for (let [property, value] of Iterator(this._unsupportedProperties)) {
   1.453 +      if (!property.startsWith("_"))
   1.454 +        json[property] = value;
   1.455 +    }
   1.456 +
   1.457 +    return json;
   1.458 +  }
   1.459 +}
   1.460 +
   1.461 +/**
   1.462 + * The add-on repository is a source of add-ons that can be installed. It can
   1.463 + * be searched in three ways. The first takes a list of IDs and returns a
   1.464 + * list of the corresponding add-ons. The second returns a list of add-ons that
   1.465 + * come highly recommended. This list should change frequently. The third is to
   1.466 + * search for specific search terms entered by the user. Searches are
   1.467 + * asynchronous and results should be passed to the provided callback object
   1.468 + * when complete. The results passed to the callback should only include add-ons
   1.469 + * that are compatible with the current application and are not already
   1.470 + * installed.
   1.471 + */
   1.472 +this.AddonRepository = {
   1.473 +  /**
   1.474 +   * Whether caching is currently enabled
   1.475 +   */
   1.476 +  get cacheEnabled() {
   1.477 +    // Act as though caching is disabled if there was an unrecoverable error
   1.478 +    // openning the database.
   1.479 +    if (!AddonDatabase.databaseOk) {
   1.480 +      logger.warn("Cache is disabled because database is not OK");
   1.481 +      return false;
   1.482 +    }
   1.483 +
   1.484 +    let preference = PREF_GETADDONS_CACHE_ENABLED;
   1.485 +    let enabled = false;
   1.486 +    try {
   1.487 +      enabled = Services.prefs.getBoolPref(preference);
   1.488 +    } catch(e) {
   1.489 +      logger.warn("cacheEnabled: Couldn't get pref: " + preference);
   1.490 +    }
   1.491 +
   1.492 +    return enabled;
   1.493 +  },
   1.494 +
   1.495 +  // A cache of the add-ons stored in the database
   1.496 +  _addons: null,
   1.497 +
   1.498 +  // An array of callbacks pending the retrieval of add-ons from AddonDatabase
   1.499 +  _pendingCallbacks: null,
   1.500 +
   1.501 +  // Whether a migration in currently in progress
   1.502 +  _migrationInProgress: false,
   1.503 +
   1.504 +  // A callback to be called when migration finishes
   1.505 +  _postMigrationCallback: null,
   1.506 +
   1.507 +  // Whether a search is currently in progress
   1.508 +  _searching: false,
   1.509 +
   1.510 +  // XHR associated with the current request
   1.511 +  _request: null,
   1.512 +
   1.513 +  /*
   1.514 +   * Addon search results callback object that contains two functions
   1.515 +   *
   1.516 +   * searchSucceeded - Called when a search has suceeded.
   1.517 +   *
   1.518 +   * @param  aAddons
   1.519 +   *         An array of the add-on results. In the case of searching for
   1.520 +   *         specific terms the ordering of results may be determined by
   1.521 +   *         the search provider.
   1.522 +   * @param  aAddonCount
   1.523 +   *         The length of aAddons
   1.524 +   * @param  aTotalResults
   1.525 +   *         The total results actually available in the repository
   1.526 +   *
   1.527 +   *
   1.528 +   * searchFailed - Called when an error occurred when performing a search.
   1.529 +   */
   1.530 +  _callback: null,
   1.531 +
   1.532 +  // Maximum number of results to return
   1.533 +  _maxResults: null,
   1.534 +  
   1.535 +  /**
   1.536 +   * Shut down AddonRepository
   1.537 +   * return: promise{integer} resolves with the result of flushing
   1.538 +   *         the AddonRepository database
   1.539 +   */
   1.540 +  shutdown: function AddonRepo_shutdown() {
   1.541 +    this.cancelSearch();
   1.542 +
   1.543 +    this._addons = null;
   1.544 +    this._pendingCallbacks = null;
   1.545 +    return AddonDatabase.shutdown(false);
   1.546 +  },
   1.547 +
   1.548 +  /**
   1.549 +   * Asynchronously get a cached add-on by id. The add-on (or null if the
   1.550 +   * add-on is not found) is passed to the specified callback. If caching is
   1.551 +   * disabled, null is passed to the specified callback.
   1.552 +   *
   1.553 +   * @param  aId
   1.554 +   *         The id of the add-on to get
   1.555 +   * @param  aCallback
   1.556 +   *         The callback to pass the result back to
   1.557 +   */
   1.558 +  getCachedAddonByID: function AddonRepo_getCachedAddonByID(aId, aCallback) {
   1.559 +    if (!aId || !this.cacheEnabled) {
   1.560 +      aCallback(null);
   1.561 +      return;
   1.562 +    }
   1.563 +
   1.564 +    let self = this;
   1.565 +    function getAddon(aAddons) {
   1.566 +      aCallback((aId in aAddons) ? aAddons[aId] : null);
   1.567 +    }
   1.568 +
   1.569 +    if (this._addons == null) {
   1.570 +      if (this._pendingCallbacks == null) {
   1.571 +        // Data has not been retrieved from the database, so retrieve it
   1.572 +        this._pendingCallbacks = [];
   1.573 +        this._pendingCallbacks.push(getAddon);
   1.574 +        AddonDatabase.retrieveStoredData(function getCachedAddonByID_retrieveData(aAddons) {
   1.575 +          let pendingCallbacks = self._pendingCallbacks;
   1.576 +
   1.577 +          // Check if cache was shutdown or deleted before callback was called
   1.578 +          if (pendingCallbacks == null)
   1.579 +            return;
   1.580 +
   1.581 +          // Callbacks may want to trigger a other caching operations that may
   1.582 +          // affect _addons and _pendingCallbacks, so set to final values early
   1.583 +          self._pendingCallbacks = null;
   1.584 +          self._addons = aAddons;
   1.585 +
   1.586 +          pendingCallbacks.forEach(function(aCallback) aCallback(aAddons));
   1.587 +        });
   1.588 +
   1.589 +        return;
   1.590 +      }
   1.591 +
   1.592 +      // Data is being retrieved from the database, so wait
   1.593 +      this._pendingCallbacks.push(getAddon);
   1.594 +      return;
   1.595 +    }
   1.596 +
   1.597 +    // Data has been retrieved, so immediately return result
   1.598 +    getAddon(this._addons);
   1.599 +  },
   1.600 +
   1.601 +  /**
   1.602 +   * Asynchronously repopulate cache so it only contains the add-ons
   1.603 +   * corresponding to the specified ids. If caching is disabled,
   1.604 +   * the cache is completely removed.
   1.605 +   *
   1.606 +   * @param  aIds
   1.607 +   *         The array of add-on ids to repopulate the cache with
   1.608 +   * @param  aCallback
   1.609 +   *         The optional callback to call once complete
   1.610 +   * @param  aTimeout
   1.611 +   *         (Optional) timeout in milliseconds to abandon the XHR request
   1.612 +   *         if we have not received a response from the server.
   1.613 +   */
   1.614 +  repopulateCache: function(aIds, aCallback, aTimeout) {
   1.615 +    this._repopulateCacheInternal(aIds, aCallback, false, aTimeout);
   1.616 +  },
   1.617 +
   1.618 +  _repopulateCacheInternal: function (aIds, aCallback, aSendPerformance, aTimeout) {
   1.619 +    // Always call AddonManager updateAddonRepositoryData after we refill the cache
   1.620 +    function repopulateAddonManager() {
   1.621 +      AddonManagerPrivate.updateAddonRepositoryData(aCallback);
   1.622 +    }
   1.623 +
   1.624 +    logger.debug("Repopulate add-on cache with " + aIds.toSource());
   1.625 +    // Completely remove cache if caching is not enabled
   1.626 +    if (!this.cacheEnabled) {
   1.627 +      logger.debug("Clearing cache because it is disabled");
   1.628 +      this._addons = null;
   1.629 +      this._pendingCallbacks = null;
   1.630 +      AddonDatabase.delete(repopulateAddonManager);
   1.631 +      return;
   1.632 +    }
   1.633 +
   1.634 +    let self = this;
   1.635 +    getAddonsToCache(aIds, function repopulateCache_getAddonsToCache(aAddons) {
   1.636 +      // Completely remove cache if there are no add-ons to cache
   1.637 +      if (aAddons.length == 0) {
   1.638 +        logger.debug("Clearing cache because 0 add-ons were requested");
   1.639 +        self._addons = null;
   1.640 +        self._pendingCallbacks = null;
   1.641 +        AddonDatabase.delete(repopulateAddonManager);
   1.642 +        return;
   1.643 +      }
   1.644 +
   1.645 +      self._beginGetAddons(aAddons, {
   1.646 +        searchSucceeded: function repopulateCacheInternal_searchSucceeded(aAddons) {
   1.647 +          self._addons = {};
   1.648 +          aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
   1.649 +          AddonDatabase.repopulate(aAddons, repopulateAddonManager);
   1.650 +        },
   1.651 +        searchFailed: function repopulateCacheInternal_searchFailed() {
   1.652 +          logger.warn("Search failed when repopulating cache");
   1.653 +          repopulateAddonManager();
   1.654 +        }
   1.655 +      }, aSendPerformance, aTimeout);
   1.656 +    });
   1.657 +  },
   1.658 +
   1.659 +  /**
   1.660 +   * Asynchronously add add-ons to the cache corresponding to the specified
   1.661 +   * ids. If caching is disabled, the cache is unchanged and the callback is
   1.662 +   * immediately called if it is defined.
   1.663 +   *
   1.664 +   * @param  aIds
   1.665 +   *         The array of add-on ids to add to the cache
   1.666 +   * @param  aCallback
   1.667 +   *         The optional callback to call once complete
   1.668 +   */
   1.669 +  cacheAddons: function AddonRepo_cacheAddons(aIds, aCallback) {
   1.670 +    logger.debug("cacheAddons: enabled " + this.cacheEnabled + " IDs " + aIds.toSource());
   1.671 +    if (!this.cacheEnabled) {
   1.672 +      if (aCallback)
   1.673 +        aCallback();
   1.674 +      return;
   1.675 +    }
   1.676 +
   1.677 +    let self = this;
   1.678 +    getAddonsToCache(aIds, function cacheAddons_getAddonsToCache(aAddons) {
   1.679 +      // If there are no add-ons to cache, act as if caching is disabled
   1.680 +      if (aAddons.length == 0) {
   1.681 +        if (aCallback)
   1.682 +          aCallback();
   1.683 +        return;
   1.684 +      }
   1.685 +
   1.686 +      self.getAddonsByIDs(aAddons, {
   1.687 +        searchSucceeded: function cacheAddons_searchSucceeded(aAddons) {
   1.688 +          aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
   1.689 +          AddonDatabase.insertAddons(aAddons, aCallback);
   1.690 +        },
   1.691 +        searchFailed: function cacheAddons_searchFailed() {
   1.692 +          logger.warn("Search failed when adding add-ons to cache");
   1.693 +          if (aCallback)
   1.694 +            aCallback();
   1.695 +        }
   1.696 +      });
   1.697 +    });
   1.698 +  },
   1.699 +
   1.700 +  /**
   1.701 +   * The homepage for visiting this repository. If the corresponding preference
   1.702 +   * is not defined, defaults to about:blank.
   1.703 +   */
   1.704 +  get homepageURL() {
   1.705 +    let url = this._formatURLPref(PREF_GETADDONS_BROWSEADDONS, {});
   1.706 +    return (url != null) ? url : "about:blank";
   1.707 +  },
   1.708 +
   1.709 +  /**
   1.710 +   * Returns whether this instance is currently performing a search. New
   1.711 +   * searches will not be performed while this is the case.
   1.712 +   */
   1.713 +  get isSearching() {
   1.714 +    return this._searching;
   1.715 +  },
   1.716 +
   1.717 +  /**
   1.718 +   * The url that can be visited to see recommended add-ons in this repository.
   1.719 +   * If the corresponding preference is not defined, defaults to about:blank.
   1.720 +   */
   1.721 +  getRecommendedURL: function AddonRepo_getRecommendedURL() {
   1.722 +    let url = this._formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED, {});
   1.723 +    return (url != null) ? url : "about:blank";
   1.724 +  },
   1.725 +
   1.726 +  /**
   1.727 +   * Retrieves the url that can be visited to see search results for the given
   1.728 +   * terms. If the corresponding preference is not defined, defaults to
   1.729 +   * about:blank.
   1.730 +   *
   1.731 +   * @param  aSearchTerms
   1.732 +   *         Search terms used to search the repository
   1.733 +   */
   1.734 +  getSearchURL: function AddonRepo_getSearchURL(aSearchTerms) {
   1.735 +    let url = this._formatURLPref(PREF_GETADDONS_BROWSESEARCHRESULTS, {
   1.736 +      TERMS : encodeURIComponent(aSearchTerms)
   1.737 +    });
   1.738 +    return (url != null) ? url : "about:blank";
   1.739 +  },
   1.740 +
   1.741 +  /**
   1.742 +   * Cancels the search in progress. If there is no search in progress this
   1.743 +   * does nothing.
   1.744 +   */
   1.745 +  cancelSearch: function AddonRepo_cancelSearch() {
   1.746 +    this._searching = false;
   1.747 +    if (this._request) {
   1.748 +      this._request.abort();
   1.749 +      this._request = null;
   1.750 +    }
   1.751 +    this._callback = null;
   1.752 +  },
   1.753 +
   1.754 +  /**
   1.755 +   * Begins a search for add-ons in this repository by ID. Results will be
   1.756 +   * passed to the given callback.
   1.757 +   *
   1.758 +   * @param  aIDs
   1.759 +   *         The array of ids to search for
   1.760 +   * @param  aCallback
   1.761 +   *         The callback to pass results to
   1.762 +   */
   1.763 +  getAddonsByIDs: function AddonRepo_getAddonsByIDs(aIDs, aCallback) {
   1.764 +    return this._beginGetAddons(aIDs, aCallback, false);
   1.765 +  },
   1.766 +
   1.767 +  /**
   1.768 +   * Begins a search of add-ons, potentially sending performance data.
   1.769 +   *
   1.770 +   * @param  aIDs
   1.771 +   *         Array of ids to search for.
   1.772 +   * @param  aCallback
   1.773 +   *         Function to pass results to.
   1.774 +   * @param  aSendPerformance
   1.775 +   *         Boolean indicating whether to send performance data with the
   1.776 +   *         request.
   1.777 +   * @param  aTimeout
   1.778 +   *         (Optional) timeout in milliseconds to abandon the XHR request
   1.779 +   *         if we have not received a response from the server.
   1.780 +   */
   1.781 +  _beginGetAddons: function(aIDs, aCallback, aSendPerformance, aTimeout) {
   1.782 +    let ids = aIDs.slice(0);
   1.783 +
   1.784 +    let params = {
   1.785 +      API_VERSION : API_VERSION,
   1.786 +      IDS : ids.map(encodeURIComponent).join(',')
   1.787 +    };
   1.788 +
   1.789 +    let pref = PREF_GETADDONS_BYIDS;
   1.790 +
   1.791 +    if (aSendPerformance) {
   1.792 +      let type = Services.prefs.getPrefType(PREF_GETADDONS_BYIDS_PERFORMANCE);
   1.793 +      if (type == Services.prefs.PREF_STRING) {
   1.794 +        pref = PREF_GETADDONS_BYIDS_PERFORMANCE;
   1.795 +
   1.796 +        let startupInfo = Cc["@mozilla.org/toolkit/app-startup;1"].
   1.797 +                          getService(Ci.nsIAppStartup).
   1.798 +                          getStartupInfo();
   1.799 +
   1.800 +        params.TIME_MAIN = "";
   1.801 +        params.TIME_FIRST_PAINT = "";
   1.802 +        params.TIME_SESSION_RESTORED = "";
   1.803 +        if (startupInfo.process) {
   1.804 +          if (startupInfo.main) {
   1.805 +            params.TIME_MAIN = startupInfo.main - startupInfo.process;
   1.806 +          }
   1.807 +          if (startupInfo.firstPaint) {
   1.808 +            params.TIME_FIRST_PAINT = startupInfo.firstPaint -
   1.809 +                                      startupInfo.process;
   1.810 +          }
   1.811 +          if (startupInfo.sessionRestored) {
   1.812 +            params.TIME_SESSION_RESTORED = startupInfo.sessionRestored -
   1.813 +                                           startupInfo.process;
   1.814 +          }
   1.815 +        }
   1.816 +      }
   1.817 +    }
   1.818 +
   1.819 +    let url = this._formatURLPref(pref, params);
   1.820 +
   1.821 +    let self = this;
   1.822 +    function handleResults(aElements, aTotalResults, aCompatData) {
   1.823 +      // Don't use this._parseAddons() so that, for example,
   1.824 +      // incompatible add-ons are not filtered out
   1.825 +      let results = [];
   1.826 +      for (let i = 0; i < aElements.length && results.length < self._maxResults; i++) {
   1.827 +        let result = self._parseAddon(aElements[i], null, aCompatData);
   1.828 +        if (result == null)
   1.829 +          continue;
   1.830 +
   1.831 +        // Ignore add-on if it wasn't actually requested
   1.832 +        let idIndex = ids.indexOf(result.addon.id);
   1.833 +        if (idIndex == -1)
   1.834 +          continue;
   1.835 +
   1.836 +        results.push(result);
   1.837 +        // Ignore this add-on from now on
   1.838 +        ids.splice(idIndex, 1);
   1.839 +      }
   1.840 +
   1.841 +      // Include any compatibility overrides for addons not hosted by the
   1.842 +      // remote repository.
   1.843 +      for each (let addonCompat in aCompatData) {
   1.844 +        if (addonCompat.hosted)
   1.845 +          continue;
   1.846 +
   1.847 +        let addon = new AddonSearchResult(addonCompat.id);
   1.848 +        // Compatibility overrides can only be for extensions.
   1.849 +        addon.type = "extension";
   1.850 +        addon.compatibilityOverrides = addonCompat.compatRanges;
   1.851 +        let result = {
   1.852 +          addon: addon,
   1.853 +          xpiURL: null,
   1.854 +          xpiHash: null
   1.855 +        };
   1.856 +        results.push(result);
   1.857 +      }
   1.858 +
   1.859 +      // aTotalResults irrelevant
   1.860 +      self._reportSuccess(results, -1);
   1.861 +    }
   1.862 +
   1.863 +    this._beginSearch(url, ids.length, aCallback, handleResults, aTimeout);
   1.864 +  },
   1.865 +
   1.866 +  /**
   1.867 +   * Performs the daily background update check.
   1.868 +   *
   1.869 +   * This API both searches for the add-on IDs specified and sends performance
   1.870 +   * data. It is meant to be called as part of the daily update ping. It should
   1.871 +   * not be used for any other purpose. Use repopulateCache instead.
   1.872 +   *
   1.873 +   * @param  aIDs
   1.874 +   *         Array of add-on IDs to repopulate the cache with.
   1.875 +   * @param  aCallback
   1.876 +   *         Function to call when data is received. Function must be an object
   1.877 +   *         with the keys searchSucceeded and searchFailed.
   1.878 +   */
   1.879 +  backgroundUpdateCheck: function AddonRepo_backgroundUpdateCheck(aIDs, aCallback) {
   1.880 +    this._repopulateCacheInternal(aIDs, aCallback, true);
   1.881 +  },
   1.882 +
   1.883 +  /**
   1.884 +   * Begins a search for recommended add-ons in this repository. Results will
   1.885 +   * be passed to the given callback.
   1.886 +   *
   1.887 +   * @param  aMaxResults
   1.888 +   *         The maximum number of results to return
   1.889 +   * @param  aCallback
   1.890 +   *         The callback to pass results to
   1.891 +   */
   1.892 +  retrieveRecommendedAddons: function AddonRepo_retrieveRecommendedAddons(aMaxResults, aCallback) {
   1.893 +    let url = this._formatURLPref(PREF_GETADDONS_GETRECOMMENDED, {
   1.894 +      API_VERSION : API_VERSION,
   1.895 +
   1.896 +      // Get twice as many results to account for potential filtering
   1.897 +      MAX_RESULTS : 2 * aMaxResults
   1.898 +    });
   1.899 +
   1.900 +    let self = this;
   1.901 +    function handleResults(aElements, aTotalResults) {
   1.902 +      self._getLocalAddonIds(function retrieveRecommendedAddons_getLocalAddonIds(aLocalAddonIds) {
   1.903 +        // aTotalResults irrelevant
   1.904 +        self._parseAddons(aElements, -1, aLocalAddonIds);
   1.905 +      });
   1.906 +    }
   1.907 +
   1.908 +    this._beginSearch(url, aMaxResults, aCallback, handleResults);
   1.909 +  },
   1.910 +
   1.911 +  /**
   1.912 +   * Begins a search for add-ons in this repository. Results will be passed to
   1.913 +   * the given callback.
   1.914 +   *
   1.915 +   * @param  aSearchTerms
   1.916 +   *         The terms to search for
   1.917 +   * @param  aMaxResults
   1.918 +   *         The maximum number of results to return
   1.919 +   * @param  aCallback
   1.920 +   *         The callback to pass results to
   1.921 +   */
   1.922 +  searchAddons: function AddonRepo_searchAddons(aSearchTerms, aMaxResults, aCallback) {
   1.923 +    let compatMode = "normal";
   1.924 +    if (!AddonManager.checkCompatibility)
   1.925 +      compatMode = "ignore";
   1.926 +    else if (AddonManager.strictCompatibility)
   1.927 +      compatMode = "strict";
   1.928 +
   1.929 +    let substitutions = {
   1.930 +      API_VERSION : API_VERSION,
   1.931 +      TERMS : encodeURIComponent(aSearchTerms),
   1.932 +      // Get twice as many results to account for potential filtering
   1.933 +      MAX_RESULTS : 2 * aMaxResults,
   1.934 +      COMPATIBILITY_MODE : compatMode,
   1.935 +    };
   1.936 +
   1.937 +    let url = this._formatURLPref(PREF_GETADDONS_GETSEARCHRESULTS, substitutions);
   1.938 +
   1.939 +    let self = this;
   1.940 +    function handleResults(aElements, aTotalResults) {
   1.941 +      self._getLocalAddonIds(function searchAddons_getLocalAddonIds(aLocalAddonIds) {
   1.942 +        self._parseAddons(aElements, aTotalResults, aLocalAddonIds);
   1.943 +      });
   1.944 +    }
   1.945 +
   1.946 +    this._beginSearch(url, aMaxResults, aCallback, handleResults);
   1.947 +  },
   1.948 +
   1.949 +  // Posts results to the callback
   1.950 +  _reportSuccess: function AddonRepo_reportSuccess(aResults, aTotalResults) {
   1.951 +    this._searching = false;
   1.952 +    this._request = null;
   1.953 +    // The callback may want to trigger a new search so clear references early
   1.954 +    let addons = [result.addon for each(result in aResults)];
   1.955 +    let callback = this._callback;
   1.956 +    this._callback = null;
   1.957 +    callback.searchSucceeded(addons, addons.length, aTotalResults);
   1.958 +  },
   1.959 +
   1.960 +  // Notifies the callback of a failure
   1.961 +  _reportFailure: function AddonRepo_reportFailure() {
   1.962 +    this._searching = false;
   1.963 +    this._request = null;
   1.964 +    // The callback may want to trigger a new search so clear references early
   1.965 +    let callback = this._callback;
   1.966 +    this._callback = null;
   1.967 +    callback.searchFailed();
   1.968 +  },
   1.969 +
   1.970 +  // Get descendant by unique tag name. Returns null if not unique tag name.
   1.971 +  _getUniqueDescendant: function AddonRepo_getUniqueDescendant(aElement, aTagName) {
   1.972 +    let elementsList = aElement.getElementsByTagName(aTagName);
   1.973 +    return (elementsList.length == 1) ? elementsList[0] : null;
   1.974 +  },
   1.975 +
   1.976 +  // Get direct descendant by unique tag name.
   1.977 +  // Returns null if not unique tag name.
   1.978 +  _getUniqueDirectDescendant: function AddonRepo_getUniqueDirectDescendant(aElement, aTagName) {
   1.979 +    let elementsList = Array.filter(aElement.children,
   1.980 +                                    function arrayFiltering(aChild) aChild.tagName == aTagName);
   1.981 +    return (elementsList.length == 1) ? elementsList[0] : null;
   1.982 +  },
   1.983 +
   1.984 +  // Parse out trimmed text content. Returns null if text content empty.
   1.985 +  _getTextContent: function AddonRepo_getTextContent(aElement) {
   1.986 +    let textContent = aElement.textContent.trim();
   1.987 +    return (textContent.length > 0) ? textContent : null;
   1.988 +  },
   1.989 +
   1.990 +  // Parse out trimmed text content of a descendant with the specified tag name
   1.991 +  // Returns null if the parsing unsuccessful.
   1.992 +  _getDescendantTextContent: function AddonRepo_getDescendantTextContent(aElement, aTagName) {
   1.993 +    let descendant = this._getUniqueDescendant(aElement, aTagName);
   1.994 +    return (descendant != null) ? this._getTextContent(descendant) : null;
   1.995 +  },
   1.996 +
   1.997 +  // Parse out trimmed text content of a direct descendant with the specified
   1.998 +  // tag name.
   1.999 +  // Returns null if the parsing unsuccessful.
  1.1000 +  _getDirectDescendantTextContent: function AddonRepo_getDirectDescendantTextContent(aElement, aTagName) {
  1.1001 +    let descendant = this._getUniqueDirectDescendant(aElement, aTagName);
  1.1002 +    return (descendant != null) ? this._getTextContent(descendant) : null;
  1.1003 +  },
  1.1004 +
  1.1005 +  /*
  1.1006 +   * Creates an AddonSearchResult by parsing an <addon> element
  1.1007 +   *
  1.1008 +   * @param  aElement
  1.1009 +   *         The <addon> element to parse
  1.1010 +   * @param  aSkip
  1.1011 +   *         Object containing ids and sourceURIs of add-ons to skip.
  1.1012 +   * @param  aCompatData
  1.1013 +   *         Array of parsed addon_compatibility elements to accosiate with the
  1.1014 +   *         resulting AddonSearchResult. Optional.
  1.1015 +   * @return Result object containing the parsed AddonSearchResult, xpiURL and
  1.1016 +   *         xpiHash if the parsing was successful. Otherwise returns null.
  1.1017 +   */
  1.1018 +  _parseAddon: function AddonRepo_parseAddon(aElement, aSkip, aCompatData) {
  1.1019 +    let skipIDs = (aSkip && aSkip.ids) ? aSkip.ids : [];
  1.1020 +    let skipSourceURIs = (aSkip && aSkip.sourceURIs) ? aSkip.sourceURIs : [];
  1.1021 +
  1.1022 +    let guid = this._getDescendantTextContent(aElement, "guid");
  1.1023 +    if (guid == null || skipIDs.indexOf(guid) != -1)
  1.1024 +      return null;
  1.1025 +
  1.1026 +    let addon = new AddonSearchResult(guid);
  1.1027 +    let result = {
  1.1028 +      addon: addon,
  1.1029 +      xpiURL: null,
  1.1030 +      xpiHash: null
  1.1031 +    };
  1.1032 +
  1.1033 +    if (aCompatData && guid in aCompatData)
  1.1034 +      addon.compatibilityOverrides = aCompatData[guid].compatRanges;
  1.1035 +
  1.1036 +    let self = this;
  1.1037 +    for (let node = aElement.firstChild; node; node = node.nextSibling) {
  1.1038 +      if (!(node instanceof Ci.nsIDOMElement))
  1.1039 +        continue;
  1.1040 +
  1.1041 +      let localName = node.localName;
  1.1042 +
  1.1043 +      // Handle case where the wanted string value is located in text content
  1.1044 +      // but only if the content is not empty
  1.1045 +      if (localName in STRING_KEY_MAP) {
  1.1046 +        addon[STRING_KEY_MAP[localName]] = this._getTextContent(node) || addon[STRING_KEY_MAP[localName]];
  1.1047 +        continue;
  1.1048 +      }
  1.1049 +
  1.1050 +      // Handle case where the wanted string value is html located in text content
  1.1051 +      if (localName in HTML_KEY_MAP) {
  1.1052 +        addon[HTML_KEY_MAP[localName]] = convertHTMLToPlainText(this._getTextContent(node));
  1.1053 +        continue;
  1.1054 +      }
  1.1055 +
  1.1056 +      // Handle case where the wanted integer value is located in text content
  1.1057 +      if (localName in INTEGER_KEY_MAP) {
  1.1058 +        let value = parseInt(this._getTextContent(node));
  1.1059 +        if (value >= 0)
  1.1060 +          addon[INTEGER_KEY_MAP[localName]] = value;
  1.1061 +        continue;
  1.1062 +      }
  1.1063 +
  1.1064 +      // Handle cases that aren't as simple as grabbing the text content
  1.1065 +      switch (localName) {
  1.1066 +        case "type":
  1.1067 +          // Map AMO's type id to corresponding string
  1.1068 +          let id = parseInt(node.getAttribute("id"));
  1.1069 +          switch (id) {
  1.1070 +            case 1:
  1.1071 +              addon.type = "extension";
  1.1072 +              break;
  1.1073 +            case 2:
  1.1074 +              addon.type = "theme";
  1.1075 +              break;
  1.1076 +            case 3:
  1.1077 +              addon.type = "dictionary";
  1.1078 +              break;
  1.1079 +            default:
  1.1080 +              logger.warn("Unknown type id when parsing addon: " + id);
  1.1081 +          }
  1.1082 +          break;
  1.1083 +        case "authors":
  1.1084 +          let authorNodes = node.getElementsByTagName("author");
  1.1085 +          for (let authorNode of authorNodes) {
  1.1086 +            let name = self._getDescendantTextContent(authorNode, "name");
  1.1087 +            let link = self._getDescendantTextContent(authorNode, "link");
  1.1088 +            if (name == null || link == null)
  1.1089 +              continue;
  1.1090 +
  1.1091 +            let author = new AddonManagerPrivate.AddonAuthor(name, link);
  1.1092 +            if (addon.creator == null)
  1.1093 +              addon.creator = author;
  1.1094 +            else {
  1.1095 +              if (addon.developers == null)
  1.1096 +                addon.developers = [];
  1.1097 +
  1.1098 +              addon.developers.push(author);
  1.1099 +            }
  1.1100 +          }
  1.1101 +          break;
  1.1102 +        case "previews":
  1.1103 +          let previewNodes = node.getElementsByTagName("preview");
  1.1104 +          for (let previewNode of previewNodes) {
  1.1105 +            let full = self._getUniqueDescendant(previewNode, "full");
  1.1106 +            if (full == null)
  1.1107 +              continue;
  1.1108 +
  1.1109 +            let fullURL = self._getTextContent(full);
  1.1110 +            let fullWidth = full.getAttribute("width");
  1.1111 +            let fullHeight = full.getAttribute("height");
  1.1112 +
  1.1113 +            let thumbnailURL, thumbnailWidth, thumbnailHeight;
  1.1114 +            let thumbnail = self._getUniqueDescendant(previewNode, "thumbnail");
  1.1115 +            if (thumbnail) {
  1.1116 +              thumbnailURL = self._getTextContent(thumbnail);
  1.1117 +              thumbnailWidth = thumbnail.getAttribute("width");
  1.1118 +              thumbnailHeight = thumbnail.getAttribute("height");
  1.1119 +            }
  1.1120 +            let caption = self._getDescendantTextContent(previewNode, "caption");
  1.1121 +            let screenshot = new AddonManagerPrivate.AddonScreenshot(fullURL, fullWidth, fullHeight,
  1.1122 +                                                                     thumbnailURL, thumbnailWidth,
  1.1123 +                                                                     thumbnailHeight, caption);
  1.1124 +
  1.1125 +            if (addon.screenshots == null)
  1.1126 +              addon.screenshots = [];
  1.1127 +
  1.1128 +            if (previewNode.getAttribute("primary") == 1)
  1.1129 +              addon.screenshots.unshift(screenshot);
  1.1130 +            else
  1.1131 +              addon.screenshots.push(screenshot);
  1.1132 +          }
  1.1133 +          break;
  1.1134 +        case "learnmore":
  1.1135 +          addon.learnmoreURL = this._getTextContent(node);
  1.1136 +          addon.homepageURL = addon.homepageURL || addon.learnmoreURL;
  1.1137 +          break;
  1.1138 +        case "contribution_data":
  1.1139 +          let meetDevelopers = this._getDescendantTextContent(node, "meet_developers");
  1.1140 +          let suggestedAmount = this._getDescendantTextContent(node, "suggested_amount");
  1.1141 +          if (meetDevelopers != null) {
  1.1142 +            addon.contributionURL = meetDevelopers;
  1.1143 +            addon.contributionAmount = suggestedAmount;
  1.1144 +          }
  1.1145 +          break
  1.1146 +        case "payment_data":
  1.1147 +          let link = this._getDescendantTextContent(node, "link");
  1.1148 +          let amountTag = this._getUniqueDescendant(node, "amount");
  1.1149 +          let amount = parseFloat(amountTag.getAttribute("amount"));
  1.1150 +          let displayAmount = this._getTextContent(amountTag);
  1.1151 +          if (link != null && amount != null && displayAmount != null) {
  1.1152 +            addon.purchaseURL = link;
  1.1153 +            addon.purchaseAmount = amount;
  1.1154 +            addon.purchaseDisplayAmount = displayAmount;
  1.1155 +          }
  1.1156 +          break
  1.1157 +        case "rating":
  1.1158 +          let averageRating = parseInt(this._getTextContent(node));
  1.1159 +          if (averageRating >= 0)
  1.1160 +            addon.averageRating = Math.min(5, averageRating);
  1.1161 +          break;
  1.1162 +        case "reviews":
  1.1163 +          let url = this._getTextContent(node);
  1.1164 +          let num = parseInt(node.getAttribute("num"));
  1.1165 +          if (url != null && num >= 0) {
  1.1166 +            addon.reviewURL = url;
  1.1167 +            addon.reviewCount = num;
  1.1168 +          }
  1.1169 +          break;
  1.1170 +        case "status":
  1.1171 +          let repositoryStatus = parseInt(node.getAttribute("id"));
  1.1172 +          if (!isNaN(repositoryStatus))
  1.1173 +            addon.repositoryStatus = repositoryStatus;
  1.1174 +          break;
  1.1175 +        case "all_compatible_os":
  1.1176 +          let nodes = node.getElementsByTagName("os");
  1.1177 +          addon.isPlatformCompatible = Array.some(nodes, function parseAddon_platformCompatFilter(aNode) {
  1.1178 +            let text = aNode.textContent.toLowerCase().trim();
  1.1179 +            return text == "all" || text == Services.appinfo.OS.toLowerCase();
  1.1180 +          });
  1.1181 +          break;
  1.1182 +        case "install":
  1.1183 +          // No os attribute means the xpi is compatible with any os
  1.1184 +          if (node.hasAttribute("os")) {
  1.1185 +            let os = node.getAttribute("os").trim().toLowerCase();
  1.1186 +            // If the os is not ALL and not the current OS then ignore this xpi
  1.1187 +            if (os != "all" && os != Services.appinfo.OS.toLowerCase())
  1.1188 +              break;
  1.1189 +          }
  1.1190 +
  1.1191 +          let xpiURL = this._getTextContent(node);
  1.1192 +          if (xpiURL == null)
  1.1193 +            break;
  1.1194 +
  1.1195 +          if (skipSourceURIs.indexOf(xpiURL) != -1)
  1.1196 +            return null;
  1.1197 +
  1.1198 +          result.xpiURL = xpiURL;
  1.1199 +          addon.sourceURI = NetUtil.newURI(xpiURL);
  1.1200 +
  1.1201 +          let size = parseInt(node.getAttribute("size"));
  1.1202 +          addon.size = (size >= 0) ? size : null;
  1.1203 +
  1.1204 +          let xpiHash = node.getAttribute("hash");
  1.1205 +          if (xpiHash != null)
  1.1206 +            xpiHash = xpiHash.trim();
  1.1207 +          result.xpiHash = xpiHash ? xpiHash : null;
  1.1208 +          break;
  1.1209 +        case "last_updated":
  1.1210 +          let epoch = parseInt(node.getAttribute("epoch"));
  1.1211 +          if (!isNaN(epoch))
  1.1212 +            addon.updateDate = new Date(1000 * epoch);
  1.1213 +          break;
  1.1214 +        case "icon":
  1.1215 +          addon.icons[node.getAttribute("size")] = this._getTextContent(node);
  1.1216 +          break;
  1.1217 +      }
  1.1218 +    }
  1.1219 +
  1.1220 +    return result;
  1.1221 +  },
  1.1222 +
  1.1223 +  _parseAddons: function AddonRepo_parseAddons(aElements, aTotalResults, aSkip) {
  1.1224 +    let self = this;
  1.1225 +    let results = [];
  1.1226 +
  1.1227 +    function isSameApplication(aAppNode) {
  1.1228 +      return self._getTextContent(aAppNode) == Services.appinfo.ID;
  1.1229 +    }
  1.1230 +
  1.1231 +    for (let i = 0; i < aElements.length && results.length < this._maxResults; i++) {
  1.1232 +      let element = aElements[i];
  1.1233 +
  1.1234 +      let tags = this._getUniqueDescendant(element, "compatible_applications");
  1.1235 +      if (tags == null)
  1.1236 +        continue;
  1.1237 +
  1.1238 +      let applications = tags.getElementsByTagName("appID");
  1.1239 +      let compatible = Array.some(applications, function parseAddons_applicationsCompatFilter(aAppNode) {
  1.1240 +        if (!isSameApplication(aAppNode))
  1.1241 +          return false;
  1.1242 +
  1.1243 +        let parent = aAppNode.parentNode;
  1.1244 +        let minVersion = self._getDescendantTextContent(parent, "min_version");
  1.1245 +        let maxVersion = self._getDescendantTextContent(parent, "max_version");
  1.1246 +        if (minVersion == null || maxVersion == null)
  1.1247 +          return false;
  1.1248 +
  1.1249 +        let currentVersion = Services.appinfo.version;
  1.1250 +        return (Services.vc.compare(minVersion, currentVersion) <= 0 &&
  1.1251 +                ((!AddonManager.strictCompatibility) ||
  1.1252 +                 Services.vc.compare(currentVersion, maxVersion) <= 0));
  1.1253 +      });
  1.1254 +
  1.1255 +      // Ignore add-ons not compatible with this Application
  1.1256 +      if (!compatible) {
  1.1257 +        if (AddonManager.checkCompatibility)
  1.1258 +          continue;
  1.1259 +
  1.1260 +        if (!Array.some(applications, isSameApplication))
  1.1261 +          continue;
  1.1262 +      }
  1.1263 +
  1.1264 +      // Add-on meets all requirements, so parse out data.
  1.1265 +      // Don't pass in compatiblity override data, because that's only returned
  1.1266 +      // in GUID searches, which don't use _parseAddons().
  1.1267 +      let result = this._parseAddon(element, aSkip);
  1.1268 +      if (result == null)
  1.1269 +        continue;
  1.1270 +
  1.1271 +      // Ignore add-on missing a required attribute
  1.1272 +      let requiredAttributes = ["id", "name", "version", "type", "creator"];
  1.1273 +      if (requiredAttributes.some(function parseAddons_attributeFilter(aAttribute) !result.addon[aAttribute]))
  1.1274 +        continue;
  1.1275 +
  1.1276 +      // Add only if the add-on is compatible with the platform
  1.1277 +      if (!result.addon.isPlatformCompatible)
  1.1278 +        continue;
  1.1279 +
  1.1280 +      // Add only if there was an xpi compatible with this OS or there was a
  1.1281 +      // way to purchase the add-on
  1.1282 +      if (!result.xpiURL && !result.addon.purchaseURL)
  1.1283 +        continue;
  1.1284 +
  1.1285 +      result.addon.isCompatible = compatible;
  1.1286 +
  1.1287 +      results.push(result);
  1.1288 +      // Ignore this add-on from now on by adding it to the skip array
  1.1289 +      aSkip.ids.push(result.addon.id);
  1.1290 +    }
  1.1291 +
  1.1292 +    // Immediately report success if no AddonInstall instances to create
  1.1293 +    let pendingResults = results.length;
  1.1294 +    if (pendingResults == 0) {
  1.1295 +      this._reportSuccess(results, aTotalResults);
  1.1296 +      return;
  1.1297 +    }
  1.1298 +
  1.1299 +    // Create an AddonInstall for each result
  1.1300 +    let self = this;
  1.1301 +    results.forEach(function(aResult) {
  1.1302 +      let addon = aResult.addon;
  1.1303 +      let callback = function addonInstallCallback(aInstall) {
  1.1304 +        addon.install = aInstall;
  1.1305 +        pendingResults--;
  1.1306 +        if (pendingResults == 0)
  1.1307 +          self._reportSuccess(results, aTotalResults);
  1.1308 +      }
  1.1309 +
  1.1310 +      if (aResult.xpiURL) {
  1.1311 +        AddonManager.getInstallForURL(aResult.xpiURL, callback,
  1.1312 +                                      "application/x-xpinstall", aResult.xpiHash,
  1.1313 +                                      addon.name, addon.icons, addon.version);
  1.1314 +      }
  1.1315 +      else {
  1.1316 +        callback(null);
  1.1317 +      }
  1.1318 +    });
  1.1319 +  },
  1.1320 +
  1.1321 +  // Parses addon_compatibility nodes, that describe compatibility overrides.
  1.1322 +  _parseAddonCompatElement: function AddonRepo_parseAddonCompatElement(aResultObj, aElement) {
  1.1323 +    let guid = this._getDescendantTextContent(aElement, "guid");
  1.1324 +    if (!guid) {
  1.1325 +        logger.debug("Compatibility override is missing guid.");
  1.1326 +      return;
  1.1327 +    }
  1.1328 +
  1.1329 +    let compat = {id: guid};
  1.1330 +    compat.hosted = aElement.getAttribute("hosted") != "false";
  1.1331 +
  1.1332 +    function findMatchingAppRange(aNodes) {
  1.1333 +      let toolkitAppRange = null;
  1.1334 +      for (let node of aNodes) {
  1.1335 +        let appID = this._getDescendantTextContent(node, "appID");
  1.1336 +        if (appID != Services.appinfo.ID && appID != TOOLKIT_ID)
  1.1337 +          continue;
  1.1338 +
  1.1339 +        let minVersion = this._getDescendantTextContent(node, "min_version");
  1.1340 +        let maxVersion = this._getDescendantTextContent(node, "max_version");
  1.1341 +        if (minVersion == null || maxVersion == null)
  1.1342 +          continue;
  1.1343 +
  1.1344 +        let appRange = { appID: appID,
  1.1345 +                         appMinVersion: minVersion,
  1.1346 +                         appMaxVersion: maxVersion };
  1.1347 +
  1.1348 +        // Only use Toolkit app ranges if no ranges match the application ID.
  1.1349 +        if (appID == TOOLKIT_ID)
  1.1350 +          toolkitAppRange = appRange;
  1.1351 +        else
  1.1352 +          return appRange;
  1.1353 +      }
  1.1354 +      return toolkitAppRange;
  1.1355 +    }
  1.1356 +
  1.1357 +    function parseRangeNode(aNode) {
  1.1358 +      let type = aNode.getAttribute("type");
  1.1359 +      // Only "incompatible" (blacklisting) is supported for now.
  1.1360 +      if (type != "incompatible") {
  1.1361 +        logger.debug("Compatibility override of unsupported type found.");
  1.1362 +        return null;
  1.1363 +      }
  1.1364 +
  1.1365 +      let override = new AddonManagerPrivate.AddonCompatibilityOverride(type);
  1.1366 +
  1.1367 +      override.minVersion = this._getDirectDescendantTextContent(aNode, "min_version");
  1.1368 +      override.maxVersion = this._getDirectDescendantTextContent(aNode, "max_version");
  1.1369 +
  1.1370 +      if (!override.minVersion) {
  1.1371 +        logger.debug("Compatibility override is missing min_version.");
  1.1372 +        return null;
  1.1373 +      }
  1.1374 +      if (!override.maxVersion) {
  1.1375 +        logger.debug("Compatibility override is missing max_version.");
  1.1376 +        return null;
  1.1377 +      }
  1.1378 +
  1.1379 +      let appRanges = aNode.querySelectorAll("compatible_applications > application");
  1.1380 +      let appRange = findMatchingAppRange.bind(this)(appRanges);
  1.1381 +      if (!appRange) {
  1.1382 +        logger.debug("Compatibility override is missing a valid application range.");
  1.1383 +        return null;
  1.1384 +      }
  1.1385 +
  1.1386 +      override.appID = appRange.appID;
  1.1387 +      override.appMinVersion = appRange.appMinVersion;
  1.1388 +      override.appMaxVersion = appRange.appMaxVersion;
  1.1389 +
  1.1390 +      return override;
  1.1391 +    }
  1.1392 +
  1.1393 +    let rangeNodes = aElement.querySelectorAll("version_ranges > version_range");
  1.1394 +    compat.compatRanges = Array.map(rangeNodes, parseRangeNode.bind(this))
  1.1395 +                               .filter(function compatRangesFilter(aItem) !!aItem);
  1.1396 +    if (compat.compatRanges.length == 0)
  1.1397 +      return;
  1.1398 +
  1.1399 +    aResultObj[compat.id] = compat;
  1.1400 +  },
  1.1401 +
  1.1402 +  // Parses addon_compatibility elements.
  1.1403 +  _parseAddonCompatData: function AddonRepo_parseAddonCompatData(aElements) {
  1.1404 +    let compatData = {};
  1.1405 +    Array.forEach(aElements, this._parseAddonCompatElement.bind(this, compatData));
  1.1406 +    return compatData;
  1.1407 +  },
  1.1408 +
  1.1409 +  // Begins a new search if one isn't currently executing
  1.1410 +  _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults, aTimeout) {
  1.1411 +    if (this._searching || aURI == null || aMaxResults <= 0) {
  1.1412 +      logger.warn("AddonRepository search failed: searching " + this._searching + " aURI " + aURI +
  1.1413 +                  " aMaxResults " + aMaxResults);
  1.1414 +      aCallback.searchFailed();
  1.1415 +      return;
  1.1416 +    }
  1.1417 +
  1.1418 +    this._searching = true;
  1.1419 +    this._callback = aCallback;
  1.1420 +    this._maxResults = aMaxResults;
  1.1421 +
  1.1422 +    logger.debug("Requesting " + aURI);
  1.1423 +
  1.1424 +    this._request = new XHRequest();
  1.1425 +    this._request.mozBackgroundRequest = true;
  1.1426 +    this._request.open("GET", aURI, true);
  1.1427 +    this._request.overrideMimeType("text/xml");
  1.1428 +    if (aTimeout) {
  1.1429 +      this._request.timeout = aTimeout;
  1.1430 +    }
  1.1431 +
  1.1432 +    this._request.addEventListener("error", aEvent => this._reportFailure(), false);
  1.1433 +    this._request.addEventListener("timeout", aEvent => this._reportFailure(), false);
  1.1434 +    this._request.addEventListener("load", aEvent => {
  1.1435 +      logger.debug("Got metadata search load event");
  1.1436 +      let request = aEvent.target;
  1.1437 +      let responseXML = request.responseXML;
  1.1438 +
  1.1439 +      if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
  1.1440 +          (request.status != 200 && request.status != 0)) {
  1.1441 +        this._reportFailure();
  1.1442 +        return;
  1.1443 +      }
  1.1444 +
  1.1445 +      let documentElement = responseXML.documentElement;
  1.1446 +      let elements = documentElement.getElementsByTagName("addon");
  1.1447 +      let totalResults = elements.length;
  1.1448 +      let parsedTotalResults = parseInt(documentElement.getAttribute("total_results"));
  1.1449 +      // Parsed value of total results only makes sense if >= elements.length
  1.1450 +      if (parsedTotalResults >= totalResults)
  1.1451 +        totalResults = parsedTotalResults;
  1.1452 +
  1.1453 +      let compatElements = documentElement.getElementsByTagName("addon_compatibility");
  1.1454 +      let compatData = this._parseAddonCompatData(compatElements);
  1.1455 +
  1.1456 +      aHandleResults(elements, totalResults, compatData);
  1.1457 +    }, false);
  1.1458 +    this._request.send(null);
  1.1459 +  },
  1.1460 +
  1.1461 +  // Gets the id's of local add-ons, and the sourceURI's of local installs,
  1.1462 +  // passing the results to aCallback
  1.1463 +  _getLocalAddonIds: function AddonRepo_getLocalAddonIds(aCallback) {
  1.1464 +    let self = this;
  1.1465 +    let localAddonIds = {ids: null, sourceURIs: null};
  1.1466 +
  1.1467 +    AddonManager.getAllAddons(function getLocalAddonIds_getAllAddons(aAddons) {
  1.1468 +      localAddonIds.ids = [a.id for each (a in aAddons)];
  1.1469 +      if (localAddonIds.sourceURIs)
  1.1470 +        aCallback(localAddonIds);
  1.1471 +    });
  1.1472 +
  1.1473 +    AddonManager.getAllInstalls(function getLocalAddonIds_getAllInstalls(aInstalls) {
  1.1474 +      localAddonIds.sourceURIs = [];
  1.1475 +      aInstalls.forEach(function(aInstall) {
  1.1476 +        if (aInstall.state != AddonManager.STATE_AVAILABLE)
  1.1477 +          localAddonIds.sourceURIs.push(aInstall.sourceURI.spec);
  1.1478 +      });
  1.1479 +
  1.1480 +      if (localAddonIds.ids)
  1.1481 +        aCallback(localAddonIds);
  1.1482 +    });
  1.1483 +  },
  1.1484 +
  1.1485 +  // Create url from preference, returning null if preference does not exist
  1.1486 +  _formatURLPref: function AddonRepo_formatURLPref(aPreference, aSubstitutions) {
  1.1487 +    let url = null;
  1.1488 +    try {
  1.1489 +      url = Services.prefs.getCharPref(aPreference);
  1.1490 +    } catch(e) {
  1.1491 +      logger.warn("_formatURLPref: Couldn't get pref: " + aPreference);
  1.1492 +      return null;
  1.1493 +    }
  1.1494 +
  1.1495 +    url = url.replace(/%([A-Z_]+)%/g, function urlSubstitution(aMatch, aKey) {
  1.1496 +      return (aKey in aSubstitutions) ? aSubstitutions[aKey] : aMatch;
  1.1497 +    });
  1.1498 +
  1.1499 +    return Services.urlFormatter.formatURL(url);
  1.1500 +  },
  1.1501 +
  1.1502 +  // Find a AddonCompatibilityOverride that matches a given aAddonVersion and
  1.1503 +  // application/platform version.
  1.1504 +  findMatchingCompatOverride: function AddonRepo_findMatchingCompatOverride(aAddonVersion,
  1.1505 +                                                                     aCompatOverrides,
  1.1506 +                                                                     aAppVersion,
  1.1507 +                                                                     aPlatformVersion) {
  1.1508 +    for (let override of aCompatOverrides) {
  1.1509 +
  1.1510 +      let appVersion = null;
  1.1511 +      if (override.appID == TOOLKIT_ID)
  1.1512 +        appVersion = aPlatformVersion || Services.appinfo.platformVersion;
  1.1513 +      else
  1.1514 +        appVersion = aAppVersion || Services.appinfo.version;
  1.1515 +
  1.1516 +      if (Services.vc.compare(override.minVersion, aAddonVersion) <= 0 &&
  1.1517 +          Services.vc.compare(aAddonVersion, override.maxVersion) <= 0 &&
  1.1518 +          Services.vc.compare(override.appMinVersion, appVersion) <= 0 &&
  1.1519 +          Services.vc.compare(appVersion, override.appMaxVersion) <= 0) {
  1.1520 +        return override;
  1.1521 +      }
  1.1522 +    }
  1.1523 +    return null;
  1.1524 +  },
  1.1525 +
  1.1526 +  flush: function() {
  1.1527 +    return AddonDatabase.flush();
  1.1528 +  }
  1.1529 +};
  1.1530 +
  1.1531 +var AddonDatabase = {
  1.1532 +  // true if the database connection has been opened
  1.1533 +  initialized: false,
  1.1534 +  // false if there was an unrecoverable error openning the database
  1.1535 +  databaseOk: true,
  1.1536 +
  1.1537 +  // the in-memory database
  1.1538 +  DB: BLANK_DB(),
  1.1539 +
  1.1540 +  /**
  1.1541 +   * A getter to retrieve an nsIFile pointer to the DB
  1.1542 +   */
  1.1543 +  get jsonFile() {
  1.1544 +    delete this.jsonFile;
  1.1545 +    return this.jsonFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
  1.1546 +  },
  1.1547 +
  1.1548 +  /**
  1.1549 +   * Synchronously opens a new connection to the database file.
  1.1550 +   */
  1.1551 +  openConnection: function() {
  1.1552 +    this.DB = BLANK_DB();
  1.1553 +    this.initialized = true;
  1.1554 +    delete this.connection;
  1.1555 +
  1.1556 +    let inputDB, fstream, cstream, schema;
  1.1557 +
  1.1558 +    try {
  1.1559 +     let data = "";
  1.1560 +     fstream = Cc["@mozilla.org/network/file-input-stream;1"]
  1.1561 +                 .createInstance(Ci.nsIFileInputStream);
  1.1562 +     cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
  1.1563 +                 .createInstance(Ci.nsIConverterInputStream);
  1.1564 +
  1.1565 +     fstream.init(this.jsonFile, -1, 0, 0);
  1.1566 +     cstream.init(fstream, "UTF-8", 0, 0);
  1.1567 +     let (str = {}) {
  1.1568 +       let read = 0;
  1.1569 +       do {
  1.1570 +         read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
  1.1571 +         data += str.value;
  1.1572 +       } while (read != 0);
  1.1573 +     }
  1.1574 +
  1.1575 +     inputDB = JSON.parse(data);
  1.1576 +
  1.1577 +     if (!inputDB.hasOwnProperty("addons") ||
  1.1578 +         !Array.isArray(inputDB.addons)) {
  1.1579 +       throw new Error("No addons array.");
  1.1580 +     }
  1.1581 +
  1.1582 +     if (!inputDB.hasOwnProperty("schema")) {
  1.1583 +       throw new Error("No schema specified.");
  1.1584 +     }
  1.1585 +
  1.1586 +     schema = parseInt(inputDB.schema, 10);
  1.1587 +
  1.1588 +     if (!Number.isInteger(schema) ||
  1.1589 +         schema < DB_MIN_JSON_SCHEMA) {
  1.1590 +       throw new Error("Invalid schema value.");
  1.1591 +     }
  1.1592 +
  1.1593 +    } catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
  1.1594 +      logger.debug("No " + FILE_DATABASE + " found.");
  1.1595 +
  1.1596 +      // Create a blank addons.json file
  1.1597 +      this._saveDBToDisk();
  1.1598 +
  1.1599 +      let dbSchema = 0;
  1.1600 +      try {
  1.1601 +        dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA);
  1.1602 +      } catch (e) {}
  1.1603 +
  1.1604 +      if (dbSchema < DB_MIN_JSON_SCHEMA) {
  1.1605 +        this._migrationInProgress = AddonRepository_SQLiteMigrator.migrate((results) => {
  1.1606 +          if (results.length)
  1.1607 +            this.insertAddons(results);
  1.1608 +
  1.1609 +          if (this._postMigrationCallback) {
  1.1610 +            this._postMigrationCallback();
  1.1611 +            this._postMigrationCallback = null;
  1.1612 +          }
  1.1613 +
  1.1614 +          this._migrationInProgress = false;
  1.1615 +        });
  1.1616 +
  1.1617 +        Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
  1.1618 +      }
  1.1619 +
  1.1620 +      return;
  1.1621 +
  1.1622 +    } catch (e) {
  1.1623 +      logger.error("Malformed " + FILE_DATABASE + ": " + e);
  1.1624 +      this.databaseOk = false;
  1.1625 +      return;
  1.1626 +
  1.1627 +    } finally {
  1.1628 +     cstream.close();
  1.1629 +     fstream.close();
  1.1630 +    }
  1.1631 +
  1.1632 +    Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
  1.1633 +
  1.1634 +    // We use _insertAddon manually instead of calling
  1.1635 +    // insertAddons to avoid the write to disk which would
  1.1636 +    // be a waste since this is the data that was just read.
  1.1637 +    for (let addon of inputDB.addons) {
  1.1638 +      this._insertAddon(addon);
  1.1639 +    }
  1.1640 +  },
  1.1641 +
  1.1642 +  /**
  1.1643 +   * A lazy getter for the database connection.
  1.1644 +   */
  1.1645 +  get connection() {
  1.1646 +    return this.openConnection();
  1.1647 +  },
  1.1648 +
  1.1649 +  /**
  1.1650 +   * Asynchronously shuts down the database connection and releases all
  1.1651 +   * cached objects
  1.1652 +   *
  1.1653 +   * @param  aCallback
  1.1654 +   *         An optional callback to call once complete
  1.1655 +   * @param  aSkipFlush
  1.1656 +   *         An optional boolean to skip flushing data to disk. Useful
  1.1657 +   *         when the database is going to be deleted afterwards.
  1.1658 +   */
  1.1659 +  shutdown: function AD_shutdown(aSkipFlush) {
  1.1660 +    this.databaseOk = true;
  1.1661 +
  1.1662 +    if (!this.initialized) {
  1.1663 +      return Promise.resolve(0);
  1.1664 +    }
  1.1665 +
  1.1666 +    this.initialized = false;
  1.1667 +
  1.1668 +    this.__defineGetter__("connection", function shutdown_connectionGetter() {
  1.1669 +      return this.openConnection();
  1.1670 +    });
  1.1671 +
  1.1672 +    if (aSkipFlush) {
  1.1673 +      return Promise.resolve(0);
  1.1674 +    } else {
  1.1675 +      return this.Writer.flush();
  1.1676 +    }
  1.1677 +  },
  1.1678 +
  1.1679 +  /**
  1.1680 +   * Asynchronously deletes the database, shutting down the connection
  1.1681 +   * first if initialized
  1.1682 +   *
  1.1683 +   * @param  aCallback
  1.1684 +   *         An optional callback to call once complete
  1.1685 +   */
  1.1686 +  delete: function AD_delete(aCallback) {
  1.1687 +    this.DB = BLANK_DB();
  1.1688 +
  1.1689 +    this._deleting = this.Writer.flush()
  1.1690 +      .then(null, () => {})
  1.1691 +      // shutdown(true) never rejects
  1.1692 +      .then(() => this.shutdown(true))
  1.1693 +      .then(() => OS.File.remove(this.jsonFile.path, {}))
  1.1694 +      .then(null, error => logger.error("Unable to delete Addon Repository file " +
  1.1695 +                                 this.jsonFile.path, error))
  1.1696 +      .then(() => this._deleting = null)
  1.1697 +      .then(aCallback);
  1.1698 +  },
  1.1699 +
  1.1700 +  toJSON: function AD_toJSON() {
  1.1701 +    let json = {
  1.1702 +      schema: this.DB.schema,
  1.1703 +      addons: []
  1.1704 +    }
  1.1705 +
  1.1706 +    for (let [, value] of this.DB.addons)
  1.1707 +      json.addons.push(value);
  1.1708 +
  1.1709 +    return json;
  1.1710 +  },
  1.1711 +
  1.1712 +  /*
  1.1713 +   * This is a deferred task writer that is used
  1.1714 +   * to batch operations done within 50ms of each
  1.1715 +   * other and thus generating only one write to disk
  1.1716 +   */
  1.1717 +  get Writer() {
  1.1718 +    delete this.Writer;
  1.1719 +    this.Writer = new DeferredSave(
  1.1720 +      this.jsonFile.path,
  1.1721 +      () => { return JSON.stringify(this); },
  1.1722 +      DB_BATCH_TIMEOUT_MS
  1.1723 +    );
  1.1724 +    return this.Writer;
  1.1725 +  },
  1.1726 +
  1.1727 +  /**
  1.1728 +   * Flush any pending I/O on the addons.json file
  1.1729 +   * @return: Promise{null}
  1.1730 +   *          Resolves when the pending I/O (writing out or deleting
  1.1731 +   *          addons.json) completes
  1.1732 +   */
  1.1733 +  flush: function() {
  1.1734 +    if (this._deleting) {
  1.1735 +      return this._deleting;
  1.1736 +    }
  1.1737 +    return this.Writer.flush();
  1.1738 +  },
  1.1739 +
  1.1740 +  /**
  1.1741 +   * Asynchronously retrieve all add-ons from the database, and pass it
  1.1742 +   * to the specified callback
  1.1743 +   *
  1.1744 +   * @param  aCallback
  1.1745 +   *         The callback to pass the add-ons back to
  1.1746 +   */
  1.1747 +  retrieveStoredData: function AD_retrieveStoredData(aCallback) {
  1.1748 +    if (!this.initialized)
  1.1749 +      this.openConnection();
  1.1750 +
  1.1751 +    let gatherResults = () => {
  1.1752 +      let result = {};
  1.1753 +      for (let [key, value] of this.DB.addons)
  1.1754 +        result[key] = value;
  1.1755 +
  1.1756 +      executeSoon(function() aCallback(result));
  1.1757 +    };
  1.1758 +
  1.1759 +    if (this._migrationInProgress)
  1.1760 +      this._postMigrationCallback = gatherResults;
  1.1761 +    else
  1.1762 +      gatherResults();
  1.1763 +  },
  1.1764 +
  1.1765 +  /**
  1.1766 +   * Asynchronously repopulates the database so it only contains the
  1.1767 +   * specified add-ons
  1.1768 +   *
  1.1769 +   * @param  aAddons
  1.1770 +   *         The array of add-ons to repopulate the database with
  1.1771 +   * @param  aCallback
  1.1772 +   *         An optional callback to call once complete
  1.1773 +   */
  1.1774 +  repopulate: function AD_repopulate(aAddons, aCallback) {
  1.1775 +    this.DB.addons.clear();
  1.1776 +    this.insertAddons(aAddons, aCallback);
  1.1777 +  },
  1.1778 +
  1.1779 +  /**
  1.1780 +   * Asynchronously inserts an array of add-ons into the database
  1.1781 +   *
  1.1782 +   * @param  aAddons
  1.1783 +   *         The array of add-ons to insert
  1.1784 +   * @param  aCallback
  1.1785 +   *         An optional callback to call once complete
  1.1786 +   */
  1.1787 +  insertAddons: function AD_insertAddons(aAddons, aCallback) {
  1.1788 +    if (!this.initialized)
  1.1789 +      this.openConnection();
  1.1790 +
  1.1791 +    for (let addon of aAddons) {
  1.1792 +      this._insertAddon(addon);
  1.1793 +    }
  1.1794 +
  1.1795 +    this._saveDBToDisk();
  1.1796 +
  1.1797 +    if (aCallback)
  1.1798 +      executeSoon(aCallback);
  1.1799 +  },
  1.1800 +
  1.1801 +  /**
  1.1802 +   * Inserts an individual add-on into the database. If the add-on already
  1.1803 +   * exists in the database (by id), then the specified add-on will not be
  1.1804 +   * inserted.
  1.1805 +   *
  1.1806 +   * @param  aAddon
  1.1807 +   *         The add-on to insert into the database
  1.1808 +   * @param  aCallback
  1.1809 +   *         The callback to call once complete
  1.1810 +   */
  1.1811 +  _insertAddon: function AD__insertAddon(aAddon) {
  1.1812 +    let newAddon = this._parseAddon(aAddon);
  1.1813 +    if (!newAddon ||
  1.1814 +        !newAddon.id ||
  1.1815 +        this.DB.addons.has(newAddon.id))
  1.1816 +      return;
  1.1817 +
  1.1818 +    this.DB.addons.set(newAddon.id, newAddon);
  1.1819 +  },
  1.1820 +
  1.1821 +  /*
  1.1822 +   * Creates an AddonSearchResult by parsing an object structure
  1.1823 +   * retrieved from the DB JSON representation.
  1.1824 +   *
  1.1825 +   * @param  aObj
  1.1826 +   *         The object to parse
  1.1827 +   * @return Returns an AddonSearchResult object.
  1.1828 +   */
  1.1829 +  _parseAddon: function (aObj) {
  1.1830 +    if (aObj instanceof AddonSearchResult)
  1.1831 +      return aObj;
  1.1832 +
  1.1833 +    let id = aObj.id;
  1.1834 +    if (!aObj.id)
  1.1835 +      return null;
  1.1836 +
  1.1837 +    let addon = new AddonSearchResult(id);
  1.1838 +
  1.1839 +    for (let [expectedProperty,] of Iterator(AddonSearchResult.prototype)) {
  1.1840 +      if (!(expectedProperty in aObj) ||
  1.1841 +          typeof(aObj[expectedProperty]) === "function")
  1.1842 +        continue;
  1.1843 +
  1.1844 +      let value = aObj[expectedProperty];
  1.1845 +
  1.1846 +      try {
  1.1847 +        switch (expectedProperty) {
  1.1848 +          case "sourceURI":
  1.1849 +            addon.sourceURI = value ? NetUtil.newURI(value) :  null;
  1.1850 +            break;
  1.1851 +
  1.1852 +          case "creator":
  1.1853 +            addon.creator = value
  1.1854 +                            ? this._makeDeveloper(value)
  1.1855 +                            : null;
  1.1856 +            break;
  1.1857 +
  1.1858 +          case "updateDate":
  1.1859 +            addon.updateDate = value ? new Date(value) : null;
  1.1860 +            break;
  1.1861 +
  1.1862 +          case "developers":
  1.1863 +            if (!addon.developers) addon.developers = [];
  1.1864 +            for (let developer of value) {
  1.1865 +              addon.developers.push(this._makeDeveloper(developer));
  1.1866 +            }
  1.1867 +            break;
  1.1868 +
  1.1869 +          case "screenshots":
  1.1870 +            if (!addon.screenshots) addon.screenshots = [];
  1.1871 +            for (let screenshot of value) {
  1.1872 +              addon.screenshots.push(this._makeScreenshot(screenshot));
  1.1873 +            }
  1.1874 +            break;
  1.1875 +
  1.1876 +          case "compatibilityOverrides":
  1.1877 +            if (!addon.compatibilityOverrides) addon.compatibilityOverrides = [];
  1.1878 +            for (let override of value) {
  1.1879 +              addon.compatibilityOverrides.push(
  1.1880 +                this._makeCompatOverride(override)
  1.1881 +              );
  1.1882 +            }
  1.1883 +            break;
  1.1884 +
  1.1885 +          case "icons":
  1.1886 +            if (!addon.icons) addon.icons = {};
  1.1887 +            for (let [size, url] of Iterator(aObj.icons)) {
  1.1888 +              addon.icons[size] = url;
  1.1889 +            }
  1.1890 +            break;
  1.1891 +
  1.1892 +          case "iconURL":
  1.1893 +            break;
  1.1894 +
  1.1895 +          default:
  1.1896 +            addon[expectedProperty] = value;
  1.1897 +        }
  1.1898 +      } catch (ex) {
  1.1899 +        logger.warn("Error in parsing property value for " + expectedProperty + " | " + ex);
  1.1900 +      }
  1.1901 +
  1.1902 +      // delete property from obj to indicate we've already
  1.1903 +      // handled it. The remaining public properties will
  1.1904 +      // be stored separately and just passed through to
  1.1905 +      // be written back to the DB.
  1.1906 +      delete aObj[expectedProperty];
  1.1907 +    }
  1.1908 +
  1.1909 +    // Copy remaining properties to a separate object
  1.1910 +    // to prevent accidental access on downgraded versions.
  1.1911 +    // The properties will be merged in the same object
  1.1912 +    // prior to being written back through toJSON.
  1.1913 +    for (let remainingProperty of Object.keys(aObj)) {
  1.1914 +      switch (typeof(aObj[remainingProperty])) {
  1.1915 +        case "boolean":
  1.1916 +        case "number":
  1.1917 +        case "string":
  1.1918 +        case "object":
  1.1919 +          // these types are accepted
  1.1920 +          break;
  1.1921 +        default:
  1.1922 +          continue;
  1.1923 +      }
  1.1924 +
  1.1925 +      if (!remainingProperty.startsWith("_"))
  1.1926 +        addon._unsupportedProperties[remainingProperty] =
  1.1927 +          aObj[remainingProperty];
  1.1928 +    }
  1.1929 +
  1.1930 +    return addon;
  1.1931 +  },
  1.1932 +
  1.1933 +  /**
  1.1934 +   * Write the in-memory DB to disk, after waiting for
  1.1935 +   * the DB_BATCH_TIMEOUT_MS timeout.
  1.1936 +   *
  1.1937 +   * @return Promise A promise that resolves after the
  1.1938 +   *                 write to disk has completed.
  1.1939 +   */
  1.1940 +  _saveDBToDisk: function() {
  1.1941 +    return this.Writer.saveChanges().then(
  1.1942 +      null,
  1.1943 +      e => logger.error("SaveDBToDisk failed", e));
  1.1944 +  },
  1.1945 +
  1.1946 +  /**
  1.1947 +   * Make a developer object from a vanilla
  1.1948 +   * JS object from the JSON database
  1.1949 +   *
  1.1950 +   * @param  aObj
  1.1951 +   *         The JS object to use
  1.1952 +   * @return The created developer
  1.1953 +   */
  1.1954 +  _makeDeveloper: function (aObj) {
  1.1955 +    let name = aObj.name;
  1.1956 +    let url = aObj.url;
  1.1957 +    return new AddonManagerPrivate.AddonAuthor(name, url);
  1.1958 +  },
  1.1959 +
  1.1960 +  /**
  1.1961 +   * Make a screenshot object from a vanilla
  1.1962 +   * JS object from the JSON database
  1.1963 +   *
  1.1964 +   * @param  aObj
  1.1965 +   *         The JS object to use
  1.1966 +   * @return The created screenshot
  1.1967 +   */
  1.1968 +  _makeScreenshot: function (aObj) {
  1.1969 +    let url = aObj.url;
  1.1970 +    let width = aObj.width;
  1.1971 +    let height = aObj.height;
  1.1972 +    let thumbnailURL = aObj.thumbnailURL;
  1.1973 +    let thumbnailWidth = aObj.thumbnailWidth;
  1.1974 +    let thumbnailHeight = aObj.thumbnailHeight;
  1.1975 +    let caption = aObj.caption;
  1.1976 +    return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
  1.1977 +                                                   thumbnailWidth, thumbnailHeight, caption);
  1.1978 +  },
  1.1979 +
  1.1980 +  /**
  1.1981 +   * Make a CompatibilityOverride from a vanilla
  1.1982 +   * JS object from the JSON database
  1.1983 +   *
  1.1984 +   * @param  aObj
  1.1985 +   *         The JS object to use
  1.1986 +   * @return The created CompatibilityOverride
  1.1987 +   */
  1.1988 +  _makeCompatOverride: function (aObj) {
  1.1989 +    let type = aObj.type;
  1.1990 +    let minVersion = aObj.minVersion;
  1.1991 +    let maxVersion = aObj.maxVersion;
  1.1992 +    let appID = aObj.appID;
  1.1993 +    let appMinVersion = aObj.appMinVersion;
  1.1994 +    let appMaxVersion = aObj.appMaxVersion;
  1.1995 +    return new AddonManagerPrivate.AddonCompatibilityOverride(type,
  1.1996 +                                                              minVersion,
  1.1997 +                                                              maxVersion,
  1.1998 +                                                              appID,
  1.1999 +                                                              appMinVersion,
  1.2000 +                                                              appMaxVersion);
  1.2001 +  },
  1.2002 +};
  1.2003 +
  1.2004 +function executeSoon(aCallback) {
  1.2005 +  Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
  1.2006 +}

mercurial