/* 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, { fallbackTitle: [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, { fallbackTitle: [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 = lazy.UrlbarSearchUtils.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();