tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

UrlbarProviderAutofill.sys.mjs (35267B)


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