summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/urlbar/UrlbarProviderAutofill.sys.mjs1096
1 files changed, 1096 insertions, 0 deletions
diff --git a/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs b/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs
new file mode 100644
index 0000000000..0802d71bcb
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs
@@ -0,0 +1,1096 @@
+/* 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 an autofill result.
+ */
+
+import {
+ UrlbarProvider,
+ UrlbarUtils,
+} from "resource:///modules/UrlbarUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AboutPagesUtils: "resource://gre/modules/AboutPagesUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
+});
+
+// AutoComplete query type constants.
+// Describes the various types of queries that we can process rows for.
+const QUERYTYPE = {
+ AUTOFILL_ORIGIN: 1,
+ AUTOFILL_URL: 2,
+ AUTOFILL_ADAPTIVE: 3,
+};
+
+// Constants to support an alternative frecency algorithm.
+const ORIGIN_USE_ALT_FRECENCY = Services.prefs.getBoolPref(
+ "places.frecency.origins.alternative.featureGate",
+ false
+);
+const ORIGIN_FRECENCY_FIELD = ORIGIN_USE_ALT_FRECENCY
+ ? "alt_frecency"
+ : "frecency";
+
+// `WITH` clause for the autofill queries. autofill_frecency_threshold.value is
+// the mean of all moz_origins.frecency values + stddevMultiplier * one standard
+// deviation. This is inlined directly in the SQL (as opposed to being a custom
+// Sqlite function for example) in order to be as efficient as possible.
+// For alternative frecency, a NULL frecency will be normalized to 0.0, and when
+// it will graduate, it will likely become 1 (official frecency is NOT NULL).
+// Thus we set a minimum threshold of 2.0, otherwise if all the visits are older
+// than the cutoff, we end up checking 0.0 (frecency) >= 0.0 (threshold) and
+// autofill everything instead of nothing.
+const SQL_AUTOFILL_WITH = ORIGIN_USE_ALT_FRECENCY
+ ? `
+ WITH
+ autofill_frecency_threshold(value) AS (
+ SELECT IFNULL(
+ (SELECT value FROM moz_meta WHERE key = 'origin_alt_frecency_threshold'),
+ 2.0
+ )
+ )
+ `
+ : `
+ WITH
+ frecency_stats(count, sum, squares) AS (
+ SELECT
+ CAST((SELECT IFNULL(value, 0.0) FROM moz_meta WHERE key = 'origin_frecency_count') AS REAL),
+ CAST((SELECT IFNULL(value, 0.0) FROM moz_meta WHERE key = 'origin_frecency_sum') AS REAL),
+ CAST((SELECT IFNULL(value, 0.0) FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares') AS REAL)
+ ),
+ autofill_frecency_threshold(value) AS (
+ SELECT
+ CASE count
+ WHEN 0 THEN 0.0
+ WHEN 1 THEN sum
+ ELSE (sum / count) + (:stddevMultiplier * sqrt((squares - ((sum * sum) / count)) / count))
+ END
+ FROM frecency_stats
+ )
+ `;
+
+const SQL_AUTOFILL_FRECENCY_THRESHOLD = `host_frecency >= (
+ SELECT value FROM autofill_frecency_threshold
+ )`;
+
+function originQuery(where) {
+ // `frecency`, `bookmarked` and `visited` are partitioned by the fixed host,
+ // without `www.`. `host_prefix` instead is partitioned by full host, because
+ // we assume a prefix may not work regardless of `www.`.
+ let selectVisited = where.includes("visited")
+ ? `MAX(EXISTS(
+ SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0
+ )) OVER (PARTITION BY fixup_url(host)) > 0`
+ : "0";
+ let selectTitle;
+ let joinBookmarks;
+ if (where.includes("bookmarked")) {
+ selectTitle = "ifnull(b.title, iif(h.frecency <> 0, h.title, NULL))";
+ joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = h.id";
+ } else {
+ selectTitle = "iif(h.frecency <> 0, h.title, NULL)";
+ joinBookmarks = "";
+ }
+ return `/* do not warn (bug no): cannot use an index to sort */
+ ${SQL_AUTOFILL_WITH},
+ origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, bookmarked, visited) AS (
+ SELECT
+ id,
+ prefix,
+ first_value(prefix) OVER (
+ PARTITION BY host ORDER BY ${ORIGIN_FRECENCY_FIELD} DESC, prefix = "https://" DESC, id DESC
+ ),
+ host,
+ fixup_url(host),
+ IFNULL(${
+ ORIGIN_USE_ALT_FRECENCY ? "avg(alt_frecency)" : "total(frecency)"
+ } OVER (PARTITION BY fixup_url(host)), 0.0),
+ ${ORIGIN_FRECENCY_FIELD},
+ MAX(EXISTS(
+ SELECT 1 FROM moz_places WHERE origin_id = o.id AND foreign_count > 0
+ )) OVER (PARTITION BY fixup_url(host)),
+ ${selectVisited}
+ FROM moz_origins o
+ WHERE prefix NOT IN ('about:', 'place:')
+ AND ((host BETWEEN :searchString AND :searchString || X'FFFF')
+ OR (host BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF'))
+ ),
+ matched_origin(host_fixed, url) AS (
+ SELECT iif(instr(host, :searchString) = 1, host, fixed) || '/',
+ ifnull(:prefix, host_prefix) || host || '/'
+ FROM origins
+ ${where}
+ ORDER BY frecency DESC, prefix = "https://" DESC, id DESC
+ LIMIT 1
+ ),
+ matched_place(host_fixed, url, id, title, frecency) AS (
+ SELECT o.host_fixed, o.url, h.id, h.title, h.frecency
+ FROM matched_origin o
+ LEFT JOIN moz_places h ON h.url_hash IN (
+ hash('https://' || o.host_fixed),
+ hash('https://www.' || o.host_fixed),
+ hash('http://' || o.host_fixed),
+ hash('http://www.' || o.host_fixed)
+ )
+ ORDER BY
+ h.title IS NOT NULL DESC,
+ h.title || '/' <> o.host_fixed DESC,
+ h.url = o.url DESC,
+ h.frecency DESC,
+ h.id DESC
+ LIMIT 1
+ )
+ SELECT :query_type AS query_type,
+ :searchString AS search_string,
+ h.host_fixed AS host_fixed,
+ h.url AS url,
+ ${selectTitle} AS title
+ FROM matched_place h
+ ${joinBookmarks}
+ `;
+}
+
+function urlQuery(where1, where2, isBookmarkContained) {
+ // We limit the search to places that are either bookmarked or have a frecency
+ // over some small, arbitrary threshold (20) in order to avoid scanning as few
+ // rows as possible. Keep in mind that we run this query every time the user
+ // types a key when the urlbar value looks like a URL with a path.
+ let selectTitle;
+ let joinBookmarks;
+ if (isBookmarkContained) {
+ selectTitle = "ifnull(b.title, matched_url.title)";
+ joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = matched_url.id";
+ } else {
+ selectTitle = "matched_url.title";
+ joinBookmarks = "";
+ }
+ return `/* do not warn (bug no): cannot use an index to sort */
+ WITH matched_url(url, title, frecency, bookmarked, visited, stripped_url, is_exact_match, id) AS (
+ SELECT url,
+ title,
+ frecency,
+ foreign_count > 0 AS bookmarked,
+ visit_count > 0 AS visited,
+ strip_prefix_and_userinfo(url) AS stripped_url,
+ strip_prefix_and_userinfo(url) = strip_prefix_and_userinfo(:strippedURL) AS is_exact_match,
+ id
+ FROM moz_places
+ WHERE rev_host = :revHost
+ ${where1}
+ UNION ALL
+ SELECT url,
+ title,
+ frecency,
+ foreign_count > 0 AS bookmarked,
+ visit_count > 0 AS visited,
+ strip_prefix_and_userinfo(url) AS stripped_url,
+ strip_prefix_and_userinfo(url) = 'www.' || strip_prefix_and_userinfo(:strippedURL) AS is_exact_match,
+ id
+ FROM moz_places
+ WHERE rev_host = :revHost || 'www.'
+ ${where2}
+ ORDER BY is_exact_match DESC, frecency DESC, id DESC
+ LIMIT 1
+ )
+ SELECT :query_type AS query_type,
+ :searchString AS search_string,
+ :strippedURL AS stripped_url,
+ matched_url.url AS url,
+ ${selectTitle} AS title
+ FROM matched_url
+ ${joinBookmarks}
+ `;
+}
+
+// Queries
+const QUERY_ORIGIN_HISTORY_BOOKMARK = originQuery(
+ `WHERE bookmarked OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
+);
+
+const QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK = originQuery(
+ `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF'
+ AND (bookmarked OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`
+);
+
+const QUERY_ORIGIN_HISTORY = originQuery(
+ `WHERE visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
+);
+
+const QUERY_ORIGIN_PREFIX_HISTORY = originQuery(
+ `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF'
+ AND visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
+);
+
+const QUERY_ORIGIN_BOOKMARK = originQuery(`WHERE bookmarked`);
+
+const QUERY_ORIGIN_PREFIX_BOOKMARK = originQuery(
+ `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF' AND bookmarked`
+);
+
+const QUERY_URL_HISTORY_BOOKMARK = urlQuery(
+ `AND (bookmarked OR frecency > 20)
+ AND stripped_url COLLATE NOCASE
+ BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
+ `AND (bookmarked OR frecency > 20)
+ AND stripped_url COLLATE NOCASE
+ BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
+ true
+);
+
+const QUERY_URL_PREFIX_HISTORY_BOOKMARK = urlQuery(
+ `AND (bookmarked OR frecency > 20)
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
+ `AND (bookmarked OR frecency > 20)
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
+ true
+);
+
+const QUERY_URL_HISTORY = urlQuery(
+ `AND (visited OR NOT bookmarked)
+ AND frecency > 20
+ AND stripped_url COLLATE NOCASE
+ BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
+ `AND (visited OR NOT bookmarked)
+ AND frecency > 20
+ AND stripped_url COLLATE NOCASE
+ BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
+ false
+);
+
+const QUERY_URL_PREFIX_HISTORY = urlQuery(
+ `AND (visited OR NOT bookmarked)
+ AND frecency > 20
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
+ `AND (visited OR NOT bookmarked)
+ AND frecency > 20
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
+ false
+);
+
+const QUERY_URL_BOOKMARK = urlQuery(
+ `AND bookmarked
+ AND stripped_url COLLATE NOCASE
+ BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
+ `AND bookmarked
+ AND stripped_url COLLATE NOCASE
+ BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
+ true
+);
+
+const QUERY_URL_PREFIX_BOOKMARK = urlQuery(
+ `AND bookmarked
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
+ `AND bookmarked
+ AND url COLLATE NOCASE
+ BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
+ true
+);
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderAutofill extends UrlbarProvider {
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the name of this provider.
+ *
+ * @returns {string} the name of this provider.
+ */
+ get name() {
+ return "Autofill";
+ }
+
+ /**
+ * 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.
+ */
+ async isActive(queryContext) {
+ let instance = this.queryInstance;
+
+ // This is usually reset on canceling or completing the query, but since we
+ // query in isActive, it may not have been canceled by the previous call.
+ // It is an object with values { result: UrlbarResult, instance: Query }.
+ // See the documentation for _getAutofillData for more information.
+ this._autofillData = null;
+
+ // First of all, check for the autoFill pref.
+ if (!lazy.UrlbarPrefs.get("autoFill")) {
+ return false;
+ }
+
+ if (!queryContext.allowAutofill) {
+ return false;
+ }
+
+ if (queryContext.tokens.length != 1) {
+ return false;
+ }
+
+ // Trying to autofill an extremely long string would be expensive, and
+ // not particularly useful since the filled part falls out of screen anyway.
+ if (queryContext.searchString.length > UrlbarUtils.MAX_TEXT_LENGTH) {
+ return false;
+ }
+
+ // autoFill can only cope with history, bookmarks, and about: entries.
+ if (
+ !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
+ !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
+ ) {
+ return false;
+ }
+
+ // Autofill doesn't search tags or titles
+ if (
+ queryContext.tokens.some(
+ t =>
+ t.type == lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG ||
+ t.type == lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE
+ )
+ ) {
+ return false;
+ }
+
+ [this._strippedPrefix, this._searchString] = UrlbarUtils.stripURLPrefix(
+ queryContext.searchString
+ );
+ this._strippedPrefix = this._strippedPrefix.toLowerCase();
+
+ // Don't try to autofill if the search term includes any whitespace.
+ // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
+ // tokenizer ends up trimming the search string and returning a value
+ // that doesn't match it, or is even shorter.
+ if (lazy.UrlbarTokenizer.REGEXP_SPACES.test(queryContext.searchString)) {
+ return false;
+ }
+
+ // Fetch autofill result now, rather than in startQuery. We do this so the
+ // muxer doesn't have to wait on autofill for every query, since startQuery
+ // will be guaranteed to return a result very quickly using this approach.
+ // Bug 1651101 is filed to improve this behaviour.
+ let result = await this._getAutofillResult(queryContext);
+ if (!result || instance != this.queryInstance) {
+ return false;
+ }
+ this._autofillData = { result, instance };
+ 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) {
+ // Priority search results are restricting.
+ if (
+ this._autofillData &&
+ this._autofillData.instance == this.queryInstance &&
+ this._autofillData.result.type == UrlbarUtils.RESULT_TYPE.SEARCH
+ ) {
+ return 1;
+ }
+
+ 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) {
+ // Check if the query was cancelled while the autofill result was being
+ // fetched. We don't expect this to be true since we also check the instance
+ // in isActive and clear _autofillData in cancelQuery, but we sanity check it.
+ if (
+ !this._autofillData ||
+ this._autofillData.instance != this.queryInstance
+ ) {
+ this.logger.error("startQuery invoked with an invalid _autofillData");
+ return;
+ }
+
+ this._autofillData.result.heuristic = true;
+ addCallback(this, this._autofillData.result);
+ this._autofillData = null;
+ }
+
+ /**
+ * Cancels a running query.
+ *
+ * @param {object} queryContext The query context object
+ */
+ cancelQuery(queryContext) {
+ if (this._autofillData?.instance == this.queryInstance) {
+ this._autofillData = null;
+ }
+ }
+
+ /**
+ * Filters hosts by retaining only the ones over the autofill threshold, then
+ * sorts them by their frecency, and extracts the one with the highest value.
+ *
+ * @param {UrlbarQueryContext} queryContext The current queryContext.
+ * @param {Array} hosts Array of host names to examine.
+ * @returns {Promise<string?>}
+ * Resolved when the filtering is complete. Resolves with the top matching
+ * host, or null if not found.
+ */
+ async getTopHostOverThreshold(queryContext, hosts) {
+ let db = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
+ let conditions = [];
+ // Pay attention to the order of params, since they are not named.
+ let params = [...hosts];
+ if (!ORIGIN_USE_ALT_FRECENCY) {
+ params.unshift(lazy.UrlbarPrefs.get("autoFill.stddevMultiplier"));
+ }
+ let sources = queryContext.sources;
+ if (
+ sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
+ sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
+ ) {
+ conditions.push(`(bookmarked OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`);
+ } else if (sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
+ conditions.push(`visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`);
+ } else if (sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
+ conditions.push("bookmarked");
+ }
+
+ let rows = await db.executeCached(
+ `
+ ${SQL_AUTOFILL_WITH},
+ origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, bookmarked, visited) AS (
+ SELECT
+ id,
+ prefix,
+ first_value(prefix) OVER (
+ PARTITION BY host ORDER BY ${ORIGIN_FRECENCY_FIELD} DESC, prefix = "https://" DESC, id DESC
+ ),
+ host,
+ fixup_url(host),
+ IFNULL(${
+ ORIGIN_USE_ALT_FRECENCY ? "avg(alt_frecency)" : "total(frecency)"
+ } OVER (PARTITION BY fixup_url(host)), 0.0),
+ ${ORIGIN_FRECENCY_FIELD},
+ MAX(EXISTS(
+ SELECT 1 FROM moz_places WHERE origin_id = o.id AND foreign_count > 0
+ )) OVER (PARTITION BY fixup_url(host)),
+ MAX(EXISTS(
+ SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0
+ )) OVER (PARTITION BY fixup_url(host))
+ FROM moz_origins o
+ WHERE o.host IN (${new Array(hosts.length).fill("?").join(",")})
+ )
+ SELECT host
+ FROM origins
+ ${conditions.length ? "WHERE " + conditions.join(" AND ") : ""}
+ ORDER BY frecency DESC, prefix = "https://" DESC, id DESC
+ LIMIT 1
+ `,
+ params
+ );
+ if (!rows.length) {
+ return null;
+ }
+ return rows[0].getResultByName("host");
+ }
+
+ /**
+ * Obtains the query to search for autofill origin results.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The current queryContext.
+ * @returns {Array} consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ _getOriginQuery(queryContext) {
+ // At this point, searchString is not a URL with a path; it does not
+ // contain a slash, except for possibly at the very end. If there is
+ // trailing slash, remove it when searching here to match the rest of the
+ // string because it may be an origin.
+ let searchStr = this._searchString.endsWith("/")
+ ? this._searchString.slice(0, -1)
+ : this._searchString;
+
+ let opts = {
+ query_type: QUERYTYPE.AUTOFILL_ORIGIN,
+ searchString: searchStr.toLowerCase(),
+ };
+ if (!ORIGIN_USE_ALT_FRECENCY) {
+ opts.stddevMultiplier = lazy.UrlbarPrefs.get("autoFill.stddevMultiplier");
+ }
+ if (this._strippedPrefix) {
+ opts.prefix = this._strippedPrefix;
+ }
+
+ if (
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
+ ) {
+ return [
+ this._strippedPrefix
+ ? QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK
+ : QUERY_ORIGIN_HISTORY_BOOKMARK,
+ opts,
+ ];
+ }
+ if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
+ return [
+ this._strippedPrefix
+ ? QUERY_ORIGIN_PREFIX_HISTORY
+ : QUERY_ORIGIN_HISTORY,
+ opts,
+ ];
+ }
+ if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
+ return [
+ this._strippedPrefix
+ ? QUERY_ORIGIN_PREFIX_BOOKMARK
+ : QUERY_ORIGIN_BOOKMARK,
+ opts,
+ ];
+ }
+ throw new Error("Either history or bookmark behavior expected");
+ }
+
+ /**
+ * Obtains the query to search for autoFill url results.
+ *
+ * @param {UrlbarQueryContext} queryContext
+ * The current queryContext.
+ * @returns {Array} consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ _getUrlQuery(queryContext) {
+ // Try to get the host from the search string. The host is the part of the
+ // URL up to either the path slash, port colon, or query "?". If the search
+ // string doesn't look like it begins with a host, then return; it doesn't
+ // make sense to do a URL query with it.
+ const urlQueryHostRegexp = /^[^/:?]+/;
+ let hostMatch = urlQueryHostRegexp.exec(this._searchString);
+ if (!hostMatch) {
+ return [null, null];
+ }
+
+ let host = hostMatch[0].toLowerCase();
+ let revHost = host.split("").reverse().join("") + ".";
+
+ // Build a string that's the URL stripped of its prefix, i.e., the host plus
+ // everything after. Use queryContext.trimmedSearchString instead of
+ // this._searchString because this._searchString has had unEscapeURIForUI()
+ // called on it. It's therefore not necessarily the literal URL.
+ let strippedURL = queryContext.trimmedSearchString;
+ if (this._strippedPrefix) {
+ strippedURL = strippedURL.substr(this._strippedPrefix.length);
+ }
+ strippedURL = host + strippedURL.substr(host.length);
+
+ let opts = {
+ query_type: QUERYTYPE.AUTOFILL_URL,
+ searchString: this._searchString,
+ revHost,
+ strippedURL,
+ };
+ if (this._strippedPrefix) {
+ opts.prefix = this._strippedPrefix;
+ }
+
+ if (
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
+ ) {
+ return [
+ this._strippedPrefix
+ ? QUERY_URL_PREFIX_HISTORY_BOOKMARK
+ : QUERY_URL_HISTORY_BOOKMARK,
+ opts,
+ ];
+ }
+ if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
+ return [
+ this._strippedPrefix ? QUERY_URL_PREFIX_HISTORY : QUERY_URL_HISTORY,
+ opts,
+ ];
+ }
+ if (queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
+ return [
+ this._strippedPrefix ? QUERY_URL_PREFIX_BOOKMARK : QUERY_URL_BOOKMARK,
+ opts,
+ ];
+ }
+ throw new Error("Either history or bookmark behavior expected");
+ }
+
+ _getAdaptiveHistoryQuery(queryContext) {
+ let sourceCondition;
+ if (
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
+ ) {
+ sourceCondition = "(h.foreign_count > 0 OR h.frecency > 20)";
+ } else if (
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)
+ ) {
+ sourceCondition =
+ "((h.visit_count > 0 OR h.foreign_count = 0) AND h.frecency > 20)";
+ } else if (
+ queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
+ ) {
+ sourceCondition = "h.foreign_count > 0";
+ } else {
+ return [];
+ }
+
+ let selectTitle;
+ let joinBookmarks;
+ if (UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
+ selectTitle = "ifnull(b.title, matched.title)";
+ joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = matched.id";
+ } else {
+ selectTitle = "matched.title";
+ joinBookmarks = "";
+ }
+
+ const params = {
+ queryType: QUERYTYPE.AUTOFILL_ADAPTIVE,
+ // `fullSearchString` is the value the user typed including a prefix if
+ // they typed one. `searchString` has been stripped of the prefix.
+ fullSearchString: queryContext.searchString.toLowerCase(),
+ searchString: this._searchString,
+ strippedPrefix: this._strippedPrefix,
+ useCountThreshold: lazy.UrlbarPrefs.get(
+ "autoFillAdaptiveHistoryUseCountThreshold"
+ ),
+ };
+
+ const query = `
+ WITH matched(input, url, title, stripped_url, is_exact_match, starts_with, id) AS (
+ SELECT
+ i.input AS input,
+ h.url AS url,
+ h.title AS title,
+ strip_prefix_and_userinfo(h.url) AS stripped_url,
+ strip_prefix_and_userinfo(h.url) = :searchString AS is_exact_match,
+ (strip_prefix_and_userinfo(h.url) COLLATE NOCASE BETWEEN :searchString AND :searchString || X'FFFF') AS starts_with,
+ h.id AS id
+ FROM moz_places h
+ JOIN moz_inputhistory i ON i.place_id = h.id
+ WHERE LENGTH(i.input) != 0
+ AND :fullSearchString BETWEEN i.input AND i.input || X'FFFF'
+ AND ${sourceCondition}
+ AND i.use_count >= :useCountThreshold
+ AND (:strippedPrefix = '' OR get_prefix(h.url) = :strippedPrefix)
+ AND (
+ starts_with OR
+ (stripped_url COLLATE NOCASE BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF')
+ )
+ ORDER BY is_exact_match DESC, i.use_count DESC, h.frecency DESC, h.id DESC
+ LIMIT 1
+ )
+ SELECT
+ :queryType AS query_type,
+ :searchString AS search_string,
+ input,
+ url,
+ iif(starts_with, stripped_url, fixup_url(stripped_url)) AS url_fixed,
+ ${selectTitle} AS title,
+ stripped_url
+ FROM matched
+ ${joinBookmarks}
+ `;
+
+ return [query, params];
+ }
+
+ /**
+ * Processes a matched row in the Places database.
+ *
+ * @param {object} row
+ * The matched row.
+ * @param {UrlbarQueryContext} queryContext
+ * The query context.
+ * @returns {UrlbarResult} a result generated from the matches row.
+ */
+ _processRow(row, queryContext) {
+ let queryType = row.getResultByName("query_type");
+ let title = row.getResultByName("title");
+
+ // `searchString` is `this._searchString` or derived from it. It is
+ // stripped, meaning the prefix (the URL protocol) has been removed.
+ let searchString = row.getResultByName("search_string");
+
+ // `fixedURL` is the part of the matching stripped URL that starts with the
+ // stripped search string. The important point here is "www" handling. If a
+ // stripped URL starts with "www", we allow the user to omit the "www" and
+ // still match it. So if the matching stripped URL starts with "www" but the
+ // stripped search string does not, `fixedURL` will also omit the "www".
+ // Otherwise `fixedURL` will be equivalent to the matching stripped URL.
+ //
+ // Example 1:
+ // stripped URL: www.example.com/
+ // searchString: exam
+ // fixedURL: example.com/
+ // Example 2:
+ // stripped URL: www.example.com/
+ // searchString: www.exam
+ // fixedURL: www.example.com/
+ // Example 3:
+ // stripped URL: example.com/
+ // searchString: exam
+ // fixedURL: example.com/
+ let fixedURL;
+
+ // `finalCompleteValue` will be the UrlbarResult's URL. If the matching
+ // stripped URL starts with "www" but the user omitted it,
+ // `finalCompleteValue` will include it to properly reflect the real URL.
+ let finalCompleteValue;
+
+ let autofilledType;
+ let adaptiveHistoryInput;
+
+ switch (queryType) {
+ case QUERYTYPE.AUTOFILL_ORIGIN: {
+ fixedURL = row.getResultByName("host_fixed");
+ finalCompleteValue = row.getResultByName("url");
+ autofilledType = "origin";
+ break;
+ }
+ case QUERYTYPE.AUTOFILL_URL: {
+ let url = row.getResultByName("url");
+ let strippedURL = row.getResultByName("stripped_url");
+
+ if (!UrlbarUtils.canAutofillURL(url, strippedURL, true)) {
+ return null;
+ }
+
+ // We autofill urls to-the-next-slash.
+ // http://mozilla.org/foo/bar/baz will be autofilled to:
+ // - http://mozilla.org/f[oo/]
+ // - http://mozilla.org/foo/b[ar/]
+ // - http://mozilla.org/foo/bar/b[az]
+ // And, toLowerCase() is preferred over toLocaleLowerCase() here
+ // because "COLLATE NOCASE" in the SQL only handles ASCII characters.
+ let strippedURLIndex = url
+ .toLowerCase()
+ .indexOf(strippedURL.toLowerCase());
+ let strippedPrefix = url.substr(0, strippedURLIndex);
+ let nextSlashIndex = url.indexOf(
+ "/",
+ strippedURLIndex + strippedURL.length - 1
+ );
+ fixedURL =
+ nextSlashIndex < 0
+ ? url.substr(strippedURLIndex)
+ : url.substring(strippedURLIndex, nextSlashIndex + 1);
+ finalCompleteValue = strippedPrefix + fixedURL;
+ if (finalCompleteValue !== url) {
+ title = null;
+ }
+ autofilledType = "url";
+ break;
+ }
+ case QUERYTYPE.AUTOFILL_ADAPTIVE: {
+ adaptiveHistoryInput = row.getResultByName("input");
+ fixedURL = row.getResultByName("url_fixed");
+ finalCompleteValue = row.getResultByName("url");
+ autofilledType = "adaptive";
+ break;
+ }
+ }
+
+ // Compute `autofilledValue`, the full value that will be placed in the
+ // input. It includes two parts: the part the user already typed in the
+ // character case they typed it (`queryContext.searchString`), and the
+ // autofilled part, which is the portion of the fixed URL starting after the
+ // stripped search string.
+ let autofilledValue =
+ queryContext.searchString + fixedURL.substring(searchString.length);
+
+ // If more than an origin was autofilled and the user typed the full
+ // autofilled value, override the final URL by using the exact value the
+ // user typed. This allows the user to visit a URL that differs from the
+ // autofilled URL only in character case (for example "wikipedia.org/RAID"
+ // vs. "wikipedia.org/Raid") by typing the full desired URL.
+ if (
+ queryType != QUERYTYPE.AUTOFILL_ORIGIN &&
+ queryContext.searchString.length == autofilledValue.length
+ ) {
+ // Use `new URL().href` to lowercase the domain in the final completed
+ // URL. This isn't necessary since domains are case insensitive, but it
+ // looks nicer because it means the domain will remain lowercased in the
+ // input, and it also reflects the fact that Firefox will visit the
+ // lowercased name.
+ const originalCompleteValue = new URL(finalCompleteValue).href;
+ let strippedAutofilledValue = autofilledValue.substring(
+ this._strippedPrefix.length
+ );
+ finalCompleteValue = new URL(
+ finalCompleteValue.substring(
+ 0,
+ finalCompleteValue.length - strippedAutofilledValue.length
+ ) + strippedAutofilledValue
+ ).href;
+
+ // If the character case of except origin part of the original
+ // finalCompleteValue differs from finalCompleteValue that includes user's
+ // input, we set title null because it expresses different web page.
+ if (finalCompleteValue !== originalCompleteValue) {
+ title = null;
+ }
+ }
+
+ let payload = {
+ url: [finalCompleteValue, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: UrlbarUtils.getIconForUrl(finalCompleteValue),
+ };
+
+ if (title) {
+ payload.title = [title, UrlbarUtils.HIGHLIGHT.TYPED];
+ } else {
+ let [autofilled] = UrlbarUtils.stripPrefixAndTrim(finalCompleteValue, {
+ stripHttp: true,
+ trimEmptyQuery: true,
+ trimSlash: !this._searchString.includes("/"),
+ });
+ payload.fallbackTitle = [autofilled, UrlbarUtils.HIGHLIGHT.TYPED];
+ }
+
+ let result = new lazy.UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...lazy.UrlbarResult.payloadAndSimpleHighlights(
+ queryContext.tokens,
+ payload
+ )
+ );
+
+ result.autofill = {
+ adaptiveHistoryInput,
+ value: autofilledValue,
+ selectionStart: queryContext.searchString.length,
+ selectionEnd: autofilledValue.length,
+ type: autofilledType,
+ };
+ return result;
+ }
+
+ async _getAutofillResult(queryContext) {
+ // We may be autofilling an about: link.
+ let result = this._matchAboutPageForAutofill(queryContext);
+ if (result) {
+ return result;
+ }
+
+ // It may also look like a URL we know from the database.
+ result = await this._matchKnownUrl(queryContext);
+ if (result) {
+ return result;
+ }
+
+ // Or we may want to fill a search engine domain regardless of the threshold.
+ result = await this._matchSearchEngineDomain(queryContext);
+ if (result) {
+ return result;
+ }
+
+ return null;
+ }
+
+ _matchAboutPageForAutofill(queryContext) {
+ // Check that the typed query is at least one character longer than the
+ // about: prefix.
+ if (this._strippedPrefix != "about:" || !this._searchString) {
+ return null;
+ }
+
+ for (const aboutUrl of lazy.AboutPagesUtils.visibleAboutUrls) {
+ if (aboutUrl.startsWith(`about:${this._searchString.toLowerCase()}`)) {
+ let [trimmedUrl] = UrlbarUtils.stripPrefixAndTrim(aboutUrl, {
+ stripHttp: true,
+ trimEmptyQuery: true,
+ trimSlash: !this._searchString.includes("/"),
+ });
+ let result = new lazy.UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [trimmedUrl, UrlbarUtils.HIGHLIGHT.TYPED],
+ url: [aboutUrl, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: UrlbarUtils.getIconForUrl(aboutUrl),
+ })
+ );
+ let autofilledValue =
+ queryContext.searchString +
+ aboutUrl.substring(queryContext.searchString.length);
+ result.autofill = {
+ type: "about",
+ value: autofilledValue,
+ selectionStart: queryContext.searchString.length,
+ selectionEnd: autofilledValue.length,
+ };
+ return result;
+ }
+ }
+ return null;
+ }
+
+ async _matchKnownUrl(queryContext) {
+ let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
+ if (!conn) {
+ return null;
+ }
+
+ // We try to autofill with adaptive history first.
+ if (
+ lazy.UrlbarPrefs.get("autoFillAdaptiveHistoryEnabled") &&
+ lazy.UrlbarPrefs.get("autoFillAdaptiveHistoryMinCharsThreshold") <=
+ queryContext.searchString.length
+ ) {
+ const [query, params] = this._getAdaptiveHistoryQuery(queryContext);
+ if (query) {
+ const resultSet = await conn.executeCached(query, params);
+ if (resultSet.length) {
+ return this._processRow(resultSet[0], queryContext);
+ }
+ }
+ }
+
+ // The adaptive history query is passed queryContext.searchString (the full
+ // search string), but the origin and URL queries are passed the prefix
+ // (this._strippedPrefix) and the rest of the search string
+ // (this._searchString) separately. The user must specify a non-prefix part
+ // to trigger origin and URL autofill.
+ if (!this._searchString.length) {
+ return null;
+ }
+
+ // If search string looks like an origin, try to autofill against origins.
+ // Otherwise treat it as a possible URL. When the string has only one slash
+ // at the end, we still treat it as an URL.
+ let query, params;
+ if (
+ lazy.UrlbarTokenizer.looksLikeOrigin(this._searchString, {
+ ignoreKnownDomains: true,
+ })
+ ) {
+ [query, params] = this._getOriginQuery(queryContext);
+ } else {
+ [query, params] = this._getUrlQuery(queryContext);
+ }
+
+ // _getUrlQuery doesn't always return a query.
+ if (query) {
+ let rows = await conn.executeCached(query, params);
+ if (rows.length) {
+ return this._processRow(rows[0], queryContext);
+ }
+ }
+ return null;
+ }
+
+ async _matchSearchEngineDomain(queryContext) {
+ if (
+ !lazy.UrlbarPrefs.get("autoFill.searchEngines") ||
+ !this._searchString.length
+ ) {
+ return null;
+ }
+
+ // enginesForDomainPrefix only matches against engine domains.
+ // Remove an eventual trailing slash from the search string (without the
+ // prefix) and check if the resulting string is worth matching.
+ // Later, we'll verify that the found result matches the original
+ // searchString and eventually discard it.
+ let searchStr = this._searchString;
+ if (searchStr.indexOf("/") == searchStr.length - 1) {
+ searchStr = searchStr.slice(0, -1);
+ }
+ // If the search string looks more like a url than a domain, bail out.
+ if (
+ !lazy.UrlbarTokenizer.looksLikeOrigin(searchStr, {
+ ignoreKnownDomains: true,
+ })
+ ) {
+ return null;
+ }
+
+ // Since we are autofilling, we can only pick one matching engine. Use the
+ // first.
+ let engine = (
+ await lazy.UrlbarSearchUtils.enginesForDomainPrefix(searchStr)
+ )[0];
+ if (!engine) {
+ return null;
+ }
+ let url = engine.searchForm;
+ let domain = engine.searchUrlDomain;
+ // Verify that the match we got is acceptable. Autofilling "example/" to
+ // "example.com/" would not be good.
+ if (
+ (this._strippedPrefix && !url.startsWith(this._strippedPrefix)) ||
+ !(domain + "/").includes(this._searchString)
+ ) {
+ return null;
+ }
+
+ // The value that's autofilled in the input is the prefix the user typed, if
+ // any, plus the portion of the engine domain that the user typed. Append a
+ // trailing slash too, as is usual with autofill.
+ let value =
+ this._strippedPrefix + domain.substr(domain.indexOf(searchStr)) + "/";
+
+ let result = 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,
+ })
+ );
+ let autofilledValue =
+ queryContext.searchString +
+ value.substring(queryContext.searchString.length);
+ result.autofill = {
+ value: autofilledValue,
+ selectionStart: queryContext.searchString.length,
+ selectionEnd: autofilledValue.length,
+ };
+ return result;
+ }
+}
+
+export var UrlbarProviderAutofill = new ProviderAutofill();