Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / components / urlbar / UrlbarProviderHeuristicFallback.sys.mjs
blob9cdf53d74c845d5730f908be8102a96f0eb0a742
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/. */
5 /**
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.
9  */
11 import {
12   UrlbarProvider,
13   UrlbarUtils,
14 } from "resource:///modules/UrlbarUtils.sys.mjs";
16 const lazy = {};
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",
23 });
25 /**
26  * Class used to create the provider.
27  */
28 class ProviderHeuristicFallback extends UrlbarProvider {
29   constructor() {
30     super();
31   }
33   /**
34    * Returns the name of this provider.
35    *
36    * @returns {string} the name of this provider.
37    */
38   get name() {
39     return "HeuristicFallback";
40   }
42   /**
43    * Returns the type of this provider.
44    *
45    * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
46    */
47   get type() {
48     return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
49   }
51   /**
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.
55    *
56    * @returns {boolean} Whether this provider should be invoked for the search.
57    */
58   isActive() {
59     return true;
60   }
62   /**
63    * Gets the provider's priority.
64    *
65    * @returns {number} The provider's priority for the given query.
66    */
67   getPriority() {
68     return 0;
69   }
71   /**
72    * Starts querying.
73    *
74    * @param {object} queryContext The query context object
75    * @param {Function} addCallback Callback invoked by the provider to add a new
76    *        result.
77    * @returns {Promise} resolved when the query stops.
78    */
79   async startQuery(queryContext, addCallback) {
80     let instance = this.queryInstance;
82     let result = this._matchUnknownUrl(queryContext);
83     if (result) {
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;
89       try {
90         new URL(str);
91       } catch (ex) {
92         if (
93           lazy.UrlbarPrefs.get("keyword.enabled") &&
94           (lazy.UrlbarTokenizer.looksLikeOrigin(str, {
95             noIp: true,
96             noPort: true,
97           }) ||
98             lazy.UrlbarTokenizer.REGEXP_COMMON_EMAIL.test(str))
99         ) {
100           let searchResult = await this._engineSearchResult(queryContext);
101           if (instance != this.queryInstance) {
102             return;
103           }
104           addCallback(this, searchResult);
105         }
106       }
107       return;
108     }
110     result = await this._searchModeKeywordResult(queryContext);
111     if (instance != this.queryInstance) {
112       return;
113     }
114     if (result) {
115       addCallback(this, result);
116       return;
117     }
119     if (
120       lazy.UrlbarPrefs.get("keyword.enabled") ||
121       queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH ||
122       queryContext.searchMode
123     ) {
124       result = await this._engineSearchResult(queryContext);
125       if (instance != this.queryInstance) {
126         return;
127       }
128       if (result) {
129         result.heuristic = true;
130         addCallback(this, result);
131       }
132     }
133   }
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.
142     if (
143       queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH ||
144       lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(
145         queryContext.restrictToken?.value
146       ) ||
147       queryContext.searchMode
148     ) {
149       return null;
150     }
152     let unescapedSearchString = UrlbarUtils.unEscapeURIForUI(
153       queryContext.searchString
154     );
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.
159       return null;
160     }
162     let searchUrl = queryContext.trimmedSearchString;
164     if (queryContext.fixupError) {
165       if (
166         queryContext.fixupError == Cr.NS_ERROR_MALFORMED_URI &&
167         !lazy.UrlbarPrefs.get("keyword.enabled")
168       ) {
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],
175           })
176         );
177         result.heuristic = true;
178         return result;
179       }
181       return null;
182     }
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
188     // a "visit".
189     if (!queryContext.fixupInfo?.href || queryContext.fixupInfo?.isSearch) {
190       return null;
191     }
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(
199       uri.protocol
200     );
201     if (hostExpected && !uri.host) {
202       return null;
203     }
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, {
211       trimURL: false,
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.
214       schemeless: !prefix,
215     });
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.
223     let iconUri;
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}/`;
230     }
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],
238         icon: iconUri,
239       })
240     );
241     result.heuristic = true;
242     return result;
243   }
245   async _searchModeKeywordResult(queryContext) {
246     if (!queryContext.tokens.length) {
247       return null;
248     }
250     let firstToken = queryContext.tokens[0].value;
251     if (!lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(firstToken)) {
252       return null;
253     }
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:
262     //
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.
269     //
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,
275       firstToken
276     );
277     if (!lazy.UrlbarTokenizer.REGEXP_SPACES_START.test(query)) {
278       return null;
279     }
281     let result;
282     if (queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH) {
283       result = await this._engineSearchResult(queryContext, firstToken);
284     } else {
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],
291         })
292       );
293     }
294     result.heuristic = true;
295     return result;
296   }
298   async _engineSearchResult(queryContext, keyword = null) {
299     let engine;
300     if (queryContext.searchMode?.engineName) {
301       engine = lazy.UrlbarSearchUtils.getEngineByName(
302         queryContext.searchMode.engineName
303       );
304     } else {
305       engine = lazy.UrlbarSearchUtils.getDefaultEngine(queryContext.isPrivate);
306     }
308     if (!engine) {
309       return null;
310     }
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;
317     if (
318       queryContext.tokens[0] &&
319       queryContext.tokens[0].value === lazy.UrlbarTokenizer.RESTRICT.SEARCH
320     ) {
321       query = UrlbarUtils.substringAfter(
322         query,
323         queryContext.tokens[0].value
324       ).trim();
325     }
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,
335       })
336     );
337   }
340 export var UrlbarProviderHeuristicFallback = new ProviderHeuristicFallback();