summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/UrlbarUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/urlbar/UrlbarUtils.jsm1758
1 files changed, 1758 insertions, 0 deletions
diff --git a/browser/components/urlbar/UrlbarUtils.jsm b/browser/components/urlbar/UrlbarUtils.jsm
new file mode 100644
index 0000000000..b8fd02a5fe
--- /dev/null
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -0,0 +1,1758 @@
+/* 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";
+
+/**
+ * This module exports the UrlbarUtils singleton, which contains constants and
+ * helper functions that are useful to all components of the urlbar.
+ */
+
+var EXPORTED_SYMBOLS = [
+ "UrlbarMuxer",
+ "UrlbarProvider",
+ "UrlbarQueryContext",
+ "UrlbarUtils",
+ "SkippableTimer",
+];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+ FormHistory: "resource://gre/modules/FormHistory.jsm",
+ Log: "resource://gre/modules/Log.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ SearchSuggestionController:
+ "resource://gre/modules/SearchSuggestionController.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+});
+
+var UrlbarUtils = {
+ // Extensions are allowed to add suggestions if they have registered a keyword
+ // with the omnibox API. This is the maximum number of suggestions an extension
+ // is allowed to add for a given search string.
+ // This value includes the heuristic result.
+ MAXIMUM_ALLOWED_EXTENSION_MATCHES: 6,
+
+ // This is used by UnifiedComplete, the new implementation will use
+ // PROVIDER_TYPE and RESULT_TYPE
+ RESULT_GROUP: {
+ HEURISTIC: "heuristic",
+ GENERAL: "general",
+ SUGGESTION: "suggestion",
+ EXTENSION: "extension",
+ },
+
+ // Defines provider types.
+ PROVIDER_TYPE: {
+ // Should be executed immediately, because it returns heuristic results
+ // that must be handed to the user asap.
+ 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 created at runtime, for example by an extension.
+ DYNAMIC: 8,
+
+ // 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,
+ },
+
+ /**
+ * Buckets used for logging telemetry to the FX_URLBAR_SELECTED_RESULT_TYPE_2
+ * histogram.
+ */
+ SELECTED_RESULT_TYPES: {
+ autofill: 0,
+ bookmark: 1,
+ history: 2,
+ keyword: 3,
+ searchengine: 4,
+ searchsuggestion: 5,
+ switchtab: 6,
+ tag: 7,
+ visiturl: 8,
+ remotetab: 9,
+ extension: 10,
+ "preloaded-top-site": 11, // This is currently unused.
+ tip: 12,
+ topsite: 13,
+ formhistory: 14,
+ dynamic: 15,
+ tabtosearch: 16,
+ // n_values = 32, so you'll need to create a new histogram if you need more.
+ },
+
+ // 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://browser/content/extension.svg",
+ HISTORY: "chrome://browser/skin/history.svg",
+ SEARCH_GLASS: "chrome://browser/skin/search-glass.svg",
+ SEARCH_GLASS_INVERTED: "chrome://browser/skin/search-glass-inverted.svg",
+ TIP: "chrome://browser/skin/tip.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,
+ },
+
+ // UnifiedComplete's autocomplete results store their titles and tags together
+ // in their comments. This separator is used to separate them. When we
+ // rewrite UnifiedComplete for quantumbar, 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+)?$/,
+
+ // Names of engines shipped in Firefox that search the web in general. These
+ // are used to update the input placeholder when entering search mode.
+ // TODO (Bug 1658661): Don't hardcode this list; store search engine category
+ // information someplace better.
+ WEB_ENGINE_NAMES: new Set([
+ "百度", // Baidu
+ "百度搜索", // "Baidu Search", the name of Baidu's OpenSearch engine.
+ "Bing",
+ "DuckDuckGo",
+ "Ecosia",
+ "Google",
+ "Qwant",
+ "Yandex",
+ "Яндекс", // Yandex, non-EN
+ ]),
+
+ // 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",
+ "other",
+ "shortcut",
+ "tabmenu",
+ "tabtosearch",
+ "tabtosearch_onboard",
+ "topsites_newtab",
+ "topsites_urlbar",
+ "touchbar",
+ "typed",
+ ]),
+
+ // 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: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ icon: "chrome://browser/skin/bookmark.svg",
+ pref: "shortcuts.bookmarks",
+ },
+ {
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ restrict: UrlbarTokenizer.RESTRICT.OPENPAGE,
+ icon: "chrome://browser/skin/tab.svg",
+ pref: "shortcuts.tabs",
+ },
+ {
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ restrict: UrlbarTokenizer.RESTRICT.HISTORY,
+ icon: "chrome://browser/skin/history.svg",
+ pref: "shortcuts.history",
+ },
+ ];
+ },
+
+ /**
+ * 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 (
+ !PrivateBrowsingUtils.isWindowPrivate(window) &&
+ url &&
+ !url.includes(" ") &&
+ // eslint-disable-next-line no-control-regex
+ !/[\x00-\x1F]/.test(url)
+ ) {
+ 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}
+ * @resolves { 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 PlacesUtils.keywords.fetch(keyword);
+ } catch (ex) {
+ Cu.reportError(`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 BrowserUtils.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.data = 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 { lowerCaseValue: needle } of tokens) {
+ // 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++;
+ }
+ }
+ }
+ }
+ // 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;
+ },
+
+ /**
+ * Extracts an url from a result, if possible.
+ * @param {UrlbarResult} result The result to extract from.
+ * @returns {object} a {url, postData} object, or null if a url can't be built
+ * from this result.
+ */
+ getUrlFromResult(result) {
+ switch (result.type) {
+ case UrlbarUtils.RESULT_TYPE.URL:
+ case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
+ case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ return { url: result.payload.url, postData: null };
+ case UrlbarUtils.RESULT_TYPE.KEYWORD:
+ return {
+ url: result.payload.url,
+ postData: result.payload.postData
+ ? this.getPostDataStream(result.payload.postData)
+ : null,
+ };
+ case UrlbarUtils.RESULT_TYPE.SEARCH: {
+ if (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 };
+ }
+ break;
+ }
+ case UrlbarUtils.RESULT_TYPE.TIP: {
+ // Return the button URL. Consumers must check payload.helpUrl
+ // themselves if they need the tip's help link.
+ return { url: result.payload.buttonUrl, postData: null };
+ }
+ }
+ return { url: null, 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
+ );
+ },
+
+ /**
+ * Get the number of rows a result should span in the autocomplete dropdown.
+ *
+ * @param {UrlbarResult} result The result being created.
+ * @returns {number}
+ * The number of rows the result should span in the autocomplete
+ * dropdown.
+ */
+ getSpanForResult(result) {
+ 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;
+ },
+
+ /**
+ * 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 == UrlbarTokenizer.RESTRICT.SEARCH) {
+ return {
+ engineName: 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.
+ * @param {nsISearchEngine|nsIURI|URL|string} urlOrEngine entity to initiate
+ * a speculative connection for.
+ * @param {window} window the window from where the connection is initialized.
+ * @note This is not infallible, if a speculative connection cannot be
+ * initialized, it will be a no-op.
+ */
+ setupSpeculativeConnection(urlOrEngine, window) {
+ if (!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 (urlOrEngine instanceof URL) {
+ urlOrEngine = urlOrEngine.href;
+ }
+
+ try {
+ let uri =
+ urlOrEngine instanceof Ci.nsIURI
+ ? urlOrEngine
+ : Services.io.newURI(urlOrEngine);
+ Services.io.speculativeConnect(
+ uri,
+ window.gBrowser.contentPrincipal,
+ null
+ );
+ } catch (ex) {
+ // Can't setup speculative connection for this url, just ignore it.
+ }
+ },
+
+ /**
+ * Strips parts of a URL defined in `options`.
+ *
+ * @param {string} spec
+ * The text to modify.
+ * @param {object} options
+ * @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 `#`.
+ * @returns {array} [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;
+ }
+ return [spec, prefix, suffix];
+ },
+
+ /**
+ * Strips a PSL verified public suffix from an hostname.
+ * @param {string} host A host name.
+ * @returns {string} Host name without the public suffix.
+ * @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.
+ */
+ 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) {
+ while (true) {
+ 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;
+ },
+
+ async addToInputHistory(url, input) {
+ await 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 }
+ );
+ });
+ },
+
+ /**
+ * 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
+ * @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, if present, two
+ * slashes. If the given string is not actually a URL, 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, then [prefix, remainder]. Otherwise, ["", str].
+ */
+ stripURLPrefix(str) {
+ const REGEXP_STRIP_PREFIX = /^[a-z]+:(?:\/){0,2}/i;
+ let match = REGEXP_STRIP_PREFIX.exec(str);
+ if (!match) {
+ return ["", str];
+ }
+ let prefix = match[0];
+ if (prefix.length < str.length && str[prefix.length] == " ") {
+ return ["", str];
+ }
+ return [prefix, str.substr(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 = BrowserWindowTracker.getTopWindow()
+ ) {
+ if (!searchString) {
+ throw new Error("Must pass a non-null search string");
+ }
+
+ let options = {
+ allowAutofill: false,
+ isPrivate: PrivateBrowsingUtils.isWindowPrivate(window),
+ maxResults: 1,
+ searchString,
+ userContextId: window.gBrowser.selectedBrowser.getAttribute(
+ "usercontextid"
+ ),
+ allowSearchSuggestions: false,
+ providers: ["UnifiedComplete", "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 UrlbarProvidersManager.startQuery(context);
+ if (!context.heuristicResult) {
+ throw new Error("There should always be an heuristic result");
+ }
+ return context.heuristicResult;
+ },
+
+ /**
+ * Creates a logger.
+ * Logging level can be controlled through browser.urlbar.loglevel.
+ * @param {string} [prefix] Prefix to use for the logged messages, "::" will
+ * be appended automatically to the prefix.
+ * @returns {object} The logger.
+ */
+ getLogger({ prefix = "" } = {}) {
+ if (!this._logger) {
+ this._logger = Log.repository.getLogger("urlbar");
+ this._logger.manageLevelFromPref("browser.urlbar.loglevel");
+ this._logger.addAppender(
+ new Log.ConsoleAppender(new Log.BasicFormatter())
+ );
+ }
+ if (prefix) {
+ // This is not an early return because it is necessary to invoke getLogger
+ // at least once before getLoggerWithMessagePrefix; it replaces a
+ // method of the original logger, rather than using an actual Proxy.
+ return Log.repository.getLoggerWithMessagePrefix("urlbar", prefix + "::");
+ }
+ return this._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 > SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH
+ ) {
+ return Promise.resolve();
+ }
+ return new Promise((resolve, reject) => {
+ FormHistory.update(
+ {
+ op: "bump",
+ fieldname: input.formHistoryName,
+ value,
+ source,
+ },
+ {
+ handleError: reject,
+ handleCompletion: resolve,
+ }
+ );
+ });
+ },
+
+ /**
+ * Extracts a telemetry type from a result, used by scalars and event
+ * telemetry.
+ *
+ * @param {UrlbarResult} result The result to analyze.
+ * @returns {string} A string type for 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.
+ */
+ telemetryTypeFromResult(result) {
+ if (!result) {
+ return "unknown";
+ }
+ switch (result.type) {
+ case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ return "switchtab";
+ case UrlbarUtils.RESULT_TYPE.SEARCH:
+ if (result.source == UrlbarUtils.RESULT_SOURCE.HISTORY) {
+ return "formhistory";
+ }
+ if (result.providerName == "TabToSearch") {
+ return "tabtosearch";
+ }
+ return result.payload.suggestion ? "searchsuggestion" : "searchengine";
+ case UrlbarUtils.RESULT_TYPE.URL:
+ if (result.autofill) {
+ return "autofill";
+ }
+ if (
+ result.source == UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL &&
+ result.heuristic
+ ) {
+ return "visiturl";
+ }
+ return result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS
+ ? "bookmark"
+ : "history";
+ 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";
+ }
+ return "unknown";
+ },
+};
+
+XPCOMUtils.defineLazyGetter(UrlbarUtils.ICON, "DEFAULT", () => {
+ return PlacesUtils.favicons.defaultFavicon.spec;
+});
+
+XPCOMUtils.defineLazyGetter(UrlbarUtils, "strings", () => {
+ return Services.strings.createBundle(
+ "chrome://global/locale/autocomplete.properties"
+ );
+});
+
+/**
+ * Payload JSON schemas for each result type. Payloads are validated against
+ * these schemas using JsonSchemaValidator.jsm.
+ */
+UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
+ [UrlbarUtils.RESULT_TYPE.TAB_SWITCH]: {
+ type: "object",
+ required: ["url"],
+ properties: {
+ displayUrl: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ title: {
+ type: "string",
+ },
+ url: {
+ type: "string",
+ },
+ userContextId: {
+ type: "number",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.SEARCH]: {
+ type: "object",
+ properties: {
+ displayUrl: {
+ type: "string",
+ },
+ engine: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ inPrivateWindow: {
+ type: "boolean",
+ },
+ isPinned: {
+ type: "boolean",
+ },
+ isPrivateEngine: {
+ type: "boolean",
+ },
+ keyword: {
+ type: "string",
+ },
+ lowerCaseSuggestion: {
+ type: "string",
+ },
+ providesSearchMode: {
+ type: "boolean",
+ },
+ query: {
+ type: "string",
+ },
+ satisfiesAutofillThreshold: {
+ type: "boolean",
+ },
+ suggestion: {
+ type: "string",
+ },
+ tail: {
+ type: "string",
+ },
+ tailPrefix: {
+ type: "string",
+ },
+ tailOffsetIndex: {
+ type: "number",
+ },
+ title: {
+ type: "string",
+ },
+ url: {
+ type: "string",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.URL]: {
+ type: "object",
+ required: ["url"],
+ properties: {
+ displayUrl: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ isPinned: {
+ type: "boolean",
+ },
+ isSponsored: {
+ type: "boolean",
+ },
+ sendAttributionRequest: {
+ type: "boolean",
+ },
+ tags: {
+ type: "array",
+ items: {
+ type: "string",
+ },
+ },
+ title: {
+ type: "string",
+ },
+ url: {
+ type: "string",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.KEYWORD]: {
+ type: "object",
+ required: ["keyword", "url"],
+ properties: {
+ displayUrl: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ input: {
+ type: "string",
+ },
+ keyword: {
+ type: "string",
+ },
+ postData: {
+ type: "string",
+ },
+ title: {
+ type: "string",
+ },
+ url: {
+ type: "string",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.OMNIBOX]: {
+ type: "object",
+ required: ["keyword"],
+ properties: {
+ content: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ keyword: {
+ type: "string",
+ },
+ title: {
+ type: "string",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.REMOTE_TAB]: {
+ type: "object",
+ required: ["device", "url"],
+ properties: {
+ device: {
+ type: "string",
+ },
+ displayUrl: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ title: {
+ type: "string",
+ },
+ url: {
+ type: "string",
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.TIP]: {
+ type: "object",
+ required: ["type"],
+ properties: {
+ // Prefer `buttonTextData` if your string is translated. This is for
+ // untranslated strings.
+ buttonText: {
+ type: "string",
+ },
+ // l10n { id, args }
+ buttonTextData: {
+ type: "object",
+ required: ["id"],
+ properties: {
+ id: {
+ type: "string",
+ },
+ args: {
+ type: "array",
+ },
+ },
+ },
+ buttonUrl: {
+ type: "string",
+ },
+ helpUrl: {
+ type: "string",
+ },
+ icon: {
+ type: "string",
+ },
+ // Prefer `text` if your string is translated. This is for untranslated
+ // strings.
+ text: {
+ type: "string",
+ },
+ // l10n { id, args }
+ textData: {
+ type: "object",
+ required: ["id"],
+ properties: {
+ id: {
+ type: "string",
+ },
+ args: {
+ type: "array",
+ },
+ },
+ },
+ // `type` is used in the names of keys in the `urlbar.tips` keyed scalar
+ // telemetry (see telemetry.rst). If you add a new type, then you are
+ // also adding new `urlbar.tips` keys and therefore need an expanded data
+ // collection review.
+ type: {
+ type: "string",
+ enum: [
+ "extension",
+ "intervention_clear",
+ "intervention_refresh",
+ "intervention_update_ask",
+ "intervention_update_refresh",
+ "intervention_update_restart",
+ "intervention_update_web",
+ "searchTip_onboard",
+ "searchTip_redirect",
+ "test", // for tests only
+ ],
+ },
+ },
+ },
+ [UrlbarUtils.RESULT_TYPE.DYNAMIC]: {
+ type: "object",
+ required: ["dynamicType"],
+ properties: {
+ dynamicType: {
+ type: "string",
+ },
+ // If `shouldNavigate` is `true` and the payload contains a `url`
+ // property, when the result is selected the browser will navigate to the
+ // `url`.
+ shouldNavigate: {
+ type: "boolean",
+ },
+ },
+ },
+};
+
+/**
+ * UrlbarQueryContext defines a user's autocomplete input from within the urlbar.
+ * It supplements it with details of how the search results should be obtained
+ * and what they consist of.
+ */
+class UrlbarQueryContext {
+ /**
+ * Constructs the UrlbarQueryContext instance.
+ *
+ * @param {object} options
+ * The initial options for UrlbarQueryContext.
+ * @param {string} options.searchString
+ * The string the user entered in autocomplete. Could be the empty string
+ * in the case of the user opening the popup via the mouse.
+ * @param {boolean} options.isPrivate
+ * Set to true if this query was started from a private browsing window.
+ * @param {number} options.maxResults
+ * The maximum number of results that will be displayed for this query.
+ * @param {boolean} options.allowAutofill
+ * Whether or not to allow providers to include autofill results.
+ * @param {number} options.userContextId
+ * The container id where this context was generated, if any.
+ * @param {array} [options.sources]
+ * A list of acceptable UrlbarUtils.RESULT_SOURCE for the context.
+ * @param {object} [options.searchMode]
+ * The input's current search mode. See UrlbarInput.setSearchMode for a
+ * description.
+ * @param {boolean} [options.allowSearchSuggestions]
+ * Whether to allow search suggestions. This is a veto, meaning that when
+ * false, suggestions will not be fetched, but when true, some other
+ * condition may still prohibit suggestions, like private browsing mode.
+ * Defaults to true.
+ * @param {string} [options.formHistoryName]
+ * The name under which the local form history is registered.
+ */
+ constructor(options = {}) {
+ this._checkRequiredOptions(options, [
+ "allowAutofill",
+ "isPrivate",
+ "maxResults",
+ "searchString",
+ ]);
+
+ if (isNaN(parseInt(options.maxResults))) {
+ throw new Error(
+ `Invalid maxResults property provided to UrlbarQueryContext`
+ );
+ }
+
+ // Manage optional properties of options.
+ for (let [prop, checkFn, defaultValue] of [
+ ["allowSearchSuggestions", v => true, true],
+ ["currentPage", v => typeof v == "string" && !!v.length],
+ ["formHistoryName", v => typeof v == "string" && !!v.length],
+ ["providers", v => Array.isArray(v) && v.length],
+ ["searchMode", v => v && typeof v == "object"],
+ ["sources", v => Array.isArray(v) && v.length],
+ ]) {
+ if (prop in options) {
+ if (!checkFn(options[prop])) {
+ throw new Error(`Invalid value for option "${prop}"`);
+ }
+ this[prop] = options[prop];
+ } else if (defaultValue !== undefined) {
+ this[prop] = defaultValue;
+ }
+ }
+
+ this.lastResultCount = 0;
+ // Note that Set is not serializable through JSON, so these may not be
+ // easily shared with add-ons.
+ this.pendingHeuristicProviders = new Set();
+ this.deferUserSelectionProviders = new Set();
+ this.trimmedSearchString = this.searchString.trim();
+ this.userContextId =
+ options.userContextId ||
+ Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+ }
+
+ /**
+ * Checks the required options, saving them as it goes.
+ *
+ * @param {object} options The options object to check.
+ * @param {array} optionNames The names of the options to check for.
+ * @throws {Error} Throws if there is a missing option.
+ */
+ _checkRequiredOptions(options, optionNames) {
+ for (let optionName of optionNames) {
+ if (!(optionName in options)) {
+ throw new Error(
+ `Missing or empty ${optionName} provided to UrlbarQueryContext`
+ );
+ }
+ this[optionName] = options[optionName];
+ }
+ }
+
+ /**
+ * Caches and returns fixup info from URIFixup for the current search string.
+ * Only returns a subset of the properties from URIFixup. This is both to
+ * reduce the memory footprint of UrlbarQueryContexts and to keep them
+ * serializable so they can be sent to extensions.
+ */
+ get fixupInfo() {
+ if (this.trimmedSearchString && !this._fixupInfo) {
+ let flags =
+ Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ if (this.isPrivate) {
+ flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+
+ try {
+ let info = Services.uriFixup.getFixupURIInfo(
+ this.trimmedSearchString,
+ flags
+ );
+ this._fixupInfo = {
+ href: info.fixedURI.spec,
+ isSearch: !!info.keywordAsSent,
+ };
+ } catch (ex) {
+ this._fixupError = ex.result;
+ }
+ }
+
+ return this._fixupInfo || null;
+ }
+
+ /**
+ * Returns the error that was thrown when fixupInfo was fetched, if any. If
+ * fixupInfo has not yet been fetched for this queryContext, it is fetched
+ * here.
+ */
+ get fixupError() {
+ if (!this.fixupInfo) {
+ return this._fixupError;
+ }
+
+ return null;
+ }
+}
+
+/**
+ * Base class for a muxer.
+ * The muxer scope is to sort a given list of results.
+ */
+class UrlbarMuxer {
+ /**
+ * Unique name for the muxer, used by the context to sort results.
+ * Not using a unique name will cause the newest registration to win.
+ * @abstract
+ */
+ get name() {
+ return "UrlbarMuxerBase";
+ }
+
+ /**
+ * Sorts queryContext results in-place.
+ * @param {UrlbarQueryContext} queryContext the context to sort results for.
+ * @abstract
+ */
+ sort(queryContext) {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+}
+
+/**
+ * Base class for a provider.
+ * The provider scope is to query a datasource and return results from it.
+ */
+class UrlbarProvider {
+ constructor() {
+ XPCOMUtils.defineLazyGetter(this, "logger", () =>
+ UrlbarUtils.getLogger({ prefix: `Provider.${this.name}` })
+ );
+ }
+
+ /**
+ * Unique name for the provider, used by the context to filter on providers.
+ * Not using a unique name will cause the newest registration to win.
+ * @abstract
+ */
+ get name() {
+ return "UrlbarProviderBase";
+ }
+
+ /**
+ * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
+ * @abstract
+ */
+ get type() {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+
+ /**
+ * Calls a method on the provider in a try-catch block and reports any error.
+ * Unlike most other provider methods, `tryMethod` is not intended to be
+ * overridden.
+ *
+ * @param {string} methodName The name of the method to call.
+ * @param {*} args The method arguments.
+ * @returns {*} The return value of the method, or undefined if the method
+ * throws an error.
+ * @abstract
+ */
+ tryMethod(methodName, ...args) {
+ try {
+ return this[methodName](...args);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ return undefined;
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ * @abstract
+ */
+ isActive(queryContext) {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+
+ /**
+ * Gets the provider's priority. Priorities are numeric values starting at
+ * zero and increasing in value. Smaller values are lower priorities, and
+ * larger values are higher priorities. For a given query, `startQuery` is
+ * called on only the active and highest-priority providers.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ * @abstract
+ */
+ getPriority(queryContext) {
+ // By default, all providers share the lowest priority.
+ return 0;
+ }
+
+ /**
+ * Starts querying.
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @param {function} addCallback Callback invoked by the provider to add a new
+ * result. A UrlbarResult should be passed to it.
+ * @note Extended classes should return a Promise resolved when the provider
+ * is done searching AND returning results.
+ * @abstract
+ */
+ startQuery(queryContext, addCallback) {
+ throw new Error("Trying to access the base class, must be overridden");
+ }
+
+ /**
+ * Cancels a running query,
+ * @param {UrlbarQueryContext} queryContext the query context object to cancel
+ * query for.
+ * @abstract
+ */
+ cancelQuery(queryContext) {
+ // Override this with your clean-up on cancel code.
+ }
+
+ /**
+ * Called when a result from the provider is picked, but currently only for
+ * tip and dynamic results. The provider should handle the pick. For tip
+ * results, this is called only when the tip's payload doesn't have a URL.
+ * For dynamic results, this is called when any selectable element in the
+ * result's view is picked.
+ *
+ * @param {UrlbarResult} result
+ * The result that was picked.
+ * @param {Element} element
+ * The element in the result's view that was picked.
+ * @abstract
+ */
+ pickResult(result, element) {}
+
+ /**
+ * Called when the user starts and ends an engagement with the urlbar.
+ *
+ * @param {boolean} isPrivate True if the engagement is in a private context.
+ * @param {string} state The state of the engagement, one of: start,
+ * engagement, abandonment, discard.
+ */
+ onEngagement(isPrivate, state) {}
+
+ /**
+ * Called when a result from the provider is selected. "Selected" refers to
+ * the user highlighing the result with the arrow keys/Tab, before it is
+ * picked. onSelection is also called when a user clicks a result. In the
+ * event of a click, onSelection is called just before pickResult. Note that
+ * this is called when heuristic results are pre-selected.
+ *
+ * @param {UrlbarResult} result
+ * The result that was selected.
+ * @param {Element} element
+ * The element in the result's view that was selected.
+ * @abstract
+ */
+ onSelection(result, element) {}
+
+ /**
+ * This is called only for dynamic result types, when the urlbar view updates
+ * the view of one of the results of the provider. It should return an object
+ * describing the view update that looks like this:
+ *
+ * {
+ * nodeNameFoo: {
+ * attributes: {
+ * someAttribute: someValue,
+ * },
+ * style: {
+ * someStyleProperty: someValue,
+ * },
+ * l10n: {
+ * id: someL10nId,
+ * args: someL10nArgs,
+ * },
+ * textContent: "some text content",
+ * },
+ * nodeNameBar: {
+ * ...
+ * },
+ * nodeNameBaz: {
+ * ...
+ * },
+ * }
+ *
+ * The object should contain a property for each element to update in the
+ * dynamic result type view. The names of these properties are the names
+ * declared in the view template of the dynamic result type; see
+ * UrlbarView.addDynamicViewTemplate(). The values are similar to the nested
+ * objects specified in the view template but not quite the same; see below.
+ * For each property, the element in the view subtree with the specified name
+ * is updated according to the object in the property's value. If an
+ * element's name is not specified, then it will not be updated and will
+ * retain its current state.
+ *
+ * @param {UrlbarResult} result
+ * The result whose view will be updated.
+ * @param {Map} idsByName
+ * A Map from an element's name, as defined by the provider; to its ID in
+ * the DOM, as defined by the browser. The browser manages element IDs for
+ * dynamic results to prevent collisions. However, a provider may need to
+ * access the IDs of the elements created for its results. For example, to
+ * set various `aria` attributes.
+ * @returns {object}
+ * A view update object as described above. The names of properties are the
+ * the names of elements declared in the view template. The values of
+ * properties are objects that describe how to update each element, and
+ * these objects may include the following properties, all of which are
+ * optional:
+ *
+ * {object} [attributes]
+ * A mapping from attribute names to values. Each name-value pair results
+ * in an attribute being added to the element. The `id` attribute is
+ * reserved and cannot be set by the provider.
+ * {object} [style]
+ * A plain object that can be used to add inline styles to the element,
+ * like `display: none`. `element.style` is updated for each name-value
+ * pair in this object.
+ * {object} [l10n]
+ * An { id, args } object that will be passed to
+ * document.l10n.setAttributes().
+ * {string} [textContent]
+ * A string that will be set as `element.textContent`.
+ */
+ getViewUpdate(result, idsByName) {
+ return null;
+ }
+
+ /**
+ * Defines whether the view should defer user selection events while waiting
+ * for the first result from this provider.
+ *
+ * @returns {boolean} Whether the provider wants to defer user selection
+ * events.
+ * @see UrlbarEventBufferer
+ * @note UrlbarEventBufferer has a timeout after which user events will be
+ * processed regardless.
+ */
+ get deferUserSelection() {
+ return false;
+ }
+}
+
+/**
+ * Class used to create a timer that can be manually fired, to immediately
+ * invoke the callback, or canceled, as necessary.
+ * Examples:
+ * let timer = new SkippableTimer();
+ * // Invokes the callback immediately without waiting for the delay.
+ * await timer.fire();
+ * // Cancel the timer, the callback won't be invoked.
+ * await timer.cancel();
+ * // Wait for the timer to have elapsed.
+ * await timer.promise;
+ */
+class SkippableTimer {
+ /**
+ * Creates a skippable timer for the given callback and time.
+ * @param {object} options An object that configures the timer
+ * @param {string} options.name The name of the timer, logged when necessary
+ * @param {function} options.callback To be invoked when requested
+ * @param {number} options.time A delay in milliseconds to wait for
+ * @param {boolean} options.reportErrorOnTimeout If true and the timer times
+ * out, an error will be logged with Cu.reportError
+ * @param {logger} options.logger An optional logger
+ */
+ constructor({
+ name = "<anonymous timer>",
+ callback = null,
+ time = 0,
+ reportErrorOnTimeout = false,
+ logger = null,
+ } = {}) {
+ this.name = name;
+ this.logger = logger;
+
+ let timerPromise = new Promise(resolve => {
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._timer.initWithCallback(
+ () => {
+ this._log(`Timed out!`, reportErrorOnTimeout);
+ resolve();
+ },
+ time,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ this._log(`Started`);
+ });
+
+ let firePromise = new Promise(resolve => {
+ this.fire = () => {
+ this._log(`Skipped`);
+ resolve();
+ return this.promise;
+ };
+ });
+
+ this.promise = Promise.race([timerPromise, firePromise]).then(() => {
+ // If we've been canceled, don't call back.
+ if (this._timer && callback) {
+ callback();
+ }
+ });
+ }
+
+ /**
+ * Allows to cancel the timer and the callback won't be invoked.
+ * It is not strictly necessary to await for this, the promise can just be
+ * used to ensure all the internal work is complete.
+ * @returns {promise} Resolved once all the cancelation work is complete.
+ */
+ cancel() {
+ this._log(`Canceling`);
+ this._timer.cancel();
+ delete this._timer;
+ return this.fire();
+ }
+
+ _log(msg, isError = false) {
+ let line = `SkippableTimer :: ${this.name} :: ${msg}`;
+ if (this.logger) {
+ this.logger.debug(line);
+ }
+ if (isError) {
+ Cu.reportError(line);
+ }
+ }
+}