commit 4d0bfd13bc758feddb3c76c28ecc4465028785a6 parent 8e23478e0e3ae93c58a9a512679b5b570576f38e Author: Thomas Wisniewski <twisniewski@mozilla.com> Date: Tue, 7 Oct 2025 15:50:02 +0000 Bug 1988056 - add support for using RemoteSettings to deliver updates to our WebCompat interventions; r=denschub,leplatrem,webcompat-reviewers Differential Revision: https://phabricator.services.mozilla.com/D264817 Diffstat:
15 files changed, 843 insertions(+), 154 deletions(-)
diff --git a/browser/extensions/webcompat/data/shims.js b/browser/extensions/webcompat/data/shims.js @@ -8,127 +8,6 @@ const AVAILABLE_SHIMS = [ { - hiddenInAboutCompat: true, - id: "LiveTestShim", - platform: "all", - name: "Live test shim", - bug: "livetest", - file: "live-test-shim.js", - matches: ["*://webcompat-addon-testbed.herokuapp.com/shims_test.js"], - needsShimHelpers: ["getOptions", "optIn"], - }, - { - hiddenInAboutCompat: true, - id: "MochitestShim", - platform: "all", - branch: ["all:ignoredOtherPlatform"], - name: "Test shim for Mochitests", - bug: "mochitest", - file: "mochitest-shim-1.js", - matches: [ - "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test.js", - ], - needsShimHelpers: ["getOptions", "optIn"], - options: { - simpleOption: true, - complexOption: { a: 1, b: "test" }, - branchValue: { value: true, branches: [] }, - platformValue: { value: true, platform: "neverUsed" }, - }, - unblocksOnOptIn: ["*://trackertest.org/*"], - }, - { - hiddenInAboutCompat: true, - disabled: true, - id: "MochitestShim2", - platform: "all", - name: "Test shim for Mochitests (disabled by default)", - bug: "mochitest", - file: "mochitest-shim-2.js", - matches: [ - "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_2.js", - ], - needsShimHelpers: ["getOptions", "optIn"], - options: { - simpleOption: true, - complexOption: { a: 1, b: "test" }, - branchValue: { value: true, branches: [] }, - platformValue: { value: true, platform: "neverUsed" }, - }, - unblocksOnOptIn: ["*://trackertest.org/*"], - }, - { - hiddenInAboutCompat: true, - id: "MochitestShim3", - platform: "all", - name: "Test shim for Mochitests (host)", - bug: "mochitest", - file: "mochitest-shim-3.js", - notHosts: ["example.com"], - matches: [ - "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", - ], - }, - { - hiddenInAboutCompat: true, - id: "MochitestShim4", - platform: "all", - name: "Test shim for Mochitests (notHost)", - bug: "mochitest", - file: "mochitest-shim-3.js", - hosts: ["example.net"], - matches: [ - "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", - ], - }, - { - hiddenInAboutCompat: true, - id: "MochitestShim5", - platform: "all", - name: "Test shim for Mochitests (branch)", - bug: "mochitest", - file: "mochitest-shim-3.js", - branches: ["never matches"], - matches: [ - "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", - ], - }, - { - hiddenInAboutCompat: true, - id: "MochitestShim6", - platform: "never matches", - name: "Test shim for Mochitests (platform)", - bug: "mochitest", - file: "mochitest-shim-3.js", - matches: [ - "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", - ], - }, - { - hiddenInAboutCompat: true, - id: "EmbedTestShim", - platform: "desktop", - name: "Test shim for smartblock embed unblocking", - bug: "1892175", - runFirst: "embed-test-shim.js", - // Blank stub file just so we run the script above when the matched script - // files get blocked. - file: "empty-script.js", - matches: [ - "https://itisatracker.org/browser/browser/extensions/webcompat/tests/browser/embed_test.js", - ], - // Use instagram logo as an example - logos: ["instagram.svg"], - needsShimHelpers: [ - "embedClicked", - "smartblockEmbedReplaced", - "smartblockGetFluentString", - ], - isSmartblockEmbedShim: true, - onlyIfBlockedByETP: true, - unblocksOnOptIn: ["*://itisatracker.org/*"], - }, - { id: "AddThis", platform: "all", name: "AddThis", diff --git a/browser/extensions/webcompat/experiment-apis/remoteSettings.js b/browser/extensions/webcompat/experiment-apis/remoteSettings.js @@ -0,0 +1,134 @@ +/* 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/. */ + +"use strict"; + +/* global ExtensionAPI, ExtensionCommon */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs", +}); + +this.remoteSettings = class RemoteSettingsAPI extends ExtensionAPI { + static COLLECTION = "webcompat-interventions"; + + getAPI(context) { + const EventManager = ExtensionCommon.EventManager; + const { uuid } = context.extension; + const missingFilesCache = {}; + + async function missingFiles(obj, basePath) { + if (!obj) { + return false; + } + for (let path of Array.isArray(obj) ? obj : [obj]) { + path = path.includes("/") ? path : `${basePath}/${path}`; + if (path in missingFilesCache) { + if (missingFilesCache[path]) { + return true; + } + continue; + } + const url = `moz-extension://${uuid}/${path}`; + missingFilesCache[path] = await new Promise(done => { + const req = new lazy.ServiceRequest(); + req.responseType = "text"; + req.onload = () => done(false); + req.onerror = () => done(true); + req.open("GET", url); + req.send(); + }); + if (missingFilesCache[path]) { + return true; + } + } + return false; + } + + async function missingFilesInIntervention(desc) { + for (let intervention of desc.interventions) { + const { content_scripts } = intervention; + if ( + (await missingFiles(content_scripts?.js, "injections/js")) || + (await missingFiles(content_scripts?.css, "injections/css")) + ) { + return true; + } + } + return false; + } + + async function missingFilesInShim(desc) { + if (desc.file) { + if (await missingFiles([desc.file], "shims")) { + return true; + } + } + if (desc.logos) { + if (await missingFiles(desc.logos, "shims")) { + return true; + } + } + const { contentScripts } = desc; + if (contentScripts) { + for (let { css, js } of contentScripts) { + if (await missingFiles(css, "shims")) { + return true; + } + if (await missingFiles(js, "shims")) { + return true; + } + } + } + return false; + } + + async function markMissingFiles(update) { + if (update?.interventions) { + for (let desc of Object.values(update.interventions)) { + desc.isMissingFiles = await missingFilesInIntervention(desc); + } + } + if (update?.shims) { + for (let desc of Object.values(update.shims)) { + desc.isMissingFiles = await missingFilesInShim(desc); + } + } + return update; + } + + return { + remoteSettings: { + onRemoteSettingsUpdate: new EventManager({ + context, + name: "remoteSettings.onRemoteSettingsUpdate", + register: fire => { + const callback = ({ data: { current } }) => { + return markMissingFiles(current[0]).then(update => { + fire.async(update).catch(() => {}); // ignore Message Manager disconnects + }); + }; + lazy + .RemoteSettings(RemoteSettingsAPI.COLLECTION) + .on("sync", callback); + return () => { + lazy + .RemoteSettings(RemoteSettingsAPI.COLLECTION) + .off("sync", callback); + }; + }, + }).api(), + async get() { + const current = await lazy + .RemoteSettings(RemoteSettingsAPI.COLLECTION) + .get(); + return await markMissingFiles(current[0]); + }, + }, + }; + } +}; diff --git a/browser/extensions/webcompat/experiment-apis/remoteSettings.json b/browser/extensions/webcompat/experiment-apis/remoteSettings.json @@ -0,0 +1,28 @@ +[ + { + "namespace": "remoteSettings", + "description": "experimental API extension to allow access to the Remote Settings API", + "events": [ + { + "name": "onRemoteSettingsUpdate", + "type": "function", + "parameters": [ + { + "name": "update", + "type": "object", + "description": "The update payload" + } + ] + } + ], + "functions": [ + { + "name": "get", + "type": "function", + "description": "Get the current collection from Remote Settings (if any)", + "parameters": [], + "async": true + } + ] + } +] diff --git a/browser/extensions/webcompat/lib/intervention_helpers.js b/browser/extensions/webcompat/lib/intervention_helpers.js @@ -310,6 +310,7 @@ var InterventionHelpers = { not_channels, only_channels, skip_if, + ua_string, } = intervention; if (firefoxChannel) { if (only_channels && !only_channels.includes(firefoxChannel)) { @@ -333,9 +334,19 @@ var InterventionHelpers = { return true; } } + if (ua_string) { + for (let ua of Array.isArray(ua_string) ? ua_string : [ua_string]) { + if (!InterventionHelpers.ua_change_functions[ua]) { + return true; + } + } + } if (skip_if) { try { - if (this.skip_if_functions[skip_if]?.()) { + if ( + !this.skip_if_functions[skip_if] || + this.skip_if_functions[skip_if]?.() + ) { return true; } } catch (e) { @@ -343,6 +354,35 @@ var InterventionHelpers = { `Error while checking skip-if condition ${skip_if} for bug ${bug}:`, e ); + return true; + } + } + return false; + }, + + nonCustomInterventionKeys: Object.freeze( + new Set([ + "content_scripts", + "enabled", + "max_version", + "min_version", + "not_platforms", + "platforms", + "not_channels", + "only_channels", + "pref_check", + "skip_if", + "ua_string", + ]) + ), + + isMissingCustomFunctions(intervention, customFunctionNames) { + for (let key of Object.keys(intervention)) { + if ( + !InterventionHelpers.nonCustomInterventionKeys.has(key) && + !customFunctionNames.has(key) + ) { + return true; } } return false; diff --git a/browser/extensions/webcompat/lib/interventions.js b/browser/extensions/webcompat/lib/interventions.js @@ -8,17 +8,16 @@ class Interventions { constructor(availableInterventions, customFunctions) { + this._originalInterventions = availableInterventions; + this.INTERVENTION_PREF = "perform_injections"; this._interventionsEnabled = true; this._readyPromise = new Promise(done => (this._resolveReady = done)); - this._availableInterventions = Object.entries(availableInterventions).map( - ([id, obj]) => { - obj.id = id; - return obj; - } + this._availableInterventions = this._reformatSourceJSON( + availableInterventions ); this._customFunctions = customFunctions; @@ -26,6 +25,31 @@ class Interventions { this._contentScriptsPerIntervention = new Map(); } + _reformatSourceJSON(availableInterventions) { + return Object.entries(availableInterventions).map(([id, obj]) => { + obj.id = id; + return obj; + }); + } + + async onRemoteSettingsUpdate(updatedInterventions) { + const oldReadyPromise = this._readyPromise; + this._readyPromise = new Promise(done => (this._resolveReady = done)); + await oldReadyPromise; + this._updateInterventions(updatedInterventions); + } + + async _updateInterventions(updatedInterventions) { + await this.disableInterventions(); + this._availableInterventions = + this._reformatSourceJSON(updatedInterventions); + await this.enableInterventions(); + } + + async _resetToDefaultInterventions() { + await this._updateInterventions(this._originalInterventions); + } + ready() { return this._readyPromise; } @@ -157,6 +181,9 @@ class Interventions { } async _enableInterventionsNow(whichInterventions) { + // resolveReady may change while we're updating + const resolveReady = this._resolveReady; + const skipped = []; const channel = await browser.appConstants.getEffectiveUpdateChannel(); @@ -168,7 +195,16 @@ class Interventions { const os = await InterventionHelpers.getOS(); this.currentPlatform = os; + const customFunctionNames = new Set(Object.keys(this._customFunctions)); + for (const config of whichInterventions) { + config.active = false; + + if (config.isMissingFiles) { + skipped.push(config.label); + continue; + } + for (const intervention of config.interventions) { intervention.enabled = false; if (!(await this._check_for_needed_prefs(intervention))) { @@ -186,6 +222,14 @@ class Interventions { if (!(await InterventionHelpers.checkPlatformMatches(intervention))) { continue; } + if ( + InterventionHelpers.isMissingCustomFunctions( + intervention, + customFunctionNames + ) + ) { + continue; + } intervention.enabled = true; config.availableOnPlatform = true; } @@ -217,7 +261,7 @@ class Interventions { this._aboutCompatBroker.filterInterventions(whichInterventions), }); - this._resolveReady(); + resolveReady(); } async enableIntervention(config) { diff --git a/browser/extensions/webcompat/lib/remote_settings_update_check.js b/browser/extensions/webcompat/lib/remote_settings_update_check.js @@ -0,0 +1,82 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +/* globals browser, module, onMessageFromTab */ + +let currentVersion = browser.runtime.getManifest().version; + +function isUpdateWanted(update) { + if (!update) { + return false; + } + + if (!update?.version || (!update?.interventions && !update.shims)) { + console.error( + "Received invalid WebCompat interventions update from Remote Settings", + update + ); + return false; + } + + if (!this.isNewerVersion(update.version, currentVersion)) { + console.error( + "Ignoring latest WebCompat Remote Settings update", + update.version, + "<=", + currentVersion + ); + return false; + } + + return true; +} + +function isNewerVersion(a_raw, b_raw) { + function num(n) { + const i = parseInt(n); + return isNaN(i) ? 0 : i; + } + const a_comp = a_raw.split("."); + const b_comp = b_raw.split("."); + const a = [num(a_comp[0]), num(a_comp[1]), num(a_comp[2]), num(a_comp[3])]; + const b = [num(b_comp[0]), num(b_comp[1]), num(b_comp[2]), num(b_comp[3])]; + for (let i = 0; i < 4; ++i) { + if (a[i] > b[i]) { + return true; + } + if (a[i] < b[i]) { + return false; + } + } + return false; +} + +function listenForRemoteSettingsUpdates(interventions, shims) { + browser.remoteSettings.onRemoteSettingsUpdate.addListener(async update => { + if (!isUpdateWanted(update)) { + console.info( + "Ignoring older version of webcompat interventions", + update.version + ); + return; + } + + console.info("Received update to webcompat interventions", update.version); + currentVersion = update.version; + + if (update.interventions) { + await interventions.onRemoteSettingsUpdate(update.interventions); + } + + if (update.shims) { + await shims.onRemoteSettingsUpdate(update.shims); + } + }); + + window._downgradeForTesting = async () => { + currentVersion = browser.runtime.getManifest().version; + await interventions._resetToDefaultInterventions(); + await shims._resetToDefaultShims(); + }; +} diff --git a/browser/extensions/webcompat/lib/shims.js b/browser/extensions/webcompat/lib/shims.js @@ -47,6 +47,7 @@ class Shim { this.hiddenInAboutCompat = opts.hiddenInAboutCompat; this.hosts = opts.hosts; this.id = opts.id; + this.isMissingFiles = opts.isMissingFiles; this.logos = opts.logos || []; this.matches = []; this.name = opts.name; @@ -173,6 +174,10 @@ class Shim { } get enabled() { + if (this.isMissingFiles) { + return false; + } + if (this._disabledGlobally || this._disabledForSession) { return false; } @@ -193,6 +198,10 @@ class Shim { } get disabledReason() { + if (this.isMissingFiles) { + return "missingFiles"; + } + if (this._disabledGlobally) { return "globalPref"; } @@ -563,12 +572,15 @@ class Shim { class Shims { constructor(availableShims) { + this._originalShims = availableShims; + if (!browser.trackingProtection) { console.error("Required experimental add-on APIs for shims unavailable"); return; } - this._readyPromise = this._registerShims(availableShims); + this._readyPromise = new Promise(done => (this._resolveReady = done)); + this._registerShims(availableShims); onMessageFromTab(this._onMessageFromShim.bind(this)); @@ -687,11 +699,35 @@ class Shims { return shims; } + async onRemoteSettingsUpdate(updatedShims) { + const oldReadyPromise = this._readyPromise; + this._readyPromise = new Promise(done => (this._resolveReady = done)); + await oldReadyPromise; + this._updateShims(updatedShims); + } + + async _updateShims(updatedShims) { + await this._unregisterShims(); + this._registerShims(updatedShims); + this._checkEnabledPref(); + await this.ready(); + } + + async _resetToDefaultShims() { + await this._updateShims(this._originalShims); + } + _registerShims(shims) { if (this.shims) { throw new Error("_registerShims has already been called"); } + this._registeredShimListeners = []; + const registerShimListener = (api, listener, ...args) => { + api.addListener(listener, ...args); + this._registeredShimListeners.push([api, listener]); + }; + this.shims = new Map(); for (const shimOpts of shims) { const { id } = shimOpts; @@ -703,7 +739,9 @@ class Shims { // Register onBeforeRequest listener which handles storage access requests // on matching redirects. let redirectTargetUrls = Array.from(shims.values()) - .filter(shim => shim.requestStorageAccessForRedirect) + .filter( + shim => !shim.isMissingFiles && shim.requestStorageAccessForRedirect + ) .flatMap(shim => shim.requestStorageAccessForRedirect) .map(([, dstUrl]) => dstUrl); @@ -714,7 +752,8 @@ class Shims { debug("Registering redirect listener for requestStorageAccess helper", { redirectTargetUrls, }); - browser.webRequest.onBeforeRequest.addListener( + registerShimListener( + browser.webRequest.onBeforeRequest, this._onRequestStorageAccessRedirect.bind(this), { urls: redirectTargetUrls, types: ["main_frame"] }, ["blocking"] @@ -735,6 +774,9 @@ class Shims { const allHeaderChangingMatchTypePatterns = new Map(); const allLogos = []; for (const shim of this.shims.values()) { + if (shim.isMissingFiles) { + continue; + } const { logos, matches } = shim; allLogos.push(...logos); for (const { patterns, target, types } of matches || []) { @@ -759,13 +801,14 @@ class Shims { shim.setActiveOnTab(tabId, false); } }; - browser.tabs.onRemoved.addListener(unmarkShimsActive); - browser.tabs.onUpdated.addListener((tabId, changeInfo) => { + registerShimListener(browser.tabs.onRemoved, unmarkShimsActive); + registerShimListener(browser.tabs.onUpdated, (tabId, changeInfo) => { if (changeInfo.discarded || changeInfo.url) { unmarkShimsActive(tabId); } }); - browser.webRequest.onBeforeRequest.addListener( + registerShimListener( + browser.webRequest.onBeforeRequest, this._redirectLogos.bind(this), { urls, types: ["image"] }, ["blocking"] @@ -779,12 +822,14 @@ class Shims { ] of allHeaderChangingMatchTypePatterns.entries()) { const urls = Array.from(patterns); debug("Shimming these", type, "URLs:", urls); - browser.webRequest.onBeforeSendHeaders.addListener( + registerShimListener( + browser.webRequest.onBeforeSendHeaders, this._onBeforeSendHeaders.bind(this), { urls, types: [type] }, ["blocking", "requestHeaders"] ); - browser.webRequest.onHeadersReceived.addListener( + registerShimListener( + browser.webRequest.onHeadersReceived, this._onHeadersReceived.bind(this), { urls, types: [type] }, ["blocking", "responseHeaders"] @@ -801,7 +846,8 @@ class Shims { const urls = Array.from(patterns); debug("Shimming these", type, "URLs:", urls); - browser.webRequest.onBeforeRequest.addListener( + registerShimListener( + browser.webRequest.onBeforeRequest, this._ensureShimForRequestOnTab.bind(this), { urls, types: [type] }, ["blocking"] @@ -809,6 +855,17 @@ class Shims { } } + _unregisterShims() { + this.enabled = false; + if (this._registeredShimListeners) { + for (let [api, listener] of this._registeredShimListeners) { + api.removeListener(listener); + } + this._registeredShimListeners = undefined; + } + this.shims = undefined; + } + async _checkEnabledPref() { await browser.aboutConfigPrefs.getPref(this.ENABLED_PREF).then(value => { if (value === undefined) { @@ -830,6 +887,8 @@ class Shims { return; } + // resolveReady may change while we're updating + const resolveReady = this._resolveReady; this._enabled = enabled; for (const shim of this.shims.values()) { @@ -839,6 +898,7 @@ class Shims { shim.onAllShimsDisabled(); } } + resolveReady(); } async _checkSmartblockEmbedsEnabledPref() { diff --git a/browser/extensions/webcompat/manifest.json b/browser/extensions/webcompat/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "Web Compatibility Interventions", "description": "Urgent post-release fixes for web compatibility.", - "version": "145.5.0", + "version": "145.6.0", "browser_specific_settings": { "gecko": { "id": "webcompat@mozilla.org", @@ -48,6 +48,14 @@ "paths": [["matchPatterns"]] } }, + "remoteSettings": { + "schema": "experiment-apis/remoteSettings.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experiment-apis/remoteSettings.js", + "paths": [["remoteSettings"]] + } + }, "systemManufacturer": { "schema": "experiment-apis/systemManufacturer.json", "child": { @@ -81,6 +89,7 @@ "background": { "scripts": [ + "lib/remote_settings_update_check.js", "lib/messaging_helper.js", "lib/ua_helpers.js", "lib/intervention_helpers.js", diff --git a/browser/extensions/webcompat/run.js b/browser/extensions/webcompat/run.js @@ -4,8 +4,9 @@ "use strict"; -/* globals AboutCompatBroker, AVAILABLE_SHIMS, - CUSTOM_FUNCTIONS, Interventions, Shims */ +/* globals AboutCompatBroker, AVAILABLE_SHIMS, CUSTOM_FUNCTIONS, + listenForRemoteSettingsUpdates, + Interventions, Shims */ var interventions, shims; @@ -36,3 +37,5 @@ try { } catch (e) { console.error("about:compat broker failed to start", e); } + +listenForRemoteSettingsUpdates(interventions, shims); diff --git a/browser/extensions/webcompat/tests/browser/browser.toml b/browser/extensions/webcompat/tests/browser/browser.toml @@ -25,6 +25,8 @@ skip-if = ["debug"] # disabled until bug 1961939 is fixed. ["browser_pref_check.js"] +["browser_remote_settings_updates.js"] + ["browser_shims.js"] https_first_disabled = true diff --git a/browser/extensions/webcompat/tests/browser/browser_remote_settings_updates.js b/browser/extensions/webcompat/tests/browser/browser_remote_settings_updates.js @@ -0,0 +1,237 @@ +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +add_setup(async function () { + // We don't send events or call official addon APIs while running + // these tests, so there a good chance that test-verify mode may + // end up seeing the addon as "idle". This pref should avoid that. + await SpecialPowers.pushPrefEnv({ + set: [["extensions.background.idle.timeout", 300_000]], + }); +}); + +function getUpdatePayload(version, extra) { + const intervention1Id = `intervention1${extra}`; + const intervention2Id = `intervention2${extra}`; + const intervention3Id = `intervention3${extra}`; + const intervention4Id = `intervention4${extra}`; + const intervention5Id = `intervention5${extra}`; + const shim1Id = `shim1${extra}`; + const shim2Id = `shim2${extra}`; + const shim3Id = `shim3${extra}`; + const shim4Id = `shim4${extra}`; + return { + id: 1, // RemoteSettings record id", + last_modified: 1368273600000, + version, + interventions: { + [intervention1Id]: { + label: intervention1Id, + bugs: { + [intervention1Id]: { + issue: "broken-audio", + matches: ["https://example.com/*"], + }, + }, + interventions: [ + { + platforms: ["all"], + ua_string: ["Chrome"], + }, + ], + }, + [intervention2Id]: { + label: intervention2Id, + bugs: { + [intervention2Id]: { + issue: "broken-audio", + matches: ["https://example.com/*"], + }, + }, + interventions: [ + { + platforms: ["all"], + content_scripts: { + js: ["no-such-file.js"], + }, + }, + ], + }, + [intervention3Id]: { + label: intervention3Id, + bugs: { + [intervention3Id]: { + issue: "broken-audio", + matches: ["https://example.com/*"], + }, + }, + interventions: [ + { + platforms: ["all"], + missing_custom_func: [{}], + }, + ], + }, + [intervention4Id]: { + label: intervention4Id, + bugs: { + [intervention4Id]: { + issue: "broken-audio", + matches: ["https://example.com/*"], + }, + }, + interventions: [ + { + platforms: ["all"], + skip_if: ["missing_skip_checker"], + }, + ], + }, + [intervention5Id]: { + label: intervention5Id, + bugs: { + [intervention5Id]: { + issue: "broken-audio", + matches: ["https://example.com/*"], + }, + }, + interventions: [ + { + platforms: ["all"], + ua_string: ["missing_UA_override"], + }, + ], + }, + }, + shims: [ + { + id: shim1Id, + platform: "all", + name: shim1Id, + bug: shim1Id, + file: "empty-script.js", + matches: ["*://example.com/*js*"], + onlyIfBlockedByETP: true, + }, + { + id: shim2Id, + platform: "all", + name: shim2Id, + bug: shim2Id, + file: "no-such-shim.js", + matches: ["*://example.com/*js*"], + onlyIfBlockedByETP: true, + }, + { + id: shim3Id, + name: shim3Id, + bug: shim3Id, + contentScripts: [ + { + js: "no-such-shim2.js", + matches: ["*://example.com/*"], + }, + ], + onlyIfDFPIActive: true, + }, + { + id: shim4Id, + name: shim4Id, + bug: shim4Id, + logos: ["no-such-shim3.svg"], + matches: ["*://example.com/*"], + onlyIfDFPIActive: true, + isSmartblockEmbedShim: true, + onlyIfBlockedByETP: true, + unblocksOnOptIn: ["*://example.net/*"], + }, + ], + }; +} + +add_task(async function test_that_updates_work() { + await WebCompatExtension.started(); + + const client = RemoteSettings("webcompat-interventions"); + let update = getUpdatePayload("9999.9999.9999.9998", "update1"); + await client.emit("sync", { data: { current: [update] } }); + + await WebCompatExtension.interventionsReady(); + let interventions = await WebCompatExtension.availableInterventions(); + is(interventions.length, 5, "Correct number of interventions"); + is(interventions[0].id, "intervention1update1", "Correct intervention"); + is(interventions[0].active, true, "Intervention should be active"); + for (let i = 1; i < 5; ++i) { + is( + interventions[i].id, + `intervention${i + 1}update1`, + "Correct intervention" + ); + is( + interventions[i].active, + false, + `Intervention ${i} should not be active` + ); + } + await WebCompatExtension.shimsReady(); + let shims = await WebCompatExtension.availableShims(); + is(Object.entries(shims).length, 4, "Correct number of shims"); + is(shims[0].id, "shim1update1", "Correct shim"); + is(shims[0].enabled, true, "Shim should be enabled"); + for (let i = 1; i < 4; ++i) { + is(shims[i].id, `shim${i + 1}update1`, "Correct shim"); + is(shims[i].enabled, false, `Shim ${i} should not be enabled`); + } + + await WebCompatExtension.interventionsReady(); + await WebCompatExtension.shimsReady(); + update = getUpdatePayload("9999.9999.9999.9999", "update2"); + await client.emit("sync", { data: { current: [update] } }); + + await WebCompatExtension.interventionsReady(); + interventions = await WebCompatExtension.availableInterventions(); + is(interventions.length, 5, "Correct number of interventions"); + is(interventions[0].id, "intervention1update2", "Correct intervention"); + is(interventions[0].active, true, "Intervention should be active"); + for (let i = 1; i < 5; ++i) { + is( + interventions[i].id, + `intervention${i + 1}update2`, + "Correct intervention" + ); + is( + interventions[i].active, + false, + `Intervention ${i} should not be active` + ); + } + await WebCompatExtension.shimsReady(); + shims = await WebCompatExtension.availableShims(); + is(Object.entries(shims).length, 4, "Correct number of shims"); + is(shims[0].id, "shim1update2", "Correct shim"); + is(shims[0].enabled, true, "Shim should be enabled"); + for (let i = 1; i < 4; ++i) { + is(shims[i].id, `shim${i + 1}update2`, "Correct shim"); + is(shims[i].enabled, false, `Shim ${i} should not be enabled`); + } + + // check that we won't downgrade to older version numbers + await WebCompatExtension.interventionsReady(); + await WebCompatExtension.shimsReady(); + update = getUpdatePayload("9998.9999.9999.9999", "update3"); + await client.emit("sync", { data: { current: [update] } }); + + await WebCompatExtension.interventionsReady(); + await WebCompatExtension.shimsReady(); + is(interventions.length, 5, "Correct number of interventions"); + is(interventions[0].id, "intervention1update2", "Correct intervention"); + is(interventions[0].active, true, "Intervention should be active"); + is(Object.entries(shims).length, 4, "Correct number of shims"); + is(shims[0].id, "shim1update2", "Correct shim"); + is(shims[0].enabled, true, "Shim should be enabled"); + + await WebCompatExtension.resetInterventionsAndShimsToDefaults(); +}); diff --git a/browser/extensions/webcompat/tests/browser/browser_shims.js b/browser/extensions/webcompat/tests/browser/browser_shims.js @@ -3,10 +3,15 @@ registerCleanupFunction(() => { UrlClassifierTestUtils.cleanupTestTrackers(); Services.prefs.clearUserPref(TRACKING_PREF); + // It's unclear why/where this pref ends up getting set, but we ought to reset it. + Services.prefs.clearUserPref( + "privacy.trackingprotection.allow_list.hasUserInteractedWithETPSettings" + ); }); add_setup(async function () { await UrlClassifierTestUtils.addTestTrackers(); + await generateTestShims(); }); add_task(async function test_shim_disabled_by_own_pref() { diff --git a/browser/extensions/webcompat/tests/browser/browser_smartblockembeds.js b/browser/extensions/webcompat/tests/browser/browser_smartblockembeds.js @@ -5,19 +5,25 @@ add_setup(async function () { await SpecialPowers.pushPrefEnv({ - set: [["test.wait300msAfterTabSwitch", true]], + set: [ + ["test.wait300msAfterTabSwitch", true], + + // Extend clickjacking delay for test because timer expiry can happen before we + // check the toggle is disabled (especially in chaos mode). + [SEC_DELAY_PREF, 1000], + [TRACKING_PREF, true], + [SMARTBLOCK_EMBEDS_ENABLED_PREF, true], + ], }); await UrlClassifierTestUtils.addTestTrackers(); - // Extend clickjacking delay for test because timer expiry can happen before we - // check the toggle is disabled (especially in chaos mode). - Services.prefs.setIntPref(SEC_DELAY_PREF, 1000); - Services.prefs.setBoolPref(TRACKING_PREF, true); - Services.prefs.setBoolPref(SMARTBLOCK_EMBEDS_ENABLED_PREF, true); + await generateTestShims(); registerCleanupFunction(() => { UrlClassifierTestUtils.cleanupTestTrackers(); - Services.prefs.clearUserPref(TRACKING_PREF); + + // It's unclear why/where this pref ends up getting set, but we ought to reset it. + Services.prefs.clearUserPref("browser.protections_panel.infoMessage.seen"); }); Services.fog.testResetFOG(); diff --git a/browser/extensions/webcompat/tests/browser/browser_smartblockembeds_mutation.js b/browser/extensions/webcompat/tests/browser/browser_smartblockembeds_mutation.js @@ -5,19 +5,22 @@ add_setup(async function () { await SpecialPowers.pushPrefEnv({ - set: [["test.wait300msAfterTabSwitch", true]], + set: [ + ["test.wait300msAfterTabSwitch", true], + + // Extend clickjacking delay for test because timer expiry can happen before we + // check the toggle is disabled (especially in chaos mode). + [SEC_DELAY_PREF, 1000], + [TRACKING_PREF, true], + [SMARTBLOCK_EMBEDS_ENABLED_PREF, true], + ], }); await UrlClassifierTestUtils.addTestTrackers(); - // Extend clickjacking delay for test because timer expiry can happen before we - // check the toggle is disabled (especially in chaos mode). - Services.prefs.setIntPref(SEC_DELAY_PREF, 1000); - Services.prefs.setBoolPref(TRACKING_PREF, true); - Services.prefs.setBoolPref(SMARTBLOCK_EMBEDS_ENABLED_PREF, true); + await generateTestShims(); registerCleanupFunction(() => { UrlClassifierTestUtils.cleanupTestTrackers(); - Services.prefs.clearUserPref(TRACKING_PREF); }); Services.fog.testResetFOG(); diff --git a/browser/extensions/webcompat/tests/browser/head.js b/browser/extensions/webcompat/tests/browser/head.js @@ -54,6 +54,41 @@ const WebCompatExtension = new (class WebCompatExtension { ); } + async resetInterventionsAndShimsToDefaults() { + return this.#run(async function () { + await content.wrappedJSObject._downgradeForTesting(); + await content.wrappedJSObject.interventions._resetToDefaultInterventions(); + await content.wrappedJSObject.shims._resetToDefaultShims(); + }); + } + + async availableInterventions() { + return this.#run(async function () { + const available = + content.wrappedJSObject.interventions._availableInterventions; + // structured cloning won't work, so get the interesting bits for tests. + return JSON.parse(JSON.stringify(available)); + }); + } + + async availableShims() { + return this.#run(async function () { + // structured cloning won't work, so get the interesting bits for tests. + const available = []; + for (let shim of content.wrappedJSObject.shims.shims.values()) { + const final = {}; + for (let [name, value] of Object.entries(shim)) { + if (name !== "manager" && !name.startsWith("_")) { + final[name] = value; + } + } + final.enabled = shim.enabled; + available.push(final); + } + return JSON.parse(JSON.stringify(available)); + }); + } + async interventionsReady() { return this.#run(async function () { await content.wrappedJSObject.interventions.ready(); @@ -97,6 +132,15 @@ const WebCompatExtension = new (class WebCompatExtension { .ALLOWED_GLOBAL_PREFS; } + async updateShims(_shims) { + return this.#run(async function (shims) { + await content.wrappedJSObject.shims.ready(); + await content.wrappedJSObject.shims._updateShims( + Cu.cloneInto(shims, content) + ); + }, _shims); + } + async shimsReady() { return this.#run(async function () { await content.wrappedJSObject.shims.ready(); @@ -140,6 +184,8 @@ async function testShimRuns( trackersAllowed = true, expectOptIn = true ) { + await WebCompatExtension.shimsReady(); + const tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser, opening: testPage, @@ -209,6 +255,8 @@ async function testShimDoesNotRun( trackersAllowed = false, testPage = SHIMMABLE_TEST_PAGE ) { + await WebCompatExtension.shimsReady(); + const tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser, opening: testPage, @@ -336,3 +384,112 @@ async function clickOnPagePlaceholder(tab) { // If this await finished, then protections panel is open return popupShownPromise; } + +async function generateTestShims() { + await WebCompatExtension.updateShims([ + { + id: "MochitestShim", + platform: "all", + branch: ["all:ignoredOtherPlatform"], + name: "Test shim for Mochitests", + bug: "mochitest", + file: "mochitest-shim-1.js", + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test.js", + ], + needsShimHelpers: ["getOptions", "optIn"], + options: { + simpleOption: true, + complexOption: { a: 1, b: "test" }, + branchValue: { value: true, branches: [] }, + platformValue: { value: true, platform: "neverUsed" }, + }, + unblocksOnOptIn: ["*://trackertest.org/*"], + }, + { + disabled: true, + id: "MochitestShim2", + platform: "all", + name: "Test shim for Mochitests (disabled by default)", + bug: "mochitest", + file: "mochitest-shim-2.js", + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_2.js", + ], + needsShimHelpers: ["getOptions", "optIn"], + options: { + simpleOption: true, + complexOption: { a: 1, b: "test" }, + branchValue: { value: true, branches: [] }, + platformValue: { value: true, platform: "neverUsed" }, + }, + unblocksOnOptIn: ["*://trackertest.org/*"], + }, + { + id: "MochitestShim3", + platform: "all", + name: "Test shim for Mochitests (host)", + bug: "mochitest", + file: "mochitest-shim-3.js", + notHosts: ["example.com"], + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", + ], + }, + { + id: "MochitestShim4", + platform: "all", + name: "Test shim for Mochitests (notHost)", + bug: "mochitest", + file: "mochitest-shim-3.js", + hosts: ["example.net"], + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", + ], + }, + { + id: "MochitestShim5", + platform: "all", + name: "Test shim for Mochitests (branch)", + bug: "mochitest", + file: "mochitest-shim-3.js", + branches: ["never matches"], + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", + ], + }, + { + id: "MochitestShim6", + platform: "never matches", + name: "Test shim for Mochitests (platform)", + bug: "mochitest", + file: "mochitest-shim-3.js", + matches: [ + "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js", + ], + }, + { + id: "EmbedTestShim", + platform: "desktop", + name: "Test shim for smartblock embed unblocking", + bug: "1892175", + runFirst: "embed-test-shim.js", + // Blank stub file just so we run the script above when the matched script + // files get blocked. + file: "empty-script.js", + matches: [ + "https://itisatracker.org/browser/browser/extensions/webcompat/tests/browser/embed_test.js", + ], + // Use instagram logo as an example + logos: ["instagram.svg"], + needsShimHelpers: [ + "embedClicked", + "smartblockEmbedReplaced", + "smartblockGetFluentString", + ], + isSmartblockEmbedShim: true, + onlyIfBlockedByETP: true, + unblocksOnOptIn: ["*://itisatracker.org/*"], + }, + ]); +}