diff --git a/services/sync/tests/unit/test_collections_recovery.js b/services/sync/tests/unit/test_collections_recovery.js index 7b4599b55a5e..fe4fce3cc4fe 100644 --- a/services/sync/tests/unit/test_collections_recovery.js +++ b/services/sync/tests/unit/test_collections_recovery.js @@ -46,6 +46,7 @@ add_task(async function test_missing_crypto_collection() { ]; // Disable addon sync because AddonManager won't be initialized here. await Service.engineManager.unregister("addons"); + await Service.engineManager.unregister("extension-storage"); for (let coll of collections) { handlers["/1.1/johndoe/storage/" + coll] = johnU( diff --git a/services/sync/tests/unit/test_corrupt_keys.js b/services/sync/tests/unit/test_corrupt_keys.js index 41ea9e3df917..d209738ef192 100644 --- a/services/sync/tests/unit/test_corrupt_keys.js +++ b/services/sync/tests/unit/test_corrupt_keys.js @@ -40,7 +40,9 @@ add_task(async function test_locally_changed_keys() { Service.clusterURL = Service.identity._token.endpoint; await Service.engineManager.register(HistoryEngine); + // Disable addon sync because AddonManager won't be initialized here. await Service.engineManager.unregister("addons"); + await Service.engineManager.unregister("extension-storage"); async function corrupt_local_keys() { Service.collectionKeys._default.keyPair = [ diff --git a/services/sync/tests/unit/test_password_engine.js b/services/sync/tests/unit/test_password_engine.js index e1bebefede31..0a3eac4539a0 100644 --- a/services/sync/tests/unit/test_password_engine.js +++ b/services/sync/tests/unit/test_password_engine.js @@ -26,7 +26,9 @@ async function cleanup(engine, server) { } add_task(async function setup() { - await Service.engineManager.unregister("addons"); // To silence errors. + // Disable addon sync because AddonManager won't be initialized here. + await Service.engineManager.unregister("addons"); + await Service.engineManager.unregister("extension-storage"); }); add_task(async function test_ignored_fields() { diff --git a/services/sync/tests/unit/test_telemetry.js b/services/sync/tests/unit/test_telemetry.js index f172a7ea3817..a07fa34bd2e0 100644 --- a/services/sync/tests/unit/test_telemetry.js +++ b/services/sync/tests/unit/test_telemetry.js @@ -91,6 +91,7 @@ async function cleanAndGo(engine, server) { add_task(async function setup() { // Avoid addon manager complaining about not being initialized await Service.engineManager.unregister("addons"); + await Service.engineManager.unregister("extension-storage"); }); add_task(async function test_basic() { diff --git a/toolkit/components/extensions/ExtensionStorageSync.jsm b/toolkit/components/extensions/ExtensionStorageSync.jsm index 0811399cba95..2d56a989ec33 100644 --- a/toolkit/components/extensions/ExtensionStorageSync.jsm +++ b/toolkit/components/extensions/ExtensionStorageSync.jsm @@ -51,6 +51,7 @@ const { ExtensionUtils } = ChromeUtils.import( ); XPCOMUtils.defineLazyModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.jsm", BulkKeyBundle: "resource://services-sync/keys.js", CollectionKeyManager: "resource://services-sync/record.js", CommonUtils: "resource://services-common/utils.js", @@ -781,8 +782,41 @@ class ExtensionStorageSync { this.listeners = new WeakMap(); } + /** + * Get a set of extensions to sync (including the ones with an + * active extension context that used the storage.sync API and + * the extensions that are enabled and have been synced before). + * + * @returns {Promise>} + * A promise which resolves to the set of the extensions to sync. + */ + async getExtensions() { + // Start from the set of the extensions with an active + // context that used the storage.sync APIs. + const extensions = new Set(extensionContexts.keys()); + + const allEnabledExtensions = await AddonManager.getAddonsByTypes([ + "extension", + ]); + + // Get the existing extension collections salts. + const keysRecord = await this.cryptoCollection.getKeyRingRecord(); + + // Add any enabled extensions that have been synced before. + for (const addon of allEnabledExtensions) { + if (this.hasSaltsFor(keysRecord, [addon.id])) { + const policy = WebExtensionPolicy.getByID(addon.id); + if (policy && policy.extension) { + extensions.add(policy.extension); + } + } + } + + return extensions; + } + async syncAll() { - const extensions = extensionContexts.keys(); + const extensions = await this.getExtensions(); const extIds = Array.from(extensions, extension => extension.id); log.debug(`Syncing extension settings for ${JSON.stringify(extIds)}`); if (extIds.length == 0) { @@ -791,7 +825,7 @@ class ExtensionStorageSync { } await this.ensureCanSync(extIds); await this.checkSyncKeyRing(); - const promises = Array.from(extensionContexts.keys(), extension => { + const promises = Array.from(extensions, extension => { return openCollection(this.cryptoCollection, extension).then(coll => { return this.sync(extension, coll); }); @@ -1296,11 +1330,11 @@ class ExtensionStorageSync { /* Wipe local data for all collections without causing the changes to be synced */ async clearAll() { - const extensions = extensionContexts.keys(); + const extensions = await this.getExtensions(); const extIds = Array.from(extensions, extension => extension.id); log.debug(`Clearing extension data for ${JSON.stringify(extIds)}`); if (extIds.length) { - const promises = Array.from(extensionContexts.keys(), extension => { + const promises = Array.from(extensions, extension => { return openCollection(this.cryptoCollection, extension).then(coll => { return coll.clear(); }); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js index 8be8c8281247..cf07fff0d19e 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js @@ -10,6 +10,7 @@ const { CommonUtils } = ChromeUtils.import( "resource://services-common/utils.js" ); const { + cleanUpForContext, CollectionKeyEncryptionRemoteTransformer, CryptoCollection, ExtensionStorageSync, @@ -22,6 +23,12 @@ const { BulkKeyBundle } = ChromeUtils.import( ); const { Utils } = ChromeUtils.import("resource://services-sync/util.js"); +const { createAppInfo, promiseStartupManager } = AddonTestUtils; + +AddonTestUtils.init(this); + +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "69"); + /* globals BulkKeyBundle, CommonUtils, EncryptionRemoteTransformer */ /* globals Utils */ @@ -637,6 +644,10 @@ function uuid() { return uuidgen.generateUUID().toString(); } +add_task(async function test_setup() { + await promiseStartupManager(); +}); + add_task(async function test_key_to_id() { equal(keyToId("foo"), "key-foo"); equal(keyToId("my-new-key"), "key-my_2D_new_2D_key"); @@ -684,46 +695,92 @@ add_task(async function test_extension_id_to_collection_id() { }); add_task(async function ensureCanSync_clearAll() { - const extensionId = uuid(); - const extension = { id: extensionId }; + // A test extension that will not have any active context around + // but it is returned from a call to AddonManager.getExtensionsByType. + const extensionId = "test-wipe-on-enabled-and-synced@mochi.test"; + const testExtension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + permissions: ["storage"], + applications: { gecko: { id: extensionId } }, + }, + }); + + await testExtension.startup(); + + // Retrieve the Extension class instance from the test extension. + const { extension } = testExtension; + + // Another test extension that will have an active extension context. + const extensionId2 = "test-wipe-on-active-context@mochi.test"; + const extension2 = { id: extensionId2 }; await withContextAndServer(async function(context, server) { await withSignedInUser(loggedInUser, async function( extensionStorageSync, fxaService ) { + async function assertSetAndGetData(extension, data) { + await extensionStorageSync.set(extension, data, context); + let storedData = await extensionStorageSync.get( + extension, + Object.keys(data), + context + ); + const extId = extensionId; + deepEqual(storedData, data, `${extId} should get back the data we set`); + } + + async function assertDataCleared(extension, keys) { + const storedData = await extensionStorageSync.get( + extension, + keys, + context + ); + deepEqual(storedData, {}, `${extension.id} should have lost the data`); + } + server.installCollection("storage-sync-crypto"); server.etag = 1000; - let newKeys = await extensionStorageSync.ensureCanSync([extensionId]); + let newKeys = await extensionStorageSync.ensureCanSync([ + extensionId, + extensionId2, + ]); ok( newKeys.hasKeysFor([extensionId]), `key isn't present for ${extensionId}` ); + ok( + newKeys.hasKeysFor([extensionId2]), + `key isn't present for ${extensionId2}` + ); let posts = server.getPosts(); equal(posts.length, 1); - const post = posts[0]; - assertPostedNewRecord(post); + assertPostedNewRecord(posts[0]); - // Set data for an extension and sync. - await extensionStorageSync.set(extension, { "my-key": 5 }, context); - let keyValue = await extensionStorageSync.get( - extension, - ["my-key"], - context - ); - equal(keyValue["my-key"], 5, "should get back the data we set"); + await assertSetAndGetData(extension, { "my-key": 1 }); + await assertSetAndGetData(extension2, { "my-key": 2 }); + + // Call cleanup for the first extension, to double check it has + // been wiped out even without an active extension context. + cleanUpForContext(extension, context); // clear everything. await extensionStorageSync.clearAll(); - keyValue = await extensionStorageSync.get(extension, ["my-key"], context); - deepEqual(keyValue, {}, "should have lost the data"); + // Assert that the data is gone for both the extensions. + await assertDataCleared(extension, ["my-key"]); + await assertDataCleared(extension2, ["my-key"]); + // should have been no posts caused by the clear. + posts = server.getPosts(); equal(posts.length, 1); }); }); + + await testExtension.unload(); }); add_task(async function ensureCanSync_posts_new_keys() { @@ -1615,6 +1672,86 @@ add_task(async function test_storage_sync_pulls_changes() { }); }); +// Tests that an enabled extension which have been synced before it is going +// to be synced on ExtensionStorageSync.syncAll even if there is no active +// context that is currently using the API. +add_task(async function test_storage_sync_on_no_active_context() { + const extensionId = "sync@mochi.test"; + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + permissions: ["storage"], + applications: { gecko: { id: extensionId } }, + }, + files: { + "ext-page.html": ` + + + + + + `, + "ext-page.js": function() { + const { browser } = this; + browser.test.onMessage.addListener(async msg => { + if (msg === "get-sync-data") { + browser.test.sendMessage( + "get-sync-data:done", + await browser.storage.sync.get(["remote-key"]) + ); + } + }); + }, + }, + }); + + await extension.startup(); + + await withServer(async server => { + await withSignedInUser(loggedInUser, async function( + extensionStorageSync, + fxaService + ) { + const cryptoCollection = new CryptoCollection(fxaService); + let transformer = new CollectionKeyEncryptionRemoteTransformer( + cryptoCollection, + extensionId + ); + server.installCollection("storage-sync-crypto"); + + await extensionStorageSync.ensureCanSync([extensionId]); + const collectionId = await cryptoCollection.extensionIdToCollectionId( + extensionId + ); + await server.encryptAndAddRecord(transformer, { + collectionId, + data: { + id: "key-remote_2D_key", + key: "remote-key", + data: 6, + }, + predicate: appearsAt(850), + }); + + server.etag = 1000; + await extensionStorageSync.syncAll(); + }); + }); + + const extPage = await ExtensionTestUtils.loadContentPage( + `moz-extension://${extension.uuid}/ext-page.html`, + { extension } + ); + + await extension.sendMessage("get-sync-data"); + const res = await extension.awaitMessage("get-sync-data:done"); + Assert.deepEqual(res, { "remote-key": 6 }, "Got the expected sync data"); + + await extPage.close(); + + await extension.unload(); +}); + add_task(async function test_storage_sync_pushes_changes() { // FIXME: This test relies on the fact that previous tests pushed // keys and salts for the default extension ID