Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / components / urlbar / UrlbarProviderAutofill.sys.mjs
blobcc12a497c3b35e4936635cdd3d1cbf83579a1e9d
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 an autofill result.
7  */
9 import {
10   UrlbarProvider,
11   UrlbarUtils,
12 } from "resource:///modules/UrlbarUtils.sys.mjs";
14 const lazy = {};
16 ChromeUtils.defineESModuleGetters(lazy, {
17   AboutPagesUtils: "resource://gre/modules/AboutPagesUtils.sys.mjs",
18   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
19   UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
20   UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
21   UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
22 });
24 // AutoComplete query type constants.
25 // Describes the various types of queries that we can process rows for.
26 const QUERYTYPE = {
27   AUTOFILL_ORIGIN: 1,
28   AUTOFILL_URL: 2,
29   AUTOFILL_ADAPTIVE: 3,
32 // Constants to support an alternative frecency algorithm.
33 const ORIGIN_USE_ALT_FRECENCY = Services.prefs.getBoolPref(
34   "places.frecency.origins.alternative.featureGate",
35   false
37 const ORIGIN_FRECENCY_FIELD = ORIGIN_USE_ALT_FRECENCY
38   ? "alt_frecency"
39   : "frecency";
41 // `WITH` clause for the autofill queries.
42 // A NULL frecency is normalized to 1.0, because the table doesn't support NULL.
43 // Because of that, here we must set a minimum threshold of 2.0, otherwise when
44 // all the visits are older than the cutoff, we'd check
45 // 0.0 (frecency) >= 0.0 (threshold) and autofill everything instead of nothing.
46 const SQL_AUTOFILL_WITH = ORIGIN_USE_ALT_FRECENCY
47   ? `
48     WITH
49     autofill_frecency_threshold(value) AS (
50       SELECT IFNULL(
51         (SELECT value FROM moz_meta WHERE key = 'origin_alt_frecency_threshold'),
52         2.0
53       )
54     )
55     `
56   : `
57     WITH
58     autofill_frecency_threshold(value) AS (
59       SELECT IFNULL(
60         (SELECT value FROM moz_meta WHERE key = 'origin_frecency_threshold'),
61         2.0
62       )
63     )
64   `;
66 const SQL_AUTOFILL_FRECENCY_THRESHOLD = `host_frecency >= (
67     SELECT value FROM autofill_frecency_threshold
68   )`;
70 function originQuery(where) {
71   // `frecency`, `n_bookmarks` and `visited` are partitioned by the fixed host,
72   // without `www.`. `host_prefix` instead is partitioned by full host, because
73   // we assume a prefix may not work regardless of `www.`.
74   let selectVisited = where.includes("visited")
75     ? `MAX(EXISTS(
76       SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0
77     )) OVER (PARTITION BY fixup_url(host)) > 0`
78     : "0";
79   let selectTitle;
80   let joinBookmarks;
81   if (where.includes("n_bookmarks")) {
82     selectTitle = "ifnull(b.title, iif(h.frecency <> 0, h.title, NULL))";
83     joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = h.id";
84   } else {
85     selectTitle = "iif(h.frecency <> 0, h.title, NULL)";
86     joinBookmarks = "";
87   }
88   return `/* do not warn (bug no): cannot use an index to sort */
89     ${SQL_AUTOFILL_WITH},
90     origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, n_bookmarks, visited) AS (
91       SELECT
92       id,
93       prefix,
94       first_value(prefix) OVER (
95         PARTITION BY host ORDER BY ${ORIGIN_FRECENCY_FIELD} DESC, prefix = "https://" DESC, id DESC
96       ),
97       host,
98       fixup_url(host),
99       total(${ORIGIN_FRECENCY_FIELD}) OVER (PARTITION BY fixup_url(host)),
100       ${ORIGIN_FRECENCY_FIELD},
101       total(
102         (SELECT total(foreign_count) FROM moz_places WHERE origin_id = o.id)
103       ) OVER (PARTITION BY fixup_url(host)),
104       ${selectVisited}
105       FROM moz_origins o
106       WHERE prefix NOT IN ('about:', 'place:')
107         AND ((host BETWEEN :searchString AND :searchString || X'FFFF')
108           OR (host BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF'))
109     ),
110     matched_origin(host_fixed, url) AS (
111       SELECT iif(instr(host, :searchString) = 1, host, fixed) || '/',
112              ifnull(:prefix, host_prefix) || host || '/'
113       FROM origins
114       ${where}
115       ORDER BY frecency DESC, n_bookmarks DESC, prefix = "https://" DESC, id DESC
116       LIMIT 1
117     ),
118     matched_place(host_fixed, url, id, title, frecency) AS (
119       SELECT o.host_fixed, o.url, h.id, h.title, h.frecency
120       FROM matched_origin o
121       LEFT JOIN moz_places h ON h.url_hash IN (
122         hash('https://' || o.host_fixed),
123         hash('https://www.' || o.host_fixed),
124         hash('http://' || o.host_fixed),
125         hash('http://www.' || o.host_fixed)
126       )
127       ORDER BY
128         h.title IS NOT NULL DESC,
129         h.title || '/' <> o.host_fixed DESC,
130         h.url = o.url DESC,
131         h.frecency DESC,
132         h.id DESC
133       LIMIT 1
134     )
135     SELECT :query_type AS query_type,
136            :searchString AS search_string,
137            h.host_fixed AS host_fixed,
138            h.url AS url,
139            ${selectTitle} AS title
140     FROM matched_place h
141     ${joinBookmarks}
142   `;
145 function urlQuery(where1, where2, isBookmarkContained) {
146   // We limit the search to places that are either bookmarked or have a frecency
147   // over some small, arbitrary threshold (20) in order to avoid scanning as few
148   // rows as possible.  Keep in mind that we run this query every time the user
149   // types a key when the urlbar value looks like a URL with a path.
150   let selectTitle;
151   let joinBookmarks;
152   if (isBookmarkContained) {
153     selectTitle = "ifnull(b.title, matched_url.title)";
154     joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = matched_url.id";
155   } else {
156     selectTitle = "matched_url.title";
157     joinBookmarks = "";
158   }
159   return `/* do not warn (bug no): cannot use an index to sort */
160     WITH matched_url(url, title, frecency, n_bookmarks, visited, stripped_url, is_exact_match, id) AS (
161       SELECT url,
162              title,
163              frecency,
164              foreign_count AS n_bookmarks,
165              visit_count > 0 AS visited,
166              strip_prefix_and_userinfo(url) AS stripped_url,
167              strip_prefix_and_userinfo(url) = strip_prefix_and_userinfo(:strippedURL) AS is_exact_match,
168              id
169       FROM moz_places
170       WHERE rev_host = :revHost
171             ${where1}
172       UNION ALL
173       SELECT url,
174              title,
175              frecency,
176              foreign_count AS n_bookmarks,
177              visit_count > 0 AS visited,
178              strip_prefix_and_userinfo(url) AS stripped_url,
179              strip_prefix_and_userinfo(url) = 'www.' || strip_prefix_and_userinfo(:strippedURL) AS is_exact_match,
180              id
181       FROM moz_places
182       WHERE rev_host = :revHost || 'www.'
183             ${where2}
184       ORDER BY is_exact_match DESC, frecency DESC, id DESC
185       LIMIT 1
186     )
187     SELECT :query_type AS query_type,
188            :searchString AS search_string,
189            :strippedURL AS stripped_url,
190            matched_url.url AS url,
191            ${selectTitle} AS title
192     FROM matched_url
193     ${joinBookmarks}
194   `;
197 // Queries
198 const QUERY_ORIGIN_HISTORY_BOOKMARK = originQuery(
199   `WHERE n_bookmarks > 0 OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
202 const QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK = originQuery(
203   `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF'
204      AND (n_bookmarks > 0 OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`
207 const QUERY_ORIGIN_HISTORY = originQuery(
208   `WHERE visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
211 const QUERY_ORIGIN_PREFIX_HISTORY = originQuery(
212   `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF'
213      AND visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
216 const QUERY_ORIGIN_BOOKMARK = originQuery(`WHERE n_bookmarks > 0`);
218 const QUERY_ORIGIN_PREFIX_BOOKMARK = originQuery(
219   `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF' AND n_bookmarks > 0`
222 const QUERY_URL_HISTORY_BOOKMARK = urlQuery(
223   `AND (n_bookmarks > 0 OR frecency > 20)
224      AND stripped_url COLLATE NOCASE
225        BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
226   `AND (n_bookmarks > 0 OR frecency > 20)
227      AND stripped_url COLLATE NOCASE
228        BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
229   true
232 const QUERY_URL_PREFIX_HISTORY_BOOKMARK = urlQuery(
233   `AND (n_bookmarks > 0 OR frecency > 20)
234      AND url COLLATE NOCASE
235        BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
236   `AND (n_bookmarks > 0 OR frecency > 20)
237      AND url COLLATE NOCASE
238        BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
239   true
242 const QUERY_URL_HISTORY = urlQuery(
243   `AND (visited OR n_bookmarks = 0)
244      AND frecency > 20
245      AND stripped_url COLLATE NOCASE
246        BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
247   `AND (visited OR n_bookmarks = 0)
248      AND frecency > 20
249      AND stripped_url COLLATE NOCASE
250        BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
251   false
254 const QUERY_URL_PREFIX_HISTORY = urlQuery(
255   `AND (visited OR n_bookmarks = 0)
256      AND frecency > 20
257      AND url COLLATE NOCASE
258        BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
259   `AND (visited OR n_bookmarks = 0)
260      AND frecency > 20
261      AND url COLLATE NOCASE
262        BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
263   false
266 const QUERY_URL_BOOKMARK = urlQuery(
267   `AND n_bookmarks > 0
268      AND stripped_url COLLATE NOCASE
269        BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
270   `AND n_bookmarks > 0
271      AND stripped_url COLLATE NOCASE
272        BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
273   true
276 const QUERY_URL_PREFIX_BOOKMARK = urlQuery(
277   `AND n_bookmarks > 0
278      AND url COLLATE NOCASE
279        BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
280   `AND n_bookmarks > 0
281      AND url COLLATE NOCASE
282        BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
283   true
287  * Class used to create the provider.
288  */
289 class ProviderAutofill extends UrlbarProvider {
290   constructor() {
291     super();
292   }
294   /**
295    * Returns the name of this provider.
296    *
297    * @returns {string} the name of this provider.
298    */
299   get name() {
300     return "Autofill";
301   }
303   /**
304    * Returns the type of this provider.
305    *
306    * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
307    */
308   get type() {
309     return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
310   }
312   /**
313    * Whether this provider should be invoked for the given context.
314    * If this method returns false, the providers manager won't start a query
315    * with this provider, to save on resources.
316    *
317    * @param {UrlbarQueryContext} queryContext The query context object
318    * @returns {boolean} Whether this provider should be invoked for the search.
319    */
320   async isActive(queryContext) {
321     let instance = this.queryInstance;
323     // This is usually reset on canceling or completing the query, but since we
324     // query in isActive, it may not have been canceled by the previous call.
325     // It is an object with values { result: UrlbarResult, instance: Query }.
326     // See the documentation for _getAutofillData for more information.
327     this._autofillData = null;
329     // First of all, check for the autoFill pref.
330     if (!lazy.UrlbarPrefs.get("autoFill")) {
331       return false;
332     }
334     if (!queryContext.allowAutofill) {
335       return false;
336     }
338     if (queryContext.tokens.length != 1) {
339       return false;
340     }
342     // Trying to autofill an extremely long string would be expensive, and
343     // not particularly useful since the filled part falls out of screen anyway.
344     if (queryContext.searchString.length > UrlbarUtils.MAX_TEXT_LENGTH) {
345       return false;
346     }
348     // autoFill can only cope with history, bookmarks, and about: entries.
349     if (
350       !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
351       !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
352     ) {
353       return false;
354     }
356     // Autofill doesn't search tags or titles
357     if (
358       queryContext.tokens.some(
359         t =>
360           t.type == lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG ||
361           t.type == lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE
362       )
363     ) {
364       return false;
365     }
367     [this._strippedPrefix, this._searchString] = UrlbarUtils.stripURLPrefix(
368       queryContext.searchString
369     );
370     this._strippedPrefix = this._strippedPrefix.toLowerCase();
372     // Don't try to autofill if the search term includes any whitespace.
373     // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
374     // tokenizer ends up trimming the search string and returning a value
375     // that doesn't match it, or is even shorter.
376     if (lazy.UrlbarTokenizer.REGEXP_SPACES.test(queryContext.searchString)) {
377       return false;
378     }
380     // Fetch autofill result now, rather than in startQuery. We do this so the
381     // muxer doesn't have to wait on autofill for every query, since startQuery
382     // will be guaranteed to return a result very quickly using this approach.
383     // Bug 1651101 is filed to improve this behaviour.
384     let result = await this._getAutofillResult(queryContext);
385     if (!result || instance != this.queryInstance) {
386       return false;
387     }
388     this._autofillData = { result, instance };
389     return true;
390   }
392   /**
393    * Gets the provider's priority.
394    *
395    * @returns {number} The provider's priority for the given query.
396    */
397   getPriority() {
398     return 0;
399   }
401   /**
402    * Starts querying.
403    *
404    * @param {object} queryContext The query context object
405    * @param {Function} addCallback Callback invoked by the provider to add a new
406    *        result.
407    * @returns {Promise} resolved when the query stops.
408    */
409   async startQuery(queryContext, addCallback) {
410     // Check if the query was cancelled while the autofill result was being
411     // fetched. We don't expect this to be true since we also check the instance
412     // in isActive and clear _autofillData in cancelQuery, but we sanity check it.
413     if (
414       !this._autofillData ||
415       this._autofillData.instance != this.queryInstance
416     ) {
417       this.logger.error("startQuery invoked with an invalid _autofillData");
418       return;
419     }
421     this._autofillData.result.heuristic = true;
422     addCallback(this, this._autofillData.result);
423     this._autofillData = null;
424   }
426   /**
427    * Cancels a running query.
428    */
429   cancelQuery() {
430     if (this._autofillData?.instance == this.queryInstance) {
431       this._autofillData = null;
432     }
433   }
435   /**
436    * Filters hosts by retaining only the ones over the autofill threshold, then
437    * sorts them by their frecency, and extracts the one with the highest value.
438    *
439    * @param {UrlbarQueryContext} queryContext The current queryContext.
440    * @param {Array} hosts Array of host names to examine.
441    * @returns {Promise<string?>}
442    *   Resolved when the filtering is complete. Resolves with the top matching
443    *   host, or null if not found.
444    */
445   async getTopHostOverThreshold(queryContext, hosts) {
446     let db = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
447     let conditions = [];
448     // Pay attention to the order of params, since they are not named.
449     let params = [...hosts];
450     let sources = queryContext.sources;
451     if (
452       sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
453       sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
454     ) {
455       conditions.push(
456         `(n_bookmarks > 0 OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`
457       );
458     } else if (sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
459       conditions.push(`visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`);
460     } else if (sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
461       conditions.push("n_bookmarks > 0");
462     }
464     let rows = await db.executeCached(
465       `
466         ${SQL_AUTOFILL_WITH},
467         origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, n_bookmarks, visited) AS (
468           SELECT
469           id,
470           prefix,
471           first_value(prefix) OVER (
472             PARTITION BY host ORDER BY ${ORIGIN_FRECENCY_FIELD} DESC, prefix = "https://" DESC, id DESC
473           ),
474           host,
475           fixup_url(host),
476           total(${ORIGIN_FRECENCY_FIELD}) OVER (PARTITION BY fixup_url(host)),
477           ${ORIGIN_FRECENCY_FIELD},
478           total(
479             (SELECT total(foreign_count) FROM moz_places WHERE origin_id = o.id)
480           ) OVER (PARTITION BY fixup_url(host)),
481           MAX(EXISTS(
482             SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0
483           )) OVER (PARTITION BY fixup_url(host))
484           FROM moz_origins o
485           WHERE o.host IN (${new Array(hosts.length).fill("?").join(",")})
486         )
487         SELECT host
488         FROM origins
489         ${conditions.length ? "WHERE " + conditions.join(" AND ") : ""}
490         ORDER BY frecency DESC, prefix = "https://" DESC, id DESC
491         LIMIT 1
492       `,
493       params
494     );
495     if (!rows.length) {
496       return null;
497     }
498     return rows[0].getResultByName("host");
499   }
501   /**
502    * Obtains the query to search for autofill origin results.
503    *
504    * @param {UrlbarQueryContext} queryContext
505    *   The current queryContext.
506    * @returns {Array} consisting of the correctly optimized query to search the
507    *         database with and an object containing the params to bound.
508    */
509   _getOriginQuery(queryContext) {
510     // At this point, searchString is not a URL with a path; it does not
511     // contain a slash, except for possibly at the very end.  If there is
512     // trailing slash, remove it when searching here to match the rest of the
513     // string because it may be an origin.
514     let searchStr = this._searchString.endsWith("/")
515       ? this._searchString.slice(0, -1)
516       : this._searchString;
518     let opts = {
519       query_type: QUERYTYPE.AUTOFILL_ORIGIN,
520       searchString: searchStr.toLowerCase(),
521     };
522     if (this._strippedPrefix) {
523       opts.prefix = this._strippedPrefix;
524     }
526     if (
527       queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
528       queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
529     ) {
530       return [
531         this._strippedPrefix
532           ? QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK
533           : QUERY_ORIGIN_HISTORY_BOOKMARK,
534         opts,
535       ];
536     }
537     if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
538       return [
539         this._strippedPrefix
540           ? QUERY_ORIGIN_PREFIX_HISTORY
541           : QUERY_ORIGIN_HISTORY,
542         opts,
543       ];
544     }
545     if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
546       return [
547         this._strippedPrefix
548           ? QUERY_ORIGIN_PREFIX_BOOKMARK
549           : QUERY_ORIGIN_BOOKMARK,
550         opts,
551       ];
552     }
553     throw new Error("Either history or bookmark behavior expected");
554   }
556   /**
557    * Obtains the query to search for autoFill url results.
558    *
559    * @param {UrlbarQueryContext} queryContext
560    *   The current queryContext.
561    * @returns {Array} consisting of the correctly optimized query to search the
562    *         database with and an object containing the params to bound.
563    */
564   _getUrlQuery(queryContext) {
565     // Try to get the host from the search string.  The host is the part of the
566     // URL up to either the path slash, port colon, or query "?".  If the search
567     // string doesn't look like it begins with a host, then return; it doesn't
568     // make sense to do a URL query with it.
569     const urlQueryHostRegexp = /^[^/:?]+/;
570     let hostMatch = urlQueryHostRegexp.exec(this._searchString);
571     if (!hostMatch) {
572       return [null, null];
573     }
575     let host = hostMatch[0].toLowerCase();
576     let revHost = host.split("").reverse().join("") + ".";
578     // Build a string that's the URL stripped of its prefix, i.e., the host plus
579     // everything after.  Use queryContext.trimmedSearchString instead of
580     // this._searchString because this._searchString has had unEscapeURIForUI()
581     // called on it.  It's therefore not necessarily the literal URL.
582     let strippedURL = queryContext.trimmedSearchString;
583     if (this._strippedPrefix) {
584       strippedURL = strippedURL.substr(this._strippedPrefix.length);
585     }
586     strippedURL = host + strippedURL.substr(host.length);
588     let opts = {
589       query_type: QUERYTYPE.AUTOFILL_URL,
590       searchString: this._searchString,
591       revHost,
592       strippedURL,
593     };
594     if (this._strippedPrefix) {
595       opts.prefix = this._strippedPrefix;
596     }
598     if (
599       queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
600       queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
601     ) {
602       return [
603         this._strippedPrefix
604           ? QUERY_URL_PREFIX_HISTORY_BOOKMARK
605           : QUERY_URL_HISTORY_BOOKMARK,
606         opts,
607       ];
608     }
609     if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
610       return [
611         this._strippedPrefix ? QUERY_URL_PREFIX_HISTORY : QUERY_URL_HISTORY,
612         opts,
613       ];
614     }
615     if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
616       return [
617         this._strippedPrefix ? QUERY_URL_PREFIX_BOOKMARK : QUERY_URL_BOOKMARK,
618         opts,
619       ];
620     }
621     throw new Error("Either history or bookmark behavior expected");
622   }
624   _getAdaptiveHistoryQuery(queryContext) {
625     let sourceCondition;
626     if (
627       queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
628       queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
629     ) {
630       sourceCondition = "(h.foreign_count > 0 OR h.frecency > 20)";
631     } else if (
632       queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)
633     ) {
634       sourceCondition =
635         "((h.visit_count > 0 OR h.foreign_count = 0) AND h.frecency > 20)";
636     } else if (
637       queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
638     ) {
639       sourceCondition = "h.foreign_count > 0";
640     } else {
641       return [];
642     }
644     let selectTitle;
645     let joinBookmarks;
646     if (UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
647       selectTitle = "ifnull(b.title, matched.title)";
648       joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = matched.id";
649     } else {
650       selectTitle = "matched.title";
651       joinBookmarks = "";
652     }
654     const params = {
655       queryType: QUERYTYPE.AUTOFILL_ADAPTIVE,
656       // `fullSearchString` is the value the user typed including a prefix if
657       // they typed one. `searchString` has been stripped of the prefix.
658       fullSearchString: queryContext.lowerCaseSearchString,
659       searchString: this._searchString,
660       strippedPrefix: this._strippedPrefix,
661       useCountThreshold: lazy.UrlbarPrefs.get(
662         "autoFillAdaptiveHistoryUseCountThreshold"
663       ),
664     };
666     const query = `
667       WITH matched(input, url, title, stripped_url, is_exact_match, starts_with, id) AS (
668         SELECT
669           i.input AS input,
670           h.url AS url,
671           h.title AS title,
672           strip_prefix_and_userinfo(h.url) AS stripped_url,
673           strip_prefix_and_userinfo(h.url) = :searchString AS is_exact_match,
674           (strip_prefix_and_userinfo(h.url) COLLATE NOCASE BETWEEN :searchString AND :searchString || X'FFFF') AS starts_with,
675           h.id AS id
676         FROM moz_places h
677         JOIN moz_inputhistory i ON i.place_id = h.id
678         WHERE LENGTH(i.input) != 0
679           AND :fullSearchString BETWEEN i.input AND i.input || X'FFFF'
680           AND ${sourceCondition}
681           AND i.use_count >= :useCountThreshold
682           AND (:strippedPrefix = '' OR get_prefix(h.url) = :strippedPrefix)
683           AND (
684             starts_with OR
685             (stripped_url COLLATE NOCASE BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF')
686           )
687         ORDER BY is_exact_match DESC, i.use_count DESC, h.frecency DESC, h.id DESC
688         LIMIT 1
689       )
690       SELECT
691         :queryType AS query_type,
692         :searchString AS search_string,
693         input,
694         url,
695         iif(starts_with, stripped_url, fixup_url(stripped_url)) AS url_fixed,
696         ${selectTitle} AS title,
697         stripped_url
698       FROM matched
699       ${joinBookmarks}
700     `;
702     return [query, params];
703   }
705   /**
706    * Processes a matched row in the Places database.
707    *
708    * @param {object} row
709    *   The matched row.
710    * @param {UrlbarQueryContext} queryContext
711    *   The query context.
712    * @returns {UrlbarResult} a result generated from the matches row.
713    */
714   _processRow(row, queryContext) {
715     let queryType = row.getResultByName("query_type");
716     let title = row.getResultByName("title");
718     // `searchString` is `this._searchString` or derived from it. It is
719     // stripped, meaning the prefix (the URL protocol) has been removed.
720     let searchString = row.getResultByName("search_string");
722     // `fixedURL` is the part of the matching stripped URL that starts with the
723     // stripped search string. The important point here is "www" handling. If a
724     // stripped URL starts with "www", we allow the user to omit the "www" and
725     // still match it. So if the matching stripped URL starts with "www" but the
726     // stripped search string does not, `fixedURL` will also omit the "www".
727     // Otherwise `fixedURL` will be equivalent to the matching stripped URL.
728     //
729     // Example 1:
730     //   stripped URL: www.example.com/
731     //   searchString: exam
732     //   fixedURL: example.com/
733     // Example 2:
734     //   stripped URL: www.example.com/
735     //   searchString: www.exam
736     //   fixedURL: www.example.com/
737     // Example 3:
738     //   stripped URL: example.com/
739     //   searchString: exam
740     //   fixedURL: example.com/
741     let fixedURL;
743     // `finalCompleteValue` will be the UrlbarResult's URL. If the matching
744     // stripped URL starts with "www" but the user omitted it,
745     // `finalCompleteValue` will include it to properly reflect the real URL.
746     let finalCompleteValue;
748     let autofilledType;
749     let adaptiveHistoryInput;
751     switch (queryType) {
752       case QUERYTYPE.AUTOFILL_ORIGIN: {
753         fixedURL = row.getResultByName("host_fixed");
754         finalCompleteValue = row.getResultByName("url");
755         autofilledType = "origin";
756         break;
757       }
758       case QUERYTYPE.AUTOFILL_URL: {
759         let url = row.getResultByName("url");
760         let strippedURL = row.getResultByName("stripped_url");
762         if (!UrlbarUtils.canAutofillURL(url, strippedURL, true)) {
763           return null;
764         }
766         // We autofill urls to-the-next-slash.
767         // http://mozilla.org/foo/bar/baz will be autofilled to:
768         //  - http://mozilla.org/f[oo/]
769         //  - http://mozilla.org/foo/b[ar/]
770         //  - http://mozilla.org/foo/bar/b[az]
771         // And, toLowerCase() is preferred over toLocaleLowerCase() here
772         // because "COLLATE NOCASE" in the SQL only handles ASCII characters.
773         let strippedURLIndex = url
774           .toLowerCase()
775           .indexOf(strippedURL.toLowerCase());
776         let strippedPrefix = url.substr(0, strippedURLIndex);
777         let nextSlashIndex = url.indexOf(
778           "/",
779           strippedURLIndex + strippedURL.length - 1
780         );
781         fixedURL =
782           nextSlashIndex < 0
783             ? url.substr(strippedURLIndex)
784             : url.substring(strippedURLIndex, nextSlashIndex + 1);
785         finalCompleteValue = strippedPrefix + fixedURL;
786         if (finalCompleteValue !== url) {
787           title = null;
788         }
789         autofilledType = "url";
790         break;
791       }
792       case QUERYTYPE.AUTOFILL_ADAPTIVE: {
793         adaptiveHistoryInput = row.getResultByName("input");
794         fixedURL = row.getResultByName("url_fixed");
795         finalCompleteValue = row.getResultByName("url");
796         autofilledType = "adaptive";
797         break;
798       }
799     }
801     // Compute `autofilledValue`, the full value that will be placed in the
802     // input. It includes two parts: the part the user already typed in the
803     // character case they typed it (`queryContext.searchString`), and the
804     // autofilled part, which is the portion of the fixed URL starting after the
805     // stripped search string.
806     let autofilledValue =
807       queryContext.searchString + fixedURL.substring(searchString.length);
809     // If more than an origin was autofilled and the user typed the full
810     // autofilled value, override the final URL by using the exact value the
811     // user typed. This allows the user to visit a URL that differs from the
812     // autofilled URL only in character case (for example "wikipedia.org/RAID"
813     // vs. "wikipedia.org/Raid") by typing the full desired URL.
814     if (
815       queryType != QUERYTYPE.AUTOFILL_ORIGIN &&
816       queryContext.searchString.length == autofilledValue.length
817     ) {
818       // Use `new URL().href` to lowercase the domain in the final completed
819       // URL. This isn't necessary since domains are case insensitive, but it
820       // looks nicer because it means the domain will remain lowercased in the
821       // input, and it also reflects the fact that Firefox will visit the
822       // lowercased name.
823       const originalCompleteValue = new URL(finalCompleteValue).href;
824       let strippedAutofilledValue = autofilledValue.substring(
825         this._strippedPrefix.length
826       );
827       finalCompleteValue = new URL(
828         finalCompleteValue.substring(
829           0,
830           finalCompleteValue.length - strippedAutofilledValue.length
831         ) + strippedAutofilledValue
832       ).href;
834       // If the character case of except origin part of the original
835       // finalCompleteValue differs from finalCompleteValue that includes user's
836       // input, we set title null because it expresses different web page.
837       if (finalCompleteValue !== originalCompleteValue) {
838         title = null;
839       }
840     }
842     let payload = {
843       url: [finalCompleteValue, UrlbarUtils.HIGHLIGHT.TYPED],
844       icon: UrlbarUtils.getIconForUrl(finalCompleteValue),
845     };
847     if (title) {
848       payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED];
849     } else {
850       let trimHttps = lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps");
851       let displaySpec = UrlbarUtils.prepareUrlForDisplay(finalCompleteValue, {
852         trimURL: false,
853       });
854       let [fallbackTitle] = UrlbarUtils.stripPrefixAndTrim(displaySpec, {
855         stripHttp: !trimHttps,
856         stripHttps: trimHttps,
857         trimEmptyQuery: true,
858         trimSlash: !this._searchString.includes("/"),
859       });
860       payload.fallbackTitle = [fallbackTitle, UrlbarUtils.HIGHLIGHT.TYPED];
861     }
863     let result = new lazy.UrlbarResult(
864       UrlbarUtils.RESULT_TYPE.URL,
865       UrlbarUtils.RESULT_SOURCE.HISTORY,
866       ...lazy.UrlbarResult.payloadAndSimpleHighlights(
867         queryContext.tokens,
868         payload
869       )
870     );
872     result.autofill = {
873       adaptiveHistoryInput,
874       value: autofilledValue,
875       selectionStart: queryContext.searchString.length,
876       selectionEnd: autofilledValue.length,
877       type: autofilledType,
878     };
879     return result;
880   }
882   async _getAutofillResult(queryContext) {
883     // We may be autofilling an about: link.
884     let result = this._matchAboutPageForAutofill(queryContext);
885     if (result) {
886       return result;
887     }
889     // It may also look like a URL we know from the database.
890     result = await this._matchKnownUrl(queryContext);
891     if (result) {
892       return result;
893     }
895     return null;
896   }
898   _matchAboutPageForAutofill(queryContext) {
899     // Check that the typed query is at least one character longer than the
900     // about: prefix.
901     if (this._strippedPrefix != "about:" || !this._searchString) {
902       return null;
903     }
905     for (const aboutUrl of lazy.AboutPagesUtils.visibleAboutUrls) {
906       if (aboutUrl.startsWith(`about:${this._searchString.toLowerCase()}`)) {
907         let [trimmedUrl] = UrlbarUtils.stripPrefixAndTrim(aboutUrl, {
908           stripHttp: true,
909           trimEmptyQuery: true,
910           trimSlash: !this._searchString.includes("/"),
911         });
912         let result = new lazy.UrlbarResult(
913           UrlbarUtils.RESULT_TYPE.URL,
914           UrlbarUtils.RESULT_SOURCE.HISTORY,
915           ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
916             title: [trimmedUrl, UrlbarUtils.HIGHLIGHT.TYPED],
917             url: [aboutUrl, UrlbarUtils.HIGHLIGHT.TYPED],
918             icon: UrlbarUtils.getIconForUrl(aboutUrl),
919           })
920         );
921         let autofilledValue =
922           queryContext.searchString +
923           aboutUrl.substring(queryContext.searchString.length);
924         result.autofill = {
925           type: "about",
926           value: autofilledValue,
927           selectionStart: queryContext.searchString.length,
928           selectionEnd: autofilledValue.length,
929         };
930         return result;
931       }
932     }
933     return null;
934   }
936   async _matchKnownUrl(queryContext) {
937     let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
938     if (!conn) {
939       return null;
940     }
942     // We try to autofill with adaptive history first.
943     if (
944       lazy.UrlbarPrefs.get("autoFillAdaptiveHistoryEnabled") &&
945       lazy.UrlbarPrefs.get("autoFillAdaptiveHistoryMinCharsThreshold") <=
946         queryContext.searchString.length
947     ) {
948       const [query, params] = this._getAdaptiveHistoryQuery(queryContext);
949       if (query) {
950         const resultSet = await conn.executeCached(query, params);
951         if (resultSet.length) {
952           return this._processRow(resultSet[0], queryContext);
953         }
954       }
955     }
957     // The adaptive history query is passed queryContext.searchString (the full
958     // search string), but the origin and URL queries are passed the prefix
959     // (this._strippedPrefix) and the rest of the search string
960     // (this._searchString) separately. The user must specify a non-prefix part
961     // to trigger origin and URL autofill.
962     if (!this._searchString.length) {
963       return null;
964     }
966     // If search string looks like an origin, try to autofill against origins.
967     // Otherwise treat it as a possible URL.  When the string has only one slash
968     // at the end, we still treat it as an URL.
969     let query, params;
970     if (
971       lazy.UrlbarTokenizer.looksLikeOrigin(this._searchString, {
972         ignoreKnownDomains: true,
973       })
974     ) {
975       [query, params] = this._getOriginQuery(queryContext);
976     } else {
977       [query, params] = this._getUrlQuery(queryContext);
978     }
980     // _getUrlQuery doesn't always return a query.
981     if (query) {
982       let rows = await conn.executeCached(query, params);
983       if (rows.length) {
984         return this._processRow(rows[0], queryContext);
985       }
986     }
987     return null;
988   }
991 export var UrlbarProviderAutofill = new ProviderAutofill();