Bug 1912086 - Fix application provided search handling in the search-detection add-on. r=robwu

Differential Revision: https://phabricator.services.mozilla.com/D218773
This commit is contained in:
Mark Banner 2024-08-23 15:51:28 +00:00
parent 67b36870e4
commit 9c45f12d2e
2 changed files with 104 additions and 61 deletions

View File

@ -16,6 +16,8 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonSearchEngine: "resource://gre/modules/AddonSearchEngine.sys.mjs",
AppProvidedSearchEngine:
"resource://gre/modules/AppProvidedSearchEngine.sys.mjs",
});
// eslint-disable-next-line mozilla/reject-importGlobalProperties
@ -46,41 +48,43 @@ this.addonsSearchDetection = class extends ExtensionAPI {
try {
await Services.search.promiseInitialized;
const visibleEngines = await Services.search.getEngines();
const engines = await Services.search.getEngines();
visibleEngines.forEach(engine => {
if (!(engine instanceof lazy.AddonSearchEngine)) {
return;
}
const { _extensionID, _urls } = engine.wrappedJSObject;
if (!_extensionID) {
// OpenSearch engines don't have an extension ID.
return;
for (let engine of engines) {
if (
!(engine instanceof lazy.AddonSearchEngine) &&
!(engine instanceof lazy.AppProvidedSearchEngine)
) {
continue;
}
_urls
// We only want to collect "search URLs" (and not "suggestion"
// ones for instance). See `URL_TYPE` in `SearchUtils.sys.mjs`.
.filter(({ type }) => type === "text/html")
.forEach(({ template }) => {
// If this is changed, double check the code in the background
// script because `webRequestCancelledHandler` splits patterns
// on `*` to retrieve URL prefixes.
const pattern = template.split("?")[0] + "*";
// The search term isn't used, but avoids a warning of an empty
// term.
let submission = engine.getSubmission("searchTerm");
if (submission) {
// If this is changed, double check the code in the background
// script because `getAddonIdsForUrl` truncates the last
// character.
const pattern =
submission.uri.prePath + submission.uri.filePath + "*";
// Multiple search engines could register URL templates that
// would become the same URL pattern as defined above so we
// store a list of extension IDs per URL pattern.
if (!patterns[pattern]) {
patterns[pattern] = [];
}
// Multiple search engines could register URL templates that
// would become the same URL pattern as defined above so we
// store a list of extension IDs per URL pattern.
if (!patterns[pattern]) {
patterns[pattern] = [];
}
if (!patterns[pattern].includes(_extensionID)) {
patterns[pattern].push(_extensionID);
}
});
});
// We don't store ids for application provided search engines
// because we don't need to report them. However, we do ensure
// the pattern is recorded (above), so that we check for
// redirects against those.
const _extensionID = engine.wrappedJSObject._extensionID;
if (_extensionID && !patterns[pattern].includes(_extensionID)) {
patterns[pattern].push(_extensionID);
}
}
}
} catch (err) {
console.error(err);
}

View File

@ -6,12 +6,14 @@
const { AddonTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/AddonTestUtils.sys.mjs"
);
const { SearchTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/SearchTestUtils.sys.mjs"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
AddonTestUtils.initMochitest(this);
SearchTestUtils.init(this);
const TELEMETRY_EVENTS_FILTERS = {
category: "addonsSearchDetection",
@ -25,6 +27,7 @@ async function testClientSideRedirect({
background,
permissions,
telemetryExpected = false,
redirectingAppProvidedEngine = false,
}) {
Services.telemetry.clearEvents();
@ -51,7 +54,9 @@ async function testClientSideRedirect({
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "https://example.com/search?q=babar",
url: redirectingAppProvidedEngine
? "https://example.org/default?q=babar"
: "https://example.com/search?q=babar",
},
() => {}
);
@ -67,7 +72,9 @@ async function testClientSideRedirect({
extra: {
addonId,
addonVersion,
from: "example.com",
from: redirectingAppProvidedEngine
? "example.org"
: "example.com",
to: "mochi.test",
},
},
@ -81,38 +88,28 @@ async function testClientSideRedirect({
add_setup(async function () {
const searchEngineName = "test search engine";
let searchEngine;
// This cleanup function has to be registered before the one registered
// internally by loadExtension, otherwise it is going to trigger a test
// failure (because it will be called too late).
registerCleanupFunction(async () => {
await searchEngine.unload();
ok(
!Services.search.getEngineByName(searchEngineName),
"test search engine unregistered"
);
});
searchEngine = ExtensionTestUtils.loadExtension({
manifest: {
chrome_settings_overrides: {
search_provider: {
name: searchEngineName,
keyword: "test",
search_url: "https://example.com/?q={searchTerms}",
await SearchTestUtils.updateRemoteSettingsConfig([
{
identifier: "default",
base: {
urls: {
search: {
base: "https://example.org/default",
searchTermParamName: "q",
},
},
},
},
// NOTE: the search extension needs to be installed through the
// AddonManager to be correctly unregistered when it is uninstalled.
useAddonManager: "temporary",
]);
await SearchTestUtils.installSearchExtension({
name: searchEngineName,
keyword: "test",
search_url: "https://example.com/?q={searchTerms}",
});
await searchEngine.startup();
await AddonTestUtils.waitForSearchProviderStartup(searchEngine);
ok(
Services.search.getEngineByName(searchEngineName),
Assert.ok(
!!Services.search.getEngineByName(searchEngineName),
"test search engine registered"
);
});
@ -137,6 +134,27 @@ add_task(function test_onBeforeRequest() {
});
});
add_task(function test_onBeforeRequest_appProvidedEngine() {
return testClientSideRedirect({
background() {
browser.webRequest.onBeforeRequest.addListener(
() => {
return {
redirectUrl: "http://mochi.test:8888/",
};
},
{ urls: ["*://example.org/*"] },
["blocking"]
);
browser.test.sendMessage("ready");
},
permissions: ["webRequest", "webRequestBlocking", "*://example.org/*"],
redirectingAppProvidedEngine: true,
telemetryExpected: true,
});
});
add_task(function test_onBeforeRequest_url_not_monitored() {
// Here, we load an extension that does a client-side redirect. Because this
// extension does not listen to the URL of the search engine registered
@ -180,6 +198,27 @@ add_task(function test_onHeadersReceived() {
});
});
add_task(function test_onHeadersReceived_appProvidedEngine() {
return testClientSideRedirect({
background() {
browser.webRequest.onHeadersReceived.addListener(
() => {
return {
redirectUrl: "http://mochi.test:8888/",
};
},
{ urls: ["*://example.org/*"], types: ["main_frame"] },
["blocking"]
);
browser.test.sendMessage("ready");
},
permissions: ["webRequest", "webRequestBlocking", "*://example.org/*"],
redirectingAppProvidedEngine: true,
telemetryExpected: true,
});
});
add_task(function test_onHeadersReceived_url_not_monitored() {
// Here, we load an extension that does a client-side redirect. Because this
// extension does not listen to the URL of the search engine registered