summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/UrlbarProviderAutofill.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/urlbar/UrlbarProviderAutofill.jsm801
1 files changed, 801 insertions, 0 deletions
diff --git a/browser/components/urlbar/UrlbarProviderAutofill.jsm b/browser/components/urlbar/UrlbarProviderAutofill.jsm
new file mode 100644
index 0000000000..25d35c79e0
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderAutofill.jsm
@@ -0,0 +1,801 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * This module exports a provider that provides an autofill result.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderAutofill"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AboutPagesUtils: "resource://gre/modules/AboutPagesUtils.jsm",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarResult: "resource:///modules/UrlbarResult.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+// AutoComplete query type constants.
+// Describes the various types of queries that we can process rows for.
+const QUERYTYPE = {
+ FILTERED: 0,
+ AUTOFILL_ORIGIN: 1,
+ AUTOFILL_URL: 2,
+ ADAPTIVE: 3,
+};
+
+// `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.
+const SQL_AUTOFILL_WITH = `
+ 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";
+ 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 frecency DESC, prefix = "https://" DESC, id DESC
+ ),
+ host,
+ fixup_url(host),
+ TOTAL(frecency) OVER (PARTITION BY fixup_url(host)),
+ frecency,
+ 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 (host BETWEEN :searchString AND :searchString || X'FFFF')
+ OR (host BETWEEN 'www.' || :searchString AND 'www.' || :searchString || X'FFFF')
+ )
+ SELECT :query_type AS query_type,
+ iif(instr(host, :searchString) = 1, host, fixed) || '/' AS host_fixed,
+ ifnull(:prefix, host_prefix) || host || '/' AS url
+ FROM origins
+ ${where}
+ ORDER BY frecency DESC, prefix = "https://" DESC, id DESC
+ LIMIT 1
+ `;
+}
+
+function urlQuery(where1, where2) {
+ // 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.
+ return `/* do not warn (bug no): cannot use an index to sort */
+ SELECT :query_type AS query_type,
+ url,
+ :strippedURL AS stripped_url,
+ frecency,
+ foreign_count > 0 AS bookmarked,
+ visit_count > 0 AS visited,
+ id
+ FROM moz_places
+ WHERE rev_host = :revHost
+ ${where1}
+ UNION ALL
+ SELECT :query_type AS query_type,
+ url,
+ :strippedURL AS stripped_url,
+ frecency,
+ foreign_count > 0 AS bookmarked,
+ visit_count > 0 AS visited,
+ id
+ FROM moz_places
+ WHERE rev_host = :revHost || 'www.'
+ ${where2}
+ ORDER BY frecency DESC, id DESC
+ LIMIT 1 `;
+}
+// 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 strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
+ `AND (bookmarked OR frecency > 20)
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`
+);
+
+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'`
+);
+
+const QUERY_URL_HISTORY = urlQuery(
+ `AND (visited OR NOT bookmarked)
+ AND frecency > 20
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
+ `AND (visited OR NOT bookmarked)
+ AND frecency > 20
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`
+);
+
+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'`
+);
+
+const QUERY_URL_BOOKMARK = urlQuery(
+ `AND bookmarked
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
+ `AND bookmarked
+ AND strip_prefix_and_userinfo(url) COLLATE NOCASE
+ BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`
+);
+
+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'`
+);
+
+const kProtocolsWithIcons = [
+ "chrome:",
+ "moz-extension:",
+ "about:",
+ "http:",
+ "https:",
+ "ftp:",
+];
+function iconHelper(url) {
+ if (typeof url == "string") {
+ return kProtocolsWithIcons.some(p => url.startsWith(p))
+ ? "page-icon:" + url
+ : UrlbarUtils.ICON.DEFAULT;
+ }
+ if (url && url instanceof URL && kProtocolsWithIcons.includes(url.protocol)) {
+ return "page-icon:" + url.href;
+ }
+ return UrlbarUtils.ICON.DEFAULT;
+}
+
+/**
+ * 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 (!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 == UrlbarTokenizer.TYPE.RESTRICT_TAG ||
+ t.type == UrlbarTokenizer.TYPE.RESTRICT_TITLE
+ )
+ ) {
+ return false;
+ }
+
+ [this._strippedPrefix, this._searchString] = UrlbarUtils.stripURLPrefix(
+ queryContext.searchString
+ );
+ this._strippedPrefix = this._strippedPrefix.toLowerCase();
+
+ if (!this._searchString || !this._searchString.length) {
+ return false;
+ }
+
+ // 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 (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} Resolved when the filtering is complete.
+ * @resolves {string} The top matching host, or null if not found.
+ */
+ async getTopHostOverThreshold(queryContext, hosts) {
+ let db = await PlacesUtils.promiseLargeCacheDBConnection();
+ let conditions = [];
+ // Pay attention to the order of params, since they are not named.
+ let params = [UrlbarPrefs.get("autoFill.stddevMultiplier"), ...hosts];
+ 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 frecency DESC, prefix = "https://" DESC, id DESC
+ ),
+ host,
+ fixup_url(host),
+ TOTAL(frecency) OVER (PARTITION BY fixup_url(host)),
+ frecency,
+ 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
+ * @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(),
+ stddevMultiplier: 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
+ * @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,
+ 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");
+ }
+
+ /**
+ * Processes a matched row in the Places database.
+ * @param {object} row
+ * The matched row.
+ * @param {UrlbarQueryContext} queryContext
+ * @returns {UrlbarResult} a result generated from the matches row.
+ */
+ _processRow(row, queryContext) {
+ let queryType = row.getResultByName("query_type");
+ let autofilledValue, finalCompleteValue;
+ switch (queryType) {
+ case QUERYTYPE.AUTOFILL_ORIGIN:
+ autofilledValue = row.getResultByName("host_fixed");
+ finalCompleteValue = row.getResultByName("url");
+ break;
+ case QUERYTYPE.AUTOFILL_URL:
+ let url = row.getResultByName("url");
+ let strippedURL = row.getResultByName("stripped_url");
+ // 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
+ );
+ if (nextSlashIndex == -1) {
+ autofilledValue = url.substr(strippedURLIndex);
+ } else {
+ autofilledValue = url.substring(strippedURLIndex, nextSlashIndex + 1);
+ }
+ finalCompleteValue = strippedPrefix + autofilledValue;
+ break;
+ }
+
+ let [title] = UrlbarUtils.stripPrefixAndTrim(finalCompleteValue, {
+ stripHttp: true,
+ trimEmptyQuery: true,
+ trimSlash: !this._searchString.includes("/"),
+ });
+ let result = new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
+ url: [finalCompleteValue, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: iconHelper(finalCompleteValue),
+ })
+ );
+ autofilledValue =
+ queryContext.searchString +
+ autofilledValue.substring(this._searchString.length);
+ result.autofill = {
+ value: autofilledValue,
+ selectionStart: queryContext.searchString.length,
+ selectionEnd: autofilledValue.length,
+ };
+ 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 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 UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ title: [trimmedUrl, UrlbarUtils.HIGHLIGHT.TYPED],
+ url: [aboutUrl, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: iconHelper(aboutUrl),
+ })
+ );
+ let autofilledValue =
+ queryContext.searchString +
+ aboutUrl.substring(queryContext.searchString.length);
+ result.autofill = {
+ value: autofilledValue,
+ selectionStart: queryContext.searchString.length,
+ selectionEnd: autofilledValue.length,
+ };
+ return result;
+ }
+ }
+ return null;
+ }
+
+ async _matchKnownUrl(queryContext) {
+ let conn = await PlacesUtils.promiseLargeCacheDBConnection();
+ if (!conn) {
+ 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 (
+ 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 (!UrlbarPrefs.get("autoFill.searchEngines")) {
+ 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 (
+ !UrlbarTokenizer.looksLikeOrigin(searchStr, { ignoreKnownDomains: true })
+ ) {
+ return null;
+ }
+
+ // Since we are autofilling, we can only pick one matching engine. Use the
+ // first.
+ let engine = (await UrlbarSearchUtils.enginesForDomainPrefix(searchStr))[0];
+ if (!engine) {
+ return null;
+ }
+ let url = engine.searchForm;
+ let domain = engine.getResultDomain();
+ // 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 UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ ...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;
+ }
+}
+
+var UrlbarProviderAutofill = new ProviderAutofill();