summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs')
-rw-r--r--browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs328
1 files changed, 328 insertions, 0 deletions
diff --git a/browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs b/browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs
new file mode 100644
index 0000000000..dd0b093b49
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs
@@ -0,0 +1,328 @@
+/* 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 a provider that provides a heuristic result. The result
+ * either vists a URL or does a search with the current engine. This result is
+ * always the ultimate fallback for any query, so this provider is always active.
+ */
+
+import {
+ UrlbarProvider,
+ UrlbarUtils,
+} from "resource:///modules/UrlbarUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
+});
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderHeuristicFallback extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the name of this provider.
+ *
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "HeuristicFallback";
+ }
+
+ /**
+ * Returns the type of this provider.
+ *
+ * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+ */
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
+ }
+
+ /**
+ * 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.
+ */
+ isActive(queryContext) {
+ return true;
+ }
+
+ /**
+ * Gets the provider's priority.
+ *
+ * @param {UrlbarQueryContext} queryContext The query context object
+ * @returns {number} The provider's priority for the given query.
+ */
+ getPriority(queryContext) {
+ return 0;
+ }
+
+ /**
+ * Starts querying.
+ *
+ * @param {object} queryContext The query context object
+ * @param {Function} addCallback Callback invoked by the provider to add a new
+ * result.
+ * @returns {Promise} resolved when the query stops.
+ */
+ async startQuery(queryContext, addCallback) {
+ let instance = this.queryInstance;
+
+ let result = this._matchUnknownUrl(queryContext);
+ if (result) {
+ addCallback(this, result);
+ // Since we can't tell if this is a real URL and whether the user wants
+ // to visit or search for it, we provide an alternative searchengine
+ // match if the string looks like an alphanumeric origin or an e-mail.
+ let str = queryContext.searchString;
+ try {
+ new URL(str);
+ } catch (ex) {
+ if (
+ lazy.UrlbarPrefs.get("keyword.enabled") &&
+ (lazy.UrlbarTokenizer.looksLikeOrigin(str, {
+ noIp: true,
+ noPort: true,
+ }) ||
+ lazy.UrlbarTokenizer.REGEXP_COMMON_EMAIL.test(str))
+ ) {
+ let searchResult = this._engineSearchResult(queryContext);
+ if (instance != this.queryInstance) {
+ return;
+ }
+ addCallback(this, searchResult);
+ }
+ }
+ return;
+ }
+
+ result = this._searchModeKeywordResult(queryContext);
+ if (result) {
+ addCallback(this, result);
+ return;
+ }
+
+ result = this._engineSearchResult(queryContext);
+ if (instance != this.queryInstance) {
+ return;
+ }
+ if (result) {
+ result.heuristic = true;
+ addCallback(this, result);
+ }
+ }
+
+ // TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the
+ // scheme isn't specificed.
+ _matchUnknownUrl(queryContext) {
+ // The user may have typed something like "word?" to run a search. We
+ // should not convert that to a URL. We should also never convert actual
+ // URLs into URL results when search mode is active or a search mode
+ // restriction token was typed.
+ if (
+ queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH ||
+ lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(
+ queryContext.restrictToken?.value
+ ) ||
+ queryContext.searchMode
+ ) {
+ return null;
+ }
+
+ let unescapedSearchString = UrlbarUtils.unEscapeURIForUI(
+ queryContext.searchString
+ );
+ let [prefix, suffix] = UrlbarUtils.stripURLPrefix(unescapedSearchString);
+ if (!suffix && prefix) {
+ // The user just typed a stripped protocol, don't build a non-sense url
+ // like http://http/ for it.
+ return null;
+ }
+
+ let searchUrl = queryContext.trimmedSearchString;
+
+ if (queryContext.fixupError) {
+ if (
+ queryContext.fixupError == Cr.NS_ERROR_MALFORMED_URI &&
+ !lazy.UrlbarPrefs.get("keyword.enabled")
+ ) {
+ let result = new lazy.UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [searchUrl, UrlbarUtils.HIGHLIGHT.NONE],
+ url: [searchUrl, UrlbarUtils.HIGHLIGHT.NONE],
+ })
+ );
+ result.heuristic = true;
+ return result;
+ }
+
+ return null;
+ }
+
+ // If the URI cannot be fixed or the preferred URI would do a keyword search,
+ // that basically means this isn't useful to us. Note that
+ // fixupInfo.keywordAsSent will never be true if the keyword.enabled pref
+ // is false or there are no engines, so in that case we will always return
+ // a "visit".
+ if (!queryContext.fixupInfo?.href || queryContext.fixupInfo?.isSearch) {
+ return null;
+ }
+
+ let uri = new URL(queryContext.fixupInfo.href);
+ // Check the host, as "http:///" is a valid nsIURI, but not useful to us.
+ // But, some schemes are expected to have no host. So we check just against
+ // schemes we know should have a host. This allows new schemes to be
+ // implemented without us accidentally blocking access to them.
+ let hostExpected = ["http:", "https:", "ftp:", "chrome:"].includes(
+ uri.protocol
+ );
+ if (hostExpected && !uri.host) {
+ return null;
+ }
+
+ // getFixupURIInfo() escaped the URI, so it may not be pretty. Embed the
+ // escaped URL in the result since that URL should be "canonical". But
+ // pass the pretty, unescaped URL as the result's title, since it is
+ // displayed to the user.
+ let escapedURL = uri.toString();
+ let displayURL = decodeURI(uri);
+
+ // We don't know if this url is in Places or not, and checking that would
+ // be expensive. Thus we also don't know if we may have an icon.
+ // If we'd just try to fetch the icon for the typed string, we'd cause icon
+ // flicker, since the url keeps changing while the user types.
+ // By default we won't provide an icon, but for the subset of urls with a
+ // host we'll check for a typed slash and set favicon for the host part.
+ let iconUri;
+ if (hostExpected && (searchUrl.endsWith("/") || uri.pathname.length > 1)) {
+ // Look for an icon with the entire URL except for the pathname, including
+ // scheme, usernames, passwords, hostname, and port.
+ let pathIndex = uri.toString().lastIndexOf(uri.pathname);
+ let prePath = uri.toString().slice(0, pathIndex);
+ iconUri = `page-icon:${prePath}/`;
+ }
+
+ let result = new lazy.UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [displayURL, UrlbarUtils.HIGHLIGHT.NONE],
+ url: [escapedURL, UrlbarUtils.HIGHLIGHT.NONE],
+ icon: iconUri,
+ })
+ );
+ result.heuristic = true;
+ return result;
+ }
+
+ _searchModeKeywordResult(queryContext) {
+ if (!queryContext.tokens.length) {
+ return null;
+ }
+
+ let firstToken = queryContext.tokens[0].value;
+ if (!lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(firstToken)) {
+ return null;
+ }
+
+ // At this point, the search string starts with a token that can be
+ // converted into search mode.
+ // Now we need to determine what to do based on the remainder of the search
+ // string. If the remainder starts with a space, then we should enter
+ // search mode, so we should continue below and create the result.
+ // Otherwise, we should not enter search mode, and in that case, the search
+ // string will look like one of the following:
+ //
+ // * The search string ends with the restriction token (e.g., the user
+ // has typed only the token by itself, with no trailing spaces).
+ // * More tokens exist, but there's no space between the restriction
+ // token and the following token. This is possible because the tokenizer
+ // does not require spaces between a restriction token and the remainder
+ // of the search string. In this case, we should not enter search mode.
+ //
+ // If we return null here and thereby do not enter search mode, then we'll
+ // continue on to _engineSearchResult, and the heuristic will be a
+ // default engine search result.
+ let query = UrlbarUtils.substringAfter(
+ queryContext.searchString,
+ firstToken
+ );
+ if (!lazy.UrlbarTokenizer.REGEXP_SPACES_START.test(query)) {
+ return null;
+ }
+
+ let result;
+ if (queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH) {
+ result = this._engineSearchResult(queryContext, firstToken);
+ } else {
+ result = new lazy.UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ query: [query.trimStart(), UrlbarUtils.HIGHLIGHT.NONE],
+ keyword: [firstToken, UrlbarUtils.HIGHLIGHT.NONE],
+ })
+ );
+ }
+ result.heuristic = true;
+ return result;
+ }
+
+ _engineSearchResult(queryContext, keyword = null) {
+ let engine;
+ if (queryContext.searchMode?.engineName) {
+ engine = Services.search.getEngineByName(
+ queryContext.searchMode.engineName
+ );
+ } else {
+ engine = lazy.UrlbarSearchUtils.getDefaultEngine(queryContext.isPrivate);
+ }
+
+ if (!engine) {
+ return null;
+ }
+
+ // Strip a leading search restriction char, because we prepend it to text
+ // when the search shortcut is used and it's not user typed. Don't strip
+ // other restriction chars, so that it's possible to search for things
+ // including one of those (e.g. "c#").
+ let query = queryContext.searchString;
+ if (
+ queryContext.tokens[0] &&
+ queryContext.tokens[0].value === lazy.UrlbarTokenizer.RESTRICT.SEARCH
+ ) {
+ query = UrlbarUtils.substringAfter(
+ query,
+ queryContext.tokens[0].value
+ ).trim();
+ }
+
+ return new lazy.UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: engine.iconURI?.spec,
+ query: [query, UrlbarUtils.HIGHLIGHT.NONE],
+ keyword: keyword ? [keyword, UrlbarUtils.HIGHLIGHT.NONE] : undefined,
+ })
+ );
+ }
+}
+
+export var UrlbarProviderHeuristicFallback = new ProviderHeuristicFallback();