1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { BaseFeature } from "resource:///modules/urlbar/private/BaseFeature.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
11 SuggestionsMap: "resource:///modules/urlbar/private/SuggestBackendJs.sys.mjs",
12 UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
13 UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
14 UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
17 const NONSPONSORED_IAB_CATEGORIES = new Set(["5 - Education"]);
20 * A feature that manages sponsored adM and non-sponsored Wikpedia (sometimes
21 * called "expanded Wikipedia") suggestions in remote settings.
23 export class AdmWikipedia extends BaseFeature {
26 this.#suggestionsMap = new lazy.SuggestionsMap();
31 lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsEnabled") &&
32 (lazy.UrlbarPrefs.get("suggest.quicksuggest.nonsponsored") ||
33 lazy.UrlbarPrefs.get("suggest.quicksuggest.sponsored"))
37 get enablingPreferences() {
39 "suggest.quicksuggest.nonsponsored",
40 "suggest.quicksuggest.sponsored",
44 get merinoProvider() {
48 get rustSuggestionTypes() {
49 return ["Amp", "Wikipedia"];
52 getSuggestionTelemetryType(suggestion) {
53 return suggestion.is_sponsored ? "adm_sponsored" : "adm_nonsponsored";
58 lazy.QuickSuggest.jsBackend.register(this);
60 lazy.QuickSuggest.jsBackend.unregister(this);
64 async queryRemoteSettings(searchString) {
65 let suggestions = this.#suggestionsMap.get(searchString);
70 // Start each icon fetch at the same time and wait for them all to finish.
71 let icons = await Promise.all(
72 suggestions.map(({ icon }) => this.#fetchIcon(icon))
75 return suggestions.map(suggestion => ({
76 full_keyword: this.#getFullKeyword(searchString, suggestion.keywords),
77 title: suggestion.title,
79 click_url: suggestion.click_url,
80 impression_url: suggestion.impression_url,
81 block_id: suggestion.id,
82 advertiser: suggestion.advertiser,
83 iab_category: suggestion.iab_category,
84 is_sponsored: !NONSPONSORED_IAB_CATEGORIES.has(suggestion.iab_category),
85 score: suggestion.score,
86 position: suggestion.position,
91 async onRemoteSettingsSync(rs) {
92 let dataType = lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsDataType");
93 this.logger.debug("Loading remote settings with type: " + dataType);
95 let [data] = await Promise.all([
96 rs.get({ filters: { type: dataType } }),
98 .get({ filters: { type: "icon" } })
100 Promise.all(icons.map(i => rs.attachments.downloadToDisk(i)))
103 if (!this.isEnabled) {
107 let suggestionsMap = new lazy.SuggestionsMap();
109 this.logger.debug(`Got data with ${data.length} records`);
110 for (let record of data) {
111 let { buffer } = await rs.attachments.download(record);
112 if (!this.isEnabled) {
116 let results = JSON.parse(new TextDecoder("utf-8").decode(buffer));
117 this.logger.debug(`Adding ${results.length} results`);
118 await suggestionsMap.add(results);
119 if (!this.isEnabled) {
124 this.#suggestionsMap = suggestionsMap;
127 makeResult(queryContext, suggestion, searchString) {
128 if (suggestion.source == "rust") {
130 title: suggestion.title,
132 icon: suggestion.icon,
133 is_sponsored: suggestion.is_sponsored,
134 full_keyword: suggestion.fullKeyword,
135 impression_url: suggestion.impressionUrl,
136 click_url: suggestion.clickUrl,
137 block_id: suggestion.blockId,
138 advertiser: suggestion.advertiser,
139 iab_category: suggestion.iabCategory,
143 // Replace the suggestion's template substrings, but first save the original
144 // URL before its timestamp template is replaced.
145 let originalUrl = suggestion.url;
146 lazy.QuickSuggest.replaceSuggestionTemplates(suggestion);
151 icon: suggestion.icon,
152 isSponsored: suggestion.is_sponsored,
153 requestId: suggestion.request_id,
154 urlTimestampIndex: suggestion.urlTimestampIndex,
155 sponsoredImpressionUrl: suggestion.impression_url,
156 sponsoredClickUrl: suggestion.click_url,
157 sponsoredBlockId: suggestion.block_id,
158 sponsoredAdvertiser: suggestion.advertiser,
159 sponsoredIabCategory: suggestion.iab_category,
160 helpUrl: lazy.QuickSuggest.HELP_URL,
162 id: "urlbar-result-menu-learn-more-about-firefox-suggest",
165 id: "urlbar-result-menu-dismiss-firefox-suggest",
169 // Determine if the suggestion itself is a best match.
170 let isSuggestionBestMatch = false;
171 if (lazy.QuickSuggest.jsBackend.config.best_match) {
172 let { best_match } = lazy.QuickSuggest.jsBackend.config;
173 isSuggestionBestMatch =
174 best_match.min_search_string_length <= searchString.length &&
175 !best_match.blocked_suggestion_ids.includes(suggestion.block_id);
178 // Determine if the urlbar result should be a best match.
179 let isResultBestMatch =
180 isSuggestionBestMatch &&
181 lazy.UrlbarPrefs.get("bestMatchEnabled") &&
182 lazy.UrlbarPrefs.get("suggest.bestmatch");
183 if (isResultBestMatch) {
184 // Show the result as a best match. Best match titles don't include the
185 // `full_keyword`, and the user's search string is highlighted.
186 payload.title = [suggestion.title, lazy.UrlbarUtils.HIGHLIGHT.TYPED];
188 // Show the result as a usual quick suggest. Include the `full_keyword`
189 // and highlight the parts that aren't in the search string.
190 payload.title = suggestion.title;
191 payload.qsSuggestion = [
192 suggestion.full_keyword,
193 lazy.UrlbarUtils.HIGHLIGHT.SUGGESTED,
197 // Set `is_top_pick` on the suggestion to tell the provider to set
198 // best-match related properties on the result.
199 suggestion.is_top_pick = isResultBestMatch;
201 payload.isBlockable = lazy.UrlbarPrefs.get(
203 ? "bestMatchBlockingEnabled"
204 : "quickSuggestBlockingEnabled"
207 let result = new lazy.UrlbarResult(
208 lazy.UrlbarUtils.RESULT_TYPE.URL,
209 lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
210 ...lazy.UrlbarResult.payloadAndSimpleHighlights(
216 if (suggestion.is_sponsored) {
217 if (!lazy.UrlbarPrefs.get("quickSuggestSponsoredPriority")) {
218 result.richSuggestionIconSize = 16;
221 result.payload.descriptionL10n = {
222 id: "urlbar-result-action-sponsored",
224 result.isRichSuggestion = true;
231 * Gets the "full keyword" (i.e., suggestion) for a query from a list of
232 * keywords. The suggestions data doesn't include full keywords, so we make
233 * our own based on the result's keyword phrases and a particular query. We
234 * use two heuristics:
236 * (1) Find the first keyword phrase that has more words than the query. Use
237 * its first `queryWords.length` words as the full keyword. e.g., if the
238 * query is "moz" and `keywords` is ["moz", "mozi", "mozil", "mozill",
239 * "mozilla", "mozilla firefox"], pick "mozilla firefox", pop off the
240 * "firefox" and use "mozilla" as the full keyword.
241 * (2) If there isn't any keyword phrase with more words, then pick the
242 * longest phrase. e.g., pick "mozilla" in the previous example (assuming
243 * the "mozilla firefox" phrase isn't there). That might be the query
246 * @param {string} query
248 * @param {Array} keywords
249 * An array of suggestion keywords.
253 #getFullKeyword(query, keywords) {
255 let trimmedQuery = query.toLocaleLowerCase().trim();
256 let queryWords = trimmedQuery.split(" ");
258 for (let phrase of keywords) {
259 if (phrase.startsWith(query)) {
260 let trimmedPhrase = phrase.trim();
261 let phraseWords = trimmedPhrase.split(" ");
262 // As an exception to (1), if the query ends with a space, then look for
263 // phrases with one more word so that the suggestion includes a word
264 // following the space.
265 let extra = query.endsWith(" ") ? 1 : 0;
266 let len = queryWords.length + extra;
267 if (len < phraseWords.length) {
268 // We found a phrase with more words.
269 return phraseWords.slice(0, len).join(" ");
272 query.length < phrase.length &&
273 (!longerPhrase || longerPhrase.length < trimmedPhrase.length)
275 // We found a longer phrase with the same number of words.
276 longerPhrase = trimmedPhrase;
280 return longerPhrase || trimmedQuery;
284 * Fetch the icon from RemoteSettings attachments.
286 * @param {string} path
287 * The icon's remote settings path.
289 async #fetchIcon(path) {
294 let { rs } = lazy.QuickSuggest.jsBackend;
301 filters: { id: `icon-${path}` },
307 return rs.attachments.downloadToDisk(record);