addons.sys.mjs (25410B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* 6 * This file defines the add-on sync functionality. 7 * 8 * There are currently a number of known limitations: 9 * - We only sync XPI extensions and themes available from addons.mozilla.org. 10 * We hope to expand support for other add-ons eventually. 11 * - We only attempt syncing of add-ons between applications of the same type. 12 * This means add-ons will not synchronize between Firefox desktop and 13 * Firefox mobile, for example. This is because of significant add-on 14 * incompatibility between application types. 15 * 16 * Add-on records exist for each known {add-on, app-id} pair in the Sync client 17 * set. Each record has a randomly chosen GUID. The records then contain 18 * basic metadata about the add-on. 19 * 20 * We currently synchronize: 21 * 22 * - Installations 23 * - Uninstallations 24 * - User enabling and disabling 25 * 26 * Synchronization is influenced by the following preferences: 27 * 28 * - services.sync.addons.ignoreUserEnabledChanges 29 * - services.sync.addons.trustedSourceHostnames 30 * 31 * and also influenced by whether addons have repository caching enabled and 32 * whether they allow installation of addons from insecure options (both of 33 * which are themselves influenced by the "extensions." pref branch) 34 * 35 * See the documentation in all.js for the behavior of these prefs. 36 */ 37 38 import { AddonUtils } from "resource://services-sync/addonutils.sys.mjs"; 39 import { AddonsReconciler } from "resource://services-sync/addonsreconciler.sys.mjs"; 40 import { 41 Store, 42 SyncEngine, 43 LegacyTracker, 44 } from "resource://services-sync/engines.sys.mjs"; 45 import { CryptoWrapper } from "resource://services-sync/record.sys.mjs"; 46 import { Svc, Utils } from "resource://services-sync/util.sys.mjs"; 47 48 import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs"; 49 import { CollectionValidator } from "resource://services-sync/collection_validator.sys.mjs"; 50 51 const lazy = {}; 52 53 ChromeUtils.defineESModuleGetters(lazy, { 54 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 55 AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", 56 }); 57 58 // 7 days in milliseconds. 59 const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000; 60 61 /** 62 * AddonRecord represents the state of an add-on in an application. 63 * 64 * Each add-on has its own record for each application ID it is installed 65 * on. 66 * 67 * The ID of add-on records is a randomly-generated GUID. It is random instead 68 * of deterministic so the URIs of the records cannot be guessed and so 69 * compromised server credentials won't result in disclosure of the specific 70 * add-ons present in a Sync account. 71 * 72 * The record contains the following fields: 73 * 74 * addonID 75 * ID of the add-on. This correlates to the "id" property on an Addon type. 76 * 77 * applicationID 78 * The application ID this record is associated with. 79 * 80 * enabled 81 * Boolean stating whether add-on is enabled or disabled by the user. 82 * 83 * source 84 * String indicating where an add-on is from. Currently, we only support 85 * the value "amo" which indicates that the add-on came from the official 86 * add-ons repository, addons.mozilla.org. In the future, we may support 87 * installing add-ons from other sources. This provides a future-compatible 88 * mechanism for clients to only apply records they know how to handle. 89 */ 90 function AddonRecord(collection, id) { 91 CryptoWrapper.call(this, collection, id); 92 } 93 AddonRecord.prototype = { 94 _logName: "Record.Addon", 95 }; 96 Object.setPrototypeOf(AddonRecord.prototype, CryptoWrapper.prototype); 97 98 Utils.deferGetSet(AddonRecord, "cleartext", [ 99 "addonID", 100 "applicationID", 101 "enabled", 102 "source", 103 ]); 104 105 /** 106 * The AddonsEngine handles synchronization of add-ons between clients. 107 * 108 * The engine maintains an instance of an AddonsReconciler, which is the entity 109 * maintaining state for add-ons. It provides the history and tracking APIs 110 * that AddonManager doesn't. 111 * 112 * The engine instance overrides a handful of functions on the base class. The 113 * rationale for each is documented by that function. 114 */ 115 export function AddonsEngine(service) { 116 SyncEngine.call(this, "Addons", service); 117 118 this._reconciler = new AddonsReconciler(this._tracker.asyncObserver); 119 } 120 121 AddonsEngine.prototype = { 122 _storeObj: AddonsStore, 123 _trackerObj: AddonsTracker, 124 _recordObj: AddonRecord, 125 version: 1, 126 127 syncPriority: 5, 128 129 _reconciler: null, 130 131 async initialize() { 132 await SyncEngine.prototype.initialize.call(this); 133 await this._reconciler.ensureStateLoaded(); 134 }, 135 136 /** 137 * Override parent method to find add-ons by their public ID, not Sync GUID. 138 */ 139 async _findDupe(item) { 140 let id = item.addonID; 141 142 // The reconciler should have been updated at the top of the sync, so we 143 // can assume it is up to date when this function is called. 144 let addons = this._reconciler.addons; 145 if (!(id in addons)) { 146 return null; 147 } 148 149 let addon = addons[id]; 150 if (addon.guid != item.id) { 151 return addon.guid; 152 } 153 154 return null; 155 }, 156 157 /** 158 * Override getChangedIDs to pull in tracker changes plus changes from the 159 * reconciler log. 160 */ 161 async getChangedIDs() { 162 let changes = {}; 163 const changedIDs = await this._tracker.getChangedIDs(); 164 for (let [id, modified] of Object.entries(changedIDs)) { 165 changes[id] = modified; 166 } 167 168 let lastSync = await this.getLastSync(); 169 let lastSyncDate = new Date(lastSync * 1000); 170 171 // The reconciler should have been refreshed at the beginning of a sync and 172 // we assume this function is only called from within a sync. 173 let reconcilerChanges = this._reconciler.getChangesSinceDate(lastSyncDate); 174 let addons = this._reconciler.addons; 175 for (let change of reconcilerChanges) { 176 let changeTime = change[0]; 177 let id = change[2]; 178 179 if (!(id in addons)) { 180 continue; 181 } 182 183 // Keep newest modified time. 184 if (id in changes && changeTime < changes[id]) { 185 continue; 186 } 187 188 if (!(await this.isAddonSyncable(addons[id]))) { 189 continue; 190 } 191 192 this._log.debug("Adding changed add-on from changes log: " + id); 193 let addon = addons[id]; 194 changes[addon.guid] = changeTime.getTime() / 1000; 195 } 196 197 return changes; 198 }, 199 200 /** 201 * Override start of sync function to refresh reconciler. 202 * 203 * Many functions in this class assume the reconciler is refreshed at the 204 * top of a sync. If this ever changes, those functions should be revisited. 205 * 206 * Technically speaking, we don't need to refresh the reconciler on every 207 * sync since it is installed as an AddonManager listener. However, add-ons 208 * are complicated and we force a full refresh, just in case the listeners 209 * missed something. 210 */ 211 async _syncStartup() { 212 // We refresh state before calling parent because syncStartup in the parent 213 // looks for changed IDs, which is dependent on add-on state being up to 214 // date. 215 await this._refreshReconcilerState(); 216 return SyncEngine.prototype._syncStartup.call(this); 217 }, 218 219 /** 220 * Override end of sync to perform a little housekeeping on the reconciler. 221 * 222 * We prune changes to prevent the reconciler state from growing without 223 * bound. Even if it grows unbounded, there would have to be many add-on 224 * changes (thousands) for it to slow things down significantly. This is 225 * highly unlikely to occur. Still, we exercise defense just in case. 226 */ 227 async _syncCleanup() { 228 let lastSync = await this.getLastSync(); 229 let ms = 1000 * lastSync - PRUNE_ADDON_CHANGES_THRESHOLD; 230 this._reconciler.pruneChangesBeforeDate(new Date(ms)); 231 return SyncEngine.prototype._syncCleanup.call(this); 232 }, 233 234 /** 235 * Helper function to ensure reconciler is up to date. 236 * 237 * This will load the reconciler's state from the file 238 * system (if needed) and refresh the state of the reconciler. 239 */ 240 async _refreshReconcilerState() { 241 this._log.debug("Refreshing reconciler state"); 242 return this._reconciler.refreshGlobalState(); 243 }, 244 245 // Returns a promise 246 isAddonSyncable(addon, ignoreRepoCheck) { 247 return this._store.isAddonSyncable(addon, ignoreRepoCheck); 248 }, 249 }; 250 Object.setPrototypeOf(AddonsEngine.prototype, SyncEngine.prototype); 251 252 /** 253 * This is the primary interface between Sync and the Addons Manager. 254 * 255 * In addition to the core store APIs, we provide convenience functions to wrap 256 * Add-on Manager APIs with Sync-specific semantics. 257 */ 258 function AddonsStore(name, engine) { 259 Store.call(this, name, engine); 260 } 261 AddonsStore.prototype = { 262 // Define the add-on types (.type) that we support. 263 _syncableTypes: ["extension", "theme"], 264 265 _extensionsPrefs: Services.prefs.getBranch("extensions."), 266 267 get reconciler() { 268 return this.engine._reconciler; 269 }, 270 271 /** 272 * Override applyIncoming to filter out records we can't handle. 273 */ 274 async applyIncoming(record) { 275 // The fields we look at aren't present when the record is deleted. 276 if (!record.deleted) { 277 // Ignore records not belonging to our application ID because that is the 278 // current policy. 279 if (record.applicationID != Services.appinfo.ID) { 280 this._log.info( 281 "Ignoring incoming record from other App ID: " + record.id 282 ); 283 return; 284 } 285 286 // Ignore records that aren't from the official add-on repository, as that 287 // is our current policy. 288 if (record.source != "amo") { 289 this._log.info( 290 "Ignoring unknown add-on source (" + 291 record.source + 292 ")" + 293 " for " + 294 record.id 295 ); 296 return; 297 } 298 } 299 300 // Ignore incoming records for which an existing non-syncable addon 301 // exists. Note that we do not insist that the addon manager already have 302 // metadata for this addon - it's possible our reconciler previously saw the 303 // addon but the addon-manager cache no longer has it - which is fine for a 304 // new incoming addon. 305 // (Note that most other cases where the addon-manager cache is invalid 306 // doesn't get this treatment because that cache self-repairs after some 307 // time - but it only re-populates addons which are currently installed.) 308 let existingMeta = this.reconciler.addons[record.addonID]; 309 if ( 310 existingMeta && 311 !(await this.isAddonSyncable(existingMeta, /* ignoreRepoCheck */ true)) 312 ) { 313 this._log.info( 314 "Ignoring incoming record for an existing but non-syncable addon", 315 record.addonID 316 ); 317 return; 318 } 319 320 await Store.prototype.applyIncoming.call(this, record); 321 }, 322 323 /** 324 * Provides core Store API to create/install an add-on from a record. 325 */ 326 async create(record) { 327 // This will throw if there was an error. This will get caught by the sync 328 // engine and the record will try to be applied later. 329 const results = await AddonUtils.installAddons([ 330 { 331 id: record.addonID, 332 syncGUID: record.id, 333 enabled: record.enabled, 334 requireSecureURI: this._extensionsPrefs.getBoolPref( 335 "install.requireSecureOrigin", 336 true 337 ), 338 }, 339 ]); 340 341 if (results.skipped.includes(record.addonID)) { 342 this._log.info("Add-on skipped: " + record.addonID); 343 // Just early-return for skipped addons - we don't want to arrange to 344 // try again next time because the condition that caused up to skip 345 // will remain true for this addon forever. 346 return; 347 } 348 349 let addon; 350 for (let a of results.addons) { 351 if (a.id == record.addonID) { 352 addon = a; 353 break; 354 } 355 } 356 357 // This should never happen, but is present as a fail-safe. 358 if (!addon) { 359 throw new Error("Add-on not found after install: " + record.addonID); 360 } 361 362 this._log.info("Add-on installed: " + record.addonID); 363 }, 364 365 /** 366 * Provides core Store API to remove/uninstall an add-on from a record. 367 */ 368 async remove(record) { 369 // If this is called, the payload is empty, so we have to find by GUID. 370 let addon = await this.getAddonByGUID(record.id); 371 if (!addon) { 372 // We don't throw because if the add-on could not be found then we assume 373 // it has already been uninstalled and there is nothing for this function 374 // to do. 375 return; 376 } 377 378 this._log.info("Uninstalling add-on: " + addon.id); 379 await AddonUtils.uninstallAddon(addon); 380 }, 381 382 /** 383 * Provides core Store API to update an add-on from a record. 384 */ 385 async update(record) { 386 let addon = await this.getAddonByID(record.addonID); 387 388 // update() is called if !this.itemExists. And, since itemExists consults 389 // the reconciler only, we need to take care of some corner cases. 390 // 391 // First, the reconciler could know about an add-on that was uninstalled 392 // and no longer present in the add-ons manager. 393 if (!addon) { 394 await this.create(record); 395 return; 396 } 397 398 // It's also possible that the add-on is non-restartless and has pending 399 // install/uninstall activity. 400 // 401 // We wouldn't get here if the incoming record was for a deletion. So, 402 // check for pending uninstall and cancel if necessary. 403 if (addon.pendingOperations & lazy.AddonManager.PENDING_UNINSTALL) { 404 addon.cancelUninstall(); 405 406 // We continue with processing because there could be state or ID change. 407 } 408 409 await this.updateUserDisabled(addon, !record.enabled); 410 }, 411 412 /** 413 * Provide core Store API to determine if a record exists. 414 */ 415 async itemExists(guid) { 416 let addon = this.reconciler.getAddonStateFromSyncGUID(guid); 417 418 return !!addon; 419 }, 420 421 /** 422 * Create an add-on record from its GUID. 423 * 424 * @param guid 425 * Add-on GUID (from extensions DB) 426 * @param collection 427 * Collection to add record to. 428 * 429 * @return AddonRecord instance 430 */ 431 async createRecord(guid, collection) { 432 let record = new AddonRecord(collection, guid); 433 record.applicationID = Services.appinfo.ID; 434 435 let addon = this.reconciler.getAddonStateFromSyncGUID(guid); 436 437 // If we don't know about this GUID or if it has been uninstalled, we mark 438 // the record as deleted. 439 if (!addon || !addon.installed) { 440 record.deleted = true; 441 return record; 442 } 443 444 record.modified = addon.modified.getTime() / 1000; 445 446 record.addonID = addon.id; 447 record.enabled = addon.enabled; 448 449 // This needs to be dynamic when add-ons don't come from AddonRepository. 450 record.source = "amo"; 451 452 return record; 453 }, 454 455 /** 456 * Changes the id of an add-on. 457 * 458 * This implements a core API of the store. 459 */ 460 async changeItemID(oldID, newID) { 461 // We always update the GUID in the reconciler because it will be 462 // referenced later in the sync process. 463 let state = this.reconciler.getAddonStateFromSyncGUID(oldID); 464 if (state) { 465 state.guid = newID; 466 await this.reconciler.saveState(); 467 } 468 469 let addon = await this.getAddonByGUID(oldID); 470 if (!addon) { 471 this._log.debug( 472 "Cannot change item ID (" + 473 oldID + 474 ") in Add-on " + 475 "Manager because old add-on not present: " + 476 oldID 477 ); 478 return; 479 } 480 481 addon.syncGUID = newID; 482 }, 483 484 /** 485 * Obtain the set of all syncable add-on Sync GUIDs. 486 * 487 * This implements a core Store API. 488 */ 489 async getAllIDs() { 490 let ids = {}; 491 492 let addons = this.reconciler.addons; 493 for (let id in addons) { 494 let addon = addons[id]; 495 if (await this.isAddonSyncable(addon)) { 496 ids[addon.guid] = true; 497 } 498 } 499 500 return ids; 501 }, 502 503 /** 504 * Wipe engine data. 505 * 506 * This uninstalls all syncable addons from the application. In case of 507 * error, it logs the error and keeps trying with other add-ons. 508 */ 509 async wipe() { 510 this._log.info("Processing wipe."); 511 512 await this.engine._refreshReconcilerState(); 513 514 // We only wipe syncable add-ons. Wipe is a Sync feature not a security 515 // feature. 516 let ids = await this.getAllIDs(); 517 for (let guid in ids) { 518 let addon = await this.getAddonByGUID(guid); 519 if (!addon) { 520 this._log.debug( 521 "Ignoring add-on because it couldn't be obtained: " + guid 522 ); 523 continue; 524 } 525 526 this._log.info("Uninstalling add-on as part of wipe: " + addon.id); 527 await Utils.catch.call(this, () => addon.uninstall())(); 528 } 529 }, 530 531 /*************************************************************************** 532 * Functions below are unique to this store and not part of the Store API * 533 ***************************************************************************/ 534 535 /** 536 * Obtain an add-on from its public ID. 537 * 538 * @param id 539 * Add-on ID 540 * @return Addon or undefined if not found 541 */ 542 async getAddonByID(id) { 543 return lazy.AddonManager.getAddonByID(id); 544 }, 545 546 /** 547 * Obtain an add-on from its Sync GUID. 548 * 549 * @param guid 550 * Add-on Sync GUID 551 * @return DBAddonInternal or null 552 */ 553 async getAddonByGUID(guid) { 554 return lazy.AddonManager.getAddonBySyncGUID(guid); 555 }, 556 557 /** 558 * Determines whether an add-on is suitable for Sync. 559 * 560 * @param addon 561 * Addon instance 562 * @param ignoreRepoCheck 563 * Should we skip checking the Addons repository (primarially useful 564 * for testing and validation). 565 * @return Boolean indicating whether it is appropriate for Sync 566 */ 567 async isAddonSyncable(addon, ignoreRepoCheck = false) { 568 // Currently, we limit syncable add-ons to those that are: 569 // 1) In a well-defined set of types 570 // 2) Installed in the current profile 571 // 3) Not installed by a foreign entity (i.e. installed by the app) 572 // since they act like global extensions. 573 // 4) Is not a hotfix. 574 // 5) The addons XPIProvider doesn't veto it (i.e not being installed in 575 // the profile directory, or any other reasons it says the addon can't 576 // be synced) 577 // 6) Are installed from AMO 578 579 // We could represent the test as a complex boolean expression. We go the 580 // verbose route so the failure reason is logged. 581 if (!addon) { 582 this._log.debug("Null object passed to isAddonSyncable."); 583 return false; 584 } 585 586 if (!this._syncableTypes.includes(addon.type)) { 587 this._log.debug( 588 addon.id + " not syncable: type not in allowed list: " + addon.type 589 ); 590 return false; 591 } 592 593 if (!(addon.scope & lazy.AddonManager.SCOPE_PROFILE)) { 594 this._log.debug(addon.id + " not syncable: not installed in profile."); 595 return false; 596 } 597 598 // If the addon manager says it's not syncable, we skip it. 599 if (!addon.isSyncable) { 600 this._log.debug(addon.id + " not syncable: vetoed by the addon manager."); 601 return false; 602 } 603 604 // This may be too aggressive. If an add-on is downloaded from AMO and 605 // manually placed in the profile directory, foreignInstall will be set. 606 // Arguably, that add-on should be syncable. 607 // TODO Address the edge case and come up with more robust heuristics. 608 if (addon.foreignInstall) { 609 this._log.debug(addon.id + " not syncable: is foreign install."); 610 return false; 611 } 612 613 // If the AddonRepository's cache isn't enabled (which it typically isn't 614 // in tests), getCachedAddonByID always returns null - so skip the check 615 // in that case. We also provide a way to specifically opt-out of the check 616 // even if the cache is enabled, which is used by the validators. 617 if (ignoreRepoCheck || !lazy.AddonRepository.cacheEnabled) { 618 return true; 619 } 620 621 let result = await new Promise(res => { 622 lazy.AddonRepository.getCachedAddonByID(addon.id, res); 623 }); 624 625 if (!result) { 626 this._log.debug( 627 addon.id + " not syncable: add-on not found in add-on repository." 628 ); 629 return false; 630 } 631 632 return this.isSourceURITrusted(result.sourceURI); 633 }, 634 635 /** 636 * Determine whether an add-on's sourceURI field is trusted and the add-on 637 * can be installed. 638 * 639 * This function should only ever be called from isAddonSyncable(). It is 640 * exposed as a separate function to make testing easier. 641 * 642 * @param uri 643 * nsIURI instance to validate 644 * @return bool 645 */ 646 isSourceURITrusted: function isSourceURITrusted(uri) { 647 // For security reasons, we currently limit synced add-ons to those 648 // installed from trusted hostname(s). We additionally require TLS with 649 // the add-ons site to help prevent forgeries. 650 let trustedHostnames = Svc.PrefBranch.getStringPref( 651 "addons.trustedSourceHostnames", 652 "" 653 ).split(","); 654 655 if (!uri) { 656 this._log.debug("Undefined argument to isSourceURITrusted()."); 657 return false; 658 } 659 660 // Scheme is validated before the hostname because uri.host may not be 661 // populated for certain schemes. It appears to always be populated for 662 // https, so we avoid the potential NS_ERROR_FAILURE on field access. 663 if (uri.scheme != "https") { 664 this._log.debug("Source URI not HTTPS: " + uri.spec); 665 return false; 666 } 667 668 if (!trustedHostnames.includes(uri.host)) { 669 this._log.debug("Source hostname not trusted: " + uri.host); 670 return false; 671 } 672 673 return true; 674 }, 675 676 /** 677 * Update the userDisabled flag on an add-on. 678 * 679 * This will enable or disable an add-on. It has no return value and does 680 * not catch or handle exceptions thrown by the addon manager. If no action 681 * is needed it will return immediately. 682 * 683 * @param addon 684 * Addon instance to manipulate. 685 * @param value 686 * Boolean to which to set userDisabled on the passed Addon. 687 */ 688 async updateUserDisabled(addon, value) { 689 if (addon.userDisabled == value) { 690 return; 691 } 692 693 // A pref allows changes to the enabled flag to be ignored. 694 if (Svc.PrefBranch.getBoolPref("addons.ignoreUserEnabledChanges", false)) { 695 this._log.info( 696 "Ignoring enabled state change due to preference: " + addon.id 697 ); 698 return; 699 } 700 701 AddonUtils.updateUserDisabled(addon, value); 702 // updating this flag doesn't send a notification for appDisabled addons, 703 // meaning the reconciler will not update its state and may resync the 704 // addon - so explicitly rectify the state (bug 1366994) 705 if (addon.appDisabled) { 706 await this.reconciler.rectifyStateFromAddon(addon); 707 } 708 }, 709 }; 710 711 Object.setPrototypeOf(AddonsStore.prototype, Store.prototype); 712 713 /** 714 * The add-ons tracker keeps track of real-time changes to add-ons. 715 * 716 * It hooks up to the reconciler and receives notifications directly from it. 717 */ 718 function AddonsTracker(name, engine) { 719 LegacyTracker.call(this, name, engine); 720 } 721 AddonsTracker.prototype = { 722 get reconciler() { 723 return this.engine._reconciler; 724 }, 725 726 get store() { 727 return this.engine._store; 728 }, 729 730 /** 731 * This callback is executed whenever the AddonsReconciler sends out a change 732 * notification. See AddonsReconciler.addChangeListener(). 733 */ 734 async changeListener(date, change, addon) { 735 this._log.debug("changeListener invoked: " + change + " " + addon.id); 736 // Ignore changes that occur during sync. 737 if (this.ignoreAll) { 738 return; 739 } 740 741 if (!(await this.store.isAddonSyncable(addon))) { 742 this._log.debug( 743 "Ignoring change because add-on isn't syncable: " + addon.id 744 ); 745 return; 746 } 747 748 const added = await this.addChangedID(addon.guid, date.getTime() / 1000); 749 if (added) { 750 this.score += SCORE_INCREMENT_XLARGE; 751 } 752 }, 753 754 onStart() { 755 this.reconciler.startListening(); 756 this.reconciler.addChangeListener(this); 757 }, 758 759 onStop() { 760 this.reconciler.removeChangeListener(this); 761 this.reconciler.stopListening(); 762 }, 763 }; 764 765 Object.setPrototypeOf(AddonsTracker.prototype, LegacyTracker.prototype); 766 767 export class AddonValidator extends CollectionValidator { 768 constructor(engine = null) { 769 super("addons", "id", ["addonID", "enabled", "applicationID", "source"]); 770 this.engine = engine; 771 } 772 773 async getClientItems() { 774 return lazy.AddonManager.getAllAddons(); 775 } 776 777 normalizeClientItem(item) { 778 let enabled = !item.userDisabled; 779 if (item.pendingOperations & lazy.AddonManager.PENDING_ENABLE) { 780 enabled = true; 781 } else if (item.pendingOperations & lazy.AddonManager.PENDING_DISABLE) { 782 enabled = false; 783 } 784 return { 785 enabled, 786 id: item.syncGUID, 787 addonID: item.id, 788 applicationID: Services.appinfo.ID, 789 source: "amo", // check item.foreignInstall? 790 original: item, 791 }; 792 } 793 794 async normalizeServerItem(item) { 795 let guid = await this.engine._findDupe(item); 796 if (guid) { 797 item.id = guid; 798 } 799 return item; 800 } 801 802 clientUnderstands(item) { 803 return item.applicationID === Services.appinfo.ID; 804 } 805 806 async syncedByClient(item) { 807 return ( 808 !item.original.hidden && 809 !item.original.isSystem && 810 !( 811 item.original.pendingOperations & lazy.AddonManager.PENDING_UNINSTALL 812 ) && 813 // No need to await the returned promise explicitely: 814 // |expr1 && expr2| evaluates to expr2 if expr1 is true. 815 this.engine.isAddonSyncable(item.original, true) 816 ); 817 } 818 }