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 +};