Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
/**
* This module exports the UrlbarUtils singleton, which contains constants and
* helper functions that are useful to all components of the urlbar.
*/
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
KeywordUtils: "resource://gre/modules/KeywordUtils.sys.mjs",
PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SearchSuggestionController:
"resource://gre/modules/SearchSuggestionController.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarProviderInterventions:
"resource:///modules/UrlbarProviderInterventions.sys.mjs",
UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
UrlbarProviderSearchTips:
"resource:///modules/UrlbarProviderSearchTips.sys.mjs",
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetter(
lazy,
"parserUtils",
"@mozilla.org/parserutils;1",
"nsIParserUtils"
);
export var UrlbarUtils = {
// Results are categorized into groups to help the muxer compose them. See
// UrlbarUtils.getResultGroup. Since result groups are stored in result
// groups and result groups are stored in prefs, additions and changes to
// result groups may require adding UI migrations to BrowserGlue. Be careful
// about making trivial changes to existing groups, like renaming them,
// because we don't want to make downgrades unnecessarily hard.
RESULT_GROUP: {
ABOUT_PAGES: "aboutPages",
GENERAL: "general",
GENERAL_PARENT: "generalParent",
FORM_HISTORY: "formHistory",
HEURISTIC_AUTOFILL: "heuristicAutofill",
HEURISTIC_ENGINE_ALIAS: "heuristicEngineAlias",
HEURISTIC_EXTENSION: "heuristicExtension",
HEURISTIC_FALLBACK: "heuristicFallback",
HEURISTIC_BOOKMARK_KEYWORD: "heuristicBookmarkKeyword",
HEURISTIC_HISTORY_URL: "heuristicHistoryUrl",
HEURISTIC_OMNIBOX: "heuristicOmnibox",
HEURISTIC_RESTRICT_KEYWORD_AUTOFILL: "heuristicRestrictKeywordAutofill",
HEURISTIC_SEARCH_TIP: "heuristicSearchTip",
HEURISTIC_TEST: "heuristicTest",
HEURISTIC_TOKEN_ALIAS_ENGINE: "heuristicTokenAliasEngine",
INPUT_HISTORY: "inputHistory",
OMNIBOX: "extension",
RECENT_SEARCH: "recentSearch",
REMOTE_SUGGESTION: "remoteSuggestion",
REMOTE_TAB: "remoteTab",
RESTRICT_SEARCH_KEYWORD: "restrictSearchKeyword",
SUGGESTED_INDEX: "suggestedIndex",
TAIL_SUGGESTION: "tailSuggestion",
},
// Defines provider types.
PROVIDER_TYPE: {
// Should be executed immediately, because it returns heuristic results
// that must be handed to the user asap.
// WARNING: these providers must be extremely fast, because the urlbar will
// await for them before returning results to the user. In particular it is
// critical to reply quickly to isActive and startQuery.
HEURISTIC: 1,
// Can be delayed, contains results coming from the session or the profile.
PROFILE: 2,
// Can be delayed, contains results coming from the network.
NETWORK: 3,
// Can be delayed, contains results coming from unknown sources.
EXTENSION: 4,
},
// Defines UrlbarResult types.
RESULT_TYPE: {
// An open tab.
TAB_SWITCH: 1,
// A search suggestion or engine.
SEARCH: 2,
// A common url/title tuple, may be a bookmark with tags.
URL: 3,
// A bookmark keyword.
KEYWORD: 4,
// A WebExtension Omnibox result.
OMNIBOX: 5,
// A tab from another synced device.
REMOTE_TAB: 6,
// An actionable message to help the user with their query.
TIP: 7,
// A type of result which layout is defined at runtime.
DYNAMIC: 8,
// A restrict keyword result, could be @bookmarks, @history, or @tabs.
RESTRICT: 9,
// When you add a new type, also add its schema to
// UrlbarUtils.RESULT_PAYLOAD_SCHEMA below. Also consider checking if
// consumers of "urlbar-user-start-navigation" need updating.
},
// This defines the source of results returned by a provider. Each provider
// can return results from more than one source. This is used by the
// ProvidersManager to decide which providers must be queried and which
// results can be returned.
// If you add new source types, consider checking if consumers of
// "urlbar-user-start-navigation" need update as well.
RESULT_SOURCE: {
BOOKMARKS: 1,
HISTORY: 2,
SEARCH: 3,
TABS: 4,
OTHER_LOCAL: 5,
OTHER_NETWORK: 6,
ADDON: 7,
ACTIONS: 8,
},
// Per-result exposure telemetry.
EXPOSURE_TELEMETRY: {
// Exposure telemetry will not be recorded for the result.
NONE: 0,
// Exposure telemetry will be recorded for the result and the result will be
// visible in the view as usual.
SHOWN: 1,
// Exposure telemetry will be recorded for the result but the result will
// not be present in the view.
HIDDEN: 2,
},
// This defines icon locations that are commonly used in the UI.
ICON: {
// DEFAULT is defined lazily so it doesn't eagerly initialize PlacesUtils.
EXTENSION: "chrome://mozapps/skin/extensions/extension.svg",
HISTORY: "chrome://browser/skin/history.svg",
SEARCH_GLASS: "chrome://global/skin/icons/search-glass.svg",
TRENDING: "chrome://global/skin/icons/trending.svg",
TIP: "chrome://global/skin/icons/lightbulb.svg",
},
// The number of results by which Page Up/Down move the selection.
PAGE_UP_DOWN_DELTA: 5,
// IME composition states.
COMPOSITION: {
NONE: 1,
COMPOSING: 2,
COMMIT: 3,
CANCELED: 4,
},
// Limit the length of titles and URLs we display so layout doesn't spend too
// much time building text runs.
MAX_TEXT_LENGTH: 255,
// Whether a result should be highlighted up to the point the user has typed
// or after that point.
HIGHLIGHT: {
NONE: 0,
TYPED: 1,
SUGGESTED: 2,
},
// UrlbarProviderPlaces's autocomplete results store their titles and tags
// together in their comments. This separator is used to separate them.
// After bug 1717511, we should stop using this old hack and store titles and
// tags separately. It's important that this be a character that no title
// would ever have. We use \x1F, the non-printable unit separator.
TITLE_TAGS_SEPARATOR: "\x1F",
// Regex matching single word hosts with an optional port; no spaces, auth or
// path-like chars are admitted.
REGEXP_SINGLE_WORD: /^[^\s@:/?#]+(:\d+)?$/,
// Valid entry points for search mode. If adding a value here, please update
// telemetry documentation and Scalars.yaml.
SEARCH_MODE_ENTRY: new Set([
"bookmarkmenu",
"handoff",
"keywordoffer",
"oneoff",
"historymenu",
"other",
"searchbutton",
"shortcut",
"tabmenu",
"tabtosearch",
"tabtosearch_onboard",
"topsites_newtab",
"topsites_urlbar",
"touchbar",
"typed",
]),
// The favicon service stores icons for URLs with the following protocols.
PROTOCOLS_WITH_ICONS: ["about:", "http:", "https:", "file:"],
// Valid URI schemes that are considered safe but don't contain
// an authority component (e.g host:port). There are many URI schemes
// that do not contain an authority, but these in particular have
// some likelihood of being entered or bookmarked by a user.
// `file:` is an exceptional case because an authority is optional
PROTOCOLS_WITHOUT_AUTHORITY: [
"about:",
"data:",
"file:",
"javascript:",
"view-source:",
],
// Search mode objects corresponding to the local shortcuts in the view, in
// order they appear. Pref names are relative to the `browser.urlbar` branch.
get LOCAL_SEARCH_MODES() {
return [
{
source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
restrict: lazy.UrlbarTokenizer.RESTRICT.BOOKMARK,
icon: "chrome://browser/skin/bookmark.svg",
pref: "shortcuts.bookmarks",
telemetryLabel: "bookmarks",
uiLabel: "urlbar-searchmode-bookmarks",
},
{
source: UrlbarUtils.RESULT_SOURCE.TABS,
restrict: lazy.UrlbarTokenizer.RESTRICT.OPENPAGE,
icon: "chrome://browser/skin/tabs.svg",
pref: "shortcuts.tabs",
telemetryLabel: "tabs",
uiLabel: "urlbar-searchmode-tabs",
},
{
source: UrlbarUtils.RESULT_SOURCE.HISTORY,
restrict: lazy.UrlbarTokenizer.RESTRICT.HISTORY,
icon: "chrome://browser/skin/history.svg",
pref: "shortcuts.history",
telemetryLabel: "history",
uiLabel: "urlbar-searchmode-history",
},
{
source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
restrict: lazy.UrlbarTokenizer.RESTRICT.ACTION,
icon: "chrome://browser/skin/quickactions.svg",
pref: "shortcuts.actions",
telemetryLabel: "actions",
uiLabel: "urlbar-searchmode-actions",
},
];
},
/**
* Returns the payload schema for the given type of result.
*
* @param {number} type One of the UrlbarUtils.RESULT_TYPE values.
* @returns {object} The schema for the given type.
*/
getPayloadSchema(type) {
return UrlbarUtils.RESULT_PAYLOAD_SCHEMA[type];
},
/**
* Adds a url to history as long as it isn't in a private browsing window,
* and it is valid.
*
* @param {string} url The url to add to history.
* @param {nsIDomWindow} window The window from where the url is being added.
*/
addToUrlbarHistory(url, window) {
if (
!lazy.PrivateBrowsingUtils.isWindowPrivate(window) &&
url &&
!url.includes(" ") &&
// eslint-disable-next-line no-control-regex
!/[\x00-\x1F]/.test(url)
) {
lazy.PlacesUIUtils.markPageAsTyped(url);
}
},
/**
* Given a string, will generate a more appropriate urlbar value if a Places
* keyword or a search alias is found at the beginning of it.
*
* @param {string} url
* A string that may begin with a keyword or an alias.
*
* @returns {Promise<{ url, postData, mayInheritPrincipal }>}
* If it's not possible to discern a keyword or an alias, url will be
* the input string.
*/
async getShortcutOrURIAndPostData(url) {
let mayInheritPrincipal = false;
let postData = null;
// Split on the first whitespace.
let [keyword, param = ""] = url.trim().split(/\s(.+)/, 2);
if (!keyword) {
return { url, postData, mayInheritPrincipal };
}
let engine = await Services.search.getEngineByAlias(keyword);
if (engine) {
let submission = engine.getSubmission(param, null, "keyword");
return {
url: submission.uri.spec,
postData: submission.postData,
mayInheritPrincipal,
};
}
// A corrupt Places database could make this throw, breaking navigation
// from the location bar.
let entry = null;
try {
entry = await lazy.PlacesUtils.keywords.fetch(keyword);
} catch (ex) {
console.error(`Unable to fetch Places keyword "${keyword}":`, ex);
}
if (!entry || !entry.url) {
// This is not a Places keyword.
return { url, postData, mayInheritPrincipal };
}
try {
[url, postData] = await lazy.KeywordUtils.parseUrlAndPostData(
entry.url.href,
entry.postData,
param
);
if (postData) {
postData = this.getPostDataStream(postData);
}
// Since this URL came from a bookmark, it's safe to let it inherit the
// current document's principal.
mayInheritPrincipal = true;
} catch (ex) {
// It was not possible to bind the param, just use the original url value.
}
return { url, postData, mayInheritPrincipal };
},
/**
* Returns an input stream wrapper for the given post data.
*
* @param {string} postDataString The string to wrap.
* @param {string} [type] The encoding type.
* @returns {nsIInputStream} An input stream of the wrapped post data.
*/
getPostDataStream(
postDataString,
type = "application/x-www-form-urlencoded"
) {
let dataStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
Ci.nsIStringInputStream
);
dataStream.setByteStringData(postDataString);
let mimeStream = Cc[
"@mozilla.org/network/mime-input-stream;1"
].createInstance(Ci.nsIMIMEInputStream);
mimeStream.addHeader("Content-Type", type);
mimeStream.setData(dataStream);
return mimeStream.QueryInterface(Ci.nsIInputStream);
},
_compareIgnoringDiacritics: null,
/**
* Returns a list of all the token substring matches in a string. Matching is
* case insensitive. Each match in the returned list is a tuple: [matchIndex,
* matchLength]. matchIndex is the index in the string of the match, and
* matchLength is the length of the match.
*
* @param {Array} tokens The tokens to search for.
* @param {string} str The string to match against.
* @param {boolean} highlightType
* One of the HIGHLIGHT values:
* TYPED: match ranges matching the tokens; or
* SUGGESTED: match ranges for words not matching the tokens and the
* endings of words that start with a token.
* @returns {Array} An array: [
* [matchIndex_0, matchLength_0],
* [matchIndex_1, matchLength_1],
* ...
* [matchIndex_n, matchLength_n]
* ].
* The array is sorted by match indexes ascending.
*/
getTokenMatches(tokens, str, highlightType) {
// Only search a portion of the string, because not more than a certain
// amount of characters are visible in the UI, matching over what is visible
// would be expensive and pointless.
str = str.substring(0, UrlbarUtils.MAX_TEXT_LENGTH).toLocaleLowerCase();
// To generate non-overlapping ranges, we start from a 0-filled array with
// the same length of the string, and use it as a collision marker, setting
// 1 where the text should be highlighted.
let hits = new Array(str.length).fill(
highlightType == this.HIGHLIGHT.SUGGESTED ? 1 : 0
);
let compareIgnoringDiacritics;
for (let i = 0, totalTokensLength = 0; i < tokens.length; i++) {
const { lowerCaseValue: needle } = tokens[i];
// Ideally we should never hit the empty token case, but just in case
// the `needle` check protects us from an infinite loop.
if (!needle) {
continue;
}
let index = 0;
let found = false;
// First try a diacritic-sensitive search.
for (;;) {
index = str.indexOf(needle, index);
if (index < 0) {
break;
}
if (highlightType == UrlbarUtils.HIGHLIGHT.SUGGESTED) {
// We de-emphasize the match only if it's preceded by a space, thus
// it's a perfect match or the beginning of a longer word.
let previousSpaceIndex = str.lastIndexOf(" ", index) + 1;
if (index != previousSpaceIndex) {
index += needle.length;
// We found the token but we won't de-emphasize it, because it's not
// after a word boundary.
found = true;
continue;
}
}
hits.fill(
highlightType == this.HIGHLIGHT.SUGGESTED ? 0 : 1,
index,
index + needle.length
);
index += needle.length;
found = true;
}
// If that fails to match anything, try a (computationally intensive)
// diacritic-insensitive search.
if (!found) {
if (!compareIgnoringDiacritics) {
if (!this._compareIgnoringDiacritics) {
// Diacritic insensitivity in the search engine follows a set of
// general rules that are not locale-dependent, so use a generic
// English collator for highlighting matching words instead of a
// collator for the user's particular locale.
this._compareIgnoringDiacritics = new Intl.Collator("en", {
sensitivity: "base",
}).compare;
}
compareIgnoringDiacritics = this._compareIgnoringDiacritics;
}
index = 0;
while (index < str.length) {
let hay = str.substr(index, needle.length);
if (compareIgnoringDiacritics(needle, hay) === 0) {
if (highlightType == UrlbarUtils.HIGHLIGHT.SUGGESTED) {
let previousSpaceIndex = str.lastIndexOf(" ", index) + 1;
if (index != previousSpaceIndex) {
index += needle.length;
continue;
}
}
hits.fill(
highlightType == this.HIGHLIGHT.SUGGESTED ? 0 : 1,
index,
index + needle.length
);
index += needle.length;
} else {
index++;
}
}
}
totalTokensLength += needle.length;
if (totalTokensLength > UrlbarUtils.MAX_TEXT_LENGTH) {
// Limit the number of tokens to reduce calculate time.
break;
}
}
// Starting from the collision array, generate [start, len] tuples
// representing the ranges to be highlighted.
let ranges = [];
for (let index = hits.indexOf(1); index >= 0 && index < hits.length; ) {
let len = 0;
// eslint-disable-next-line no-empty
for (let j = index; j < hits.length && hits[j]; ++j, ++len) {}
ranges.push([index, len]);
// Move to the next 1.
index = hits.indexOf(1, index + len);
}
return ranges;
},
/**
* Returns the group for a result.
*
* @param {UrlbarResult} result
* The result.
* @returns {UrlbarUtils.RESULT_GROUP}
* The reuslt's group.
*/
getResultGroup(result) {
if (result.group) {
return result.group;
}
if (result.hasSuggestedIndex && !result.isSuggestedIndexRelativeToGroup) {
return UrlbarUtils.RESULT_GROUP.SUGGESTED_INDEX;
}
if (result.heuristic) {
switch (result.providerName) {
case "AliasEngines":
return UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS;
case "Autofill":
return UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL;
case "BookmarkKeywords":
return UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD;
case "HeuristicFallback":
return UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK;
case "Omnibox":
return UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX;
case "RestrictKeywordsAutofill":
return UrlbarUtils.RESULT_GROUP.HEURISTIC_RESTRICT_KEYWORD_AUTOFILL;
case "TokenAliasEngines":
return UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE;
case "UrlbarProviderSearchTips":
return UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP;
case "HistoryUrlHeuristic":
return UrlbarUtils.RESULT_GROUP.HEURISTIC_HISTORY_URL;
default:
if (result.providerName.startsWith("TestProvider")) {
return UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST;
}
break;
}
if (result.providerType == UrlbarUtils.PROVIDER_TYPE.EXTENSION) {
return UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION;
}
console.error(
"Returning HEURISTIC_FALLBACK for unrecognized heuristic result: ",
result
);
return UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK;
}
switch (result.providerName) {
case "AboutPages":
return UrlbarUtils.RESULT_GROUP.ABOUT_PAGES;
case "InputHistory":
return UrlbarUtils.RESULT_GROUP.INPUT_HISTORY;
case "UrlbarProviderQuickSuggest":
return UrlbarUtils.RESULT_GROUP.GENERAL_PARENT;
default:
break;
}
switch (result.type) {
case UrlbarUtils.RESULT_TYPE.SEARCH:
if (result.source == UrlbarUtils.RESULT_SOURCE.HISTORY) {
return result.providerName == "RecentSearches"
? UrlbarUtils.RESULT_GROUP.RECENT_SEARCH
: UrlbarUtils.RESULT_GROUP.FORM_HISTORY;
}
if (result.payload.tail && !result.isRichSuggestion) {
return UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION;
}
if (result.payload.suggestion) {
return UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION;
}
break;
case UrlbarUtils.RESULT_TYPE.OMNIBOX:
return UrlbarUtils.RESULT_GROUP.OMNIBOX;
case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
return UrlbarUtils.RESULT_GROUP.REMOTE_TAB;
case UrlbarUtils.RESULT_TYPE.RESTRICT:
return UrlbarUtils.RESULT_GROUP.RESTRICT_SEARCH_KEYWORD;
}
return UrlbarUtils.RESULT_GROUP.GENERAL;
},
/**
* Extracts the URL from a result.
*
* @param {UrlbarResult} result
* The result to extract from.
* @returns {object}
* An object: `{ url, postData }`
* `url` will be null if the result doesn't have a URL. `postData` will be
* null if the result doesn't have post data.
*/
getUrlFromResult(result) {
if (
result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
result.payload.engine
) {
const engine = Services.search.getEngineByName(result.payload.engine);
let [url, postData] = this.getSearchQueryUrl(
engine,
result.payload.suggestion || result.payload.query
);
return { url, postData };
}
return {
url: result.payload.url ?? null,
postData: result.payload.postData
? this.getPostDataStream(result.payload.postData)
: null,
};
},
/**
* Get the url to load for the search query.
*
* @param {nsISearchEngine} engine
* The engine to generate the query for.
* @param {string} query
* The query string to search for.
* @returns {Array}
* Returns an array containing the query url (string) and the
* post data (object).
*/
getSearchQueryUrl(engine, query) {
let submission = engine.getSubmission(query, null, "keyword");
return [submission.uri.spec, submission.postData];
},
// Ranks a URL prefix from 3 - 0 with the following preferences:
// https:// > https://www. > http:// > http://www.
// Higher is better for the purposes of deduping URLs.
// Returns -1 if the prefix does not match any of the above.
getPrefixRank(prefix) {
return ["http://www.", "http://", "https://www.", "https://"].indexOf(
prefix
);
},
/**
* Gets the number of rows a result should span in the view.
*
* @param {UrlbarResult} result
* The result.
* @param {bool} includeHiddenExposures
* Whether a span should be returned if the result is a hidden exposure. If
* false and `result.isHiddenExposure` is true, zero will be returned since
* the result should be hidden and not take up any rows at all. Otherwise
* the result's true span is returned.
* @returns {number}
* The number of rows the result should span in the view.
*/
getSpanForResult(result, { includeHiddenExposures = false } = {}) {
if (!includeHiddenExposures && result.isHiddenExposure) {
return 0;
}
if (result.resultSpan) {
return result.resultSpan;
}
switch (result.type) {
case UrlbarUtils.RESULT_TYPE.URL:
case UrlbarUtils.RESULT_TYPE.BOOKMARKS:
case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
case UrlbarUtils.RESULT_TYPE.KEYWORD:
case UrlbarUtils.RESULT_TYPE.SEARCH:
case UrlbarUtils.RESULT_TYPE.OMNIBOX:
return 1;
case UrlbarUtils.RESULT_TYPE.TIP:
return 3;
}
return 1;
},
/**
* Gets a default icon for a URL.
*
* @param {string} url
* The URL to get the icon for.
* @returns {string} A URI pointing to an icon for `url`.
*/
getIconForUrl(url) {
if (typeof url == "string") {
return UrlbarUtils.PROTOCOLS_WITH_ICONS.some(p => url.startsWith(p))
? "page-icon:" + url
: UrlbarUtils.ICON.DEFAULT;
}
if (
URL.isInstance(url) &&
UrlbarUtils.PROTOCOLS_WITH_ICONS.includes(url.protocol)
) {
return "page-icon:" + url.href;
}
return UrlbarUtils.ICON.DEFAULT;
},
/**
* Returns a search mode object if a token should enter search mode when
* typed. This does not handle engine aliases.
*
* @param {UrlbarUtils.RESTRICT} token
* A restriction token to convert to search mode.
* @returns {object}
* A search mode object. Null if search mode should not be entered. See
* setSearchMode documentation for details.
*/
searchModeForToken(token) {
if (token == lazy.UrlbarTokenizer.RESTRICT.SEARCH) {
return {
engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)
?.name,
};
}
let mode = UrlbarUtils.LOCAL_SEARCH_MODES.find(m => m.restrict == token);
if (!mode) {
return null;
}
// Return a copy so callers don't modify the object in LOCAL_SEARCH_MODES.
return { ...mode };
},
/**
* Tries to initiate a speculative connection to a given url.
*
* Note: This is not infallible, if a speculative connection cannot be
* initialized, it will be a no-op.
*
* @param {nsISearchEngine|nsIURI|URL|string} urlOrEngine entity to initiate
* a speculative connection for.
* @param {window} window the window from where the connection is initialized.
*/
setupSpeculativeConnection(urlOrEngine, window) {
if (!lazy.UrlbarPrefs.get("speculativeConnect.enabled")) {
return;
}
if (urlOrEngine instanceof Ci.nsISearchEngine) {
try {
urlOrEngine.speculativeConnect({
window,
originAttributes: window.gBrowser.contentPrincipal.originAttributes,
});
} catch (ex) {
// Can't setup speculative connection for this url, just ignore it.
}
return;
}
if (URL.isInstance(urlOrEngine)) {
urlOrEngine = urlOrEngine.href;
}
try {
let uri =
urlOrEngine instanceof Ci.nsIURI
? urlOrEngine
: Services.io.newURI(urlOrEngine);
Services.io.speculativeConnect(
uri,
window.gBrowser.contentPrincipal,
null,
false
);
} catch (ex) {
// Can't setup speculative connection for this url, just ignore it.
}
},
/**
* Splits a url into base and ref strings, according to nsIURI.idl.
* Base refers to the part of the url before the ref, excluding the #.
*
* @param {string} url
* The url to split.
* @returns {object} { base, ref }
* Base and ref parts of the given url. Ref is an empty string
* if there is no ref and undefined if url is not well-formed.
*/
extractRefFromUrl(url) {
let uri = URL.parse(url)?.URI;
if (uri) {
return { base: uri.specIgnoringRef, ref: uri.ref };
}
return { base: url };
},
/**
* Strips parts of a URL defined in `options`.
*
* @param {string} spec
* The text to modify.
* @param {object} [options]
* The options object.
* @param {boolean} options.stripHttp
* Whether to strip http.
* @param {boolean} options.stripHttps
* Whether to strip https.
* @param {boolean} options.stripWww
* Whether to strip `www.`.
* @param {boolean} options.trimSlash
* Whether to trim the trailing slash.
* @param {boolean} options.trimEmptyQuery
* Whether to trim a trailing `?`.
* @param {boolean} options.trimEmptyHash
* Whether to trim a trailing `#`.
* @param {boolean} options.trimTrailingDot
* Whether to trim a trailing '.'.
* @returns {string[]} [modified, prefix, suffix]
* modified: {string} The modified spec.
* prefix: {string} The parts stripped from the prefix, if any.
* suffix: {string} The parts trimmed from the suffix, if any.
*/
stripPrefixAndTrim(spec, options = {}) {
let prefix = "";
let suffix = "";
if (options.stripHttp && spec.startsWith("http://")) {
spec = spec.slice(7);
prefix = "http://";
} else if (options.stripHttps && spec.startsWith("https://")) {
spec = spec.slice(8);
prefix = "https://";
}
if (options.stripWww && spec.startsWith("www.")) {
spec = spec.slice(4);
prefix += "www.";
}
if (options.trimEmptyHash && spec.endsWith("#")) {
spec = spec.slice(0, -1);
suffix = "#" + suffix;
}
if (options.trimEmptyQuery && spec.endsWith("?")) {
spec = spec.slice(0, -1);
suffix = "?" + suffix;
}
if (options.trimSlash && spec.endsWith("/")) {
spec = spec.slice(0, -1);
suffix = "/" + suffix;
}
if (options.trimTrailingDot && spec.endsWith(".")) {
spec = spec.slice(0, -1);
suffix = "." + suffix;
}
return [spec, prefix, suffix];
},
/**
* Strips a PSL verified public suffix from an hostname.
*
* Note: Because stripping the full suffix requires to verify it against the
* Public Suffix List, this call is not the cheapest, and thus it should
* not be used in hot paths.
*
* @param {string} host A host name.
* @returns {string} Host name without the public suffix.
*/
stripPublicSuffixFromHost(host) {
try {
return host.substring(
0,
host.length - Services.eTLD.getKnownPublicSuffixFromHost(host).length
);
} catch (ex) {
if (ex.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
throw ex;
}
}
return host;
},
/**
* Used to filter out the javascript protocol from URIs, since we don't
* support LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL for those.
*
* @param {string} pasteData The data to check for javacript protocol.
* @returns {string} The modified paste data.
*/
stripUnsafeProtocolOnPaste(pasteData) {
for (;;) {
let scheme = "";
try {
scheme = Services.io.extractScheme(pasteData);
} catch (ex) {
// If it throws, this is not a javascript scheme.
}
if (scheme != "javascript") {
break;
}
pasteData = pasteData.substring(pasteData.indexOf(":") + 1);
}
return pasteData;
},
/**
* Add a (url, input) tuple to the input history table that drives adaptive
* results.
*
* @param {string} url The url to add input history for
* @param {string} input The associated search term
*/
async addToInputHistory(url, input) {
await lazy.PlacesUtils.withConnectionWrapper("addToInputHistory", db => {
// use_count will asymptotically approach the max of 10.
return db.executeCached(
`
INSERT OR REPLACE INTO moz_inputhistory
SELECT h.id, IFNULL(i.input, :input), IFNULL(i.use_count, 0) * .9 + 1
FROM moz_places h
LEFT JOIN moz_inputhistory i ON i.place_id = h.id AND i.input = :input
WHERE url_hash = hash(:url) AND url = :url
`,
{ url, input: input.toLowerCase() }
);
});
},
/**
* Remove a (url, input*) tuple from the input history table that drives
* adaptive results.
* Note the input argument is used as a wildcard so any match starting with
* it will also be removed.
*
* @param {string} url The url to add input history for
* @param {string} input The associated search term
*/
async removeInputHistory(url, input) {
await lazy.PlacesUtils.withConnectionWrapper("removeInputHistory", db => {
return db.executeCached(
`
DELETE FROM moz_inputhistory
WHERE input BETWEEN :input AND :input || X'FFFF'
AND place_id =
(SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url)
`,
{ url, input: input.toLowerCase() }
);
});
},
/**
* Whether the passed-in input event is paste event.
*
* @param {DOMEvent} event an input DOM event.
* @returns {boolean} Whether the event is a paste event.
*/
isPasteEvent(event) {
return (
event.inputType &&
(event.inputType.startsWith("insertFromPaste") ||
event.inputType == "insertFromYank")
);
},
/**
* Given a string, checks if it looks like a single word host, not containing
* spaces nor dots (apart from a possible trailing one).
*
* Note: This matching should stay in sync with the related code in
* URIFixup::KeywordURIFixup
*
* @param {string} value
* The string to check.
* @returns {boolean}
* Whether the value looks like a single word host.
*/
looksLikeSingleWordHost(value) {
let str = value.trim();
return this.REGEXP_SINGLE_WORD.test(str);
},
/**
* Returns the portion of a string starting at the index where another string
* begins.
*
* @param {string} sourceStr
* The string to search within.
* @param {string} targetStr
* The string to search for.
* @returns {string} The substring within sourceStr starting at targetStr, or
* the empty string if targetStr does not occur in sourceStr.
*/
substringAt(sourceStr, targetStr) {
let index = sourceStr.indexOf(targetStr);
return index < 0 ? "" : sourceStr.substr(index);
},
/**
* Returns the portion of a string starting at the index where another string
* ends.
*
* @param {string} sourceStr
* The string to search within.
* @param {string} targetStr
* The string to search for.
* @returns {string} The substring within sourceStr where targetStr ends, or
* the empty string if targetStr does not occur in sourceStr.
*/
substringAfter(sourceStr, targetStr) {
let index = sourceStr.indexOf(targetStr);
return index < 0 ? "" : sourceStr.substr(index + targetStr.length);
},
/**
* Strips the prefix from a URL and returns the prefix and the remainder of
* the URL. "Prefix" is defined to be the scheme and colon plus zero to two
* slashes (see `UrlbarTokenizer.REGEXP_PREFIX`). If the given string is not
* actually a URL or it has a prefix we don't recognize, then an empty prefix
* and the string itself is returned.
*
* @param {string} str The possible URL to strip.
* @returns {Array} If `str` is a URL with a prefix we recognize,
* then [prefix, remainder]. Otherwise, ["", str].
*/
stripURLPrefix(str) {
let match = lazy.UrlbarTokenizer.REGEXP_PREFIX.exec(str);
if (!match) {
return ["", str];
}
let prefix = match[0];
if (prefix.length < str.length && str[prefix.length] == " ") {
// A space following a prefix:
// e.g. "http:// some search string", "about: some search string"
return ["", str];
}
if (
prefix.endsWith(":") &&
!UrlbarUtils.PROTOCOLS_WITHOUT_AUTHORITY.includes(prefix.toLowerCase())
) {
// Something that looks like a URI scheme but we won't treat as one:
// e.g. "localhost:8888"
return ["", str];
}
return [prefix, str.substring(prefix.length)];
},
/**
* Runs a search for the given string, and returns the heuristic result.
*
* @param {string} searchString The string to search for.
* @param {nsIDOMWindow} window The window requesting it.
* @returns {UrlbarResult} an heuristic result.
*/
async getHeuristicResultFor(searchString, window) {
if (!searchString) {
throw new Error("Must pass a non-null search string");
}
let options = {
allowAutofill: false,
isPrivate: lazy.PrivateBrowsingUtils.isWindowPrivate(window),
maxResults: 1,
searchString,
userContextId: parseInt(
window.gBrowser.selectedBrowser.getAttribute("usercontextid") || 0
),
prohibitRemoteResults: true,
providers: ["AliasEngines", "BookmarkKeywords", "HeuristicFallback"],
};
if (window.gURLBar.searchMode) {
let searchMode = window.gURLBar.searchMode;
options.searchMode = searchMode;
if (searchMode.source) {
options.sources = [searchMode.source];
}
}
let context = new UrlbarQueryContext(options);
await lazy.UrlbarProvidersManager.startQuery(context);
if (!context.heuristicResult) {
throw new Error("There should always be an heuristic result");
}
return context.heuristicResult;
},
/**
* Creates a console logger.
* Logging level can be controlled through the `browser.urlbar.loglevel`
* preference.
*
* @param {object} [options] Options for the logger.
* @param {string} [options.prefix] Prefix to use for the logged messages.
* @returns {ConsoleInstance} The console logger.
*/
getLogger({ prefix = "" } = {}) {
if (!this._loggers) {
this._loggers = new Map();
}
let logger = this._loggers.get(prefix);
if (!logger) {
logger = console.createInstance({
prefix: `URLBar${prefix ? " - " + prefix : ""}`,
maxLogLevelPref: "browser.urlbar.loglevel",
});
this._loggers.set(prefix, logger);
}
return logger;
},
/**
* Returns the name of a result source. The name is the lowercase name of the
* corresponding property in the RESULT_SOURCE object.
*
* @param {string} source A UrlbarUtils.RESULT_SOURCE value.
* @returns {string} The token's name, a lowercased name in the RESULT_SOURCE
* object.
*/
getResultSourceName(source) {
if (!this._resultSourceNamesBySource) {
this._resultSourceNamesBySource = new Map();
for (let [name, src] of Object.entries(this.RESULT_SOURCE)) {
this._resultSourceNamesBySource.set(src, name.toLowerCase());
}
}
return this._resultSourceNamesBySource.get(source);
},
/**
* Add the search to form history. This also updates any existing form
* history for the search.
*
* @param {UrlbarInput} input The UrlbarInput object requesting the addition.
* @param {string} value The value to add.
* @param {string} [source] The source of the addition, usually
* the name of the engine the search was made with.
* @returns {Promise} resolved once the operation is complete
*/
addToFormHistory(input, value, source) {
// If the user types a search engine alias without a search string,
// we have an empty search string and we can't bump it.
// We also don't want to add history in private browsing mode.
// Finally we don't want to store extremely long strings that would not be
// particularly useful to the user.
if (
!value ||
input.isPrivate ||
value.length >
lazy.SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH
) {
return Promise.resolve();
}
return lazy.FormHistory.update({
op: "bump",
fieldname: input.formHistoryName,
value,
source,
});
},
/**
* Returns whether a URL can be autofilled from a candidate string. This
* function is specifically designed for origin and up-to-the-next-slash URL
* autofill. It should not be used for other types of autofill.
*
* @param {string} url
* The URL to test
* @param {string} candidate
* The candidate string to test against
* @param {string} checkFragmentOnly
* If want to check the fragment only, pass true.
* Otherwise, check whole url.
* @returns {boolean} true: can autofill
*/
canAutofillURL(url, candidate, checkFragmentOnly = false) {
// If the URL does not start with the candidate, it can't be autofilled.
// The length check is an optimization to short-circuit the `startsWith()`.
if (
!checkFragmentOnly &&
(url.length <= candidate.length ||
!url.toLocaleLowerCase().startsWith(candidate.toLocaleLowerCase()))
) {
return false;
}
// Create `URL` objects to make the logic below easier. The strings must
// include schemes for this to work.
if (!lazy.UrlbarTokenizer.REGEXP_PREFIX.test(url)) {
url = "http://" + url;
}
if (!lazy.UrlbarTokenizer.REGEXP_PREFIX.test(candidate)) {
candidate = "http://" + candidate;
}
url = URL.parse(url);
candidate = URL.parse(candidate);
if (!url || !candidate) {
return false;
}
if (checkFragmentOnly) {
return url.hash.startsWith(candidate.hash);
}
// For both origin and URL autofill, autofill should stop when the user
// types a trailing slash. This is a fundamental part of autofill's
// up-to-the-next-slash behavior. We handle that here in the else-if branch.
// The length and hash checks in the else-if condition aren't strictly
// necessary -- the else-if branch could simply be an else-branch that
// returns false -- but they mean this function will return true when the
// URL and candidate have the same case-insenstive path and no hash. In
// other words, we allow a URL to autofill itself.
if (!candidate.href.endsWith("/")) {
// The candidate doesn't end in a slash. The URL can't be autofilled if
// its next slash is not at the end.
let nextSlashIndex = url.pathname.indexOf("/", candidate.pathname.length);
if (nextSlashIndex >= 0 && nextSlashIndex != url.pathname.length - 1) {
return false;
}
} else if (url.pathname.length > candidate.pathname.length || url.hash) {
return false;
}
return url.hash.startsWith(candidate.hash);
},
/**
* Extracts a telemetry type from a result, used by scalars and event
* telemetry.
*
* Note: New types should be added to Scalars.yaml under the urlbar.picked
* category and documented in the in-tree documentation. A data-review
* is always necessary.
*
* @param {UrlbarResult} result The result to analyze.
* @param {boolean} camelCase Whether the returned telemetry type should be the
camelCase version.
Eventually this should be the default (bug 1928946).
* @returns {string} A string type for telemetry.
*/
telemetryTypeFromResult(result, camelCase = false) {
if (!result) {
return "unknown";
}
switch (result.type) {
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
return "switchtab";
case UrlbarUtils.RESULT_TYPE.SEARCH:
if (result.providerName == "RecentSearches") {
return camelCase ? "recentSearch" : "recent_search";
}
if (result.source == UrlbarUtils.RESULT_SOURCE.HISTORY) {
return "formhistory";
}
if (result.providerName == "TabToSearch") {
return "tabtosearch";
}
if (result.payload.suggestion) {
let type = result.payload.trending ? "trending" : "searchsuggestion";
if (result.isRichSuggestion) {
type += camelCase ? "Rich" : "_rich";
}
return type;
}
return "searchengine";
case UrlbarUtils.RESULT_TYPE.URL:
if (result.autofill) {
let { type } = result.autofill;
if (!type) {
type = "other";
console.error(
new Error(
"`result.autofill.type` not set, falling back to 'other'"
)
);
}
if (camelCase) {
return `autofill${type[0].toUpperCase()}${type.slice(1)}`;
}
return `autofill_${type}`;
}
if (
result.source == UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL &&
result.heuristic
) {
return "visiturl";
}
if (result.providerName == "UrlbarProviderQuickSuggest") {
// Don't add any more `urlbar.picked` legacy telemetry if possible!
// Return "quicksuggest" here and rely on Glean instead.
switch (result.payload.telemetryType) {
case "top_picks":
return "navigational";
case "wikipedia":
return camelCase ? "dynamicWikipedia" : "dynamic_wikipedia";
}
return "quicksuggest";
}
if (result.providerName == "UrlbarProviderClipboard") {
return "clipboard";
}
{
let type =
result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS
? "bookmark"
: "history";
if (result.providerName == "InputHistory") {
return type + (camelCase ? "Adaptive" : "adaptive");
}
return type;
}
case UrlbarUtils.RESULT_TYPE.KEYWORD:
return "keyword";
case UrlbarUtils.RESULT_TYPE.OMNIBOX:
return "extension";
case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
return "remotetab";
case UrlbarUtils.RESULT_TYPE.TIP:
return "tip";
case UrlbarUtils.RESULT_TYPE.DYNAMIC:
if (result.providerName == "TabToSearch") {
// This is the onboarding result.
return "tabtosearch";
}
return "dynamic";
case UrlbarUtils.RESULT_TYPE.RESTRICT:
if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.BOOKMARK) {
return camelCase
? "restrictKeywordBookmarks"
: "restrict_keyword_bookmarks";
}
if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.OPENPAGE) {
return camelCase ? "restrictKeywordTabs" : "restrict_keyword_tabs";
}
if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.HISTORY) {
return camelCase
? "restrictKeywordHistory"
: "restrict_keyword_history";
}
if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.ACTION) {
return camelCase
? "restrictKeywordActions"
: "restrict_keyword_actions";
}
}
return "unknown";
},
/**
* Unescape the given uri to use as UI.
* NOTE: If the length of uri is over MAX_TEXT_LENGTH,
* return the given uri as it is.
*
* @param {string} uri will be unescaped.
* @returns {string} Unescaped uri.
*/
unEscapeURIForUI(uri) {
return uri.length > UrlbarUtils.MAX_TEXT_LENGTH
? uri
: Services.textToSubURI.unEscapeURIForUI(uri);
},
/**
* Checks whether a given text has right-to-left direction or not.
*
* @param {string} value The text which should be check for RTL direction.
* @param {Window} window The window where 'value' is going to be displayed.
* @returns {boolean} Returns true if text has right-to-left direction and
* false otherwise.
*/
isTextDirectionRTL(value, window) {
let directionality = window.windowUtils.getDirectionFromText(value);
return directionality == window.windowUtils.DIRECTION_RTL;
},
/**
* Unescape, decode punycode, and trim (both protocol and trailing slash)
* the URL. Use for displaying purposes only!
*
* @param {string} url The url that should be prepared for display.
* @param {object} [options] Preparation options.
* @param {boolean} [options.trimURL] Whether the displayed URL should be
* trimmed or not.
* @param {boolean} [options.schemeless] Trim `http(s)://`.
* @returns {string} Prepared url.
*/
prepareUrlForDisplay(url, { trimURL = true, schemeless = false } = {}) {
// Some domains are encoded in punycode. The following ensures we display
// the url in utf-8.
try {
url = new URL(url).URI.displaySpec;
} catch {} // In some cases url is not a valid url.
if (url) {
if (schemeless) {
url = UrlbarUtils.stripPrefixAndTrim(url, {
stripHttp: true,
stripHttps: true,
})[0];
} else if (trimURL && lazy.UrlbarPrefs.get("trimURLs")) {
url = lazy.BrowserUIUtils.removeSingleTrailingSlashFromURL(url);
if (url.startsWith("https://")) {
url = url.substring(8);
if (url.startsWith("www.")) {
url = url.substring(4);
}
}
}
}
return this.unEscapeURIForUI(url);
},
/**
* Extracts a group for search engagement telemetry from a result.
*
* @param {UrlbarResult} result The result to analyze.
* @returns {string} Group name as string.
*/
searchEngagementTelemetryGroup(result) {
if (!result) {
return "unknown";
}
if (result.isBestMatch) {
return "top_pick";
}
if (result.providerName === "UrlbarProviderTopSites") {
return "top_site";
}
switch (this.getResultGroup(result)) {
case UrlbarUtils.RESULT_GROUP.INPUT_HISTORY: {
return "adaptive_history";
}
case UrlbarUtils.RESULT_GROUP.RECENT_SEARCH: {
return "recent_search";
}
case UrlbarUtils.RESULT_GROUP.FORM_HISTORY: {
return "search_history";
}
case UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION:
case UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION: {
let group = result.payload.trending
? "trending_search"
: "search_suggest";
if (result.isRichSuggestion) {
group += "_rich";
}
return group;
}
case UrlbarUtils.RESULT_GROUP.REMOTE_TAB: {
return "remote_tab";
}
case UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION:
case UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX:
case UrlbarUtils.RESULT_GROUP.OMNIBOX: {
return "addon";
}
case UrlbarUtils.RESULT_GROUP.GENERAL: {
return "general";
}
// Group of UrlbarProviderQuickSuggest is GENERAL_PARENT.
case UrlbarUtils.RESULT_GROUP.GENERAL_PARENT: {
return "suggest";
}
case UrlbarUtils.RESULT_GROUP.ABOUT_PAGES: {
return "about_page";
}
case UrlbarUtils.RESULT_GROUP.SUGGESTED_INDEX: {
return "suggested_index";
}
case UrlbarUtils.RESULT_GROUP.RESTRICT_SEARCH_KEYWORD: {
return "restrict_keyword";
}
}
return result.heuristic ? "heuristic" : "unknown";
},
/**
* Extracts a type for search engagement telemetry from a result.
*
* @param {UrlbarResult} result The result to analyze.
* @param {string} selType An optional parameter for the selected type.
* @returns {string} Type as string.
*/
searchEngagementTelemetryType(result, selType = null) {
if (!result) {
return selType === "oneoff" ? "search_shortcut_button" : "input_field";
}
// While product doesn't use experimental addons anymore, tests may still do
// for testing purposes.
if (
result.providerType === UrlbarUtils.PROVIDER_TYPE.EXTENSION &&
result.providerName != "Omnibox"
) {
return "experimental_addon";
}
switch (result.type) {
case UrlbarUtils.RESULT_TYPE.DYNAMIC:
switch (result.providerName) {
case "calculator":
return "calc";
case "TabToSearch":
return "tab_to_search";
case "UnitConversion":
return "unit";
case "UrlbarProviderQuickSuggest":
return this._getQuickSuggestTelemetryType(result);
case "UrlbarProviderQuickSuggestContextualOptIn":
return "fxsuggest_data_sharing_opt_in";
case "UrlbarProviderGlobalActions":
case "UrlbarProviderActionsSearchMode":
return "action";
}
break;
case UrlbarUtils.RESULT_TYPE.KEYWORD:
return "keyword";
case UrlbarUtils.RESULT_TYPE.OMNIBOX:
return "addon";
case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
return "remote_tab";
case UrlbarUtils.RESULT_TYPE.SEARCH:
if (result.providerName === "TabToSearch") {
return "tab_to_search";
}
if (result.source == UrlbarUtils.RESULT_SOURCE.HISTORY) {
return result.providerName == "RecentSearches"
? "recent_search"
: "search_history";
}
if (result.payload.suggestion) {
let type = result.payload.trending
? "trending_search"
: "search_suggest";
if (result.isRichSuggestion) {
type += "_rich";
}
return type;
}
return "search_engine";
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
return "tab";
case UrlbarUtils.RESULT_TYPE.TIP:
if (result.providerName === "UrlbarProviderInterventions") {
switch (result.payload.type) {
case lazy.UrlbarProviderInterventions.TIP_TYPE.CLEAR:
return "intervention_clear";
case lazy.UrlbarProviderInterventions.TIP_TYPE.REFRESH:
return "intervention_refresh";
case lazy.UrlbarProviderInterventions.TIP_TYPE.UPDATE_ASK:
case lazy.UrlbarProviderInterventions.TIP_TYPE.UPDATE_CHECKING:
case lazy.UrlbarProviderInterventions.TIP_TYPE.UPDATE_REFRESH:
case lazy.UrlbarProviderInterventions.TIP_TYPE.UPDATE_RESTART:
case lazy.UrlbarProviderInterventions.TIP_TYPE.UPDATE_WEB:
return "intervention_update";
default:
return "intervention_unknown";
}
}
switch (result.payload.type) {
case lazy.UrlbarProviderSearchTips.TIP_TYPE.ONBOARD:
return "tip_onboard";
case lazy.UrlbarProviderSearchTips.TIP_TYPE.REDIRECT:
return "tip_redirect";
case "dismissalAcknowledgment":
return "tip_dismissal_acknowledgment";
default:
return "tip_unknown";
}
case UrlbarUtils.RESULT_TYPE.URL:
if (
result.source === UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL &&
result.heuristic
) {
return "url";
}
if (result.autofill) {
return `autofill_${result.autofill.type ?? "unknown"}`;
}
if (result.providerName === "UrlbarProviderQuickSuggest") {
return this._getQuickSuggestTelemetryType(result);
}
if (result.providerName === "UrlbarProviderTopSites") {
return "top_site";
}
if (result.providerName === "UrlbarProviderClipboard") {
return "clipboard";
}
return result.source === UrlbarUtils.RESULT_SOURCE.BOOKMARKS
? "bookmark"
: "history";
case UrlbarUtils.RESULT_TYPE.RESTRICT:
if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.BOOKMARK) {
return "restrict_keyword_bookmarks";
}
if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.OPENPAGE) {
return "restrict_keyword_tabs";
}
if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.HISTORY) {
return "restrict_keyword_history";
}
if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.ACTION) {
return "restrict_keyword_actions";
}
}
return "unknown";
},
searchEngagementTelemetryAction(result) {
if (result.providerName != "UrlbarProviderGlobalActions") {
return result.payload.action?.key ?? "none";
}
return result.payload.results.map(({ key }) => key).join(",");
},
_getQuickSuggestTelemetryType(result) {
if (result.payload.telemetryType == "weather") {
// Return "weather" without the usual source prefix for consistency with
// past reporting of weather suggestions.
return "weather";
}
return result.payload.source + "_" + result.payload.telemetryType;
},
/**
* For use when we want to hash a pair of items in a dictionary
*
* @param {string[]} tokens
* list of tokens to join into a string eg "a" "b" "c"
* @returns {string}
* the tokens joined in a string "a|b|c"
*/
tupleString(...tokens) {
return tokens.filter(t => t).join("|");
},
/**
* Creates camelCase versions of snake_case keys in the given object and
* recursively all nested objects. All objects are modified in place and the
* original snake_case keys are preserved.
*
* @param {object} obj
* The object to modify.
* @param {boolean} overwrite
* Controls what happens when a camelCase key is already defined for a
* snake_case key (excluding keys that don't have underscores). If true the
* existing key will be overwritten. If false an error will be thrown.
* @returns {object} The passed-in modified-in-place object.
*/