mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	This adds two calls that were originally included in https://phabricator.services.mozilla.com/D231168 but appear to have been clobbered by https://phabricator.services.mozilla.com/D252333 by accident. Differential Revision: https://phabricator.services.mozilla.com/D254441
		
			
				
	
	
		
			1571 lines
		
	
	
	
		
			53 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1571 lines
		
	
	
	
		
			53 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 | 
						|
 * vim: sw=2 ts=2 sts=2 expandtab
 | 
						|
 * 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/. */
 | 
						|
/* eslint complexity: ["error", 53] */
 | 
						|
 | 
						|
/**
 | 
						|
 * This module exports a provider that provides results from the Places
 | 
						|
 * database, including history, bookmarks, and open tabs.
 | 
						|
 */
 | 
						|
// Constants
 | 
						|
 | 
						|
// The default frecency value used when inserting matches with unknown frecency.
 | 
						|
const FRECENCY_DEFAULT = 1000;
 | 
						|
 | 
						|
// The result is notified on a delay, to avoid rebuilding the panel at every match.
 | 
						|
const NOTIFYRESULT_DELAY_MS = 16;
 | 
						|
 | 
						|
// This SQL query fragment provides the following:
 | 
						|
//   - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED)
 | 
						|
//   - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE)
 | 
						|
//   - the tags associated with a bookmarked entry (QUERYINDEX_TAGS)
 | 
						|
const SQL_BOOKMARK_TAGS_FRAGMENT = `
 | 
						|
   EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,
 | 
						|
   ( SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL
 | 
						|
     ORDER BY lastModified DESC LIMIT 1
 | 
						|
   ) AS btitle,
 | 
						|
   ( SELECT GROUP_CONCAT(t.title ORDER BY t.title)
 | 
						|
     FROM moz_bookmarks b
 | 
						|
     JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent
 | 
						|
     WHERE b.fk = h.id
 | 
						|
   ) AS tags`;
 | 
						|
 | 
						|
// TODO bug 412736: in case of a frecency tie, we might break it with h.typed
 | 
						|
// and h.visit_count.  That is slower though, so not doing it yet...
 | 
						|
// NB: as a slight performance optimization, we only evaluate the "bookmarked"
 | 
						|
// condition once, and avoid evaluating "btitle" and "tags" when it is false.
 | 
						|
function defaultQuery(conditions = "") {
 | 
						|
  let query = `
 | 
						|
     SELECT h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT}, h.id, t.open_count,
 | 
						|
            ${lazy.PAGES_FRECENCY_FIELD} AS frecency, t.userContextId,
 | 
						|
            h.last_visit_date, t.groupId
 | 
						|
     FROM moz_places h
 | 
						|
     LEFT JOIN moz_openpages_temp t
 | 
						|
            ON t.url = h.url
 | 
						|
            AND (t.userContextId = :userContextId OR (t.userContextId <> -1 AND :userContextId IS NULL))
 | 
						|
     WHERE (
 | 
						|
        (:switchTabsEnabled AND t.open_count > 0) OR
 | 
						|
        ${lazy.PAGES_FRECENCY_FIELD} <> 0
 | 
						|
       )
 | 
						|
       AND CASE WHEN bookmarked
 | 
						|
         THEN
 | 
						|
           AUTOCOMPLETE_MATCH(:searchString, h.url,
 | 
						|
                              IFNULL(btitle, h.title), tags,
 | 
						|
                              h.visit_count, h.typed,
 | 
						|
                              1, t.open_count,
 | 
						|
                              :matchBehavior, :searchBehavior, NULL)
 | 
						|
         ELSE
 | 
						|
           AUTOCOMPLETE_MATCH(:searchString, h.url,
 | 
						|
                              h.title, '',
 | 
						|
                              h.visit_count, h.typed,
 | 
						|
                              0, t.open_count,
 | 
						|
                              :matchBehavior, :searchBehavior, NULL)
 | 
						|
         END
 | 
						|
       ${conditions ? "AND" : ""} ${conditions}
 | 
						|
     ORDER BY ${lazy.PAGES_FRECENCY_FIELD} DESC, h.id DESC
 | 
						|
     LIMIT :maxResults`;
 | 
						|
  return query;
 | 
						|
}
 | 
						|
 | 
						|
const SQL_SWITCHTAB_QUERY = `
 | 
						|
    SELECT t.url, t.url AS title, 0 AS bookmarked, NULL AS btitle,
 | 
						|
           NULL AS tags, NULL AS id, t.open_count, NULL AS frecency,
 | 
						|
           t.userContextId, NULL AS last_visit_date, t.groupId
 | 
						|
   FROM moz_openpages_temp t
 | 
						|
   LEFT JOIN moz_places h ON h.url_hash = hash(t.url) AND h.url = t.url
 | 
						|
   WHERE h.id IS NULL
 | 
						|
     AND (t.userContextId = :userContextId OR (t.userContextId <> -1 AND :userContextId IS NULL))
 | 
						|
     AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,
 | 
						|
                            NULL, NULL, NULL, t.open_count,
 | 
						|
                            :matchBehavior, :searchBehavior, NULL)
 | 
						|
   ORDER BY t.ROWID DESC
 | 
						|
   LIMIT :maxResults`;
 | 
						|
 | 
						|
// Getters
 | 
						|
 | 
						|
import {
 | 
						|
  UrlbarProvider,
 | 
						|
  UrlbarUtils,
 | 
						|
} from "resource:///modules/UrlbarUtils.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  KeywordUtils: "resource://gre/modules/KeywordUtils.sys.mjs",
 | 
						|
  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
 | 
						|
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
 | 
						|
  Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
 | 
						|
  UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
 | 
						|
  UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
 | 
						|
  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
 | 
						|
  UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
 | 
						|
  UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
 | 
						|
  UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
// Constants to support an alternative frecency algorithm.
 | 
						|
ChromeUtils.defineLazyGetter(lazy, "PAGES_FRECENCY_FIELD", () => {
 | 
						|
  return lazy.PlacesUtils.history.isAlternativeFrecencyEnabled
 | 
						|
    ? "alt_frecency"
 | 
						|
    : "frecency";
 | 
						|
});
 | 
						|
 | 
						|
function setTimeout(callback, ms) {
 | 
						|
  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 | 
						|
  timer.initWithCallback(callback, ms, timer.TYPE_ONE_SHOT);
 | 
						|
  return timer;
 | 
						|
}
 | 
						|
 | 
						|
// Maps restriction character types to textual behaviors.
 | 
						|
ChromeUtils.defineLazyGetter(lazy, "typeToBehaviorMap", () => {
 | 
						|
  return new Map([
 | 
						|
    [lazy.UrlbarTokenizer.TYPE.RESTRICT_HISTORY, "history"],
 | 
						|
    [lazy.UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK, "bookmark"],
 | 
						|
    [lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG, "tag"],
 | 
						|
    [lazy.UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE, "openpage"],
 | 
						|
    [lazy.UrlbarTokenizer.TYPE.RESTRICT_SEARCH, "search"],
 | 
						|
    [lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE, "title"],
 | 
						|
    [lazy.UrlbarTokenizer.TYPE.RESTRICT_URL, "url"],
 | 
						|
  ]);
 | 
						|
});
 | 
						|
 | 
						|
ChromeUtils.defineLazyGetter(lazy, "sourceToBehaviorMap", () => {
 | 
						|
  return new Map([
 | 
						|
    [UrlbarUtils.RESULT_SOURCE.HISTORY, "history"],
 | 
						|
    [UrlbarUtils.RESULT_SOURCE.BOOKMARKS, "bookmark"],
 | 
						|
    [UrlbarUtils.RESULT_SOURCE.TABS, "openpage"],
 | 
						|
    [UrlbarUtils.RESULT_SOURCE.SEARCH, "search"],
 | 
						|
  ]);
 | 
						|
});
 | 
						|
 | 
						|
// Helper functions
 | 
						|
 | 
						|
/**
 | 
						|
 * Constructs the map key by joining the url with the userContextId if the pref is
 | 
						|
 * set. Otherwise, just the url is used
 | 
						|
 *
 | 
						|
 * @param   {string} url
 | 
						|
 *          The url to use
 | 
						|
 * @param   {object} match
 | 
						|
 *          The match object with the (optional) userContextId
 | 
						|
 * @returns {string} map key
 | 
						|
 */
 | 
						|
function makeMapKeyForResult(url, match) {
 | 
						|
  let action = lazy.PlacesUtils.parseActionUrl(match.value);
 | 
						|
  return UrlbarUtils.tupleString(
 | 
						|
    url,
 | 
						|
    action?.type == "switchtab" &&
 | 
						|
      lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") &&
 | 
						|
      lazy.UrlbarProviderOpenTabs.isNonPrivateUserContextId(match.userContextId)
 | 
						|
      ? match.userContextId
 | 
						|
      : undefined
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns the key to be used for a match in a map for the purposes of removing
 | 
						|
 * duplicate entries - any 2 matches that should be considered the same should
 | 
						|
 * return the same key.  The type of the returned key depends on the type of the
 | 
						|
 * match.
 | 
						|
 *
 | 
						|
 * @param   {object} match
 | 
						|
 *          The match object.
 | 
						|
 * @returns {object} Some opaque key object.  Use ObjectUtils.deepEqual() to
 | 
						|
 *          compare keys.
 | 
						|
 */
 | 
						|
function makeKeyForMatch(match) {
 | 
						|
  let key, prefix;
 | 
						|
  let action = lazy.PlacesUtils.parseActionUrl(match.value);
 | 
						|
  if (!action) {
 | 
						|
    [key, prefix] = UrlbarUtils.stripPrefixAndTrim(match.value, {
 | 
						|
      stripHttp: true,
 | 
						|
      stripHttps: true,
 | 
						|
      stripWww: true,
 | 
						|
      trimSlash: true,
 | 
						|
      trimEmptyQuery: true,
 | 
						|
      trimEmptyHash: true,
 | 
						|
    });
 | 
						|
    return [makeMapKeyForResult(key, match), prefix, null];
 | 
						|
  }
 | 
						|
 | 
						|
  switch (action.type) {
 | 
						|
    case "searchengine":
 | 
						|
      // We want to exclude search suggestion matches that simply echo back the
 | 
						|
      // query string in the heuristic result.  For example, if the user types
 | 
						|
      // "@engine test", we want to exclude a "test" suggestion match.
 | 
						|
      key = [
 | 
						|
        action.type,
 | 
						|
        action.params.engineName,
 | 
						|
        (
 | 
						|
          action.params.searchSuggestion || action.params.searchQuery
 | 
						|
        ).toLocaleLowerCase(),
 | 
						|
      ];
 | 
						|
      break;
 | 
						|
    default:
 | 
						|
      [key, prefix] = UrlbarUtils.stripPrefixAndTrim(
 | 
						|
        action.params.url || match.value,
 | 
						|
        {
 | 
						|
          stripHttp: true,
 | 
						|
          stripHttps: true,
 | 
						|
          stripWww: true,
 | 
						|
          trimEmptyQuery: true,
 | 
						|
          trimSlash: true,
 | 
						|
        }
 | 
						|
      );
 | 
						|
      break;
 | 
						|
  }
 | 
						|
  let resKey = makeMapKeyForResult(key, match);
 | 
						|
  return [resKey, prefix, action];
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Makes a moz-action url for the given action and set of parameters.
 | 
						|
 *
 | 
						|
 * @param   {string} type
 | 
						|
 *          The action type.
 | 
						|
 * @param   {object} params
 | 
						|
 *          A JS object of action params.
 | 
						|
 * @returns {string} A moz-action url as a string.
 | 
						|
 */
 | 
						|
function makeActionUrl(type, params) {
 | 
						|
  let encodedParams = {};
 | 
						|
  for (let key in params) {
 | 
						|
    // Strip null or undefined.
 | 
						|
    // Regardless, don't encode them or they would be converted to a string.
 | 
						|
    if (params[key] === null || params[key] === undefined) {
 | 
						|
      continue;
 | 
						|
    }
 | 
						|
    encodedParams[key] = encodeURIComponent(params[key]);
 | 
						|
  }
 | 
						|
  return `moz-action:${type},${JSON.stringify(encodedParams)}`;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Converts an array of legacy match objects into UrlbarResults.
 | 
						|
 * Note that at every call we get the full set of results, included the
 | 
						|
 * previously returned ones, and new results may be inserted in the middle.
 | 
						|
 * This means we could sort these wrongly, the muxer should take care of it.
 | 
						|
 *
 | 
						|
 * @param {UrlbarQueryContext} context the query context.
 | 
						|
 * @param {Array} matches The match objects.
 | 
						|
 * @param {Set} urls a Set containing all the found urls, userContextId tuple
 | 
						|
 *        strings used to discard already added results.
 | 
						|
 * @returns {Array} converted results
 | 
						|
 */
 | 
						|
function convertLegacyMatches(context, matches, urls) {
 | 
						|
  let results = [];
 | 
						|
  for (let match of matches) {
 | 
						|
    // First, let's check if we already added this result.
 | 
						|
    // `matches` always contains all of the results, includes ones
 | 
						|
    // we may have added already. This means we'll end up adding things in the
 | 
						|
    // wrong order here, but that's a task for the UrlbarMuxer.
 | 
						|
    let url = match.finalCompleteValue || match.value;
 | 
						|
    if (urls.has(makeMapKeyForResult(url, match))) {
 | 
						|
      continue;
 | 
						|
    }
 | 
						|
    urls.add(makeMapKeyForResult(url, match));
 | 
						|
    let result = makeUrlbarResult(context.tokens, {
 | 
						|
      url,
 | 
						|
      // `match.icon` is an empty string if there is no icon. Use undefined
 | 
						|
      // instead so that tests can be simplified by not including `icon: ""` in
 | 
						|
      // all their payloads.
 | 
						|
      icon: match.icon || undefined,
 | 
						|
      style: match.style,
 | 
						|
      comment: match.comment,
 | 
						|
      firstToken: context.tokens[0],
 | 
						|
      userContextId: match.userContextId,
 | 
						|
      lastVisit: match.lastVisit,
 | 
						|
      tabGroup: match.tabGroup,
 | 
						|
      frecency: match.frecency,
 | 
						|
    });
 | 
						|
    // Should not happen, but better safe than sorry.
 | 
						|
    if (!result) {
 | 
						|
      continue;
 | 
						|
    }
 | 
						|
 | 
						|
    results.push(result);
 | 
						|
  }
 | 
						|
  return results;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Creates a new UrlbarResult from the provided data.
 | 
						|
 *
 | 
						|
 * @param {Array} tokens the search tokens.
 | 
						|
 * @param {object} info includes properties from the legacy result.
 | 
						|
 * @returns {object} an UrlbarResult
 | 
						|
 */
 | 
						|
function makeUrlbarResult(tokens, info) {
 | 
						|
  let action = lazy.PlacesUtils.parseActionUrl(info.url);
 | 
						|
  if (action) {
 | 
						|
    switch (action.type) {
 | 
						|
      case "searchengine":
 | 
						|
        // Return a form history result.
 | 
						|
        return new lazy.UrlbarResult(
 | 
						|
          UrlbarUtils.RESULT_TYPE.SEARCH,
 | 
						|
          UrlbarUtils.RESULT_SOURCE.HISTORY,
 | 
						|
          ...lazy.UrlbarResult.payloadAndSimpleHighlights(tokens, {
 | 
						|
            engine: action.params.engineName,
 | 
						|
            isBlockable: true,
 | 
						|
            blockL10n: { id: "urlbar-result-menu-remove-from-history" },
 | 
						|
            helpUrl:
 | 
						|
              Services.urlFormatter.formatURLPref("app.support.baseURL") +
 | 
						|
              "awesome-bar-result-menu",
 | 
						|
            suggestion: [
 | 
						|
              action.params.searchSuggestion,
 | 
						|
              UrlbarUtils.HIGHLIGHT.SUGGESTED,
 | 
						|
            ],
 | 
						|
            lowerCaseSuggestion:
 | 
						|
              action.params.searchSuggestion.toLocaleLowerCase(),
 | 
						|
          })
 | 
						|
        );
 | 
						|
      case "switchtab": {
 | 
						|
        let payload = lazy.UrlbarResult.payloadAndSimpleHighlights(tokens, {
 | 
						|
          url: [action.params.url, UrlbarUtils.HIGHLIGHT.TYPED],
 | 
						|
          title: [info.comment, UrlbarUtils.HIGHLIGHT.TYPED],
 | 
						|
          icon: info.icon,
 | 
						|
          userContextId: info.userContextId,
 | 
						|
          lastVisit: info.lastVisit,
 | 
						|
          tabGroup: info.tabGroup,
 | 
						|
          frecency: info.frecency,
 | 
						|
        });
 | 
						|
        if (lazy.UrlbarPrefs.get("secondaryActions.switchToTab")) {
 | 
						|
          payload[0].action = UrlbarUtils.createTabSwitchSecondaryAction(
 | 
						|
            info.userContextId
 | 
						|
          );
 | 
						|
        }
 | 
						|
        return new lazy.UrlbarResult(
 | 
						|
          UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
 | 
						|
          UrlbarUtils.RESULT_SOURCE.TABS,
 | 
						|
          ...payload
 | 
						|
        );
 | 
						|
      }
 | 
						|
      default:
 | 
						|
        console.error(`Unexpected action type: ${action.type}`);
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // This is a normal url/title tuple.
 | 
						|
  let source;
 | 
						|
  let tags = [];
 | 
						|
  let comment = info.comment;
 | 
						|
  let isBlockable;
 | 
						|
  let blockL10n;
 | 
						|
  let helpUrl;
 | 
						|
 | 
						|
  // The legacy autocomplete result may return "bookmark", "bookmark-tag" or
 | 
						|
  // "tag". In the last case it should not be considered a bookmark, but an
 | 
						|
  // history item with tags. We don't show tags for non bookmarked items though.
 | 
						|
  if (info.style.includes("bookmark")) {
 | 
						|
    source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS;
 | 
						|
  } else {
 | 
						|
    source = UrlbarUtils.RESULT_SOURCE.HISTORY;
 | 
						|
    isBlockable = true;
 | 
						|
    blockL10n = { id: "urlbar-result-menu-remove-from-history" };
 | 
						|
    helpUrl =
 | 
						|
      Services.urlFormatter.formatURLPref("app.support.baseURL") +
 | 
						|
      "awesome-bar-result-menu";
 | 
						|
  }
 | 
						|
 | 
						|
  // If the style indicates that the result is tagged, then the tags are
 | 
						|
  // included in the title, and we must extract them.
 | 
						|
  if (info.style.includes("tag")) {
 | 
						|
    [comment, tags] = info.comment.split(UrlbarUtils.TITLE_TAGS_SEPARATOR);
 | 
						|
 | 
						|
    // However, as mentioned above, we don't want to show tags for non-
 | 
						|
    // bookmarked items, so we include tags in the final result only if it's
 | 
						|
    // bookmarked, and we drop the tags otherwise.
 | 
						|
    if (source != UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
 | 
						|
      tags = "";
 | 
						|
    }
 | 
						|
 | 
						|
    // Tags are separated by a comma.
 | 
						|
    // We should also just include tags that match the searchString.
 | 
						|
    tags = tags.split(",").filter(tag => {
 | 
						|
      let lowerCaseTag = tag.toLocaleLowerCase();
 | 
						|
      return tokens.some(token => lowerCaseTag.includes(token.lowerCaseValue));
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  return new lazy.UrlbarResult(
 | 
						|
    UrlbarUtils.RESULT_TYPE.URL,
 | 
						|
    source,
 | 
						|
    ...lazy.UrlbarResult.payloadAndSimpleHighlights(tokens, {
 | 
						|
      url: [info.url, UrlbarUtils.HIGHLIGHT.TYPED],
 | 
						|
      icon: info.icon,
 | 
						|
      title: [comment, UrlbarUtils.HIGHLIGHT.TYPED],
 | 
						|
      tags: [tags, UrlbarUtils.HIGHLIGHT.TYPED],
 | 
						|
      isBlockable,
 | 
						|
      blockL10n,
 | 
						|
      helpUrl,
 | 
						|
      lastVisit: info.lastVisit,
 | 
						|
      frecency: info.frecency,
 | 
						|
    })
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
const MATCH_TYPE = {
 | 
						|
  HEURISTIC: "heuristic",
 | 
						|
  GENERAL: "general",
 | 
						|
  SUGGESTION: "suggestion",
 | 
						|
  EXTENSION: "extension",
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Manages a single instance of a Places search.
 | 
						|
 *
 | 
						|
 * @param {UrlbarQueryContext} queryContext
 | 
						|
 *   The query context.
 | 
						|
 * @param {Function} listener
 | 
						|
 *   Called as: `listener(matches, searchOngoing)`
 | 
						|
 * @param {UrlbarProviderPlaces} provider
 | 
						|
 *   The singleton that contains Places information
 | 
						|
 */
 | 
						|
function Search(queryContext, listener, provider) {
 | 
						|
  // We want to store the original string for case sensitive searches.
 | 
						|
  this._originalSearchString = queryContext.searchString;
 | 
						|
  this._trimmedOriginalSearchString = queryContext.trimmedSearchString;
 | 
						|
  let unescapedSearchString = UrlbarUtils.unEscapeURIForUI(
 | 
						|
    this._trimmedOriginalSearchString
 | 
						|
  );
 | 
						|
  // We want to make sure "about:" is not stripped as a prefix so that the
 | 
						|
  // about pages provider will run and ultimately only suggest about pages when
 | 
						|
  // a user types "about:" into the address bar.
 | 
						|
  let prefix, suffix;
 | 
						|
  if (unescapedSearchString.startsWith("about:")) {
 | 
						|
    prefix = "";
 | 
						|
    suffix = unescapedSearchString;
 | 
						|
  } else {
 | 
						|
    [prefix, suffix] = UrlbarUtils.stripURLPrefix(unescapedSearchString);
 | 
						|
  }
 | 
						|
  this._searchString = suffix;
 | 
						|
  this._strippedPrefix = prefix.toLowerCase();
 | 
						|
 | 
						|
  this._matchBehavior = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
 | 
						|
  // Set the default behavior for this search.
 | 
						|
  this._behavior = this._searchString
 | 
						|
    ? lazy.UrlbarPrefs.get("defaultBehavior")
 | 
						|
    : this._emptySearchDefaultBehavior;
 | 
						|
 | 
						|
  this._inPrivateWindow = queryContext.isPrivate;
 | 
						|
  this._prohibitAutoFill = !queryContext.allowAutofill;
 | 
						|
  // Increase the limit for the query because some results might
 | 
						|
  // get deduplicated if their URLs only differ by their refs.
 | 
						|
  this._maxResults = Math.round(queryContext.maxResults * 1.5);
 | 
						|
  this._userContextId = queryContext.userContextId;
 | 
						|
  this._currentPage = queryContext.currentPage;
 | 
						|
  this._searchModeEngine = queryContext.searchMode?.engineName;
 | 
						|
  this._searchMode = queryContext.searchMode;
 | 
						|
  if (this._searchModeEngine) {
 | 
						|
    // Filter Places results on host.
 | 
						|
    let engine = Services.search.getEngineByName(this._searchModeEngine);
 | 
						|
    this._filterOnHost = engine.searchUrlDomain;
 | 
						|
  }
 | 
						|
 | 
						|
  // Use the original string here, not the stripped one, so the tokenizer can
 | 
						|
  // properly recognize token types.
 | 
						|
  let { tokens } = lazy.UrlbarTokenizer.tokenize({
 | 
						|
    searchString: unescapedSearchString,
 | 
						|
    trimmedSearchString: unescapedSearchString.trim(),
 | 
						|
  });
 | 
						|
 | 
						|
  // This allows to handle leading or trailing restriction characters specially.
 | 
						|
  this._leadingRestrictionToken = null;
 | 
						|
  if (tokens.length) {
 | 
						|
    if (
 | 
						|
      lazy.UrlbarTokenizer.isRestrictionToken(tokens[0]) &&
 | 
						|
      (tokens.length > 1 ||
 | 
						|
        tokens[0].type == lazy.UrlbarTokenizer.TYPE.RESTRICT_SEARCH)
 | 
						|
    ) {
 | 
						|
      this._leadingRestrictionToken = tokens[0].value;
 | 
						|
    }
 | 
						|
 | 
						|
    // Check if the first token has a strippable prefix other than "about:"
 | 
						|
    // and remove it, but don't create an empty token. We preserve "about:"
 | 
						|
    // so that the about pages provider will run and ultimately only suggest
 | 
						|
    // about pages when a user types "about:" into the address bar.
 | 
						|
    if (
 | 
						|
      prefix &&
 | 
						|
      prefix != "about:" &&
 | 
						|
      tokens[0].value.length > prefix.length
 | 
						|
    ) {
 | 
						|
      tokens[0].value = tokens[0].value.substring(prefix.length);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Eventually filter restriction tokens. In general it's a good idea, but if
 | 
						|
  // the consumer requested search mode, we should use the full string to avoid
 | 
						|
  // ignoring valid tokens.
 | 
						|
  this._searchTokens =
 | 
						|
    !queryContext || queryContext.restrictToken
 | 
						|
      ? this.filterTokens(tokens)
 | 
						|
      : tokens;
 | 
						|
 | 
						|
  // The behavior can be set through:
 | 
						|
  // 1. a specific restrictSource in the QueryContext
 | 
						|
  // 2. typed restriction tokens
 | 
						|
  if (
 | 
						|
    queryContext &&
 | 
						|
    queryContext.restrictSource &&
 | 
						|
    lazy.sourceToBehaviorMap.has(queryContext.restrictSource)
 | 
						|
  ) {
 | 
						|
    this._behavior = 0;
 | 
						|
    this.setBehavior("restrict");
 | 
						|
    let behavior = lazy.sourceToBehaviorMap.get(queryContext.restrictSource);
 | 
						|
    this.setBehavior(behavior);
 | 
						|
 | 
						|
    // When we are in restrict mode, all the tokens are valid for searching, so
 | 
						|
    // there is no _heuristicToken.
 | 
						|
    this._heuristicToken = null;
 | 
						|
  } else {
 | 
						|
    // The heuristic token is the first filtered search token, but only when it's
 | 
						|
    // actually the first thing in the search string.  If a prefix or restriction
 | 
						|
    // character occurs first, then the heurstic token is null.  We use the
 | 
						|
    // heuristic token to help determine the heuristic result.
 | 
						|
    let firstToken = !!this._searchTokens.length && this._searchTokens[0].value;
 | 
						|
    this._heuristicToken =
 | 
						|
      firstToken && this._trimmedOriginalSearchString.startsWith(firstToken)
 | 
						|
        ? firstToken
 | 
						|
        : null;
 | 
						|
  }
 | 
						|
 | 
						|
  // Set the right JavaScript behavior based on our preference.  Note that the
 | 
						|
  // preference is whether or not we should filter JavaScript, and the
 | 
						|
  // behavior is if we should search it or not.
 | 
						|
  if (!lazy.UrlbarPrefs.get("filter.javascript")) {
 | 
						|
    this.setBehavior("javascript");
 | 
						|
  }
 | 
						|
 | 
						|
  this._listener = listener;
 | 
						|
  this._provider = provider;
 | 
						|
  this._matches = [];
 | 
						|
 | 
						|
  // These are used to avoid adding duplicate entries to the results.
 | 
						|
  this._usedURLs = [];
 | 
						|
  this._usedPlaceIds = new Set();
 | 
						|
 | 
						|
  // Counters for the number of results per MATCH_TYPE.
 | 
						|
  this._counts = Object.values(MATCH_TYPE).reduce((o, p) => {
 | 
						|
    o[p] = 0;
 | 
						|
    return o;
 | 
						|
  }, {});
 | 
						|
}
 | 
						|
 | 
						|
Search.prototype = {
 | 
						|
  /**
 | 
						|
   * Enables the desired AutoComplete behavior.
 | 
						|
   *
 | 
						|
   * @param {string} type
 | 
						|
   *        The behavior type to set.
 | 
						|
   */
 | 
						|
  setBehavior(type) {
 | 
						|
    type = type.toUpperCase();
 | 
						|
    this._behavior |= Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type];
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Determines if the specified AutoComplete behavior is set.
 | 
						|
   *
 | 
						|
   * @param {string} type
 | 
						|
   *        The behavior type to test for.
 | 
						|
   * @returns {boolean} true if the behavior is set, false otherwise.
 | 
						|
   */
 | 
						|
  hasBehavior(type) {
 | 
						|
    let behavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
 | 
						|
    return this._behavior & behavior;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Given an array of tokens, this function determines which query should be
 | 
						|
   * ran.  It also removes any special search tokens.
 | 
						|
   *
 | 
						|
   * @param {Array} tokens
 | 
						|
   *        An array of search tokens.
 | 
						|
   * @returns {Array} A new, filtered array of tokens.
 | 
						|
   */
 | 
						|
  filterTokens(tokens) {
 | 
						|
    let foundToken = false;
 | 
						|
    // Set the proper behavior while filtering tokens.
 | 
						|
    let filtered = [];
 | 
						|
    for (let token of tokens) {
 | 
						|
      if (!lazy.UrlbarTokenizer.isRestrictionToken(token)) {
 | 
						|
        filtered.push(token);
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      let behavior = lazy.typeToBehaviorMap.get(token.type);
 | 
						|
      if (!behavior) {
 | 
						|
        throw new Error(`Unknown token type ${token.type}`);
 | 
						|
      }
 | 
						|
      // Don't use the suggest preferences if it is a token search and
 | 
						|
      // set the restrict bit to 1 (to intersect the search results).
 | 
						|
      if (!foundToken) {
 | 
						|
        foundToken = true;
 | 
						|
        // Do not take into account previous behavior (e.g.: history, bookmark)
 | 
						|
        this._behavior = 0;
 | 
						|
        this.setBehavior("restrict");
 | 
						|
      }
 | 
						|
      this.setBehavior(behavior);
 | 
						|
      // We return tags only for bookmarks, thus when tags are enforced, we
 | 
						|
      // must also set the bookmark behavior.
 | 
						|
      if (behavior == "tag") {
 | 
						|
        this.setBehavior("bookmark");
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return filtered;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Stop this search.
 | 
						|
   * After invoking this method, we won't run any more searches or heuristics,
 | 
						|
   * and no new matches may be added to the current result.
 | 
						|
   */
 | 
						|
  stop() {
 | 
						|
    // Avoid multiple calls or re-entrance.
 | 
						|
    if (!this.pending) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (this._notifyTimer) {
 | 
						|
      this._notifyTimer.cancel();
 | 
						|
    }
 | 
						|
    this._notifyDelaysCount = 0;
 | 
						|
    if (typeof this.interrupt == "function") {
 | 
						|
      this.interrupt();
 | 
						|
    }
 | 
						|
    this.pending = false;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Whether this search is active.
 | 
						|
   */
 | 
						|
  pending: true,
 | 
						|
 | 
						|
  /**
 | 
						|
   * Execute the search and populate results.
 | 
						|
   *
 | 
						|
   * @param {mozIStorageAsyncConnection} conn
 | 
						|
   *        The Sqlite connection.
 | 
						|
   */
 | 
						|
  async execute(conn) {
 | 
						|
    // A search might be canceled before it starts.
 | 
						|
    if (!this.pending) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Used by stop() to interrupt an eventual running statement.
 | 
						|
    this.interrupt = () => {
 | 
						|
      // Interrupt any ongoing statement to run the search sooner.
 | 
						|
      if (!lazy.UrlbarProvidersManager.interruptLevel) {
 | 
						|
        conn.interrupt();
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    // For any given search, we run these queries:
 | 
						|
    // 1) open pages not supported by history (this._switchToTabQuery)
 | 
						|
    // 2) query based on match behavior
 | 
						|
 | 
						|
    // If the query is simply "@" and we have tokenAliasEngines then return
 | 
						|
    // early. UrlbarProviderTokenAliasEngines will add engine results.
 | 
						|
    let tokenAliasEngines = await lazy.UrlbarSearchUtils.tokenAliasEngines();
 | 
						|
    if (this._trimmedOriginalSearchString == "@" && tokenAliasEngines.length) {
 | 
						|
      this._provider.finishSearch(true);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Check if the first token is an action. If it is, we should set a flag
 | 
						|
    // so we don't include it in our searches.
 | 
						|
    this._firstTokenIsKeyword =
 | 
						|
      this._firstTokenIsKeyword || (await this._checkIfFirstTokenIsKeyword());
 | 
						|
    if (!this.pending) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this._trimmedOriginalSearchString) {
 | 
						|
      // If the user typed the search restriction char or we're in
 | 
						|
      // search-restriction mode, then we're done.
 | 
						|
      // UrlbarProviderSearchSuggestions will handle suggestions, if any.
 | 
						|
      let emptySearchRestriction =
 | 
						|
        this._trimmedOriginalSearchString.length <= 3 &&
 | 
						|
        this._leadingRestrictionToken == lazy.UrlbarTokenizer.RESTRICT.SEARCH &&
 | 
						|
        /\s*\S?$/.test(this._trimmedOriginalSearchString);
 | 
						|
      if (
 | 
						|
        emptySearchRestriction ||
 | 
						|
        (tokenAliasEngines.length &&
 | 
						|
          this._trimmedOriginalSearchString.startsWith("@")) ||
 | 
						|
        (this.hasBehavior("search") && this.hasBehavior("restrict"))
 | 
						|
      ) {
 | 
						|
        this._provider.finishSearch(true);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Run our standard Places query.
 | 
						|
    let queries = [];
 | 
						|
    // "openpage" behavior is supported by the default query.
 | 
						|
    // _switchToTabQuery instead returns only pages not supported by history.
 | 
						|
    if (this.hasBehavior("openpage")) {
 | 
						|
      queries.push(this._switchToTabQuery);
 | 
						|
    }
 | 
						|
    queries.push(this._searchQuery);
 | 
						|
    for (let [query, params] of queries) {
 | 
						|
      await conn.executeCached(query, params, this._onResultRow.bind(this));
 | 
						|
      if (!this.pending) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // If we do not have enough matches search again with MATCH_ANYWHERE, to
 | 
						|
    // get more matches.
 | 
						|
    let count = this._counts[MATCH_TYPE.GENERAL];
 | 
						|
    if (count < this._maxResults) {
 | 
						|
      this._matchBehavior = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
 | 
						|
      queries = [this._searchQuery];
 | 
						|
      if (this.hasBehavior("openpage")) {
 | 
						|
        queries.unshift(this._switchToTabQuery);
 | 
						|
      }
 | 
						|
      for (let [query, params] of queries) {
 | 
						|
        await conn.executeCached(query, params, this._onResultRow.bind(this));
 | 
						|
        if (!this.pending) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  async _checkIfFirstTokenIsKeyword() {
 | 
						|
    if (!this._heuristicToken) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    let aliasEngine = await lazy.UrlbarSearchUtils.engineForAlias(
 | 
						|
      this._heuristicToken,
 | 
						|
      this._originalSearchString
 | 
						|
    );
 | 
						|
 | 
						|
    if (aliasEngine) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    let { entry } = await lazy.KeywordUtils.getBindableKeyword(
 | 
						|
      this._heuristicToken,
 | 
						|
      this._originalSearchString
 | 
						|
    );
 | 
						|
    if (entry) {
 | 
						|
      this._filterOnHost = entry.url.host;
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  _onResultRow(row, cancel) {
 | 
						|
    this._addFilteredQueryMatch(row);
 | 
						|
 | 
						|
    // If the search has been canceled by the user or by _addMatch, or we
 | 
						|
    // fetched enough results, we can stop the underlying Sqlite query.
 | 
						|
    let count = this._counts[MATCH_TYPE.GENERAL];
 | 
						|
    if (!this.pending || count >= this._maxResults) {
 | 
						|
      cancel();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Maybe restyle a SERP in history as a search-type result. To do this,
 | 
						|
   * we extract the search term from the SERP in history then generate a search
 | 
						|
   * URL with that search term. We restyle the SERP in history if its query
 | 
						|
   * parameters are a subset of those of the generated SERP. We check for a
 | 
						|
   * subset instead of exact equivalence since the generated URL may contain
 | 
						|
   * attribution parameters while a SERP in history from an organic search would
 | 
						|
   * not. We don't allow extra params in the history URL since they might
 | 
						|
   * indicate the search is not a first-page web SERP (as opposed to a image or
 | 
						|
   * other non-web SERP).
 | 
						|
   *
 | 
						|
   * Note: We will mistakenly dedupe SERPs for engines that have the same
 | 
						|
   *   hostname as another engine. One example is if the user installed a
 | 
						|
   *   Google Image Search engine. That engine's search URLs might only be
 | 
						|
   *   distinguished by query params from search URLs from the default Google
 | 
						|
   *   engine.
 | 
						|
   *
 | 
						|
   * @param {object} match
 | 
						|
   *   The match to maybe restyle.
 | 
						|
   * @returns {boolean} True if the match can be restyled, false otherwise.
 | 
						|
   */
 | 
						|
  _maybeRestyleSearchMatch(match) {
 | 
						|
    // Return if the URL does not represent a search result.
 | 
						|
    let historyUrl = match.value;
 | 
						|
    let parseResult = Services.search.parseSubmissionURL(historyUrl);
 | 
						|
    if (!parseResult?.engine) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Here we check that the user typed all or part of the search string in the
 | 
						|
    // search history result.
 | 
						|
    let terms = parseResult.terms.toLowerCase();
 | 
						|
    if (
 | 
						|
      this._searchTokens.length &&
 | 
						|
      this._searchTokens.every(token => !terms.includes(token.value))
 | 
						|
    ) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // The URL for the search suggestion formed by the user's typed query.
 | 
						|
    let [generatedSuggestionUrl] = UrlbarUtils.getSearchQueryUrl(
 | 
						|
      parseResult.engine,
 | 
						|
      this._searchTokens.map(t => t.value).join(" ")
 | 
						|
    );
 | 
						|
 | 
						|
    // We ignore termsParameterName when checking for a subset because we
 | 
						|
    // already checked that the typed query is a subset of the search history
 | 
						|
    // query above with this._searchTokens.every(...).
 | 
						|
    if (
 | 
						|
      !lazy.UrlbarSearchUtils.serpsAreEquivalent(
 | 
						|
        historyUrl,
 | 
						|
        generatedSuggestionUrl,
 | 
						|
        [parseResult.termsParameterName]
 | 
						|
      )
 | 
						|
    ) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Turn the match into a searchengine action with a favicon.
 | 
						|
    match.value = makeActionUrl("searchengine", {
 | 
						|
      engineName: parseResult.engine.name,
 | 
						|
      input: parseResult.terms,
 | 
						|
      searchSuggestion: parseResult.terms,
 | 
						|
      searchQuery: parseResult.terms,
 | 
						|
      isSearchHistory: true,
 | 
						|
    });
 | 
						|
    match.comment = parseResult.engine.name;
 | 
						|
    match.icon = match.icon || match.iconUrl;
 | 
						|
    match.style = "action searchengine favicon suggestion";
 | 
						|
    return true;
 | 
						|
  },
 | 
						|
 | 
						|
  _addMatch(match) {
 | 
						|
    if (typeof match.frecency != "number") {
 | 
						|
      throw new Error("Frecency not provided");
 | 
						|
    }
 | 
						|
 | 
						|
    if (typeof match.type != "string") {
 | 
						|
      match.type = MATCH_TYPE.GENERAL;
 | 
						|
    }
 | 
						|
 | 
						|
    // A search could be canceled between a query start and its completion,
 | 
						|
    // in such a case ensure we won't notify any result for it.
 | 
						|
    if (!this.pending) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    match.style = match.style || "favicon";
 | 
						|
 | 
						|
    // Restyle past searches, unless they are bookmarks or special results.
 | 
						|
    if (
 | 
						|
      match.style == "favicon" &&
 | 
						|
      (lazy.UrlbarPrefs.get("restyleSearches") || this._searchModeEngine)
 | 
						|
    ) {
 | 
						|
      let restyled = this._maybeRestyleSearchMatch(match);
 | 
						|
      if (
 | 
						|
        restyled &&
 | 
						|
        lazy.UrlbarPrefs.get("maxHistoricalSearchSuggestions") == 0
 | 
						|
      ) {
 | 
						|
        // The user doesn't want search history.
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    match.icon = match.icon || "";
 | 
						|
    match.finalCompleteValue = match.finalCompleteValue || "";
 | 
						|
 | 
						|
    let { index, replace } = this._getInsertIndexForMatch(match);
 | 
						|
    if (index == -1) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (replace) {
 | 
						|
      // Replacing an existing match from the previous search.
 | 
						|
      this._matches.splice(index, 1);
 | 
						|
    }
 | 
						|
    this._matches.splice(index, 0, match);
 | 
						|
    this._counts[match.type]++;
 | 
						|
 | 
						|
    this.notifyResult(true);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * @typedef {object} MatchPositionInformation
 | 
						|
   * @property {number} index
 | 
						|
   *   The index the match should take in the results. Return -1 if the match
 | 
						|
   *   should be discarded.
 | 
						|
   * @property {boolean} replace
 | 
						|
   *   True if the match should replace the result already at
 | 
						|
   *   matchPosition.index.
 | 
						|
   */
 | 
						|
 | 
						|
  /**
 | 
						|
   * Check for duplicates and either discard the duplicate or replace the
 | 
						|
   * original match, in case the new one is more specific. For example,
 | 
						|
   * a Remote Tab wins over History, and a Switch to Tab wins over a Remote Tab.
 | 
						|
   * We must check both id and url for duplication, because keywords may change
 | 
						|
   * the url by replacing the %s placeholder.
 | 
						|
   *
 | 
						|
   * @param {object} match
 | 
						|
   *   The match to insert.
 | 
						|
   * @returns {MatchPositionInformation}
 | 
						|
   */
 | 
						|
  _getInsertIndexForMatch(match) {
 | 
						|
    let [urlMapKey, prefix, action] = makeKeyForMatch(match);
 | 
						|
    if (
 | 
						|
      (match.placeId &&
 | 
						|
        this._usedPlaceIds.has(makeMapKeyForResult(match.placeId, match))) ||
 | 
						|
      this._usedURLs.some(e => lazy.ObjectUtils.deepEqual(e.key, urlMapKey))
 | 
						|
    ) {
 | 
						|
      let isDupe = true;
 | 
						|
      if (action && ["switchtab", "remotetab"].includes(action.type)) {
 | 
						|
        // The new entry is a switch/remote tab entry, look for the duplicate
 | 
						|
        // among current matches.
 | 
						|
        for (let i = 0; i < this._usedURLs.length; ++i) {
 | 
						|
          let { key: matchKey, action: matchAction } = this._usedURLs[i];
 | 
						|
          if (lazy.ObjectUtils.deepEqual(matchKey, urlMapKey)) {
 | 
						|
            isDupe = true;
 | 
						|
            if (!matchAction || action.type == "switchtab") {
 | 
						|
              this._usedURLs[i] = {
 | 
						|
                key: urlMapKey,
 | 
						|
                action,
 | 
						|
                type: match.type,
 | 
						|
                prefix,
 | 
						|
                comment: match.comment,
 | 
						|
              };
 | 
						|
              return { index: i, replace: true };
 | 
						|
            }
 | 
						|
            break; // Found the duplicate, no reason to continue.
 | 
						|
          }
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        // Dedupe with this flow:
 | 
						|
        // 1. If the two URLs are the same, dedupe the newer one.
 | 
						|
        // 2. If they both contain www. or both do not contain it, prefer https.
 | 
						|
        // 3. If they differ by www., send both results to the Muxer and allow
 | 
						|
        //    it to decide based on results from other providers.
 | 
						|
        let prefixRank = UrlbarUtils.getPrefixRank(prefix);
 | 
						|
        for (let i = 0; i < this._usedURLs.length; ++i) {
 | 
						|
          if (!this._usedURLs[i]) {
 | 
						|
            // This is true when the result at [i] is a searchengine result.
 | 
						|
            continue;
 | 
						|
          }
 | 
						|
 | 
						|
          let { key: existingKey, prefix: existingPrefix } = this._usedURLs[i];
 | 
						|
 | 
						|
          let existingPrefixRank = UrlbarUtils.getPrefixRank(existingPrefix);
 | 
						|
          if (lazy.ObjectUtils.deepEqual(existingKey, urlMapKey)) {
 | 
						|
            isDupe = true;
 | 
						|
 | 
						|
            if (prefix == existingPrefix) {
 | 
						|
              // The URLs are identical. Throw out the new result.
 | 
						|
              break;
 | 
						|
            }
 | 
						|
 | 
						|
            if (prefix.endsWith("www.") == existingPrefix.endsWith("www.")) {
 | 
						|
              // The results differ only by protocol.
 | 
						|
              if (prefixRank <= existingPrefixRank) {
 | 
						|
                break; // Replace match.
 | 
						|
              } else {
 | 
						|
                this._usedURLs[i] = {
 | 
						|
                  key: urlMapKey,
 | 
						|
                  action,
 | 
						|
                  type: match.type,
 | 
						|
                  prefix,
 | 
						|
                  comment: match.comment,
 | 
						|
                };
 | 
						|
                return { index: i, replace: true };
 | 
						|
              }
 | 
						|
            } else {
 | 
						|
              // We have two identical URLs that differ only by www. We need to
 | 
						|
              // be sure what the heuristic result is before deciding how we
 | 
						|
              // should dedupe. We mark these as non-duplicates and let the
 | 
						|
              // muxer handle it.
 | 
						|
              isDupe = false;
 | 
						|
              continue;
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      // Discard the duplicate.
 | 
						|
      if (isDupe) {
 | 
						|
        return { index: -1, replace: false };
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Add this to our internal tracker to ensure duplicates do not end up in
 | 
						|
    // the result.
 | 
						|
    // Not all entries have a place id, thus we fallback to the url for them.
 | 
						|
    // We cannot use only the url since keywords entries are modified to
 | 
						|
    // include the search string, and would be returned multiple times.  Ids
 | 
						|
    // are faster too.
 | 
						|
    if (match.placeId) {
 | 
						|
      this._usedPlaceIds.add(makeMapKeyForResult(match.placeId, match));
 | 
						|
    }
 | 
						|
 | 
						|
    let index = 0;
 | 
						|
    if (!this._groups) {
 | 
						|
      this._groups = [];
 | 
						|
      this._makeGroups(lazy.UrlbarPrefs.resultGroups, this._maxResults);
 | 
						|
    }
 | 
						|
 | 
						|
    let replace = 0;
 | 
						|
    for (let group of this._groups) {
 | 
						|
      // Move to the next group if the match type is incompatible, or if there
 | 
						|
      // is no available space or if the frecency is below the threshold.
 | 
						|
      if (match.type != group.type || !group.available) {
 | 
						|
        index += group.count;
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      index += group.insertIndex;
 | 
						|
      group.available--;
 | 
						|
      if (group.insertIndex < group.count) {
 | 
						|
        replace = true;
 | 
						|
      } else {
 | 
						|
        group.count++;
 | 
						|
      }
 | 
						|
      group.insertIndex++;
 | 
						|
      break;
 | 
						|
    }
 | 
						|
    this._usedURLs[index] = {
 | 
						|
      key: urlMapKey,
 | 
						|
      action,
 | 
						|
      type: match.type,
 | 
						|
      prefix,
 | 
						|
      comment: match.comment || "",
 | 
						|
    };
 | 
						|
    return { index, replace };
 | 
						|
  },
 | 
						|
 | 
						|
  _makeGroups(resultGroup, maxResultCount) {
 | 
						|
    if (!resultGroup.children) {
 | 
						|
      let type;
 | 
						|
      switch (resultGroup.group) {
 | 
						|
        case UrlbarUtils.RESULT_GROUP.FORM_HISTORY:
 | 
						|
        case UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION:
 | 
						|
        case UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION:
 | 
						|
          type = MATCH_TYPE.SUGGESTION;
 | 
						|
          break;
 | 
						|
        case UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL:
 | 
						|
        case UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION:
 | 
						|
        case UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK:
 | 
						|
        case UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX:
 | 
						|
        case UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP:
 | 
						|
        case UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST:
 | 
						|
        case UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE:
 | 
						|
          type = MATCH_TYPE.HEURISTIC;
 | 
						|
          break;
 | 
						|
        case UrlbarUtils.RESULT_GROUP.OMNIBOX:
 | 
						|
          type = MATCH_TYPE.EXTENSION;
 | 
						|
          break;
 | 
						|
        default:
 | 
						|
          type = MATCH_TYPE.GENERAL;
 | 
						|
          break;
 | 
						|
      }
 | 
						|
      if (this._groups.length) {
 | 
						|
        let last = this._groups[this._groups.length - 1];
 | 
						|
        if (last.type == type) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
      }
 | 
						|
      // - `available` is the number of available slots in the group
 | 
						|
      // - `insertIndex` is the index of the first available slot in the group
 | 
						|
      // - `count` is the number of matches in the group, note that it also
 | 
						|
      //   accounts for matches from the previous search, while `available` and
 | 
						|
      //   `insertIndex` don't.
 | 
						|
      this._groups.push({
 | 
						|
        type,
 | 
						|
        available: maxResultCount,
 | 
						|
        insertIndex: 0,
 | 
						|
        count: 0,
 | 
						|
      });
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let initialMaxResultCount;
 | 
						|
    if (typeof resultGroup.maxResultCount == "number") {
 | 
						|
      initialMaxResultCount = resultGroup.maxResultCount;
 | 
						|
    } else if (typeof resultGroup.availableSpan == "number") {
 | 
						|
      initialMaxResultCount = resultGroup.availableSpan;
 | 
						|
    } else {
 | 
						|
      initialMaxResultCount = this._maxResults;
 | 
						|
    }
 | 
						|
    let childMaxResultCount = Math.min(initialMaxResultCount, maxResultCount);
 | 
						|
    for (let child of resultGroup.children) {
 | 
						|
      this._makeGroups(child, childMaxResultCount);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _addFilteredQueryMatch(row) {
 | 
						|
    let placeId = row.getResultByName("id");
 | 
						|
    let url = row.getResultByName("url");
 | 
						|
    let openPageCount = row.getResultByName("open_count") || 0;
 | 
						|
    let historyTitle = row.getResultByName("title") || "";
 | 
						|
    let bookmarked = row.getResultByName("bookmarked");
 | 
						|
    let bookmarkTitle = bookmarked ? row.getResultByName("btitle") : null;
 | 
						|
    let tags = row.getResultByName("tags") || "";
 | 
						|
    let frecency = row.getResultByName("frecency");
 | 
						|
    let userContextId = row.getResultByName("userContextId");
 | 
						|
    let lastVisitPRTime = row.getResultByName("last_visit_date");
 | 
						|
    let lastVisit = lastVisitPRTime
 | 
						|
      ? lazy.PlacesUtils.toDate(lastVisitPRTime).getTime()
 | 
						|
      : undefined;
 | 
						|
    let tabGroup = row.getResultByName("groupId");
 | 
						|
 | 
						|
    let match = {
 | 
						|
      placeId,
 | 
						|
      value: url,
 | 
						|
      comment: bookmarkTitle || historyTitle,
 | 
						|
      icon: UrlbarUtils.getIconForUrl(url),
 | 
						|
      frecency: frecency || FRECENCY_DEFAULT,
 | 
						|
      userContextId,
 | 
						|
      lastVisit,
 | 
						|
      tabGroup,
 | 
						|
    };
 | 
						|
    if (openPageCount > 0 && this.hasBehavior("openpage")) {
 | 
						|
      if (
 | 
						|
        this._currentPage == match.value &&
 | 
						|
        (!lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") ||
 | 
						|
          this._userContextId == match.userContextId)
 | 
						|
      ) {
 | 
						|
        // Don't suggest switching to the current tab.
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // Actions are enabled and the page is open.  Add a switch-to-tab result.
 | 
						|
      match.value = makeActionUrl("switchtab", { url: match.value });
 | 
						|
      match.style = "action switchtab";
 | 
						|
    } else if (
 | 
						|
      this.hasBehavior("history") &&
 | 
						|
      !this.hasBehavior("bookmark") &&
 | 
						|
      !tags
 | 
						|
    ) {
 | 
						|
      // The consumer wants only history and not bookmarks and there are no
 | 
						|
      // tags.  We'll act as if the page is not bookmarked.
 | 
						|
      match.style = "favicon";
 | 
						|
    } else if (tags) {
 | 
						|
      // Store the tags in the title.  It's up to the consumer to extract them.
 | 
						|
      match.comment += UrlbarUtils.TITLE_TAGS_SEPARATOR + tags;
 | 
						|
      // If we're not suggesting bookmarks, then this shouldn't display as one.
 | 
						|
      match.style = this.hasBehavior("bookmark") ? "bookmark-tag" : "tag";
 | 
						|
    } else if (bookmarked) {
 | 
						|
      match.style = "bookmark";
 | 
						|
    }
 | 
						|
 | 
						|
    this._addMatch(match);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * @returns {string}
 | 
						|
   * A string consisting of the search query to be used based on the previously
 | 
						|
   * set urlbar suggestion preferences.
 | 
						|
   */
 | 
						|
  get _suggestionPrefQuery() {
 | 
						|
    let conditions = [];
 | 
						|
    if (this._filterOnHost) {
 | 
						|
      conditions.push("h.rev_host = get_unreversed_host(:host || '.') || '.'");
 | 
						|
      // When filtering on a host we are in some sort of site specific search,
 | 
						|
      // thus we want a cleaner set of results, compared to a general search.
 | 
						|
      // This means removing less interesting urls, like redirects or
 | 
						|
      // non-bookmarked title-less pages.
 | 
						|
 | 
						|
      if (lazy.UrlbarPrefs.get("restyleSearches") || this._searchModeEngine) {
 | 
						|
        // If restyle is enabled, we want to filter out redirect targets,
 | 
						|
        // because sources are urls built using search engines definitions that
 | 
						|
        // we can reverse-parse.
 | 
						|
        // In this case we can't filter on title-less pages because redirect
 | 
						|
        // sources likely don't have a title and recognizing sources is costly.
 | 
						|
        // Bug 468710 may help with this.
 | 
						|
        conditions.push(`NOT EXISTS (
 | 
						|
          WITH visits(type) AS (
 | 
						|
            SELECT visit_type
 | 
						|
            FROM moz_historyvisits
 | 
						|
            WHERE place_id = h.id
 | 
						|
            ORDER BY visit_date DESC
 | 
						|
            LIMIT 10 /* limit to the last 10 visits */
 | 
						|
          )
 | 
						|
          SELECT 1 FROM visits
 | 
						|
          WHERE type IN (5,6)
 | 
						|
        )`);
 | 
						|
      } else {
 | 
						|
        // If instead restyle is disabled, we want to keep redirect targets,
 | 
						|
        // because sources are often unreadable title-less urls.
 | 
						|
        conditions.push(`NOT EXISTS (
 | 
						|
          WITH visits(id) AS (
 | 
						|
            SELECT id
 | 
						|
            FROM moz_historyvisits
 | 
						|
            WHERE place_id = h.id
 | 
						|
            ORDER BY visit_date DESC
 | 
						|
            LIMIT 10 /* limit to the last 10 visits */
 | 
						|
            )
 | 
						|
           SELECT 1
 | 
						|
           FROM visits src
 | 
						|
           JOIN moz_historyvisits dest ON src.id = dest.from_visit
 | 
						|
           WHERE dest.visit_type IN (5,6)
 | 
						|
        )`);
 | 
						|
        // Filter out empty-titled pages, they could be redirect sources that
 | 
						|
        // we can't recognize anymore because their target was wrongly expired
 | 
						|
        // due to Bug 1664252.
 | 
						|
        conditions.push("(h.foreign_count > 0 OR h.title NOTNULL)");
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
      this.hasBehavior("restrict") ||
 | 
						|
      (!this.hasBehavior("openpage") &&
 | 
						|
        (!this.hasBehavior("history") || !this.hasBehavior("bookmark")))
 | 
						|
    ) {
 | 
						|
      if (this.hasBehavior("history")) {
 | 
						|
        // Enforce ignoring the visit_count index, since the frecency one is much
 | 
						|
        // faster in this case.  ANALYZE helps the query planner to figure out the
 | 
						|
        // faster path, but it may not have up-to-date information yet.
 | 
						|
        conditions.push("+h.visit_count > 0");
 | 
						|
      }
 | 
						|
      if (this.hasBehavior("bookmark")) {
 | 
						|
        conditions.push("bookmarked");
 | 
						|
      }
 | 
						|
      if (this.hasBehavior("tag")) {
 | 
						|
        conditions.push("tags NOTNULL");
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return defaultQuery(conditions.join(" AND "));
 | 
						|
  },
 | 
						|
 | 
						|
  get _emptySearchDefaultBehavior() {
 | 
						|
    // Further restrictions to apply for "empty searches" (searching for
 | 
						|
    // "").  The empty behavior is typed history, if history is enabled.
 | 
						|
    // Otherwise, it is bookmarks, if they are enabled. If both history and
 | 
						|
    // bookmarks are disabled, it defaults to open pages.
 | 
						|
    let val = Ci.mozIPlacesAutoComplete.BEHAVIOR_RESTRICT;
 | 
						|
    if (lazy.UrlbarPrefs.get("suggest.history")) {
 | 
						|
      val |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY;
 | 
						|
    } else if (lazy.UrlbarPrefs.get("suggest.bookmark")) {
 | 
						|
      val |= Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
 | 
						|
    } else {
 | 
						|
      val |= Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE;
 | 
						|
    }
 | 
						|
    return val;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * If the user-provided string starts with a keyword that gave a heuristic
 | 
						|
   * result, this will strip it.
 | 
						|
   *
 | 
						|
   * @returns {string} The filtered search string.
 | 
						|
   */
 | 
						|
  get _keywordFilteredSearchString() {
 | 
						|
    let tokens = this._searchTokens.map(t => t.value);
 | 
						|
    if (this._firstTokenIsKeyword) {
 | 
						|
      tokens = tokens.slice(1);
 | 
						|
    }
 | 
						|
    return tokens.join(" ");
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Obtains the search query to be used based on the previously set search
 | 
						|
   * preferences (accessed by this.hasBehavior).
 | 
						|
   *
 | 
						|
   * @returns {Array}
 | 
						|
   *   An array consisting of the correctly optimized query to search the
 | 
						|
   *   database with and an object containing the params to bound.
 | 
						|
   */
 | 
						|
  get _searchQuery() {
 | 
						|
    let params = {
 | 
						|
      parent: lazy.PlacesUtils.tagsFolderId,
 | 
						|
      matchBehavior: this._matchBehavior,
 | 
						|
      searchBehavior: this._behavior,
 | 
						|
      // We only want to search the tokens that we are left with - not the
 | 
						|
      // original search string.
 | 
						|
      searchString: this._keywordFilteredSearchString,
 | 
						|
      // Limit the query to the the maximum number of desired results.
 | 
						|
      // This way we can avoid doing more work than needed.
 | 
						|
      maxResults: this._maxResults,
 | 
						|
      switchTabsEnabled: this.hasBehavior("openpage"),
 | 
						|
    };
 | 
						|
    params.userContextId = lazy.UrlbarPrefs.get(
 | 
						|
      "switchTabs.searchAllContainers"
 | 
						|
    )
 | 
						|
      ? lazy.UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
 | 
						|
          null,
 | 
						|
          this._inPrivateWindow
 | 
						|
        )
 | 
						|
      : this._userContextId;
 | 
						|
 | 
						|
    if (this._filterOnHost) {
 | 
						|
      params.host = this._filterOnHost;
 | 
						|
    }
 | 
						|
    return [this._suggestionPrefQuery, params];
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Obtains the query to search for switch-to-tab entries.
 | 
						|
   *
 | 
						|
   * @returns {Array}
 | 
						|
   *   An array consisting of the correctly optimized query to search the
 | 
						|
   *   database with and an object containing the params to bound.
 | 
						|
   */
 | 
						|
  get _switchToTabQuery() {
 | 
						|
    return [
 | 
						|
      SQL_SWITCHTAB_QUERY,
 | 
						|
      {
 | 
						|
        matchBehavior: this._matchBehavior,
 | 
						|
        searchBehavior: this._behavior,
 | 
						|
        // We only want to search the tokens that we are left with - not the
 | 
						|
        // original search string.
 | 
						|
        searchString: this._keywordFilteredSearchString,
 | 
						|
        userContextId: lazy.UrlbarPrefs.get("switchTabs.searchAllContainers")
 | 
						|
          ? lazy.UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
 | 
						|
              null,
 | 
						|
              this._inPrivateWindow
 | 
						|
            )
 | 
						|
          : this._userContextId,
 | 
						|
        maxResults: this._maxResults,
 | 
						|
      },
 | 
						|
    ];
 | 
						|
  },
 | 
						|
 | 
						|
  // The result is notified to the search listener on a timer, to chunk multiple
 | 
						|
  // match updates together and avoid rebuilding the popup at every new match.
 | 
						|
  _notifyTimer: null,
 | 
						|
 | 
						|
  /**
 | 
						|
   * Notifies the current result to the listener.
 | 
						|
   *
 | 
						|
   * @param searchOngoing
 | 
						|
   *        Indicates whether the search result should be marked as ongoing.
 | 
						|
   */
 | 
						|
  _notifyDelaysCount: 0,
 | 
						|
  notifyResult(searchOngoing) {
 | 
						|
    let notify = () => {
 | 
						|
      if (!this.pending) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      this._notifyDelaysCount = 0;
 | 
						|
      this._listener(this._matches, searchOngoing);
 | 
						|
      if (!searchOngoing) {
 | 
						|
        // Break possible cycles.
 | 
						|
        this._listener = null;
 | 
						|
        this._provider = null;
 | 
						|
        this.stop();
 | 
						|
      }
 | 
						|
    };
 | 
						|
    if (this._notifyTimer) {
 | 
						|
      this._notifyTimer.cancel();
 | 
						|
    }
 | 
						|
    // In the worst case, we may get evenly spaced matches that would end up
 | 
						|
    // delaying the UI by N_MATCHES * NOTIFYRESULT_DELAY_MS. Thus, we clamp the
 | 
						|
    // number of times we may delay matches.
 | 
						|
    if (this._notifyDelaysCount > 3) {
 | 
						|
      notify();
 | 
						|
    } else {
 | 
						|
      this._notifyDelaysCount++;
 | 
						|
      this._notifyTimer = setTimeout(notify, NOTIFYRESULT_DELAY_MS);
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Class used to create the provider.
 | 
						|
 */
 | 
						|
class ProviderPlaces extends UrlbarProvider {
 | 
						|
  // Promise resolved when the database initialization has completed, or null
 | 
						|
  // if it has never been requested.
 | 
						|
  _promiseDatabase = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns the name of this provider.
 | 
						|
   *
 | 
						|
   * @returns {string} the name of this provider.
 | 
						|
   */
 | 
						|
  get name() {
 | 
						|
    return "Places";
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
 | 
						|
   */
 | 
						|
  get type() {
 | 
						|
    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Gets a Sqlite database handle.
 | 
						|
   *
 | 
						|
   * @returns {Promise<OpenedConnection>}
 | 
						|
   *   A connection to the Sqlite database handle (according to {@link Sqlite.sys.mjs}).
 | 
						|
   * @throws A javascript exception
 | 
						|
   */
 | 
						|
  getDatabaseHandle() {
 | 
						|
    if (!this._promiseDatabase) {
 | 
						|
      this._promiseDatabase = (async () => {
 | 
						|
        let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
 | 
						|
 | 
						|
        // We don't catch exceptions here as it is too late to block shutdown.
 | 
						|
        lazy.Sqlite.shutdown.addBlocker("UrlbarProviderPlaces closing", () => {
 | 
						|
          // Break a possible cycle through the
 | 
						|
          // previous result, the controller and
 | 
						|
          // ourselves.
 | 
						|
          this._currentSearch = null;
 | 
						|
        });
 | 
						|
 | 
						|
        return conn;
 | 
						|
      })().catch(ex => {
 | 
						|
        dump("Couldn't get database handle: " + ex + "\n");
 | 
						|
        this.logger.error(ex);
 | 
						|
      });
 | 
						|
    }
 | 
						|
    return this._promiseDatabase;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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
 | 
						|
   */
 | 
						|
  async isActive(queryContext) {
 | 
						|
    if (
 | 
						|
      !queryContext.trimmedSearchString &&
 | 
						|
      queryContext.searchMode?.engineName
 | 
						|
    ) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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.
 | 
						|
   */
 | 
						|
  startQuery(queryContext, addCallback) {
 | 
						|
    let instance = this.queryInstance;
 | 
						|
    let urls = new Set();
 | 
						|
    this._startLegacyQuery(queryContext, matches => {
 | 
						|
      if (instance != this.queryInstance) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      let results = convertLegacyMatches(queryContext, matches, urls);
 | 
						|
      for (let result of results) {
 | 
						|
        addCallback(this, result);
 | 
						|
      }
 | 
						|
    });
 | 
						|
    return this._deferred.promise;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Cancels a running query.
 | 
						|
   */
 | 
						|
  cancelQuery() {
 | 
						|
    if (this._currentSearch) {
 | 
						|
      this._currentSearch.stop();
 | 
						|
    }
 | 
						|
    if (this._deferred) {
 | 
						|
      this._deferred.resolve();
 | 
						|
    }
 | 
						|
    // Don't notify since we are canceling this search.  This also means we
 | 
						|
    // won't fire onSearchComplete for this search.
 | 
						|
    this.finishSearch();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Properly cleans up when searching is completed.
 | 
						|
   *
 | 
						|
   * @param {boolean} [notify]
 | 
						|
   *        Indicates if we should notify the AutoComplete listener about our
 | 
						|
   *        results or not. Default false.
 | 
						|
   */
 | 
						|
  finishSearch(notify = false) {
 | 
						|
    // Clear state now to avoid race conditions, see below.
 | 
						|
    let search = this._currentSearch;
 | 
						|
    if (!search) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this._lastLowResultsSearchSuggestion =
 | 
						|
      search._lastLowResultsSearchSuggestion;
 | 
						|
 | 
						|
    if (!notify || !search.pending) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // There is a possible race condition here.
 | 
						|
    // When a search completes it calls finishSearch that notifies results
 | 
						|
    // here.  When the controller gets the last result it fires
 | 
						|
    // onSearchComplete.
 | 
						|
    // If onSearchComplete immediately starts a new search it will set a new
 | 
						|
    // _currentSearch, and on return the execution will continue here, after
 | 
						|
    // notifyResult.
 | 
						|
    // Thus, ensure that notifyResult is the last call in this method,
 | 
						|
    // otherwise you might be touching the wrong search.
 | 
						|
    search.notifyResult(false);
 | 
						|
  }
 | 
						|
 | 
						|
  onEngagement(queryContext, controller, details) {
 | 
						|
    let { result } = details;
 | 
						|
    if (details.selType == "dismiss") {
 | 
						|
      switch (result.type) {
 | 
						|
        case UrlbarUtils.RESULT_TYPE.SEARCH:
 | 
						|
          // URL restyled as a search suggestion. Generate the URL and remove it
 | 
						|
          // from browsing history.
 | 
						|
          let { url } = UrlbarUtils.getUrlFromResult(result);
 | 
						|
          lazy.PlacesUtils.history.remove(url).catch(console.error);
 | 
						|
          controller.removeResult(result);
 | 
						|
          break;
 | 
						|
        case UrlbarUtils.RESULT_TYPE.URL:
 | 
						|
          // Remove browsing history entries from Places.
 | 
						|
          lazy.PlacesUtils.history
 | 
						|
            .remove(result.payload.url)
 | 
						|
            .catch(console.error);
 | 
						|
          controller.removeResult(result);
 | 
						|
          break;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  _startLegacyQuery(queryContext, callback) {
 | 
						|
    let deferred = Promise.withResolvers();
 | 
						|
    let listener = (matches, searchOngoing) => {
 | 
						|
      callback(matches);
 | 
						|
      if (!searchOngoing) {
 | 
						|
        deferred.resolve();
 | 
						|
      }
 | 
						|
    };
 | 
						|
    this._startSearch(queryContext.searchString, listener, queryContext);
 | 
						|
    this._deferred = deferred;
 | 
						|
  }
 | 
						|
 | 
						|
  _startSearch(searchString, listener, queryContext) {
 | 
						|
    // Stop the search in case the controller has not taken care of it.
 | 
						|
    if (this._currentSearch) {
 | 
						|
      this.cancelQuery();
 | 
						|
    }
 | 
						|
 | 
						|
    let search = (this._currentSearch = new Search(
 | 
						|
      queryContext,
 | 
						|
      listener,
 | 
						|
      this
 | 
						|
    ));
 | 
						|
    this.getDatabaseHandle()
 | 
						|
      .then(conn => search.execute(conn))
 | 
						|
      .catch(ex => {
 | 
						|
        dump(`Query failed: ${ex}\n`);
 | 
						|
        this.logger.error(ex);
 | 
						|
      })
 | 
						|
      .then(() => {
 | 
						|
        if (search == this._currentSearch) {
 | 
						|
          this.finishSearch(true);
 | 
						|
        }
 | 
						|
      });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export var UrlbarProviderPlaces = new ProviderPlaces();
 |