diff options
Diffstat (limited to 'browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs')
-rw-r--r-- | browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs | 328 |
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(); |