services/sync/modules/engines/addons.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/sync/modules/engines/addons.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,703 @@
     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 + * This file defines the add-on sync functionality.
    1.10 + *
    1.11 + * There are currently a number of known limitations:
    1.12 + *  - We only sync XPI extensions and themes available from addons.mozilla.org.
    1.13 + *    We hope to expand support for other add-ons eventually.
    1.14 + *  - We only attempt syncing of add-ons between applications of the same type.
    1.15 + *    This means add-ons will not synchronize between Firefox desktop and
    1.16 + *    Firefox mobile, for example. This is because of significant add-on
    1.17 + *    incompatibility between application types.
    1.18 + *
    1.19 + * Add-on records exist for each known {add-on, app-id} pair in the Sync client
    1.20 + * set. Each record has a randomly chosen GUID. The records then contain
    1.21 + * basic metadata about the add-on.
    1.22 + *
    1.23 + * We currently synchronize:
    1.24 + *
    1.25 + *  - Installations
    1.26 + *  - Uninstallations
    1.27 + *  - User enabling and disabling
    1.28 + *
    1.29 + * Synchronization is influenced by the following preferences:
    1.30 + *
    1.31 + *  - services.sync.addons.ignoreRepositoryChecking
    1.32 + *  - services.sync.addons.ignoreUserEnabledChanges
    1.33 + *  - services.sync.addons.trustedSourceHostnames
    1.34 + *
    1.35 + * See the documentation in services-sync.js for the behavior of these prefs.
    1.36 + */
    1.37 +"use strict";
    1.38 +
    1.39 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    1.40 +
    1.41 +Cu.import("resource://services-sync/addonutils.js");
    1.42 +Cu.import("resource://services-sync/addonsreconciler.js");
    1.43 +Cu.import("resource://services-sync/engines.js");
    1.44 +Cu.import("resource://services-sync/record.js");
    1.45 +Cu.import("resource://services-sync/util.js");
    1.46 +Cu.import("resource://services-sync/constants.js");
    1.47 +Cu.import("resource://services-common/async.js");
    1.48 +
    1.49 +Cu.import("resource://gre/modules/Preferences.jsm");
    1.50 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.51 +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
    1.52 +                                  "resource://gre/modules/AddonManager.jsm");
    1.53 +XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
    1.54 +                                  "resource://gre/modules/addons/AddonRepository.jsm");
    1.55 +
    1.56 +this.EXPORTED_SYMBOLS = ["AddonsEngine"];
    1.57 +
    1.58 +// 7 days in milliseconds.
    1.59 +const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
    1.60 +
    1.61 +/**
    1.62 + * AddonRecord represents the state of an add-on in an application.
    1.63 + *
    1.64 + * Each add-on has its own record for each application ID it is installed
    1.65 + * on.
    1.66 + *
    1.67 + * The ID of add-on records is a randomly-generated GUID. It is random instead
    1.68 + * of deterministic so the URIs of the records cannot be guessed and so
    1.69 + * compromised server credentials won't result in disclosure of the specific
    1.70 + * add-ons present in a Sync account.
    1.71 + *
    1.72 + * The record contains the following fields:
    1.73 + *
    1.74 + *  addonID
    1.75 + *    ID of the add-on. This correlates to the "id" property on an Addon type.
    1.76 + *
    1.77 + *  applicationID
    1.78 + *    The application ID this record is associated with.
    1.79 + *
    1.80 + *  enabled
    1.81 + *    Boolean stating whether add-on is enabled or disabled by the user.
    1.82 + *
    1.83 + *  source
    1.84 + *    String indicating where an add-on is from. Currently, we only support
    1.85 + *    the value "amo" which indicates that the add-on came from the official
    1.86 + *    add-ons repository, addons.mozilla.org. In the future, we may support
    1.87 + *    installing add-ons from other sources. This provides a future-compatible
    1.88 + *    mechanism for clients to only apply records they know how to handle.
    1.89 + */
    1.90 +function AddonRecord(collection, id) {
    1.91 +  CryptoWrapper.call(this, collection, id);
    1.92 +}
    1.93 +AddonRecord.prototype = {
    1.94 +  __proto__: CryptoWrapper.prototype,
    1.95 +  _logName: "Record.Addon"
    1.96 +};
    1.97 +
    1.98 +Utils.deferGetSet(AddonRecord, "cleartext", ["addonID",
    1.99 +                                             "applicationID",
   1.100 +                                             "enabled",
   1.101 +                                             "source"]);
   1.102 +
   1.103 +/**
   1.104 + * The AddonsEngine handles synchronization of add-ons between clients.
   1.105 + *
   1.106 + * The engine maintains an instance of an AddonsReconciler, which is the entity
   1.107 + * maintaining state for add-ons. It provides the history and tracking APIs
   1.108 + * that AddonManager doesn't.
   1.109 + *
   1.110 + * The engine instance overrides a handful of functions on the base class. The
   1.111 + * rationale for each is documented by that function.
   1.112 + */
   1.113 +this.AddonsEngine = function AddonsEngine(service) {
   1.114 +  SyncEngine.call(this, "Addons", service);
   1.115 +
   1.116 +  this._reconciler = new AddonsReconciler();
   1.117 +}
   1.118 +AddonsEngine.prototype = {
   1.119 +  __proto__:              SyncEngine.prototype,
   1.120 +  _storeObj:              AddonsStore,
   1.121 +  _trackerObj:            AddonsTracker,
   1.122 +  _recordObj:             AddonRecord,
   1.123 +  version:                1,
   1.124 +
   1.125 +  _reconciler:            null,
   1.126 +
   1.127 +  /**
   1.128 +   * Override parent method to find add-ons by their public ID, not Sync GUID.
   1.129 +   */
   1.130 +  _findDupe: function _findDupe(item) {
   1.131 +    let id = item.addonID;
   1.132 +
   1.133 +    // The reconciler should have been updated at the top of the sync, so we
   1.134 +    // can assume it is up to date when this function is called.
   1.135 +    let addons = this._reconciler.addons;
   1.136 +    if (!(id in addons)) {
   1.137 +      return null;
   1.138 +    }
   1.139 +
   1.140 +    let addon = addons[id];
   1.141 +    if (addon.guid != item.id) {
   1.142 +      return addon.guid;
   1.143 +    }
   1.144 +
   1.145 +    return null;
   1.146 +  },
   1.147 +
   1.148 +  /**
   1.149 +   * Override getChangedIDs to pull in tracker changes plus changes from the
   1.150 +   * reconciler log.
   1.151 +   */
   1.152 +  getChangedIDs: function getChangedIDs() {
   1.153 +    let changes = {};
   1.154 +    for (let [id, modified] in Iterator(this._tracker.changedIDs)) {
   1.155 +      changes[id] = modified;
   1.156 +    }
   1.157 +
   1.158 +    let lastSyncDate = new Date(this.lastSync * 1000);
   1.159 +
   1.160 +    // The reconciler should have been refreshed at the beginning of a sync and
   1.161 +    // we assume this function is only called from within a sync.
   1.162 +    let reconcilerChanges = this._reconciler.getChangesSinceDate(lastSyncDate);
   1.163 +    let addons = this._reconciler.addons;
   1.164 +    for each (let change in reconcilerChanges) {
   1.165 +      let changeTime = change[0];
   1.166 +      let id = change[2];
   1.167 +
   1.168 +      if (!(id in addons)) {
   1.169 +        continue;
   1.170 +      }
   1.171 +
   1.172 +      // Keep newest modified time.
   1.173 +      if (id in changes && changeTime < changes[id]) {
   1.174 +          continue;
   1.175 +      }
   1.176 +
   1.177 +      if (!this._store.isAddonSyncable(addons[id])) {
   1.178 +        continue;
   1.179 +      }
   1.180 +
   1.181 +      this._log.debug("Adding changed add-on from changes log: " + id);
   1.182 +      let addon = addons[id];
   1.183 +      changes[addon.guid] = changeTime.getTime() / 1000;
   1.184 +    }
   1.185 +
   1.186 +    return changes;
   1.187 +  },
   1.188 +
   1.189 +  /**
   1.190 +   * Override start of sync function to refresh reconciler.
   1.191 +   *
   1.192 +   * Many functions in this class assume the reconciler is refreshed at the
   1.193 +   * top of a sync. If this ever changes, those functions should be revisited.
   1.194 +   *
   1.195 +   * Technically speaking, we don't need to refresh the reconciler on every
   1.196 +   * sync since it is installed as an AddonManager listener. However, add-ons
   1.197 +   * are complicated and we force a full refresh, just in case the listeners
   1.198 +   * missed something.
   1.199 +   */
   1.200 +  _syncStartup: function _syncStartup() {
   1.201 +    // We refresh state before calling parent because syncStartup in the parent
   1.202 +    // looks for changed IDs, which is dependent on add-on state being up to
   1.203 +    // date.
   1.204 +    this._refreshReconcilerState();
   1.205 +
   1.206 +    SyncEngine.prototype._syncStartup.call(this);
   1.207 +  },
   1.208 +
   1.209 +  /**
   1.210 +   * Override end of sync to perform a little housekeeping on the reconciler.
   1.211 +   *
   1.212 +   * We prune changes to prevent the reconciler state from growing without
   1.213 +   * bound. Even if it grows unbounded, there would have to be many add-on
   1.214 +   * changes (thousands) for it to slow things down significantly. This is
   1.215 +   * highly unlikely to occur. Still, we exercise defense just in case.
   1.216 +   */
   1.217 +  _syncCleanup: function _syncCleanup() {
   1.218 +    let ms = 1000 * this.lastSync - PRUNE_ADDON_CHANGES_THRESHOLD;
   1.219 +    this._reconciler.pruneChangesBeforeDate(new Date(ms));
   1.220 +
   1.221 +    SyncEngine.prototype._syncCleanup.call(this);
   1.222 +  },
   1.223 +
   1.224 +  /**
   1.225 +   * Helper function to ensure reconciler is up to date.
   1.226 +   *
   1.227 +   * This will synchronously load the reconciler's state from the file
   1.228 +   * system (if needed) and refresh the state of the reconciler.
   1.229 +   */
   1.230 +  _refreshReconcilerState: function _refreshReconcilerState() {
   1.231 +    this._log.debug("Refreshing reconciler state");
   1.232 +    let cb = Async.makeSpinningCallback();
   1.233 +    this._reconciler.refreshGlobalState(cb);
   1.234 +    cb.wait();
   1.235 +  }
   1.236 +};
   1.237 +
   1.238 +/**
   1.239 + * This is the primary interface between Sync and the Addons Manager.
   1.240 + *
   1.241 + * In addition to the core store APIs, we provide convenience functions to wrap
   1.242 + * Add-on Manager APIs with Sync-specific semantics.
   1.243 + */
   1.244 +function AddonsStore(name, engine) {
   1.245 +  Store.call(this, name, engine);
   1.246 +}
   1.247 +AddonsStore.prototype = {
   1.248 +  __proto__: Store.prototype,
   1.249 +
   1.250 +  // Define the add-on types (.type) that we support.
   1.251 +  _syncableTypes: ["extension", "theme"],
   1.252 +
   1.253 +  _extensionsPrefs: new Preferences("extensions."),
   1.254 +
   1.255 +  get reconciler() {
   1.256 +    return this.engine._reconciler;
   1.257 +  },
   1.258 +
   1.259 +  /**
   1.260 +   * Override applyIncoming to filter out records we can't handle.
   1.261 +   */
   1.262 +  applyIncoming: function applyIncoming(record) {
   1.263 +    // The fields we look at aren't present when the record is deleted.
   1.264 +    if (!record.deleted) {
   1.265 +      // Ignore records not belonging to our application ID because that is the
   1.266 +      // current policy.
   1.267 +      if (record.applicationID != Services.appinfo.ID) {
   1.268 +        this._log.info("Ignoring incoming record from other App ID: " +
   1.269 +                        record.id);
   1.270 +        return;
   1.271 +      }
   1.272 +
   1.273 +      // Ignore records that aren't from the official add-on repository, as that
   1.274 +      // is our current policy.
   1.275 +      if (record.source != "amo") {
   1.276 +        this._log.info("Ignoring unknown add-on source (" + record.source + ")" +
   1.277 +                       " for " + record.id);
   1.278 +        return;
   1.279 +      }
   1.280 +    }
   1.281 +
   1.282 +    Store.prototype.applyIncoming.call(this, record);
   1.283 +  },
   1.284 +
   1.285 +
   1.286 +  /**
   1.287 +   * Provides core Store API to create/install an add-on from a record.
   1.288 +   */
   1.289 +  create: function create(record) {
   1.290 +    let cb = Async.makeSpinningCallback();
   1.291 +    AddonUtils.installAddons([{
   1.292 +      id:               record.addonID,
   1.293 +      syncGUID:         record.id,
   1.294 +      enabled:          record.enabled,
   1.295 +      requireSecureURI: !Svc.Prefs.get("addons.ignoreRepositoryChecking", false),
   1.296 +    }], cb);
   1.297 +
   1.298 +    // This will throw if there was an error. This will get caught by the sync
   1.299 +    // engine and the record will try to be applied later.
   1.300 +    let results = cb.wait();
   1.301 +
   1.302 +    let addon;
   1.303 +    for each (let a in results.addons) {
   1.304 +      if (a.id == record.addonID) {
   1.305 +        addon = a;
   1.306 +        break;
   1.307 +      }
   1.308 +    }
   1.309 +
   1.310 +    // This should never happen, but is present as a fail-safe.
   1.311 +    if (!addon) {
   1.312 +      throw new Error("Add-on not found after install: " + record.addonID);
   1.313 +    }
   1.314 +
   1.315 +    this._log.info("Add-on installed: " + record.addonID);
   1.316 +  },
   1.317 +
   1.318 +  /**
   1.319 +   * Provides core Store API to remove/uninstall an add-on from a record.
   1.320 +   */
   1.321 +  remove: function remove(record) {
   1.322 +    // If this is called, the payload is empty, so we have to find by GUID.
   1.323 +    let addon = this.getAddonByGUID(record.id);
   1.324 +    if (!addon) {
   1.325 +      // We don't throw because if the add-on could not be found then we assume
   1.326 +      // it has already been uninstalled and there is nothing for this function
   1.327 +      // to do.
   1.328 +      return;
   1.329 +    }
   1.330 +
   1.331 +    this._log.info("Uninstalling add-on: " + addon.id);
   1.332 +    let cb = Async.makeSpinningCallback();
   1.333 +    AddonUtils.uninstallAddon(addon, cb);
   1.334 +    cb.wait();
   1.335 +  },
   1.336 +
   1.337 +  /**
   1.338 +   * Provides core Store API to update an add-on from a record.
   1.339 +   */
   1.340 +  update: function update(record) {
   1.341 +    let addon = this.getAddonByID(record.addonID);
   1.342 +
   1.343 +    // update() is called if !this.itemExists. And, since itemExists consults
   1.344 +    // the reconciler only, we need to take care of some corner cases.
   1.345 +    //
   1.346 +    // First, the reconciler could know about an add-on that was uninstalled
   1.347 +    // and no longer present in the add-ons manager.
   1.348 +    if (!addon) {
   1.349 +      this.create(record);
   1.350 +      return;
   1.351 +    }
   1.352 +
   1.353 +    // It's also possible that the add-on is non-restartless and has pending
   1.354 +    // install/uninstall activity.
   1.355 +    //
   1.356 +    // We wouldn't get here if the incoming record was for a deletion. So,
   1.357 +    // check for pending uninstall and cancel if necessary.
   1.358 +    if (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) {
   1.359 +      addon.cancelUninstall();
   1.360 +
   1.361 +      // We continue with processing because there could be state or ID change.
   1.362 +    }
   1.363 +
   1.364 +    let cb = Async.makeSpinningCallback();
   1.365 +    this.updateUserDisabled(addon, !record.enabled, cb);
   1.366 +    cb.wait();
   1.367 +  },
   1.368 +
   1.369 +  /**
   1.370 +   * Provide core Store API to determine if a record exists.
   1.371 +   */
   1.372 +  itemExists: function itemExists(guid) {
   1.373 +    let addon = this.reconciler.getAddonStateFromSyncGUID(guid);
   1.374 +
   1.375 +    return !!addon;
   1.376 +  },
   1.377 +
   1.378 +  /**
   1.379 +   * Create an add-on record from its GUID.
   1.380 +   *
   1.381 +   * @param guid
   1.382 +   *        Add-on GUID (from extensions DB)
   1.383 +   * @param collection
   1.384 +   *        Collection to add record to.
   1.385 +   *
   1.386 +   * @return AddonRecord instance
   1.387 +   */
   1.388 +  createRecord: function createRecord(guid, collection) {
   1.389 +    let record = new AddonRecord(collection, guid);
   1.390 +    record.applicationID = Services.appinfo.ID;
   1.391 +
   1.392 +    let addon = this.reconciler.getAddonStateFromSyncGUID(guid);
   1.393 +
   1.394 +    // If we don't know about this GUID or if it has been uninstalled, we mark
   1.395 +    // the record as deleted.
   1.396 +    if (!addon || !addon.installed) {
   1.397 +      record.deleted = true;
   1.398 +      return record;
   1.399 +    }
   1.400 +
   1.401 +    record.modified = addon.modified.getTime() / 1000;
   1.402 +
   1.403 +    record.addonID = addon.id;
   1.404 +    record.enabled = addon.enabled;
   1.405 +
   1.406 +    // This needs to be dynamic when add-ons don't come from AddonRepository.
   1.407 +    record.source = "amo";
   1.408 +
   1.409 +    return record;
   1.410 +  },
   1.411 +
   1.412 +  /**
   1.413 +   * Changes the id of an add-on.
   1.414 +   *
   1.415 +   * This implements a core API of the store.
   1.416 +   */
   1.417 +  changeItemID: function changeItemID(oldID, newID) {
   1.418 +    // We always update the GUID in the reconciler because it will be
   1.419 +    // referenced later in the sync process.
   1.420 +    let state = this.reconciler.getAddonStateFromSyncGUID(oldID);
   1.421 +    if (state) {
   1.422 +      state.guid = newID;
   1.423 +      let cb = Async.makeSpinningCallback();
   1.424 +      this.reconciler.saveState(null, cb);
   1.425 +      cb.wait();
   1.426 +    }
   1.427 +
   1.428 +    let addon = this.getAddonByGUID(oldID);
   1.429 +    if (!addon) {
   1.430 +      this._log.debug("Cannot change item ID (" + oldID + ") in Add-on " +
   1.431 +                      "Manager because old add-on not present: " + oldID);
   1.432 +      return;
   1.433 +    }
   1.434 +
   1.435 +    addon.syncGUID = newID;
   1.436 +  },
   1.437 +
   1.438 +  /**
   1.439 +   * Obtain the set of all syncable add-on Sync GUIDs.
   1.440 +   *
   1.441 +   * This implements a core Store API.
   1.442 +   */
   1.443 +  getAllIDs: function getAllIDs() {
   1.444 +    let ids = {};
   1.445 +
   1.446 +    let addons = this.reconciler.addons;
   1.447 +    for each (let addon in addons) {
   1.448 +      if (this.isAddonSyncable(addon)) {
   1.449 +        ids[addon.guid] = true;
   1.450 +      }
   1.451 +    }
   1.452 +
   1.453 +    return ids;
   1.454 +  },
   1.455 +
   1.456 +  /**
   1.457 +   * Wipe engine data.
   1.458 +   *
   1.459 +   * This uninstalls all syncable addons from the application. In case of
   1.460 +   * error, it logs the error and keeps trying with other add-ons.
   1.461 +   */
   1.462 +  wipe: function wipe() {
   1.463 +    this._log.info("Processing wipe.");
   1.464 +
   1.465 +    this.engine._refreshReconcilerState();
   1.466 +
   1.467 +    // We only wipe syncable add-ons. Wipe is a Sync feature not a security
   1.468 +    // feature.
   1.469 +    for (let guid in this.getAllIDs()) {
   1.470 +      let addon = this.getAddonByGUID(guid);
   1.471 +      if (!addon) {
   1.472 +        this._log.debug("Ignoring add-on because it couldn't be obtained: " +
   1.473 +                        guid);
   1.474 +        continue;
   1.475 +      }
   1.476 +
   1.477 +      this._log.info("Uninstalling add-on as part of wipe: " + addon.id);
   1.478 +      Utils.catch(addon.uninstall)();
   1.479 +    }
   1.480 +  },
   1.481 +
   1.482 +  /***************************************************************************
   1.483 +   * Functions below are unique to this store and not part of the Store API  *
   1.484 +   ***************************************************************************/
   1.485 +
   1.486 +  /**
   1.487 +   * Synchronously obtain an add-on from its public ID.
   1.488 +   *
   1.489 +   * @param id
   1.490 +   *        Add-on ID
   1.491 +   * @return Addon or undefined if not found
   1.492 +   */
   1.493 +  getAddonByID: function getAddonByID(id) {
   1.494 +    let cb = Async.makeSyncCallback();
   1.495 +    AddonManager.getAddonByID(id, cb);
   1.496 +    return Async.waitForSyncCallback(cb);
   1.497 +  },
   1.498 +
   1.499 +  /**
   1.500 +   * Synchronously obtain an add-on from its Sync GUID.
   1.501 +   *
   1.502 +   * @param  guid
   1.503 +   *         Add-on Sync GUID
   1.504 +   * @return DBAddonInternal or null
   1.505 +   */
   1.506 +  getAddonByGUID: function getAddonByGUID(guid) {
   1.507 +    let cb = Async.makeSyncCallback();
   1.508 +    AddonManager.getAddonBySyncGUID(guid, cb);
   1.509 +    return Async.waitForSyncCallback(cb);
   1.510 +  },
   1.511 +
   1.512 +  /**
   1.513 +   * Determines whether an add-on is suitable for Sync.
   1.514 +   *
   1.515 +   * @param  addon
   1.516 +   *         Addon instance
   1.517 +   * @return Boolean indicating whether it is appropriate for Sync
   1.518 +   */
   1.519 +  isAddonSyncable: function isAddonSyncable(addon) {
   1.520 +    // Currently, we limit syncable add-ons to those that are:
   1.521 +    //   1) In a well-defined set of types
   1.522 +    //   2) Installed in the current profile
   1.523 +    //   3) Not installed by a foreign entity (i.e. installed by the app)
   1.524 +    //      since they act like global extensions.
   1.525 +    //   4) Is not a hotfix.
   1.526 +    //   5) Are installed from AMO
   1.527 +
   1.528 +    // We could represent the test as a complex boolean expression. We go the
   1.529 +    // verbose route so the failure reason is logged.
   1.530 +    if (!addon) {
   1.531 +      this._log.debug("Null object passed to isAddonSyncable.");
   1.532 +      return false;
   1.533 +    }
   1.534 +
   1.535 +    if (this._syncableTypes.indexOf(addon.type) == -1) {
   1.536 +      this._log.debug(addon.id + " not syncable: type not in whitelist: " +
   1.537 +                      addon.type);
   1.538 +      return false;
   1.539 +    }
   1.540 +
   1.541 +    if (!(addon.scope & AddonManager.SCOPE_PROFILE)) {
   1.542 +      this._log.debug(addon.id + " not syncable: not installed in profile.");
   1.543 +      return false;
   1.544 +    }
   1.545 +
   1.546 +    // This may be too aggressive. If an add-on is downloaded from AMO and
   1.547 +    // manually placed in the profile directory, foreignInstall will be set.
   1.548 +    // Arguably, that add-on should be syncable.
   1.549 +    // TODO Address the edge case and come up with more robust heuristics.
   1.550 +    if (addon.foreignInstall) {
   1.551 +      this._log.debug(addon.id + " not syncable: is foreign install.");
   1.552 +      return false;
   1.553 +    }
   1.554 +
   1.555 +    // Ignore hotfix extensions (bug 741670). The pref may not be defined.
   1.556 +    if (this._extensionsPrefs.get("hotfix.id", null) == addon.id) {
   1.557 +      this._log.debug(addon.id + " not syncable: is a hotfix.");
   1.558 +      return false;
   1.559 +    }
   1.560 +
   1.561 +    // We provide a back door to skip the repository checking of an add-on.
   1.562 +    // This is utilized by the tests to make testing easier. Users could enable
   1.563 +    // this, but it would sacrifice security.
   1.564 +    if (Svc.Prefs.get("addons.ignoreRepositoryChecking", false)) {
   1.565 +      return true;
   1.566 +    }
   1.567 +
   1.568 +    let cb = Async.makeSyncCallback();
   1.569 +    AddonRepository.getCachedAddonByID(addon.id, cb);
   1.570 +    let result = Async.waitForSyncCallback(cb);
   1.571 +
   1.572 +    if (!result) {
   1.573 +      this._log.debug(addon.id + " not syncable: add-on not found in add-on " +
   1.574 +                      "repository.");
   1.575 +      return false;
   1.576 +    }
   1.577 +
   1.578 +    return this.isSourceURITrusted(result.sourceURI);
   1.579 +  },
   1.580 +
   1.581 +  /**
   1.582 +   * Determine whether an add-on's sourceURI field is trusted and the add-on
   1.583 +   * can be installed.
   1.584 +   *
   1.585 +   * This function should only ever be called from isAddonSyncable(). It is
   1.586 +   * exposed as a separate function to make testing easier.
   1.587 +   *
   1.588 +   * @param  uri
   1.589 +   *         nsIURI instance to validate
   1.590 +   * @return bool
   1.591 +   */
   1.592 +  isSourceURITrusted: function isSourceURITrusted(uri) {
   1.593 +    // For security reasons, we currently limit synced add-ons to those
   1.594 +    // installed from trusted hostname(s). We additionally require TLS with
   1.595 +    // the add-ons site to help prevent forgeries.
   1.596 +    let trustedHostnames = Svc.Prefs.get("addons.trustedSourceHostnames", "")
   1.597 +                           .split(",");
   1.598 +
   1.599 +    if (!uri) {
   1.600 +      this._log.debug("Undefined argument to isSourceURITrusted().");
   1.601 +      return false;
   1.602 +    }
   1.603 +
   1.604 +    // Scheme is validated before the hostname because uri.host may not be
   1.605 +    // populated for certain schemes. It appears to always be populated for
   1.606 +    // https, so we avoid the potential NS_ERROR_FAILURE on field access.
   1.607 +    if (uri.scheme != "https") {
   1.608 +      this._log.debug("Source URI not HTTPS: " + uri.spec);
   1.609 +      return false;
   1.610 +    }
   1.611 +
   1.612 +    if (trustedHostnames.indexOf(uri.host) == -1) {
   1.613 +      this._log.debug("Source hostname not trusted: " + uri.host);
   1.614 +      return false;
   1.615 +    }
   1.616 +
   1.617 +    return true;
   1.618 +  },
   1.619 +
   1.620 +  /**
   1.621 +   * Update the userDisabled flag on an add-on.
   1.622 +   *
   1.623 +   * This will enable or disable an add-on and call the supplied callback when
   1.624 +   * the action is complete. If no action is needed, the callback gets called
   1.625 +   * immediately.
   1.626 +   *
   1.627 +   * @param addon
   1.628 +   *        Addon instance to manipulate.
   1.629 +   * @param value
   1.630 +   *        Boolean to which to set userDisabled on the passed Addon.
   1.631 +   * @param callback
   1.632 +   *        Function to be called when action is complete. Will receive 2
   1.633 +   *        arguments, a truthy value that signifies error, and the Addon
   1.634 +   *        instance passed to this function.
   1.635 +   */
   1.636 +  updateUserDisabled: function updateUserDisabled(addon, value, callback) {
   1.637 +    if (addon.userDisabled == value) {
   1.638 +      callback(null, addon);
   1.639 +      return;
   1.640 +    }
   1.641 +
   1.642 +    // A pref allows changes to the enabled flag to be ignored.
   1.643 +    if (Svc.Prefs.get("addons.ignoreUserEnabledChanges", false)) {
   1.644 +      this._log.info("Ignoring enabled state change due to preference: " +
   1.645 +                     addon.id);
   1.646 +      callback(null, addon);
   1.647 +      return;
   1.648 +    }
   1.649 +
   1.650 +    AddonUtils.updateUserDisabled(addon, value, callback);
   1.651 +  },
   1.652 +};
   1.653 +
   1.654 +/**
   1.655 + * The add-ons tracker keeps track of real-time changes to add-ons.
   1.656 + *
   1.657 + * It hooks up to the reconciler and receives notifications directly from it.
   1.658 + */
   1.659 +function AddonsTracker(name, engine) {
   1.660 +  Tracker.call(this, name, engine);
   1.661 +}
   1.662 +AddonsTracker.prototype = {
   1.663 +  __proto__: Tracker.prototype,
   1.664 +
   1.665 +  get reconciler() {
   1.666 +    return this.engine._reconciler;
   1.667 +  },
   1.668 +
   1.669 +  get store() {
   1.670 +    return this.engine._store;
   1.671 +  },
   1.672 +
   1.673 +  /**
   1.674 +   * This callback is executed whenever the AddonsReconciler sends out a change
   1.675 +   * notification. See AddonsReconciler.addChangeListener().
   1.676 +   */
   1.677 +  changeListener: function changeHandler(date, change, addon) {
   1.678 +    this._log.debug("changeListener invoked: " + change + " " + addon.id);
   1.679 +    // Ignore changes that occur during sync.
   1.680 +    if (this.ignoreAll) {
   1.681 +      return;
   1.682 +    }
   1.683 +
   1.684 +    if (!this.store.isAddonSyncable(addon)) {
   1.685 +      this._log.debug("Ignoring change because add-on isn't syncable: " +
   1.686 +                      addon.id);
   1.687 +      return;
   1.688 +    }
   1.689 +
   1.690 +    this.addChangedID(addon.guid, date.getTime() / 1000);
   1.691 +    this.score += SCORE_INCREMENT_XLARGE;
   1.692 +  },
   1.693 +
   1.694 +  startTracking: function() {
   1.695 +    if (this.engine.enabled) {
   1.696 +      this.reconciler.startListening();
   1.697 +    }
   1.698 +
   1.699 +    this.reconciler.addChangeListener(this);
   1.700 +  },
   1.701 +
   1.702 +  stopTracking: function() {
   1.703 +    this.reconciler.removeChangeListener(this);
   1.704 +    this.reconciler.stopListening();
   1.705 +  },
   1.706 +};

mercurial