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 an autofill result.
12 } from "resource:///modules/UrlbarUtils.sys.mjs";
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",
24 // AutoComplete query type constants.
25 // Describes the various types of queries that we can process rows for.
32 // Constants to support an alternative frecency algorithm.
33 const ORIGIN_USE_ALT_FRECENCY = Services.prefs.getBoolPref(
34 "places.frecency.origins.alternative.featureGate",
37 const ORIGIN_FRECENCY_FIELD = ORIGIN_USE_ALT_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
49 autofill_frecency_threshold(value) AS (
51 (SELECT value FROM moz_meta WHERE key = 'origin_alt_frecency_threshold'),
58 autofill_frecency_threshold(value) AS (
60 (SELECT value FROM moz_meta WHERE key = 'origin_frecency_threshold'),
66 const SQL_AUTOFILL_FRECENCY_THRESHOLD = `host_frecency >= (
67 SELECT value FROM autofill_frecency_threshold
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")
76 SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0
77 )) OVER (PARTITION BY fixup_url(host)) > 0`
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";
85 selectTitle = "iif(h.frecency <> 0, h.title, NULL)";
88 return `/* do not warn (bug no): cannot use an index to sort */
90 origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, n_bookmarks, visited) AS (
94 first_value(prefix) OVER (
95 PARTITION BY host ORDER BY ${ORIGIN_FRECENCY_FIELD} DESC, prefix = "https://" DESC, id DESC
99 total(${ORIGIN_FRECENCY_FIELD}) OVER (PARTITION BY fixup_url(host)),
100 ${ORIGIN_FRECENCY_FIELD},
102 (SELECT total(foreign_count) FROM moz_places WHERE origin_id = o.id)
103 ) OVER (PARTITION BY fixup_url(host)),
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'))
110 matched_origin(host_fixed, url) AS (
111 SELECT iif(instr(host, :searchString) = 1, host, fixed) || '/',
112 ifnull(:prefix, host_prefix) || host || '/'
115 ORDER BY frecency DESC, n_bookmarks DESC, prefix = "https://" DESC, id DESC
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)
128 h.title IS NOT NULL DESC,
129 h.title || '/' <> o.host_fixed DESC,
135 SELECT :query_type AS query_type,
136 :searchString AS search_string,
137 h.host_fixed AS host_fixed,
139 ${selectTitle} AS title
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.
152 if (isBookmarkContained) {
153 selectTitle = "ifnull(b.title, matched_url.title)";
154 joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = matched_url.id";
156 selectTitle = "matched_url.title";
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 (
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,
170 WHERE rev_host = :revHost
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,
182 WHERE rev_host = :revHost || 'www.'
184 ORDER BY is_exact_match DESC, frecency DESC, id DESC
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
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'`,
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'`,
242 const QUERY_URL_HISTORY = urlQuery(
243 `AND (visited OR n_bookmarks = 0)
245 AND stripped_url COLLATE NOCASE
246 BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
247 `AND (visited OR n_bookmarks = 0)
249 AND stripped_url COLLATE NOCASE
250 BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
254 const QUERY_URL_PREFIX_HISTORY = urlQuery(
255 `AND (visited OR n_bookmarks = 0)
257 AND url COLLATE NOCASE
258 BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
259 `AND (visited OR n_bookmarks = 0)
261 AND url COLLATE NOCASE
262 BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
266 const QUERY_URL_BOOKMARK = urlQuery(
268 AND stripped_url COLLATE NOCASE
269 BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
271 AND stripped_url COLLATE NOCASE
272 BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
276 const QUERY_URL_PREFIX_BOOKMARK = urlQuery(
278 AND url COLLATE NOCASE
279 BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
281 AND url COLLATE NOCASE
282 BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
287 * Class used to create the provider.
289 class ProviderAutofill extends UrlbarProvider {
295 * Returns the name of this provider.
297 * @returns {string} the name of this provider.
304 * Returns the type of this provider.
306 * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
309 return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
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.
317 * @param {UrlbarQueryContext} queryContext The query context object
318 * @returns {boolean} Whether this provider should be invoked for the search.
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")) {
334 if (!queryContext.allowAutofill) {
338 if (queryContext.tokens.length != 1) {
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) {
348 // autoFill can only cope with history, bookmarks, and about: entries.
350 !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
351 !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
356 // Autofill doesn't search tags or titles
358 queryContext.tokens.some(
360 t.type == lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG ||
361 t.type == lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE
367 [this._strippedPrefix, this._searchString] = UrlbarUtils.stripURLPrefix(
368 queryContext.searchString
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)) {
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) {
388 this._autofillData = { result, instance };
393 * Gets the provider's priority.
395 * @returns {number} The provider's priority for the given query.
404 * @param {object} queryContext The query context object
405 * @param {Function} addCallback Callback invoked by the provider to add a new
407 * @returns {Promise} resolved when the query stops.
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.
414 !this._autofillData ||
415 this._autofillData.instance != this.queryInstance
417 this.logger.error("startQuery invoked with an invalid _autofillData");
421 this._autofillData.result.heuristic = true;
422 addCallback(this, this._autofillData.result);
423 this._autofillData = null;
427 * Cancels a running query.
430 if (this._autofillData?.instance == this.queryInstance) {
431 this._autofillData = null;
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.
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.
445 async getTopHostOverThreshold(queryContext, hosts) {
446 let db = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
448 // Pay attention to the order of params, since they are not named.
449 let params = [...hosts];
450 let sources = queryContext.sources;
452 sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
453 sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
456 `(n_bookmarks > 0 OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`
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");
464 let rows = await db.executeCached(
466 ${SQL_AUTOFILL_WITH},
467 origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, n_bookmarks, visited) AS (
471 first_value(prefix) OVER (
472 PARTITION BY host ORDER BY ${ORIGIN_FRECENCY_FIELD} DESC, prefix = "https://" DESC, id DESC
476 total(${ORIGIN_FRECENCY_FIELD}) OVER (PARTITION BY fixup_url(host)),
477 ${ORIGIN_FRECENCY_FIELD},
479 (SELECT total(foreign_count) FROM moz_places WHERE origin_id = o.id)
480 ) OVER (PARTITION BY fixup_url(host)),
482 SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0
483 )) OVER (PARTITION BY fixup_url(host))
485 WHERE o.host IN (${new Array(hosts.length).fill("?").join(",")})
489 ${conditions.length ? "WHERE " + conditions.join(" AND ") : ""}
490 ORDER BY frecency DESC, prefix = "https://" DESC, id DESC
498 return rows[0].getResultByName("host");
502 * Obtains the query to search for autofill origin results.
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.
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;
519 query_type: QUERYTYPE.AUTOFILL_ORIGIN,
520 searchString: searchStr.toLowerCase(),
522 if (this._strippedPrefix) {
523 opts.prefix = this._strippedPrefix;
527 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
528 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
532 ? QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK
533 : QUERY_ORIGIN_HISTORY_BOOKMARK,
537 if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
540 ? QUERY_ORIGIN_PREFIX_HISTORY
541 : QUERY_ORIGIN_HISTORY,
545 if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
548 ? QUERY_ORIGIN_PREFIX_BOOKMARK
549 : QUERY_ORIGIN_BOOKMARK,
553 throw new Error("Either history or bookmark behavior expected");
557 * Obtains the query to search for autoFill url results.
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.
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);
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);
586 strippedURL = host + strippedURL.substr(host.length);
589 query_type: QUERYTYPE.AUTOFILL_URL,
590 searchString: this._searchString,
594 if (this._strippedPrefix) {
595 opts.prefix = this._strippedPrefix;
599 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
600 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
604 ? QUERY_URL_PREFIX_HISTORY_BOOKMARK
605 : QUERY_URL_HISTORY_BOOKMARK,
609 if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
611 this._strippedPrefix ? QUERY_URL_PREFIX_HISTORY : QUERY_URL_HISTORY,
615 if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
617 this._strippedPrefix ? QUERY_URL_PREFIX_BOOKMARK : QUERY_URL_BOOKMARK,
621 throw new Error("Either history or bookmark behavior expected");
624 _getAdaptiveHistoryQuery(queryContext) {
627 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
628 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
630 sourceCondition = "(h.foreign_count > 0 OR h.frecency > 20)";
632 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)
635 "((h.visit_count > 0 OR h.foreign_count = 0) AND h.frecency > 20)";
637 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
639 sourceCondition = "h.foreign_count > 0";
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";
650 selectTitle = "matched.title";
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"
667 WITH matched(input, url, title, stripped_url, is_exact_match, starts_with, id) AS (
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,
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)
685 (stripped_url COLLATE NOCASE BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF')
687 ORDER BY is_exact_match DESC, i.use_count DESC, h.frecency DESC, h.id DESC
691 :queryType AS query_type,
692 :searchString AS search_string,
695 iif(starts_with, stripped_url, fixup_url(stripped_url)) AS url_fixed,
696 ${selectTitle} AS title,
702 return [query, params];
706 * Processes a matched row in the Places database.
708 * @param {object} row
710 * @param {UrlbarQueryContext} queryContext
712 * @returns {UrlbarResult} a result generated from the matches row.
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.
730 // stripped URL: www.example.com/
731 // searchString: exam
732 // fixedURL: example.com/
734 // stripped URL: www.example.com/
735 // searchString: www.exam
736 // fixedURL: www.example.com/
738 // stripped URL: example.com/
739 // searchString: exam
740 // fixedURL: example.com/
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;
749 let adaptiveHistoryInput;
752 case QUERYTYPE.AUTOFILL_ORIGIN: {
753 fixedURL = row.getResultByName("host_fixed");
754 finalCompleteValue = row.getResultByName("url");
755 autofilledType = "origin";
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)) {
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
775 .indexOf(strippedURL.toLowerCase());
776 let strippedPrefix = url.substr(0, strippedURLIndex);
777 let nextSlashIndex = url.indexOf(
779 strippedURLIndex + strippedURL.length - 1
783 ? url.substr(strippedURLIndex)
784 : url.substring(strippedURLIndex, nextSlashIndex + 1);
785 finalCompleteValue = strippedPrefix + fixedURL;
786 if (finalCompleteValue !== url) {
789 autofilledType = "url";
792 case QUERYTYPE.AUTOFILL_ADAPTIVE: {
793 adaptiveHistoryInput = row.getResultByName("input");
794 fixedURL = row.getResultByName("url_fixed");
795 finalCompleteValue = row.getResultByName("url");
796 autofilledType = "adaptive";
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.
815 queryType != QUERYTYPE.AUTOFILL_ORIGIN &&
816 queryContext.searchString.length == autofilledValue.length
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
823 const originalCompleteValue = new URL(finalCompleteValue).href;
824 let strippedAutofilledValue = autofilledValue.substring(
825 this._strippedPrefix.length
827 finalCompleteValue = new URL(
828 finalCompleteValue.substring(
830 finalCompleteValue.length - strippedAutofilledValue.length
831 ) + strippedAutofilledValue
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) {
843 url: [finalCompleteValue, UrlbarUtils.HIGHLIGHT.TYPED],
844 icon: UrlbarUtils.getIconForUrl(finalCompleteValue),
848 payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED];
850 let trimHttps = lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps");
851 let displaySpec = UrlbarUtils.prepareUrlForDisplay(finalCompleteValue, {
854 let [fallbackTitle] = UrlbarUtils.stripPrefixAndTrim(displaySpec, {
855 stripHttp: !trimHttps,
856 stripHttps: trimHttps,
857 trimEmptyQuery: true,
858 trimSlash: !this._searchString.includes("/"),
860 payload.fallbackTitle = [fallbackTitle, UrlbarUtils.HIGHLIGHT.TYPED];
863 let result = new lazy.UrlbarResult(
864 UrlbarUtils.RESULT_TYPE.URL,
865 UrlbarUtils.RESULT_SOURCE.HISTORY,
866 ...lazy.UrlbarResult.payloadAndSimpleHighlights(
873 adaptiveHistoryInput,
874 value: autofilledValue,
875 selectionStart: queryContext.searchString.length,
876 selectionEnd: autofilledValue.length,
877 type: autofilledType,
882 async _getAutofillResult(queryContext) {
883 // We may be autofilling an about: link.
884 let result = this._matchAboutPageForAutofill(queryContext);
889 // It may also look like a URL we know from the database.
890 result = await this._matchKnownUrl(queryContext);
898 _matchAboutPageForAutofill(queryContext) {
899 // Check that the typed query is at least one character longer than the
901 if (this._strippedPrefix != "about:" || !this._searchString) {
905 for (const aboutUrl of lazy.AboutPagesUtils.visibleAboutUrls) {
906 if (aboutUrl.startsWith(`about:${this._searchString.toLowerCase()}`)) {
907 let [trimmedUrl] = UrlbarUtils.stripPrefixAndTrim(aboutUrl, {
909 trimEmptyQuery: true,
910 trimSlash: !this._searchString.includes("/"),
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),
921 let autofilledValue =
922 queryContext.searchString +
923 aboutUrl.substring(queryContext.searchString.length);
926 value: autofilledValue,
927 selectionStart: queryContext.searchString.length,
928 selectionEnd: autofilledValue.length,
936 async _matchKnownUrl(queryContext) {
937 let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
942 // We try to autofill with adaptive history first.
944 lazy.UrlbarPrefs.get("autoFillAdaptiveHistoryEnabled") &&
945 lazy.UrlbarPrefs.get("autoFillAdaptiveHistoryMinCharsThreshold") <=
946 queryContext.searchString.length
948 const [query, params] = this._getAdaptiveHistoryQuery(queryContext);
950 const resultSet = await conn.executeCached(query, params);
951 if (resultSet.length) {
952 return this._processRow(resultSet[0], queryContext);
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) {
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.
971 lazy.UrlbarTokenizer.looksLikeOrigin(this._searchString, {
972 ignoreKnownDomains: true,
975 [query, params] = this._getOriginQuery(queryContext);
977 [query, params] = this._getUrlQuery(queryContext);
980 // _getUrlQuery doesn't always return a query.
982 let rows = await conn.executeCached(query, params);
984 return this._processRow(rows[0], queryContext);
991 export var UrlbarProviderAutofill = new ProviderAutofill();