Loading toolkit/components/extensions/ExtensionDNR.sys.mjs +9 −0 Original line number Original line Diff line number Diff line Loading @@ -1368,6 +1368,14 @@ function validateManifestEntry(extension) { } } } } async function updateEnabledStaticRulesets(extension, updateRulesetOptions) { await ensureInitialized(extension); await lazy.ExtensionDNRStore.updateEnabledStaticRulesets( extension, updateRulesetOptions ); } // exports used by the DNR API implementation. // exports used by the DNR API implementation. export const ExtensionDNR = { export const ExtensionDNR = { RuleValidator, RuleValidator, Loading @@ -1375,6 +1383,7 @@ export const ExtensionDNR = { ensureInitialized, ensureInitialized, getMatchedRulesForRequest, getMatchedRulesForRequest, getRuleManager, getRuleManager, updateEnabledStaticRulesets, validateManifestEntry, validateManifestEntry, // TODO(Bug 1803370): consider allowing changing DNR limits through about:config prefs). // TODO(Bug 1803370): consider allowing changing DNR limits through about:config prefs). limits: { limits: { Loading toolkit/components/extensions/ExtensionDNRStore.sys.mjs +181 −2 Original line number Original line Diff line number Diff line Loading @@ -6,18 +6,24 @@ const { ExtensionParent } = ChromeUtils.import( "resource://gre/modules/ExtensionParent.jsm" "resource://gre/modules/ExtensionParent.jsm" ); ); const { ExtensionUtils } = ChromeUtils.import( "resource://gre/modules/ExtensionUtils.jsm" ); import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; const lazy = {}; XPCOMUtils.defineLazyModuleGetters(lazy, { XPCOMUtils.defineLazyModuleGetters(lazy, { Schemas: "resource://gre/modules/Schemas.jsm", Schemas: "resource://gre/modules/Schemas.jsm", PromiseUtils: "resource://gre/modules/PromiseUtils.jsm", }); }); ChromeUtils.defineESModuleGetters(lazy, { ChromeUtils.defineESModuleGetters(lazy, { ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs", ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs", }); }); const { DefaultMap, ExtensionError } = ExtensionUtils; const { StartupCache } = ExtensionParent; const { StartupCache } = ExtensionParent; // DNR Rules store subdirectory/file names and file extensions. // DNR Rules store subdirectory/file names and file extensions. Loading Loading @@ -79,6 +85,63 @@ class StoreData { } } } } class Queue { #tasks = []; #runningTask = null; #closed = false; get hasPendingTasks() { return !!this.#runningTask || !!this.#tasks.length; } get isClosed() { return this.#closed; } async close() { if (this.#closed) { return; } const drainedQueuePromise = this.queueTask(() => {}); this.#closed = true; return drainedQueuePromise; } queueTask(callback) { if (this.#closed) { throw new Error("Unexpected queueTask call on closed queue"); } const deferred = lazy.PromiseUtils.defer(); this.#tasks.push({ callback, deferred }); // Run the queued task right away if there isn't one already running. if (!this.#runningTask) { this.#runNextTask(); } return deferred.promise; } async #runNextTask() { if (!this.#tasks.length) { this.#runningTask = null; return; } this.#runningTask = this.#tasks.shift(); const { callback, deferred } = this.#runningTask; try { let result = callback(); if (result instanceof Promise) { result = await result; } deferred.resolve(result); } catch (err) { deferred.reject(err); } this.#runNextTask(); } } /** /** * Class managing the rulesets persisted across browser sessions. * Class managing the rulesets persisted across browser sessions. * * Loading @@ -103,6 +166,8 @@ class RulesetsStore { this._dataPromises = new Map(); this._dataPromises = new Map(); // Map<extensionUUID, Promise<void>> // Map<extensionUUID, Promise<void>> this._savePromises = new Map(); this._savePromises = new Map(); // Map<extensionUUID, Queue> this._dataUpdateQueues = new DefaultMap(() => new Queue()); // Map<extensionUUID, { close: Function }> // Map<extensionUUID, { close: Function }> this._shutdownHandlers = new Map(); this._shutdownHandlers = new Map(); // Promise to await on to ensure the store parent directory exist // Promise to await on to ensure the store parent directory exist Loading Loading @@ -181,6 +246,27 @@ class RulesetsStore { return data?.staticRulesets; return data?.staticRulesets; } } /** * Update the enabled rulesets, queue changes to prevent races between calls * that may be triggered while an update is still in process. * * @param {Extension} extension * @param {object} params * @param {Array<string>} [params.disableRulesetIds=[]] * @param {Array<string>} [params.enableRulesetIds=[]] */ async updateEnabledStaticRulesets( extension, { disableRulesetIds, enableRulesetIds } ) { return this._dataUpdateQueues.get(extension.uuid).queueTask(() => { return this.#updateEnabledStaticRulesets(extension, { disableRulesetIds, enableRulesetIds, }); }); } /** /** * Return the store file path for the given the extension's uuid. * Return the store file path for the given the extension's uuid. * * Loading Loading @@ -227,7 +313,23 @@ class RulesetsStore { let shutdownHandler = this._shutdownHandlers.get(extensionUUID); let shutdownHandler = this._shutdownHandlers.get(extensionUUID); if (!shutdownHandler) { if (!shutdownHandler) { shutdownHandler = { shutdownHandler = { close: () => this.unloadData(extensionUUID), close: async () => { // Wait for the update tasks to have been executed, then unload the // data. const dataUpdateQueue = this._dataUpdateQueues.has(extensionUUID) ? this._dataUpdateQueues.get(extensionUUID) : undefined; if (dataUpdateQueue) { try { await dataUpdateQueue.close(); } catch (err) { // Unexpected error on closing the update queue. Cu.reportError(err); } this._dataUpdateQueues.delete(extensionUUID); } this.unloadData(extensionUUID); }, }; }; this._shutdownHandlers.set(extensionUUID, shutdownHandler); this._shutdownHandlers.set(extensionUUID, shutdownHandler); } } Loading @@ -247,6 +349,7 @@ class RulesetsStore { await savePromise; await savePromise; this._savePromises.delete(extensionUUID); this._savePromises.delete(extensionUUID); } } this._dataPromises.delete(extensionUUID); this._dataPromises.delete(extensionUUID); this._data.delete(extensionUUID); this._data.delete(extensionUUID); } } Loading Loading @@ -588,6 +691,80 @@ class RulesetsStore { this._savePromises.delete(extensionUUID); this._savePromises.delete(extensionUUID); } } } } /** * Internal implementation for updating the enabled rulesets and enforcing * static rulesets and rules count limits. * * @param {Extension} extension * @param {object} params * @param {Array<string>} [params.disableRulesetIds=[]] * @param {Array<string>} [params.enableRulesetIds=[]] */ async #updateEnabledStaticRulesets( extension, { disableRulesetIds, enableRulesetIds } ) { const ruleResources = extension.manifest.declarative_net_request?.rule_resources; if (!Array.isArray(ruleResources)) { return; } const enabledRulesets = await this.getEnabledStaticRulesets(extension); const updatedEnabledRulesets = new Map(); let disableIds = new Set(disableRulesetIds); let enableIds = new Set(enableRulesetIds); // valiate the ruleset ids for existence (which will also reject calls // including the reserved _session and _dynamic, because static rulesets // id are validated as part of the manifest validation and they are not // allowed to start with '_'). const existingIds = new Set(ruleResources.map(rs => rs.id)); const errorOnInvalidRulesetIds = rsIdSet => { for (const rsId of rsIdSet) { if (!existingIds.has(rsId)) { throw new ExtensionError(`Invalid ruleset id: "${rsId}"`); } } }; errorOnInvalidRulesetIds(disableIds); errorOnInvalidRulesetIds(enableIds); // Copy into the updatedEnabledRulesets Map any ruleset that is not // requested to be disabled or is enabled back in the same request. for (const [rulesetId, ruleset] of enabledRulesets) { if (!disableIds.has(rulesetId) || enableIds.has(rulesetId)) { updatedEnabledRulesets.set(rulesetId, ruleset); enableIds.delete(rulesetId); } } const { MAX_NUMBER_OF_ENABLED_STATIC_RULESETS } = lazy.ExtensionDNR.limits; const maxNewRulesetsCount = MAX_NUMBER_OF_ENABLED_STATIC_RULESETS - updatedEnabledRulesets.size; if (enableIds.size > maxNewRulesetsCount) { // Log an error for the developer. throw new ExtensionError( `updatedEnabledRulesets request is exceeding MAX_NUMBER_OF_ENABLED_STATIC_RULESETS` ); } const newRulesets = await this.#getManifestStaticRulesets( extension, Array.from(enableIds) ); for (const [rulesetId, ruleset] of newRulesets.entries()) { updatedEnabledRulesets.set(rulesetId, ruleset); } this._data.get(extension.uuid).staticRulesets = updatedEnabledRulesets; await this.save(extension); await this.updateRulesetManager(extension); } } } const store = new RulesetsStore(); const store = new RulesetsStore(); Loading Loading @@ -659,7 +836,9 @@ export const ExtensionDNRStore = { return store.clearOnUninstall(extensionUUID); return store.clearOnUninstall(extensionUUID); }, }, initExtension, initExtension, async updateEnabledStaticRulesets(extension, updateRulesetOptions) { await store.updateEnabledStaticRulesets(extension, updateRulesetOptions); }, // Test-only helpers // Test-only helpers _getStoreForTesting() { _getStoreForTesting() { requireTestOnlyCallers(); requireTestOnlyCallers(); Loading toolkit/components/extensions/parent/ext-declarativeNetRequest.js +7 −0 Original line number Original line Diff line number Diff line Loading @@ -52,6 +52,13 @@ this.declarativeNetRequest = class extends ExtensionAPI { return ruleManager.enabledStaticRulesetIds; return ruleManager.enabledStaticRulesetIds; }, }, updateEnabledRulesets({ disableRulesetIds, enableRulesetIds }) { return ExtensionDNR.updateEnabledStaticRulesets(extension, { disableRulesetIds, enableRulesetIds, }); }, getSessionRules() { getSessionRules() { // ruleManager.getSessionRules() returns an array of Rule instances. // ruleManager.getSessionRules() returns an array of Rule instances. // When these are structurally cloned (to send them to the child), // When these are structurally cloned (to send them to the child), Loading toolkit/components/extensions/schemas/declarative_net_request.json +31 −0 Original line number Original line Diff line number Diff line Loading @@ -475,6 +475,37 @@ } } ] ] }, }, { "name": "updateEnabledRulesets", "type": "function", "description": "Returns the ids for the current set of enabled static rulesets.", "async": "callback", "parameters": [ { "name": "updateRulesetOptions", "type": "object", "properties": { "disableRulesetIds": { "type": "array", "items": { "type": "string" }, "optional": true, "default": [] }, "enableRulesetIds": { "type": "array", "items": { "type": "string" }, "optional": true, "default": [] } } }, { "name": "callback", "type": "function", "parameters": [] } ] }, { { "name": "getSessionRules", "name": "getSessionRules", "type": "function", "type": "function", Loading toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js +248 −8 Original line number Original line Diff line number Diff line Loading @@ -27,6 +27,19 @@ function backgroundWithDNRAPICallHandlers() { ) ) ); ); break; break; case "updateEnabledRulesets": // Run (one or more than one concurrently) updateEnabledRulesets calls // and report back the results. result = await Promise.all( args.map(arg => { return browser.declarativeNetRequest .updateEnabledRulesets(arg) .catch(err => { return { rejectedWithErrorMessage: err.message }; }); }) ); break; default: default: browser.test.fail(`Unexpected test message: ${msg}`); browser.test.fail(`Unexpected test message: ${msg}`); return; return; Loading Loading @@ -142,6 +155,18 @@ const assertDNRTestMatchOutcome = async ( ); ); }; }; const assertDNRGetEnabledRulesets = async ( extensionTestWrapper, expectedRulesetIds ) => { extensionTestWrapper.sendMessage("getEnabledRulesets"); Assert.deepEqual( await extensionTestWrapper.awaitMessage("getEnabledRulesets:done"), expectedRulesetIds, "Got the expected enabled ruleset ids from dnr.getEnabledRulesets API method" ); }; const assertDNRStoreData = async ( const assertDNRStoreData = async ( dnrStore, dnrStore, extensionTestWrapper, extensionTestWrapper, Loading @@ -160,13 +185,6 @@ const assertDNRStoreData = async ( return acc; return acc; }, new Map()); }, new Map()); extensionTestWrapper.sendMessage("getEnabledRulesets"); Assert.deepEqual( await extensionTestWrapper.awaitMessage("getEnabledRulesets:done"), expectedRulesetIds, "Got the expected enabled ruleset ids from dnr.getEnabledRulesets API method" ); ok( ok( dnrStore._dataPromises.has(extUUID), dnrStore._dataPromises.has(extUUID), "Got promise for the test extension DNR data being loaded" "Got promise for the test extension DNR data being loaded" Loading Loading @@ -267,6 +285,8 @@ add_task(async function test_load_static_rules() { const dnrStore = ExtensionDNRStore._getStoreForTesting(); const dnrStore = ExtensionDNRStore._getStoreForTesting(); info("Verify DNRStore data for the test extension"); info("Verify DNRStore data for the test extension"); await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_2"]); await assertDNRStoreData(dnrStore, extension, { await assertDNRStoreData(dnrStore, extension, { ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), Loading Loading @@ -338,6 +358,8 @@ add_task(async function test_load_static_rules() { await addon.enable(); await addon.enable(); await extension.awaitMessage("bgpage:ready"); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_2"]); await assertDNRStoreData(dnrStore, extension, { await assertDNRStoreData(dnrStore, extension, { ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), Loading Loading @@ -377,6 +399,7 @@ add_task(async function test_load_static_rules() { }) }) ); ); await extension.awaitMessage("bgpage:ready"); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets(extension, ["ruleset_2"]); await assertDNRStoreData(dnrStore, extension, { await assertDNRStoreData(dnrStore, extension, { ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), }); }); Loading Loading @@ -416,6 +439,7 @@ add_task(async function test_load_static_rules() { }) }) ); ); await extension.awaitMessage("bgpage:ready"); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); await assertDNRStoreData(dnrStore, extension, { await assertDNRStoreData(dnrStore, extension, { ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), }); }); Loading Loading @@ -443,6 +467,7 @@ add_task(async function test_load_static_rules() { }) }) ); ); await extension.awaitMessage("bgpage:ready"); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets(extension, []); await assertDNRStoreData(dnrStore, extension, {}); await assertDNRStoreData(dnrStore, extension, {}); info("Verify matched rules using testMatchOutcome"); info("Verify matched rules using testMatchOutcome"); Loading Loading @@ -672,6 +697,137 @@ add_task(async function test_ruleset_validation() { } } }); }); add_task(async function test_updateEnabledRuleset_id_validation() { const rule_resources = [ { id: "ruleset_1", enabled: true, path: "ruleset_1.json", }, { id: "ruleset_2", enabled: false, path: "ruleset_2.json", }, ]; const ruleset1Data = [ getDNRRule({ action: { type: "allow" }, condition: { resourceTypes: ["main_frame"] }, }), ]; const ruleset2Data = [ getDNRRule({ action: { type: "block" }, condition: { resourceTypes: ["main_frame", "script"] }, }), ]; const files = { "ruleset_1.json": JSON.stringify(ruleset1Data), "ruleset_2.json": JSON.stringify(ruleset2Data), }; let extension = ExtensionTestUtils.loadExtension( getDNRExtension({ rule_resources, files }) ); await extension.startup(); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); const dnrStore = ExtensionDNRStore._getStoreForTesting(); await assertDNRStoreData(dnrStore, extension, { ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), }); const invalidStaticRulesetIds = [ // The following two are reserved for session and dynamic rules. "_session", "_dynamic", "ruleset_non_existing", ]; for (const invalidRSId of invalidStaticRulesetIds) { extension.sendMessage( "updateEnabledRulesets", // Only in rulesets to be disabled. { disableRulesetIds: [invalidRSId] }, // Only in rulesets to be enabled. { enableRulesetIds: [invalidRSId] }, // In both rulesets to be enabled and disabled. { disableRulesetIds: [invalidRSId], enableRulesetIds: [invalidRSId] }, // Along with existing rulesets (and expected the existing rulesets // to stay unchanged due to the invalid ruleset ids.) { disableRulesetIds: [invalidRSId, "ruleset_1"], enableRulesetIds: [invalidRSId, "ruleset_2"], } ); const [ resInDisable, resInEnable, resInEnableAndDisable, resInSameRequestAsValid, ] = await extension.awaitMessage("updateEnabledRulesets:done"); await Assert.rejects( Promise.reject(resInDisable?.rejectedWithErrorMessage), new RegExp(`Invalid ruleset id: "${invalidRSId}"`), `Got the expected rejection on invalid ruleset id "${invalidRSId}" in disableRulesetIds` ); await Assert.rejects( Promise.reject(resInEnable?.rejectedWithErrorMessage), new RegExp(`Invalid ruleset id: "${invalidRSId}"`), `Got the expected rejection on invalid ruleset id "${invalidRSId}" in enableRulesetIds` ); await Assert.rejects( Promise.reject(resInEnableAndDisable?.rejectedWithErrorMessage), new RegExp(`Invalid ruleset id: "${invalidRSId}"`), `Got the expected rejection on invalid ruleset id "${invalidRSId}" in both enable/disableRulesetIds` ); await Assert.rejects( Promise.reject(resInSameRequestAsValid?.rejectedWithErrorMessage), new RegExp(`Invalid ruleset id: "${invalidRSId}"`), `Got the expected rejection on invalid ruleset id "${invalidRSId}" along with valid ruleset ids` ); } // Confirm that the expected rulesets didn't change neither. await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); await assertDNRStoreData(dnrStore, extension, { ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), }); // - List the same ruleset ids more than ones is expected to work and // to be resulting in the same set of rules being enabled // - Disabling and Enabling the same ruleset id should result in the // ruleset being enabled. await extension.sendMessage("updateEnabledRulesets", { disableRulesetIds: [ "ruleset_1", "ruleset_1", "ruleset_2", "ruleset_2", "ruleset_2", ], enableRulesetIds: ["ruleset_2", "ruleset_2"], }); Assert.deepEqual( await extension.awaitMessage("updateEnabledRulesets:done"), [undefined], "Expect the updateEnabledRulesets to result successfully" ); await assertDNRGetEnabledRulesets(extension, ["ruleset_2"]); await assertDNRStoreData(dnrStore, extension, { ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), }); await extension.unload(); }); add_task(async function test_static_rulesets_limits() { add_task(async function test_static_rulesets_limits() { const dnrStore = ExtensionDNRStore._getStoreForTesting(); const dnrStore = ExtensionDNRStore._getStoreForTesting(); Loading Loading @@ -719,13 +875,14 @@ add_task(async function test_static_rulesets_limits() { }) }) ); ); const expectedEnabledRulesets = {}; const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { ExtensionTestUtils.failOnSchemaWarnings(false); ExtensionTestUtils.failOnSchemaWarnings(false); await extension.startup(); await extension.startup(); ExtensionTestUtils.failOnSchemaWarnings(true); ExtensionTestUtils.failOnSchemaWarnings(true); await extension.awaitMessage("bgpage:ready"); await extension.awaitMessage("bgpage:ready"); const expectedEnabledRulesets = {}; for (let i = 0; i < MAX_NUMBER_OF_ENABLED_STATIC_RULESETS; i++) { for (let i = 0; i < MAX_NUMBER_OF_ENABLED_STATIC_RULESETS; i++) { expectedEnabledRulesets[`ruleset_${i}`] = getSchemaNormalizedRules( expectedEnabledRulesets[`ruleset_${i}`] = getSchemaNormalizedRules( extension, extension, Loading @@ -733,6 +890,10 @@ add_task(async function test_static_rulesets_limits() { ); ); } } await assertDNRGetEnabledRulesets( extension, Array.from(Object.keys(expectedEnabledRulesets)) ); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); }); }); Loading @@ -753,5 +914,84 @@ add_task(async function test_static_rulesets_limits() { ], ], }); }); info( "Verify updateEnabledRulesets reject when the request is exceeding the enabled rulesets count limit" ); extension.sendMessage("updateEnabledRulesets", { disableRulesetIds: ["ruleset_0"], enableRulesetIds: ["ruleset_10", "ruleset_11"], }); await Assert.rejects( extension.awaitMessage("updateEnabledRulesets:done").then(results => { if (results[0].rejectedWithErrorMessage) { return Promise.reject(new Error(results[0].rejectedWithErrorMessage)); } return results[0]; }), /updatedEnabledRulesets request is exceeding MAX_NUMBER_OF_ENABLED_STATIC_RULESETS/, "Expected rejection on updateEnabledRulesets exceeting enabled rulesets count limit" ); // Confirm that the expected rulesets didn't change neither. await assertDNRGetEnabledRulesets( extension, Array.from(Object.keys(expectedEnabledRulesets)) ); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); info( "Verify updateEnabledRulesets applies the expected changes when resolves successfully" ); extension.sendMessage( "updateEnabledRulesets", { disableRulesetIds: ["ruleset_0"], enableRulesetIds: ["ruleset_10"], }, { disableRulesetIds: ["ruleset_10"], enableRulesetIds: ["ruleset_11"], } ); await extension.awaitMessage("updateEnabledRulesets:done"); // Expect ruleset_0 disabled, ruleset_10 to be enabled but then disabled by the // second update queued after the first one, and ruleset_11 to be enabled. delete expectedEnabledRulesets.ruleset_0; expectedEnabledRulesets.ruleset_11 = getSchemaNormalizedRules( extension, rules ); await assertDNRGetEnabledRulesets( extension, Array.from(Object.keys(expectedEnabledRulesets)) ); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); // Ensure all changes were stored and reloaded from disk store and the // DNR store update queue can accept new updates. info("Verify static rules load and updates after extension is restarted"); await AddonTestUtils.promiseRestartManager(); await extension.awaitStartup(); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets( extension, Array.from(Object.keys(expectedEnabledRulesets)) ); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); extension.sendMessage("updateEnabledRulesets", { disableRulesetIds: ["ruleset_11"], }); await extension.awaitMessage("updateEnabledRulesets:done"); delete expectedEnabledRulesets.ruleset_11; await assertDNRGetEnabledRulesets( extension, Array.from(Object.keys(expectedEnabledRulesets)) ); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); await extension.unload(); await extension.unload(); }); }); Loading
toolkit/components/extensions/ExtensionDNR.sys.mjs +9 −0 Original line number Original line Diff line number Diff line Loading @@ -1368,6 +1368,14 @@ function validateManifestEntry(extension) { } } } } async function updateEnabledStaticRulesets(extension, updateRulesetOptions) { await ensureInitialized(extension); await lazy.ExtensionDNRStore.updateEnabledStaticRulesets( extension, updateRulesetOptions ); } // exports used by the DNR API implementation. // exports used by the DNR API implementation. export const ExtensionDNR = { export const ExtensionDNR = { RuleValidator, RuleValidator, Loading @@ -1375,6 +1383,7 @@ export const ExtensionDNR = { ensureInitialized, ensureInitialized, getMatchedRulesForRequest, getMatchedRulesForRequest, getRuleManager, getRuleManager, updateEnabledStaticRulesets, validateManifestEntry, validateManifestEntry, // TODO(Bug 1803370): consider allowing changing DNR limits through about:config prefs). // TODO(Bug 1803370): consider allowing changing DNR limits through about:config prefs). limits: { limits: { Loading
toolkit/components/extensions/ExtensionDNRStore.sys.mjs +181 −2 Original line number Original line Diff line number Diff line Loading @@ -6,18 +6,24 @@ const { ExtensionParent } = ChromeUtils.import( "resource://gre/modules/ExtensionParent.jsm" "resource://gre/modules/ExtensionParent.jsm" ); ); const { ExtensionUtils } = ChromeUtils.import( "resource://gre/modules/ExtensionUtils.jsm" ); import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; const lazy = {}; XPCOMUtils.defineLazyModuleGetters(lazy, { XPCOMUtils.defineLazyModuleGetters(lazy, { Schemas: "resource://gre/modules/Schemas.jsm", Schemas: "resource://gre/modules/Schemas.jsm", PromiseUtils: "resource://gre/modules/PromiseUtils.jsm", }); }); ChromeUtils.defineESModuleGetters(lazy, { ChromeUtils.defineESModuleGetters(lazy, { ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs", ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs", }); }); const { DefaultMap, ExtensionError } = ExtensionUtils; const { StartupCache } = ExtensionParent; const { StartupCache } = ExtensionParent; // DNR Rules store subdirectory/file names and file extensions. // DNR Rules store subdirectory/file names and file extensions. Loading Loading @@ -79,6 +85,63 @@ class StoreData { } } } } class Queue { #tasks = []; #runningTask = null; #closed = false; get hasPendingTasks() { return !!this.#runningTask || !!this.#tasks.length; } get isClosed() { return this.#closed; } async close() { if (this.#closed) { return; } const drainedQueuePromise = this.queueTask(() => {}); this.#closed = true; return drainedQueuePromise; } queueTask(callback) { if (this.#closed) { throw new Error("Unexpected queueTask call on closed queue"); } const deferred = lazy.PromiseUtils.defer(); this.#tasks.push({ callback, deferred }); // Run the queued task right away if there isn't one already running. if (!this.#runningTask) { this.#runNextTask(); } return deferred.promise; } async #runNextTask() { if (!this.#tasks.length) { this.#runningTask = null; return; } this.#runningTask = this.#tasks.shift(); const { callback, deferred } = this.#runningTask; try { let result = callback(); if (result instanceof Promise) { result = await result; } deferred.resolve(result); } catch (err) { deferred.reject(err); } this.#runNextTask(); } } /** /** * Class managing the rulesets persisted across browser sessions. * Class managing the rulesets persisted across browser sessions. * * Loading @@ -103,6 +166,8 @@ class RulesetsStore { this._dataPromises = new Map(); this._dataPromises = new Map(); // Map<extensionUUID, Promise<void>> // Map<extensionUUID, Promise<void>> this._savePromises = new Map(); this._savePromises = new Map(); // Map<extensionUUID, Queue> this._dataUpdateQueues = new DefaultMap(() => new Queue()); // Map<extensionUUID, { close: Function }> // Map<extensionUUID, { close: Function }> this._shutdownHandlers = new Map(); this._shutdownHandlers = new Map(); // Promise to await on to ensure the store parent directory exist // Promise to await on to ensure the store parent directory exist Loading Loading @@ -181,6 +246,27 @@ class RulesetsStore { return data?.staticRulesets; return data?.staticRulesets; } } /** * Update the enabled rulesets, queue changes to prevent races between calls * that may be triggered while an update is still in process. * * @param {Extension} extension * @param {object} params * @param {Array<string>} [params.disableRulesetIds=[]] * @param {Array<string>} [params.enableRulesetIds=[]] */ async updateEnabledStaticRulesets( extension, { disableRulesetIds, enableRulesetIds } ) { return this._dataUpdateQueues.get(extension.uuid).queueTask(() => { return this.#updateEnabledStaticRulesets(extension, { disableRulesetIds, enableRulesetIds, }); }); } /** /** * Return the store file path for the given the extension's uuid. * Return the store file path for the given the extension's uuid. * * Loading Loading @@ -227,7 +313,23 @@ class RulesetsStore { let shutdownHandler = this._shutdownHandlers.get(extensionUUID); let shutdownHandler = this._shutdownHandlers.get(extensionUUID); if (!shutdownHandler) { if (!shutdownHandler) { shutdownHandler = { shutdownHandler = { close: () => this.unloadData(extensionUUID), close: async () => { // Wait for the update tasks to have been executed, then unload the // data. const dataUpdateQueue = this._dataUpdateQueues.has(extensionUUID) ? this._dataUpdateQueues.get(extensionUUID) : undefined; if (dataUpdateQueue) { try { await dataUpdateQueue.close(); } catch (err) { // Unexpected error on closing the update queue. Cu.reportError(err); } this._dataUpdateQueues.delete(extensionUUID); } this.unloadData(extensionUUID); }, }; }; this._shutdownHandlers.set(extensionUUID, shutdownHandler); this._shutdownHandlers.set(extensionUUID, shutdownHandler); } } Loading @@ -247,6 +349,7 @@ class RulesetsStore { await savePromise; await savePromise; this._savePromises.delete(extensionUUID); this._savePromises.delete(extensionUUID); } } this._dataPromises.delete(extensionUUID); this._dataPromises.delete(extensionUUID); this._data.delete(extensionUUID); this._data.delete(extensionUUID); } } Loading Loading @@ -588,6 +691,80 @@ class RulesetsStore { this._savePromises.delete(extensionUUID); this._savePromises.delete(extensionUUID); } } } } /** * Internal implementation for updating the enabled rulesets and enforcing * static rulesets and rules count limits. * * @param {Extension} extension * @param {object} params * @param {Array<string>} [params.disableRulesetIds=[]] * @param {Array<string>} [params.enableRulesetIds=[]] */ async #updateEnabledStaticRulesets( extension, { disableRulesetIds, enableRulesetIds } ) { const ruleResources = extension.manifest.declarative_net_request?.rule_resources; if (!Array.isArray(ruleResources)) { return; } const enabledRulesets = await this.getEnabledStaticRulesets(extension); const updatedEnabledRulesets = new Map(); let disableIds = new Set(disableRulesetIds); let enableIds = new Set(enableRulesetIds); // valiate the ruleset ids for existence (which will also reject calls // including the reserved _session and _dynamic, because static rulesets // id are validated as part of the manifest validation and they are not // allowed to start with '_'). const existingIds = new Set(ruleResources.map(rs => rs.id)); const errorOnInvalidRulesetIds = rsIdSet => { for (const rsId of rsIdSet) { if (!existingIds.has(rsId)) { throw new ExtensionError(`Invalid ruleset id: "${rsId}"`); } } }; errorOnInvalidRulesetIds(disableIds); errorOnInvalidRulesetIds(enableIds); // Copy into the updatedEnabledRulesets Map any ruleset that is not // requested to be disabled or is enabled back in the same request. for (const [rulesetId, ruleset] of enabledRulesets) { if (!disableIds.has(rulesetId) || enableIds.has(rulesetId)) { updatedEnabledRulesets.set(rulesetId, ruleset); enableIds.delete(rulesetId); } } const { MAX_NUMBER_OF_ENABLED_STATIC_RULESETS } = lazy.ExtensionDNR.limits; const maxNewRulesetsCount = MAX_NUMBER_OF_ENABLED_STATIC_RULESETS - updatedEnabledRulesets.size; if (enableIds.size > maxNewRulesetsCount) { // Log an error for the developer. throw new ExtensionError( `updatedEnabledRulesets request is exceeding MAX_NUMBER_OF_ENABLED_STATIC_RULESETS` ); } const newRulesets = await this.#getManifestStaticRulesets( extension, Array.from(enableIds) ); for (const [rulesetId, ruleset] of newRulesets.entries()) { updatedEnabledRulesets.set(rulesetId, ruleset); } this._data.get(extension.uuid).staticRulesets = updatedEnabledRulesets; await this.save(extension); await this.updateRulesetManager(extension); } } } const store = new RulesetsStore(); const store = new RulesetsStore(); Loading Loading @@ -659,7 +836,9 @@ export const ExtensionDNRStore = { return store.clearOnUninstall(extensionUUID); return store.clearOnUninstall(extensionUUID); }, }, initExtension, initExtension, async updateEnabledStaticRulesets(extension, updateRulesetOptions) { await store.updateEnabledStaticRulesets(extension, updateRulesetOptions); }, // Test-only helpers // Test-only helpers _getStoreForTesting() { _getStoreForTesting() { requireTestOnlyCallers(); requireTestOnlyCallers(); Loading
toolkit/components/extensions/parent/ext-declarativeNetRequest.js +7 −0 Original line number Original line Diff line number Diff line Loading @@ -52,6 +52,13 @@ this.declarativeNetRequest = class extends ExtensionAPI { return ruleManager.enabledStaticRulesetIds; return ruleManager.enabledStaticRulesetIds; }, }, updateEnabledRulesets({ disableRulesetIds, enableRulesetIds }) { return ExtensionDNR.updateEnabledStaticRulesets(extension, { disableRulesetIds, enableRulesetIds, }); }, getSessionRules() { getSessionRules() { // ruleManager.getSessionRules() returns an array of Rule instances. // ruleManager.getSessionRules() returns an array of Rule instances. // When these are structurally cloned (to send them to the child), // When these are structurally cloned (to send them to the child), Loading
toolkit/components/extensions/schemas/declarative_net_request.json +31 −0 Original line number Original line Diff line number Diff line Loading @@ -475,6 +475,37 @@ } } ] ] }, }, { "name": "updateEnabledRulesets", "type": "function", "description": "Returns the ids for the current set of enabled static rulesets.", "async": "callback", "parameters": [ { "name": "updateRulesetOptions", "type": "object", "properties": { "disableRulesetIds": { "type": "array", "items": { "type": "string" }, "optional": true, "default": [] }, "enableRulesetIds": { "type": "array", "items": { "type": "string" }, "optional": true, "default": [] } } }, { "name": "callback", "type": "function", "parameters": [] } ] }, { { "name": "getSessionRules", "name": "getSessionRules", "type": "function", "type": "function", Loading
toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js +248 −8 Original line number Original line Diff line number Diff line Loading @@ -27,6 +27,19 @@ function backgroundWithDNRAPICallHandlers() { ) ) ); ); break; break; case "updateEnabledRulesets": // Run (one or more than one concurrently) updateEnabledRulesets calls // and report back the results. result = await Promise.all( args.map(arg => { return browser.declarativeNetRequest .updateEnabledRulesets(arg) .catch(err => { return { rejectedWithErrorMessage: err.message }; }); }) ); break; default: default: browser.test.fail(`Unexpected test message: ${msg}`); browser.test.fail(`Unexpected test message: ${msg}`); return; return; Loading Loading @@ -142,6 +155,18 @@ const assertDNRTestMatchOutcome = async ( ); ); }; }; const assertDNRGetEnabledRulesets = async ( extensionTestWrapper, expectedRulesetIds ) => { extensionTestWrapper.sendMessage("getEnabledRulesets"); Assert.deepEqual( await extensionTestWrapper.awaitMessage("getEnabledRulesets:done"), expectedRulesetIds, "Got the expected enabled ruleset ids from dnr.getEnabledRulesets API method" ); }; const assertDNRStoreData = async ( const assertDNRStoreData = async ( dnrStore, dnrStore, extensionTestWrapper, extensionTestWrapper, Loading @@ -160,13 +185,6 @@ const assertDNRStoreData = async ( return acc; return acc; }, new Map()); }, new Map()); extensionTestWrapper.sendMessage("getEnabledRulesets"); Assert.deepEqual( await extensionTestWrapper.awaitMessage("getEnabledRulesets:done"), expectedRulesetIds, "Got the expected enabled ruleset ids from dnr.getEnabledRulesets API method" ); ok( ok( dnrStore._dataPromises.has(extUUID), dnrStore._dataPromises.has(extUUID), "Got promise for the test extension DNR data being loaded" "Got promise for the test extension DNR data being loaded" Loading Loading @@ -267,6 +285,8 @@ add_task(async function test_load_static_rules() { const dnrStore = ExtensionDNRStore._getStoreForTesting(); const dnrStore = ExtensionDNRStore._getStoreForTesting(); info("Verify DNRStore data for the test extension"); info("Verify DNRStore data for the test extension"); await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_2"]); await assertDNRStoreData(dnrStore, extension, { await assertDNRStoreData(dnrStore, extension, { ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), Loading Loading @@ -338,6 +358,8 @@ add_task(async function test_load_static_rules() { await addon.enable(); await addon.enable(); await extension.awaitMessage("bgpage:ready"); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_2"]); await assertDNRStoreData(dnrStore, extension, { await assertDNRStoreData(dnrStore, extension, { ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), Loading Loading @@ -377,6 +399,7 @@ add_task(async function test_load_static_rules() { }) }) ); ); await extension.awaitMessage("bgpage:ready"); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets(extension, ["ruleset_2"]); await assertDNRStoreData(dnrStore, extension, { await assertDNRStoreData(dnrStore, extension, { ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), }); }); Loading Loading @@ -416,6 +439,7 @@ add_task(async function test_load_static_rules() { }) }) ); ); await extension.awaitMessage("bgpage:ready"); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); await assertDNRStoreData(dnrStore, extension, { await assertDNRStoreData(dnrStore, extension, { ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), }); }); Loading Loading @@ -443,6 +467,7 @@ add_task(async function test_load_static_rules() { }) }) ); ); await extension.awaitMessage("bgpage:ready"); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets(extension, []); await assertDNRStoreData(dnrStore, extension, {}); await assertDNRStoreData(dnrStore, extension, {}); info("Verify matched rules using testMatchOutcome"); info("Verify matched rules using testMatchOutcome"); Loading Loading @@ -672,6 +697,137 @@ add_task(async function test_ruleset_validation() { } } }); }); add_task(async function test_updateEnabledRuleset_id_validation() { const rule_resources = [ { id: "ruleset_1", enabled: true, path: "ruleset_1.json", }, { id: "ruleset_2", enabled: false, path: "ruleset_2.json", }, ]; const ruleset1Data = [ getDNRRule({ action: { type: "allow" }, condition: { resourceTypes: ["main_frame"] }, }), ]; const ruleset2Data = [ getDNRRule({ action: { type: "block" }, condition: { resourceTypes: ["main_frame", "script"] }, }), ]; const files = { "ruleset_1.json": JSON.stringify(ruleset1Data), "ruleset_2.json": JSON.stringify(ruleset2Data), }; let extension = ExtensionTestUtils.loadExtension( getDNRExtension({ rule_resources, files }) ); await extension.startup(); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); const dnrStore = ExtensionDNRStore._getStoreForTesting(); await assertDNRStoreData(dnrStore, extension, { ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), }); const invalidStaticRulesetIds = [ // The following two are reserved for session and dynamic rules. "_session", "_dynamic", "ruleset_non_existing", ]; for (const invalidRSId of invalidStaticRulesetIds) { extension.sendMessage( "updateEnabledRulesets", // Only in rulesets to be disabled. { disableRulesetIds: [invalidRSId] }, // Only in rulesets to be enabled. { enableRulesetIds: [invalidRSId] }, // In both rulesets to be enabled and disabled. { disableRulesetIds: [invalidRSId], enableRulesetIds: [invalidRSId] }, // Along with existing rulesets (and expected the existing rulesets // to stay unchanged due to the invalid ruleset ids.) { disableRulesetIds: [invalidRSId, "ruleset_1"], enableRulesetIds: [invalidRSId, "ruleset_2"], } ); const [ resInDisable, resInEnable, resInEnableAndDisable, resInSameRequestAsValid, ] = await extension.awaitMessage("updateEnabledRulesets:done"); await Assert.rejects( Promise.reject(resInDisable?.rejectedWithErrorMessage), new RegExp(`Invalid ruleset id: "${invalidRSId}"`), `Got the expected rejection on invalid ruleset id "${invalidRSId}" in disableRulesetIds` ); await Assert.rejects( Promise.reject(resInEnable?.rejectedWithErrorMessage), new RegExp(`Invalid ruleset id: "${invalidRSId}"`), `Got the expected rejection on invalid ruleset id "${invalidRSId}" in enableRulesetIds` ); await Assert.rejects( Promise.reject(resInEnableAndDisable?.rejectedWithErrorMessage), new RegExp(`Invalid ruleset id: "${invalidRSId}"`), `Got the expected rejection on invalid ruleset id "${invalidRSId}" in both enable/disableRulesetIds` ); await Assert.rejects( Promise.reject(resInSameRequestAsValid?.rejectedWithErrorMessage), new RegExp(`Invalid ruleset id: "${invalidRSId}"`), `Got the expected rejection on invalid ruleset id "${invalidRSId}" along with valid ruleset ids` ); } // Confirm that the expected rulesets didn't change neither. await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); await assertDNRStoreData(dnrStore, extension, { ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), }); // - List the same ruleset ids more than ones is expected to work and // to be resulting in the same set of rules being enabled // - Disabling and Enabling the same ruleset id should result in the // ruleset being enabled. await extension.sendMessage("updateEnabledRulesets", { disableRulesetIds: [ "ruleset_1", "ruleset_1", "ruleset_2", "ruleset_2", "ruleset_2", ], enableRulesetIds: ["ruleset_2", "ruleset_2"], }); Assert.deepEqual( await extension.awaitMessage("updateEnabledRulesets:done"), [undefined], "Expect the updateEnabledRulesets to result successfully" ); await assertDNRGetEnabledRulesets(extension, ["ruleset_2"]); await assertDNRStoreData(dnrStore, extension, { ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), }); await extension.unload(); }); add_task(async function test_static_rulesets_limits() { add_task(async function test_static_rulesets_limits() { const dnrStore = ExtensionDNRStore._getStoreForTesting(); const dnrStore = ExtensionDNRStore._getStoreForTesting(); Loading Loading @@ -719,13 +875,14 @@ add_task(async function test_static_rulesets_limits() { }) }) ); ); const expectedEnabledRulesets = {}; const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { ExtensionTestUtils.failOnSchemaWarnings(false); ExtensionTestUtils.failOnSchemaWarnings(false); await extension.startup(); await extension.startup(); ExtensionTestUtils.failOnSchemaWarnings(true); ExtensionTestUtils.failOnSchemaWarnings(true); await extension.awaitMessage("bgpage:ready"); await extension.awaitMessage("bgpage:ready"); const expectedEnabledRulesets = {}; for (let i = 0; i < MAX_NUMBER_OF_ENABLED_STATIC_RULESETS; i++) { for (let i = 0; i < MAX_NUMBER_OF_ENABLED_STATIC_RULESETS; i++) { expectedEnabledRulesets[`ruleset_${i}`] = getSchemaNormalizedRules( expectedEnabledRulesets[`ruleset_${i}`] = getSchemaNormalizedRules( extension, extension, Loading @@ -733,6 +890,10 @@ add_task(async function test_static_rulesets_limits() { ); ); } } await assertDNRGetEnabledRulesets( extension, Array.from(Object.keys(expectedEnabledRulesets)) ); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); }); }); Loading @@ -753,5 +914,84 @@ add_task(async function test_static_rulesets_limits() { ], ], }); }); info( "Verify updateEnabledRulesets reject when the request is exceeding the enabled rulesets count limit" ); extension.sendMessage("updateEnabledRulesets", { disableRulesetIds: ["ruleset_0"], enableRulesetIds: ["ruleset_10", "ruleset_11"], }); await Assert.rejects( extension.awaitMessage("updateEnabledRulesets:done").then(results => { if (results[0].rejectedWithErrorMessage) { return Promise.reject(new Error(results[0].rejectedWithErrorMessage)); } return results[0]; }), /updatedEnabledRulesets request is exceeding MAX_NUMBER_OF_ENABLED_STATIC_RULESETS/, "Expected rejection on updateEnabledRulesets exceeting enabled rulesets count limit" ); // Confirm that the expected rulesets didn't change neither. await assertDNRGetEnabledRulesets( extension, Array.from(Object.keys(expectedEnabledRulesets)) ); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); info( "Verify updateEnabledRulesets applies the expected changes when resolves successfully" ); extension.sendMessage( "updateEnabledRulesets", { disableRulesetIds: ["ruleset_0"], enableRulesetIds: ["ruleset_10"], }, { disableRulesetIds: ["ruleset_10"], enableRulesetIds: ["ruleset_11"], } ); await extension.awaitMessage("updateEnabledRulesets:done"); // Expect ruleset_0 disabled, ruleset_10 to be enabled but then disabled by the // second update queued after the first one, and ruleset_11 to be enabled. delete expectedEnabledRulesets.ruleset_0; expectedEnabledRulesets.ruleset_11 = getSchemaNormalizedRules( extension, rules ); await assertDNRGetEnabledRulesets( extension, Array.from(Object.keys(expectedEnabledRulesets)) ); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); // Ensure all changes were stored and reloaded from disk store and the // DNR store update queue can accept new updates. info("Verify static rules load and updates after extension is restarted"); await AddonTestUtils.promiseRestartManager(); await extension.awaitStartup(); await extension.awaitMessage("bgpage:ready"); await assertDNRGetEnabledRulesets( extension, Array.from(Object.keys(expectedEnabledRulesets)) ); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); extension.sendMessage("updateEnabledRulesets", { disableRulesetIds: ["ruleset_11"], }); await extension.awaitMessage("updateEnabledRulesets:done"); delete expectedEnabledRulesets.ruleset_11; await assertDNRGetEnabledRulesets( extension, Array.from(Object.keys(expectedEnabledRulesets)) ); await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); await extension.unload(); await extension.unload(); }); });