michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * This file defines the add-on sync functionality. michael@0: * michael@0: * There are currently a number of known limitations: michael@0: * - We only sync XPI extensions and themes available from addons.mozilla.org. michael@0: * We hope to expand support for other add-ons eventually. michael@0: * - We only attempt syncing of add-ons between applications of the same type. michael@0: * This means add-ons will not synchronize between Firefox desktop and michael@0: * Firefox mobile, for example. This is because of significant add-on michael@0: * incompatibility between application types. michael@0: * michael@0: * Add-on records exist for each known {add-on, app-id} pair in the Sync client michael@0: * set. Each record has a randomly chosen GUID. The records then contain michael@0: * basic metadata about the add-on. michael@0: * michael@0: * We currently synchronize: michael@0: * michael@0: * - Installations michael@0: * - Uninstallations michael@0: * - User enabling and disabling michael@0: * michael@0: * Synchronization is influenced by the following preferences: michael@0: * michael@0: * - services.sync.addons.ignoreRepositoryChecking michael@0: * - services.sync.addons.ignoreUserEnabledChanges michael@0: * - services.sync.addons.trustedSourceHostnames michael@0: * michael@0: * See the documentation in services-sync.js for the behavior of these prefs. michael@0: */ michael@0: "use strict"; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: Cu.import("resource://services-sync/addonutils.js"); michael@0: Cu.import("resource://services-sync/addonsreconciler.js"); michael@0: Cu.import("resource://services-sync/engines.js"); michael@0: Cu.import("resource://services-sync/record.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: Cu.import("resource://services-sync/constants.js"); michael@0: Cu.import("resource://services-common/async.js"); michael@0: michael@0: Cu.import("resource://gre/modules/Preferences.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", michael@0: "resource://gre/modules/AddonManager.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", michael@0: "resource://gre/modules/addons/AddonRepository.jsm"); michael@0: michael@0: this.EXPORTED_SYMBOLS = ["AddonsEngine"]; michael@0: michael@0: // 7 days in milliseconds. michael@0: const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000; michael@0: michael@0: /** michael@0: * AddonRecord represents the state of an add-on in an application. michael@0: * michael@0: * Each add-on has its own record for each application ID it is installed michael@0: * on. michael@0: * michael@0: * The ID of add-on records is a randomly-generated GUID. It is random instead michael@0: * of deterministic so the URIs of the records cannot be guessed and so michael@0: * compromised server credentials won't result in disclosure of the specific michael@0: * add-ons present in a Sync account. michael@0: * michael@0: * The record contains the following fields: michael@0: * michael@0: * addonID michael@0: * ID of the add-on. This correlates to the "id" property on an Addon type. michael@0: * michael@0: * applicationID michael@0: * The application ID this record is associated with. michael@0: * michael@0: * enabled michael@0: * Boolean stating whether add-on is enabled or disabled by the user. michael@0: * michael@0: * source michael@0: * String indicating where an add-on is from. Currently, we only support michael@0: * the value "amo" which indicates that the add-on came from the official michael@0: * add-ons repository, addons.mozilla.org. In the future, we may support michael@0: * installing add-ons from other sources. This provides a future-compatible michael@0: * mechanism for clients to only apply records they know how to handle. michael@0: */ michael@0: function AddonRecord(collection, id) { michael@0: CryptoWrapper.call(this, collection, id); michael@0: } michael@0: AddonRecord.prototype = { michael@0: __proto__: CryptoWrapper.prototype, michael@0: _logName: "Record.Addon" michael@0: }; michael@0: michael@0: Utils.deferGetSet(AddonRecord, "cleartext", ["addonID", michael@0: "applicationID", michael@0: "enabled", michael@0: "source"]); michael@0: michael@0: /** michael@0: * The AddonsEngine handles synchronization of add-ons between clients. michael@0: * michael@0: * The engine maintains an instance of an AddonsReconciler, which is the entity michael@0: * maintaining state for add-ons. It provides the history and tracking APIs michael@0: * that AddonManager doesn't. michael@0: * michael@0: * The engine instance overrides a handful of functions on the base class. The michael@0: * rationale for each is documented by that function. michael@0: */ michael@0: this.AddonsEngine = function AddonsEngine(service) { michael@0: SyncEngine.call(this, "Addons", service); michael@0: michael@0: this._reconciler = new AddonsReconciler(); michael@0: } michael@0: AddonsEngine.prototype = { michael@0: __proto__: SyncEngine.prototype, michael@0: _storeObj: AddonsStore, michael@0: _trackerObj: AddonsTracker, michael@0: _recordObj: AddonRecord, michael@0: version: 1, michael@0: michael@0: _reconciler: null, michael@0: michael@0: /** michael@0: * Override parent method to find add-ons by their public ID, not Sync GUID. michael@0: */ michael@0: _findDupe: function _findDupe(item) { michael@0: let id = item.addonID; michael@0: michael@0: // The reconciler should have been updated at the top of the sync, so we michael@0: // can assume it is up to date when this function is called. michael@0: let addons = this._reconciler.addons; michael@0: if (!(id in addons)) { michael@0: return null; michael@0: } michael@0: michael@0: let addon = addons[id]; michael@0: if (addon.guid != item.id) { michael@0: return addon.guid; michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Override getChangedIDs to pull in tracker changes plus changes from the michael@0: * reconciler log. michael@0: */ michael@0: getChangedIDs: function getChangedIDs() { michael@0: let changes = {}; michael@0: for (let [id, modified] in Iterator(this._tracker.changedIDs)) { michael@0: changes[id] = modified; michael@0: } michael@0: michael@0: let lastSyncDate = new Date(this.lastSync * 1000); michael@0: michael@0: // The reconciler should have been refreshed at the beginning of a sync and michael@0: // we assume this function is only called from within a sync. michael@0: let reconcilerChanges = this._reconciler.getChangesSinceDate(lastSyncDate); michael@0: let addons = this._reconciler.addons; michael@0: for each (let change in reconcilerChanges) { michael@0: let changeTime = change[0]; michael@0: let id = change[2]; michael@0: michael@0: if (!(id in addons)) { michael@0: continue; michael@0: } michael@0: michael@0: // Keep newest modified time. michael@0: if (id in changes && changeTime < changes[id]) { michael@0: continue; michael@0: } michael@0: michael@0: if (!this._store.isAddonSyncable(addons[id])) { michael@0: continue; michael@0: } michael@0: michael@0: this._log.debug("Adding changed add-on from changes log: " + id); michael@0: let addon = addons[id]; michael@0: changes[addon.guid] = changeTime.getTime() / 1000; michael@0: } michael@0: michael@0: return changes; michael@0: }, michael@0: michael@0: /** michael@0: * Override start of sync function to refresh reconciler. michael@0: * michael@0: * Many functions in this class assume the reconciler is refreshed at the michael@0: * top of a sync. If this ever changes, those functions should be revisited. michael@0: * michael@0: * Technically speaking, we don't need to refresh the reconciler on every michael@0: * sync since it is installed as an AddonManager listener. However, add-ons michael@0: * are complicated and we force a full refresh, just in case the listeners michael@0: * missed something. michael@0: */ michael@0: _syncStartup: function _syncStartup() { michael@0: // We refresh state before calling parent because syncStartup in the parent michael@0: // looks for changed IDs, which is dependent on add-on state being up to michael@0: // date. michael@0: this._refreshReconcilerState(); michael@0: michael@0: SyncEngine.prototype._syncStartup.call(this); michael@0: }, michael@0: michael@0: /** michael@0: * Override end of sync to perform a little housekeeping on the reconciler. michael@0: * michael@0: * We prune changes to prevent the reconciler state from growing without michael@0: * bound. Even if it grows unbounded, there would have to be many add-on michael@0: * changes (thousands) for it to slow things down significantly. This is michael@0: * highly unlikely to occur. Still, we exercise defense just in case. michael@0: */ michael@0: _syncCleanup: function _syncCleanup() { michael@0: let ms = 1000 * this.lastSync - PRUNE_ADDON_CHANGES_THRESHOLD; michael@0: this._reconciler.pruneChangesBeforeDate(new Date(ms)); michael@0: michael@0: SyncEngine.prototype._syncCleanup.call(this); michael@0: }, michael@0: michael@0: /** michael@0: * Helper function to ensure reconciler is up to date. michael@0: * michael@0: * This will synchronously load the reconciler's state from the file michael@0: * system (if needed) and refresh the state of the reconciler. michael@0: */ michael@0: _refreshReconcilerState: function _refreshReconcilerState() { michael@0: this._log.debug("Refreshing reconciler state"); michael@0: let cb = Async.makeSpinningCallback(); michael@0: this._reconciler.refreshGlobalState(cb); michael@0: cb.wait(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * This is the primary interface between Sync and the Addons Manager. michael@0: * michael@0: * In addition to the core store APIs, we provide convenience functions to wrap michael@0: * Add-on Manager APIs with Sync-specific semantics. michael@0: */ michael@0: function AddonsStore(name, engine) { michael@0: Store.call(this, name, engine); michael@0: } michael@0: AddonsStore.prototype = { michael@0: __proto__: Store.prototype, michael@0: michael@0: // Define the add-on types (.type) that we support. michael@0: _syncableTypes: ["extension", "theme"], michael@0: michael@0: _extensionsPrefs: new Preferences("extensions."), michael@0: michael@0: get reconciler() { michael@0: return this.engine._reconciler; michael@0: }, michael@0: michael@0: /** michael@0: * Override applyIncoming to filter out records we can't handle. michael@0: */ michael@0: applyIncoming: function applyIncoming(record) { michael@0: // The fields we look at aren't present when the record is deleted. michael@0: if (!record.deleted) { michael@0: // Ignore records not belonging to our application ID because that is the michael@0: // current policy. michael@0: if (record.applicationID != Services.appinfo.ID) { michael@0: this._log.info("Ignoring incoming record from other App ID: " + michael@0: record.id); michael@0: return; michael@0: } michael@0: michael@0: // Ignore records that aren't from the official add-on repository, as that michael@0: // is our current policy. michael@0: if (record.source != "amo") { michael@0: this._log.info("Ignoring unknown add-on source (" + record.source + ")" + michael@0: " for " + record.id); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: Store.prototype.applyIncoming.call(this, record); michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Provides core Store API to create/install an add-on from a record. michael@0: */ michael@0: create: function create(record) { michael@0: let cb = Async.makeSpinningCallback(); michael@0: AddonUtils.installAddons([{ michael@0: id: record.addonID, michael@0: syncGUID: record.id, michael@0: enabled: record.enabled, michael@0: requireSecureURI: !Svc.Prefs.get("addons.ignoreRepositoryChecking", false), michael@0: }], cb); michael@0: michael@0: // This will throw if there was an error. This will get caught by the sync michael@0: // engine and the record will try to be applied later. michael@0: let results = cb.wait(); michael@0: michael@0: let addon; michael@0: for each (let a in results.addons) { michael@0: if (a.id == record.addonID) { michael@0: addon = a; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // This should never happen, but is present as a fail-safe. michael@0: if (!addon) { michael@0: throw new Error("Add-on not found after install: " + record.addonID); michael@0: } michael@0: michael@0: this._log.info("Add-on installed: " + record.addonID); michael@0: }, michael@0: michael@0: /** michael@0: * Provides core Store API to remove/uninstall an add-on from a record. michael@0: */ michael@0: remove: function remove(record) { michael@0: // If this is called, the payload is empty, so we have to find by GUID. michael@0: let addon = this.getAddonByGUID(record.id); michael@0: if (!addon) { michael@0: // We don't throw because if the add-on could not be found then we assume michael@0: // it has already been uninstalled and there is nothing for this function michael@0: // to do. michael@0: return; michael@0: } michael@0: michael@0: this._log.info("Uninstalling add-on: " + addon.id); michael@0: let cb = Async.makeSpinningCallback(); michael@0: AddonUtils.uninstallAddon(addon, cb); michael@0: cb.wait(); michael@0: }, michael@0: michael@0: /** michael@0: * Provides core Store API to update an add-on from a record. michael@0: */ michael@0: update: function update(record) { michael@0: let addon = this.getAddonByID(record.addonID); michael@0: michael@0: // update() is called if !this.itemExists. And, since itemExists consults michael@0: // the reconciler only, we need to take care of some corner cases. michael@0: // michael@0: // First, the reconciler could know about an add-on that was uninstalled michael@0: // and no longer present in the add-ons manager. michael@0: if (!addon) { michael@0: this.create(record); michael@0: return; michael@0: } michael@0: michael@0: // It's also possible that the add-on is non-restartless and has pending michael@0: // install/uninstall activity. michael@0: // michael@0: // We wouldn't get here if the incoming record was for a deletion. So, michael@0: // check for pending uninstall and cancel if necessary. michael@0: if (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) { michael@0: addon.cancelUninstall(); michael@0: michael@0: // We continue with processing because there could be state or ID change. michael@0: } michael@0: michael@0: let cb = Async.makeSpinningCallback(); michael@0: this.updateUserDisabled(addon, !record.enabled, cb); michael@0: cb.wait(); michael@0: }, michael@0: michael@0: /** michael@0: * Provide core Store API to determine if a record exists. michael@0: */ michael@0: itemExists: function itemExists(guid) { michael@0: let addon = this.reconciler.getAddonStateFromSyncGUID(guid); michael@0: michael@0: return !!addon; michael@0: }, michael@0: michael@0: /** michael@0: * Create an add-on record from its GUID. michael@0: * michael@0: * @param guid michael@0: * Add-on GUID (from extensions DB) michael@0: * @param collection michael@0: * Collection to add record to. michael@0: * michael@0: * @return AddonRecord instance michael@0: */ michael@0: createRecord: function createRecord(guid, collection) { michael@0: let record = new AddonRecord(collection, guid); michael@0: record.applicationID = Services.appinfo.ID; michael@0: michael@0: let addon = this.reconciler.getAddonStateFromSyncGUID(guid); michael@0: michael@0: // If we don't know about this GUID or if it has been uninstalled, we mark michael@0: // the record as deleted. michael@0: if (!addon || !addon.installed) { michael@0: record.deleted = true; michael@0: return record; michael@0: } michael@0: michael@0: record.modified = addon.modified.getTime() / 1000; michael@0: michael@0: record.addonID = addon.id; michael@0: record.enabled = addon.enabled; michael@0: michael@0: // This needs to be dynamic when add-ons don't come from AddonRepository. michael@0: record.source = "amo"; michael@0: michael@0: return record; michael@0: }, michael@0: michael@0: /** michael@0: * Changes the id of an add-on. michael@0: * michael@0: * This implements a core API of the store. michael@0: */ michael@0: changeItemID: function changeItemID(oldID, newID) { michael@0: // We always update the GUID in the reconciler because it will be michael@0: // referenced later in the sync process. michael@0: let state = this.reconciler.getAddonStateFromSyncGUID(oldID); michael@0: if (state) { michael@0: state.guid = newID; michael@0: let cb = Async.makeSpinningCallback(); michael@0: this.reconciler.saveState(null, cb); michael@0: cb.wait(); michael@0: } michael@0: michael@0: let addon = this.getAddonByGUID(oldID); michael@0: if (!addon) { michael@0: this._log.debug("Cannot change item ID (" + oldID + ") in Add-on " + michael@0: "Manager because old add-on not present: " + oldID); michael@0: return; michael@0: } michael@0: michael@0: addon.syncGUID = newID; michael@0: }, michael@0: michael@0: /** michael@0: * Obtain the set of all syncable add-on Sync GUIDs. michael@0: * michael@0: * This implements a core Store API. michael@0: */ michael@0: getAllIDs: function getAllIDs() { michael@0: let ids = {}; michael@0: michael@0: let addons = this.reconciler.addons; michael@0: for each (let addon in addons) { michael@0: if (this.isAddonSyncable(addon)) { michael@0: ids[addon.guid] = true; michael@0: } michael@0: } michael@0: michael@0: return ids; michael@0: }, michael@0: michael@0: /** michael@0: * Wipe engine data. michael@0: * michael@0: * This uninstalls all syncable addons from the application. In case of michael@0: * error, it logs the error and keeps trying with other add-ons. michael@0: */ michael@0: wipe: function wipe() { michael@0: this._log.info("Processing wipe."); michael@0: michael@0: this.engine._refreshReconcilerState(); michael@0: michael@0: // We only wipe syncable add-ons. Wipe is a Sync feature not a security michael@0: // feature. michael@0: for (let guid in this.getAllIDs()) { michael@0: let addon = this.getAddonByGUID(guid); michael@0: if (!addon) { michael@0: this._log.debug("Ignoring add-on because it couldn't be obtained: " + michael@0: guid); michael@0: continue; michael@0: } michael@0: michael@0: this._log.info("Uninstalling add-on as part of wipe: " + addon.id); michael@0: Utils.catch(addon.uninstall)(); michael@0: } michael@0: }, michael@0: michael@0: /*************************************************************************** michael@0: * Functions below are unique to this store and not part of the Store API * michael@0: ***************************************************************************/ michael@0: michael@0: /** michael@0: * Synchronously obtain an add-on from its public ID. michael@0: * michael@0: * @param id michael@0: * Add-on ID michael@0: * @return Addon or undefined if not found michael@0: */ michael@0: getAddonByID: function getAddonByID(id) { michael@0: let cb = Async.makeSyncCallback(); michael@0: AddonManager.getAddonByID(id, cb); michael@0: return Async.waitForSyncCallback(cb); michael@0: }, michael@0: michael@0: /** michael@0: * Synchronously obtain an add-on from its Sync GUID. michael@0: * michael@0: * @param guid michael@0: * Add-on Sync GUID michael@0: * @return DBAddonInternal or null michael@0: */ michael@0: getAddonByGUID: function getAddonByGUID(guid) { michael@0: let cb = Async.makeSyncCallback(); michael@0: AddonManager.getAddonBySyncGUID(guid, cb); michael@0: return Async.waitForSyncCallback(cb); michael@0: }, michael@0: michael@0: /** michael@0: * Determines whether an add-on is suitable for Sync. michael@0: * michael@0: * @param addon michael@0: * Addon instance michael@0: * @return Boolean indicating whether it is appropriate for Sync michael@0: */ michael@0: isAddonSyncable: function isAddonSyncable(addon) { michael@0: // Currently, we limit syncable add-ons to those that are: michael@0: // 1) In a well-defined set of types michael@0: // 2) Installed in the current profile michael@0: // 3) Not installed by a foreign entity (i.e. installed by the app) michael@0: // since they act like global extensions. michael@0: // 4) Is not a hotfix. michael@0: // 5) Are installed from AMO michael@0: michael@0: // We could represent the test as a complex boolean expression. We go the michael@0: // verbose route so the failure reason is logged. michael@0: if (!addon) { michael@0: this._log.debug("Null object passed to isAddonSyncable."); michael@0: return false; michael@0: } michael@0: michael@0: if (this._syncableTypes.indexOf(addon.type) == -1) { michael@0: this._log.debug(addon.id + " not syncable: type not in whitelist: " + michael@0: addon.type); michael@0: return false; michael@0: } michael@0: michael@0: if (!(addon.scope & AddonManager.SCOPE_PROFILE)) { michael@0: this._log.debug(addon.id + " not syncable: not installed in profile."); michael@0: return false; michael@0: } michael@0: michael@0: // This may be too aggressive. If an add-on is downloaded from AMO and michael@0: // manually placed in the profile directory, foreignInstall will be set. michael@0: // Arguably, that add-on should be syncable. michael@0: // TODO Address the edge case and come up with more robust heuristics. michael@0: if (addon.foreignInstall) { michael@0: this._log.debug(addon.id + " not syncable: is foreign install."); michael@0: return false; michael@0: } michael@0: michael@0: // Ignore hotfix extensions (bug 741670). The pref may not be defined. michael@0: if (this._extensionsPrefs.get("hotfix.id", null) == addon.id) { michael@0: this._log.debug(addon.id + " not syncable: is a hotfix."); michael@0: return false; michael@0: } michael@0: michael@0: // We provide a back door to skip the repository checking of an add-on. michael@0: // This is utilized by the tests to make testing easier. Users could enable michael@0: // this, but it would sacrifice security. michael@0: if (Svc.Prefs.get("addons.ignoreRepositoryChecking", false)) { michael@0: return true; michael@0: } michael@0: michael@0: let cb = Async.makeSyncCallback(); michael@0: AddonRepository.getCachedAddonByID(addon.id, cb); michael@0: let result = Async.waitForSyncCallback(cb); michael@0: michael@0: if (!result) { michael@0: this._log.debug(addon.id + " not syncable: add-on not found in add-on " + michael@0: "repository."); michael@0: return false; michael@0: } michael@0: michael@0: return this.isSourceURITrusted(result.sourceURI); michael@0: }, michael@0: michael@0: /** michael@0: * Determine whether an add-on's sourceURI field is trusted and the add-on michael@0: * can be installed. michael@0: * michael@0: * This function should only ever be called from isAddonSyncable(). It is michael@0: * exposed as a separate function to make testing easier. michael@0: * michael@0: * @param uri michael@0: * nsIURI instance to validate michael@0: * @return bool michael@0: */ michael@0: isSourceURITrusted: function isSourceURITrusted(uri) { michael@0: // For security reasons, we currently limit synced add-ons to those michael@0: // installed from trusted hostname(s). We additionally require TLS with michael@0: // the add-ons site to help prevent forgeries. michael@0: let trustedHostnames = Svc.Prefs.get("addons.trustedSourceHostnames", "") michael@0: .split(","); michael@0: michael@0: if (!uri) { michael@0: this._log.debug("Undefined argument to isSourceURITrusted()."); michael@0: return false; michael@0: } michael@0: michael@0: // Scheme is validated before the hostname because uri.host may not be michael@0: // populated for certain schemes. It appears to always be populated for michael@0: // https, so we avoid the potential NS_ERROR_FAILURE on field access. michael@0: if (uri.scheme != "https") { michael@0: this._log.debug("Source URI not HTTPS: " + uri.spec); michael@0: return false; michael@0: } michael@0: michael@0: if (trustedHostnames.indexOf(uri.host) == -1) { michael@0: this._log.debug("Source hostname not trusted: " + uri.host); michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Update the userDisabled flag on an add-on. michael@0: * michael@0: * This will enable or disable an add-on and call the supplied callback when michael@0: * the action is complete. If no action is needed, the callback gets called michael@0: * immediately. michael@0: * michael@0: * @param addon michael@0: * Addon instance to manipulate. michael@0: * @param value michael@0: * Boolean to which to set userDisabled on the passed Addon. michael@0: * @param callback michael@0: * Function to be called when action is complete. Will receive 2 michael@0: * arguments, a truthy value that signifies error, and the Addon michael@0: * instance passed to this function. michael@0: */ michael@0: updateUserDisabled: function updateUserDisabled(addon, value, callback) { michael@0: if (addon.userDisabled == value) { michael@0: callback(null, addon); michael@0: return; michael@0: } michael@0: michael@0: // A pref allows changes to the enabled flag to be ignored. michael@0: if (Svc.Prefs.get("addons.ignoreUserEnabledChanges", false)) { michael@0: this._log.info("Ignoring enabled state change due to preference: " + michael@0: addon.id); michael@0: callback(null, addon); michael@0: return; michael@0: } michael@0: michael@0: AddonUtils.updateUserDisabled(addon, value, callback); michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * The add-ons tracker keeps track of real-time changes to add-ons. michael@0: * michael@0: * It hooks up to the reconciler and receives notifications directly from it. michael@0: */ michael@0: function AddonsTracker(name, engine) { michael@0: Tracker.call(this, name, engine); michael@0: } michael@0: AddonsTracker.prototype = { michael@0: __proto__: Tracker.prototype, michael@0: michael@0: get reconciler() { michael@0: return this.engine._reconciler; michael@0: }, michael@0: michael@0: get store() { michael@0: return this.engine._store; michael@0: }, michael@0: michael@0: /** michael@0: * This callback is executed whenever the AddonsReconciler sends out a change michael@0: * notification. See AddonsReconciler.addChangeListener(). michael@0: */ michael@0: changeListener: function changeHandler(date, change, addon) { michael@0: this._log.debug("changeListener invoked: " + change + " " + addon.id); michael@0: // Ignore changes that occur during sync. michael@0: if (this.ignoreAll) { michael@0: return; michael@0: } michael@0: michael@0: if (!this.store.isAddonSyncable(addon)) { michael@0: this._log.debug("Ignoring change because add-on isn't syncable: " + michael@0: addon.id); michael@0: return; michael@0: } michael@0: michael@0: this.addChangedID(addon.guid, date.getTime() / 1000); michael@0: this.score += SCORE_INCREMENT_XLARGE; michael@0: }, michael@0: michael@0: startTracking: function() { michael@0: if (this.engine.enabled) { michael@0: this.reconciler.startListening(); michael@0: } michael@0: michael@0: this.reconciler.addChangeListener(this); michael@0: }, michael@0: michael@0: stopTracking: function() { michael@0: this.reconciler.removeChangeListener(this); michael@0: this.reconciler.stopListening(); michael@0: }, michael@0: };