818 lines
25 KiB
JavaScript
818 lines
25 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/*
|
|
* This file defines the add-on sync functionality.
|
|
*
|
|
* There are currently a number of known limitations:
|
|
* - We only sync XPI extensions and themes available from addons.mozilla.org.
|
|
* We hope to expand support for other add-ons eventually.
|
|
* - We only attempt syncing of add-ons between applications of the same type.
|
|
* This means add-ons will not synchronize between IceCat desktop and
|
|
* IceCat mobile, for example. This is because of significant add-on
|
|
* incompatibility between application types.
|
|
*
|
|
* Add-on records exist for each known {add-on, app-id} pair in the Sync client
|
|
* set. Each record has a randomly chosen GUID. The records then contain
|
|
* basic metadata about the add-on.
|
|
*
|
|
* We currently synchronize:
|
|
*
|
|
* - Installations
|
|
* - Uninstallations
|
|
* - User enabling and disabling
|
|
*
|
|
* Synchronization is influenced by the following preferences:
|
|
*
|
|
* - services.sync.addons.ignoreUserEnabledChanges
|
|
* - services.sync.addons.trustedSourceHostnames
|
|
*
|
|
* and also influenced by whether addons have repository caching enabled and
|
|
* whether they allow installation of addons from insecure options (both of
|
|
* which are themselves influenced by the "extensions." pref branch)
|
|
*
|
|
* See the documentation in all.js for the behavior of these prefs.
|
|
*/
|
|
|
|
import { AddonUtils } from "resource://services-sync/addonutils.sys.mjs";
|
|
import { AddonsReconciler } from "resource://services-sync/addonsreconciler.sys.mjs";
|
|
import {
|
|
Store,
|
|
SyncEngine,
|
|
LegacyTracker,
|
|
} from "resource://services-sync/engines.sys.mjs";
|
|
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
|
|
import { Svc, Utils } from "resource://services-sync/util.sys.mjs";
|
|
|
|
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
|
|
import { CollectionValidator } from "resource://services-sync/collection_validator.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
|
|
});
|
|
|
|
// 7 days in milliseconds.
|
|
const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
|
|
|
|
/**
|
|
* AddonRecord represents the state of an add-on in an application.
|
|
*
|
|
* Each add-on has its own record for each application ID it is installed
|
|
* on.
|
|
*
|
|
* The ID of add-on records is a randomly-generated GUID. It is random instead
|
|
* of deterministic so the URIs of the records cannot be guessed and so
|
|
* compromised server credentials won't result in disclosure of the specific
|
|
* add-ons present in a Sync account.
|
|
*
|
|
* The record contains the following fields:
|
|
*
|
|
* addonID
|
|
* ID of the add-on. This correlates to the "id" property on an Addon type.
|
|
*
|
|
* applicationID
|
|
* The application ID this record is associated with.
|
|
*
|
|
* enabled
|
|
* Boolean stating whether add-on is enabled or disabled by the user.
|
|
*
|
|
* source
|
|
* String indicating where an add-on is from. Currently, we only support
|
|
* the value "amo" which indicates that the add-on came from the official
|
|
* add-ons repository, addons.mozilla.org. In the future, we may support
|
|
* installing add-ons from other sources. This provides a future-compatible
|
|
* mechanism for clients to only apply records they know how to handle.
|
|
*/
|
|
function AddonRecord(collection, id) {
|
|
CryptoWrapper.call(this, collection, id);
|
|
}
|
|
AddonRecord.prototype = {
|
|
_logName: "Record.Addon",
|
|
};
|
|
Object.setPrototypeOf(AddonRecord.prototype, CryptoWrapper.prototype);
|
|
|
|
Utils.deferGetSet(AddonRecord, "cleartext", [
|
|
"addonID",
|
|
"applicationID",
|
|
"enabled",
|
|
"source",
|
|
]);
|
|
|
|
/**
|
|
* The AddonsEngine handles synchronization of add-ons between clients.
|
|
*
|
|
* The engine maintains an instance of an AddonsReconciler, which is the entity
|
|
* maintaining state for add-ons. It provides the history and tracking APIs
|
|
* that AddonManager doesn't.
|
|
*
|
|
* The engine instance overrides a handful of functions on the base class. The
|
|
* rationale for each is documented by that function.
|
|
*/
|
|
export function AddonsEngine(service) {
|
|
SyncEngine.call(this, "Addons", service);
|
|
|
|
this._reconciler = new AddonsReconciler(this._tracker.asyncObserver);
|
|
}
|
|
|
|
AddonsEngine.prototype = {
|
|
_storeObj: AddonsStore,
|
|
_trackerObj: AddonsTracker,
|
|
_recordObj: AddonRecord,
|
|
version: 1,
|
|
|
|
syncPriority: 5,
|
|
|
|
_reconciler: null,
|
|
|
|
async initialize() {
|
|
await SyncEngine.prototype.initialize.call(this);
|
|
await this._reconciler.ensureStateLoaded();
|
|
},
|
|
|
|
/**
|
|
* Override parent method to find add-ons by their public ID, not Sync GUID.
|
|
*/
|
|
async _findDupe(item) {
|
|
let id = item.addonID;
|
|
|
|
// The reconciler should have been updated at the top of the sync, so we
|
|
// can assume it is up to date when this function is called.
|
|
let addons = this._reconciler.addons;
|
|
if (!(id in addons)) {
|
|
return null;
|
|
}
|
|
|
|
let addon = addons[id];
|
|
if (addon.guid != item.id) {
|
|
return addon.guid;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Override getChangedIDs to pull in tracker changes plus changes from the
|
|
* reconciler log.
|
|
*/
|
|
async getChangedIDs() {
|
|
let changes = {};
|
|
const changedIDs = await this._tracker.getChangedIDs();
|
|
for (let [id, modified] of Object.entries(changedIDs)) {
|
|
changes[id] = modified;
|
|
}
|
|
|
|
let lastSync = await this.getLastSync();
|
|
let lastSyncDate = new Date(lastSync * 1000);
|
|
|
|
// The reconciler should have been refreshed at the beginning of a sync and
|
|
// we assume this function is only called from within a sync.
|
|
let reconcilerChanges = this._reconciler.getChangesSinceDate(lastSyncDate);
|
|
let addons = this._reconciler.addons;
|
|
for (let change of reconcilerChanges) {
|
|
let changeTime = change[0];
|
|
let id = change[2];
|
|
|
|
if (!(id in addons)) {
|
|
continue;
|
|
}
|
|
|
|
// Keep newest modified time.
|
|
if (id in changes && changeTime < changes[id]) {
|
|
continue;
|
|
}
|
|
|
|
if (!(await this.isAddonSyncable(addons[id]))) {
|
|
continue;
|
|
}
|
|
|
|
this._log.debug("Adding changed add-on from changes log: " + id);
|
|
let addon = addons[id];
|
|
changes[addon.guid] = changeTime.getTime() / 1000;
|
|
}
|
|
|
|
return changes;
|
|
},
|
|
|
|
/**
|
|
* Override start of sync function to refresh reconciler.
|
|
*
|
|
* Many functions in this class assume the reconciler is refreshed at the
|
|
* top of a sync. If this ever changes, those functions should be revisited.
|
|
*
|
|
* Technically speaking, we don't need to refresh the reconciler on every
|
|
* sync since it is installed as an AddonManager listener. However, add-ons
|
|
* are complicated and we force a full refresh, just in case the listeners
|
|
* missed something.
|
|
*/
|
|
async _syncStartup() {
|
|
// We refresh state before calling parent because syncStartup in the parent
|
|
// looks for changed IDs, which is dependent on add-on state being up to
|
|
// date.
|
|
await this._refreshReconcilerState();
|
|
return SyncEngine.prototype._syncStartup.call(this);
|
|
},
|
|
|
|
/**
|
|
* Override end of sync to perform a little housekeeping on the reconciler.
|
|
*
|
|
* We prune changes to prevent the reconciler state from growing without
|
|
* bound. Even if it grows unbounded, there would have to be many add-on
|
|
* changes (thousands) for it to slow things down significantly. This is
|
|
* highly unlikely to occur. Still, we exercise defense just in case.
|
|
*/
|
|
async _syncCleanup() {
|
|
let lastSync = await this.getLastSync();
|
|
let ms = 1000 * lastSync - PRUNE_ADDON_CHANGES_THRESHOLD;
|
|
this._reconciler.pruneChangesBeforeDate(new Date(ms));
|
|
return SyncEngine.prototype._syncCleanup.call(this);
|
|
},
|
|
|
|
/**
|
|
* Helper function to ensure reconciler is up to date.
|
|
*
|
|
* This will load the reconciler's state from the file
|
|
* system (if needed) and refresh the state of the reconciler.
|
|
*/
|
|
async _refreshReconcilerState() {
|
|
this._log.debug("Refreshing reconciler state");
|
|
return this._reconciler.refreshGlobalState();
|
|
},
|
|
|
|
// Returns a promise
|
|
isAddonSyncable(addon, ignoreRepoCheck) {
|
|
return this._store.isAddonSyncable(addon, ignoreRepoCheck);
|
|
},
|
|
};
|
|
Object.setPrototypeOf(AddonsEngine.prototype, SyncEngine.prototype);
|
|
|
|
/**
|
|
* This is the primary interface between Sync and the Addons Manager.
|
|
*
|
|
* In addition to the core store APIs, we provide convenience functions to wrap
|
|
* Add-on Manager APIs with Sync-specific semantics.
|
|
*/
|
|
function AddonsStore(name, engine) {
|
|
Store.call(this, name, engine);
|
|
}
|
|
AddonsStore.prototype = {
|
|
// Define the add-on types (.type) that we support.
|
|
_syncableTypes: ["extension", "theme"],
|
|
|
|
_extensionsPrefs: Services.prefs.getBranch("extensions."),
|
|
|
|
get reconciler() {
|
|
return this.engine._reconciler;
|
|
},
|
|
|
|
/**
|
|
* Override applyIncoming to filter out records we can't handle.
|
|
*/
|
|
async applyIncoming(record) {
|
|
// The fields we look at aren't present when the record is deleted.
|
|
if (!record.deleted) {
|
|
// Ignore records not belonging to our application ID because that is the
|
|
// current policy.
|
|
if (record.applicationID != Services.appinfo.ID) {
|
|
this._log.info(
|
|
"Ignoring incoming record from other App ID: " + record.id
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Ignore records that aren't from the official add-on repository, as that
|
|
// is our current policy.
|
|
if (record.source != "amo") {
|
|
this._log.info(
|
|
"Ignoring unknown add-on source (" +
|
|
record.source +
|
|
")" +
|
|
" for " +
|
|
record.id
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Ignore incoming records for which an existing non-syncable addon
|
|
// exists. Note that we do not insist that the addon manager already have
|
|
// metadata for this addon - it's possible our reconciler previously saw the
|
|
// addon but the addon-manager cache no longer has it - which is fine for a
|
|
// new incoming addon.
|
|
// (Note that most other cases where the addon-manager cache is invalid
|
|
// doesn't get this treatment because that cache self-repairs after some
|
|
// time - but it only re-populates addons which are currently installed.)
|
|
let existingMeta = this.reconciler.addons[record.addonID];
|
|
if (
|
|
existingMeta &&
|
|
!(await this.isAddonSyncable(existingMeta, /* ignoreRepoCheck */ true))
|
|
) {
|
|
this._log.info(
|
|
"Ignoring incoming record for an existing but non-syncable addon",
|
|
record.addonID
|
|
);
|
|
return;
|
|
}
|
|
|
|
await Store.prototype.applyIncoming.call(this, record);
|
|
},
|
|
|
|
/**
|
|
* Provides core Store API to create/install an add-on from a record.
|
|
*/
|
|
async create(record) {
|
|
// This will throw if there was an error. This will get caught by the sync
|
|
// engine and the record will try to be applied later.
|
|
const results = await AddonUtils.installAddons([
|
|
{
|
|
id: record.addonID,
|
|
syncGUID: record.id,
|
|
enabled: record.enabled,
|
|
requireSecureURI: this._extensionsPrefs.getBoolPref(
|
|
"install.requireSecureOrigin",
|
|
true
|
|
),
|
|
},
|
|
]);
|
|
|
|
if (results.skipped.includes(record.addonID)) {
|
|
this._log.info("Add-on skipped: " + record.addonID);
|
|
// Just early-return for skipped addons - we don't want to arrange to
|
|
// try again next time because the condition that caused up to skip
|
|
// will remain true for this addon forever.
|
|
return;
|
|
}
|
|
|
|
let addon;
|
|
for (let a of results.addons) {
|
|
if (a.id == record.addonID) {
|
|
addon = a;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// This should never happen, but is present as a fail-safe.
|
|
if (!addon) {
|
|
throw new Error("Add-on not found after install: " + record.addonID);
|
|
}
|
|
|
|
this._log.info("Add-on installed: " + record.addonID);
|
|
},
|
|
|
|
/**
|
|
* Provides core Store API to remove/uninstall an add-on from a record.
|
|
*/
|
|
async remove(record) {
|
|
// If this is called, the payload is empty, so we have to find by GUID.
|
|
let addon = await this.getAddonByGUID(record.id);
|
|
if (!addon) {
|
|
// We don't throw because if the add-on could not be found then we assume
|
|
// it has already been uninstalled and there is nothing for this function
|
|
// to do.
|
|
return;
|
|
}
|
|
|
|
this._log.info("Uninstalling add-on: " + addon.id);
|
|
await AddonUtils.uninstallAddon(addon);
|
|
},
|
|
|
|
/**
|
|
* Provides core Store API to update an add-on from a record.
|
|
*/
|
|
async update(record) {
|
|
let addon = await this.getAddonByID(record.addonID);
|
|
|
|
// update() is called if !this.itemExists. And, since itemExists consults
|
|
// the reconciler only, we need to take care of some corner cases.
|
|
//
|
|
// First, the reconciler could know about an add-on that was uninstalled
|
|
// and no longer present in the add-ons manager.
|
|
if (!addon) {
|
|
await this.create(record);
|
|
return;
|
|
}
|
|
|
|
// It's also possible that the add-on is non-restartless and has pending
|
|
// install/uninstall activity.
|
|
//
|
|
// We wouldn't get here if the incoming record was for a deletion. So,
|
|
// check for pending uninstall and cancel if necessary.
|
|
if (addon.pendingOperations & lazy.AddonManager.PENDING_UNINSTALL) {
|
|
addon.cancelUninstall();
|
|
|
|
// We continue with processing because there could be state or ID change.
|
|
}
|
|
|
|
await this.updateUserDisabled(addon, !record.enabled);
|
|
},
|
|
|
|
/**
|
|
* Provide core Store API to determine if a record exists.
|
|
*/
|
|
async itemExists(guid) {
|
|
let addon = this.reconciler.getAddonStateFromSyncGUID(guid);
|
|
|
|
return !!addon;
|
|
},
|
|
|
|
/**
|
|
* Create an add-on record from its GUID.
|
|
*
|
|
* @param guid
|
|
* Add-on GUID (from extensions DB)
|
|
* @param collection
|
|
* Collection to add record to.
|
|
*
|
|
* @return AddonRecord instance
|
|
*/
|
|
async createRecord(guid, collection) {
|
|
let record = new AddonRecord(collection, guid);
|
|
record.applicationID = Services.appinfo.ID;
|
|
|
|
let addon = this.reconciler.getAddonStateFromSyncGUID(guid);
|
|
|
|
// If we don't know about this GUID or if it has been uninstalled, we mark
|
|
// the record as deleted.
|
|
if (!addon || !addon.installed) {
|
|
record.deleted = true;
|
|
return record;
|
|
}
|
|
|
|
record.modified = addon.modified.getTime() / 1000;
|
|
|
|
record.addonID = addon.id;
|
|
record.enabled = addon.enabled;
|
|
|
|
// This needs to be dynamic when add-ons don't come from AddonRepository.
|
|
record.source = "amo";
|
|
|
|
return record;
|
|
},
|
|
|
|
/**
|
|
* Changes the id of an add-on.
|
|
*
|
|
* This implements a core API of the store.
|
|
*/
|
|
async changeItemID(oldID, newID) {
|
|
// We always update the GUID in the reconciler because it will be
|
|
// referenced later in the sync process.
|
|
let state = this.reconciler.getAddonStateFromSyncGUID(oldID);
|
|
if (state) {
|
|
state.guid = newID;
|
|
await this.reconciler.saveState();
|
|
}
|
|
|
|
let addon = await this.getAddonByGUID(oldID);
|
|
if (!addon) {
|
|
this._log.debug(
|
|
"Cannot change item ID (" +
|
|
oldID +
|
|
") in Add-on " +
|
|
"Manager because old add-on not present: " +
|
|
oldID
|
|
);
|
|
return;
|
|
}
|
|
|
|
addon.syncGUID = newID;
|
|
},
|
|
|
|
/**
|
|
* Obtain the set of all syncable add-on Sync GUIDs.
|
|
*
|
|
* This implements a core Store API.
|
|
*/
|
|
async getAllIDs() {
|
|
let ids = {};
|
|
|
|
let addons = this.reconciler.addons;
|
|
for (let id in addons) {
|
|
let addon = addons[id];
|
|
if (await this.isAddonSyncable(addon)) {
|
|
ids[addon.guid] = true;
|
|
}
|
|
}
|
|
|
|
return ids;
|
|
},
|
|
|
|
/**
|
|
* Wipe engine data.
|
|
*
|
|
* This uninstalls all syncable addons from the application. In case of
|
|
* error, it logs the error and keeps trying with other add-ons.
|
|
*/
|
|
async wipe() {
|
|
this._log.info("Processing wipe.");
|
|
|
|
await this.engine._refreshReconcilerState();
|
|
|
|
// We only wipe syncable add-ons. Wipe is a Sync feature not a security
|
|
// feature.
|
|
let ids = await this.getAllIDs();
|
|
for (let guid in ids) {
|
|
let addon = await this.getAddonByGUID(guid);
|
|
if (!addon) {
|
|
this._log.debug(
|
|
"Ignoring add-on because it couldn't be obtained: " + guid
|
|
);
|
|
continue;
|
|
}
|
|
|
|
this._log.info("Uninstalling add-on as part of wipe: " + addon.id);
|
|
await Utils.catch.call(this, () => addon.uninstall())();
|
|
}
|
|
},
|
|
|
|
/** *************************************************************************
|
|
* Functions below are unique to this store and not part of the Store API *
|
|
***************************************************************************/
|
|
|
|
/**
|
|
* Obtain an add-on from its public ID.
|
|
*
|
|
* @param id
|
|
* Add-on ID
|
|
* @return Addon or undefined if not found
|
|
*/
|
|
async getAddonByID(id) {
|
|
return lazy.AddonManager.getAddonByID(id);
|
|
},
|
|
|
|
/**
|
|
* Obtain an add-on from its Sync GUID.
|
|
*
|
|
* @param guid
|
|
* Add-on Sync GUID
|
|
* @return DBAddonInternal or null
|
|
*/
|
|
async getAddonByGUID(guid) {
|
|
return lazy.AddonManager.getAddonBySyncGUID(guid);
|
|
},
|
|
|
|
/**
|
|
* Determines whether an add-on is suitable for Sync.
|
|
*
|
|
* @param addon
|
|
* Addon instance
|
|
* @param ignoreRepoCheck
|
|
* Should we skip checking the Addons repository (primarially useful
|
|
* for testing and validation).
|
|
* @return Boolean indicating whether it is appropriate for Sync
|
|
*/
|
|
async isAddonSyncable(addon, ignoreRepoCheck = false) {
|
|
// Currently, we limit syncable add-ons to those that are:
|
|
// 1) In a well-defined set of types
|
|
// 2) Installed in the current profile
|
|
// 3) Not installed by a foreign entity (i.e. installed by the app)
|
|
// since they act like global extensions.
|
|
// 4) Is not a hotfix.
|
|
// 5) The addons XPIProvider doesn't veto it (i.e not being installed in
|
|
// the profile directory, or any other reasons it says the addon can't
|
|
// be synced)
|
|
// 6) Are installed from AMO
|
|
|
|
// We could represent the test as a complex boolean expression. We go the
|
|
// verbose route so the failure reason is logged.
|
|
if (!addon) {
|
|
this._log.debug("Null object passed to isAddonSyncable.");
|
|
return false;
|
|
}
|
|
|
|
if (!this._syncableTypes.includes(addon.type)) {
|
|
this._log.debug(
|
|
addon.id + " not syncable: type not in allowed list: " + addon.type
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (!(addon.scope & lazy.AddonManager.SCOPE_PROFILE)) {
|
|
this._log.debug(addon.id + " not syncable: not installed in profile.");
|
|
return false;
|
|
}
|
|
|
|
// If the addon manager says it's not syncable, we skip it.
|
|
if (!addon.isSyncable) {
|
|
this._log.debug(addon.id + " not syncable: vetoed by the addon manager.");
|
|
return false;
|
|
}
|
|
|
|
// This may be too aggressive. If an add-on is downloaded from AMO and
|
|
// manually placed in the profile directory, foreignInstall will be set.
|
|
// Arguably, that add-on should be syncable.
|
|
// TODO Address the edge case and come up with more robust heuristics.
|
|
if (addon.foreignInstall) {
|
|
this._log.debug(addon.id + " not syncable: is foreign install.");
|
|
return false;
|
|
}
|
|
|
|
// If the AddonRepository's cache isn't enabled (which it typically isn't
|
|
// in tests), getCachedAddonByID always returns null - so skip the check
|
|
// in that case. We also provide a way to specifically opt-out of the check
|
|
// even if the cache is enabled, which is used by the validators.
|
|
if (ignoreRepoCheck || !lazy.AddonRepository.cacheEnabled) {
|
|
return true;
|
|
}
|
|
|
|
let result = await new Promise(res => {
|
|
lazy.AddonRepository.getCachedAddonByID(addon.id, res);
|
|
});
|
|
|
|
if (!result) {
|
|
this._log.debug(
|
|
addon.id + " not syncable: add-on not found in add-on repository."
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return this.isSourceURITrusted(result.sourceURI);
|
|
},
|
|
|
|
/**
|
|
* Determine whether an add-on's sourceURI field is trusted and the add-on
|
|
* can be installed.
|
|
*
|
|
* This function should only ever be called from isAddonSyncable(). It is
|
|
* exposed as a separate function to make testing easier.
|
|
*
|
|
* @param uri
|
|
* nsIURI instance to validate
|
|
* @return bool
|
|
*/
|
|
isSourceURITrusted: function isSourceURITrusted(uri) {
|
|
// For security reasons, we currently limit synced add-ons to those
|
|
// installed from trusted hostname(s). We additionally require TLS with
|
|
// the add-ons site to help prevent forgeries.
|
|
let trustedHostnames = Svc.PrefBranch.getStringPref(
|
|
"addons.trustedSourceHostnames",
|
|
""
|
|
).split(",");
|
|
|
|
if (!uri) {
|
|
this._log.debug("Undefined argument to isSourceURITrusted().");
|
|
return false;
|
|
}
|
|
|
|
// Scheme is validated before the hostname because uri.host may not be
|
|
// populated for certain schemes. It appears to always be populated for
|
|
// https, so we avoid the potential NS_ERROR_FAILURE on field access.
|
|
if (uri.scheme != "https") {
|
|
this._log.debug("Source URI not HTTPS: " + uri.spec);
|
|
return false;
|
|
}
|
|
|
|
if (!trustedHostnames.includes(uri.host)) {
|
|
this._log.debug("Source hostname not trusted: " + uri.host);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Update the userDisabled flag on an add-on.
|
|
*
|
|
* This will enable or disable an add-on. It has no return value and does
|
|
* not catch or handle exceptions thrown by the addon manager. If no action
|
|
* is needed it will return immediately.
|
|
*
|
|
* @param addon
|
|
* Addon instance to manipulate.
|
|
* @param value
|
|
* Boolean to which to set userDisabled on the passed Addon.
|
|
*/
|
|
async updateUserDisabled(addon, value) {
|
|
if (addon.userDisabled == value) {
|
|
return;
|
|
}
|
|
|
|
// A pref allows changes to the enabled flag to be ignored.
|
|
if (Svc.PrefBranch.getBoolPref("addons.ignoreUserEnabledChanges", false)) {
|
|
this._log.info(
|
|
"Ignoring enabled state change due to preference: " + addon.id
|
|
);
|
|
return;
|
|
}
|
|
|
|
AddonUtils.updateUserDisabled(addon, value);
|
|
// updating this flag doesn't send a notification for appDisabled addons,
|
|
// meaning the reconciler will not update its state and may resync the
|
|
// addon - so explicitly rectify the state (bug 1366994)
|
|
if (addon.appDisabled) {
|
|
await this.reconciler.rectifyStateFromAddon(addon);
|
|
}
|
|
},
|
|
};
|
|
|
|
Object.setPrototypeOf(AddonsStore.prototype, Store.prototype);
|
|
|
|
/**
|
|
* The add-ons tracker keeps track of real-time changes to add-ons.
|
|
*
|
|
* It hooks up to the reconciler and receives notifications directly from it.
|
|
*/
|
|
function AddonsTracker(name, engine) {
|
|
LegacyTracker.call(this, name, engine);
|
|
}
|
|
AddonsTracker.prototype = {
|
|
get reconciler() {
|
|
return this.engine._reconciler;
|
|
},
|
|
|
|
get store() {
|
|
return this.engine._store;
|
|
},
|
|
|
|
/**
|
|
* This callback is executed whenever the AddonsReconciler sends out a change
|
|
* notification. See AddonsReconciler.addChangeListener().
|
|
*/
|
|
async changeListener(date, change, addon) {
|
|
this._log.debug("changeListener invoked: " + change + " " + addon.id);
|
|
// Ignore changes that occur during sync.
|
|
if (this.ignoreAll) {
|
|
return;
|
|
}
|
|
|
|
if (!(await this.store.isAddonSyncable(addon))) {
|
|
this._log.debug(
|
|
"Ignoring change because add-on isn't syncable: " + addon.id
|
|
);
|
|
return;
|
|
}
|
|
|
|
const added = await this.addChangedID(addon.guid, date.getTime() / 1000);
|
|
if (added) {
|
|
this.score += SCORE_INCREMENT_XLARGE;
|
|
}
|
|
},
|
|
|
|
onStart() {
|
|
this.reconciler.startListening();
|
|
this.reconciler.addChangeListener(this);
|
|
},
|
|
|
|
onStop() {
|
|
this.reconciler.removeChangeListener(this);
|
|
this.reconciler.stopListening();
|
|
},
|
|
};
|
|
|
|
Object.setPrototypeOf(AddonsTracker.prototype, LegacyTracker.prototype);
|
|
|
|
export class AddonValidator extends CollectionValidator {
|
|
constructor(engine = null) {
|
|
super("addons", "id", ["addonID", "enabled", "applicationID", "source"]);
|
|
this.engine = engine;
|
|
}
|
|
|
|
async getClientItems() {
|
|
return lazy.AddonManager.getAllAddons();
|
|
}
|
|
|
|
normalizeClientItem(item) {
|
|
let enabled = !item.userDisabled;
|
|
if (item.pendingOperations & lazy.AddonManager.PENDING_ENABLE) {
|
|
enabled = true;
|
|
} else if (item.pendingOperations & lazy.AddonManager.PENDING_DISABLE) {
|
|
enabled = false;
|
|
}
|
|
return {
|
|
enabled,
|
|
id: item.syncGUID,
|
|
addonID: item.id,
|
|
applicationID: Services.appinfo.ID,
|
|
source: "amo", // check item.foreignInstall?
|
|
original: item,
|
|
};
|
|
}
|
|
|
|
async normalizeServerItem(item) {
|
|
let guid = await this.engine._findDupe(item);
|
|
if (guid) {
|
|
item.id = guid;
|
|
}
|
|
return item;
|
|
}
|
|
|
|
clientUnderstands(item) {
|
|
return item.applicationID === Services.appinfo.ID;
|
|
}
|
|
|
|
async syncedByClient(item) {
|
|
return (
|
|
!item.original.hidden &&
|
|
!item.original.isSystem &&
|
|
!(
|
|
item.original.pendingOperations & lazy.AddonManager.PENDING_UNINSTALL
|
|
) &&
|
|
// No need to await the returned promise explicitely:
|
|
// |expr1 && expr2| evaluates to expr2 if expr1 is true.
|
|
this.engine.isAddonSyncable(item.original, true)
|
|
);
|
|
}
|
|
}
|