1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * This module exports a provider that provides a heuristic result. The result
7 * either vists a URL or does a search with the current engine. This result is
8 * always the ultimate fallback for any query, so this provider is always active.
14 } from "resource:///modules/UrlbarUtils.sys.mjs";
18 ChromeUtils.defineESModuleGetters(lazy, {
19 UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
20 UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
21 UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
22 UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
26 * Class used to create the provider.
28 class ProviderHeuristicFallback extends UrlbarProvider {
34 * Returns the name of this provider.
36 * @returns {string} the name of this provider.
39 return "HeuristicFallback";
43 * Returns the type of this provider.
45 * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
48 return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
52 * Whether this provider should be invoked for the given context.
53 * If this method returns false, the providers manager won't start a query
54 * with this provider, to save on resources.
56 * @returns {boolean} Whether this provider should be invoked for the search.
63 * Gets the provider's priority.
65 * @returns {number} The provider's priority for the given query.
74 * @param {object} queryContext The query context object
75 * @param {Function} addCallback Callback invoked by the provider to add a new
77 * @returns {Promise} resolved when the query stops.
79 async startQuery(queryContext, addCallback) {
80 let instance = this.queryInstance;
82 let result = this._matchUnknownUrl(queryContext);
84 addCallback(this, result);
85 // Since we can't tell if this is a real URL and whether the user wants
86 // to visit or search for it, we provide an alternative searchengine
87 // match if the string looks like an alphanumeric origin or an e-mail.
88 let str = queryContext.searchString;
93 lazy.UrlbarPrefs.get("keyword.enabled") &&
94 (lazy.UrlbarTokenizer.looksLikeOrigin(str, {
98 lazy.UrlbarTokenizer.REGEXP_COMMON_EMAIL.test(str))
100 let searchResult = await this._engineSearchResult(queryContext);
101 if (instance != this.queryInstance) {
104 addCallback(this, searchResult);
110 result = await this._searchModeKeywordResult(queryContext);
111 if (instance != this.queryInstance) {
115 addCallback(this, result);
120 lazy.UrlbarPrefs.get("keyword.enabled") ||
121 queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH ||
122 queryContext.searchMode
124 result = await this._engineSearchResult(queryContext);
125 if (instance != this.queryInstance) {
129 result.heuristic = true;
130 addCallback(this, result);
135 // TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the
136 // scheme isn't specificed.
137 _matchUnknownUrl(queryContext) {
138 // The user may have typed something like "word?" to run a search. We
139 // should not convert that to a URL. We should also never convert actual
140 // URLs into URL results when search mode is active or a search mode
141 // restriction token was typed.
143 queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH ||
144 lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(
145 queryContext.restrictToken?.value
147 queryContext.searchMode
152 let unescapedSearchString = UrlbarUtils.unEscapeURIForUI(
153 queryContext.searchString
155 let [prefix, suffix] = UrlbarUtils.stripURLPrefix(unescapedSearchString);
156 if (!suffix && prefix) {
157 // The user just typed a stripped protocol, don't build a non-sense url
158 // like http://http/ for it.
162 let searchUrl = queryContext.trimmedSearchString;
164 if (queryContext.fixupError) {
166 queryContext.fixupError == Cr.NS_ERROR_MALFORMED_URI &&
167 !lazy.UrlbarPrefs.get("keyword.enabled")
169 let result = new lazy.UrlbarResult(
170 UrlbarUtils.RESULT_TYPE.URL,
171 UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
172 ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
173 fallbackTitle: [searchUrl, UrlbarUtils.HIGHLIGHT.NONE],
174 url: [searchUrl, UrlbarUtils.HIGHLIGHT.NONE],
177 result.heuristic = true;
184 // If the URI cannot be fixed or the preferred URI would do a keyword search,
185 // that basically means this isn't useful to us. Note that
186 // fixupInfo.keywordAsSent will never be true if the keyword.enabled pref
187 // is false or there are no engines, so in that case we will always return
189 if (!queryContext.fixupInfo?.href || queryContext.fixupInfo?.isSearch) {
193 let uri = new URL(queryContext.fixupInfo.href);
194 // Check the host, as "http:///" is a valid nsIURI, but not useful to us.
195 // But, some schemes are expected to have no host. So we check just against
196 // schemes we know should have a host. This allows new schemes to be
197 // implemented without us accidentally blocking access to them.
198 let hostExpected = ["http:", "https:", "ftp:", "chrome:"].includes(
201 if (hostExpected && !uri.host) {
205 // getFixupURIInfo() escaped the URI, so it may not be pretty. Embed the
206 // escaped URL in the result since that URL should be "canonical". But
207 // pass the pretty, unescaped URL as the result's title, since it is
208 // displayed to the user.
209 let escapedURL = uri.toString();
210 let displayURL = UrlbarUtils.prepareUrlForDisplay(uri, {
212 // If the user didn't type a protocol, and we added one, don't show it,
213 // as https-first may upgrade it, potentially breaking expectations.
217 // We don't know if this url is in Places or not, and checking that would
218 // be expensive. Thus we also don't know if we may have an icon.
219 // If we'd just try to fetch the icon for the typed string, we'd cause icon
220 // flicker, since the url keeps changing while the user types.
221 // By default we won't provide an icon, but for the subset of urls with a
222 // host we'll check for a typed slash and set favicon for the host part.
224 if (hostExpected && (searchUrl.endsWith("/") || uri.pathname.length > 1)) {
225 // Look for an icon with the entire URL except for the pathname, including
226 // scheme, usernames, passwords, hostname, and port.
227 let pathIndex = uri.toString().lastIndexOf(uri.pathname);
228 let prePath = uri.toString().slice(0, pathIndex);
229 iconUri = `page-icon:${prePath}/`;
232 let result = new lazy.UrlbarResult(
233 UrlbarUtils.RESULT_TYPE.URL,
234 UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
235 ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
236 fallbackTitle: [displayURL, UrlbarUtils.HIGHLIGHT.NONE],
237 url: [escapedURL, UrlbarUtils.HIGHLIGHT.NONE],
241 result.heuristic = true;
245 async _searchModeKeywordResult(queryContext) {
246 if (!queryContext.tokens.length) {
250 let firstToken = queryContext.tokens[0].value;
251 if (!lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(firstToken)) {
255 // At this point, the search string starts with a token that can be
256 // converted into search mode.
257 // Now we need to determine what to do based on the remainder of the search
258 // string. If the remainder starts with a space, then we should enter
259 // search mode, so we should continue below and create the result.
260 // Otherwise, we should not enter search mode, and in that case, the search
261 // string will look like one of the following:
263 // * The search string ends with the restriction token (e.g., the user
264 // has typed only the token by itself, with no trailing spaces).
265 // * More tokens exist, but there's no space between the restriction
266 // token and the following token. This is possible because the tokenizer
267 // does not require spaces between a restriction token and the remainder
268 // of the search string. In this case, we should not enter search mode.
270 // If we return null here and thereby do not enter search mode, then we'll
271 // continue on to _engineSearchResult, and the heuristic will be a
272 // default engine search result.
273 let query = UrlbarUtils.substringAfter(
274 queryContext.searchString,
277 if (!lazy.UrlbarTokenizer.REGEXP_SPACES_START.test(query)) {
282 if (queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH) {
283 result = await this._engineSearchResult(queryContext, firstToken);
285 result = new lazy.UrlbarResult(
286 UrlbarUtils.RESULT_TYPE.SEARCH,
287 UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
288 ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
289 query: [query.trimStart(), UrlbarUtils.HIGHLIGHT.NONE],
290 keyword: [firstToken, UrlbarUtils.HIGHLIGHT.NONE],
294 result.heuristic = true;
298 async _engineSearchResult(queryContext, keyword = null) {
300 if (queryContext.searchMode?.engineName) {
301 engine = lazy.UrlbarSearchUtils.getEngineByName(
302 queryContext.searchMode.engineName
305 engine = lazy.UrlbarSearchUtils.getDefaultEngine(queryContext.isPrivate);
312 // Strip a leading search restriction char, because we prepend it to text
313 // when the search shortcut is used and it's not user typed. Don't strip
314 // other restriction chars, so that it's possible to search for things
315 // including one of those (e.g. "c#").
316 let query = queryContext.searchString;
318 queryContext.tokens[0] &&
319 queryContext.tokens[0].value === lazy.UrlbarTokenizer.RESTRICT.SEARCH
321 query = UrlbarUtils.substringAfter(
323 queryContext.tokens[0].value
327 return new lazy.UrlbarResult(
328 UrlbarUtils.RESULT_TYPE.SEARCH,
329 UrlbarUtils.RESULT_SOURCE.SEARCH,
330 ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
331 engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
332 icon: UrlbarUtils.ICON.SEARCH_GLASS,
333 query: [query, UrlbarUtils.HIGHLIGHT.NONE],
334 keyword: keyword ? [keyword, UrlbarUtils.HIGHLIGHT.NONE] : undefined,
340 export var UrlbarProviderHeuristicFallback = new ProviderHeuristicFallback();