Files
tubestation/browser/components/urlbar/private/AdmWikipedia.sys.mjs
Drew Willcoxon 21b13c63b8 Bug 1832927 - Move sponsored/nonsponsored checks back to UrlbarProviderQuickSuggest. r=daisuke
Please see bug 1832927 for background. This fixes the problem on 115 by moving
the sponsored/nonsponsored logic back to the provider.

I added a task to test_quicksuggest_topPicks.js, and I created xpcshell tests
for addons and dynamic Wikipedia with similar tasks. It would be nice to unify
this check for all quick suggest types somehow -- maybe a task in
test_quicksuggest.js, but that's not quite as simple as it seems because each
suggestion type has its own suggestion object and expected result payload. We
might also want to add more tasks to these new files. We can think about that
later because there are other opportunities for test consolidation too.

Differential Revision: https://phabricator.services.mozilla.com/D177952
2023-05-16 01:17:06 +00:00

285 lines
9.1 KiB
JavaScript

/* 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/. */
import { BaseFeature } from "resource:///modules/urlbar/private/BaseFeature.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
QuickSuggestRemoteSettings:
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
SuggestionsMap:
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
});
const NONSPONSORED_IAB_CATEGORIES = new Set(["5 - Education"]);
/**
* A feature that manages sponsored adM and non-sponsored Wikpedia (sometimes
* called "expanded Wikipedia") suggestions in remote settings.
*/
export class AdmWikipedia extends BaseFeature {
constructor() {
super();
this.#suggestionsMap = new lazy.SuggestionsMap();
}
get shouldEnable() {
return (
lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsEnabled") &&
(lazy.UrlbarPrefs.get("suggest.quicksuggest.nonsponsored") ||
lazy.UrlbarPrefs.get("suggest.quicksuggest.sponsored"))
);
}
get enablingPreferences() {
return [
"suggest.quicksuggest.nonsponsored",
"suggest.quicksuggest.sponsored",
];
}
enable(enabled) {
if (enabled) {
lazy.QuickSuggestRemoteSettings.register(this);
} else {
lazy.QuickSuggestRemoteSettings.unregister(this);
}
}
async queryRemoteSettings(searchString) {
let suggestions = this.#suggestionsMap.get(searchString);
if (!suggestions) {
return [];
}
// Start each icon fetch at the same time and wait for them all to finish.
let icons = await Promise.all(
suggestions.map(({ icon }) => this.#fetchIcon(icon))
);
return suggestions.map(suggestion => ({
full_keyword: this.#getFullKeyword(searchString, suggestion.keywords),
title: suggestion.title,
url: suggestion.url,
click_url: suggestion.click_url,
impression_url: suggestion.impression_url,
block_id: suggestion.id,
advertiser: suggestion.advertiser,
iab_category: suggestion.iab_category,
is_sponsored: !NONSPONSORED_IAB_CATEGORIES.has(suggestion.iab_category),
score: suggestion.score,
position: suggestion.position,
icon: icons.shift(),
}));
}
async onRemoteSettingsSync(rs) {
let dataType = lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsDataType");
this.logger.debug("Loading remote settings with type: " + dataType);
let [data] = await Promise.all([
rs.get({ filters: { type: dataType } }),
rs
.get({ filters: { type: "icon" } })
.then(icons =>
Promise.all(icons.map(i => rs.attachments.downloadToDisk(i)))
),
]);
if (rs != lazy.QuickSuggestRemoteSettings.rs) {
return;
}
let suggestionsMap = new lazy.SuggestionsMap();
this.logger.debug(`Got data with ${data.length} records`);
for (let record of data) {
let { buffer } = await rs.attachments.download(record);
if (rs != lazy.QuickSuggestRemoteSettings.rs) {
return;
}
let results = JSON.parse(new TextDecoder("utf-8").decode(buffer));
this.logger.debug(`Adding ${results.length} results`);
await suggestionsMap.add(results);
if (rs != lazy.QuickSuggestRemoteSettings.rs) {
return;
}
}
this.#suggestionsMap = suggestionsMap;
}
makeResult(queryContext, suggestion, searchString) {
// Replace the suggestion's template substrings, but first save the original
// URL before its timestamp template is replaced.
let originalUrl = suggestion.url;
lazy.QuickSuggest.replaceSuggestionTemplates(suggestion);
let payload = {
originalUrl,
url: suggestion.url,
icon: suggestion.icon,
isSponsored: suggestion.is_sponsored,
source: suggestion.source,
telemetryType: suggestion.is_sponsored
? "adm_sponsored"
: "adm_nonsponsored",
requestId: suggestion.request_id,
urlTimestampIndex: suggestion.urlTimestampIndex,
sponsoredImpressionUrl: suggestion.impression_url,
sponsoredClickUrl: suggestion.click_url,
sponsoredBlockId: suggestion.block_id,
sponsoredAdvertiser: suggestion.advertiser,
sponsoredIabCategory: suggestion.iab_category,
helpUrl: lazy.QuickSuggest.HELP_URL,
helpL10n: {
id: "urlbar-result-menu-learn-more-about-firefox-suggest",
},
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
};
// Determine if the suggestion itself is a best match.
let isSuggestionBestMatch = false;
if (lazy.QuickSuggestRemoteSettings.config.best_match) {
let { best_match } = lazy.QuickSuggestRemoteSettings.config;
isSuggestionBestMatch =
best_match.min_search_string_length <= searchString.length &&
!best_match.blocked_suggestion_ids.includes(suggestion.block_id);
}
// Determine if the urlbar result should be a best match.
let isResultBestMatch =
isSuggestionBestMatch &&
lazy.UrlbarPrefs.get("bestMatchEnabled") &&
lazy.UrlbarPrefs.get("suggest.bestmatch");
if (isResultBestMatch) {
// Show the result as a best match. Best match titles don't include the
// `full_keyword`, and the user's search string is highlighted.
payload.title = [suggestion.title, lazy.UrlbarUtils.HIGHLIGHT.TYPED];
} else {
// Show the result as a usual quick suggest. Include the `full_keyword`
// and highlight the parts that aren't in the search string.
payload.title = suggestion.title;
payload.qsSuggestion = [
suggestion.full_keyword,
lazy.UrlbarUtils.HIGHLIGHT.SUGGESTED,
];
}
payload.isBlockable = lazy.UrlbarPrefs.get(
isResultBestMatch
? "bestMatchBlockingEnabled"
: "quickSuggestBlockingEnabled"
);
let result = new lazy.UrlbarResult(
lazy.UrlbarUtils.RESULT_TYPE.URL,
lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
...lazy.UrlbarResult.payloadAndSimpleHighlights(
queryContext.tokens,
payload
)
);
if (isResultBestMatch) {
result.isBestMatch = true;
result.suggestedIndex = 1;
}
return result;
}
/**
* Gets the "full keyword" (i.e., suggestion) for a query from a list of
* keywords. The suggestions data doesn't include full keywords, so we make
* our own based on the result's keyword phrases and a particular query. We
* use two heuristics:
*
* (1) Find the first keyword phrase that has more words than the query. Use
* its first `queryWords.length` words as the full keyword. e.g., if the
* query is "moz" and `keywords` is ["moz", "mozi", "mozil", "mozill",
* "mozilla", "mozilla firefox"], pick "mozilla firefox", pop off the
* "firefox" and use "mozilla" as the full keyword.
* (2) If there isn't any keyword phrase with more words, then pick the
* longest phrase. e.g., pick "mozilla" in the previous example (assuming
* the "mozilla firefox" phrase isn't there). That might be the query
* itself.
*
* @param {string} query
* The query string.
* @param {Array} keywords
* An array of suggestion keywords.
* @returns {string}
* The full keyword.
*/
#getFullKeyword(query, keywords) {
let longerPhrase;
let trimmedQuery = query.toLocaleLowerCase().trim();
let queryWords = trimmedQuery.split(" ");
for (let phrase of keywords) {
if (phrase.startsWith(query)) {
let trimmedPhrase = phrase.trim();
let phraseWords = trimmedPhrase.split(" ");
// As an exception to (1), if the query ends with a space, then look for
// phrases with one more word so that the suggestion includes a word
// following the space.
let extra = query.endsWith(" ") ? 1 : 0;
let len = queryWords.length + extra;
if (len < phraseWords.length) {
// We found a phrase with more words.
return phraseWords.slice(0, len).join(" ");
}
if (
query.length < phrase.length &&
(!longerPhrase || longerPhrase.length < trimmedPhrase.length)
) {
// We found a longer phrase with the same number of words.
longerPhrase = trimmedPhrase;
}
}
}
return longerPhrase || trimmedQuery;
}
/**
* Fetch the icon from RemoteSettings attachments.
*
* @param {string} path
* The icon's remote settings path.
*/
async #fetchIcon(path) {
if (!path) {
return null;
}
let { rs } = lazy.QuickSuggestRemoteSettings;
if (!rs) {
return null;
}
let record = (
await rs.get({
filters: { id: `icon-${path}` },
})
).pop();
if (!record) {
return null;
}
return rs.attachments.downloadToDisk(record);
}
get _test_suggestionsMap() {
return this.#suggestionsMap;
}
#suggestionsMap;
}